diff --git a/docs/api.md b/docs/api.md index 8aea1f3..3f54769 100644 --- a/docs/api.md +++ b/docs/api.md @@ -15,11 +15,33 @@ Every class/function below is exported from the top-level `he11lib` package ## Quick start ```python -from he11lib import BeamReconstructor, MeasurementPlane +from he11lib import ( + BeamReconstructor, + CameraModel, + CameraModelTolerance, + MeasurementPlane, +) # planes: a list of >=3 MeasurementPlane objects built from your own # flux arrays (see MeasurementPlane below). -reconstructor = BeamReconstructor(w0=5e-3, z0=0.5, wavelength=1.76e-3) + +# Nominal camera pose/intrinsics from calibration; every field here is +# refined jointly with the mode fit because its tolerance is nonzero. +camera = CameraModel( + focal_length_px=2000.0, + position=(0.0, 0.0, -2.0), + orientation_deg=(0.0, 0.0, 0.0), +) +camera_tolerance = CameraModelTolerance( + focal_length_px=20.0, + position=(0.01, 0.01, 0.05), + orientation_deg=(2.0, 2.0, 2.0), +) + +reconstructor = BeamReconstructor( + w0=5e-3, z0=0.5, wavelength=1.76e-3, + camera=camera, camera_tolerance=camera_tolerance, +) result = reconstructor.reconstruct(planes) for mode, (power_fraction, phase_rad) in result.purity.items(): @@ -28,19 +50,22 @@ for mode, (power_fraction, phase_rad) in result.purity.items(): ## `data` — `MeasurementPlane`, `ReconstructionResult` -### `MeasurementPlane(flux, z, pixel_scale=None, viewing_angle_deg=None, label=None)` +### `MeasurementPlane(flux, z, z_tolerance=0.0, label=None)` One measurement: a 2D flux array plus its acquisition metadata. - `flux` — 2D `np.ndarray` of flux values. Dead-pixel correction, background subtraction, and saturation clipping are assumed already handled upstream. - `z` — nominal distance from the output window, in meters. Must be `> 0`. -- `pixel_scale` — known meters/pixel, or `None` if unknown (then jointly - fit by `ModalFitter`/`BeamReconstructor`). -- `viewing_angle_deg` — known camera viewing angle relative to the beam - axis, in degrees, or `None` if unknown (also jointly fit). +- `z_tolerance` — `+/-` bound, in meters, around the nominal `z` within + which the true distance is jointly refined by `ModalFitter`. Must be + `>= 0`; `0` (the default) means `z` is trusted exactly and held fixed. - `label` — optional human-readable identifier. +Per-plane camera geometry (`pixel_scale`/`viewing_angle_deg`) no longer +lives on `MeasurementPlane` — camera pose/intrinsics are a single shared +`CameraModel` for the whole reconstruction (see `geometry` below). + ### `validate_planes(planes)` Raises `ValueError` if there are fewer than 3 planes, planes have @@ -58,9 +83,14 @@ and `BeamReconstructor.reconstruct`): - `purity: dict[(p, l), (power_fraction, phase_rad)]` - `reconstructed_field: np.ndarray` — reconstructed complex field. - `centers: list[(x, y)]` — fitted beam transverse center per plane, meters. -- `pointing_angle_deg: float` — fitted shared beam pointing angle (tilt). -- `geometry: dict[str, float]` — geometry parameters used or fitted (keys - `pixel_scale_{i}`, `viewing_angle_deg_{i}` per plane index `i`). +- `pointing_angle_horizontal_deg`, `pointing_angle_vertical_deg: float` — + fitted shared beam pointing (tilt) angles, independent horizontal and + vertical. +- `geometry: dict[str, float]` — geometry parameters used or fitted: the 9 + `CameraModel` field names from `he11lib.geometry.CAMERA_FIELD_NAMES` + (`focal_length_px`, `position_x`, `position_y`, `position_z`, `yaw_deg`, + `pitch_deg`, `roll_deg`, `principal_point_x`, `principal_point_y`), plus + `z_{i}` per plane index `i` (that plane's fitted/held distance). - `residuals: list[np.ndarray]` — per-plane (measured − modeled) flux maps. Empty when `used_phase_retrieval` is `True`. - `coefficient_uncertainty: dict[(p, l), float]` — 1-sigma uncertainty on @@ -87,17 +117,55 @@ waist radius `w0` (m), waist location `z0` (m), and radiation `wavelength` onto each `(p, l)` in `modes`, returning `dict[(p, l), complex]` coefficients (Riemann-sum inner product; `dx` is the grid spacing). -## `geometry` — `GeometryCalibration` +## `geometry` — `CameraModel`, `CameraModelTolerance`, `GeometryCalibration` -`GeometryCalibration(plane)` wraps a single `MeasurementPlane` and resolves -its pixel-to-physical-coordinate mapping. +### `CameraModel(focal_length_px, position, orientation_deg, principal_point=(0.0, 0.0))` -- `pixel_scale_known` / `viewing_angle_known` — `bool` properties. -- `physical_coordinates(pixel_scale=None, viewing_angle_deg=None)` — - returns `(x, y)` physical coordinate grids matching the plane's flux - shape. Values known on the `MeasurementPlane` take precedence over the - `override` arguments; raises `ValueError` if a value is neither known nor - overridden. +A nominal pinhole camera pose/intrinsics shared across every plane in one +reconstruction. Always a point estimate — never trusted as exact by +itself; trust is expressed via the paired `CameraModelTolerance`. + +- `focal_length_px` — focal length in pixel units. +- `position` — `(x, y, z)` camera position in the beam-axis world frame, + meters; `z=0` is the output window. +- `orientation_deg` — `(yaw, pitch, roll)`, degrees. All-zero means the + boresight is normal to every `z=const` target plane with no in-plane + rotation. +- `principal_point` — `(px, px)` offset from the frame center. + +### `CameraModelTolerance(focal_length_px, position, orientation_deg, principal_point=(0.0, 0.0))` + +Per-field `+/-` refinement bound, same shape as `CameraModel`. Every field +must be `>= 0` (raises `ValueError` otherwise). A field's tolerance of `0` +holds that `CameraModel` field fixed at its nominal value during fitting; +`> 0` lets `ModalFitter` refine it within `[nominal - tolerance, nominal + +tolerance]`. + +### `GeometryCalibration(camera)` + +Wraps a `CameraModel` and resolves pixel <-> physical coordinate mappings +via true pinhole projection (not a uniform affine/cosine approximation). + +- `pixel_coordinates(x, y, z) -> (row, col)` — forward-projects physical + `(x, y)` at depth `z` to pixel coordinates. Raises `ValueError` if the + point is behind the camera (`Z_cam <= 0`). +- `physical_coordinates(image_shape, z) -> (x, y)` — inverse-projects every + pixel in a frame of `image_shape` to physical `(x, y)` on the `z=const` + plane, via ray-plane intersection (this is what produces genuine + keystoning — non-uniform spacing across the frame — for tilted/off-axis + poses). Raises `ValueError` if the plane is edge-on to or behind the + camera. +- `effective_pixel_scale(image_shape, z) -> float` — a single isotropic + meters/pixel figure (finite-difference approximation at the frame + center), for callers like `DiffusionDeconvolver` that assume one + isotropic pixel-space kernel. + +### `CAMERA_FIELD_NAMES`, `camera_to_values`, `tolerance_to_values`, `camera_from_values` + +Module-level helpers used internally by `ModalFitter` to flatten/unflatten +`CameraModel`/`CameraModelTolerance` into the optimizer's parameter vector. +Not usually needed by application code, but exported for advanced use +(e.g. inspecting `CAMERA_FIELD_NAMES` to interpret `ReconstructionResult.geometry` keys). ## `noise` — `NoiseEstimator` @@ -121,25 +189,30 @@ pass a `deconvolver` to `BeamReconstructor`. - `deconvolve(image, pixel_scale, noise_to_signal_ratio=1e-3)` — regularized (Wiener) removal of the blur. -Note: the blur/deconvolution kernel is isotropic in pixel space. If a -plane has a nonzero `viewing_angle_deg`, its `x` and `y` pixel axes have -different physical scales (see `SyntheticBeamGenerator` below), so -deconvolution is only exact for `viewing_angle_deg == 0`; at oblique -angles it is an approximation. +Note: the blur/deconvolution kernel is isotropic in pixel space. A tilted +or off-axis `CameraModel` produces a pixel scale that varies across the +frame and between `x`/`y` (keystoning), so `deconvolve` uses +`GeometryCalibration.effective_pixel_scale` — a single isotropic +approximation evaluated at the frame center. This is exact only for an +on-axis, untilted camera; at oblique poses it is an accepted +approximation (see `CLAUDE.md`). ## `synthetic` — `SyntheticBeamGenerator` -`SyntheticBeamGenerator(basis, image_shape, pixel_scale)` — forward model -used to validate the pipeline against known ground truth, and to evaluate -experimental design (e.g. "would these distances separate my modes?"). -`pixel_scale` is the physical pixel size, in meters, along the non-tilted -`y` axis; the `x` axis is compressed by `1/cos(viewing_angle_deg)` to model -an oblique camera view. +`SyntheticBeamGenerator(basis, camera)` — forward model used to validate +the pipeline against known ground truth, and to evaluate experimental +design. `camera` is the ground-truth `CameraModel` (position/orientation/ +intrinsics) used to render each plane via true perspective projection. -- `generate(coefficients, z_list, *, center=(0, 0), pointing_angle_deg=0.0, viewing_angle_deg=0.0, noise_std=0.0, seed=None)` - — returns one `MeasurementPlane` per `z` in `z_list`. The beam's - transverse center drifts linearly with `z` according to - `pointing_angle_deg`, starting from `center` at `z0`. +- `generate(coefficients, z_list, image_shape, *, center=(0.0, 0.0), pointing_angle_horizontal_deg=0.0, pointing_angle_vertical_deg=0.0, z_tolerance=0.0, nominal_z_offsets=None, noise_std=0.0, seed=None) -> list[MeasurementPlane]` + — returns one `MeasurementPlane` per (true) `z` in `z_list`. The beam's + transverse center drifts linearly with `z` according to the two + independent pointing angles, starting from `center` at the basis's + `z0`. `nominal_z_offsets`, if given, maps a true `z` to an offset + applied to that plane's *nominal* `z` — letting a reconstruction be + tested against a deliberately-offset nominal input while the plane's + flux is still rendered at the true `z`. Every resulting plane shares + `z_tolerance`. ## `fitting` — `ModalFitter`, `generate_mode_shells` @@ -152,16 +225,22 @@ Groups candidate `LG_{p,l}` modes into shells of increasing order ### `ModalFitter(basis, noise_estimator=None)` Core reconstruction path: a joint nonlinear least-squares fit of complex LG -coefficients, beam center/pointing, and (if unknown) geometry. +coefficients, beam center/pointing, and any nonzero-tolerance camera/`z` +geometry. -- `fit(planes, modes, initial_coefficients=None, initial_center=(0.0, 0.0), initial_tilt_deg=(0.0, 0.0), initial_pixel_scale=None, initial_viewing_angle_deg=0.0) -> ReconstructionResult` - — fits exactly the given candidate `modes`. -- `fit_auto(planes, max_order=4, bic_improvement_threshold=10.0) -> ReconstructionResult` +- `fit(planes, modes, camera, camera_tolerance, initial_coefficients=None, initial_center=(0.0, 0.0), initial_pointing_deg=(0.0, 0.0)) -> ReconstructionResult` + — fits exactly the given candidate `modes`. Every `CameraModel` field + with a nonzero `camera_tolerance` entry, and every plane whose + `z_tolerance` is nonzero, is refined within `[nominal - tolerance, + nominal + tolerance]`; zero-tolerance fields are held fixed at their + nominal value. +- `fit_auto(planes, camera, camera_tolerance, max_order=4, bic_improvement_threshold=10.0) -> ReconstructionResult` — starts from `LG_00` and grows the candidate mode set shell-by-shell (via `generate_mode_shells`), stopping once BIC no longer improves by more than `bic_improvement_threshold`, capped at `max_order`. Emits a `UserWarning` (does not raise) if the cap is reached while the fit is - still improving. + still improving, or if the number of free camera+`z` parameters is large + relative to the number of planes (see `CLAUDE.md`'s degeneracy pitfall). ## `phase_retrieval` — `PhaseRetriever`, `propagate_angular_spectrum` @@ -176,11 +255,12 @@ model implicitly assumed by `LGBasis`'s closed-form paraxial modes. ### `PhaseRetriever(wavelength)` -- `retrieve(planes, pixel_scale=None, viewing_angle_deg=None, max_iterations=200) -> PhaseRetrievalResult` +- `retrieve(planes, camera, max_iterations=200) -> PhaseRetrievalResult` — multi-plane Gerchberg-Saxton phase retrieval: propagates a trial complex field back and forth between planes, enforcing the measured amplitude (`sqrt(flux)`) at each plane, without assuming a finite mode - basis. + basis. All planes are propagated on one common physical grid, derived + from `camera` at the smallest-`z` plane's depth. ### `PhaseRetrievalResult` @@ -192,23 +272,27 @@ table, as `BeamReconstructor` does internally for its fallback path. ## `reconstruct` — `BeamReconstructor` -`BeamReconstructor(w0, z0, wavelength, max_order=4, noise_estimator=None, deconvolver=None, force_phase_retrieval=False, phase_retrieval_residual_threshold=None)` +`BeamReconstructor(w0, z0, wavelength, camera, camera_tolerance, max_order=4, noise_estimator=None, deconvolver=None, force_phase_retrieval=False, phase_retrieval_residual_threshold=None)` High-level orchestrator wiring together the full pipeline: optional diffusion deblurring → `ModalFitter.fit_auto` → optional -`PhaseRetriever` fallback. +`PhaseRetriever` fallback. `camera`/`camera_tolerance` are the nominal +shared `CameraModel` and its per-field refinement bounds for this +reconstruction. - `reconstruct(planes) -> ReconstructionResult` 1. Validates `planes` (see `validate_planes`). - 2. If `deconvolver` is set, deblurs each plane (raises `ValueError` if a - plane's `pixel_scale` isn't known). - 3. Runs `ModalFitter(basis, noise_estimator).fit_auto(planes, max_order)`. + 2. If `deconvolver` is set, deblurs each plane using + `GeometryCalibration(camera).effective_pixel_scale(plane.flux.shape, plane.z)`. + 3. Runs `ModalFitter(basis, noise_estimator).fit_auto(planes, camera, camera_tolerance, max_order)`. 4. Runs the `PhaseRetriever` fallback instead, projecting its recovered field onto all modes up to `max_order`, if `force_phase_retrieval` is `True`, or if `phase_retrieval_residual_threshold` is set and the modal fit's noise-weighted RMS residual exceeds it. In that case - `result.residuals` is empty and `coefficient_uncertainty` is `NaN` - per mode (phase retrieval doesn't produce a fit covariance). + `result.residuals` is empty, `coefficient_uncertainty` is `NaN` per + mode, `geometry` is empty, and both pointing-angle fields are `NaN` + (phase retrieval doesn't fit geometry/pointing or produce a fit + covariance). ## `plotting` — diagnostic visualizations