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 <noreply@anthropic.com>
This commit is contained in:
+17
-12
@@ -9,24 +9,22 @@ import numpy as np
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MeasurementPlane:
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
flux : 2D array of flux values (already dead-pixel/background/saturation
|
flux : 2D array of flux values (already dead-pixel/background/saturation
|
||||||
corrected upstream).
|
corrected upstream).
|
||||||
z : distance from the output window, in meters. Must be positive.
|
z : nominal distance from the output window, in meters. Must be positive.
|
||||||
pixel_scale : known mm/pixel scale, if calibrated. If None, treated as an
|
z_tolerance : +/- bound, in meters, around the nominal `z` within which
|
||||||
unknown to be estimated jointly during reconstruction.
|
the true distance is refined during fitting. Must be `>= 0`; `0`
|
||||||
viewing_angle_deg : known camera viewing angle relative to the beam axis,
|
means `z` is trusted exactly and held fixed.
|
||||||
in degrees, if calibrated. If None, treated as an unknown.
|
|
||||||
label : optional human-readable label (e.g. "plane_40cm").
|
label : optional human-readable label (e.g. "plane_40cm").
|
||||||
"""
|
"""
|
||||||
|
|
||||||
flux: np.ndarray
|
flux: np.ndarray
|
||||||
z: float
|
z: float
|
||||||
pixel_scale: float | None = None
|
z_tolerance: float = 0.0
|
||||||
viewing_angle_deg: float | None = None
|
|
||||||
label: str | None = None
|
label: str | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
@@ -36,6 +34,10 @@ class MeasurementPlane:
|
|||||||
)
|
)
|
||||||
if self.z <= 0:
|
if self.z <= 0:
|
||||||
raise ValueError(f"MeasurementPlane.z must be positive, got {self.z}")
|
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:
|
def validate_planes(planes: list[MeasurementPlane]) -> None:
|
||||||
@@ -68,9 +70,11 @@ class ReconstructionResult:
|
|||||||
reconstructed_field : reconstructed complex field (at the reference
|
reconstructed_field : reconstructed complex field (at the reference
|
||||||
waist, or as configured).
|
waist, or as configured).
|
||||||
centers : fitted beam transverse center (x, y) in meters, per plane.
|
centers : fitted beam transverse center (x, y) in meters, per plane.
|
||||||
pointing_angle_deg : fitted shared beam pointing angle (tilt), in degrees.
|
pointing_angle_horizontal_deg, pointing_angle_vertical_deg : fitted
|
||||||
geometry : geometry parameters used or fitted (e.g. pixel_scale,
|
shared beam pointing (tilt) angles, in degrees.
|
||||||
viewing_angle_deg), keyed by name.
|
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).
|
residuals : per-plane residual maps (measured - modeled flux).
|
||||||
coefficient_uncertainty : mapping from (p, l) mode index to the
|
coefficient_uncertainty : mapping from (p, l) mode index to the
|
||||||
1-sigma uncertainty on its fitted power fraction.
|
1-sigma uncertainty on its fitted power fraction.
|
||||||
@@ -81,7 +85,8 @@ class ReconstructionResult:
|
|||||||
purity: dict[tuple[int, int], tuple[float, float]]
|
purity: dict[tuple[int, int], tuple[float, float]]
|
||||||
reconstructed_field: np.ndarray
|
reconstructed_field: np.ndarray
|
||||||
centers: list[tuple[float, float]]
|
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)
|
geometry: dict[str, float] = field(default_factory=dict)
|
||||||
residuals: list[np.ndarray] = field(default_factory=list)
|
residuals: list[np.ndarray] = field(default_factory=list)
|
||||||
coefficient_uncertainty: dict[tuple[int, int], float] = field(default_factory=dict)
|
coefficient_uncertainty: dict[tuple[int, int], float] = field(default_factory=dict)
|
||||||
|
|||||||
+5
-1
@@ -41,7 +41,11 @@ def plot_center_trace(
|
|||||||
ax_y.plot(z_values, y_values, marker="o")
|
ax_y.plot(z_values, y_values, marker="o")
|
||||||
ax_y.set_xlabel("z (m)")
|
ax_y.set_xlabel("z (m)")
|
||||||
ax_y.set_ylabel("center y (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
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+13
-9
@@ -10,19 +10,15 @@ def test_measurement_plane_stores_fields():
|
|||||||
|
|
||||||
assert plane.z == 0.3
|
assert plane.z == 0.3
|
||||||
assert np.array_equal(plane.flux, flux)
|
assert np.array_equal(plane.flux, flux)
|
||||||
assert plane.pixel_scale is None
|
assert plane.z_tolerance == 0.0
|
||||||
assert plane.viewing_angle_deg is None
|
|
||||||
assert plane.label is None
|
assert plane.label is None
|
||||||
|
|
||||||
|
|
||||||
def test_measurement_plane_stores_optional_fields():
|
def test_measurement_plane_stores_optional_fields():
|
||||||
flux = np.ones((4, 4))
|
flux = np.ones((4, 4))
|
||||||
plane = MeasurementPlane(
|
plane = MeasurementPlane(flux=flux, z=0.4, z_tolerance=0.01, label="plane_40cm")
|
||||||
flux=flux, z=0.4, pixel_scale=0.1, viewing_angle_deg=5.0, label="plane_40cm"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert plane.pixel_scale == 0.1
|
assert plane.z_tolerance == 0.01
|
||||||
assert plane.viewing_angle_deg == 5.0
|
|
||||||
assert plane.label == "plane_40cm"
|
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)
|
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():
|
def test_validate_planes_rejects_fewer_than_three():
|
||||||
planes = [
|
planes = [
|
||||||
MeasurementPlane(flux=np.ones((4, 4)), z=0.3),
|
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)},
|
purity={(0, 0): (1.0, 0.0)},
|
||||||
reconstructed_field=np.ones((4, 4), dtype=complex),
|
reconstructed_field=np.ones((4, 4), dtype=complex),
|
||||||
centers=[(0.0, 0.0)],
|
centers=[(0.0, 0.0)],
|
||||||
pointing_angle_deg=0.0,
|
pointing_angle_horizontal_deg=0.1,
|
||||||
geometry={"pixel_scale": 0.1, "viewing_angle_deg": 2.0},
|
pointing_angle_vertical_deg=-0.2,
|
||||||
|
geometry={"focal_length_px": 2000.0},
|
||||||
residuals=[np.zeros((4, 4))],
|
residuals=[np.zeros((4, 4))],
|
||||||
coefficient_uncertainty={(0, 0): 0.01},
|
coefficient_uncertainty={(0, 0): 0.01},
|
||||||
used_phase_retrieval=False,
|
used_phase_retrieval=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.purity[(0, 0)] == (1.0, 0.0)
|
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
|
assert result.used_phase_retrieval is False
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ def make_result(**overrides):
|
|||||||
purity={(0, 0): (0.9, 0.1), (1, 0): (0.1, -0.2)},
|
purity={(0, 0): (0.9, 0.1), (1, 0): (0.1, -0.2)},
|
||||||
reconstructed_field=np.zeros((5, 5), dtype=complex),
|
reconstructed_field=np.zeros((5, 5), dtype=complex),
|
||||||
centers=[(0.0, 0.0), (1e-4, -1e-4), (2e-4, -2e-4)],
|
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],
|
residuals=[np.ones((5, 5)), np.ones((5, 5)) * 2, np.ones((5, 5)) * 3],
|
||||||
)
|
)
|
||||||
defaults.update(overrides)
|
defaults.update(overrides)
|
||||||
|
|||||||
Reference in New Issue
Block a user