From d006edb140fd78d9f87b2e458fe154361ad716ea Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 2 Oct 2025 05:10:20 +0000 Subject: [PATCH 1/4] Add in support for pulling out extra params fields defined in instrument_type validation_schema for OCS --- README.md | 6 +- codegen/lco/generator.py | 11 ++++ codegen/lco/templates/instruments.jinja | 29 +++++++-- src/aeonlib/ocs/blanco/instruments.py | 25 ++++++-- src/aeonlib/ocs/lco/instruments.py | 78 ++++++++++++++++++++----- src/aeonlib/ocs/soar/instruments.py | 75 +++++++++++++++++++----- 6 files changed, 185 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 62d4f08..ce016aa 100644 --- a/README.md +++ b/README.md @@ -90,20 +90,20 @@ This ensures regular users of the library do not need to install these dependenc The `generate.py` script takes as input JSON as produced by the instruments endpoint: ```bash -codegen/lco/generator.py instruments.json +codegen/lco/generator.py {facility} instruments.json ``` Or directly from stdin using a pipe: ```bash -curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py +curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py {facility} ``` If the output looks satisfactory, you can redirect the output to overwrite the LCO instruments definition file: ```bash -curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py > src/aeonlib/ocs/lco/instruments.py +curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py {facility} > src/aeonlib/ocs/lco/instruments.py ``` # Supported Facilities diff --git a/codegen/lco/generator.py b/codegen/lco/generator.py index ac31584..c2aaec5 100755 --- a/codegen/lco/generator.py +++ b/codegen/lco/generator.py @@ -11,6 +11,14 @@ VALID_FACILITIES = ["SOAR", "LCO", "SAAO", "BLANCO"] +def extract_default(properties): + if properties['type'] == 'integer' or properties['type'] == 'float' or properties['type'] == 'boolean': + return properties['default'] + elif properties['type'] == 'string': + return f'"{properties['default']}"' + return Any + + def get_modes(ins: dict[str, Any], type: str) -> list[str]: try: return [m["code"] for m in ins["modes"][type]["modes"]] @@ -37,6 +45,7 @@ def generate_instrument_configs(ins_s: str, facility: str) -> str: trim_blocks=True, lstrip_blocks=True, ) + j_env.filters['extract_default'] = extract_default template = j_env.get_template("instruments.jinja") ins_data = json.loads(ins_s) instruments: list[dict[str, Any]] = [] @@ -84,6 +93,8 @@ def generate_instrument_configs(ins_s: str, facility: str) -> str: k.rstrip("s"): v for k, v in ins["optical_elements"].items() }, + "configuration_extra_params": ins['validation_schema'].get('extra_params', {}).get('schema', {}), + "instrument_config_extra_params": ins['validation_schema'].get('instrument_configs', {}).get('schema', {}).get('schema', {}).get('extra_params', {}).get('schema', {}) } ) diff --git a/codegen/lco/templates/instruments.jinja b/codegen/lco/templates/instruments.jinja index d36e42e..34d4774 100644 --- a/codegen/lco/templates/instruments.jinja +++ b/codegen/lco/templates/instruments.jinja @@ -1,10 +1,10 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal +from typing import Any, Annotated, Literal, Optional -from annotated_types import Le -from pydantic import BaseModel, ConfigDict +from annotated_types import Le, Ge +from pydantic import BaseModel, ConfigDict, Field from pydantic.types import NonNegativeInt, PositiveInt from aeonlib.models import TARGET_TYPES @@ -13,6 +13,25 @@ from aeonlib.ocs.config_models import Roi {% for ctx in instruments %} + +class {{ ctx.class_name}}ConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + {% for field, properties in ctx.configuration_extra_params.items() %} + {% set optional = ('required' not in properties or not properties.required) and 'default' not in properties %} + {{ field }}: {% if optional %}Optional[{% endif %}{% if 'allowed' in properties %}Literal[{{ properties.allowed }}]{% else %}Annotated[{% if properties.type == 'string' %}str{% elif properties.type == 'integer' %}int{% elif properties.type == 'float' %}float{% elif properties.type == 'boolean' %}bool{% endif %}{% if 'min' in properties %}, Ge({{ properties.min }}){% endif %}{% if 'max' in properties %}, Le({{ properties.max }}){% endif %}]{% endif %}{% if optional %}] = None{% elif 'default' in properties %} = {{ properties | extract_default }}{% endif %} + + {% endfor %} + + +class {{ ctx.class_name}}InstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + {% for field, properties in ctx.instrument_config_extra_params.items() %} + {% set optional = ('required' not in properties or not properties.required) and 'default' not in properties %} + {{ field }}: {% if optional %}Optional[{% endif %}{% if 'allowed' in properties %}Literal[{{ properties.allowed }}]{% else %}Annotated[{% if properties.type == 'string' %}str{% elif properties.type == 'integer' %}int{% elif properties.type == 'float' %}float{% elif properties.type == 'boolean' %}bool{% endif %}{% if 'min' in properties %}, Ge({{ properties.min }}){% endif %}{% if 'max' in properties %}, Le({{ properties.max }}){% endif %}]{% endif %}{% if optional %}] = None{% elif 'default' in properties %} = {{ properties | extract_default }}{% endif %} + + {% endfor %} + + class {{ ctx.class_name }}OpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) {% for key, values in ctx.optical_elements.items() %} @@ -49,7 +68,7 @@ class {{ ctx.class_name }}Config(BaseModel): rotator_mode: Literal[{% for m in ctx.rotator_modes %}"{{ m }}"{% if not loop.last %}, {% endif %}{% endfor %}] {% endif %} rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: {{ ctx.class_name }}InstrumentConfigExtraParams = Field(default_factory={{ ctx.class_name }}InstrumentConfigExtraParams) optical_elements: {{ ctx.class_name}}OpticalElements @@ -58,7 +77,7 @@ class {{ ctx.class_name }}(BaseModel): type: Literal[{% for t in ctx.config_types %}"{{ t }}"{% if not loop.last %}, {% endif %}{% endfor %}] instrument_type: Literal["{{ ctx.instrument_type }}"] = "{{ ctx.instrument_type }}" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: {{ ctx.class_name }}ConfigExtraParams = Field(default_factory={{ ctx.class_name }}ConfigExtraParams) instrument_configs: list[{{ ctx.class_name }}Config] = [] acquisition_config: {{ ctx.class_name }}AcquisitionConfig guiding_config: {{ ctx.class_name }}GuidingConfig diff --git a/src/aeonlib/ocs/blanco/instruments.py b/src/aeonlib/ocs/blanco/instruments.py index 35882d5..75b9e36 100644 --- a/src/aeonlib/ocs/blanco/instruments.py +++ b/src/aeonlib/ocs/blanco/instruments.py @@ -1,10 +1,10 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal +from typing import Any, Annotated, Literal, Optional -from annotated_types import Le -from pydantic import BaseModel, ConfigDict +from annotated_types import Le, Ge +from pydantic import BaseModel, ConfigDict, Field from pydantic.types import NonNegativeInt, PositiveInt from aeonlib.models import TARGET_TYPES @@ -12,6 +12,21 @@ from aeonlib.ocs.config_models import Roi + +class BlancoNewfirmConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + dither_value: Annotated[int, Ge(0), Le(1600)] = 80 + dither_sequence: Literal[['2x2', '3x3', '4x4', '5-point']] = "2x2" + detector_centering: Literal[['none', 'det_1', 'det_2', 'det_3', 'det_4']] = "det_1" + dither_sequence_random_offset: Literal[[True, False]] = True + + +class BlancoNewfirmInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + coadds: Annotated[int, Ge(1), Le(100)] = 1 + sequence_repeats: Annotated[int, Ge(1), Le(500)] = 1 + + class BlancoNewfirmOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) filter: Literal["JX", "HX", "KXs"] @@ -43,7 +58,7 @@ class BlancoNewfirmConfig(BaseModel): """ Exposure time in seconds""" mode: Literal["fowler1", "fowler2"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: BlancoNewfirmInstrumentConfigExtraParams = Field(default_factory=BlancoNewfirmInstrumentConfigExtraParams) optical_elements: BlancoNewfirmOpticalElements @@ -52,7 +67,7 @@ class BlancoNewfirm(BaseModel): type: Literal["EXPOSE", "SKY_FLAT", "STANDARD", "DARK"] instrument_type: Literal["BLANCO_NEWFIRM"] = "BLANCO_NEWFIRM" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: BlancoNewfirmConfigExtraParams = Field(default_factory=BlancoNewfirmConfigExtraParams) instrument_configs: list[BlancoNewfirmConfig] = [] acquisition_config: BlancoNewfirmAcquisitionConfig guiding_config: BlancoNewfirmGuidingConfig diff --git a/src/aeonlib/ocs/lco/instruments.py b/src/aeonlib/ocs/lco/instruments.py index ccb9c8a..66d4055 100644 --- a/src/aeonlib/ocs/lco/instruments.py +++ b/src/aeonlib/ocs/lco/instruments.py @@ -1,10 +1,10 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal +from typing import Any, Annotated, Literal, Optional -from annotated_types import Le -from pydantic import BaseModel, ConfigDict +from annotated_types import Le, Ge +from pydantic import BaseModel, ConfigDict, Field from pydantic.types import NonNegativeInt, PositiveInt from aeonlib.models import TARGET_TYPES @@ -12,6 +12,18 @@ from aeonlib.ocs.config_models import Roi + +class Lco0M4ScicamQhy600ConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + sub_expose: Literal[[False, True]] = False + sub_exposure_time: Optional[Annotated[float, Ge(15.0)]] = None + + +class Lco0M4ScicamQhy600InstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class Lco0M4ScicamQhy600OpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) filter: Literal["OIII", "SII", "Astrodon-Exo", "w", "opaque", "up", "rp", "ip", "gp", "zs", "V", "B", "H-Alpha"] @@ -43,7 +55,7 @@ class Lco0M4ScicamQhy600Config(BaseModel): """ Exposure time in seconds""" mode: Literal["central30x30", "full_frame"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco0M4ScicamQhy600InstrumentConfigExtraParams = Field(default_factory=Lco0M4ScicamQhy600InstrumentConfigExtraParams) optical_elements: Lco0M4ScicamQhy600OpticalElements @@ -52,7 +64,7 @@ class Lco0M4ScicamQhy600(BaseModel): type: Literal["EXPOSE", "REPEAT_EXPOSE", "AUTO_FOCUS", "BIAS", "DARK", "STANDARD", "SKY_FLAT"] instrument_type: Literal["0M4-SCICAM-QHY600"] = "0M4-SCICAM-QHY600" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco0M4ScicamQhy600ConfigExtraParams = Field(default_factory=Lco0M4ScicamQhy600ConfigExtraParams) instrument_configs: list[Lco0M4ScicamQhy600Config] = [] acquisition_config: Lco0M4ScicamQhy600AcquisitionConfig guiding_config: Lco0M4ScicamQhy600GuidingConfig @@ -65,6 +77,16 @@ class Lco0M4ScicamQhy600(BaseModel): optical_elements_class = Lco0M4ScicamQhy600OpticalElements + +class Lco1M0NresScicamConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class Lco1M0NresScicamInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class Lco1M0NresScicamOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) @@ -95,7 +117,7 @@ class Lco1M0NresScicamConfig(BaseModel): """ Exposure time in seconds""" mode: Literal["default"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco1M0NresScicamInstrumentConfigExtraParams = Field(default_factory=Lco1M0NresScicamInstrumentConfigExtraParams) optical_elements: Lco1M0NresScicamOpticalElements @@ -104,7 +126,7 @@ class Lco1M0NresScicam(BaseModel): type: Literal["NRES_SPECTRUM", "REPEAT_NRES_SPECTRUM", "NRES_EXPOSE", "NRES_TEST", "SCRIPT", "ENGINEERING", "ARC", "LAMP_FLAT", "NRES_BIAS", "NRES_DARK", "AUTO_FOCUS"] instrument_type: Literal["1M0-NRES-SCICAM"] = "1M0-NRES-SCICAM" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco1M0NresScicamConfigExtraParams = Field(default_factory=Lco1M0NresScicamConfigExtraParams) instrument_configs: list[Lco1M0NresScicamConfig] = [] acquisition_config: Lco1M0NresScicamAcquisitionConfig guiding_config: Lco1M0NresScicamGuidingConfig @@ -117,6 +139,16 @@ class Lco1M0NresScicam(BaseModel): optical_elements_class = Lco1M0NresScicamOpticalElements + +class Lco1M0ScicamSinistroConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class Lco1M0ScicamSinistroInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class Lco1M0ScicamSinistroOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) filter: Literal["I", "R", "U", "w", "Y", "up", "rp", "ip", "gp", "zs", "V", "B", "400um-Pinhole", "150um-Pinhole", "CN"] @@ -148,7 +180,7 @@ class Lco1M0ScicamSinistroConfig(BaseModel): """ Exposure time in seconds""" mode: Literal["full_frame", "central_2k_2x2"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco1M0ScicamSinistroInstrumentConfigExtraParams = Field(default_factory=Lco1M0ScicamSinistroInstrumentConfigExtraParams) optical_elements: Lco1M0ScicamSinistroOpticalElements @@ -157,7 +189,7 @@ class Lco1M0ScicamSinistro(BaseModel): type: Literal["EXPOSE", "REPEAT_EXPOSE", "BIAS", "DARK", "STANDARD", "SCRIPT", "AUTO_FOCUS", "ENGINEERING", "SKY_FLAT"] instrument_type: Literal["1M0-SCICAM-SINISTRO"] = "1M0-SCICAM-SINISTRO" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco1M0ScicamSinistroConfigExtraParams = Field(default_factory=Lco1M0ScicamSinistroConfigExtraParams) instrument_configs: list[Lco1M0ScicamSinistroConfig] = [] acquisition_config: Lco1M0ScicamSinistroAcquisitionConfig guiding_config: Lco1M0ScicamSinistroGuidingConfig @@ -170,6 +202,16 @@ class Lco1M0ScicamSinistro(BaseModel): optical_elements_class = Lco1M0ScicamSinistroOpticalElements + +class Lco2M0FloydsScicamConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class Lco2M0FloydsScicamInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class Lco2M0FloydsScicamOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) slit: Literal["slit_6.0as", "slit_1.6as", "slit_2.0as", "slit_1.2as"] @@ -202,7 +244,7 @@ class Lco2M0FloydsScicamConfig(BaseModel): mode: Literal["default"] rotator_mode: Literal["VFLOAT", "SKY"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco2M0FloydsScicamInstrumentConfigExtraParams = Field(default_factory=Lco2M0FloydsScicamInstrumentConfigExtraParams) optical_elements: Lco2M0FloydsScicamOpticalElements @@ -211,7 +253,7 @@ class Lco2M0FloydsScicam(BaseModel): type: Literal["SPECTRUM", "REPEAT_SPECTRUM", "ARC", "ENGINEERING", "SCRIPT", "LAMP_FLAT"] instrument_type: Literal["2M0-FLOYDS-SCICAM"] = "2M0-FLOYDS-SCICAM" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco2M0FloydsScicamConfigExtraParams = Field(default_factory=Lco2M0FloydsScicamConfigExtraParams) instrument_configs: list[Lco2M0FloydsScicamConfig] = [] acquisition_config: Lco2M0FloydsScicamAcquisitionConfig guiding_config: Lco2M0FloydsScicamGuidingConfig @@ -224,6 +266,16 @@ class Lco2M0FloydsScicam(BaseModel): optical_elements_class = Lco2M0FloydsScicamOpticalElements + +class Lco2M0ScicamMuscatConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class Lco2M0ScicamMuscatInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-8.0), Le(8.0)]] = None + + class Lco2M0ScicamMuscatOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) narrowband_g_position: Literal["out", "in"] @@ -258,7 +310,7 @@ class Lco2M0ScicamMuscatConfig(BaseModel): """ Exposure time in seconds""" mode: Literal["MUSCAT_SLOW", "MUSCAT_FAST"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco2M0ScicamMuscatInstrumentConfigExtraParams = Field(default_factory=Lco2M0ScicamMuscatInstrumentConfigExtraParams) optical_elements: Lco2M0ScicamMuscatOpticalElements @@ -267,7 +319,7 @@ class Lco2M0ScicamMuscat(BaseModel): type: Literal["EXPOSE", "REPEAT_EXPOSE", "BIAS", "DARK", "STANDARD", "SCRIPT", "AUTO_FOCUS", "ENGINEERING", "SKY_FLAT"] instrument_type: Literal["2M0-SCICAM-MUSCAT"] = "2M0-SCICAM-MUSCAT" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: Lco2M0ScicamMuscatConfigExtraParams = Field(default_factory=Lco2M0ScicamMuscatConfigExtraParams) instrument_configs: list[Lco2M0ScicamMuscatConfig] = [] acquisition_config: Lco2M0ScicamMuscatAcquisitionConfig guiding_config: Lco2M0ScicamMuscatGuidingConfig diff --git a/src/aeonlib/ocs/soar/instruments.py b/src/aeonlib/ocs/soar/instruments.py index 88b199e..e277822 100644 --- a/src/aeonlib/ocs/soar/instruments.py +++ b/src/aeonlib/ocs/soar/instruments.py @@ -1,10 +1,10 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal +from typing import Any, Annotated, Literal, Optional -from annotated_types import Le -from pydantic import BaseModel, ConfigDict +from annotated_types import Le, Ge +from pydantic import BaseModel, ConfigDict, Field from pydantic.types import NonNegativeInt, PositiveInt from aeonlib.models import TARGET_TYPES @@ -12,6 +12,16 @@ from aeonlib.ocs.config_models import Roi + +class SoarGhtsBluecamConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class SoarGhtsBluecamInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class SoarGhtsBluecamOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) @@ -43,7 +53,7 @@ class SoarGhtsBluecamConfig(BaseModel): mode: Literal["GHTS_B_600UV_2x2_slit1p5", "GHTS_B_400m1_2x2", "GHTS_B_600UV_2x2_slit1p0", "GHTS_B_930m2_1x2_slit0p45"] rotator_mode: Literal["SKY"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsBluecamInstrumentConfigExtraParams = Field(default_factory=SoarGhtsBluecamInstrumentConfigExtraParams) optical_elements: SoarGhtsBluecamOpticalElements @@ -52,7 +62,7 @@ class SoarGhtsBluecam(BaseModel): type: Literal["SPECTRUM", "ENGINEERING", "SCRIPT", "LAMP_FLAT", "ARC"] instrument_type: Literal["SOAR_GHTS_BLUECAM"] = "SOAR_GHTS_BLUECAM" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsBluecamConfigExtraParams = Field(default_factory=SoarGhtsBluecamConfigExtraParams) instrument_configs: list[SoarGhtsBluecamConfig] = [] acquisition_config: SoarGhtsBluecamAcquisitionConfig guiding_config: SoarGhtsBluecamGuidingConfig @@ -65,6 +75,16 @@ class SoarGhtsBluecam(BaseModel): optical_elements_class = SoarGhtsBluecamOpticalElements + +class SoarGhtsBluecamImagerConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class SoarGhtsBluecamImagerInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class SoarGhtsBluecamImagerOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) filter: Literal["u-SDSS", "g-SDSS", "r-SDSS", "i-SDSS"] @@ -97,7 +117,7 @@ class SoarGhtsBluecamImagerConfig(BaseModel): mode: Literal["GHTS_B_Image_2x2"] rotator_mode: Literal["SKY"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsBluecamImagerInstrumentConfigExtraParams = Field(default_factory=SoarGhtsBluecamImagerInstrumentConfigExtraParams) optical_elements: SoarGhtsBluecamImagerOpticalElements @@ -106,7 +126,7 @@ class SoarGhtsBluecamImager(BaseModel): type: Literal["EXPOSE"] instrument_type: Literal["SOAR_GHTS_BLUECAM_IMAGER"] = "SOAR_GHTS_BLUECAM_IMAGER" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsBluecamImagerConfigExtraParams = Field(default_factory=SoarGhtsBluecamImagerConfigExtraParams) instrument_configs: list[SoarGhtsBluecamImagerConfig] = [] acquisition_config: SoarGhtsBluecamImagerAcquisitionConfig guiding_config: SoarGhtsBluecamImagerGuidingConfig @@ -119,6 +139,16 @@ class SoarGhtsBluecamImager(BaseModel): optical_elements_class = SoarGhtsBluecamImagerOpticalElements + +class SoarGhtsRedcamConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class SoarGhtsRedcamInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class SoarGhtsRedcamOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) @@ -150,7 +180,7 @@ class SoarGhtsRedcamConfig(BaseModel): mode: Literal["GHTS_R_400m1_2x2", "GHTS_R_400m2_2x2"] rotator_mode: Literal["SKY"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsRedcamInstrumentConfigExtraParams = Field(default_factory=SoarGhtsRedcamInstrumentConfigExtraParams) optical_elements: SoarGhtsRedcamOpticalElements @@ -159,7 +189,7 @@ class SoarGhtsRedcam(BaseModel): type: Literal["SPECTRUM", "ENGINEERING", "SCRIPT", "ARC", "LAMP_FLAT"] instrument_type: Literal["SOAR_GHTS_REDCAM"] = "SOAR_GHTS_REDCAM" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsRedcamConfigExtraParams = Field(default_factory=SoarGhtsRedcamConfigExtraParams) instrument_configs: list[SoarGhtsRedcamConfig] = [] acquisition_config: SoarGhtsRedcamAcquisitionConfig guiding_config: SoarGhtsRedcamGuidingConfig @@ -172,6 +202,16 @@ class SoarGhtsRedcam(BaseModel): optical_elements_class = SoarGhtsRedcamOpticalElements + +class SoarGhtsRedcamImagerConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class SoarGhtsRedcamImagerInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + + class SoarGhtsRedcamImagerOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) filter: Literal["g-SDSS", "r-SDSS", "i-SDSS", "z-SDSS"] @@ -204,7 +244,7 @@ class SoarGhtsRedcamImagerConfig(BaseModel): mode: Literal["GHTS_R_Image_2x2"] rotator_mode: Literal["SKY"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsRedcamImagerInstrumentConfigExtraParams = Field(default_factory=SoarGhtsRedcamImagerInstrumentConfigExtraParams) optical_elements: SoarGhtsRedcamImagerOpticalElements @@ -213,7 +253,7 @@ class SoarGhtsRedcamImager(BaseModel): type: Literal["EXPOSE"] instrument_type: Literal["SOAR_GHTS_REDCAM_IMAGER"] = "SOAR_GHTS_REDCAM_IMAGER" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarGhtsRedcamImagerConfigExtraParams = Field(default_factory=SoarGhtsRedcamImagerConfigExtraParams) instrument_configs: list[SoarGhtsRedcamImagerConfig] = [] acquisition_config: SoarGhtsRedcamImagerAcquisitionConfig guiding_config: SoarGhtsRedcamImagerGuidingConfig @@ -226,6 +266,15 @@ class SoarGhtsRedcamImager(BaseModel): optical_elements_class = SoarGhtsRedcamImagerOpticalElements + +class SoarTriplespecConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + +class SoarTriplespecInstrumentConfigExtraParams(BaseModel): + model_config = ConfigDict(validate_assignment=True, extra='allow') + + class SoarTriplespecOpticalElements(BaseModel): model_config = ConfigDict(validate_assignment=True) @@ -257,7 +306,7 @@ class SoarTriplespecConfig(BaseModel): mode: Literal["fowler1_coadds2", "fowler4_coadds1", "fowler8_coadds1", "fowler16_coadds1", "fowler1_coadds1"] rotator_mode: Literal["SKY"] rois: list[Roi] | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarTriplespecInstrumentConfigExtraParams = Field(default_factory=SoarTriplespecInstrumentConfigExtraParams) optical_elements: SoarTriplespecOpticalElements @@ -266,7 +315,7 @@ class SoarTriplespec(BaseModel): type: Literal["SPECTRUM", "STANDARD", "ARC", "LAMP_FLAT", "BIAS"] instrument_type: Literal["SOAR_TRIPLESPEC"] = "SOAR_TRIPLESPEC" repeat_duration: NonNegativeInt | None = None - extra_params: dict[Any, Any] = {} + extra_params: SoarTriplespecConfigExtraParams = Field(default_factory=SoarTriplespecConfigExtraParams) instrument_configs: list[SoarTriplespecConfig] = [] acquisition_config: SoarTriplespecAcquisitionConfig guiding_config: SoarTriplespecGuidingConfig From 0a998b5296dbdf70684f283036d445834b59701b Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 2 Oct 2025 21:01:17 +0000 Subject: [PATCH 2/4] Moved validation_schema extra_params field parsing to the python side --- codegen/lco/generator.py | 49 ++++++++++++++++++++----- codegen/lco/templates/instruments.jinja | 15 +++----- src/aeonlib/ocs/blanco/instruments.py | 15 ++++---- src/aeonlib/ocs/lco/instruments.py | 21 +++++++---- src/aeonlib/ocs/soar/instruments.py | 15 +++++--- 5 files changed, 77 insertions(+), 38 deletions(-) diff --git a/codegen/lco/generator.py b/codegen/lco/generator.py index c2aaec5..01024f6 100755 --- a/codegen/lco/generator.py +++ b/codegen/lco/generator.py @@ -11,12 +11,44 @@ VALID_FACILITIES = ["SOAR", "LCO", "SAAO", "BLANCO"] -def extract_default(properties): - if properties['type'] == 'integer' or properties['type'] == 'float' or properties['type'] == 'boolean': - return properties['default'] - elif properties['type'] == 'string': - return f'"{properties['default']}"' - return Any +def get_extra_params_fields(extra_params_validation_schema: dict) -> dict: + """ Loops over the "extra_params" section of a validation_schema dict and creates a dictionary of + field to aeonlib field_class to place into the template + """ + fields = {} + for field, properties in extra_params_validation_schema.items(): + field_class = '' + # If a set of allowed values is present, use that to make a Literal unless this is a boolean variable + if 'allowed' in properties and properties.get('type') != 'boolean': + allowed_values = [f'"{val}"' if properties['type'] == 'string' else val for val in properties['allowed']] + field_class += f"Literal[{', '.join(allowed_values)}]" + else: + # Otherwise form an Annotated field based on its datatype, with min/max validation if present + field_class += "Annotated[" + match properties['type']: + case 'string': + field_class += "str" + case 'integer': + field_class += "int" + case 'float': + field_class += "float" + case 'boolean': + field_class += "bool" + if 'min' in properties: + field_class += f", Ge({properties['min']})" + if 'max' in properties: + field_class += f", Le({properties['max']})" + # Add description to Annotated field. Annotated fields must have at least 2 properties. + field_class += f', "{properties.get('description', "")}"]' + if not properties.get('required', False) and 'default' not in properties: + # The field is considered optional if it doesn't have a default or required is set to True + field_class += " | None = None" + elif 'default' in properties: + # If a default value is present, provide it + default = f'"{properties['default']}"' if properties['type'] == 'string' else properties['default'] + field_class += f" = {default}" + fields[field] = field_class + return fields def get_modes(ins: dict[str, Any], type: str) -> list[str]: @@ -45,7 +77,6 @@ def generate_instrument_configs(ins_s: str, facility: str) -> str: trim_blocks=True, lstrip_blocks=True, ) - j_env.filters['extract_default'] = extract_default template = j_env.get_template("instruments.jinja") ins_data = json.loads(ins_s) instruments: list[dict[str, Any]] = [] @@ -93,8 +124,8 @@ def generate_instrument_configs(ins_s: str, facility: str) -> str: k.rstrip("s"): v for k, v in ins["optical_elements"].items() }, - "configuration_extra_params": ins['validation_schema'].get('extra_params', {}).get('schema', {}), - "instrument_config_extra_params": ins['validation_schema'].get('instrument_configs', {}).get('schema', {}).get('schema', {}).get('extra_params', {}).get('schema', {}) + "configuration_extra_params": get_extra_params_fields(ins['validation_schema'].get('extra_params', {}).get('schema', {})), + "instrument_config_extra_params": get_extra_params_fields(ins['validation_schema'].get('instrument_configs', {}).get('schema', {}).get('schema', {}).get('extra_params', {}).get('schema', {})) } ) diff --git a/codegen/lco/templates/instruments.jinja b/codegen/lco/templates/instruments.jinja index 34d4774..8931a8b 100644 --- a/codegen/lco/templates/instruments.jinja +++ b/codegen/lco/templates/instruments.jinja @@ -1,7 +1,7 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal, Optional +from typing import Any, Annotated, Literal from annotated_types import Le, Ge from pydantic import BaseModel, ConfigDict, Field @@ -14,21 +14,18 @@ from aeonlib.ocs.config_models import Roi {% for ctx in instruments %} + class {{ ctx.class_name}}ConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - {% for field, properties in ctx.configuration_extra_params.items() %} - {% set optional = ('required' not in properties or not properties.required) and 'default' not in properties %} - {{ field }}: {% if optional %}Optional[{% endif %}{% if 'allowed' in properties %}Literal[{{ properties.allowed }}]{% else %}Annotated[{% if properties.type == 'string' %}str{% elif properties.type == 'integer' %}int{% elif properties.type == 'float' %}float{% elif properties.type == 'boolean' %}bool{% endif %}{% if 'min' in properties %}, Ge({{ properties.min }}){% endif %}{% if 'max' in properties %}, Le({{ properties.max }}){% endif %}]{% endif %}{% if optional %}] = None{% elif 'default' in properties %} = {{ properties | extract_default }}{% endif %} - + {% for field, field_class in ctx.configuration_extra_params.items() %} + {{ field }}: {{ field_class }} {% endfor %} class {{ ctx.class_name}}InstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - {% for field, properties in ctx.instrument_config_extra_params.items() %} - {% set optional = ('required' not in properties or not properties.required) and 'default' not in properties %} - {{ field }}: {% if optional %}Optional[{% endif %}{% if 'allowed' in properties %}Literal[{{ properties.allowed }}]{% else %}Annotated[{% if properties.type == 'string' %}str{% elif properties.type == 'integer' %}int{% elif properties.type == 'float' %}float{% elif properties.type == 'boolean' %}bool{% endif %}{% if 'min' in properties %}, Ge({{ properties.min }}){% endif %}{% if 'max' in properties %}, Le({{ properties.max }}){% endif %}]{% endif %}{% if optional %}] = None{% elif 'default' in properties %} = {{ properties | extract_default }}{% endif %} - + {% for field, field_class in ctx.instrument_config_extra_params.items() %} + {{ field }}: {{ field_class }} {% endfor %} diff --git a/src/aeonlib/ocs/blanco/instruments.py b/src/aeonlib/ocs/blanco/instruments.py index 75b9e36..77bb606 100644 --- a/src/aeonlib/ocs/blanco/instruments.py +++ b/src/aeonlib/ocs/blanco/instruments.py @@ -1,7 +1,7 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal, Optional +from typing import Any, Annotated, Literal from annotated_types import Le, Ge from pydantic import BaseModel, ConfigDict, Field @@ -13,18 +13,19 @@ + class BlancoNewfirmConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - dither_value: Annotated[int, Ge(0), Le(1600)] = 80 - dither_sequence: Literal[['2x2', '3x3', '4x4', '5-point']] = "2x2" - detector_centering: Literal[['none', 'det_1', 'det_2', 'det_3', 'det_4']] = "det_1" - dither_sequence_random_offset: Literal[[True, False]] = True + dither_value: Annotated[int, Ge(0), Le(1600), "The amount in arc seconds between dither points"] = 80 + dither_sequence: Literal["2x2", "3x3", "4x4", "5-point"] = "2x2" + detector_centering: Literal["none", "det_1", "det_2", "det_3", "det_4"] = "det_1" + dither_sequence_random_offset: Annotated[bool, "Implements a random offset between dither patterns if repeating the dither pattern, i.e. when sequence repeats > 1"] = True class BlancoNewfirmInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - coadds: Annotated[int, Ge(1), Le(100)] = 1 - sequence_repeats: Annotated[int, Ge(1), Le(500)] = 1 + coadds: Annotated[int, Ge(1), Le(100), "This reduces data volume with short integration times necessary for broadband H and Ks observations. Coadding is digital summation of the images to avoid long integrations that could cause saturation of the detector."] = 1 + sequence_repeats: Annotated[int, Ge(1), Le(500), "The number of times to repeat the dither sequence"] = 1 class BlancoNewfirmOpticalElements(BaseModel): diff --git a/src/aeonlib/ocs/lco/instruments.py b/src/aeonlib/ocs/lco/instruments.py index 66d4055..6fedd03 100644 --- a/src/aeonlib/ocs/lco/instruments.py +++ b/src/aeonlib/ocs/lco/instruments.py @@ -1,7 +1,7 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal, Optional +from typing import Any, Annotated, Literal from annotated_types import Le, Ge from pydantic import BaseModel, ConfigDict, Field @@ -13,15 +13,16 @@ + class Lco0M4ScicamQhy600ConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - sub_expose: Literal[[False, True]] = False - sub_exposure_time: Optional[Annotated[float, Ge(15.0)]] = None + sub_expose: Annotated[bool, "Whether or not to split your exposures into sub_exposures to guide during the observation, and stack them together at the end for the final data product."] = False + sub_exposure_time: Annotated[float, Ge(15.0), "Exposure time for the sub-exposures in seconds, if sub_expose mode is set"] | None = None class Lco0M4ScicamQhy600InstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class Lco0M4ScicamQhy600OpticalElements(BaseModel): @@ -78,13 +79,14 @@ class Lco0M4ScicamQhy600(BaseModel): + class Lco1M0NresScicamConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class Lco1M0NresScicamInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class Lco1M0NresScicamOpticalElements(BaseModel): @@ -140,13 +142,14 @@ class Lco1M0NresScicam(BaseModel): + class Lco1M0ScicamSinistroConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class Lco1M0ScicamSinistroInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class Lco1M0ScicamSinistroOpticalElements(BaseModel): @@ -203,13 +206,14 @@ class Lco1M0ScicamSinistro(BaseModel): + class Lco2M0FloydsScicamConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class Lco2M0FloydsScicamInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), ""] | None = None class Lco2M0FloydsScicamOpticalElements(BaseModel): @@ -267,13 +271,14 @@ class Lco2M0FloydsScicam(BaseModel): + class Lco2M0ScicamMuscatConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class Lco2M0ScicamMuscatInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-8.0), Le(8.0)]] = None + defocus: Annotated[float, Ge(-8.0), Le(8.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 8mm."] | None = None class Lco2M0ScicamMuscatOpticalElements(BaseModel): diff --git a/src/aeonlib/ocs/soar/instruments.py b/src/aeonlib/ocs/soar/instruments.py index e277822..61a1012 100644 --- a/src/aeonlib/ocs/soar/instruments.py +++ b/src/aeonlib/ocs/soar/instruments.py @@ -1,7 +1,7 @@ # pyright: reportUnannotatedClassAttribute=false # This file is generated automatically and should not be edited by hand. -from typing import Any, Annotated, Literal, Optional +from typing import Any, Annotated, Literal from annotated_types import Le, Ge from pydantic import BaseModel, ConfigDict, Field @@ -13,13 +13,14 @@ + class SoarGhtsBluecamConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class SoarGhtsBluecamInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class SoarGhtsBluecamOpticalElements(BaseModel): @@ -76,13 +77,14 @@ class SoarGhtsBluecam(BaseModel): + class SoarGhtsBluecamImagerConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class SoarGhtsBluecamImagerInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class SoarGhtsBluecamImagerOpticalElements(BaseModel): @@ -140,13 +142,14 @@ class SoarGhtsBluecamImager(BaseModel): + class SoarGhtsRedcamConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class SoarGhtsRedcamInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class SoarGhtsRedcamOpticalElements(BaseModel): @@ -203,13 +206,14 @@ class SoarGhtsRedcam(BaseModel): + class SoarGhtsRedcamImagerConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') class SoarGhtsRedcamImagerInstrumentConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') - defocus: Optional[Annotated[float, Ge(-5.0), Le(5.0)]] = None + defocus: Annotated[float, Ge(-5.0), Le(5.0), "Observations may be defocused to prevent the CCD from saturating on bright targets. This term describes the offset (in mm) of the secondary mirror from its default (focused) position. The limits are ± 5mm."] | None = None class SoarGhtsRedcamImagerOpticalElements(BaseModel): @@ -267,6 +271,7 @@ class SoarGhtsRedcamImager(BaseModel): + class SoarTriplespecConfigExtraParams(BaseModel): model_config = ConfigDict(validate_assignment=True, extra='allow') From dc15d3edf15273876913f167a02f0901fb93fc97 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 2 Oct 2025 23:20:42 +0000 Subject: [PATCH 3/4] Thanks ruff --- codegen/lco/generator.py | 52 ++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/codegen/lco/generator.py b/codegen/lco/generator.py index 01024f6..15eaa4e 100755 --- a/codegen/lco/generator.py +++ b/codegen/lco/generator.py @@ -12,40 +12,47 @@ def get_extra_params_fields(extra_params_validation_schema: dict) -> dict: - """ Loops over the "extra_params" section of a validation_schema dict and creates a dictionary of - field to aeonlib field_class to place into the template + """Loops over the "extra_params" section of a validation_schema dict and creates a dictionary of + field to aeonlib field_class to place into the template """ fields = {} for field, properties in extra_params_validation_schema.items(): - field_class = '' + field_class = "" # If a set of allowed values is present, use that to make a Literal unless this is a boolean variable - if 'allowed' in properties and properties.get('type') != 'boolean': - allowed_values = [f'"{val}"' if properties['type'] == 'string' else val for val in properties['allowed']] + if "allowed" in properties and properties.get("type") != "boolean": + allowed_values = [ + f'"{val}"' if properties["type"] == "string" else val + for val in properties["allowed"] + ] field_class += f"Literal[{', '.join(allowed_values)}]" else: # Otherwise form an Annotated field based on its datatype, with min/max validation if present field_class += "Annotated[" - match properties['type']: - case 'string': + match properties["type"]: + case "string": field_class += "str" - case 'integer': + case "integer": field_class += "int" - case 'float': + case "float": field_class += "float" - case 'boolean': + case "boolean": field_class += "bool" - if 'min' in properties: + if "min" in properties: field_class += f", Ge({properties['min']})" - if 'max' in properties: + if "max" in properties: field_class += f", Le({properties['max']})" # Add description to Annotated field. Annotated fields must have at least 2 properties. - field_class += f', "{properties.get('description', "")}"]' - if not properties.get('required', False) and 'default' not in properties: + field_class += f', "{properties.get("description", "")}"]' + if not properties.get("required", False) and "default" not in properties: # The field is considered optional if it doesn't have a default or required is set to True field_class += " | None = None" - elif 'default' in properties: + elif "default" in properties: # If a default value is present, provide it - default = f'"{properties['default']}"' if properties['type'] == 'string' else properties['default'] + default = ( + f'"{properties["default"]}"' + if properties["type"] == "string" + else properties["default"] + ) field_class += f" = {default}" fields[field] = field_class return fields @@ -124,8 +131,17 @@ def generate_instrument_configs(ins_s: str, facility: str) -> str: k.rstrip("s"): v for k, v in ins["optical_elements"].items() }, - "configuration_extra_params": get_extra_params_fields(ins['validation_schema'].get('extra_params', {}).get('schema', {})), - "instrument_config_extra_params": get_extra_params_fields(ins['validation_schema'].get('instrument_configs', {}).get('schema', {}).get('schema', {}).get('extra_params', {}).get('schema', {})) + "configuration_extra_params": get_extra_params_fields( + ins["validation_schema"].get("extra_params", {}).get("schema", {}) + ), + "instrument_config_extra_params": get_extra_params_fields( + ins["validation_schema"] + .get("instrument_configs", {}) + .get("schema", {}) + .get("schema", {}) + .get("extra_params", {}) + .get("schema", {}) + ), } ) From 49c94ff0313e1324335467e7fe5f366b8b36e1c8 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 3 Oct 2025 05:54:57 +0000 Subject: [PATCH 4/4] Fix comment --- codegen/lco/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen/lco/generator.py b/codegen/lco/generator.py index 15eaa4e..dc27cd1 100755 --- a/codegen/lco/generator.py +++ b/codegen/lco/generator.py @@ -44,7 +44,7 @@ def get_extra_params_fields(extra_params_validation_schema: dict) -> dict: # Add description to Annotated field. Annotated fields must have at least 2 properties. field_class += f', "{properties.get("description", "")}"]' if not properties.get("required", False) and "default" not in properties: - # The field is considered optional if it doesn't have a default or required is set to True + # The field is considered optional if it doesn't have a default or required is not set to True field_class += " | None = None" elif "default" in properties: # If a default value is present, provide it