Coverage for python / astro_metadata_translator / properties.py: 57%

148 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-23 08:08 +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. 

11 

12"""Properties calculated by this package. 

13 

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""" 

20 

21from __future__ import annotations 

22 

23__all__ = ( 

24 "PROPERTIES", 

25 "PropertyDefinition", 

26) 

27 

28from collections.abc import Callable 

29from typing import Annotated, Any, Protocol, SupportsFloat, cast 

30 

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 

38 

39 

40class _AstropyFieldAdapter: 

41 """Pydantic adapter that round-trips an astropy-like value through a 

42 simple JSON-friendly representation. 

43 

44 The adapter provides: 

45 

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``. 

52 

53 All three concerns are collapsed into one Annotated metadata object so 

54 each field annotation reads simply as ``py_type``. 

55 

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 """ 

71 

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 

84 

85 def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema: 

86 py_type = self._py_type 

87 complexifier = self._complexifier 

88 

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) 

94 

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 ) 

103 

104 def __get_pydantic_json_schema__( 

105 self, core_schema_obj: CoreSchema, handler: GetJsonSchemaHandler 

106 ) -> JsonSchemaValue: 

107 return dict(self._json_schema) 

108 

109 

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`. 

118 

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. 

130 

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 ] 

145 

146 

147def _tuple_of_floats_schema(n: int) -> JsonSchemaValue: 

148 """Return a JSON Schema fragment for a fixed-length tuple of floats. 

149 

150 Parameters 

151 ---------- 

152 n : `int` 

153 Number of elements in the tuple. 

154 

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 } 

166 

167 

168def _quantity_in_unit_schema(unit: astropy.units.UnitBase) -> JsonSchemaValue: 

169 """Return a JSON Schema fragment for a `Quantity` serialized as a float. 

170 

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. 

174 

175 Parameters 

176 ---------- 

177 unit : `astropy.units.UnitBase` 

178 Unit assumed for the serialized float value. 

179 

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)} 

186 

187 

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. 

193 

194 

195class _ToValueProtocol(Protocol): 

196 """Protocol for Quantity-like class that has to_value method.""" 

197 

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. 

200 

201 Parameters 

202 ---------- 

203 unit : `astropy.units.UnitBase` or `None`, optional 

204 Optional unit to use when converting the values to floats. 

205 """ 

206 ... 

207 

208 

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. 

212 

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. 

220 

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) 

235 

236 

237def earthlocation_to_simple( 

238 location: astropy.coordinates.EarthLocation, 

239) -> tuple[float, float, float]: 

240 """Convert EarthLocation to tuple. 

241 

242 Parameters 

243 ---------- 

244 location : `astropy.coordinates.EarthLocation` 

245 The location to simplify. 

246 

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 ) 

257 

258 

259def simple_to_earthlocation(simple: tuple[float, ...], **kwargs: Any) -> astropy.coordinates.EarthLocation: 

260 """Convert simple form back to EarthLocation. 

261 

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. 

268 

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) 

275 

276 

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) 

283 

284 

285def datetime_to_simple(datetime: astropy.time.Time) -> tuple[float, float]: 

286 """Convert Time to tuple. 

287 

288 Parameters 

289 ---------- 

290 datetime : `astropy.time.Time` 

291 The time to simplify. 

292 

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) 

300 

301 

302def simple_to_datetime(simple: tuple[float, float], **kwargs: Any) -> astropy.time.Time: 

303 """Convert simple form back to `astropy.time.Time`. 

304 

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. 

311 

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") 

318 

319 

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) 

326 

327 

328def exptime_to_simple(exptime: astropy.units.Quantity) -> float: 

329 """Convert exposure time Quantity to seconds. 

330 

331 Parameters 

332 ---------- 

333 exptime : `astropy.units.Quantity` 

334 The exposure time as a quantity. 

335 

336 Returns 

337 ------- 

338 e : `float` 

339 Exposure time in seconds. 

340 """ 

341 return _quantity_to_float(exptime, astropy.units.s) 

342 

343 

344def simple_to_exptime(simple: float, **kwargs: Any) -> astropy.units.Quantity: 

345 """Convert simple form back to Quantity. 

346 

347 Parameters 

348 ---------- 

349 simple : `float` 

350 Exposure time in seconds. 

351 **kwargs : `typing.Any` 

352 Keyword arguments. Currently not used. 

353 

354 Returns 

355 ------- 

356 q : `astropy.units.Quantity` 

357 The exposure time as a quantity. 

358 """ 

359 return simple * astropy.units.s 

360 

361 

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) 

368 

369 

370def angle_to_simple(angle: astropy.coordinates.Angle) -> float: 

371 """Convert Angle to degrees. 

372 

373 Parameters 

374 ---------- 

375 angle : `astropy.coordinates.Angle` 

376 The angle. 

377 

378 Returns 

379 ------- 

380 a : `float` 

381 The angle in degrees. 

382 """ 

383 return _quantity_to_float(angle, astropy.units.deg) 

384 

385 

386def simple_to_angle(simple: float, **kwargs: Any) -> astropy.coordinates.Angle: 

387 """Convert degrees to Angle. 

388 

389 Parameters 

390 ---------- 

391 simple : `float` 

392 The angle in degrees. 

393 **kwargs : `typing.Any` 

394 Keyword arguments. Currently not used. 

395 

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) 

407 

408 

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) 

415 

416 

417def focusz_to_simple(focusz: astropy.units.Quantity) -> float: 

418 """Convert focusz to meters. 

419 

420 Parameters 

421 ---------- 

422 focusz : `astropy.units.Quantity` 

423 The z-focus as a quantity. 

424 

425 Returns 

426 ------- 

427 f : `float` 

428 The z-focus in meters. 

429 """ 

430 return _quantity_to_float(focusz, astropy.units.m) 

431 

432 

433def simple_to_focusz(simple: float, **kwargs: Any) -> astropy.units.Quantity: 

434 """Convert simple form back to Quantity. 

435 

436 Parameters 

437 ---------- 

438 simple : `float` 

439 The z-focus in meters. 

440 **kwargs : `typing.Any` 

441 Keyword arguments. Currently not used. 

442 

443 Returns 

444 ------- 

445 q : `astropy.units.Quantity` 

446 The z-focus as a quantity. 

447 """ 

448 return simple * astropy.units.m 

449 

450 

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) 

457 

458 

459def temperature_to_simple(temp: astropy.units.Quantity) -> float: 

460 """Convert temperature to kelvin. 

461 

462 Parameters 

463 ---------- 

464 temp : `astropy.units.Quantity` 

465 The temperature as a quantity. 

466 

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) 

474 

475 

476def simple_to_temperature(simple: float, **kwargs: Any) -> astropy.units.Quantity: 

477 """Convert scalar kelvin value back to quantity. 

478 

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. 

485 

486 Returns 

487 ------- 

488 q : `astropy.units.Quantity` 

489 The temperature as a quantity. 

490 """ 

491 return simple * astropy.units.K 

492 

493 

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) 

500 

501 

502def pressure_to_simple(press: astropy.units.Quantity) -> float: 

503 """Convert pressure Quantity to hPa. 

504 

505 Parameters 

506 ---------- 

507 press : `astropy.units.Quantity` 

508 The pressure as a quantity. 

509 

510 Returns 

511 ------- 

512 hpa : `float` 

513 The pressure in units of hPa. 

514 """ 

515 return _quantity_to_float(press, astropy.units.hPa) 

516 

517 

518def simple_to_pressure(simple: float, **kwargs: Any) -> astropy.units.Quantity: 

519 """Convert the pressure scalar back to Quantity. 

520 

521 Parameters 

522 ---------- 

523 simple : `float` 

524 Pressure in units of hPa. 

525 **kwargs : `typing.Any` 

526 Keyword arguments. Currently not used. 

527 

528 Returns 

529 ------- 

530 q : `astropy.units.Quantity` 

531 The pressure as a quantity. 

532 """ 

533 return simple * astropy.units.hPa 

534 

535 

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) 

542 

543 

544def skycoord_to_simple(skycoord: astropy.coordinates.SkyCoord) -> tuple[float, float]: 

545 """Convert SkyCoord to ICRS RA/Dec tuple. 

546 

547 Parameters 

548 ---------- 

549 skycoord : `astropy.coordinates.SkyCoord` 

550 Sky coordinates in astropy form. 

551 

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)) 

565 

566 

567def simple_to_skycoord(simple: tuple[float, float], **kwargs: Any) -> astropy.coordinates.SkyCoord: 

568 """Convert ICRS tuple to SkyCoord. 

569 

570 Parameters 

571 ---------- 

572 simple : `tuple` [`float`, `float`] 

573 Sky coordinates in degrees. 

574 **kwargs : `typing.Any` 

575 Keyword arguments. Currently not used. 

576 

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) 

583 

584 

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) 

591 

592 

593def altaz_to_simple(altaz: astropy.coordinates.AltAz) -> tuple[float, float]: 

594 """Convert AltAz to Alt/Az tuple. 

595 

596 Do not include obstime or location in simplification. It is assumed 

597 that those will be present from other properties. 

598 

599 Parameters 

600 ---------- 

601 altaz : `astropy.coordinates.AltAz` 

602 The alt/az in astropy form. 

603 

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)) 

611 

612 

613def simple_to_altaz(simple: tuple[float, float], **kwargs: Any) -> astropy.coordinates.AltAz: 

614 """Convert simple altaz tuple to AltAz. 

615 

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``. 

623 

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 

638 

639 location = kwargs.get("location") 

640 obstime = kwargs.get("datetime_begin") 

641 

642 return astropy.coordinates.AltAz( 

643 simple[0] * astropy.units.deg, simple[1] * astropy.units.deg, obstime=obstime, location=location 

644 ) 

645 

646 

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) 

653 

654 

655def timedelta_to_simple(delta: astropy.time.TimeDelta) -> int: 

656 """Convert a TimeDelta to integer seconds. 

657 

658 This property does not need to support floating point seconds. 

659 

660 Parameters 

661 ---------- 

662 delta : `astropy.time.TimeDelta` 

663 The time offset. 

664 

665 Returns 

666 ------- 

667 sec : `int` 

668 Offset in integer seconds. 

669 """ 

670 return round(_quantity_to_float(delta, astropy.units.s)) 

671 

672 

673def simple_to_timedelta(simple: int, **kwargs: Any) -> astropy.time.TimeDelta: 

674 """Convert integer seconds to a `~astropy.time.TimeDelta`. 

675 

676 Parameters 

677 ---------- 

678 simple : `int` 

679 The offset in integer seconds. 

680 **kwargs : `dict` 

681 Additional information. Unused. 

682 

683 Returns 

684 ------- 

685 delta : `astropy.time.TimeDelta` 

686 The delta object. 

687 """ 

688 return astropy.time.TimeDelta(simple, format="sec", scale="tai") 

689 

690 

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) 

697 

698 

699class PropertyDefinition: 

700 """Definition of an instrumental property. 

701 

702 Supports both signatures: 

703 

704 - ``(doc, py_type, to_simple=None, from_simple=None)`` 

705 - ``(doc, legacy_str_type, py_type, to_simple=None, from_simple=None)`` 

706 

707 Modern preference is to not specify the string type since that can be 

708 derived directly from the python type. 

709 

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 """ 

718 

719 __slots__ = ("doc", "py_type", "to_simple", "from_simple") 

720 

721 doc: str 

722 py_type: type 

723 to_simple: Callable[[Any], Any] | None 

724 from_simple: Callable[[Any], Any] | None 

725 

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") 

729 

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:] 

740 

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") 

743 

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 

746 

747 self.doc = doc 

748 self.py_type = py_type 

749 self.to_simple = to_simple 

750 self.from_simple = from_simple 

751 

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__}" 

758 

759 def is_value_conformant(self, value: Any) -> bool: 

760 """Compare the supplied value against the expected type as defined 

761 for this property. 

762 

763 Parameters 

764 ---------- 

765 value : `object` 

766 Value of the property to validate. Can be `None`. 

767 

768 Returns 

769 ------- 

770 is_ok : `bool` 

771 `True` if the value is of an appropriate type. 

772 

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 

781 

782 return isinstance(value, self.py_type) 

783 

784 

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. 

805 

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}