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