API reference

Top-level

telescope-sim — composable, config-driven simulation of multi-aperture telescope PSFs.

Built on HCIPy. The package exposes a pluggable pipeline of optical stages (aperture, correctors, coronagraph, focal plane, output taps, post-processing) driven by YAML configuration. Currently in alpha; concrete implementations are under active development.

class telescope_sim.TelescopeSim(components: _PipelineComponents)[source]

Bases: object

Composable telescope-PSF simulator.

Construct via one of the classmethods (not via __init__ directly):

  • from_preset() — load a packaged preset by name

  • from_yaml() — load a user-supplied YAML config

  • from_components() — instantiate from already-resolved components (primarily for the pipeline’s own internal use and for advanced users bypassing the YAML/pydantic layer)

property aperture: ApertureResult
property correctors: dict[str, Corrector]
property focal_planes: dict[str, FocalPlane]
classmethod from_components(components: _PipelineComponents) TelescopeSim[source]

Instantiate from already-resolved pipeline components.

classmethod from_preset(name: str) TelescopeSim[source]

Load a packaged preset by name.

classmethod from_yaml(path: str | Path) TelescopeSim[source]

Load a configuration YAML and build the pipeline.

sample(actuations: Mapping[str, ArrayLike] | None = None, *, atmos: Any = None, output_overrides: Mapping[str, Mapping[str, Any]] | None = None, meas_strehl: bool = False, meas_pupil_opd: bool = False) dict[str, Any][source]

Run the optical chain and return a dict of outputs.

Parameters:
  • actuations – Per-corrector actuator state. Keys are corrector names (matching those declared in the config); each value is whatever shape that corrector’s set_actuators accepts.

  • atmos

    Per-sample external atmosphere. Any callable taking a hcipy.Wavefront and returning a modified hcipy.Wavefront (typically a hcipy.InfiniteAtmosphericLayer or hcipy.MultiLayerAtmosphere, but any wf→wf callable works). Applied at the front of the chain, before any corrector. The caller owns time evolution — v2 holds no atmosphere state.

    If the object also exposes .phase_for(lam) returning HCIPy-convention phase = 2π·OPD/lam, the atmosphere’s OPD is seeded into the per-corrector cumulative-OPD stream, so fit-role correctors with fit_source="cumulative_phase_pre_self" will naturally fit to (and cancel) the atmosphere. Without .phase_for the wavefront is still modified, but fit-role correctors only see corrector-chain OPD.

    The reference PSF is never atmospheric — it’s cached once at sim-build time with atmos=None implicit.

  • output_overrides – Per-sample tap-config overrides, keyed by output name. For example, {"psf": {"int_phot_flux": 5.0e7}} to vary the photon flux on a noisy-intensity tap from sample to sample. Taps that have no per-sample state ignore the override.

  • meas_strehl – If True, includes a strehls entry in the returned dict.

  • meas_pupil_opd – If True, includes a pupil_opd entry in the returned dict: the cumulative pupil-plane OPD seen at the back of the chain, as an hcipy.Field on the simulator’s pupil grid in meters. Sums atmosphere (when atmos.phase_for is available) + every DM-backed corrector’s surface × 2. Companion to sim.aperture.field for masked display via hcipy.imshow_field(out["pupil_opd"], mask=sim.aperture.field).

Returns:

images — dict of output_name → numpy array actuations — dict of corrector_name → numpy array

(only for correctors with target=True)

strehls — present iff meas_strehl is True pupil_opd — present iff meas_pupil_opd is True;

hcipy.Field of cumulative pupil-plane OPD in meters

Return type:

dict

telescope_sim.register(kind: str, name: str) Callable[[T], T][source]

Decorator: register a class as a named implementation of a pipeline-stage kind.

Pipeline

Pipeline orchestrator — holds the optical chain and runs sample().

This module exposes the top-level TelescopeSim class, the entry point for constructing a simulation from a YAML config, a preset, or a validated pydantic config object.

class telescope_sim.pipeline.TelescopeSim(components: _PipelineComponents)[source]

Bases: object

Composable telescope-PSF simulator.

Construct via one of the classmethods (not via __init__ directly):

  • from_preset() — load a packaged preset by name

  • from_yaml() — load a user-supplied YAML config

  • from_components() — instantiate from already-resolved components (primarily for the pipeline’s own internal use and for advanced users bypassing the YAML/pydantic layer)

property aperture: ApertureResult
property correctors: dict[str, Corrector]
property focal_planes: dict[str, FocalPlane]
classmethod from_components(components: _PipelineComponents) TelescopeSim[source]

Instantiate from already-resolved pipeline components.

classmethod from_preset(name: str) TelescopeSim[source]

Load a packaged preset by name.

classmethod from_yaml(path: str | Path) TelescopeSim[source]

Load a configuration YAML and build the pipeline.

sample(actuations: Mapping[str, ArrayLike] | None = None, *, atmos: Any = None, output_overrides: Mapping[str, Mapping[str, Any]] | None = None, meas_strehl: bool = False, meas_pupil_opd: bool = False) dict[str, Any][source]

Run the optical chain and return a dict of outputs.

Parameters:
  • actuations – Per-corrector actuator state. Keys are corrector names (matching those declared in the config); each value is whatever shape that corrector’s set_actuators accepts.

  • atmos

    Per-sample external atmosphere. Any callable taking a hcipy.Wavefront and returning a modified hcipy.Wavefront (typically a hcipy.InfiniteAtmosphericLayer or hcipy.MultiLayerAtmosphere, but any wf→wf callable works). Applied at the front of the chain, before any corrector. The caller owns time evolution — v2 holds no atmosphere state.

    If the object also exposes .phase_for(lam) returning HCIPy-convention phase = 2π·OPD/lam, the atmosphere’s OPD is seeded into the per-corrector cumulative-OPD stream, so fit-role correctors with fit_source="cumulative_phase_pre_self" will naturally fit to (and cancel) the atmosphere. Without .phase_for the wavefront is still modified, but fit-role correctors only see corrector-chain OPD.

    The reference PSF is never atmospheric — it’s cached once at sim-build time with atmos=None implicit.

  • output_overrides – Per-sample tap-config overrides, keyed by output name. For example, {"psf": {"int_phot_flux": 5.0e7}} to vary the photon flux on a noisy-intensity tap from sample to sample. Taps that have no per-sample state ignore the override.

  • meas_strehl – If True, includes a strehls entry in the returned dict.

  • meas_pupil_opd – If True, includes a pupil_opd entry in the returned dict: the cumulative pupil-plane OPD seen at the back of the chain, as an hcipy.Field on the simulator’s pupil grid in meters. Sums atmosphere (when atmos.phase_for is available) + every DM-backed corrector’s surface × 2. Companion to sim.aperture.field for masked display via hcipy.imshow_field(out["pupil_opd"], mask=sim.aperture.field).

Returns:

images — dict of output_name → numpy array actuations — dict of corrector_name → numpy array

(only for correctors with target=True)

strehls — present iff meas_strehl is True pupil_opd — present iff meas_pupil_opd is True;

hcipy.Field of cumulative pupil-plane OPD in meters

Return type:

dict

Abstract base classes

Abstract base classes for pluggable pipeline stages.

Each ABC defines the minimal interface a concrete implementation must satisfy to be registered and consumed by the pipeline. See individual module docstrings for semantics and the role/relationship system documented on Corrector.

class telescope_sim.abc.Aperture[source]

Bases: ABC

ABC for pupil-plane aperture constructors.

Subclasses are typically registered via telescope_sim.registry.register():

@register("aperture", "segmented_circular")
class SegmentedCircularAperture(Aperture):
    def build(self, pupil_grid):
        ...
abstractmethod build(pupil_grid: Any) ApertureResult[source]

Construct the aperture on the given pupil grid.

Parameters:

pupil_grid – HCIPy pupil grid (typically make_pupil_grid(resolution, extent)).

Returns:

The transmission field, collecting area, and optional segmentation.

Return type:

ApertureResult

class telescope_sim.abc.ApertureResult(field: Any, area: float, segments: Any | None = None, segment_coords: NDArray[floating] | None = None, metadata: dict[str, ~typing.Any]=<factory>)[source]

Bases: object

The outputs of an Aperture build step.

field

HCIPy Field representing the pupil-plane transmission (real-valued).

Type:

Any

area

Effective collecting area in pupil-grid units squared (used for photon flux scaling in noise calculations).

Type:

float

segments

Optional SegmentedAperture (HCIPy) for segmented mirrors. None for monolithic apertures.

Type:

Any | None

segment_coords

Optional (n_segments, 2) array of segment center coordinates. Used by per-segment PTT measurement (_measure_atmos_ptt-style fits).

Type:

numpy._typing._array_like.NDArray[numpy.floating] | None

area: float = <dataclasses._MISSING_TYPE object>
field: Any = <dataclasses._MISSING_TYPE object>
metadata: dict[str, Any] = <dataclasses._MISSING_TYPE object>
segment_coords: NDArray[floating] | None = None
segments: Any | None = None
class telescope_sim.abc.Coronagraph[source]

Bases: ABC

ABC for pupil-plane coronagraph elements.

abstractmethod apply(wf: Any) Any[source]

Apply the coronagraph to a pupil-plane wavefront, returning the modified wavefront.

name: str
class telescope_sim.abc.Corrector[source]

Bases: ABC

ABC for pluggable pupil-plane correctors (DMs, segmented mirrors, etc.).

Concrete implementations must provide apply() and set_actuators(). Implementations that participate in fit-roles or residual-fit targets must additionally provide fit_surface().

abstract property actuators: NDArray
abstractmethod apply(wf: Any) Any[source]

Apply the corrector’s current state to a wavefront in-chain.

fit_source: str | None = None
fit_surface(phase: NDArray[floating]) NDArray[floating][source]

Fit the corrector’s actuator basis to a pupil-plane OPD array.

Required when participating in role=fit or target_strategy=actuators_plus_residual_fit / residual_fit_only. Default implementation raises NotImplementedError to make misconfiguration visible.

Convention

  • Input phase: pupil-plane optical path difference (OPD) in meters — i.e. path-length, OPD = 2 * surface for a single DM reflection. The pipeline accumulates this as it walks the chain.

  • Output: matching caller-facing actuator values — i.e. values that, when set via set_actuators(), would reproduce the input OPD as this corrector’s surface contribution (modulo the round-trip factor; surface = OPD / 2).

  • Sign: matching, not cancellation. To cancel the input OPD, the caller negates. The pipeline does this automatically at the apply site for wavefront_role="fit".

  • Y echoes for target_strategy="actuators_plus_residual_fit" and "residual_fit_only" consume fit_surface unnegated: Y reports the wavefront state in this corrector’s basis (matches legacy v1 out_actuate = caller + matching_fit(atmos)). An ML model trained to predict Y is then driven through -Y by the downstream controller to cancel.

flatten() None[source]

Zero all actuators. Default impl calls set_actuators(0)-shaped.

abstract property n_actuators: int
name: str
abstractmethod set_actuators(values: ArrayLike) None[source]

Set the corrector’s actuator state.

target: bool = False
target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only'] = 'none'
wavefront_role: Literal['actuate', 'impose', 'fit']
class telescope_sim.abc.FocalPlane[source]

Bases: ABC

ABC for focal-plane setups (propagator + grid + optional detector).

integrate(wf: Any, t_exp: float = 1.0) NDArray[floating][source]

Integrate a focal-plane wavefront through the detector if present.

Default impl: returns wf.intensity.shaped (no detector noise). Implementations with detectors override this to call the in-chain NoisyDetector.

name: str
abstractmethod propagate(wf: Any) Any[source]

Propagate a pupil-plane wavefront to this focal plane.

Returns the focal-plane Wavefront (keeping phase info, so downstream taps like fiber coupling can use it).

class telescope_sim.abc.LoaderBindable[source]

Bases: object

Marker mixin for post-processors that need loader-injected dependencies.

The loader walks each output’s post-processing list and calls _bind_loader_dependencies(aperture_result, focal_planes, focal_plane_names) on any processor exposing the hook. Used by noisy_detector (needs the focal grid + aperture area) and convolve_image (needs the reference PSF sum). Processors without runtime dependencies simply don’t expose the hook and the loader skips them.

class telescope_sim.abc.OutputTap[source]

Bases: ABC

ABC for output extractors on named intermediate wavefronts.

abstractmethod extract(wf: Any, *, overrides: dict[str, Any] | None = None) NDArray[floating][source]

Return a numpy array representing this tap’s view of the wavefront.

Parameters:
  • wf – The tap’s input — typically the fp_results dict mapping focal- plane name to FocalPlaneResult.

  • overrides – Per-sample tap-config overrides from sim.sample(output_overrides=...). Most taps ignore this; taps that have per-sample state (e.g. photon flux for a noisy detector) read named fields here.

name: str
source: str
class telescope_sim.abc.PipelineContext(output_name: str, focal_plane_name: str, reference_peak_intensity: float | None = None, reference_psf_sum: float | None = None, overrides: dict[str, ~typing.Any]=<factory>, extras: dict[str, ~typing.Any]=<factory>)[source]

Bases: object

Per-sample state available to post-processors.

Populated by the pipeline before running the post-processing list for each output. Includes reference values (peak intensity, normalized reference PSF) used by normalization steps, plus any per-sample overrides the caller supplied via sim.sample(output_overrides={...}).

extras: dict[str, Any] = <dataclasses._MISSING_TYPE object>
focal_plane_name: str = <dataclasses._MISSING_TYPE object>
output_name: str = <dataclasses._MISSING_TYPE object>
overrides: dict[str, Any] = <dataclasses._MISSING_TYPE object>
reference_peak_intensity: float | None = None
reference_psf_sum: float | None = None
class telescope_sim.abc.PostProcessor[source]

Bases: ABC

ABC for image-space transforms applied after wavefront extraction.

name: str

Aperture ABC — constructs the static pupil-plane transmission mask.

Implementations are registered with @register("aperture", "<name>") and return an ApertureResult from Aperture.build(). The pipeline calls build() once at construction time.

class telescope_sim.abc.aperture.Aperture[source]

Bases: ABC

ABC for pupil-plane aperture constructors.

Subclasses are typically registered via telescope_sim.registry.register():

@register("aperture", "segmented_circular")
class SegmentedCircularAperture(Aperture):
    def build(self, pupil_grid):
        ...
abstractmethod build(pupil_grid: Any) ApertureResult[source]

Construct the aperture on the given pupil grid.

Parameters:

pupil_grid – HCIPy pupil grid (typically make_pupil_grid(resolution, extent)).

Returns:

The transmission field, collecting area, and optional segmentation.

Return type:

ApertureResult

class telescope_sim.abc.aperture.ApertureResult(field: Any, area: float, segments: Any | None = None, segment_coords: NDArray[floating] | None = None, metadata: dict[str, ~typing.Any]=<factory>)[source]

Bases: object

The outputs of an Aperture build step.

field

HCIPy Field representing the pupil-plane transmission (real-valued).

Type:

Any

area

Effective collecting area in pupil-grid units squared (used for photon flux scaling in noise calculations).

Type:

float

segments

Optional SegmentedAperture (HCIPy) for segmented mirrors. None for monolithic apertures.

Type:

Any | None

segment_coords

Optional (n_segments, 2) array of segment center coordinates. Used by per-segment PTT measurement (_measure_atmos_ptt-style fits).

Type:

numpy._typing._array_like.NDArray[numpy.floating] | None

area: float = <dataclasses._MISSING_TYPE object>
field: Any = <dataclasses._MISSING_TYPE object>
metadata: dict[str, Any] = <dataclasses._MISSING_TYPE object>
segment_coords: NDArray[floating] | None = None
segments: Any | None = None

Corrector ABC — pupil-plane element with actuator state and role-based behavior.

Two orthogonal axes control a corrector’s role in the pipeline:

  • wavefront_role: how actuator state gets set during sample()
    • "actuate" — set by the ML model (caller’s actuations dict)

    • "impose" — set by the caller via a separate channel

      (e.g. environment, atmosphere, fixed aberrations)

    • "fit" — lstsq-fit to another corrector’s surface or a named

      wavefront. Requires fit_source.

  • target_strategy: what this corrector contributes to the Y output dict
    • "none" — not in Y

    • "actuators" — echo back current actuator state

    • "actuators_plus_residual_fit"``actuators + fit_surface(

      cumulative_phase_pre_self)``

    • "residual_fit_only"``fit_surface(

      cumulative_phase_pre_self)`` only

fit_source (used when wavefront_role == "fit") can be:

  • a corrector name (string) — fit to that corrector’s surface

  • a named wavefront — fit to that wavefront’s phase

  • the special value "cumulative_phase_pre_self" — fit to cumulative phase just before

    this corrector in the chain

Common patterns this expresses:

  • Segment-impose / DM-fit: segments (actuate, target=actuators) plus a downstream xinetics corrector (fit, fit_source=segments, target=none) — the segmented mirror’s commanded surface is approximated by the DM in the beam, but Y reports the segment commands.

  • Cumulative residual-fit DM: an actuate corrector with target_strategy=actuators_plus_residual_fit and fit_source="cumulative_phase_pre_self" — Y reports model_actuators + fit(cumulative_phase_pre_self), i.e. the correction the model would have needed to fully cancel everything upstream. The upstream signal can be other imposed correctors and the external atmos=... kwarg to sample(); atmosphere is not a corrector but it does seed the cumulative-OPD stream when it exposes .phase_for(lam).

  • Stacked imposes + fitted DM: any number of impose correctors followed by a single actuate corrector with target_strategy=actuators_plus_residual_fit for ML training against cumulative residual phase.

class telescope_sim.abc.corrector.Corrector[source]

Bases: ABC

ABC for pluggable pupil-plane correctors (DMs, segmented mirrors, etc.).

Concrete implementations must provide apply() and set_actuators(). Implementations that participate in fit-roles or residual-fit targets must additionally provide fit_surface().

abstract property actuators: NDArray
abstractmethod apply(wf: Any) Any[source]

Apply the corrector’s current state to a wavefront in-chain.

fit_source: str | None = None
fit_surface(phase: NDArray[floating]) NDArray[floating][source]

Fit the corrector’s actuator basis to a pupil-plane OPD array.

Required when participating in role=fit or target_strategy=actuators_plus_residual_fit / residual_fit_only. Default implementation raises NotImplementedError to make misconfiguration visible.

Convention

  • Input phase: pupil-plane optical path difference (OPD) in meters — i.e. path-length, OPD = 2 * surface for a single DM reflection. The pipeline accumulates this as it walks the chain.

  • Output: matching caller-facing actuator values — i.e. values that, when set via set_actuators(), would reproduce the input OPD as this corrector’s surface contribution (modulo the round-trip factor; surface = OPD / 2).

  • Sign: matching, not cancellation. To cancel the input OPD, the caller negates. The pipeline does this automatically at the apply site for wavefront_role="fit".

  • Y echoes for target_strategy="actuators_plus_residual_fit" and "residual_fit_only" consume fit_surface unnegated: Y reports the wavefront state in this corrector’s basis (matches legacy v1 out_actuate = caller + matching_fit(atmos)). An ML model trained to predict Y is then driven through -Y by the downstream controller to cancel.

flatten() None[source]

Zero all actuators. Default impl calls set_actuators(0)-shaped.

abstract property n_actuators: int
name: str
abstractmethod set_actuators(values: ArrayLike) None[source]

Set the corrector’s actuator state.

target: bool = False
target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only'] = 'none'
wavefront_role: Literal['actuate', 'impose', 'fit']

Coronagraph ABC — optional pupil-plane stage between correctors and focal plane.

The pipeline always includes a coronagraph slot; the identity implementation is a no-op that keeps the chain uniform when no coronagraph is configured. The reference PSF (for Strehl normalization) is always generated with the identity coronagraph regardless of what’s installed.

class telescope_sim.abc.coronagraph.Coronagraph[source]

Bases: ABC

ABC for pupil-plane coronagraph elements.

abstractmethod apply(wf: Any) Any[source]

Apply the coronagraph to a pupil-plane wavefront, returning the modified wavefront.

name: str

FocalPlane ABC — owns a propagator, focal grid, wavelength sampling, optional detector.

Multiple named focal planes can fan out from the same pupil_post_coro wavefront, each with its own propagator and grid. This supports per-filter PSFs and mixed-physics setups (e.g. an angular focal plane for the science detector alongside a physical focal plane for fiber coupling).

class telescope_sim.abc.focal_plane.FocalPlane[source]

Bases: ABC

ABC for focal-plane setups (propagator + grid + optional detector).

integrate(wf: Any, t_exp: float = 1.0) NDArray[floating][source]

Integrate a focal-plane wavefront through the detector if present.

Default impl: returns wf.intensity.shaped (no detector noise). Implementations with detectors override this to call the in-chain NoisyDetector.

name: str
abstractmethod propagate(wf: Any) Any[source]

Propagate a pupil-plane wavefront to this focal plane.

Returns the focal-plane Wavefront (keeping phase info, so downstream taps like fiber coupling can use it).

OutputTap ABC — extracts a numpy array from a named intermediate wavefront.

Output taps reference a named wavefront in the pipeline (pupil_post_coro, focal-plane names, pupil_pre_<corrector>, etc.) and produce an array that becomes one of the entries in the returned images dict.

class telescope_sim.abc.output_tap.OutputTap[source]

Bases: ABC

ABC for output extractors on named intermediate wavefronts.

abstractmethod extract(wf: Any, *, overrides: dict[str, Any] | None = None) NDArray[floating][source]

Return a numpy array representing this tap’s view of the wavefront.

Parameters:
  • wf – The tap’s input — typically the fp_results dict mapping focal- plane name to FocalPlaneResult.

  • overrides – Per-sample tap-config overrides from sim.sample(output_overrides=...). Most taps ignore this; taps that have per-sample state (e.g. photon flux for a noisy detector) read named fields here.

name: str
source: str

PostProcessor ABC — ordered image transforms applied per output.

Each output declares a list of post-processors that run in order. A processor takes the current image plus a PipelineContext (which exposes reference PSFs, peak intensities, and any per-sample state needed for normalization) and returns the next image. Processors may change the array’s shape (e.g. fft_channels appends channels; channels_first transposes).

class telescope_sim.abc.post_processor.LoaderBindable[source]

Bases: object

Marker mixin for post-processors that need loader-injected dependencies.

The loader walks each output’s post-processing list and calls _bind_loader_dependencies(aperture_result, focal_planes, focal_plane_names) on any processor exposing the hook. Used by noisy_detector (needs the focal grid + aperture area) and convolve_image (needs the reference PSF sum). Processors without runtime dependencies simply don’t expose the hook and the loader skips them.

class telescope_sim.abc.post_processor.PipelineContext(output_name: str, focal_plane_name: str, reference_peak_intensity: float | None = None, reference_psf_sum: float | None = None, overrides: dict[str, ~typing.Any]=<factory>, extras: dict[str, ~typing.Any]=<factory>)[source]

Bases: object

Per-sample state available to post-processors.

Populated by the pipeline before running the post-processing list for each output. Includes reference values (peak intensity, normalized reference PSF) used by normalization steps, plus any per-sample overrides the caller supplied via sim.sample(output_overrides={...}).

extras: dict[str, Any] = <dataclasses._MISSING_TYPE object>
focal_plane_name: str = <dataclasses._MISSING_TYPE object>
output_name: str = <dataclasses._MISSING_TYPE object>
overrides: dict[str, Any] = <dataclasses._MISSING_TYPE object>
reference_peak_intensity: float | None = None
reference_psf_sum: float | None = None
class telescope_sim.abc.post_processor.PostProcessor[source]

Bases: ABC

ABC for image-space transforms applied after wavefront extraction.

name: str

Configuration

Pydantic v2 schemas for the telescope-sim configuration.

This module exposes the top-level SimConfig and per-stage sub-schemas. The schema is intentionally permissive on the per-stage config payloads: those are passed straight through to the registered implementation’s constructor, which is free to validate further.

class telescope_sim.config.schema.CorrectorConfig(*, type: str, wavefront_role: Literal['actuate', 'impose', 'fit'] = 'actuate', target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only'] = 'none', fit_source: str | None = None, target: bool = False, **extra_data: Any)[source]

Bases: StageConfig

A corrector + its role/target-strategy settings.

fit_source: str | None
model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

target: bool
target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only']
wavefront_role: Literal['actuate', 'impose', 'fit']
class telescope_sim.config.schema.OutputConfig(*, tap: StageConfig, post_processing: list[StageConfig | str] = <factory>)[source]

Bases: BaseModel

One named output: a tap plus its ordered post-processing list.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

post_processing: list[StageConfig | str]
tap: StageConfig
class telescope_sim.config.schema.PostProcessorConfig(*, type: str, **extra_data: Any)[source]

Bases: BaseModel

Either {type: <name>, ...payload} or just a bare string for no-arg processors.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

type: str
class telescope_sim.config.schema.PupilConfig(*, resolution: int, extent: float)[source]

Bases: BaseModel

extent: float
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

resolution: int
class telescope_sim.config.schema.SimConfig(*, pupil: ~telescope_sim.config.schema.PupilConfig, aperture: ~telescope_sim.config.schema.StageConfig, correctors: dict[str, ~telescope_sim.config.schema.CorrectorConfig] = <factory>, corrector_chain: list[str] = <factory>, coronagraph: ~telescope_sim.config.schema.StageConfig | None = None, focal_planes: dict[str, ~telescope_sim.config.schema.StageConfig], outputs: dict[str, ~telescope_sim.config.schema.OutputConfig], strehl_method: ~typing.Literal['peak', 'matched_filter'] | None = None, strehl_core_rad: float | None = None)[source]

Bases: BaseModel

Top-level simulation config.

aperture: StageConfig
coronagraph: StageConfig | None
corrector_chain: list[str]
correctors: dict[str, CorrectorConfig]
focal_planes: dict[str, StageConfig]
model_config = {'arbitrary_types_allowed': True, 'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

outputs: dict[str, OutputConfig]
pupil: PupilConfig
strehl_core_rad: float | None
strehl_method: Literal['peak', 'matched_filter'] | None
class telescope_sim.config.schema.StageConfig(*, type: str, **extra_data: Any)[source]

Bases: BaseModel

Common shape: {type: <name>, ...payload}.

The type field selects which registered implementation to use; everything else is forwarded to its constructor.

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

type: str

YAML loader — parses a config file, validates it, and builds the pipeline.

Resolves stage types via telescope_sim.registry, instantiates the concrete classes, and assembles a telescope_sim.pipeline.TelescopeSim.

Triggering imports of the standard implementations as a side effect of importing this module ensures the registry is populated before any config is resolved.

telescope_sim.config.loader.build(config: SimConfig) TelescopeSim[source]

Instantiate a TelescopeSim from a validated config.

telescope_sim.config.loader.build_from_preset(name: str) TelescopeSim[source]

Resolve a preset name to a packaged YAML and build the pipeline.

telescope_sim.config.loader.build_from_yaml(path: str | Path) TelescopeSim[source]

Convenience: load a YAML file and build the pipeline in one step.

telescope_sim.config.loader.load_yaml(path: str | Path) SimConfig[source]

Load and validate a YAML configuration file.

Concrete implementations

Segmented-circular aperture: N circular sub-apertures arranged on a layout.

Supported layouts:
  • “elf”: ring of sub-apertures at a given radius

  • “custom”: user-provided positions

This is the minimal mini-ELF / DASIE aperture builder the canonical-family fixtures use. Hexagonal / monolithic / external_pupil variants are separate implementations.

class telescope_sim.apertures.segmented_circular.SegmentedCircularAperture(segment_diameter: float, layout: str = 'elf', n_segments: int | None = None, ring_radius: float | None = None, positions: NDArray[floating] | None = None, supersample: int = 16, spider: dict | None = None)[source]

Bases: Aperture

N circular sub-apertures on a configurable layout.

Parameters:
  • segment_diameter (float) – Diameter of each sub-aperture in pupil-grid units.

  • layout (str) – “elf” or “custom”.

  • n_segments (int | None) – Number of segments (required for “elf”).

  • ring_radius (float | None) – Radius of the ELF ring (required for “elf”).

  • positions (numpy._typing._array_like.NDArray[numpy.floating] | None) – Explicit (N, 2) list of (x, y) coordinates (required for “custom”).

  • supersample (int) – Supersampling factor for aperture evaluation (default 16, matching the canonical 2024-09 implementation; older variants used 1).

  • spider (dict | None) – Optional dict {width, angle}. If provided, two perpendicular spiders are multiplied into the aperture mask. angle is in degrees; the second spider is offset by 90°. Mirrors the canonical spider construction.

build(pupil_grid) ApertureResult[source]

Construct the aperture on the given pupil grid.

Parameters:

pupil_grid – HCIPy pupil grid (typically make_pupil_grid(resolution, extent)).

Returns:

The transmission field, collecting area, and optional segmentation.

Return type:

ApertureResult

layout: str = 'elf'
n_segments: int | None = None
positions: NDArray[floating] | None = None
ring_radius: float | None = None
segment_diameter: float = <dataclasses._MISSING_TYPE object>
spider: dict | None = None
supersample: int = 16

ExternalPupilAperture — wraps an arbitrary Python callable as an aperture.

Two calling conventions are supported via the mode field:

  • "field": the function returns an HCIPy Field directly when called with a pupil_grid keyword. Use for callables like miles_pupil. generate_pupil(outer=..., pupil_grid=...) and miles_synthpsf. generate_pupil(...).

  • "callable": the function returns an HCIPy aperture callable when called with whatever kwargs are supplied; the pipeline then evaluates it on the pupil grid via evaluate_supersampled. Use for callables like hcipy.make_keck_aperture() or hcipy.make_circular_aperture(D).

The module is imported via importlib; the path can be either a standard dotted module name (my_package.my_module) or an absolute filesystem path to a .py file (/path/to/file.py).

class telescope_sim.apertures.external_pupil.ExternalPupilAperture(module: str = '', function: str = 'generate_pupil', mode: str = 'field', kwargs: dict[str, ~typing.Any]=<factory>, area: float = 0.0, supersample: int = 16)[source]

Bases: Aperture

Aperture built by an external Python callable.

Parameters:
  • module (str) – Dotted module name or filesystem path to a .py file.

  • function (str) – Name of the callable inside the module.

  • mode (str) – "field" (default) or "callable" — see module docstring.

  • kwargs (dict[str, Any]) – Extra kwargs forwarded to the callable.

  • area (float) – Effective collecting area (used for photon flux scaling). Optional; defaults to 0.0 (only matters for noise computations).

  • supersample (int) – Used only when mode == "callable".

area: float = 0.0
build(pupil_grid: Any) ApertureResult[source]

Construct the aperture on the given pupil grid.

Parameters:

pupil_grid – HCIPy pupil grid (typically make_pupil_grid(resolution, extent)).

Returns:

The transmission field, collecting area, and optional segmentation.

Return type:

ApertureResult

function: str = 'generate_pupil'
kwargs: dict[str, Any] = <dataclasses._MISSING_TYPE object>
mode: str = 'field'
module: str = ''
supersample: int = 16

Segmented piston/tip/tilt corrector — wraps HCIPy’s SegmentedDeformableMirror.

Actuator state has shape (n_segments, 3): piston (meters), tip-slope, tilt-slope per segment. Caller-facing values are scaled by piston_scale and tip_tilt_scale; internally the corrector multiplies through to get the absolute surface deformation HCIPy expects.

This corrector is the primary “actuate” or “impose” element for the canonical-family fixtures (mini-ELF and similar segmented designs). It supports the role-based wavefront / target-strategy system documented on telescope_sim.abc.Corrector.

class telescope_sim.correctors.segmented_ptt.SegmentedPTTCorrector(segments: Any, segment_coords: NDArray[floating], *, name: str = 'segments', piston_scale: float = 1e-06, tip_tilt_scale: float = 1e-06, wavefront_role: Literal['actuate', 'impose', 'fit'] = 'actuate', target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only'] = 'none', fit_source: str | None = None, target: bool = False)[source]

Bases: Corrector

Per-segment piston/tip/tilt actuator on top of HCIPy’s SegmentedDeformableMirror.

Construct from an aperture build result that includes segments (ApertureResult.segments) and segment coordinates.

property actuators: NDArray

Return the caller-facing (n_segments, 3) PTT values.

HCIPy stores actuators in block layout [p_0..p_{n-1}, t_0..t_{n-1}, T_0..T_{n-1}] (not row-major); de-interleave back to the per-segment (p, t, T) rows callers expect.

apply(wf: Any) Any[source]

Apply the segmented mirror’s current state to a wavefront.

fit_surface(phase: NDArray[floating]) NDArray[floating][source]

Per-segment least-squares fit of piston/tip/tilt to a pupil-plane OPD.

Mirrors the canonical v1 _measure_atmos_ptt (see Corrector.fit_surface for the convention). Input is OPD in meters; for each segment, fits OPD p + t_x*x + t_y*y over the segment’s pixels. Returns matching caller-facing PTT — an (n_segments, 3) array divided by piston_scale / tip_tilt_scale and by 2 (surface→OPD round-trip).

The aperture-masked mean of the input is subtracted before the per-segment lstsq (idempotent with the post-fit per-segment mean-piston subtraction below, but keeps the convention uniform with ZernikeCorrector.fit_surface).

flatten() None[source]

Zero all actuators. Default impl calls set_actuators(0)-shaped.

property n_actuators: int
property segment_coords: NDArray[floating]
set_actuators(values: ArrayLike) None[source]

Set actuator state.

Accepts either: - (n_segments, 3) array — caller-facing PTT values, scaled here - flat array of length 3 * n_segments — same content, reshaped

Zernike-basis deformable mirror corrector.

Wraps HCIPy’s DeformableMirror over a Zernike mode basis with the per-mode normalization the VAMPIRES-family variants use (each mode is divided by its peak absolute value so caller-facing actuator amplitudes are in units of max-mode-amplitude × actuate_scale).

Caller-facing actuator state has shape (n_modes,). The corrector multiplies caller values by actuate_scale before handing them to the underlying HCIPy DM.

class telescope_sim.correctors.zernike.ZernikeCorrector(n_modes: int, zernike_diameter: float, *, starting_mode: int = 2, actuate_scale: float = 1.0, name: str = 'zernike_dm', wavefront_role: Literal['actuate', 'impose', 'fit'] = 'actuate', target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only'] = 'none', fit_source: str | None = None, target: bool = False)[source]

Bases: Corrector

Zernike-mode deformable mirror.

Parameters:
  • n_modes – Number of Zernike modes (Noll indexing; starts at starting_mode).

  • zernike_diameter – Diameter (pupil-plane units) over which the Zernike basis is defined.

  • starting_mode – First Noll index in the basis (default 2, skipping piston).

  • actuate_scale – Multiplicative factor applied to caller actuator values before they are written to the underlying HCIPy DM.

property actuators: NDArray
apply(wf: Any) Any[source]

Apply the corrector’s current state to a wavefront in-chain.

fit_surface(phase: NDArray[floating]) NDArray[floating][source]

Project a pupil-plane OPD onto the Zernike basis.

Input is OPD in meters (path-length); output is caller-facing actuator amplitudes that reproduce that OPD as this DM’s surface contribution (matching, not cancellation — see Corrector.fit_surface() for the convention). Modes are peak-normalized; this is the standard diagonal projection, which is exact when the basis is orthogonal over the aperture footprint (e.g. a clean circular aperture).

The / 2.0 is the surface→OPD round-trip factor: setting actuator value v produces surface v * actuate_scale but contributes 2 * v * actuate_scale to the OPD.

Defensive: the aperture-masked mean of the input is subtracted before the lstsq. Uniform OPD offsets are unobservable in the PSF, but a non-zero-mean input (atmosphere phase screen, fit-source corrector surface, etc.) would otherwise be absorbed into non-piston modes, distorting them and polluting any ML target derived from the fit.

flatten() None[source]

Zero all actuators. Default impl calls set_actuators(0)-shaped.

property n_actuators: int
set_actuators(values: ArrayLike) None[source]

Set the corrector’s actuator state.

Actuator-grid deformable mirror corrector.

Wraps HCIPy’s DeformableMirror over an N×N grid of influence functions (make_gaussian_influence_functions or make_xinetics_influence_functions), driven by raw per-actuator commands. DM misalignment relative to the pupil — rotation and mirrored actuator indexing — is baked in at construction so a simulated bench can reproduce the command-to-surface mapping of a physically mounted DM.

Caller-facing actuator state has shape (N²,) (a shaped (N, N) command array is accepted equivalently). The corrector multiplies caller values by actuate_scale before handing them to the underlying HCIPy DM, so callers command in their own units (e.g. the bench’s raw DM units) and actuate_scale carries the units→meters-of-surface calibration.

Command-array and misalignment conventions

  • A shaped (N, N) command indexes the actuator lattice as cmd[iy, ix]: axis 0 walks the y coordinate ascending, axis 1 walks x ascending. Flattening is row-major, matching HCIPy’s actuator-position ordering (x varies fastest). The lattice is centered on the optical axis with spacing actuator_pitch.

  • flip_x / flip_y mirror the command indexing (fliplr / flipud of the shaped command) before it reaches the DM — the simulated analogue of a mirrored cable/mapping between the command array and the physical actuators. The actuators readback un-applies the flips, so callers always round-trip their own values.

  • rotation_deg rotates the influence-function geometry: positive values rotate the DM counterclockwise relative to the pupil (x right, y up) as seen in a plotted surface. Composition order is flip-then-rotate: the command array is mirrored first, then the rotated DM renders it.

class telescope_sim.correctors.actuator_grid.ActuatorGridCorrector(num_actuators: int, actuator_pitch: float, *, influence: str = 'gaussian', crosstalk: float = 0.15, rotation_deg: float = 0.0, flip_x: bool = False, flip_y: bool = False, actuate_scale: float = 1.0, name: str = 'actuator_dm', wavefront_role: Literal['actuate', 'impose', 'fit'] = 'actuate', target_strategy: Literal['none', 'actuators', 'actuators_plus_residual_fit', 'residual_fit_only'] = 'none', fit_source: str | None = None, target: bool = False)[source]

Bases: Corrector

N×N actuator-grid deformable mirror with influence functions.

Parameters:
  • num_actuators – Actuators per side; the DM has num_actuators² total.

  • actuator_pitch – Actuator spacing in pupil-plane projected units (meters for a metric pupil grid).

  • influence – Influence-function model: "gaussian" (Gaussian pokes with nearest-neighbour crosstalk) or "xinetics" (HCIPy’s measured Xinetics actuator shape).

  • crosstalk – Influence-function value at a nearest-neighbour actuator. Only meaningful for influence="gaussian"; ignored for "xinetics" (whose shape is fixed by measurement).

  • rotation_deg – DM orientation relative to the pupil. Positive values rotate the DM counterclockwise (x right, y up) as seen in a plotted surface.

  • flip_x – Mirror the command-array indexing along x (fliplr) / y (flipud) before commands reach the DM. Applied before the rotation (flip-then-rotate).

  • flip_y – Mirror the command-array indexing along x (fliplr) / y (flipud) before commands reach the DM. Applied before the rotation (flip-then-rotate).

  • actuate_scale – Multiplicative factor applied to caller actuator values before they are written to the underlying HCIPy DM (caller units → meters of surface).

property actuators: NDArray
apply(wf: Any) Any[source]

Apply the corrector’s current state to a wavefront in-chain.

fit_surface(phase: NDArray[floating]) NDArray[floating][source]

Least-squares-project a pupil-plane OPD onto the influence basis.

Input is OPD in meters (path-length); output is caller-facing actuator commands that reproduce that OPD as this DM’s surface contribution (matching, not cancellation — see Corrector.fit_surface() for the convention). The fit is an aperture-masked regularized least squares onto the (rotated) influence functions; command flips are un-applied on the way out, so set_actuators(fit_surface(opd)) reproduces the fit regardless of the configured misalignment.

The / 2.0 is the surface→OPD round-trip factor: setting actuator value v produces surface v * actuate_scale but contributes 2 * v * actuate_scale to the OPD.

The aperture-masked mean of the input is subtracted before the fit. A uniform OPD offset is unobservable in the PSF, and unlike a global phase the DM’s approximation of one is not: the influence basis only holds a flat to within a print-through ripple, so fitting the mean would leak mean x ripple into the corrected wavefront and pollute residual-fit ML targets with an arbitrary common-mode term. Piston is therefore never commanded.

flatten() None[source]

Zero all actuators. Default impl calls set_actuators(0)-shaped.

property n_actuators: int
set_actuators(values: ArrayLike) None[source]

Set the corrector’s actuator state.

Standard coronagraph implementations: identity, vortex, vector_vortex.

A coronagraph wraps an HCIPy coronagraph element and applies it after the corrector chain but before propagation. The pipeline always generates the reference PSF (for Strehl normalization) with the coronagraph bypassed.

Each implementation takes an optional lyot Aperture config that specifies the downstream Lyot stop’s transmission field.

class telescope_sim.coronagraphs.standard.IdentityCoronagraph(**_: Any)[source]

Bases: Coronagraph

Passthrough — used as the “no coronagraph” placeholder.

apply(wf: Any) Any[source]

Apply the coronagraph to a pupil-plane wavefront, returning the modified wavefront.

name: str = 'identity'
class telescope_sim.coronagraphs.standard.VectorVortexCoronagraphImpl(charge: int = 4, lyot: dict | None = None)[source]

Bases: Coronagraph

HCIPy VectorVortexCoronagraph at a configurable charge.

apply(wf: Any) Any[source]

Apply the coronagraph to a pupil-plane wavefront, returning the modified wavefront.

name: str = 'vector_vortex'
class telescope_sim.coronagraphs.standard.VortexCoronagraphImpl(charge: int = 2, lyot: dict | None = None)[source]

Bases: Coronagraph

HCIPy VortexCoronagraph at a configurable charge.

The Lyot stop is built from an Aperture sub-config; the coronagraph is constructed lazily once the pipeline binds a pupil grid via _bind_pupil_grid().

apply(wf: Any) Any[source]

Apply the coronagraph to a pupil-plane wavefront, returning the modified wavefront.

name: str = 'vortex'

Angular focal plane: arcsec-extent focal grid with FraunhoferPropagator.

This is the canonical-family focal plane: extent in arcsec, broadband sampling via N monochromatic wavefronts across a fractional bandwidth. The reference PSF (at-rest, no actuators, no atmosphere) is built once at construction time and exposed via reference_psf, reference_peak_intensity, and reference_psf_sum.

Detector noise is currently deferred; we’ll add a NoisyDetector path when the canonical fixtures that exercise it land. The canonical-family fixtures #01/#02/#10/#11 don’t use detectors.

class telescope_sim.focal_planes.angular.AngularFocalPlane(*, central_lam: float, focal_extent: float, focal_res: int, fractional_bandwidth: float = 0.0, num_samples: int = 1, name: str = 'filter')[source]

Bases: FocalPlane

One filter on an arcsec-extent focal grid.

Parameters:
  • central_lam – Central wavelength in meters.

  • focal_extent – Full focal-plane extent in arcsec (square).

  • focal_res – Number of pixels per side (square).

  • fractional_bandwidth – Width of the broadband sampling range, fraction of central_lam.

  • num_samples – Number of monochromatic wavelength samples across the bandwidth.

  • name (str) – Filter name, used as the wavefront name in the chain (e.g. "focal:H_band").

build(pupil_grid: Any, aperture_field: Any) None[source]

Build the propagator, wavefronts, and reference PSF for this filter.

compute_reference_psf(corrector_chain: list[Any]) None[source]

Walk the chain at-rest (no coronagraph) for normalization/strehl.

property lam_setup: _LamSetup
propagate(wf: Any) Any[source]

Propagate a pupil-plane wavefront to this focal plane.

Returns the focal-plane Wavefront (keeping phase info, so downstream taps like fiber coupling can use it).

property reference_peak_intensity: float | None
property reference_psf: NDArray[floating] | None
property reference_psf_sum: float | None

Physical focal plane — focal-plane grid in physical units (meters).

Used when the focal-plane physics depends on absolute spot size rather than angular extent (e.g. fiber coupling, where the MMF mode field is defined in physical coordinates). Construction requires an explicit focal_length so the FraunhoferPropagator can convert between pupil and focal-plane geometries.

This focal plane retains the per-wavelength focal-plane wavefronts so downstream taps (notably fiber_coupled) can use the full complex field rather than just summed intensity.

class telescope_sim.focal_planes.physical.FocalPlaneResult(intensity: NDArray[floating], wavefronts: list[Any])[source]

Bases: object

Per-sample focal-plane output retained for downstream taps.

intensity: NDArray[floating] = <dataclasses._MISSING_TYPE object>
wavefronts: list[Any] = <dataclasses._MISSING_TYPE object>
class telescope_sim.focal_planes.physical.PhysicalFocalPlane(*, central_lam: float, focal_extent: float, focal_res: int, focal_length: float, fractional_bandwidth: float = 0.0, num_samples: int = 1, wavefront_total_power: float | None = None, name: str = 'filter')[source]

Bases: FocalPlane

Focal-plane grid in physical (metres) units.

Parameters:
  • central_lam – Central wavelength in meters.

  • focal_extent – Full focal-plane extent in meters (square).

  • focal_res – Number of pixels per side.

  • focal_length – Optical focal length (meters); passed to FraunhoferPropagator.

  • fractional_bandwidth – Broadband sampling — same semantics as AngularFocalPlane.

  • num_samples – Broadband sampling — same semantics as AngularFocalPlane.

build(pupil_grid: Any, aperture_field: Any) None[source]
compute_reference_psf(corrector_chain: list[Any]) None[source]
property lam_setup: _PhysicalLamSetup
propagate(wf: Any) Any[source]

Propagate a pupil-plane wavefront to this focal plane.

Returns the focal-plane Wavefront (keeping phase info, so downstream taps like fiber coupling can use it).

property reference_peak_intensity: float | None
property reference_psf: NDArray[floating] | None
property reference_psf_sum: float | None

IntensityOutputTap — channels-last stack of per-focal-plane PSF intensities.

Mirrors the canonical-family behaviour where one OutputTap collects the PSF intensities from one or more named focal planes and stacks them along the last axis (shape (H, W, n_focal_planes)).

class telescope_sim.outputs.intensity.IntensityOutputTap(focal_plane_names: list[str], *, name: str = 'psf')[source]

Bases: OutputTap

Stack per-focal-plane PSF intensities along the channel (last) axis.

Parameters:
  • focal_plane_names – One or more named focal planes whose summed intensities feed this tap.

  • name (str) – Output name (used as the key in sample()’s returned images dict).

extract(fp_results: Any, *, overrides: dict[str, Any] | None = None) NDArray[floating][source]

Extract from a dict mapping focal_plane name → FocalPlaneResult.

Uses the summed-intensity field from each focal plane and stacks channels-last so the canonical (H, W, n_filters) layout falls out. overrides is accepted for ABC compatibility and ignored — this tap has no per-sample state.

FiberDualOutputTap — dual-stack focal intensity + fiber-coupled intensity.

For dual-output fiber experiments (e.g. fixture #15): runs each focal-plane wavefront through a multi-mode fiber (HCIPy StepIndexFiber) and produces np.stack([focal_intensity, fiber_intensity]) summed over wavelengths.

Returns shape (2, H, W, 1) matching the legacy fiber variant — first axis is [focal, mmf], last axis is the (single) filter channel.

class telescope_sim.outputs.fiber_dual.FiberDualOutputTap(focal_plane_name: str, fiber: dict, *, name: str = 'fiber_dual')[source]

Bases: OutputTap

Stack focal-plane intensity and fiber-coupled intensity along axis 0.

Parameters:
  • focal_plane_name – Name of the (physical) focal plane to consume.

  • fiber – Sub-config for the fiber: {type, ...kwargs}. Currently only "step_index" is supported.

  • name (str) – Output name (key in sample()['images']).

extract(fp_results: Any, *, overrides: dict[str, Any] | None = None) NDArray[floating][source]

Return a numpy array representing this tap’s view of the wavefront.

Parameters:
  • wf – The tap’s input — typically the fp_results dict mapping focal- plane name to FocalPlaneResult.

  • overrides – Per-sample tap-config overrides from sim.sample(output_overrides=...). Most taps ignore this; taps that have per-sample state (e.g. photon flux for a noisy detector) read named fields here.

Normalization post-processors used by the canonical-family fixtures.

class telescope_sim.post.normalization.ChannelsFirst[source]

Bases: PostProcessor

Transpose (H, W, C)(C, H, W) for PyTorch-style consumers.

name: str = 'channels_first'
class telescope_sim.post.normalization.MaxImageNorm[source]

Bases: PostProcessor

Divide each channel by its own max value.

Mirrors the coronagraph-variant max_im_norm flag — useful when the per-image dynamic range varies a lot (e.g. coronagraph variants where the reference PSF peak isn’t a meaningful normalization target).

name: str = 'max_image_norm'
class telescope_sim.post.normalization.MaxIntensityNorm[source]

Bases: PostProcessor

Divide each channel by the corresponding reference PSF peak intensity.

Mirrors the canonical max_inten_norm flag — the per-filter peak of the at-rest reference PSF is precomputed by the pipeline; this transform divides each channel by that scalar. Channels here = last axis of an (H, W, C) image stack, where C corresponds to the focal planes listed in the parent IntensityOutputTap.

name: str = 'max_intensity_norm'
class telescope_sim.post.normalization.PerSampleNorm[source]

Bases: PostProcessor

Min-max normalize each channel to [0, 1].

name: str = 'per_sample_norm'

NoisyDetector post-processor.

Refactored in v2.0.0a9 from the v2.0.0a7 NoisyIntensityOutputTap. The math is unchanged — single-call Detector.integrate(power_field, dt=1) with the field built from intensity * grid.weights and optionally rescaled to flux * area. The move to a post-processor lets noise compose with the new convolve_image processor (convolve first, then apply noise to the convolved image — physically correct ordering).

Per-sample override flow: the pipeline populates PipelineContext.overrides with output_overrides[output_name] before invoking each post-processor in the chain. This processor reads int_phot_flux from the overrides dict, falling back to the YAML default.

Loader dependencies (focal grid + aperture area) are injected via _bind_loader_dependencies at sim-build time. Eagerly constructs the underlying hcipy.NoisyDetector so its flat_field setter’s np.random.normal(...) call burns RNG state BEFORE any user-level np.random.seed() reset — matches legacy sampler-init timing.

Single-focal-plane only: the post-processor needs one focal grid, so multi-channel outputs that want noise must split into one output per filter.

class telescope_sim.post.noisy_detector.NoisyDetectorPostProcessor(*, int_phot_flux: float | None = None, detector: dict[str, Any] | None = None, clamp_nonnegative: bool = True)[source]

Bases: PostProcessor, LoaderBindable

Apply HCIPy NoisyDetector noise to an intensity image.

Parameters:
  • int_phot_flux – Photon flux in photons/m². When set, rescales the input image so total accumulated charge = int_phot_flux * aperture_area (the legacy _addNoiseToObservation contract). None integrates the input at its natural scale — useful when an upstream convolve_image post-processor has already imposed an absolute brightness.

  • detector – Sub-config forwarded to hcipy.NoisyDetector: read_noise, dark_current_rate, flat_field, include_photon_noise, subsampling.

  • clamp_nonnegative – If True, apply np.abs to the read-out image. Mirrors the legacy _addNoiseToObservation workaround for Gaussian read noise pulling pixels negative. Disable for science-faithful analyses.

name: str = 'noisy_detector'

Convolve-with-image post-processor.

Closes the convolve_im feature gap from the v2.0.0a6 audit.

Legacy behavior (multi_aperture_psf.py:489-491):

if convolve_im is not None:
out_samp = fftconvolve(convolve_im, psf / lam_setup[‘ref_psf_sum’],

mode=’same’)

else:

out_samp = psf

The PSF is normalized by the at-rest reference’s sum (so the kernel integrates to ~1, preserving the input image’s flux scale), then scipy.signal.fftconvolve(image, kernel, mode='same') produces an output with the same shape as the input image.

v2 ships this as a post-processor (rather than a tap option) so it composes cleanly with the new noisy_detector post-processor: intensity convolve noise is the physical chain. Per-sample image override via the PipelineContext.overrides channel.

Single-focal-plane only — the kernel normalization needs a single reference_psf_sum. Multi-channel outputs that want convolution must split into one output per filter.

class telescope_sim.post.convolve.ConvolveImagePostProcessor(*, image: Any = None)[source]

Bases: PostProcessor, LoaderBindable

Convolve a PSF image with a caller-supplied scene.

Parameters:

image – Default scene to convolve with. 2D ndarray. None means no default — the caller must supply convolve_image via output_overrides per sample.

name: str = 'convolve_image'

Strehl ratio estimators — exactly mirroring the legacy _strehl.

Two methods, selected by SimConfig.strehl_method:

  • "peak": legacy psf.flat[ref_psf.argmax()] / ref_psf.max(). Reads the current PSF at the reference PSF’s argmax — so a tip-tilt that moves the peak off-center correctly drops Strehl below 1.

  • "matched_filter": legacy sum(psf_core * ref_core) / sum(ref_core**2). A matched-filter projection over a circular core mask centered on the focal-grid origin. Pixels near the reference peak are upweighted by the reference intensity itself.

The legacy code precomputed all reference-PSF-derived quantities (argmax, mask, weighted sums) once per filter at __init__. We do the same with frozen dataclass estimators built in build_strehl_estimator(). The per-sample compute() is then O(1) for peak and O(core_pixels) for matched-filter — no recomputed argmax or mask.

class telescope_sim.strehl.StrehlEstimator(*args, **kwargs)[source]

Bases: Protocol

Anything with a cached state and a .compute(psf) -> float method.

compute(psf: NDArray[floating]) float[source]
telescope_sim.strehl.build_strehl_estimator(method: str, reference_psf: NDArray[floating], focal_grid: Any, core_radius_rad: float | None) StrehlEstimator[source]

Build the cached estimator for the configured method.

Called once per focal plane at pipeline construction, after the at-rest reference PSF has been captured.

Parameters:
  • method"peak" or "matched_filter".

  • reference_psf – The at-rest reference PSF (full 2D array).

  • focal_grid – The HCIPy focal grid (.x and .y flat coordinate arrays). Required for matched_filter; ignored for peak.

  • core_radius_rad – Radius of the core mask in radians, measured from the focal-grid origin. Required for matched_filter; ignored for peak.

Helpers

Diagnostic plotting helpers for tutorials and interactive use.

These are convenience wrappers — nothing here is on the simulation hot path. Functions here adopt a single visual convention: every PSF panel is paired with the cumulative pupil-plane OPD that produced it, both with colorbars.

telescope_sim.helpers.diagnostics.plot_opd_and_psfs(sim: Any, out: dict[str, Any], *, log_dynamic_range: float = 1e-08, suptitle: str | None = None, figsize: tuple[float, float] | None = None) Any[source]

Plot cumulative pupil OPD (left) and per-filter PSFs (right) side-by-side.

Adopts the recommended diagnostic convention: every PSF is shown next to the wavefront state that produced it. The OPD panel uses hcipy.imshow_field() with the aperture as mask; PSFs use matplotlib.colors.LogNorm so the colorbar labels carry the actual intensity values. Every panel gets a colorbar.

Parameters:
  • simTelescopeSim instance. Used for sim.aperture.field (mask) and sim.focal_planes (filter labels).

  • out – Dict returned by sim.sample(meas_pupil_opd=True). Must contain images["psf"] and pupil_opd. If strehls is also present (i.e. meas_strehl=True), Strehl values annotate the per-filter titles.

  • log_dynamic_range – Ratio of vmin to vmax for the PSF colormap. Default 1e-8 (eight decades of dynamic range).

  • suptitle – Optional figure suptitle.

  • figsize – Override the auto-computed figure size. Default is (4 * (1 + n_filters), 4).

Returns:

The constructed figure (not closed); caller can plt.show() or further customize.

Return type:

matplotlib.figure.Figure

Registry

Tiny dict-of-dicts registry for pluggable pipeline stages.

Usage:

from telescope_sim.registry import register
from telescope_sim.abc import Aperture

@register("aperture", "segmented_circular")
class SegmentedCircularAperture(Aperture):
    ...

cls = registry["aperture"]["segmented_circular"]

Users may register their own implementations in their own packages without modifying telescope-sim.

telescope_sim.registry.available(kind: str) list[str][source]

List registered implementation names for a given kind.

telescope_sim.registry.lookup(kind: str, name: str) type[source]

Resolve a registered implementation, raising KeyError with a helpful message.

telescope_sim.registry.register(kind: str, name: str) Callable[[T], T][source]

Decorator: register a class as a named implementation of a pipeline-stage kind.