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

112 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 08:38 +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 Any, Protocol, SupportsFloat 

30 

31import astropy.coordinates 

32import astropy.time 

33import astropy.units 

34import numpy as np 

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 

43class _ToValueProtocol(Protocol): 

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

45 

46 def to_value(self, unit: astropy.units.UnitBase | None = None) -> SupportsFloat | np.ndarray: 

47 """Return converted value that might be ndarray or a single number. 

48 

49 Parameters 

50 ---------- 

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

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

53 """ 

54 ... 

55 

56 

57def _quantity_to_float(q: _ToValueProtocol, unit: astropy.units.UnitBase | None = None) -> float: 

58 """Convert a quantity to a float, in a type safe manner, returning 

59 a single float. 

60 

61 Parameters 

62 ---------- 

63 q : `_ToValueProtocol` 

64 The Astropy object to extract the float value from. Must support a 

65 ``to_value()`` method. 

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

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

68 

69 Returns 

70 ------- 

71 value : `float` 

72 Single float corresponding to the quantity-like input. 

73 """ 

74 # Quantity.to_value is typed to return np.ndarray or a scalar-like value 

75 # that supports float conversion. 

76 # We only went a single float and it is an error to return multiples. 

77 values = q.to_value(unit=unit) 

78 if isinstance(values, np.ndarray): 

79 raise ValueError( 

80 f"Converting quantity to a float failed because unexpectedly got more than one float: {values}" 

81 ) 

82 return float(values) 

83 

84 

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

86 """Convert EarthLocation to tuple. 

87 

88 Parameters 

89 ---------- 

90 location : `astropy.coordinates.EarthLocation` 

91 The location to simplify. 

92 

93 Returns 

94 ------- 

95 geocentric : `tuple` of (`float`, `float`, `float`) 

96 The geocentric location as three floats in meters. 

97 """ 

98 geocentric = location.to_geocentric() 

99 return tuple(_quantity_to_float(c, astropy.units.m) for c in geocentric) 

100 

101 

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

103 """Convert simple form back to EarthLocation. 

104 

105 Parameters 

106 ---------- 

107 simple : `tuple` [`float`, ...] 

108 The geocentric location as three floats in meters. 

109 **kwargs : `typing.Any` 

110 Keyword arguments. Currently not used. 

111 

112 Returns 

113 ------- 

114 loc : `astropy.coordinates.EarthLocation` 

115 The location on the Earth. 

116 """ 

117 return astropy.coordinates.EarthLocation.from_geocentric(*simple, unit=astropy.units.m) 

118 

119 

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

121 """Convert Time to tuple. 

122 

123 Parameters 

124 ---------- 

125 datetime : `astropy.time.Time` 

126 The time to simplify. 

127 

128 Returns 

129 ------- 

130 mjds : `tuple` of (`float`, `float`) 

131 The two MJDs in TAI. 

132 """ 

133 tai = datetime.tai 

134 return (tai.jd1, tai.jd2) 

135 

136 

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

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

139 

140 Parameters 

141 ---------- 

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

143 The time represented by two MJDs. 

144 **kwargs : `typing.Any` 

145 Keyword arguments. Currently not used. 

146 

147 Returns 

148 ------- 

149 t : `astropy.time.Time` 

150 An astropy time object. 

151 """ 

152 return astropy.time.Time(simple[0], val2=simple[1], format="jd", scale="tai") 

153 

154 

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

156 """Convert exposure time Quantity to seconds. 

157 

158 Parameters 

159 ---------- 

160 exptime : `astropy.units.Quantity` 

161 The exposure time as a quantity. 

162 

163 Returns 

164 ------- 

165 e : `float` 

166 Exposure time in seconds. 

167 """ 

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

169 

170 

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

172 """Convert simple form back to Quantity. 

173 

174 Parameters 

175 ---------- 

176 simple : `float` 

177 Exposure time in seconds. 

178 **kwargs : `typing.Any` 

179 Keyword arguments. Currently not used. 

180 

181 Returns 

182 ------- 

183 q : `astropy.units.Quantity` 

184 The exposure time as a quantity. 

185 """ 

186 return simple * astropy.units.s 

187 

188 

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

190 """Convert Angle to degrees. 

191 

192 Parameters 

193 ---------- 

194 angle : `astropy.coordinates.Angle` 

195 The angle. 

196 

197 Returns 

198 ------- 

199 a : `float` 

200 The angle in degrees. 

201 """ 

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

203 

204 

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

206 """Convert degrees to Angle. 

207 

208 Parameters 

209 ---------- 

210 simple : `float` 

211 The angle in degrees. 

212 **kwargs : `typing.Any` 

213 Keyword arguments. Currently not used. 

214 

215 Returns 

216 ------- 

217 a : `astropy.coordinates.Angle` 

218 The angle as an object. 

219 """ 

220 # Quantity of 45. deg is not the same as Angle. 

221 if isinstance(simple, astropy.units.Quantity): 

222 angle = simple 

223 else: 

224 angle = simple * astropy.units.deg 

225 return astropy.coordinates.Angle(angle) 

226 

227 

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

229 """Convert focusz to meters. 

230 

231 Parameters 

232 ---------- 

233 focusz : `astropy.units.Quantity` 

234 The z-focus as a quantity. 

235 

236 Returns 

237 ------- 

238 f : `float` 

239 The z-focus in meters. 

240 """ 

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

242 

243 

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

245 """Convert simple form back to Quantity. 

246 

247 Parameters 

248 ---------- 

249 simple : `float` 

250 The z-focus in meters. 

251 **kwargs : `typing.Any` 

252 Keyword arguments. Currently not used. 

253 

254 Returns 

255 ------- 

256 q : `astropy.units.Quantity` 

257 The z-focus as a quantity. 

258 """ 

259 return simple * astropy.units.m 

260 

261 

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

263 """Convert temperature to kelvin. 

264 

265 Parameters 

266 ---------- 

267 temp : `astropy.units.Quantity` 

268 The temperature as a quantity. 

269 

270 Returns 

271 ------- 

272 t : `float` 

273 The temperature in kelvin. 

274 """ 

275 q = temp.to(astropy.units.K, equivalencies=astropy.units.temperature()) 

276 return _quantity_to_float(q) 

277 

278 

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

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

281 

282 Parameters 

283 ---------- 

284 simple : `float` 

285 Temperature as a float in units of kelvin. 

286 **kwargs : `typing.Any` 

287 Keyword arguments. Currently not used. 

288 

289 Returns 

290 ------- 

291 q : `astropy.units.Quantity` 

292 The temperature as a quantity. 

293 """ 

294 return simple * astropy.units.K 

295 

296 

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

298 """Convert pressure Quantity to hPa. 

299 

300 Parameters 

301 ---------- 

302 press : `astropy.units.Quantity` 

303 The pressure as a quantity. 

304 

305 Returns 

306 ------- 

307 hpa : `float` 

308 The pressure in units of hPa. 

309 """ 

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

311 

312 

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

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

315 

316 Parameters 

317 ---------- 

318 simple : `float` 

319 Pressure in units of hPa. 

320 **kwargs : `typing.Any` 

321 Keyword arguments. Currently not used. 

322 

323 Returns 

324 ------- 

325 q : `astropy.units.Quantity` 

326 The pressure as a quantity. 

327 """ 

328 return simple * astropy.units.hPa 

329 

330 

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

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

333 

334 Parameters 

335 ---------- 

336 skycoord : `astropy.coordinates.SkyCoord` 

337 Sky coordinates in astropy form. 

338 

339 Returns 

340 ------- 

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

342 Sky coordinates as a tuple of two floats in units of degrees. 

343 """ 

344 icrs = skycoord.icrs 

345 if not isinstance(icrs, astropy.coordinates.SkyCoord): 

346 raise ValueError(f"Could not extract ICRS coordinates from SkyCoord {skycoord}") 

347 ra = icrs.ra 

348 assert isinstance(ra, astropy.coordinates.Longitude) 

349 dec = icrs.dec 

350 assert isinstance(dec, astropy.coordinates.Latitude) 

351 return (_quantity_to_float(ra, astropy.units.deg), _quantity_to_float(dec, astropy.units.deg)) 

352 

353 

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

355 """Convert ICRS tuple to SkyCoord. 

356 

357 Parameters 

358 ---------- 

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

360 Sky coordinates in degrees. 

361 **kwargs : `typing.Any` 

362 Keyword arguments. Currently not used. 

363 

364 Returns 

365 ------- 

366 skycoord : `astropy.coordinates.SkyCoord` 

367 The sky coordinates in astropy form. 

368 """ 

369 return astropy.coordinates.SkyCoord(*simple, unit=astropy.units.deg) 

370 

371 

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

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

374 

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

376 that those will be present from other properties. 

377 

378 Parameters 

379 ---------- 

380 altaz : `astropy.coordinates.AltAz` 

381 The alt/az in astropy form. 

382 

383 Returns 

384 ------- 

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

386 The Alt/Az as a tuple of two floats representing the position in 

387 units of degrees. 

388 """ 

389 return (_quantity_to_float(altaz.az, astropy.units.deg), _quantity_to_float(altaz.alt, astropy.units.deg)) 

390 

391 

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

393 """Convert simple altaz tuple to AltAz. 

394 

395 Parameters 

396 ---------- 

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

398 Altitude and elevation in degrees. 

399 **kwargs : `dict` 

400 Additional information. Must contain ``location`` and 

401 ``datetime_begin``. 

402 

403 Returns 

404 ------- 

405 altaz : `astropy.coordinates.AltAz` 

406 The altaz in astropy form. 

407 """ 

408 # Sometimes we get given a SkyCoord that contains an AltAz frame that needs 

409 # to be extracted. 

410 if isinstance(simple, astropy.coordinates.SkyCoord): 

411 frame = simple.frame 

412 if isinstance(frame, astropy.coordinates.AltAz): 

413 return frame 

414 # If there is no AltAz frame, return what we have so that downstream 

415 # validation can fail. 

416 return simple 

417 

418 location = kwargs.get("location") 

419 obstime = kwargs.get("datetime_begin") 

420 

421 return astropy.coordinates.AltAz( 

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

423 ) 

424 

425 

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

427 """Convert a TimeDelta to integer seconds. 

428 

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

430 

431 Parameters 

432 ---------- 

433 delta : `astropy.time.TimeDelta` 

434 The time offset. 

435 

436 Returns 

437 ------- 

438 sec : `int` 

439 Offset in integer seconds. 

440 """ 

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

442 

443 

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

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

446 

447 Parameters 

448 ---------- 

449 simple : `int` 

450 The offset in integer seconds. 

451 **kwargs : `dict` 

452 Additional information. Unused. 

453 

454 Returns 

455 ------- 

456 delta : `astropy.time.TimeDelta` 

457 The delta object. 

458 """ 

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

460 

461 

462class PropertyDefinition: 

463 """Definition of an instrumental property. 

464 

465 Supports both signatures: 

466 

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

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

469 

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

471 derived directly from the python type. 

472 

473 Parameters 

474 ---------- 

475 doc : `str` 

476 Documentation string for the property. 

477 *args : `typing.Any` 

478 Remaining constructor arguments in one of the supported 

479 signatures. 

480 """ 

481 

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

483 

484 doc: str 

485 py_type: type 

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

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

488 

489 def __init__(self, doc: str, *args: Any) -> None: 

490 if not args: 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true

491 raise TypeError("PropertyDefinition requires at least a py_type argument") 

492 

493 if isinstance(args[0], str): 

494 if len(args) < 2 or not isinstance(args[1], type): 494 ↛ 495line 494 didn't jump to line 495 because the condition on line 494 was never true

495 raise TypeError("Legacy PropertyDefinition signature requires (doc, str_type, py_type, ...)") 

496 py_type = args[1] 

497 rest = args[2:] 

498 else: 

499 if not isinstance(args[0], type): 499 ↛ 500line 499 didn't jump to line 500 because the condition on line 499 was never true

500 raise TypeError("PropertyDefinition py_type must be a type") 

501 py_type = args[0] 

502 rest = args[1:] 

503 

504 if len(rest) > 2: 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true

505 raise TypeError("PropertyDefinition accepts at most two converter callables") 

506 

507 to_simple: Callable[[Any], Any] | None = rest[0] if rest else None 

508 from_simple: Callable[[Any], Any] | None = rest[1] if len(rest) > 1 else None 

509 

510 self.doc = doc 

511 self.py_type = py_type 

512 self.to_simple = to_simple 

513 self.from_simple = from_simple 

514 

515 @property 

516 def str_type(self) -> str: 

517 """Python type of property as a string suitable for messages/docs.""" 

518 if self.py_type.__module__ == "builtins": 

519 return self.py_type.__name__ 

520 return f"{self.py_type.__module__}.{self.py_type.__qualname__}" 

521 

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

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

524 for this property. 

525 

526 Parameters 

527 ---------- 

528 value : `object` 

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

530 

531 Returns 

532 ------- 

533 is_ok : `bool` 

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

535 

536 Notes 

537 ----- 

538 Currently only the type of the property is validated. There is no 

539 attempt to check bounds or determine that a Quantity is compatible 

540 with the property. 

541 """ 

542 if value is None: 

543 return True 

544 

545 return isinstance(value, self.py_type) 

546 

547 

548# This dict defines all the core properties of an ObservationInfo. 

549# The PropertyDefinition is keyed by the property name. 

550# The doc string is used to define the Pydantic model. 

551# The py_type/doc are used to create the translator methods. 

552# The optional callables are used to convert types for serialization and 

553# validation. 

554PROPERTIES = { 

555 "telescope": PropertyDefinition("Full name of the telescope.", str), 

556 "instrument": PropertyDefinition("The instrument used to observe the exposure.", str), 

557 "location": PropertyDefinition( 

558 "Location of the observatory.", 

559 astropy.coordinates.EarthLocation, 

560 earthlocation_to_simple, 

561 simple_to_earthlocation, 

562 ), 

563 "exposure_id": PropertyDefinition( 

564 "Unique (with instrument) integer identifier for this observation.", int 

565 ), 

566 "visit_id": PropertyDefinition( 

567 """ID of the Visit this Exposure is associated with. 

568 

569Science observations should essentially always be 

570associated with a visit, but calibration observations 

571may not be.""", 

572 int, 

573 ), 

574 "physical_filter": PropertyDefinition("The bandpass filter used for this observation.", str), 

575 "datetime_begin": PropertyDefinition( 

576 "Time of the start of the observation.", 

577 astropy.time.Time, 

578 datetime_to_simple, 

579 simple_to_datetime, 

580 ), 

581 "datetime_end": PropertyDefinition( 

582 "Time of the end of the observation.", 

583 astropy.time.Time, 

584 datetime_to_simple, 

585 simple_to_datetime, 

586 ), 

587 "exposure_time": PropertyDefinition( 

588 "Actual duration of the exposure (seconds).", 

589 astropy.units.Quantity, 

590 exptime_to_simple, 

591 simple_to_exptime, 

592 ), 

593 "exposure_time_requested": PropertyDefinition( 

594 "Requested duration of the exposure (seconds).", 

595 astropy.units.Quantity, 

596 exptime_to_simple, 

597 simple_to_exptime, 

598 ), 

599 "dark_time": PropertyDefinition( 

600 "Duration of the exposure with shutter closed (seconds).", 

601 astropy.units.Quantity, 

602 exptime_to_simple, 

603 simple_to_exptime, 

604 ), 

605 "boresight_airmass": PropertyDefinition("Airmass of the boresight of the telescope.", float), 

606 "boresight_rotation_angle": PropertyDefinition( 

607 "Angle of the instrument in boresight_rotation_coord frame.", 

608 astropy.coordinates.Angle, 

609 angle_to_simple, 

610 simple_to_angle, 

611 ), 

612 "boresight_rotation_coord": PropertyDefinition( 

613 "Coordinate frame of the instrument rotation angle (options: sky, unknown).", 

614 str, 

615 ), 

616 "detector_num": PropertyDefinition("Unique (for instrument) integer identifier for the sensor.", int), 

617 "detector_name": PropertyDefinition( 

618 "Name of the detector within the instrument (might not be unique if there are detector groups).", 

619 str, 

620 ), 

621 "detector_unique_name": PropertyDefinition( 

622 ( 

623 "Unique name of the detector within the focal plane, generally combining detector_group with " 

624 "detector_name." 

625 ), 

626 str, 

627 ), 

628 "detector_serial": PropertyDefinition("Serial number/string associated with this detector.", str), 

629 "detector_group": PropertyDefinition( 

630 "Collection name of which this detector is a part. Can be None if there are no detector groupings.", 

631 str, 

632 ), 

633 "detector_exposure_id": PropertyDefinition( 

634 "Unique integer identifier for this detector in this exposure.", 

635 int, 

636 ), 

637 "focus_z": PropertyDefinition( 

638 "Defocal distance.", 

639 astropy.units.Quantity, 

640 focusz_to_simple, 

641 simple_to_focusz, 

642 ), 

643 "object": PropertyDefinition("Object of interest or field name.", str), 

644 "temperature": PropertyDefinition( 

645 "Temperature outside the dome.", 

646 astropy.units.Quantity, 

647 temperature_to_simple, 

648 simple_to_temperature, 

649 ), 

650 "pressure": PropertyDefinition( 

651 "Atmospheric pressure outside the dome.", 

652 astropy.units.Quantity, 

653 pressure_to_simple, 

654 simple_to_pressure, 

655 ), 

656 "relative_humidity": PropertyDefinition("Relative humidity outside the dome.", float), 

657 "tracking_radec": PropertyDefinition( 

658 "Requested RA/Dec to track.", 

659 astropy.coordinates.SkyCoord, 

660 skycoord_to_simple, 

661 simple_to_skycoord, 

662 ), 

663 "altaz_begin": PropertyDefinition( 

664 "Telescope boresight azimuth and elevation at start of observation.", 

665 astropy.coordinates.AltAz, 

666 altaz_to_simple, 

667 simple_to_altaz, 

668 ), 

669 "altaz_end": PropertyDefinition( 

670 "Telescope boresight azimuth and elevation at end of observation.", 

671 astropy.coordinates.AltAz, 

672 altaz_to_simple, 

673 simple_to_altaz, 

674 ), 

675 "science_program": PropertyDefinition("Observing program (survey or proposal) identifier.", str), 

676 "observation_type": PropertyDefinition( 

677 "Type of observation (currently: science, dark, flat, bias, focus).", 

678 str, 

679 ), 

680 "observation_id": PropertyDefinition( 

681 "Label uniquely identifying this observation (can be related to 'exposure_id').", 

682 str, 

683 ), 

684 "observation_reason": PropertyDefinition( 

685 "Reason this observation was taken, or its purpose ('science' and 'calibration' are common values)", 

686 str, 

687 ), 

688 "exposure_group": PropertyDefinition( 

689 "Label to use to associate this exposure with others (can be related to 'exposure_id').", 

690 str, 

691 ), 

692 "observing_day": PropertyDefinition( 

693 "Integer in YYYYMMDD format corresponding to the day of observation.", int 

694 ), 

695 "observing_day_offset": PropertyDefinition( 

696 ( 

697 "Offset to subtract from an observation date when calculating the observing day. " 

698 "Conversely, the offset to add to an observing day when calculating the time span of a day." 

699 ), 

700 astropy.time.TimeDelta, 

701 timedelta_to_simple, 

702 simple_to_timedelta, 

703 ), 

704 "observation_counter": PropertyDefinition( 

705 ( 

706 "Counter of this observation. Can be counter within observing_day or a global counter. " 

707 "Likely to be observatory specific." 

708 ), 

709 int, 

710 ), 

711 "has_simulated_content": PropertyDefinition( 

712 "Boolean indicating whether any part of this observation was simulated.", bool, None, None 

713 ), 

714 "group_counter_start": PropertyDefinition( 

715 "Observation counter for the start of the exposure group." 

716 "Depending on the instrument the relevant group may be " 

717 "visit_id or exposure_group.", 

718 int, 

719 None, 

720 None, 

721 ), 

722 "group_counter_end": PropertyDefinition( 

723 "Observation counter for the end of the exposure group. " 

724 "Depending on the instrument the relevant group may be " 

725 "visit_id or exposure_group.", 

726 int, 

727 None, 

728 None, 

729 ), 

730 "can_see_sky": PropertyDefinition( 

731 "True if the observation is looking at sky, False if it is definitely" 

732 " not looking at the sky. None indicates that it is not known whether" 

733 " sky could be seen.", 

734 bool, 

735 ), 

736}