Generalizes camera modeling from a single pixel-scale/viewing-angle pair to a full pinhole camera (3D orientation + position + intrinsics), projected via true perspective (not just uniform cosine compression), plus 2D beam pointing and per-plane z uncertainty. Every nominal geometry value is now paired with an explicit tolerance that determines whether it's held fixed or refined jointly with the mode fit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
he11lib — Full Camera Geometry & Measurement Uncertainty Redesign
Date: 2026-07-03
Status: Approved for planning
Supersedes: parts of docs/superpowers/specs/2026-07-02-gyrotron-mode-purity-design.md
relating to MeasurementPlane.pixel_scale/viewing_angle_deg, GeometryCalibration,
and the single-scalar beam pointing angle. All other decisions in the original spec
(mode basis, noise, deconvolution, phase-retrieval fallback, package layout) are
unchanged and still apply.
Purpose
The original design modeled camera geometry as a single, isotropic pixel scale plus
one viewing-angle tilt (a uniform, affine correction across the whole frame), and
treated each MeasurementPlane's z distance as exact. Real measurement setups are
more demanding:
- The camera's orientation relative to the beam axis has a full 3 rotational degrees of freedom (yaw, pitch, roll), not one.
- The camera is close/wide-angle enough that true perspective projection (keystoning that varies across the frame) matters, not just a uniform compression factor.
- A physical calibration step (e.g. fiducial markers) gives nominal values for camera position, orientation, and intrinsics — but mechanical vibration means none of these can be trusted as exact; all must be refined jointly with everything else.
- The beam's pointing/tilt is two-dimensional (independent horizontal and vertical tilt), not a single scalar angle.
- Each plane's
zdistance (from a translation stage or tape measure) is also only known to a nominal precision and should be refined, not trusted exactly.
Scope of this change
- Replace the single pixel-scale/viewing-angle model with a full pinhole camera model shared across all planes in one reconstruction.
- Generalize beam pointing from one angle to two (horizontal, vertical).
- Model each plane's
zas a nominal value with its own refinable uncertainty. - Unify "trusted/fixed" vs. "uncertain/refined" behind a single tolerance mechanism
(see below), replacing the old
None-means-unknown convention for geometry. - Out of scope: lens distortion (radial/tangential), rolling-shutter effects, and multi-camera setups. These are not part of the current measurement setup and would need a separate design if they become relevant.
Architecture
Data model changes (data.py)
MeasurementPlanedropspixel_scaleandviewing_angle_deg(per-plane geometry no longer makes sense once the camera pose is a single shared physical setup for the whole reconstruction). It gainsz_tolerance: float, the ± bound (in meters) around the nominalzwithin which the true distance is refined.z_tolerancemust be>= 0;0meanszis trusted exactly and held fixed.ReconstructionResult.pointing_angle_deg: floatbecomes two fields:pointing_angle_horizontal_deg: floatandpointing_angle_vertical_deg: float.ReconstructionResult.geometrygains entries for the fittedCameraModelfields (see below) alongside the existing per-plane fittedzvalues.
CameraModel and CameraModelTolerance (geometry.py)
@dataclass
class CameraModel:
focal_length_px: float
position: tuple[float, float, float] # (x, y, z) in the beam-axis frame,
# z=0 at the output window
orientation_deg: tuple[float, float, float] # (yaw, pitch, roll); all-zero =
# boresight normal to the target
# plane, no in-plane rotation
principal_point: tuple[float, float] = (0.0, 0.0) # (px, px) offset from frame center
@dataclass
class CameraModelTolerance:
focal_length_px: float
position: tuple[float, float, float]
orientation_deg: tuple[float, float, float]
principal_point: tuple[float, float] = (0.0, 0.0) # (px, px)
CameraModel always represents a nominal point estimate (from calibration, or an
assumed default), never a value trusted as exact by itself — trust/uncertainty is
expressed entirely through the paired CameraModelTolerance (see "Tolerance
mechanism" below).
GeometryCalibration is rewritten around these two types. Given a CameraModel and a
target plane's z, it provides:
- Forward projection — physical
(X, Y)at thatzto pixel(row, col), via standard pinhole projection: rotate/translate the world point into the camera frame usingorientation_deg/position, then perspective-divide usingfocal_length_px/principal_point. - Inverse projection — pixel
(row, col)to physical(X, Y), via ray–plane intersection: cast a ray from the camera through each pixel and intersect it with the knownz =const target plane. This is what produces genuine keystoning (the correction varies across the frame), replacing the old uniformx = col * scale / cos(angle)formula. - Raises
ValueErrorfor degenerate poses where the target plane is edge-on to or behind the camera (no valid intersection).
Tolerance mechanism (applies to CameraModel/CameraModelTolerance and
MeasurementPlane.z/z_tolerance)
Every nominal geometry value is paired with a tolerance that determines how
ModalFitter treats it:
tolerance == 0: held fixed at the nominal value. Excluded entirely from the optimizer's parameter vector and substituted as a constant into the model function. This recovers the old "fully known, don't fit it" behavior.tolerance > 0: included in the fit, bounded to[nominal - tolerance, nominal + tolerance]viascipy.optimize.least_squares'bounds=.
There is no "fully unbounded" mode for these parameters — if a value is genuinely unconstrained, its tolerance should be set generously wide rather than infinite, since an unbounded 7-9 parameter homography fit from 3-10 planes has little chance of converging usefully without some bound.
ModalFitter changes (fitting.py)
The optimizer's parameter vector is now built dynamically per reconstruction:
- Always free (unchanged in kind, generalized in the pointing case): complex LG mode
coefficients, per-plane beam transverse center
(x, y), and the two beam pointing angles (pointing_angle_horizontal_deg,pointing_angle_vertical_deg). - Conditionally free (new): any
CameraModelfield whose pairedCameraModelToleranceentry is nonzero, and any plane'szwhosez_toleranceis nonzero — each bounded as described above.
BeamReconstructor.__init__ gains required camera: CameraModel and
camera_tolerance: CameraModelTolerance parameters (alongside existing w0, z0,
wavelength), replacing the old optional per-plane pixel-scale/viewing-angle
handling.
SyntheticBeamGenerator changes (synthetic.py)
Takes an exact ground-truth CameraModel (position/orientation/intrinsics) and the
two beam pointing angles, and generates each plane at its own exact/ground-truth z
— which may deliberately differ from the nominal z given to the resulting
MeasurementPlane, so tests can verify the fit recovers the true z despite a
deliberately offset nominal input.
Data flow (updated)
- Build a list of
MeasurementPlane(flux array + nominalz+z_tolerance+ label), plus a nominalCameraModel+CameraModelTolerancefor the whole reconstruction. GeometryCalibrationresolves the pixel↔physical mapping per plane from the (possibly-still-being-refined)CameraModeland that plane's (possibly-being- refined)z.NoiseEstimatorcomputes per-plane noise weights (unchanged).DiffusionDeconvolveroptionally deblurs each plane (unchanged; still assumes an isotropic pixel-space kernel — noted as an existing approximation, not addressed by this redesign).ModalFitterruns the joint noise-weighted nonlinear least-squares fit over LG coefficients + per-plane center + 2 pointing angles + any nonzero-tolerance camera andzparameters, growing the mode set automatically as before.- Phase-retrieval fallback and
ReconstructionResultassembly proceed as in the original design, withpointing_angle_degreplaced by the horizontal/vertical pair andgeometryextended to include the fittedCameraModelfields and per-plane fittedz.
Testing strategy
In addition to the original design's synthetic-ground-truth-recovery approach:
CameraModelprojection round-trip: for several poses (on-axis, tilted, off-center) and severalzvalues, physical→pixel→physical recovers the original point.- Keystone regression: projecting a symmetric grid through a tilted/off-axis
CameraModelproduces non-uniform spacing across the frame (distinguishing it from the old uniform cosine-compression model). - Degenerate pose: a pose placing the target plane edge-on to or behind the
camera raises
ValueError. - Tolerance semantics: a
tolerance=0field stays exactly at its (deliberately wrong) nominal value rather than being corrected; atolerance>0field recovers a ground truth offset from nominal but within its band; a ground truth placed outside a deliberately too-tight band is clipped to the bound rather than escaping it. - End-to-end: the full pipeline recovers mode purity, both pointing angles, the
camera pose, and per-plane
z, whenSyntheticBeamGenerator's ground truth is offset from the nominal inputs (within their tolerances) — simulating realistic calibration/measurement error rather than assuming perfect nominal values.
Error handling
CameraModelTolerancefields andMeasurementPlane.z_tolerancemust be>= 0;ValueErrorotherwise. Validated at construction, consistent with the existing "validate only at boundaries" approach.- Degenerate camera geometry (target plane edge-on to or behind the camera) raises
ValueErrorfrom the projection code rather than producing NaNs. - New documented pitfall: with only 3-10 planes, adding ~7-9 shared camera
unknowns plus one
zcorrection per plane 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/BeamReconstructoremit aUserWarning(not an error, consistent with existing warn-don't-raise style) when the free-parameter count is large relative to the number of planes, prompting the user toward tighter tolerances rather than silently returning an ill-conditioned fit.
Migration impact
This is a breaking change to the public API, acceptable pre-1.0 with no external users yet:
MeasurementPlane(pixel_scale=..., viewing_angle_deg=...)→MeasurementPlane(z_tolerance=...)+ a reconstruction-levelCameraModel/CameraModelTolerance.BeamReconstructor(...)requires newcamera/camera_tolerancearguments.SyntheticBeamGenerator.generate(...)requires aCameraModeland two pointing angles instead of scalarviewing_angle_deg/pixel_scale/pointing_angle_deg.ReconstructionResult.pointing_angle_deg→pointing_angle_horizontal_deg+pointing_angle_vertical_deg.docs/api.mdandexamples/full_pipeline_example.pyneed updating to match.- The existing pitfalls documented in
CLAUDE.md(Rayleigh-range clipping, mode-growth overfitting) still apply unchanged; the new degeneracy pitfall above is additive.
Deliverables
- Updated
geometry.py(CameraModel,CameraModelTolerance, rewrittenGeometryCalibration),data.py,fitting.py,synthetic.py,reconstruct.py. - Updated tests covering projection round-trip, keystone behavior, degenerate poses, tolerance semantics, and end-to-end recovery under offset nominal inputs.
- Updated
docs/api.mdandexamples/full_pipeline_example.pyreflecting the new public interface. - Updated
CLAUDE.mdwith the new degeneracy pitfall.