Coverage for python / astro_metadata_translator / properties.py: 62%
66 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-20 01:28 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-20 01:28 -0800
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 dataclasses import dataclass
30from typing import Any
32import astropy.coordinates
33import astropy.time
34import astropy.units
36# Helper functions to convert complex types to simple form suitable
37# for JSON serialization
38# All take the complex type and return simple python form using str, float,
39# int, dict, or list.
40# All assume the supplied parameter is not None.
43def earthlocation_to_simple(location: astropy.coordinates.EarthLocation) -> tuple[float, ...]:
44 """Convert EarthLocation to tuple.
46 Parameters
47 ----------
48 location : `astropy.coordinates.EarthLocation`
49 The location to simplify.
51 Returns
52 -------
53 geocentric : `tuple` of (`float`, `float`, `float`)
54 The geocentric location as three floats in meters.
55 """
56 geocentric = location.to_geocentric()
57 return tuple(c.to_value(astropy.units.m) for c in geocentric)
60def simple_to_earthlocation(simple: tuple[float, ...], **kwargs: Any) -> astropy.coordinates.EarthLocation:
61 """Convert simple form back to EarthLocation.
63 Parameters
64 ----------
65 simple : `tuple` [`float`, ...]
66 The geocentric location as three floats in meters.
67 **kwargs : `typing.Any`
68 Keyword arguments. Currently not used.
70 Returns
71 -------
72 loc : `astropy.coordinates.EarthLocation`
73 The location on the Earth.
74 """
75 return astropy.coordinates.EarthLocation.from_geocentric(*simple, unit=astropy.units.m)
78def datetime_to_simple(datetime: astropy.time.Time) -> tuple[float, float]:
79 """Convert Time to tuple.
81 Parameters
82 ----------
83 datetime : `astropy.time.Time`
84 The time to simplify.
86 Returns
87 -------
88 mjds : `tuple` of (`float`, `float`)
89 The two MJDs in TAI.
90 """
91 tai = datetime.tai
92 return (tai.jd1, tai.jd2)
95def simple_to_datetime(simple: tuple[float, float], **kwargs: Any) -> astropy.time.Time:
96 """Convert simple form back to `astropy.time.Time`.
98 Parameters
99 ----------
100 simple : `tuple` [`float`, `float`]
101 The time represented by two MJDs.
102 **kwargs : `typing.Any`
103 Keyword arguments. Currently not used.
105 Returns
106 -------
107 t : `astropy.time.Time`
108 An astropy time object.
109 """
110 return astropy.time.Time(*simple, format="jd", scale="tai")
113def exptime_to_simple(exptime: astropy.units.Quantity) -> float:
114 """Convert exposure time Quantity to seconds.
116 Parameters
117 ----------
118 exptime : `astropy.units.Quantity`
119 The exposure time as a quantity.
121 Returns
122 -------
123 e : `float`
124 Exposure time in seconds.
125 """
126 return exptime.to_value(astropy.units.s)
129def simple_to_exptime(simple: float, **kwargs: Any) -> astropy.units.Quantity:
130 """Convert simple form back to Quantity.
132 Parameters
133 ----------
134 simple : `float`
135 Exposure time in seconds.
136 **kwargs : `typing.Any`
137 Keyword arguments. Currently not used.
139 Returns
140 -------
141 q : `astropy.units.Quantity`
142 The exposure time as a quantity.
143 """
144 return simple * astropy.units.s
147def angle_to_simple(angle: astropy.coordinates.Angle) -> float:
148 """Convert Angle to degrees.
150 Parameters
151 ----------
152 angle : `astropy.coordinates.Angle`
153 The angle.
155 Returns
156 -------
157 a : `float`
158 The angle in degrees.
159 """
160 return angle.to_value(astropy.units.deg)
163def simple_to_angle(simple: float, **kwargs: Any) -> astropy.coordinates.Angle:
164 """Convert degrees to Angle.
166 Parameters
167 ----------
168 simple : `float`
169 The angle in degrees.
170 **kwargs : `typing.Any`
171 Keyword arguments. Currently not used.
173 Returns
174 -------
175 a : `astropy.coordinates.Angle`
176 The angle as an object.
177 """
178 return astropy.coordinates.Angle(simple * astropy.units.deg)
181def focusz_to_simple(focusz: astropy.units.Quantity) -> float:
182 """Convert focusz to meters.
184 Parameters
185 ----------
186 focusz : `astropy.units.Quantity`
187 The z-focus as a quantity.
189 Returns
190 -------
191 f : `float`
192 The z-focus in meters.
193 """
194 return focusz.to_value(astropy.units.m)
197def simple_to_focusz(simple: float, **kwargs: Any) -> astropy.units.Quantity:
198 """Convert simple form back to Quantity.
200 Parameters
201 ----------
202 simple : `float`
203 The z-focus in meters.
204 **kwargs : `typing.Any`
205 Keyword arguments. Currently not used.
207 Returns
208 -------
209 q : `astropy.units.Quantity`
210 The z-focus as a quantity.
211 """
212 return simple * astropy.units.m
215def temperature_to_simple(temp: astropy.units.Quantity) -> float:
216 """Convert temperature to kelvin.
218 Parameters
219 ----------
220 temp : `astropy.units.Quantity`
221 The temperature as a quantity.
223 Returns
224 -------
225 t : `float`
226 The temperature in kelvin.
227 """
228 return temp.to(astropy.units.K, equivalencies=astropy.units.temperature()).to_value()
231def simple_to_temperature(simple: float, **kwargs: Any) -> astropy.units.Quantity:
232 """Convert scalar kelvin value back to quantity.
234 Parameters
235 ----------
236 simple : `float`
237 Temperature as a float in units of kelvin.
238 **kwargs : `typing.Any`
239 Keyword arguments. Currently not used.
241 Returns
242 -------
243 q : `astropy.units.Quantity`
244 The temperature as a quantity.
245 """
246 return simple * astropy.units.K
249def pressure_to_simple(press: astropy.units.Quantity) -> float:
250 """Convert pressure Quantity to hPa.
252 Parameters
253 ----------
254 press : `astropy.units.Quantity`
255 The pressure as a quantity.
257 Returns
258 -------
259 hpa : `float`
260 The pressure in units of hPa.
261 """
262 return press.to_value(astropy.units.hPa)
265def simple_to_pressure(simple: float, **kwargs: Any) -> astropy.units.Quantity:
266 """Convert the pressure scalar back to Quantity.
268 Parameters
269 ----------
270 simple : `float`
271 Pressure in units of hPa.
272 **kwargs : `typing.Any`
273 Keyword arguments. Currently not used.
275 Returns
276 -------
277 q : `astropy.units.Quantity`
278 The pressure as a quantity.
279 """
280 return simple * astropy.units.hPa
283def skycoord_to_simple(skycoord: astropy.coordinates.SkyCoord) -> tuple[float, float]:
284 """Convert SkyCoord to ICRS RA/Dec tuple.
286 Parameters
287 ----------
288 skycoord : `astropy.coordinates.SkyCoord`
289 Sky coordinates in astropy form.
291 Returns
292 -------
293 simple : `tuple` [`float`, `float`]
294 Sky coordinates as a tuple of two floats in units of degrees.
295 """
296 icrs = skycoord.icrs
297 return (icrs.ra.to_value(astropy.units.deg), icrs.dec.to_value(astropy.units.deg))
300def simple_to_skycoord(simple: tuple[float, float], **kwargs: Any) -> astropy.coordinates.SkyCoord:
301 """Convert ICRS tuple to SkyCoord.
303 Parameters
304 ----------
305 simple : `tuple` [`float`, `float`]
306 Sky coordinates in degrees.
307 **kwargs : `typing.Any`
308 Keyword arguments. Currently not used.
310 Returns
311 -------
312 skycoord : `astropy.coordinates.SkyCoord`
313 The sky coordinates in astropy form.
314 """
315 return astropy.coordinates.SkyCoord(*simple, unit=astropy.units.deg)
318def altaz_to_simple(altaz: astropy.coordinates.AltAz) -> tuple[float, float]:
319 """Convert AltAz to Alt/Az tuple.
321 Do not include obstime or location in simplification. It is assumed
322 that those will be present from other properties.
324 Parameters
325 ----------
326 altaz : `astropy.coordinates.AltAz`
327 The alt/az in astropy form.
329 Returns
330 -------
331 simple : `tuple` [`float`, `float`]
332 The Alt/Az as a tuple of two floats representing the position in
333 units of degrees.
334 """
335 return (altaz.az.to_value(astropy.units.deg), altaz.alt.to_value(astropy.units.deg))
338def simple_to_altaz(simple: tuple[float, float], **kwargs: Any) -> astropy.coordinates.AltAz:
339 """Convert simple altaz tuple to AltAz.
341 Parameters
342 ----------
343 simple : `tuple` [`float`, `float`]
344 Altitude and elevation in degrees.
345 **kwargs : `dict`
346 Additional information. Must contain ``location`` and
347 ``datetime_begin``.
349 Returns
350 -------
351 altaz : `astropy.coordinates.AltAz`
352 The altaz in astropy form.
353 """
354 location = kwargs.get("location")
355 obstime = kwargs.get("datetime_begin")
357 return astropy.coordinates.AltAz(
358 simple[0] * astropy.units.deg, simple[1] * astropy.units.deg, obstime=obstime, location=location
359 )
362def timedelta_to_simple(delta: astropy.time.TimeDelta) -> int:
363 """Convert a TimeDelta to integer seconds.
365 This property does not need to support floating point seconds.
367 Parameters
368 ----------
369 delta : `astropy.time.TimeDelta`
370 The time offset.
372 Returns
373 -------
374 sec : `int`
375 Offset in integer seconds.
376 """
377 return round(delta.to_value("s"))
380def simple_to_timedelta(simple: int, **kwargs: Any) -> astropy.time.TimeDelta:
381 """Convert integer seconds to a `~astropy.time.TimeDelta`.
383 Parameters
384 ----------
385 simple : `int`
386 The offset in integer seconds.
387 **kwargs : `dict`
388 Additional information. Unused.
390 Returns
391 -------
392 delta : `astropy.time.TimeDelta`
393 The delta object.
394 """
395 return astropy.time.TimeDelta(simple, format="sec", scale="tai")
398@dataclass
399class PropertyDefinition:
400 """Definition of an instrumental property."""
402 doc: str
403 """Docstring for the property."""
405 str_type: str
406 """Python type of property as a string (suitable for docstrings)."""
408 py_type: type
409 """Actual python type."""
411 to_simple: Callable[[Any], Any] | None = None
412 """Function to convert value to simple form (can be ``None``)."""
414 from_simple: Callable[[Any], Any] | None = None
415 """Function to convert from simple form back to required type (can be
416 ``None``)."""
419# Dict of properties to tuple where tuple is:
420# - description of property
421# - Python type of property as a string (suitable for docstrings)
422# - Actual python type as a type
423# - Simplification function (can be None)
424# - Function to convert simple form back to required type (can be None)
425PROPERTIES = {
426 "telescope": PropertyDefinition("Full name of the telescope.", "str", str),
427 "instrument": PropertyDefinition("The instrument used to observe the exposure.", "str", str),
428 "location": PropertyDefinition(
429 "Location of the observatory.",
430 "astropy.coordinates.EarthLocation",
431 astropy.coordinates.EarthLocation,
432 earthlocation_to_simple,
433 simple_to_earthlocation,
434 ),
435 "exposure_id": PropertyDefinition(
436 "Unique (with instrument) integer identifier for this observation.", "int", int
437 ),
438 "visit_id": PropertyDefinition(
439 """ID of the Visit this Exposure is associated with.
441Science observations should essentially always be
442associated with a visit, but calibration observations
443may not be.""",
444 "int",
445 int,
446 ),
447 "physical_filter": PropertyDefinition("The bandpass filter used for this observation.", "str", str),
448 "datetime_begin": PropertyDefinition(
449 "Time of the start of the observation.",
450 "astropy.time.Time",
451 astropy.time.Time,
452 datetime_to_simple,
453 simple_to_datetime,
454 ),
455 "datetime_end": PropertyDefinition(
456 "Time of the end of the observation.",
457 "astropy.time.Time",
458 astropy.time.Time,
459 datetime_to_simple,
460 simple_to_datetime,
461 ),
462 "exposure_time": PropertyDefinition(
463 "Actual duration of the exposure (seconds).",
464 "astropy.units.Quantity",
465 astropy.units.Quantity,
466 exptime_to_simple,
467 simple_to_exptime,
468 ),
469 "exposure_time_requested": PropertyDefinition(
470 "Requested duration of the exposure (seconds).",
471 "astropy.units.Quantity",
472 astropy.units.Quantity,
473 exptime_to_simple,
474 simple_to_exptime,
475 ),
476 "dark_time": PropertyDefinition(
477 "Duration of the exposure with shutter closed (seconds).",
478 "astropy.units.Quantity",
479 astropy.units.Quantity,
480 exptime_to_simple,
481 simple_to_exptime,
482 ),
483 "boresight_airmass": PropertyDefinition("Airmass of the boresight of the telescope.", "float", float),
484 "boresight_rotation_angle": PropertyDefinition(
485 "Angle of the instrument in boresight_rotation_coord frame.",
486 "astropy.coordinates.Angle",
487 astropy.coordinates.Angle,
488 angle_to_simple,
489 simple_to_angle,
490 ),
491 "boresight_rotation_coord": PropertyDefinition(
492 "Coordinate frame of the instrument rotation angle (options: sky, unknown).",
493 "str",
494 str,
495 ),
496 "detector_num": PropertyDefinition(
497 "Unique (for instrument) integer identifier for the sensor.", "int", int
498 ),
499 "detector_name": PropertyDefinition(
500 "Name of the detector within the instrument (might not be unique if there are detector groups).",
501 "str",
502 str,
503 ),
504 "detector_unique_name": PropertyDefinition(
505 (
506 "Unique name of the detector within the focal plane, generally combining detector_group with "
507 "detector_name."
508 ),
509 "str",
510 str,
511 ),
512 "detector_serial": PropertyDefinition("Serial number/string associated with this detector.", "str", str),
513 "detector_group": PropertyDefinition(
514 "Collection name of which this detector is a part. Can be None if there are no detector groupings.",
515 "str",
516 str,
517 ),
518 "detector_exposure_id": PropertyDefinition(
519 "Unique integer identifier for this detector in this exposure.",
520 "int",
521 int,
522 ),
523 "focus_z": PropertyDefinition(
524 "Defocal distance.",
525 "astropy.units.Quantity",
526 astropy.units.Quantity,
527 focusz_to_simple,
528 simple_to_focusz,
529 ),
530 "object": PropertyDefinition("Object of interest or field name.", "str", str),
531 "temperature": PropertyDefinition(
532 "Temperature outside the dome.",
533 "astropy.units.Quantity",
534 astropy.units.Quantity,
535 temperature_to_simple,
536 simple_to_temperature,
537 ),
538 "pressure": PropertyDefinition(
539 "Atmospheric pressure outside the dome.",
540 "astropy.units.Quantity",
541 astropy.units.Quantity,
542 pressure_to_simple,
543 simple_to_pressure,
544 ),
545 "relative_humidity": PropertyDefinition("Relative humidity outside the dome.", "float", float),
546 "tracking_radec": PropertyDefinition(
547 "Requested RA/Dec to track.",
548 "astropy.coordinates.SkyCoord",
549 astropy.coordinates.SkyCoord,
550 skycoord_to_simple,
551 simple_to_skycoord,
552 ),
553 "altaz_begin": PropertyDefinition(
554 "Telescope boresight azimuth and elevation at start of observation.",
555 "astropy.coordinates.AltAz",
556 astropy.coordinates.AltAz,
557 altaz_to_simple,
558 simple_to_altaz,
559 ),
560 "altaz_end": PropertyDefinition(
561 "Telescope boresight azimuth and elevation at end of observation.",
562 "astropy.coordinates.AltAz",
563 astropy.coordinates.AltAz,
564 altaz_to_simple,
565 simple_to_altaz,
566 ),
567 "science_program": PropertyDefinition("Observing program (survey or proposal) identifier.", "str", str),
568 "observation_type": PropertyDefinition(
569 "Type of observation (currently: science, dark, flat, bias, focus).",
570 "str",
571 str,
572 ),
573 "observation_id": PropertyDefinition(
574 "Label uniquely identifying this observation (can be related to 'exposure_id').",
575 "str",
576 str,
577 ),
578 "observation_reason": PropertyDefinition(
579 "Reason this observation was taken, or its purpose ('science' and 'calibration' are common values)",
580 "str",
581 str,
582 ),
583 "exposure_group": PropertyDefinition(
584 "Label to use to associate this exposure with others (can be related to 'exposure_id').",
585 "str",
586 str,
587 ),
588 "observing_day": PropertyDefinition(
589 "Integer in YYYYMMDD format corresponding to the day of observation.", "int", int
590 ),
591 "observing_day_offset": PropertyDefinition(
592 (
593 "Offset to subtract from an observation date when calculating the observing day. "
594 "Conversely, the offset to add to an observing day when calculating the time span of a day."
595 ),
596 "astropy.time.TimeDelta",
597 astropy.time.TimeDelta,
598 timedelta_to_simple,
599 simple_to_timedelta,
600 ),
601 "observation_counter": PropertyDefinition(
602 (
603 "Counter of this observation. Can be counter within observing_day or a global counter. "
604 "Likely to be observatory specific."
605 ),
606 "int",
607 int,
608 ),
609 "has_simulated_content": PropertyDefinition(
610 "Boolean indicating whether any part of this observation was simulated.", "bool", bool, None, None
611 ),
612 "group_counter_start": PropertyDefinition(
613 "Observation counter for the start of the exposure group."
614 "Depending on the instrument the relevant group may be "
615 "visit_id or exposure_group.",
616 "int",
617 int,
618 None,
619 None,
620 ),
621 "group_counter_end": PropertyDefinition(
622 "Observation counter for the end of the exposure group. "
623 "Depending on the instrument the relevant group may be "
624 "visit_id or exposure_group.",
625 "int",
626 int,
627 None,
628 None,
629 ),
630 "can_see_sky": PropertyDefinition(
631 "True if the observation is looking at sky, False if it is definitely"
632 " not looking at the sky. None indicates that it is not known whether"
633 " sky could be seen.",
634 "bool",
635 bool,
636 ),
637}