Loading csst_msc_instrument/README.md +30 −6 Original line number Diff line number Diff line Loading @@ -29,10 +29,11 @@ The built-in compositions currently pin: - `legacy-v1`: definition set `legacy/1.0.0`; - `next-v1`: definition set `next/1.0.0` with 31 science slots (`00` through `30`) and the existing FGS definitions; - `legacy-v1` pins reference set `1.0.0` for each category; - `next-v1` pins detector responses, filter transmissions, and system throughputs `2.0.0`, while unchanged reference categories remain pinned to `1.0.0`. - `legacy-v1` pins its detector, bandpass, band-scalar optics, and shared sky-background references `1.0.0`; - `next-v1` pins detector responses, filter transmissions, system throughputs, and optics references `2.0.0`, while the shared sky-background and unchanged reference categories remain pinned to `1.0.0`. Released versions should be treated as immutable. Publish changed definitions or reference files under a new version, then add or update a composition Loading Loading @@ -108,6 +109,28 @@ configs = instrument.materialized_detector_reference_paths( ) ``` ### Spectral response contracts Imaging files in `system_throughput` are complete total-system responses. They include telescope optics, filter transmission, and detector QE. Consumers should use the semantic APIs rather than multiplying component files: ```python system = instrument.system_response("i", detector_id="07") optics = instrument.optics_response() order = instrument.slitless_response("GI", "1") ``` `system` is applied directly to imaging spectra. `optics` is an optics-only curve and is applied to slitless calculations only when `optics` is absent from the slitless response's `included_components`. Legacy per-band optics scalars are interpolated at system-derived pivot wavelengths by `optics_response()`, so callers receive the same spectral interface for every release. The legacy slitless sensitivity already includes optics and therefore does not consume that interpolated diagnostic curve. `system_pivot_wavelength()` and `system_response_support()` provide derived diagnostics without introducing separate hard-coded band constants. The simulation selects a built-in composition by default. A custom `release_directory` points to an instrument-data root containing `releases/`, `definition_sets/`, and `reference_sets/`: Loading @@ -125,9 +148,10 @@ instrument_data: Low-level `reference_path()` and `materialized_reference_path()` APIs remain available for explicit user overrides. Main simulation code should use the purpose-based accessors `global_reference_path()`, `detector_reference_path()`, `materialized_global_reference_path()`, `materialized_detector_reference_paths()`, `filter_reference_path()`, and `slitless_throughput_path()`. This keeps physical filenames and category choices inside the versioned definition set rather than `observation_sim`. `slitless_throughput_path()`. This keeps physical filenames and category choices inside the versioned definition set rather than `observation_sim`. ## Validation and schemas Loading csst_msc_instrument/__init__.py +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ from .repository import ( InstrumentRepository, ResolvedChannel, ResolvedDetector, SpectralResponse, get_builtin_repository, ) from .schema import emit_schemas Loading @@ -39,6 +40,7 @@ __all__ = [ "ResolvedChannel", "ResolvedDetector", "ResponseProfileDefinition", "SpectralResponse", "ShutterTiming", "emit_schemas", "get_builtin_repository", Loading csst_msc_instrument/definition_sets/legacy/1.0.0/reference_assignments.yaml +29 −0 Original line number Diff line number Diff line Loading @@ -8,11 +8,38 @@ global_references: telescope_efficiency: group: optics file: mirror_ccdnote.txt response_sampling: band_scalar value_unit: dimensionless default_sky_spectrum: group: sky_background file: sky_emiss_hubble_50_50_A.dat wavelength_unit: angstrom value_unit: photon / (s m2 arcsec2 angstrom) bright_star_catalog: group: sky_background file: bright_star_runtime_g10.fits bright_star_sed_library: group: sky_background file: bright_star_seds_g10.hdf5 straylight_pst: group: sky_background file: PST earth_albedo_map: group: sky_background file: R earthshine_spectrum: group: sky_background file: earthshine_spectrum.dat wavelength_unit: angstrom value_unit: relative / angstrom zodiacal_map: group: sky_background file: Zodiacal zodiacal_spectrum: group: sky_background file: zodiacal_spectrum.dat wavelength_unit: angstrom value_unit: erg / (s cm2 arcsec2 angstrom) detector_defaults: field_distortion: Loading Loading @@ -125,6 +152,8 @@ filter_references: throughput: {group: system_throughput, file: fgs_throughput.txt, wavelength_unit: angstrom} subbands: {group: filter_transmission, file: fgs_sub.list, wavelength_unit: angstrom} slitless_included_components: [optics, grating, detector_qe] slitless_throughputs: GU: "-2": {group: slitless_spectroscopy, file: GU.Throughput.-2st.fits, wavelength_unit: angstrom} Loading csst_msc_instrument/definition_sets/next/1.0.0/reference_assignments.yaml +31 −1 Original line number Diff line number Diff line Loading @@ -7,12 +7,40 @@ global_references: file: wfc-cr-attachpixel.dat telescope_efficiency: group: optics file: mirror_ccdnote.txt file: mirrors_total.txt response_sampling: spectral wavelength_unit: nm value_unit: dimensionless default_sky_spectrum: group: sky_background file: sky_emiss_hubble_50_50_A.dat wavelength_unit: angstrom value_unit: photon / (s m2 arcsec2 angstrom) bright_star_catalog: group: sky_background file: bright_star_runtime_g10.fits bright_star_sed_library: group: sky_background file: bright_star_seds_g10.hdf5 straylight_pst: group: sky_background file: PST earth_albedo_map: group: sky_background file: R earthshine_spectrum: group: sky_background file: earthshine_spectrum.dat wavelength_unit: angstrom value_unit: relative / angstrom zodiacal_map: group: sky_background file: Zodiacal zodiacal_spectrum: group: sky_background file: zodiacal_spectrum.dat wavelength_unit: angstrom value_unit: erg / (s cm2 arcsec2 angstrom) detector_defaults: field_distortion: Loading Loading @@ -147,6 +175,8 @@ response_profiles: wi1: {filter: wi, throughput: {group: system_throughput, file: WI1_throughput.txt, wavelength_unit: nm}, includes_detector_qe: true} wi2: {filter: wi, throughput: {group: system_throughput, file: WI2_throughput.txt, wavelength_unit: nm}, includes_detector_qe: true} slitless_included_components: [grating, detector_qe] slitless_throughputs: GU: "-2": {group: slitless_spectroscopy, file: GU.Throughput.-2st.fits, wavelength_unit: angstrom} Loading csst_msc_instrument/models.py +76 −18 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ Version = Annotated[ ) ), ] def _validate_relative_path(value: str) -> str: path = PurePosixPath(value) if not value or path.is_absolute() or ".." in path.parts: Loading Loading @@ -367,8 +369,7 @@ class FilterCatalog(StrictModel): _validate_path_segment(filter_id) if definition.display_name.lower() != filter_id: raise ValueError( "filter display names must match their canonical keys " "ignoring case" "filter display names must match their canonical keys ignoring case" ) display_names = [value.display_name.lower() for value in self.filters.values()] legacy_ids = [value.legacy_id for value in self.filters.values()] Loading @@ -395,6 +396,7 @@ ReferenceGroupName = Literal[ class ReferenceTarget(StrictModel): group: ReferenceGroupName file: str response_sampling: Literal["spectral", "band_scalar"] | None = None wavelength_unit: Literal["nm", "angstrom"] | None = None value_unit: str | None = Field(default=None, min_length=1) Loading Loading @@ -425,7 +427,32 @@ class ResponseProfileDefinition(StrictModel): filter: str throughput: ReferenceTarget transmission: ReferenceTarget | None = None includes_detector_qe: bool = False response_kind: Literal["total_system"] = "total_system" included_components: tuple[Literal["optics", "filter", "detector_qe"], ...] = ( "optics", "filter", "detector_qe", ) includes_detector_qe: bool = True @field_validator("included_components", mode="before") @classmethod def freeze_included_components(cls, value): return tuple(value) @model_validator(mode="after") def validate_total_system_components(self) -> "ResponseProfileDefinition": required = {"optics", "filter", "detector_qe"} if set(self.included_components) != required: raise ValueError( "total-system response profiles must include optics, filter, " "and detector_qe" ) if not self.includes_detector_qe: raise ValueError( "total-system response profiles must set includes_detector_qe=true" ) return self class ReferenceAssignmentsCatalog(StrictModel): Loading @@ -437,17 +464,50 @@ class ReferenceAssignmentsCatalog(StrictModel): response_profiles: dict[str, ResponseProfileDefinition] = Field( default_factory=dict ) slitless_included_components: tuple[ Literal["optics", "grating", "detector_qe"], ... ] = ("grating", "detector_qe") slitless_throughputs: dict[str, dict[str, ReferenceTarget]] @field_validator("slitless_included_components", mode="before") @classmethod def freeze_slitless_components(cls, value): return tuple(value) @model_validator(mode="after") def validate_spectral_wavelength_units(self) -> "ReferenceAssignmentsCatalog": spectral_targets: list[tuple[str, ReferenceTarget]] = [] sky_target = self.global_references.get("default_sky_spectrum") if sky_target is not None: spectral_targets.append(("global default_sky_spectrum", sky_target)) for role in ( "default_sky_spectrum", "earthshine_spectrum", "zodiacal_spectrum", ): sky_target = self.global_references.get(role) if sky_target is None: continue spectral_targets.append((f"global {role}", sky_target)) if sky_target.value_unit is None: raise ValueError(f"global {role} is missing its value unit") telescope_target = self.global_references.get("telescope_efficiency") if telescope_target is not None: sampling = telescope_target.response_sampling if sampling is None: raise ValueError( "global telescope_efficiency is missing response_sampling" ) if telescope_target.value_unit is None: raise ValueError( "global telescope_efficiency is missing its value unit" ) if sampling == "spectral": spectral_targets.append(( "global telescope_efficiency", telescope_target, )) elif telescope_target.wavelength_unit is not None: raise ValueError( "global default_sky_spectrum is missing its value unit" "band-scalar telescope_efficiency must not declare a " "wavelength unit" ) for filter_id, assignments in self.filter_references.items(): for role in ("transmission", "throughput", "subbands"): Loading @@ -455,16 +515,15 @@ class ReferenceAssignmentsCatalog(StrictModel): if target is not None: spectral_targets.append((f"filter {filter_id} {role}", target)) for profile_id, profile in self.response_profiles.items(): spectral_targets.append( (f"response profile {profile_id} throughput", profile.throughput) ) spectral_targets.append(( f"response profile {profile_id} throughput", profile.throughput, )) if profile.transmission is not None: spectral_targets.append( ( spectral_targets.append(( f"response profile {profile_id} transmission", profile.transmission, ) ) )) for grating, order_map in self.slitless_throughputs.items(): spectral_targets.extend( (f"slitless throughput {grating} order {order}", target) Loading @@ -478,8 +537,7 @@ class ReferenceAssignmentsCatalog(StrictModel): ] if missing_units: raise ValueError( "spectral references are missing wavelength units: " f"{missing_units}" f"spectral references are missing wavelength units: {missing_units}" ) return self Loading Loading
csst_msc_instrument/README.md +30 −6 Original line number Diff line number Diff line Loading @@ -29,10 +29,11 @@ The built-in compositions currently pin: - `legacy-v1`: definition set `legacy/1.0.0`; - `next-v1`: definition set `next/1.0.0` with 31 science slots (`00` through `30`) and the existing FGS definitions; - `legacy-v1` pins reference set `1.0.0` for each category; - `next-v1` pins detector responses, filter transmissions, and system throughputs `2.0.0`, while unchanged reference categories remain pinned to `1.0.0`. - `legacy-v1` pins its detector, bandpass, band-scalar optics, and shared sky-background references `1.0.0`; - `next-v1` pins detector responses, filter transmissions, system throughputs, and optics references `2.0.0`, while the shared sky-background and unchanged reference categories remain pinned to `1.0.0`. Released versions should be treated as immutable. Publish changed definitions or reference files under a new version, then add or update a composition Loading Loading @@ -108,6 +109,28 @@ configs = instrument.materialized_detector_reference_paths( ) ``` ### Spectral response contracts Imaging files in `system_throughput` are complete total-system responses. They include telescope optics, filter transmission, and detector QE. Consumers should use the semantic APIs rather than multiplying component files: ```python system = instrument.system_response("i", detector_id="07") optics = instrument.optics_response() order = instrument.slitless_response("GI", "1") ``` `system` is applied directly to imaging spectra. `optics` is an optics-only curve and is applied to slitless calculations only when `optics` is absent from the slitless response's `included_components`. Legacy per-band optics scalars are interpolated at system-derived pivot wavelengths by `optics_response()`, so callers receive the same spectral interface for every release. The legacy slitless sensitivity already includes optics and therefore does not consume that interpolated diagnostic curve. `system_pivot_wavelength()` and `system_response_support()` provide derived diagnostics without introducing separate hard-coded band constants. The simulation selects a built-in composition by default. A custom `release_directory` points to an instrument-data root containing `releases/`, `definition_sets/`, and `reference_sets/`: Loading @@ -125,9 +148,10 @@ instrument_data: Low-level `reference_path()` and `materialized_reference_path()` APIs remain available for explicit user overrides. Main simulation code should use the purpose-based accessors `global_reference_path()`, `detector_reference_path()`, `materialized_global_reference_path()`, `materialized_detector_reference_paths()`, `filter_reference_path()`, and `slitless_throughput_path()`. This keeps physical filenames and category choices inside the versioned definition set rather than `observation_sim`. `slitless_throughput_path()`. This keeps physical filenames and category choices inside the versioned definition set rather than `observation_sim`. ## Validation and schemas Loading
csst_msc_instrument/__init__.py +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ from .repository import ( InstrumentRepository, ResolvedChannel, ResolvedDetector, SpectralResponse, get_builtin_repository, ) from .schema import emit_schemas Loading @@ -39,6 +40,7 @@ __all__ = [ "ResolvedChannel", "ResolvedDetector", "ResponseProfileDefinition", "SpectralResponse", "ShutterTiming", "emit_schemas", "get_builtin_repository", Loading
csst_msc_instrument/definition_sets/legacy/1.0.0/reference_assignments.yaml +29 −0 Original line number Diff line number Diff line Loading @@ -8,11 +8,38 @@ global_references: telescope_efficiency: group: optics file: mirror_ccdnote.txt response_sampling: band_scalar value_unit: dimensionless default_sky_spectrum: group: sky_background file: sky_emiss_hubble_50_50_A.dat wavelength_unit: angstrom value_unit: photon / (s m2 arcsec2 angstrom) bright_star_catalog: group: sky_background file: bright_star_runtime_g10.fits bright_star_sed_library: group: sky_background file: bright_star_seds_g10.hdf5 straylight_pst: group: sky_background file: PST earth_albedo_map: group: sky_background file: R earthshine_spectrum: group: sky_background file: earthshine_spectrum.dat wavelength_unit: angstrom value_unit: relative / angstrom zodiacal_map: group: sky_background file: Zodiacal zodiacal_spectrum: group: sky_background file: zodiacal_spectrum.dat wavelength_unit: angstrom value_unit: erg / (s cm2 arcsec2 angstrom) detector_defaults: field_distortion: Loading Loading @@ -125,6 +152,8 @@ filter_references: throughput: {group: system_throughput, file: fgs_throughput.txt, wavelength_unit: angstrom} subbands: {group: filter_transmission, file: fgs_sub.list, wavelength_unit: angstrom} slitless_included_components: [optics, grating, detector_qe] slitless_throughputs: GU: "-2": {group: slitless_spectroscopy, file: GU.Throughput.-2st.fits, wavelength_unit: angstrom} Loading
csst_msc_instrument/definition_sets/next/1.0.0/reference_assignments.yaml +31 −1 Original line number Diff line number Diff line Loading @@ -7,12 +7,40 @@ global_references: file: wfc-cr-attachpixel.dat telescope_efficiency: group: optics file: mirror_ccdnote.txt file: mirrors_total.txt response_sampling: spectral wavelength_unit: nm value_unit: dimensionless default_sky_spectrum: group: sky_background file: sky_emiss_hubble_50_50_A.dat wavelength_unit: angstrom value_unit: photon / (s m2 arcsec2 angstrom) bright_star_catalog: group: sky_background file: bright_star_runtime_g10.fits bright_star_sed_library: group: sky_background file: bright_star_seds_g10.hdf5 straylight_pst: group: sky_background file: PST earth_albedo_map: group: sky_background file: R earthshine_spectrum: group: sky_background file: earthshine_spectrum.dat wavelength_unit: angstrom value_unit: relative / angstrom zodiacal_map: group: sky_background file: Zodiacal zodiacal_spectrum: group: sky_background file: zodiacal_spectrum.dat wavelength_unit: angstrom value_unit: erg / (s cm2 arcsec2 angstrom) detector_defaults: field_distortion: Loading Loading @@ -147,6 +175,8 @@ response_profiles: wi1: {filter: wi, throughput: {group: system_throughput, file: WI1_throughput.txt, wavelength_unit: nm}, includes_detector_qe: true} wi2: {filter: wi, throughput: {group: system_throughput, file: WI2_throughput.txt, wavelength_unit: nm}, includes_detector_qe: true} slitless_included_components: [grating, detector_qe] slitless_throughputs: GU: "-2": {group: slitless_spectroscopy, file: GU.Throughput.-2st.fits, wavelength_unit: angstrom} Loading
csst_msc_instrument/models.py +76 −18 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ Version = Annotated[ ) ), ] def _validate_relative_path(value: str) -> str: path = PurePosixPath(value) if not value or path.is_absolute() or ".." in path.parts: Loading Loading @@ -367,8 +369,7 @@ class FilterCatalog(StrictModel): _validate_path_segment(filter_id) if definition.display_name.lower() != filter_id: raise ValueError( "filter display names must match their canonical keys " "ignoring case" "filter display names must match their canonical keys ignoring case" ) display_names = [value.display_name.lower() for value in self.filters.values()] legacy_ids = [value.legacy_id for value in self.filters.values()] Loading @@ -395,6 +396,7 @@ ReferenceGroupName = Literal[ class ReferenceTarget(StrictModel): group: ReferenceGroupName file: str response_sampling: Literal["spectral", "band_scalar"] | None = None wavelength_unit: Literal["nm", "angstrom"] | None = None value_unit: str | None = Field(default=None, min_length=1) Loading Loading @@ -425,7 +427,32 @@ class ResponseProfileDefinition(StrictModel): filter: str throughput: ReferenceTarget transmission: ReferenceTarget | None = None includes_detector_qe: bool = False response_kind: Literal["total_system"] = "total_system" included_components: tuple[Literal["optics", "filter", "detector_qe"], ...] = ( "optics", "filter", "detector_qe", ) includes_detector_qe: bool = True @field_validator("included_components", mode="before") @classmethod def freeze_included_components(cls, value): return tuple(value) @model_validator(mode="after") def validate_total_system_components(self) -> "ResponseProfileDefinition": required = {"optics", "filter", "detector_qe"} if set(self.included_components) != required: raise ValueError( "total-system response profiles must include optics, filter, " "and detector_qe" ) if not self.includes_detector_qe: raise ValueError( "total-system response profiles must set includes_detector_qe=true" ) return self class ReferenceAssignmentsCatalog(StrictModel): Loading @@ -437,17 +464,50 @@ class ReferenceAssignmentsCatalog(StrictModel): response_profiles: dict[str, ResponseProfileDefinition] = Field( default_factory=dict ) slitless_included_components: tuple[ Literal["optics", "grating", "detector_qe"], ... ] = ("grating", "detector_qe") slitless_throughputs: dict[str, dict[str, ReferenceTarget]] @field_validator("slitless_included_components", mode="before") @classmethod def freeze_slitless_components(cls, value): return tuple(value) @model_validator(mode="after") def validate_spectral_wavelength_units(self) -> "ReferenceAssignmentsCatalog": spectral_targets: list[tuple[str, ReferenceTarget]] = [] sky_target = self.global_references.get("default_sky_spectrum") if sky_target is not None: spectral_targets.append(("global default_sky_spectrum", sky_target)) for role in ( "default_sky_spectrum", "earthshine_spectrum", "zodiacal_spectrum", ): sky_target = self.global_references.get(role) if sky_target is None: continue spectral_targets.append((f"global {role}", sky_target)) if sky_target.value_unit is None: raise ValueError(f"global {role} is missing its value unit") telescope_target = self.global_references.get("telescope_efficiency") if telescope_target is not None: sampling = telescope_target.response_sampling if sampling is None: raise ValueError( "global telescope_efficiency is missing response_sampling" ) if telescope_target.value_unit is None: raise ValueError( "global telescope_efficiency is missing its value unit" ) if sampling == "spectral": spectral_targets.append(( "global telescope_efficiency", telescope_target, )) elif telescope_target.wavelength_unit is not None: raise ValueError( "global default_sky_spectrum is missing its value unit" "band-scalar telescope_efficiency must not declare a " "wavelength unit" ) for filter_id, assignments in self.filter_references.items(): for role in ("transmission", "throughput", "subbands"): Loading @@ -455,16 +515,15 @@ class ReferenceAssignmentsCatalog(StrictModel): if target is not None: spectral_targets.append((f"filter {filter_id} {role}", target)) for profile_id, profile in self.response_profiles.items(): spectral_targets.append( (f"response profile {profile_id} throughput", profile.throughput) ) spectral_targets.append(( f"response profile {profile_id} throughput", profile.throughput, )) if profile.transmission is not None: spectral_targets.append( ( spectral_targets.append(( f"response profile {profile_id} transmission", profile.transmission, ) ) )) for grating, order_map in self.slitless_throughputs.items(): spectral_targets.extend( (f"slitless throughput {grating} order {order}", target) Loading @@ -478,8 +537,7 @@ class ReferenceAssignmentsCatalog(StrictModel): ] if missing_units: raise ValueError( "spectral references are missing wavelength units: " f"{missing_units}" f"spectral references are missing wavelength units: {missing_units}" ) return self Loading