Spaces:
Running on Zero
Running on Zero
| """Fast offline tests for WP-6 Double-DIP baseline. | |
| - Smoke: tiny iters + 32x32 crop → valid SeparationResult + final loss < initial (teeth-proven). | |
| - Registration: include_deep_prior=True yields method=="deep_prior" candidate that ranks. | |
| - density=None path is skipped (no candidate added). | |
| Full 2000-iter bench is CLI-only and marked slow (not run in CI). | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pytest | |
| from PIL import Image | |
| from app.api_client import SeparationResult | |
| from app.preprocessing import preprocess_negative | |
| from app.scoring import rank_candidates | |
| from film_physics import get_film_curve | |
| # Direct import for core tests (fast path) | |
| from baselines.double_dip import DoubleDIPConfig, double_dip_separate | |
| def _load_fixture_as_pil(idx: int = 0) -> Image.Image: | |
| from pathlib import Path | |
| fix = sorted(Path("synth/fixtures").glob("case_*.npz"))[idx] | |
| d = np.load(fix) | |
| arr = (d["scan"] * 255).clip(0, 255).astype(np.uint8) | |
| return Image.fromarray(arr) | |
| def test_double_dip_smoke(): | |
| """Tiny config returns valid shapes/dtypes/ranges and improves loss (teeth: fails if opt disabled).""" | |
| # Use full small fixture (64x64); DIP downs internally via max_side. Crop was causing densitometry soft-fail on tiny input. | |
| pil = _load_fixture_as_pil(0) | |
| pre = preprocess_negative(pil) | |
| curve = get_film_curve("Generic") | |
| cfg = DoubleDIPConfig(iterations=30, max_side=64, seed=0) | |
| res = double_dip_separate( | |
| pre.rgb, pre.log_exposure, pre.density, pre.confidence_mask, curve, cfg | |
| ) | |
| assert res is not None | |
| assert isinstance(res, SeparationResult) | |
| assert res.method == "deep_prior" | |
| assert res.candidate_id.startswith("dip_i") | |
| assert res.image_a.shape == res.image_b.shape == pre.rgb.shape | |
| assert res.image_a.dtype == res.image_b.dtype == np.float32 | |
| assert 0.0 <= float(res.image_a.min()) <= float(res.image_a.max()) <= 1.0 | |
| assert 0.0 <= float(res.image_b.min()) <= float(res.image_b.max()) <= 1.0 | |
| # Structured diagnostics (not message parsing) carry the loss improvement | |
| assert res.diagnostics is not None | |
| init_l = res.diagnostics["init_loss"] | |
| best_l = res.diagnostics["best_loss"] | |
| assert best_l < init_l, f"DIP did not improve loss: init={init_l} best={best_l}" | |
| def test_double_dip_registration(monkeypatch): | |
| """The REAL integration surface: generate_candidates(include_deep_prior=True) registers a | |
| deep_prior candidate (tiny config monkeypatched in) that rank_candidates then scores. | |
| """ | |
| import baselines.double_dip as dd | |
| from app.api_client import generate_candidates | |
| pil = _load_fixture_as_pil(0) | |
| pre = preprocess_negative(pil) | |
| curve = get_film_curve("Generic") | |
| # Tiny config so the wiring test stays fast; the helper's lazy import picks this up. | |
| monkeypatch.setattr( | |
| dd, "DoubleDIPConfig", lambda **kw: DoubleDIPConfig(iterations=12, max_side=64, seed=7) | |
| ) | |
| cands, mode = generate_candidates( | |
| pre.rgb, | |
| num_candidates=2, | |
| h_total=pre.h_total, | |
| confidence_mask=pre.confidence_mask, | |
| density=pre.density, | |
| log_exposure=pre.log_exposure, | |
| include_deep_prior=True, | |
| film_curve=curve, | |
| ) | |
| assert mode == "demo" | |
| dips = [c for c in cands if c.method == "deep_prior"] | |
| assert dips, "generate_candidates(include_deep_prior=True) did not register a deep_prior candidate" | |
| # The mixed pool participates in normal ranking | |
| ranked = rank_candidates( | |
| candidates=cands, | |
| observed_log_exposure=pre.log_exposure, | |
| observed_rgb=pre.rgb, | |
| film_curve=curve, | |
| physics_weight=1.0, | |
| perceptual_weight=0.0, | |
| density=pre.density, | |
| confidence_mask=pre.confidence_mask, | |
| ) | |
| assert any(r.separation.method == "deep_prior" for r in ranked) | |
| def test_double_dip_skips_without_density(): | |
| """density=None (or conf=None) produces no deep_prior candidate (same rule as demix).""" | |
| pil = _load_fixture_as_pil(0) | |
| pre = preprocess_negative(pil) | |
| # Direct path (generate wiring with the kw args lands in commit 2) | |
| res = double_dip_separate( | |
| pre.rgb, pre.log_exposure, density=None, confidence_mask=pre.confidence_mask, film_curve=get_film_curve("Generic") | |
| ) | |
| assert res is None | |