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

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

20from __future__ import annotations 

21 

22__all__ = ( 

23 "PropertyDefinition", 

24 "PROPERTIES", 

25) 

26 

27from collections.abc import Callable 

28from dataclasses import dataclass 

29from typing import Any 

30 

31import astropy.coordinates 

32import astropy.time 

33import astropy.units 

34 

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. 

40 

41 

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

43 """Convert EarthLocation to tuple. 

44 

45 Parameters 

46 ---------- 

47 location : `astropy.coordinates.EarthLocation` 

48 The location to simplify. 

49 

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) 

57 

58 

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

60 """Convert simple form back to EarthLocation. 

61 

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. 

68 

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) 

75 

76 

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

78 """Convert Time to tuple. 

79 

80 Parameters 

81 ---------- 

82 datetime : `astropy.time.Time` 

83 The time to simplify. 

84 

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) 

92 

93 

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

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

96 

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. 

103 

104 Returns 

105 ------- 

106 t : `astropy.time.Time` 

107 An astropy time object. 

108 """ 

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

110 

111 

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

113 """Convert exposure time Quantity to seconds. 

114 

115 Parameters 

116 ---------- 

117 exptime : `astropy.units.Quantity` 

118 The exposure time as a quantity. 

119 

120 Returns 

121 ------- 

122 e : `float` 

123 Exposure time in seconds. 

124 """ 

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

126 

127 

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

129 """Convert simple form back to Quantity. 

130 

131 Parameters 

132 ---------- 

133 simple : `float` 

134 Exposure time in seconds. 

135 **kwargs : `typing.Any` 

136 Keyword arguments. Currently not used. 

137 

138 Returns 

139 ------- 

140 q : `astropy.units.Quantity` 

141 The exposure time as a quantity. 

142 """ 

143 return simple * astropy.units.s 

144 

145 

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

147 """Convert Angle to degrees. 

148 

149 Parameters 

150 ---------- 

151 angle : `astropy.coordinates.Angle` 

152 The angle. 

153 

154 Returns 

155 ------- 

156 a : `float` 

157 The angle in degrees. 

158 """ 

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

160 

161 

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

163 """Convert degrees to Angle. 

164 

165 Parameters 

166 ---------- 

167 simple : `float` 

168 The angle in degrees. 

169 **kwargs : `typing.Any` 

170 Keyword arguments. Currently not used. 

171 

172 Returns 

173 ------- 

174 a : `astropy.coordinates.Angle` 

175 The angle as an object. 

176 """ 

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

178 

179 

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

181 """Convert focusz to meters. 

182 

183 Parameters 

184 ---------- 

185 focusz : `astropy.units.Quantity` 

186 The z-focus as a quantity. 

187 

188 Returns 

189 ------- 

190 f : `float` 

191 The z-focus in meters. 

192 """ 

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

194 

195 

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

197 """Convert simple form back to Quantity. 

198 

199 Parameters 

200 ---------- 

201 simple : `float` 

202 The z-focus in meters. 

203 **kwargs : `typing.Any` 

204 Keyword arguments. Currently not used. 

205 

206 Returns 

207 ------- 

208 q : `astropy.units.Quantity` 

209 The z-focus as a quantity. 

210 """ 

211 return simple * astropy.units.m 

212 

213 

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

215 """Convert temperature to kelvin. 

216 

217 Parameters 

218 ---------- 

219 temp : `astropy.units.Quantity` 

220 The temperature as a quantity. 

221 

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

228 

229 

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

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

232 

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. 

239 

240 Returns 

241 ------- 

242 q : `astropy.units.Quantity` 

243 The temperature as a quantity. 

244 """ 

245 return simple * astropy.units.K 

246 

247 

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

249 """Convert pressure Quantity to hPa. 

250 

251 Parameters 

252 ---------- 

253 press : `astropy.units.Quantity` 

254 The pressure as a quantity. 

255 

256 Returns 

257 ------- 

258 hpa : `float` 

259 The pressure in units of hPa. 

260 """ 

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

262 

263 

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

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

266 

267 Parameters 

268 ---------- 

269 simple : `float` 

270 Pressure in units of hPa. 

271 **kwargs : `typing.Any` 

272 Keyword arguments. Currently not used. 

273 

274 Returns 

275 ------- 

276 q : `astropy.units.Quantity` 

277 The pressure as a quantity. 

278 """ 

279 return simple * astropy.units.hPa 

280 

281 

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

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

284 

285 Parameters 

286 ---------- 

287 skycoord : `astropy.coordinates.SkyCoord` 

288 Sky coordinates in astropy form. 

289 

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

297 

298 

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

300 """Convert ICRS tuple to SkyCoord. 

301 

302 Parameters 

303 ---------- 

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

305 Sky coordinates in degrees. 

306 **kwargs : `typing.Any` 

307 Keyword arguments. Currently not used. 

308 

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) 

315 

316 

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

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

319 

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

321 that those will be present from other properties. 

322 

323 Parameters 

324 ---------- 

325 altaz : `astropy.coordinates.AltAz` 

326 The alt/az in astropy form. 

327 

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

335 

336 

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

338 """Convert simple altaz tuple to AltAz. 

339 

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

347 

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

355 

356 return astropy.coordinates.AltAz( 

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

358 ) 

359 

360 

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

362 """Convert a TimeDelta to integer seconds. 

363 

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

365 

366 Parameters 

367 ---------- 

368 delta : `astropy.time.TimeDelta` 

369 The time offset. 

370 

371 Returns 

372 ------- 

373 sec : `int` 

374 Offset in integer seconds. 

375 """ 

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

377 

378 

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

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

381 

382 Parameters 

383 ---------- 

384 simple : `int` 

385 The offset in integer seconds. 

386 **kwargs : `dict` 

387 Additional information. Unused. 

388 

389 Returns 

390 ------- 

391 delta : `astropy.time.TimeDelta` 

392 The delta object. 

393 """ 

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

395 

396 

397@dataclass 

398class PropertyDefinition: 

399 """Definition of an instrumental property.""" 

400 

401 doc: str 

402 """Docstring for the property.""" 

403 

404 str_type: str 

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

406 

407 py_type: type 

408 """Actual python type.""" 

409 

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

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

412 

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

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

415 ``None``).""" 

416 

417 

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. 

439 

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}