Skip to content

Commit 6eb80fe

Browse files
authored
Merge pull request #57 from AEONplus/feature/ocs_validation_schema_support
Add in support for pulling out extra params fields defined in instrum…
2 parents 4d3928c + 49c94ff commit 6eb80fe

6 files changed

Lines changed: 236 additions & 35 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,20 @@ This ensures regular users of the library do not need to install these dependenc
9090
The `generate.py` script takes as input JSON as produced by the instruments endpoint:
9191

9292
```bash
93-
codegen/lco/generator.py instruments.json
93+
codegen/lco/generator.py {facility} instruments.json
9494
```
9595

9696
Or directly from stdin using a pipe:
9797

9898
```bash
99-
curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py
99+
curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py {facility}
100100
```
101101

102102
If the output looks satisfactory, you can redirect the output to overwrite the
103103
LCO instruments definition file:
104104

105105
```bash
106-
curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py > src/aeonlib/ocs/lco/instruments.py
106+
curl https://observe.lco.global/api/instruments/ | codegen/lco/generator.py {facility} > src/aeonlib/ocs/lco/instruments.py
107107
```
108108
# Supported Facilities
109109

codegen/lco/generator.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,53 @@
1111
VALID_FACILITIES = ["SOAR", "LCO", "SAAO", "BLANCO"]
1212

1313

14+
def get_extra_params_fields(extra_params_validation_schema: dict) -> dict:
15+
"""Loops over the "extra_params" section of a validation_schema dict and creates a dictionary of
16+
field to aeonlib field_class to place into the template
17+
"""
18+
fields = {}
19+
for field, properties in extra_params_validation_schema.items():
20+
field_class = ""
21+
# If a set of allowed values is present, use that to make a Literal unless this is a boolean variable
22+
if "allowed" in properties and properties.get("type") != "boolean":
23+
allowed_values = [
24+
f'"{val}"' if properties["type"] == "string" else val
25+
for val in properties["allowed"]
26+
]
27+
field_class += f"Literal[{', '.join(allowed_values)}]"
28+
else:
29+
# Otherwise form an Annotated field based on its datatype, with min/max validation if present
30+
field_class += "Annotated["
31+
match properties["type"]:
32+
case "string":
33+
field_class += "str"
34+
case "integer":
35+
field_class += "int"
36+
case "float":
37+
field_class += "float"
38+
case "boolean":
39+
field_class += "bool"
40+
if "min" in properties:
41+
field_class += f", Ge({properties['min']})"
42+
if "max" in properties:
43+
field_class += f", Le({properties['max']})"
44+
# Add description to Annotated field. Annotated fields must have at least 2 properties.
45+
field_class += f', "{properties.get("description", "")}"]'
46+
if not properties.get("required", False) and "default" not in properties:
47+
# The field is considered optional if it doesn't have a default or required is not set to True
48+
field_class += " | None = None"
49+
elif "default" in properties:
50+
# If a default value is present, provide it
51+
default = (
52+
f'"{properties["default"]}"'
53+
if properties["type"] == "string"
54+
else properties["default"]
55+
)
56+
field_class += f" = {default}"
57+
fields[field] = field_class
58+
return fields
59+
60+
1461
def get_modes(ins: dict[str, Any], type: str) -> list[str]:
1562
try:
1663
return [m["code"] for m in ins["modes"][type]["modes"]]
@@ -84,6 +131,17 @@ def generate_instrument_configs(ins_s: str, facility: str) -> str:
84131
k.rstrip("s"): v
85132
for k, v in ins["optical_elements"].items()
86133
},
134+
"configuration_extra_params": get_extra_params_fields(
135+
ins["validation_schema"].get("extra_params", {}).get("schema", {})
136+
),
137+
"instrument_config_extra_params": get_extra_params_fields(
138+
ins["validation_schema"]
139+
.get("instrument_configs", {})
140+
.get("schema", {})
141+
.get("schema", {})
142+
.get("extra_params", {})
143+
.get("schema", {})
144+
),
87145
}
88146
)
89147

codegen/lco/templates/instruments.jinja

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
from typing import Any, Annotated, Literal
55

6-
from annotated_types import Le
7-
from pydantic import BaseModel, ConfigDict
6+
from annotated_types import Le, Ge
7+
from pydantic import BaseModel, ConfigDict, Field
88
from pydantic.types import NonNegativeInt, PositiveInt
99

1010
from aeonlib.models import TARGET_TYPES
@@ -13,6 +13,22 @@ from aeonlib.ocs.config_models import Roi
1313

1414

1515
{% for ctx in instruments %}
16+
17+
18+
class {{ ctx.class_name}}ConfigExtraParams(BaseModel):
19+
model_config = ConfigDict(validate_assignment=True, extra='allow')
20+
{% for field, field_class in ctx.configuration_extra_params.items() %}
21+
{{ field }}: {{ field_class }}
22+
{% endfor %}
23+
24+
25+
class {{ ctx.class_name}}InstrumentConfigExtraParams(BaseModel):
26+
model_config = ConfigDict(validate_assignment=True, extra='allow')
27+
{% for field, field_class in ctx.instrument_config_extra_params.items() %}
28+
{{ field }}: {{ field_class }}
29+
{% endfor %}
30+
31+
1632
class {{ ctx.class_name }}OpticalElements(BaseModel):
1733
model_config = ConfigDict(validate_assignment=True)
1834
{% for key, values in ctx.optical_elements.items() %}
@@ -49,7 +65,7 @@ class {{ ctx.class_name }}Config(BaseModel):
4965
rotator_mode: Literal[{% for m in ctx.rotator_modes %}"{{ m }}"{% if not loop.last %}, {% endif %}{% endfor %}]
5066
{% endif %}
5167
rois: list[Roi] | None = None
52-
extra_params: dict[Any, Any] = {}
68+
extra_params: {{ ctx.class_name }}InstrumentConfigExtraParams = Field(default_factory={{ ctx.class_name }}InstrumentConfigExtraParams)
5369
optical_elements: {{ ctx.class_name}}OpticalElements
5470

5571

@@ -58,7 +74,7 @@ class {{ ctx.class_name }}(BaseModel):
5874
type: Literal[{% for t in ctx.config_types %}"{{ t }}"{% if not loop.last %}, {% endif %}{% endfor %}]
5975
instrument_type: Literal["{{ ctx.instrument_type }}"] = "{{ ctx.instrument_type }}"
6076
repeat_duration: NonNegativeInt | None = None
61-
extra_params: dict[Any, Any] = {}
77+
extra_params: {{ ctx.class_name }}ConfigExtraParams = Field(default_factory={{ ctx.class_name }}ConfigExtraParams)
6278
instrument_configs: list[{{ ctx.class_name }}Config] = []
6379
acquisition_config: {{ ctx.class_name }}AcquisitionConfig
6480
guiding_config: {{ ctx.class_name }}GuidingConfig

src/aeonlib/ocs/blanco/instruments.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,31 @@
33

44
from typing import Any, Annotated, Literal
55

6-
from annotated_types import Le
7-
from pydantic import BaseModel, ConfigDict
6+
from annotated_types import Le, Ge
7+
from pydantic import BaseModel, ConfigDict, Field
88
from pydantic.types import NonNegativeInt, PositiveInt
99

1010
from aeonlib.models import TARGET_TYPES
1111
from aeonlib.ocs.target_models import Constraints
1212
from aeonlib.ocs.config_models import Roi
1313

1414

15+
16+
17+
class BlancoNewfirmConfigExtraParams(BaseModel):
18+
model_config = ConfigDict(validate_assignment=True, extra='allow')
19+
dither_value: Annotated[int, Ge(0), Le(1600), "The amount in arc seconds between dither points"] = 80
20+
dither_sequence: Literal["2x2", "3x3", "4x4", "5-point"] = "2x2"
21+
detector_centering: Literal["none", "det_1", "det_2", "det_3", "det_4"] = "det_1"
22+
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
23+
24+
25+
class BlancoNewfirmInstrumentConfigExtraParams(BaseModel):
26+
model_config = ConfigDict(validate_assignment=True, extra='allow')
27+
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
28+
sequence_repeats: Annotated[int, Ge(1), Le(500), "The number of times to repeat the dither sequence"] = 1
29+
30+
1531
class BlancoNewfirmOpticalElements(BaseModel):
1632
model_config = ConfigDict(validate_assignment=True)
1733
filter: Literal["JX", "HX", "KXs"]
@@ -43,7 +59,7 @@ class BlancoNewfirmConfig(BaseModel):
4359
""" Exposure time in seconds"""
4460
mode: Literal["fowler1", "fowler2"]
4561
rois: list[Roi] | None = None
46-
extra_params: dict[Any, Any] = {}
62+
extra_params: BlancoNewfirmInstrumentConfigExtraParams = Field(default_factory=BlancoNewfirmInstrumentConfigExtraParams)
4763
optical_elements: BlancoNewfirmOpticalElements
4864

4965

@@ -52,7 +68,7 @@ class BlancoNewfirm(BaseModel):
5268
type: Literal["EXPOSE", "SKY_FLAT", "STANDARD", "DARK"]
5369
instrument_type: Literal["BLANCO_NEWFIRM"] = "BLANCO_NEWFIRM"
5470
repeat_duration: NonNegativeInt | None = None
55-
extra_params: dict[Any, Any] = {}
71+
extra_params: BlancoNewfirmConfigExtraParams = Field(default_factory=BlancoNewfirmConfigExtraParams)
5672
instrument_configs: list[BlancoNewfirmConfig] = []
5773
acquisition_config: BlancoNewfirmAcquisitionConfig
5874
guiding_config: BlancoNewfirmGuidingConfig

src/aeonlib/ocs/lco/instruments.py

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@
33

44
from typing import Any, Annotated, Literal
55

6-
from annotated_types import Le
7-
from pydantic import BaseModel, ConfigDict
6+
from annotated_types import Le, Ge
7+
from pydantic import BaseModel, ConfigDict, Field
88
from pydantic.types import NonNegativeInt, PositiveInt
99

1010
from aeonlib.models import TARGET_TYPES
1111
from aeonlib.ocs.target_models import Constraints
1212
from aeonlib.ocs.config_models import Roi
1313

1414

15+
16+
17+
class Lco0M4ScicamQhy600ConfigExtraParams(BaseModel):
18+
model_config = ConfigDict(validate_assignment=True, extra='allow')
19+
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
20+
sub_exposure_time: Annotated[float, Ge(15.0), "Exposure time for the sub-exposures in seconds, if sub_expose mode is set"] | None = None
21+
22+
23+
class Lco0M4ScicamQhy600InstrumentConfigExtraParams(BaseModel):
24+
model_config = ConfigDict(validate_assignment=True, extra='allow')
25+
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
26+
27+
1528
class Lco0M4ScicamQhy600OpticalElements(BaseModel):
1629
model_config = ConfigDict(validate_assignment=True)
1730
filter: Literal["OIII", "SII", "Astrodon-Exo", "w", "opaque", "up", "rp", "ip", "gp", "zs", "V", "B", "H-Alpha"]
@@ -43,7 +56,7 @@ class Lco0M4ScicamQhy600Config(BaseModel):
4356
""" Exposure time in seconds"""
4457
mode: Literal["central30x30", "full_frame"]
4558
rois: list[Roi] | None = None
46-
extra_params: dict[Any, Any] = {}
59+
extra_params: Lco0M4ScicamQhy600InstrumentConfigExtraParams = Field(default_factory=Lco0M4ScicamQhy600InstrumentConfigExtraParams)
4760
optical_elements: Lco0M4ScicamQhy600OpticalElements
4861

4962

@@ -52,7 +65,7 @@ class Lco0M4ScicamQhy600(BaseModel):
5265
type: Literal["EXPOSE", "REPEAT_EXPOSE", "AUTO_FOCUS", "BIAS", "DARK", "STANDARD", "SKY_FLAT"]
5366
instrument_type: Literal["0M4-SCICAM-QHY600"] = "0M4-SCICAM-QHY600"
5467
repeat_duration: NonNegativeInt | None = None
55-
extra_params: dict[Any, Any] = {}
68+
extra_params: Lco0M4ScicamQhy600ConfigExtraParams = Field(default_factory=Lco0M4ScicamQhy600ConfigExtraParams)
5669
instrument_configs: list[Lco0M4ScicamQhy600Config] = []
5770
acquisition_config: Lco0M4ScicamQhy600AcquisitionConfig
5871
guiding_config: Lco0M4ScicamQhy600GuidingConfig
@@ -65,6 +78,17 @@ class Lco0M4ScicamQhy600(BaseModel):
6578
optical_elements_class = Lco0M4ScicamQhy600OpticalElements
6679

6780

81+
82+
83+
class Lco1M0NresScicamConfigExtraParams(BaseModel):
84+
model_config = ConfigDict(validate_assignment=True, extra='allow')
85+
86+
87+
class Lco1M0NresScicamInstrumentConfigExtraParams(BaseModel):
88+
model_config = ConfigDict(validate_assignment=True, extra='allow')
89+
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
90+
91+
6892
class Lco1M0NresScicamOpticalElements(BaseModel):
6993
model_config = ConfigDict(validate_assignment=True)
7094

@@ -95,7 +119,7 @@ class Lco1M0NresScicamConfig(BaseModel):
95119
""" Exposure time in seconds"""
96120
mode: Literal["default"]
97121
rois: list[Roi] | None = None
98-
extra_params: dict[Any, Any] = {}
122+
extra_params: Lco1M0NresScicamInstrumentConfigExtraParams = Field(default_factory=Lco1M0NresScicamInstrumentConfigExtraParams)
99123
optical_elements: Lco1M0NresScicamOpticalElements
100124

101125

@@ -104,7 +128,7 @@ class Lco1M0NresScicam(BaseModel):
104128
type: Literal["NRES_SPECTRUM", "REPEAT_NRES_SPECTRUM", "NRES_EXPOSE", "NRES_TEST", "SCRIPT", "ENGINEERING", "ARC", "LAMP_FLAT", "NRES_BIAS", "NRES_DARK", "AUTO_FOCUS"]
105129
instrument_type: Literal["1M0-NRES-SCICAM"] = "1M0-NRES-SCICAM"
106130
repeat_duration: NonNegativeInt | None = None
107-
extra_params: dict[Any, Any] = {}
131+
extra_params: Lco1M0NresScicamConfigExtraParams = Field(default_factory=Lco1M0NresScicamConfigExtraParams)
108132
instrument_configs: list[Lco1M0NresScicamConfig] = []
109133
acquisition_config: Lco1M0NresScicamAcquisitionConfig
110134
guiding_config: Lco1M0NresScicamGuidingConfig
@@ -117,6 +141,17 @@ class Lco1M0NresScicam(BaseModel):
117141
optical_elements_class = Lco1M0NresScicamOpticalElements
118142

119143

144+
145+
146+
class Lco1M0ScicamSinistroConfigExtraParams(BaseModel):
147+
model_config = ConfigDict(validate_assignment=True, extra='allow')
148+
149+
150+
class Lco1M0ScicamSinistroInstrumentConfigExtraParams(BaseModel):
151+
model_config = ConfigDict(validate_assignment=True, extra='allow')
152+
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
153+
154+
120155
class Lco1M0ScicamSinistroOpticalElements(BaseModel):
121156
model_config = ConfigDict(validate_assignment=True)
122157
filter: Literal["I", "R", "U", "w", "Y", "up", "rp", "ip", "gp", "zs", "V", "B", "400um-Pinhole", "150um-Pinhole", "CN"]
@@ -148,7 +183,7 @@ class Lco1M0ScicamSinistroConfig(BaseModel):
148183
""" Exposure time in seconds"""
149184
mode: Literal["full_frame", "central_2k_2x2"]
150185
rois: list[Roi] | None = None
151-
extra_params: dict[Any, Any] = {}
186+
extra_params: Lco1M0ScicamSinistroInstrumentConfigExtraParams = Field(default_factory=Lco1M0ScicamSinistroInstrumentConfigExtraParams)
152187
optical_elements: Lco1M0ScicamSinistroOpticalElements
153188

154189

@@ -157,7 +192,7 @@ class Lco1M0ScicamSinistro(BaseModel):
157192
type: Literal["EXPOSE", "REPEAT_EXPOSE", "BIAS", "DARK", "STANDARD", "SCRIPT", "AUTO_FOCUS", "ENGINEERING", "SKY_FLAT"]
158193
instrument_type: Literal["1M0-SCICAM-SINISTRO"] = "1M0-SCICAM-SINISTRO"
159194
repeat_duration: NonNegativeInt | None = None
160-
extra_params: dict[Any, Any] = {}
195+
extra_params: Lco1M0ScicamSinistroConfigExtraParams = Field(default_factory=Lco1M0ScicamSinistroConfigExtraParams)
161196
instrument_configs: list[Lco1M0ScicamSinistroConfig] = []
162197
acquisition_config: Lco1M0ScicamSinistroAcquisitionConfig
163198
guiding_config: Lco1M0ScicamSinistroGuidingConfig
@@ -170,6 +205,17 @@ class Lco1M0ScicamSinistro(BaseModel):
170205
optical_elements_class = Lco1M0ScicamSinistroOpticalElements
171206

172207

208+
209+
210+
class Lco2M0FloydsScicamConfigExtraParams(BaseModel):
211+
model_config = ConfigDict(validate_assignment=True, extra='allow')
212+
213+
214+
class Lco2M0FloydsScicamInstrumentConfigExtraParams(BaseModel):
215+
model_config = ConfigDict(validate_assignment=True, extra='allow')
216+
defocus: Annotated[float, Ge(-5.0), Le(5.0), ""] | None = None
217+
218+
173219
class Lco2M0FloydsScicamOpticalElements(BaseModel):
174220
model_config = ConfigDict(validate_assignment=True)
175221
slit: Literal["slit_6.0as", "slit_1.6as", "slit_2.0as", "slit_1.2as"]
@@ -202,7 +248,7 @@ class Lco2M0FloydsScicamConfig(BaseModel):
202248
mode: Literal["default"]
203249
rotator_mode: Literal["VFLOAT", "SKY"]
204250
rois: list[Roi] | None = None
205-
extra_params: dict[Any, Any] = {}
251+
extra_params: Lco2M0FloydsScicamInstrumentConfigExtraParams = Field(default_factory=Lco2M0FloydsScicamInstrumentConfigExtraParams)
206252
optical_elements: Lco2M0FloydsScicamOpticalElements
207253

208254

@@ -211,7 +257,7 @@ class Lco2M0FloydsScicam(BaseModel):
211257
type: Literal["SPECTRUM", "REPEAT_SPECTRUM", "ARC", "ENGINEERING", "SCRIPT", "LAMP_FLAT"]
212258
instrument_type: Literal["2M0-FLOYDS-SCICAM"] = "2M0-FLOYDS-SCICAM"
213259
repeat_duration: NonNegativeInt | None = None
214-
extra_params: dict[Any, Any] = {}
260+
extra_params: Lco2M0FloydsScicamConfigExtraParams = Field(default_factory=Lco2M0FloydsScicamConfigExtraParams)
215261
instrument_configs: list[Lco2M0FloydsScicamConfig] = []
216262
acquisition_config: Lco2M0FloydsScicamAcquisitionConfig
217263
guiding_config: Lco2M0FloydsScicamGuidingConfig
@@ -224,6 +270,17 @@ class Lco2M0FloydsScicam(BaseModel):
224270
optical_elements_class = Lco2M0FloydsScicamOpticalElements
225271

226272

273+
274+
275+
class Lco2M0ScicamMuscatConfigExtraParams(BaseModel):
276+
model_config = ConfigDict(validate_assignment=True, extra='allow')
277+
278+
279+
class Lco2M0ScicamMuscatInstrumentConfigExtraParams(BaseModel):
280+
model_config = ConfigDict(validate_assignment=True, extra='allow')
281+
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
282+
283+
227284
class Lco2M0ScicamMuscatOpticalElements(BaseModel):
228285
model_config = ConfigDict(validate_assignment=True)
229286
narrowband_g_position: Literal["out", "in"]
@@ -258,7 +315,7 @@ class Lco2M0ScicamMuscatConfig(BaseModel):
258315
""" Exposure time in seconds"""
259316
mode: Literal["MUSCAT_SLOW", "MUSCAT_FAST"]
260317
rois: list[Roi] | None = None
261-
extra_params: dict[Any, Any] = {}
318+
extra_params: Lco2M0ScicamMuscatInstrumentConfigExtraParams = Field(default_factory=Lco2M0ScicamMuscatInstrumentConfigExtraParams)
262319
optical_elements: Lco2M0ScicamMuscatOpticalElements
263320

264321

@@ -267,7 +324,7 @@ class Lco2M0ScicamMuscat(BaseModel):
267324
type: Literal["EXPOSE", "REPEAT_EXPOSE", "BIAS", "DARK", "STANDARD", "SCRIPT", "AUTO_FOCUS", "ENGINEERING", "SKY_FLAT"]
268325
instrument_type: Literal["2M0-SCICAM-MUSCAT"] = "2M0-SCICAM-MUSCAT"
269326
repeat_duration: NonNegativeInt | None = None
270-
extra_params: dict[Any, Any] = {}
327+
extra_params: Lco2M0ScicamMuscatConfigExtraParams = Field(default_factory=Lco2M0ScicamMuscatConfigExtraParams)
271328
instrument_configs: list[Lco2M0ScicamMuscatConfig] = []
272329
acquisition_config: Lco2M0ScicamMuscatAcquisitionConfig
273330
guiding_config: Lco2M0ScicamMuscatGuidingConfig

0 commit comments

Comments
 (0)