diff --git a/CLAUDE.md b/CLAUDE.md index 3a47c72..abaf250 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,46 +35,63 @@ This project is not (yet) a git repository. ## Architecture Data flows through the pipeline as a list of `MeasurementPlane` (one per imaging -distance `z`), each holding a raw 2D `flux` array plus optionally-known -`pixel_scale`/`viewing_angle_deg`. Everything downstream is keyed off `LGBasis`, which +distance `z`), each holding a raw 2D `flux` array plus a nominal `z` and `z_tolerance`. +Camera pose/intrinsics are a single shared `CameraModel`/`CameraModelTolerance` for the +whole reconstruction, not per-plane. Everything downstream is keyed off `LGBasis`, which defines the mode basis relative to a known waist `w0`/`z0`/`wavelength`. Module responsibilities (`he11lib/`): -- **`data.py`** — `MeasurementPlane`, `ReconstructionResult` (the shared input/output - types) and `validate_planes` (>=3 planes, matching shapes, distinct `z`). +- **`data.py`** — `MeasurementPlane` (flux, nominal `z`, `z_tolerance`), `ReconstructionResult` + (the shared input/output types) and `validate_planes` (>=3 planes, matching shapes, + distinct `z`). - **`modes.py`** — `LGBasis`: closed-form paraxial LG fields, beam radius `w(z)`, Gouy phase, inverse radius of curvature, and projection of a measured field onto a candidate mode set. This is the analytic ground truth all fitting is checked against. -- **`geometry.py`** — `GeometryCalibration`: resolves a plane's pixel-to-physical - coordinate grid, deferring to known `pixel_scale`/`viewing_angle_deg` on the plane - over any override passed in. +- **`geometry.py`** — `CameraModel`/`CameraModelTolerance` (a nominal pinhole camera + pose/intrinsics and its paired per-field refinement bound) and `GeometryCalibration`: + resolves pixel<->physical coordinates via true pinhole forward/inverse projection + (ray-plane intersection), producing genuine keystoning for tilted/off-axis poses + rather than a uniform affine correction. A tolerance of `0` on a `CameraModel` field + means it's trusted exactly; `>0` means it's refined within `[nominal-tolerance, + nominal+tolerance]` by `ModalFitter` — the same mechanism applies to + `MeasurementPlane.z`/`z_tolerance`. - **`noise.py`** — `NoiseEstimator`: automatic per-image noise-std estimation (Laplacian method) and per-pixel weights for noise-weighted least squares. - **`deconvolution.py`** — `DiffusionDeconvolver`: optional forward blur / Wiener deconvolution for thermal-diffusion blur in the absorbing target. The blur kernel is - isotropic in pixel space, so it's only exact when `viewing_angle_deg == 0` (an - oblique view makes x/y pixel scales differ) — an accepted approximation, not a bug. + isotropic in pixel space; callers use `GeometryCalibration.effective_pixel_scale` + (a finite-difference approximation at the frame center) as a single figure, so it's + only exact for an on-axis, untilted camera — an accepted approximation, not a bug. - **`synthetic.py`** — `SyntheticBeamGenerator`: forward model that produces - `MeasurementPlane`s from known ground-truth coefficients/center/pointing/geometry. - Used throughout the test suite and examples to validate the pipeline end-to-end. + `MeasurementPlane`s from a known ground-truth `CameraModel`, coefficients, center, + and two pointing angles, rendering each plane at its own true `z` (which may + deliberately differ from the plane's nominal `z`, via `nominal_z_offsets`). Used + throughout the test suite and examples to validate the pipeline end-to-end. - **`fitting.py`** — `ModalFitter` (`fit`, `fit_auto`) and `generate_mode_shells`: the - core joint nonlinear least-squares fit (complex LG coefficients + beam - center/pointing + unknown geometry) via `scipy.optimize.least_squares`. `fit_auto` - grows the candidate mode set shell-by-shell (by order `2p + |l|`), stopping via a BIC - improvement threshold, capped at `max_order` (emits `UserWarning`, doesn't raise, if - still improving at the cap). + core joint nonlinear least-squares fit via `scipy.optimize.least_squares`. Complex LG + coefficients, per-plane beam center, and the two pointing angles (horizontal/vertical) + are always free; each `CameraModel` field and each plane's `z` is additionally free + (bounded by its tolerance) only when its paired tolerance is nonzero, otherwise held + fixed as a constant. `fit_auto` grows the candidate mode set shell-by-shell (by order + `2p + |l|`), stopping via a BIC improvement threshold, capped at `max_order` (emits + `UserWarning`, doesn't raise, if still improving at the cap, or if the free + camera+`z` parameter count is large relative to the number of planes — see the + degeneracy pitfall below). - **`phase_retrieval.py`** — `propagate_angular_spectrum` (FFT-based paraxial free-space propagation) and `PhaseRetriever` (multi-plane Gerchberg-Saxton), the - fallback reconstruction path for when a finite mode basis doesn't fit well. -- **`reconstruct.py`** — `BeamReconstructor`: the orchestrator. Pipeline order: - validate planes → optional deconvolution (requires known `pixel_scale` per plane) → + fallback reconstruction path for when a finite mode basis doesn't fit well. Takes a + shared `CameraModel` (not per-plane pixel scale) to derive its common physical grid. +- **`reconstruct.py`** — `BeamReconstructor`: the orchestrator, now constructed with a + required `camera`/`camera_tolerance`. Pipeline order: validate planes → optional + deconvolution (using `GeometryCalibration(camera).effective_pixel_scale`) → `ModalFitter.fit_auto` → optional `PhaseRetriever` fallback (forced via `force_phase_retrieval`, or triggered automatically when the noise-weighted RMS residual exceeds `phase_retrieval_residual_threshold`). The fallback path projects the recovered field onto all modes up to `max_order` and produces a - `ReconstructionResult` with `used_phase_retrieval=True`, empty `residuals`, and NaN - `coefficient_uncertainty` (no fit covariance available from phase retrieval). + `ReconstructionResult` with `used_phase_retrieval=True`, empty `residuals`, empty + `geometry`, NaN pointing angles, and NaN `coefficient_uncertainty` (no fit covariance + available from phase retrieval). - **`plotting.py`** — diagnostic figures (`plot_mode_purity`, `plot_center_trace`, `plot_residuals`); each returns a `Figure` rather than calling `plt.show()`. @@ -102,3 +119,13 @@ debugging time in this project's history: 2-mode ground truth). When demonstrating growth with deconvolution or noise, set `max_order` close to the true expected mode content rather than generously high, unless the test specifically targets growth behavior itself. +3. **Shared camera/`z` geometry can be underdetermined with few planes.** With only + 3-10 planes, adding the ~7-9 shared `CameraModel` unknowns (whichever have nonzero + `CameraModelTolerance`) plus one `z` correction per plane (for nonzero + `z_tolerance`) can be practically underdetermined even though each plane + contributes many pixels of data, because those unknowns are *global* and only + weakly constrained by subtle keystone differences between planes. `fit_auto`/ + `BeamReconstructor` emit a `UserWarning` (not an error) when the free-parameter + count is large relative to the number of planes — if you see it, tighten + `CameraModelTolerance`/`z_tolerance` toward values you actually trust rather than + leaving them generously wide.