"""Pydantic v2 schemas for the telescope-sim configuration.
This module exposes the top-level :class:`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.
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
[docs]
class StageConfig(BaseModel):
"""Common shape: ``{type: <name>, ...payload}``.
The ``type`` field selects which registered implementation to use;
everything else is forwarded to its constructor.
"""
model_config = ConfigDict(extra="allow")
type: str
[docs]
class CorrectorConfig(StageConfig):
"""A corrector + its role/target-strategy settings."""
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
[docs]
class PostProcessorConfig(BaseModel):
"""Either ``{type: <name>, ...payload}`` or just a bare string for no-arg processors."""
model_config = ConfigDict(extra="allow")
type: str
[docs]
class OutputConfig(BaseModel):
"""One named output: a tap plus its ordered post-processing list."""
model_config = ConfigDict(extra="forbid")
tap: StageConfig
post_processing: list[StageConfig | str] = Field(default_factory=list)
[docs]
class PupilConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
resolution: int
extent: float
[docs]
class SimConfig(BaseModel):
"""Top-level simulation config."""
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
pupil: PupilConfig
aperture: StageConfig
correctors: dict[str, CorrectorConfig] = Field(default_factory=dict)
corrector_chain: list[str] = Field(default_factory=list)
coronagraph: StageConfig | None = None
focal_planes: dict[str, StageConfig]
outputs: dict[str, OutputConfig]
strehl_method: Literal["peak", "matched_filter"] | None = None
strehl_core_rad: float | None = None
@model_validator(mode="after")
def _resolve_strehl(self) -> SimConfig:
# Back-compat: if the user set strehl_core_rad but not strehl_method,
# promote to matched_filter (the old "core_rad is not None" branch).
# Otherwise default to peak.
if self.strehl_method is None:
self.strehl_method = "matched_filter" if self.strehl_core_rad is not None else "peak"
if self.strehl_method == "matched_filter" and (
self.strehl_core_rad is None or self.strehl_core_rad <= 0
):
raise ValueError("strehl_method='matched_filter' requires a positive strehl_core_rad")
return self
__all__ = [
"SimConfig",
"StageConfig",
"CorrectorConfig",
"PostProcessorConfig",
"OutputConfig",
"PupilConfig",
]