c2ae95e01f
Renders each plane via true pinhole projection through a shared CameraModel instead of the old cosine-compression formula, adds independent horizontal/vertical pointing drift, and supports generating a deliberately-offset nominal z (vs. true z) per plane for tolerance- recovery testing in later tasks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""Forward model: synthetic thermal (flux) images from known ground truth.
|
|
|
|
Used to validate the reconstruction pipeline (recover known mode content)
|
|
and to help users evaluate experimental design (e.g. whether a given set of
|
|
measurement distances will separate candidate modes).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from .data import MeasurementPlane
|
|
from .geometry import CameraModel, GeometryCalibration
|
|
from .modes import LGBasis
|
|
|
|
|
|
class SyntheticBeamGenerator:
|
|
"""Generates synthetic multi-plane flux images for a known ground-truth beam.
|
|
|
|
Parameters
|
|
----------
|
|
basis : LGBasis defining the reference w0, z0, wavelength.
|
|
camera : ground-truth CameraModel (position/orientation/intrinsics) used
|
|
to render each plane via true perspective projection.
|
|
"""
|
|
|
|
def __init__(self, basis: LGBasis, camera: CameraModel):
|
|
self.basis = basis
|
|
self.camera = camera
|
|
self.calibration = GeometryCalibration(camera)
|
|
|
|
def generate(
|
|
self,
|
|
coefficients: dict[tuple[int, int], complex],
|
|
z_list: list[float],
|
|
image_shape: tuple[int, int],
|
|
*,
|
|
center: tuple[float, float] = (0.0, 0.0),
|
|
pointing_angle_horizontal_deg: float = 0.0,
|
|
pointing_angle_vertical_deg: float = 0.0,
|
|
z_tolerance: float = 0.0,
|
|
nominal_z_offsets: dict[float, float] | None = None,
|
|
noise_std: float = 0.0,
|
|
seed: int | None = None,
|
|
) -> list[MeasurementPlane]:
|
|
"""Generate one MeasurementPlane per requested (true) z distance.
|
|
|
|
The beam transverse center drifts linearly with z according to the
|
|
two pointing angles, starting from `center` at the basis's
|
|
reference z0. `nominal_z_offsets`, if given, maps a true z (as
|
|
given in z_list) to an offset applied to the *nominal* z stored on
|
|
the resulting MeasurementPlane -- letting tests verify a fit
|
|
recovers the true z despite a deliberately-offset nominal input.
|
|
Every resulting plane shares `z_tolerance`.
|
|
"""
|
|
rng = np.random.default_rng(seed)
|
|
tilt_h_rad = np.deg2rad(pointing_angle_horizontal_deg)
|
|
tilt_v_rad = np.deg2rad(pointing_angle_vertical_deg)
|
|
offsets = nominal_z_offsets or {}
|
|
|
|
planes = []
|
|
for z in z_list:
|
|
drift_x = (z - self.basis.z0) * np.tan(tilt_h_rad)
|
|
drift_y = (z - self.basis.z0) * np.tan(tilt_v_rad)
|
|
cx = center[0] + drift_x
|
|
cy = center[1] + drift_y
|
|
|
|
x, y = self.calibration.physical_coordinates(image_shape, z)
|
|
field = self.basis.field_superposition(x - cx, y - cy, z, coefficients)
|
|
flux = np.abs(field) ** 2
|
|
|
|
if noise_std > 0:
|
|
flux = flux + rng.normal(0.0, noise_std, size=flux.shape)
|
|
|
|
nominal_z = z + offsets.get(z, 0.0)
|
|
planes.append(
|
|
MeasurementPlane(flux=flux, z=nominal_z, z_tolerance=z_tolerance)
|
|
)
|
|
return planes
|