Coverage for python/astro_metadata_translator/properties.py: 57%
148 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-30 08:37 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-30 08:37 +0000
1# This file is part of astro_metadata_translator.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the LICENSE file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12"""Properties calculated by this package.
14Defines all properties in one place so that both `ObservationInfo` and
15`MetadataTranslator` can use them. In particular, the translator
16base class can use knowledge of these properties to predefine translation
17stubs with documentation attached, and `ObservationInfo` can automatically
18define the getter methods.
19"""
21from __future__ import annotations
23__all__ = (
24 "PROPERTIES",
25 "PropertyDefinition",
26)
28from collections.abc import Callable
29from typing import Annotated, Any, Protocol, SupportsFloat, cast
31import astropy.coordinates
32import astropy.time
33import astropy.units
34import numpy as np
35from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
36from pydantic.json_schema import JsonSchemaValue
37from pydantic_core import CoreSchema, core_schema
40class _AstropyFieldAdapter:
41 """Pydantic adapter that round-trips an astropy-like value through a
42 simple JSON-friendly representation.
44 The adapter provides:
46 - a validator that accepts either an instance of ``py_type`` (passthrough)
47 or the simple form, which is converted via ``complexifier``;
48 - a serializer that converts an instance of ``py_type`` to its simple
49 form via ``simplifier``;
50 - an explicit JSON Schema for the wire form so the schema generator does
51 not have to introspect ``py_type``.
53 All three concerns are collapsed into one Annotated metadata object so
54 each field annotation reads simply as ``py_type``.
56 Parameters
57 ----------
58 py_type : `type`
59 Concrete Python type of the field value. Used for the ``isinstance``
60 passthrough check and as the core-schema's ``is_instance`` validator.
61 simplifier : `~collections.abc.Callable`
62 Function converting an instance of ``py_type`` to its JSON-friendly
63 wire form (a number or a tuple of numbers).
64 complexifier : `~collections.abc.Callable`
65 Function converting the wire form back to ``py_type``. Called as
66 ``complexifier(value, **other_fields)``; only `simple_to_altaz`
67 consumes the extra fields (``location``, ``datetime_begin``).
68 json_schema : `dict`
69 Explicit JSON Schema for the wire form.
70 """
72 def __init__(
73 self,
74 py_type: type,
75 *,
76 simplifier: Callable[[Any], Any],
77 complexifier: Callable[..., Any],
78 json_schema: JsonSchemaValue,
79 ) -> None:
80 self._py_type = py_type
81 self._simplifier = simplifier
82 self._complexifier = complexifier
83 self._json_schema = json_schema
85 def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
86 py_type = self._py_type
87 complexifier = self._complexifier
89 def validate(value: Any, info: core_schema.ValidationInfo) -> Any:
90 if value is None or isinstance(value, py_type):
91 return value
92 context = info.data if isinstance(info.data, dict) else {}
93 return complexifier(value, **context)
95 return core_schema.with_info_before_validator_function(
96 validate,
97 core_schema.is_instance_schema(py_type),
98 serialization=core_schema.plain_serializer_function_ser_schema(
99 self._simplifier,
100 when_used="unless-none",
101 ),
102 )
104 def __get_pydantic_json_schema__(
105 self, core_schema_obj: CoreSchema, handler: GetJsonSchemaHandler
106 ) -> JsonSchemaValue:
107 return dict(self._json_schema)
110def _make_annotated(
111 py_type: type,
112 *,
113 simplifier: Callable[[Any], Any],
114 complexifier: Callable[..., Any],
115 json_schema: JsonSchemaValue,
116) -> Any:
117 """Return an `Annotated` typedef driven by `_AstropyFieldAdapter`.
119 Parameters
120 ----------
121 py_type : `type`
122 Concrete Python type of the field value.
123 simplifier : `~collections.abc.Callable`
124 Function converting an instance of ``py_type`` to its JSON-friendly
125 wire form.
126 complexifier : `~collections.abc.Callable`
127 Function converting the wire form back to ``py_type``.
128 json_schema : `dict`
129 Explicit JSON Schema for the wire form.
131 Returns
132 -------
133 annotated : `typing.Annotated`
134 Annotated alias of ``py_type`` carrying the pydantic adapter.
135 """
136 return Annotated[
137 py_type,
138 _AstropyFieldAdapter(
139 py_type,
140 simplifier=simplifier,
141 complexifier=complexifier,
142 json_schema=json_schema,
143 ),
144 ]
147def _tuple_of_floats_schema(n: int) -> JsonSchemaValue:
148 """Return a JSON Schema fragment for a fixed-length tuple of floats.
150 Parameters
151 ----------
152 n : `int`
153 Number of elements in the tuple.
155 Returns
156 -------
157 schema : `dict`
158 JSON Schema describing an array of exactly ``n`` numbers.
159 """
160 return {
161 "type": "array",
162 "prefixItems": [{"type": "number"}] * n,
163 "minItems": n,
164 "maxItems": n,
165 }
168def _quantity_in_unit_schema(unit: astropy.units.UnitBase) -> JsonSchemaValue:
169 """Return a JSON Schema fragment for a `Quantity` serialized as a float.
171 The ``x-unit`` keyword records the assumed serialization unit so consumers
172 can interpret the bare number; ``x-`` is the conventional prefix for JSON
173 Schema vocabulary extensions.
175 Parameters
176 ----------
177 unit : `astropy.units.UnitBase`
178 Unit assumed for the serialized float value.
180 Returns
181 -------
182 schema : `dict`
183 JSON Schema describing a number with an attached ``x-unit`` keyword.
184 """
185 return {"type": "number", "x-unit": str(unit)}
188# Helper functions to convert complex types to simple form suitable
189# for JSON serialization
190# All take the complex type and return simple python form using str, float,
191# int, dict, or list.
192# All assume the supplied parameter is not None.
195class _ToValueProtocol(Protocol):
196 """Protocol for Quantity-like class that has to_value method."""
198 def to_value(self, unit: astropy.units.UnitBase | None = None) -> SupportsFloat | np.ndarray:
199 """Return converted value that might be ndarray or a single number.
201 Parameters
202 ----------
203 unit : `astropy.units.UnitBase` or `None`, optional
204 Optional unit to use when converting the values to floats.
205 """
206 ...
209def _quantity_to_float(q: _ToValueProtocol, unit: astropy.units.UnitBase | None = None) -> float:
210 """Convert a quantity to a float, in a type safe manner, returning
211 a single float.
213 Parameters
214 ----------
215 q : `_ToValueProtocol`
216 The Astropy object to extract the float value from. Must support a
217 ``to_value()`` method.
218 unit : `astropy.units.UnitBase` or `None`, optional
219 Optional unit to use when converting the values to floats.
221 Returns
222 -------
223 value : `float`
224 Single float corresponding to the quantity-like input.
225 """
226 # Quantity.to_value is typed to return np.ndarray or a scalar-like value
227 # that supports float conversion.
228 # We only went a single float and it is an error to return multiples.
229 values = q.to_value(unit=unit)
230 if isinstance(values, np.ndarray):
231 raise ValueError(
232 f"Converting quantity to a float failed because unexpectedly got more than one float: {values}"
233 )
234 return float(values)
237def earthlocation_to_simple(
238 location: astropy.coordinates.EarthLocation,
239) -> tuple[float, float, float]:
240 """Convert EarthLocation to tuple.
242 Parameters
243 ----------
244 location : `astropy.coordinates.EarthLocation`
245 The location to simplify.
247 Returns
248 -------
249 geocentric : `tuple` of (`float`, `float`, `float`)
250 The geocentric location as three floats in meters.
251 """
252 geocentric = location.to_geocentric()
253 return cast(
254 "tuple[float, float, float]",
255 tuple(_quantity_to_float(c, astropy.units.m) for c in geocentric),
256 )
259def simple_to_earthlocation(simple: tuple[float, ...], **kwargs: Any) -> astropy.coordinates.EarthLocation:
260 """Convert simple form back to EarthLocation.
262 Parameters
263 ----------
264 simple : `tuple` [`float`, ...]
265 The geocentric location as three floats in meters.
266 **kwargs : `typing.Any`
267 Keyword arguments. Currently not used.
269 Returns
270 -------
271 loc : `astropy.coordinates.EarthLocation`
272 The location on the Earth.
273 """
274 return astropy.coordinates.EarthLocation.from_geocentric(*simple, unit=astropy.units.m)
277EarthLocationAnnotated = _make_annotated(
278 astropy.coordinates.EarthLocation,
279 simplifier=earthlocation_to_simple,
280 complexifier=simple_to_earthlocation,
281 json_schema={**_tuple_of_floats_schema(3), "x-unit": "m"},
282)
285def datetime_to_simple(datetime: astropy.time.Time) -> tuple[float, float]:
286 """Convert Time to tuple.
288 Parameters
289 ----------
290 datetime : `astropy.time.Time`
291 The time to simplify.
293 Returns
294 -------
295 mjds : `tuple` of (`float`, `float`)
296 The two MJDs in TAI.
297 """
298 tai = datetime.tai
299 return (tai.jd1, tai.jd2)
302def simple_to_datetime(simple: tuple[float, float], **kwargs: Any) -> astropy.time.Time:
303 """Convert simple form back to `astropy.time.Time`.
305 Parameters
306 ----------
307 simple : `tuple` [`float`, `float`]
308 The time represented by two MJDs.
309 **kwargs : `typing.Any`
310 Keyword arguments. Currently not used.
312 Returns
313 -------
314 t : `astropy.time.Time`
315 An astropy time object.
316 """
317 return astropy.time.Time(simple[0], val2=simple[1], format="jd", scale="tai")
320TimeAnnotated = _make_annotated(
321 astropy.time.Time,
322 simplifier=datetime_to_simple,
323 complexifier=simple_to_datetime,
324 json_schema={**_tuple_of_floats_schema(2), "x-format": "tai-jd"},
325)
328def exptime_to_simple(exptime: astropy.units.Quantity) -> float:
329 """Convert exposure time Quantity to seconds.
331 Parameters
332 ----------
333 exptime : `astropy.units.Quantity`
334 The exposure time as a quantity.
336 Returns
337 -------
338 e : `float`
339 Exposure time in seconds.
340 """
341 return _quantity_to_float(exptime, astropy.units.s)
344def simple_to_exptime(simple: float, **kwargs: Any) -> astropy.units.Quantity:
345 """Convert simple form back to Quantity.
347 Parameters
348 ----------
349 simple : `float`
350 Exposure time in seconds.
351 **kwargs : `typing.Any`
352 Keyword arguments. Currently not used.
354 Returns
355 -------
356 q : `astropy.units.Quantity`
357 The exposure time as a quantity.
358 """
359 return simple * astropy.units.s
362ExposureTimeAnnotated = _make_annotated(
363 astropy.units.Quantity,
364 simplifier=exptime_to_simple,
365 complexifier=simple_to_exptime,
366 json_schema=_quantity_in_unit_schema(astropy.units.s),
367)
370def angle_to_simple(angle: astropy.coordinates.Angle) -> float:
371 """Convert Angle to degrees.
373 Parameters
374 ----------
375 angle : `astropy.coordinates.Angle`
376 The angle.
378 Returns
379 -------
380 a : `float`
381 The angle in degrees.
382 """
383 return _quantity_to_float(angle, astropy.units.deg)
386def simple_to_angle(simple: float, **kwargs: Any) -> astropy.coordinates.Angle:
387 """Convert degrees to Angle.
389 Parameters
390 ----------
391 simple : `float`
392 The angle in degrees.
393 **kwargs : `typing.Any`
394 Keyword arguments. Currently not used.
396 Returns
397 -------
398 a : `astropy.coordinates.Angle`
399 The angle as an object.
400 """
401 # Quantity of 45. deg is not the same as Angle.
402 if isinstance(simple, astropy.units.Quantity):
403 angle = simple
404 else:
405 angle = simple * astropy.units.deg
406 return astropy.coordinates.Angle(angle)
409AngleAnnotated = _make_annotated(
410 astropy.coordinates.Angle,
411 simplifier=angle_to_simple,
412 complexifier=simple_to_angle,
413 json_schema=_quantity_in_unit_schema(astropy.units.deg),
414)
417def focusz_to_simple(focusz: astropy.units.Quantity) -> float:
418 """Convert focusz to meters.
420 Parameters
421 ----------
422 focusz : `astropy.units.Quantity`
423 The z-focus as a quantity.
425 Returns
426 -------
427 f : `float`
428 The z-focus in meters.
429 """
430 return _quantity_to_float(focusz, astropy.units.m)
433def simple_to_focusz(simple: float, **kwargs: Any) -> astropy.units.Quantity:
434 """Convert simple form back to Quantity.
436 Parameters
437 ----------
438 simple : `float`
439 The z-focus in meters.
440 **kwargs : `typing.Any`
441 Keyword arguments. Currently not used.
443 Returns
444 -------
445 q : `astropy.units.Quantity`
446 The z-focus as a quantity.
447 """
448 return simple * astropy.units.m
451FocusZAnnotated = _make_annotated(
452 astropy.units.Quantity,
453 simplifier=focusz_to_simple,
454 complexifier=simple_to_focusz,
455 json_schema=_quantity_in_unit_schema(astropy.units.m),
456)
459def temperature_to_simple(temp: astropy.units.Quantity) -> float:
460 """Convert temperature to kelvin.
462 Parameters
463 ----------
464 temp : `astropy.units.Quantity`
465 The temperature as a quantity.
467 Returns
468 -------
469 t : `float`
470 The temperature in kelvin.
471 """
472 q = temp.to(astropy.units.K, equivalencies=astropy.units.temperature())
473 return _quantity_to_float(q)
476def simple_to_temperature(simple: float, **kwargs: Any) -> astropy.units.Quantity:
477 """Convert scalar kelvin value back to quantity.
479 Parameters
480 ----------
481 simple : `float`
482 Temperature as a float in units of kelvin.
483 **kwargs : `typing.Any`
484 Keyword arguments. Currently not used.
486 Returns
487 -------
488 q : `astropy.units.Quantity`
489 The temperature as a quantity.
490 """
491 return simple * astropy.units.K
494TemperatureAnnotated = _make_annotated(
495 astropy.units.Quantity,
496 simplifier=temperature_to_simple,
497 complexifier=simple_to_temperature,
498 json_schema=_quantity_in_unit_schema(astropy.units.K),
499)
502def pressure_to_simple(press: astropy.units.Quantity) -> float:
503 """Convert pressure Quantity to hPa.
505 Parameters
506 ----------
507 press : `astropy.units.Quantity`
508 The pressure as a quantity.
510 Returns
511 -------
512 hpa : `float`
513 The pressure in units of hPa.
514 """
515 return _quantity_to_float(press, astropy.units.hPa)
518def simple_to_pressure(simple: float, **kwargs: Any) -> astropy.units.Quantity:
519 """Convert the pressure scalar back to Quantity.
521 Parameters
522 ----------
523 simple : `float`
524 Pressure in units of hPa.
525 **kwargs : `typing.Any`
526 Keyword arguments. Currently not used.
528 Returns
529 -------
530 q : `astropy.units.Quantity`
531 The pressure as a quantity.
532 """
533 return simple * astropy.units.hPa
536PressureAnnotated = _make_annotated(
537 astropy.units.Quantity,
538 simplifier=pressure_to_simple,
539 complexifier=simple_to_pressure,
540 json_schema=_quantity_in_unit_schema(astropy.units.hPa),
541)
544def skycoord_to_simple(skycoord: astropy.coordinates.SkyCoord) -> tuple[float, float]:
545 """Convert SkyCoord to ICRS RA/Dec tuple.
547 Parameters
548 ----------
549 skycoord : `astropy.coordinates.SkyCoord`
550 Sky coordinates in astropy form.
552 Returns
553 -------
554 simple : `tuple` [`float`, `float`]
555 Sky coordinates as a tuple of two floats in units of degrees.
556 """
557 icrs = skycoord.icrs
558 if not isinstance(icrs, astropy.coordinates.SkyCoord):
559 raise ValueError(f"Could not extract ICRS coordinates from SkyCoord {skycoord}")
560 ra = icrs.ra
561 assert isinstance(ra, astropy.coordinates.Longitude)
562 dec = icrs.dec
563 assert isinstance(dec, astropy.coordinates.Latitude)
564 return (_quantity_to_float(ra, astropy.units.deg), _quantity_to_float(dec, astropy.units.deg))
567def simple_to_skycoord(simple: tuple[float, float], **kwargs: Any) -> astropy.coordinates.SkyCoord:
568 """Convert ICRS tuple to SkyCoord.
570 Parameters
571 ----------
572 simple : `tuple` [`float`, `float`]
573 Sky coordinates in degrees.
574 **kwargs : `typing.Any`
575 Keyword arguments. Currently not used.
577 Returns
578 -------
579 skycoord : `astropy.coordinates.SkyCoord`
580 The sky coordinates in astropy form.
581 """
582 return astropy.coordinates.SkyCoord(*simple, unit=astropy.units.deg)
585SkyCoordAnnotated = _make_annotated(
586 astropy.coordinates.SkyCoord,
587 simplifier=skycoord_to_simple,
588 complexifier=simple_to_skycoord,
589 json_schema={**_tuple_of_floats_schema(2), "x-unit": "deg", "x-frame": "icrs"},
590)
593def altaz_to_simple(altaz: astropy.coordinates.AltAz) -> tuple[float, float]:
594 """Convert AltAz to Alt/Az tuple.
596 Do not include obstime or location in simplification. It is assumed
597 that those will be present from other properties.
599 Parameters
600 ----------
601 altaz : `astropy.coordinates.AltAz`
602 The alt/az in astropy form.
604 Returns
605 -------
606 simple : `tuple` [`float`, `float`]
607 The Alt/Az as a tuple of two floats representing the position in
608 units of degrees.
609 """
610 return (_quantity_to_float(altaz.az, astropy.units.deg), _quantity_to_float(altaz.alt, astropy.units.deg))
613def simple_to_altaz(simple: tuple[float, float], **kwargs: Any) -> astropy.coordinates.AltAz:
614 """Convert simple altaz tuple to AltAz.
616 Parameters
617 ----------
618 simple : `tuple` [`float`, `float`]
619 Altitude and elevation in degrees.
620 **kwargs : `dict`
621 Additional information. Must contain ``location`` and
622 ``datetime_begin``.
624 Returns
625 -------
626 altaz : `astropy.coordinates.AltAz`
627 The altaz in astropy form.
628 """
629 # Sometimes we get given a SkyCoord that contains an AltAz frame that needs
630 # to be extracted.
631 if isinstance(simple, astropy.coordinates.SkyCoord):
632 frame = simple.frame
633 if isinstance(frame, astropy.coordinates.AltAz):
634 return frame
635 # If there is no AltAz frame, return what we have so that downstream
636 # validation can fail.
637 return simple
639 location = kwargs.get("location")
640 obstime = kwargs.get("datetime_begin")
642 return astropy.coordinates.AltAz(
643 simple[0] * astropy.units.deg, simple[1] * astropy.units.deg, obstime=obstime, location=location
644 )
647AltAzAnnotated = _make_annotated(
648 astropy.coordinates.AltAz,
649 simplifier=altaz_to_simple,
650 complexifier=simple_to_altaz,
651 json_schema={**_tuple_of_floats_schema(2), "x-unit": "deg", "x-format": "az-alt"},
652)
655def timedelta_to_simple(delta: astropy.time.TimeDelta) -> int:
656 """Convert a TimeDelta to integer seconds.
658 This property does not need to support floating point seconds.
660 Parameters
661 ----------
662 delta : `astropy.time.TimeDelta`
663 The time offset.
665 Returns
666 -------
667 sec : `int`
668 Offset in integer seconds.
669 """
670 return round(_quantity_to_float(delta, astropy.units.s))
673def simple_to_timedelta(simple: int, **kwargs: Any) -> astropy.time.TimeDelta:
674 """Convert integer seconds to a `~astropy.time.TimeDelta`.
676 Parameters
677 ----------
678 simple : `int`
679 The offset in integer seconds.
680 **kwargs : `dict`
681 Additional information. Unused.
683 Returns
684 -------
685 delta : `astropy.time.TimeDelta`
686 The delta object.
687 """
688 return astropy.time.TimeDelta(simple, format="sec", scale="tai")
691TimeDeltaAnnotated = _make_annotated(
692 astropy.time.TimeDelta,
693 simplifier=timedelta_to_simple,
694 complexifier=simple_to_timedelta,
695 json_schema={"type": "integer", "x-unit": "s"},
696)
699class PropertyDefinition:
700 """Definition of an instrumental property.
702 Supports both signatures:
704 - ``(doc, py_type, to_simple=None, from_simple=None)``
705 - ``(doc, legacy_str_type, py_type, to_simple=None, from_simple=None)``
707 Modern preference is to not specify the string type since that can be
708 derived directly from the python type.
710 Parameters
711 ----------
712 doc : `str`
713 Documentation string for the property.
714 *args : `typing.Any`
715 Remaining constructor arguments in one of the supported
716 signatures.
717 """
719 __slots__ = ("doc", "py_type", "to_simple", "from_simple")
721 doc: str
722 py_type: type
723 to_simple: Callable[[Any], Any] | None
724 from_simple: Callable[[Any], Any] | None
726 def __init__(self, doc: str, *args: Any) -> None:
727 if not args: 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true
728 raise TypeError("PropertyDefinition requires at least a py_type argument")
730 if isinstance(args[0], str):
731 if len(args) < 2 or not isinstance(args[1], type): 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true
732 raise TypeError("Legacy PropertyDefinition signature requires (doc, str_type, py_type, ...)")
733 py_type = args[1]
734 rest = args[2:]
735 else:
736 if not isinstance(args[0], type): 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true
737 raise TypeError("PropertyDefinition py_type must be a type")
738 py_type = args[0]
739 rest = args[1:]
741 if len(rest) > 2: 741 ↛ 742line 741 didn't jump to line 742 because the condition on line 741 was never true
742 raise TypeError("PropertyDefinition accepts at most two converter callables")
744 to_simple: Callable[[Any], Any] | None = rest[0] if rest else None
745 from_simple: Callable[[Any], Any] | None = rest[1] if len(rest) > 1 else None
747 self.doc = doc
748 self.py_type = py_type
749 self.to_simple = to_simple
750 self.from_simple = from_simple
752 @property
753 def str_type(self) -> str:
754 """Python type of property as a string suitable for messages/docs."""
755 if self.py_type.__module__ == "builtins":
756 return self.py_type.__name__
757 return f"{self.py_type.__module__}.{self.py_type.__qualname__}"
759 def is_value_conformant(self, value: Any) -> bool:
760 """Compare the supplied value against the expected type as defined
761 for this property.
763 Parameters
764 ----------
765 value : `object`
766 Value of the property to validate. Can be `None`.
768 Returns
769 -------
770 is_ok : `bool`
771 `True` if the value is of an appropriate type.
773 Notes
774 -----
775 Currently only the type of the property is validated. There is no
776 attempt to check bounds or determine that a Quantity is compatible
777 with the property.
778 """
779 if value is None:
780 return True
782 return isinstance(value, self.py_type)
785# This dict defines all the core properties of an ObservationInfo.
786# The PropertyDefinition is keyed by the property name.
787# The doc string is used to define the Pydantic model.
788# The py_type/doc are used to create the translator methods.
789# The optional callables are used to convert types for serialization and
790# validation.
791PROPERTIES = {
792 "telescope": PropertyDefinition("Full name of the telescope.", str),
793 "instrument": PropertyDefinition("The instrument used to observe the exposure.", str),
794 "location": PropertyDefinition(
795 "Location of the observatory.",
796 astropy.coordinates.EarthLocation,
797 earthlocation_to_simple,
798 simple_to_earthlocation,
799 ),
800 "exposure_id": PropertyDefinition(
801 "Unique (with instrument) integer identifier for this observation.", int
802 ),
803 "visit_id": PropertyDefinition(
804 """ID of the Visit this Exposure is associated with.
806Science observations should essentially always be
807associated with a visit, but calibration observations
808may not be.""",
809 int,
810 ),
811 "physical_filter": PropertyDefinition("The bandpass filter used for this observation.", str),
812 "datetime_begin": PropertyDefinition(
813 "Time of the start of the observation.",
814 astropy.time.Time,
815 datetime_to_simple,
816 simple_to_datetime,
817 ),
818 "datetime_end": PropertyDefinition(
819 "Time of the end of the observation.",
820 astropy.time.Time,
821 datetime_to_simple,
822 simple_to_datetime,
823 ),
824 "exposure_time": PropertyDefinition(
825 "Actual duration of the exposure (seconds).",
826 astropy.units.Quantity,
827 exptime_to_simple,
828 simple_to_exptime,
829 ),
830 "exposure_time_requested": PropertyDefinition(
831 "Requested duration of the exposure (seconds).",
832 astropy.units.Quantity,
833 exptime_to_simple,
834 simple_to_exptime,
835 ),
836 "dark_time": PropertyDefinition(
837 "Duration of the exposure with shutter closed (seconds).",
838 astropy.units.Quantity,
839 exptime_to_simple,
840 simple_to_exptime,
841 ),
842 "boresight_airmass": PropertyDefinition("Airmass of the boresight of the telescope.", float),
843 "boresight_rotation_angle": PropertyDefinition(
844 "Angle of the instrument in boresight_rotation_coord frame.",
845 astropy.coordinates.Angle,
846 angle_to_simple,
847 simple_to_angle,
848 ),
849 "boresight_rotation_coord": PropertyDefinition(
850 "Coordinate frame of the instrument rotation angle (options: sky, unknown).",
851 str,
852 ),
853 "detector_num": PropertyDefinition("Unique (for instrument) integer identifier for the sensor.", int),
854 "detector_name": PropertyDefinition(
855 "Name of the detector within the instrument (might not be unique if there are detector groups).",
856 str,
857 ),
858 "detector_unique_name": PropertyDefinition(
859 (
860 "Unique name of the detector within the focal plane, generally combining detector_group with "
861 "detector_name."
862 ),
863 str,
864 ),
865 "detector_serial": PropertyDefinition("Serial number/string associated with this detector.", str),
866 "detector_group": PropertyDefinition(
867 "Collection name of which this detector is a part. Can be None if there are no detector groupings.",
868 str,
869 ),
870 "detector_exposure_id": PropertyDefinition(
871 "Unique integer identifier for this detector in this exposure.",
872 int,
873 ),
874 "focus_z": PropertyDefinition(
875 "Defocal distance.",
876 astropy.units.Quantity,
877 focusz_to_simple,
878 simple_to_focusz,
879 ),
880 "object": PropertyDefinition("Object of interest or field name.", str),
881 "temperature": PropertyDefinition(
882 "Temperature outside the dome.",
883 astropy.units.Quantity,
884 temperature_to_simple,
885 simple_to_temperature,
886 ),
887 "pressure": PropertyDefinition(
888 "Atmospheric pressure outside the dome.",
889 astropy.units.Quantity,
890 pressure_to_simple,
891 simple_to_pressure,
892 ),
893 "relative_humidity": PropertyDefinition("Relative humidity outside the dome.", float),
894 "tracking_radec": PropertyDefinition(
895 "Requested RA/Dec to track.",
896 astropy.coordinates.SkyCoord,
897 skycoord_to_simple,
898 simple_to_skycoord,
899 ),
900 "altaz_begin": PropertyDefinition(
901 "Telescope boresight azimuth and elevation at start of observation.",
902 astropy.coordinates.AltAz,
903 altaz_to_simple,
904 simple_to_altaz,
905 ),
906 "altaz_end": PropertyDefinition(
907 "Telescope boresight azimuth and elevation at end of observation.",
908 astropy.coordinates.AltAz,
909 altaz_to_simple,
910 simple_to_altaz,
911 ),
912 "science_program": PropertyDefinition("Observing program (survey or proposal) identifier.", str),
913 "observation_type": PropertyDefinition(
914 "Type of observation (currently: science, dark, flat, bias, focus).",
915 str,
916 ),
917 "observation_id": PropertyDefinition(
918 "Label uniquely identifying this observation (can be related to 'exposure_id').",
919 str,
920 ),
921 "observation_reason": PropertyDefinition(
922 "Reason this observation was taken, or its purpose ('science' and 'calibration' are common values)",
923 str,
924 ),
925 "exposure_group": PropertyDefinition(
926 "Label to use to associate this exposure with others (can be related to 'exposure_id').",
927 str,
928 ),
929 "observing_day": PropertyDefinition(
930 "Integer in YYYYMMDD format corresponding to the day of observation.", int
931 ),
932 "observing_day_offset": PropertyDefinition(
933 (
934 "Offset to subtract from an observation date when calculating the observing day. "
935 "Conversely, the offset to add to an observing day when calculating the time span of a day."
936 ),
937 astropy.time.TimeDelta,
938 timedelta_to_simple,
939 simple_to_timedelta,
940 ),
941 "observation_counter": PropertyDefinition(
942 (
943 "Counter of this observation. Can be counter within observing_day or a global counter. "
944 "Likely to be observatory specific."
945 ),
946 int,
947 ),
948 "has_simulated_content": PropertyDefinition(
949 "Boolean indicating whether any part of this observation was simulated.", bool, None, None
950 ),
951 "group_counter_start": PropertyDefinition(
952 "Observation counter for the start of the exposure group."
953 "Depending on the instrument the relevant group may be "
954 "visit_id or exposure_group.",
955 int,
956 None,
957 None,
958 ),
959 "group_counter_end": PropertyDefinition(
960 "Observation counter for the end of the exposure group. "
961 "Depending on the instrument the relevant group may be "
962 "visit_id or exposure_group.",
963 int,
964 None,
965 None,
966 ),
967 "can_see_sky": PropertyDefinition(
968 "True if the observation is looking at sky, False if it is definitely"
969 " not looking at the sky. None indicates that it is not known whether"
970 " sky could be seen.",
971 bool,
972 ),
973}