From dffca62f817c3b77235fc1d11b10973d50a12d1c Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 3 Jul 2026 11:39:16 +0200 Subject: [PATCH] Replace per-plane pixel_scale/viewing_angle with z_tolerance; split pointing angle MeasurementPlane now carries a z_tolerance (uniform tolerance mechanism) instead of the removed pixel_scale/viewing_angle_deg fields. ReconstructionResult.pointing_angle_deg becomes horizontal/vertical fields to match the beam's two independent tilt degrees of freedom. Co-Authored-By: Claude Sonnet 5 --- he11lib/data.py | 29 +++++++++++++++++------------ he11lib/plotting.py | 6 +++++- tests/test_data.py | 22 +++++++++++++--------- tests/test_plotting.py | 3 ++- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/he11lib/data.py b/he11lib/data.py index b88b889..c0cfbc4 100644 --- a/he11lib/data.py +++ b/he11lib/data.py @@ -9,24 +9,22 @@ import numpy as np @dataclass class MeasurementPlane: - """A single thermal (flux) image at a known distance from the output window. + """A single thermal (flux) image at a nominal distance from the output window. Parameters ---------- flux : 2D array of flux values (already dead-pixel/background/saturation corrected upstream). - z : distance from the output window, in meters. Must be positive. - pixel_scale : known mm/pixel scale, if calibrated. If None, treated as an - unknown to be estimated jointly during reconstruction. - viewing_angle_deg : known camera viewing angle relative to the beam axis, - in degrees, if calibrated. If None, treated as an unknown. + z : nominal distance from the output window, in meters. Must be positive. + z_tolerance : +/- bound, in meters, around the nominal `z` within which + the true distance is refined during fitting. Must be `>= 0`; `0` + means `z` is trusted exactly and held fixed. label : optional human-readable label (e.g. "plane_40cm"). """ flux: np.ndarray z: float - pixel_scale: float | None = None - viewing_angle_deg: float | None = None + z_tolerance: float = 0.0 label: str | None = None def __post_init__(self) -> None: @@ -36,6 +34,10 @@ class MeasurementPlane: ) if self.z <= 0: raise ValueError(f"MeasurementPlane.z must be positive, got {self.z}") + if self.z_tolerance < 0: + raise ValueError( + f"MeasurementPlane.z_tolerance must be >= 0, got {self.z_tolerance}" + ) def validate_planes(planes: list[MeasurementPlane]) -> None: @@ -68,9 +70,11 @@ class ReconstructionResult: reconstructed_field : reconstructed complex field (at the reference waist, or as configured). centers : fitted beam transverse center (x, y) in meters, per plane. - pointing_angle_deg : fitted shared beam pointing angle (tilt), in degrees. - geometry : geometry parameters used or fitted (e.g. pixel_scale, - viewing_angle_deg), keyed by name. + pointing_angle_horizontal_deg, pointing_angle_vertical_deg : fitted + shared beam pointing (tilt) angles, in degrees. + geometry : fitted/held geometry parameters, keyed by name (the 9 + `CameraModel` field names from `he11lib.geometry.CAMERA_FIELD_NAMES`, + plus `z_{i}` per plane index `i`). residuals : per-plane residual maps (measured - modeled flux). coefficient_uncertainty : mapping from (p, l) mode index to the 1-sigma uncertainty on its fitted power fraction. @@ -81,7 +85,8 @@ class ReconstructionResult: purity: dict[tuple[int, int], tuple[float, float]] reconstructed_field: np.ndarray centers: list[tuple[float, float]] - pointing_angle_deg: float + pointing_angle_horizontal_deg: float + pointing_angle_vertical_deg: float geometry: dict[str, float] = field(default_factory=dict) residuals: list[np.ndarray] = field(default_factory=list) coefficient_uncertainty: dict[tuple[int, int], float] = field(default_factory=dict) diff --git a/he11lib/plotting.py b/he11lib/plotting.py index ccb3138..0422b85 100644 --- a/he11lib/plotting.py +++ b/he11lib/plotting.py @@ -41,7 +41,11 @@ def plot_center_trace( ax_y.plot(z_values, y_values, marker="o") ax_y.set_xlabel("z (m)") ax_y.set_ylabel("center y (m)") - fig.suptitle(f"Beam center (pointing angle {result.pointing_angle_deg:.3g} deg)") + fig.suptitle( + "Beam center (pointing angle " + f"h={result.pointing_angle_horizontal_deg:.3g} deg, " + f"v={result.pointing_angle_vertical_deg:.3g} deg)" + ) return fig diff --git a/tests/test_data.py b/tests/test_data.py index b4d2a52..d96d3f1 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -10,19 +10,15 @@ def test_measurement_plane_stores_fields(): assert plane.z == 0.3 assert np.array_equal(plane.flux, flux) - assert plane.pixel_scale is None - assert plane.viewing_angle_deg is None + assert plane.z_tolerance == 0.0 assert plane.label is None def test_measurement_plane_stores_optional_fields(): flux = np.ones((4, 4)) - plane = MeasurementPlane( - flux=flux, z=0.4, pixel_scale=0.1, viewing_angle_deg=5.0, label="plane_40cm" - ) + plane = MeasurementPlane(flux=flux, z=0.4, z_tolerance=0.01, label="plane_40cm") - assert plane.pixel_scale == 0.1 - assert plane.viewing_angle_deg == 5.0 + assert plane.z_tolerance == 0.01 assert plane.label == "plane_40cm" @@ -39,6 +35,11 @@ def test_measurement_plane_rejects_non_positive_z(): MeasurementPlane(flux=np.ones((4, 4)), z=-0.1) +def test_measurement_plane_rejects_negative_z_tolerance(): + with pytest.raises(ValueError, match="z_tolerance"): + MeasurementPlane(flux=np.ones((4, 4)), z=0.3, z_tolerance=-0.01) + + def test_validate_planes_rejects_fewer_than_three(): planes = [ MeasurementPlane(flux=np.ones((4, 4)), z=0.3), @@ -82,12 +83,15 @@ def test_reconstruction_result_stores_fields(): purity={(0, 0): (1.0, 0.0)}, reconstructed_field=np.ones((4, 4), dtype=complex), centers=[(0.0, 0.0)], - pointing_angle_deg=0.0, - geometry={"pixel_scale": 0.1, "viewing_angle_deg": 2.0}, + pointing_angle_horizontal_deg=0.1, + pointing_angle_vertical_deg=-0.2, + geometry={"focal_length_px": 2000.0}, residuals=[np.zeros((4, 4))], coefficient_uncertainty={(0, 0): 0.01}, used_phase_retrieval=False, ) assert result.purity[(0, 0)] == (1.0, 0.0) + assert result.pointing_angle_horizontal_deg == 0.1 + assert result.pointing_angle_vertical_deg == -0.2 assert result.used_phase_retrieval is False diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a376bb9..04265d7 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -11,7 +11,8 @@ def make_result(**overrides): purity={(0, 0): (0.9, 0.1), (1, 0): (0.1, -0.2)}, reconstructed_field=np.zeros((5, 5), dtype=complex), centers=[(0.0, 0.0), (1e-4, -1e-4), (2e-4, -2e-4)], - pointing_angle_deg=0.5, + pointing_angle_horizontal_deg=0.3, + pointing_angle_vertical_deg=0.4, residuals=[np.ones((5, 5)), np.ones((5, 5)) * 2, np.ones((5, 5)) * 3], ) defaults.update(overrides)