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

66 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-13 01:21 -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. 

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 dataclasses import dataclass 

30from typing import Any 

31 

32import astropy.coordinates 

33import astropy.time 

34import astropy.units 

35 

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. 

41 

42 

43def earthlocation_to_simple(location: astropy.coordinates.EarthLocation) -> tuple[float, ...]: 

44 """Convert EarthLocation to tuple. 

45 

46 Parameters 

47 ---------- 

48 location : `astropy.coordinates.EarthLocation` 

49 The location to simplify. 

50 

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) 

58 

59 

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

61 """Convert simple form back to EarthLocation. 

62 

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. 

69 

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) 

76 

77 

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

79 """Convert Time to tuple. 

80 

81 Parameters 

82 ---------- 

83 datetime : `astropy.time.Time` 

84 The time to simplify. 

85 

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) 

93 

94 

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

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

97 

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. 

104 

105 Returns 

106 ------- 

107 t : `astropy.time.Time` 

108 An astropy time object. 

109 """ 

110 return astropy.time.Time(*simple, format="jd", scale="tai") 

111 

112 

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

114 """Convert exposure time Quantity to seconds. 

115 

116 Parameters 

117 ---------- 

118 exptime : `astropy.units.Quantity` 

119 The exposure time as a quantity. 

120 

121 Returns 

122 ------- 

123 e : `float` 

124 Exposure time in seconds. 

125 """ 

126 return exptime.to_value(astropy.units.s) 

127 

128 

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

130 """Convert simple form back to Quantity. 

131 

132 Parameters 

133 ---------- 

134 simple : `float` 

135 Exposure time in seconds. 

136 **kwargs : `typing.Any` 

137 Keyword arguments. Currently not used. 

138 

139 Returns 

140 ------- 

141 q : `astropy.units.Quantity` 

142 The exposure time as a quantity. 

143 """ 

144 return simple * astropy.units.s 

145 

146 

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

148 """Convert Angle to degrees. 

149 

150 Parameters 

151 ---------- 

152 angle : `astropy.coordinates.Angle` 

153 The angle. 

154 

155 Returns 

156 ------- 

157 a : `float` 

158 The angle in degrees. 

159 """ 

160 return angle.to_value(astropy.units.deg) 

161 

162 

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

164 """Convert degrees to Angle. 

165 

166 Parameters 

167 ---------- 

168 simple : `float` 

169 The angle in degrees. 

170 **kwargs : `typing.Any` 

171 Keyword arguments. Currently not used. 

172 

173 Returns 

174 ------- 

175 a : `astropy.coordinates.Angle` 

176 The angle as an object. 

177 """ 

178 return astropy.coordinates.Angle(simple * astropy.units.deg) 

179 

180 

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

182 """Convert focusz to meters. 

183 

184 Parameters 

185 ---------- 

186 focusz : `astropy.units.Quantity` 

187 The z-focus as a quantity. 

188 

189 Returns 

190 ------- 

191 f : `float` 

192 The z-focus in meters. 

193 """ 

194 return focusz.to_value(astropy.units.m) 

195 

196 

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

198 """Convert simple form back to Quantity. 

199 

200 Parameters 

201 ---------- 

202 simple : `float` 

203 The z-focus in meters. 

204 **kwargs : `typing.Any` 

205 Keyword arguments. Currently not used. 

206 

207 Returns 

208 ------- 

209 q : `astropy.units.Quantity` 

210 The z-focus as a quantity. 

211 """ 

212 return simple * astropy.units.m 

213 

214 

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

216 """Convert temperature to kelvin. 

217 

218 Parameters 

219 ---------- 

220 temp : `astropy.units.Quantity` 

221 The temperature as a quantity. 

222 

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

229 

230 

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

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

233 

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. 

240 

241 Returns 

242 ------- 

243 q : `astropy.units.Quantity` 

244 The temperature as a quantity. 

245 """ 

246 return simple * astropy.units.K 

247 

248 

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

250 """Convert pressure Quantity to hPa. 

251 

252 Parameters 

253 ---------- 

254 press : `astropy.units.Quantity` 

255 The pressure as a quantity. 

256 

257 Returns 

258 ------- 

259 hpa : `float` 

260 The pressure in units of hPa. 

261 """ 

262 return press.to_value(astropy.units.hPa) 

263 

264 

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

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

267 

268 Parameters 

269 ---------- 

270 simple : `float` 

271 Pressure in units of hPa. 

272 **kwargs : `typing.Any` 

273 Keyword arguments. Currently not used. 

274 

275 Returns 

276 ------- 

277 q : `astropy.units.Quantity` 

278 The pressure as a quantity. 

279 """ 

280 return simple * astropy.units.hPa 

281 

282 

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

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

285 

286 Parameters 

287 ---------- 

288 skycoord : `astropy.coordinates.SkyCoord` 

289 Sky coordinates in astropy form. 

290 

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

298 

299 

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

301 """Convert ICRS tuple to SkyCoord. 

302 

303 Parameters 

304 ---------- 

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

306 Sky coordinates in degrees. 

307 **kwargs : `typing.Any` 

308 Keyword arguments. Currently not used. 

309 

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) 

316 

317 

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

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

320 

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

322 that those will be present from other properties. 

323 

324 Parameters 

325 ---------- 

326 altaz : `astropy.coordinates.AltAz` 

327 The alt/az in astropy form. 

328 

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

336 

337 

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

339 """Convert simple altaz tuple to AltAz. 

340 

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

348 

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

356 

357 return astropy.coordinates.AltAz( 

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

359 ) 

360 

361 

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

363 """Convert a TimeDelta to integer seconds. 

364 

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

366 

367 Parameters 

368 ---------- 

369 delta : `astropy.time.TimeDelta` 

370 The time offset. 

371 

372 Returns 

373 ------- 

374 sec : `int` 

375 Offset in integer seconds. 

376 """ 

377 return round(delta.to_value("s")) 

378 

379 

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

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

382 

383 Parameters 

384 ---------- 

385 simple : `int` 

386 The offset in integer seconds. 

387 **kwargs : `dict` 

388 Additional information. Unused. 

389 

390 Returns 

391 ------- 

392 delta : `astropy.time.TimeDelta` 

393 The delta object. 

394 """ 

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

396 

397 

398@dataclass 

399class PropertyDefinition: 

400 """Definition of an instrumental property.""" 

401 

402 doc: str 

403 """Docstring for the property.""" 

404 

405 str_type: str 

406 """Python type of property as a string (suitable for docstrings).""" 

407 

408 py_type: type 

409 """Actual python type.""" 

410 

411 to_simple: Callable[[Any], Any] | None = None 

412 """Function to convert value to simple form (can be ``None``).""" 

413 

414 from_simple: Callable[[Any], Any] | None = None 

415 """Function to convert from simple form back to required type (can be 

416 ``None``).""" 

417 

418 

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. 

440 

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}