Coverage for python/astro_metadata_translator/observationInfo.py: 18%

230 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"""Represent standard metadata from instrument headers.""" 

13 

14from __future__ import annotations 

15 

16__all__ = ("ObservationInfo", "makeObservationInfo") 

17 

18import copy 

19import itertools 

20import json 

21import logging 

22import math 

23from collections.abc import Callable, MutableMapping, Sequence 

24from typing import TYPE_CHECKING, Any 

25 

26import astropy.time 

27from astropy.coordinates import AltAz, SkyCoord 

28 

29from .headers import fix_header 

30from .properties import PROPERTIES, PropertyDefinition 

31from .translator import MetadataTranslator 

32 

33if TYPE_CHECKING: 

34 import astropy.coordinates 

35 import astropy.units 

36 

37log = logging.getLogger(__name__) 

38 

39 

40class ObservationInfo: 

41 """Standardized representation of an instrument header for a single 

42 exposure observation. 

43 

44 There is a core set of instrumental properties that are pre-defined. 

45 Additional properties may be defined, either through the 

46 ``makeObservationInfo`` factory function by providing the ``extensions`` 

47 definitions, or through the regular ``ObservationInfo`` constructor when 

48 the extensions have been defined in the ``MetadataTranslator`` for the 

49 instrument of interest (or in the provided ``translator_class``). 

50 

51 Parameters 

52 ---------- 

53 header : `dict`-like 

54 Representation of an instrument header accessible as a `dict`. 

55 May be updated with header corrections if corrections are found. 

56 filename : `str`, optional 

57 Name of the file whose header is being translated. For some 

58 datasets with missing header information this can sometimes 

59 allow for some fixups in translations. 

60 translator_class : `MetadataTranslator`-class, optional 

61 If not `None`, the class to use to translate the supplied headers 

62 into standard form. Otherwise each registered translator class will 

63 be asked in turn if it knows how to translate the supplied header. 

64 pedantic : `bool`, optional 

65 If True the translation must succeed for all properties. If False 

66 individual property translations must all be implemented but can fail 

67 and a warning will be issued. 

68 search_path : iterable, optional 

69 Override search paths to use during header fix up. 

70 required : `set`, optional 

71 This parameter can be used to confirm that all properties contained 

72 in the set must translate correctly and also be non-None. For the case 

73 where ``pedantic`` is `True` this will still check that the resulting 

74 value is not `None`. 

75 subset : `set`, optional 

76 If not `None`, controls the translations that will be performed 

77 during construction. This can be useful if the caller is only 

78 interested in a subset of the properties and knows that some of 

79 the others might be slow to compute (for example the airmass if it 

80 has to be derived). 

81 

82 Raises 

83 ------ 

84 ValueError 

85 Raised if the supplied header was not recognized by any of the 

86 registered translators. Also raised if the request property subset 

87 is not a subset of the known properties. 

88 TypeError 

89 Raised if the supplied translator class was not a MetadataTranslator. 

90 KeyError 

91 Raised if a required property cannot be calculated, or if pedantic 

92 mode is enabled and any translations fails. 

93 NotImplementedError 

94 Raised if the selected translator does not support a required 

95 property. 

96 

97 Notes 

98 ----- 

99 Headers will be corrected if correction files are located and this will 

100 modify the header provided to the constructor. 

101 

102 Values of the properties are read-only. 

103 """ 

104 

105 # Static typing requires that we define the standard dynamic properties 

106 # statically. 

107 if TYPE_CHECKING: 

108 telescope: int 

109 instrument: str 

110 location: astropy.coordinates.EarthLocation 

111 exposure_id: int 

112 visit_id: int 

113 physical_filter: str 

114 datetime_begin: astropy.time.Time 

115 datetime_end: astropy.time.Time 

116 exposure_group: str 

117 exposure_time: astropy.units.Quantity 

118 dark_time: astropy.units.Quantity 

119 boresight_airmass: float 

120 boresight_rotation_angle: astropy.units.Quantity 

121 boresight_rotation_coord: str 

122 detector_num: int 

123 detector_name: str 

124 detector_serial: str 

125 detector_group: str 

126 detector_exposure_id: int 

127 focus_z: astropy.units.Quantity 

128 object: str 

129 temperature: astropy.units.Quantity 

130 pressure: astropy.units.Quantity 

131 relative_humidity: float 

132 tracking_radec: astropy.coordinates.SkyCoord 

133 altaz_begin: astropy.coordinates.AltAz 

134 science_program: str 

135 observation_counter: int 

136 observation_reason: str 

137 observation_type: str 

138 observation_id: str 

139 observing_day: int 

140 observing_day_offset: astropy.time.TimeDelta | None 

141 group_counter_start: int 

142 group_counter_end: int 

143 has_simulated_content: bool 

144 

145 def __init__( 

146 self, 

147 header: MutableMapping[str, Any] | None, 

148 filename: str | None = None, 

149 translator_class: type[MetadataTranslator] | None = None, 

150 pedantic: bool = False, 

151 search_path: Sequence[str] | None = None, 

152 required: set[str] | None = None, 

153 subset: set[str] | None = None, 

154 ) -> None: 

155 # Initialize the empty object 

156 self._header: MutableMapping[str, Any] = {} 

157 self.filename = filename 

158 self._translator = None 

159 self.translator_class_name = "<None>" 

160 

161 # To allow makeObservationInfo to work, we special case a None 

162 # header 

163 if header is None: 

164 return 

165 

166 # Fix up the header (if required) 

167 fix_header(header, translator_class=translator_class, filename=filename, search_path=search_path) 

168 

169 # Store the supplied header for later stripping 

170 self._header = header 

171 

172 if translator_class is None: 

173 translator_class = MetadataTranslator.determine_translator(header, filename=filename) 

174 elif not issubclass(translator_class, MetadataTranslator): 

175 raise TypeError(f"Translator class must be a MetadataTranslator, not {translator_class}") 

176 

177 self._declare_extensions(translator_class.extensions) 

178 

179 # Create an instance for this header 

180 translator = translator_class(header, filename=filename) 

181 

182 # Store the translator 

183 self._translator = translator 

184 self.translator_class_name = translator_class.__name__ 

185 

186 # Form file information string in case we need an error message 

187 if filename: 

188 file_info = f" and file {filename}" 

189 else: 

190 file_info = "" 

191 

192 # Determine the properties of interest 

193 full_set = set(self.all_properties) 

194 if subset is not None: 

195 if not subset: 

196 raise ValueError("Cannot request no properties be calculated.") 

197 if not subset.issubset(full_set): 

198 raise ValueError( 

199 "Requested subset is not a subset of known properties. " f"Got extra: {subset - full_set}" 

200 ) 

201 properties = subset 

202 else: 

203 properties = full_set 

204 

205 if required is None: 

206 required = set() 

207 else: 

208 if not required.issubset(full_set): 

209 raise ValueError("Requested required properties include unknowns: " f"{required - full_set}") 

210 

211 # Loop over each property and request the translated form 

212 for t in properties: 

213 # prototype code 

214 method = f"to_{t}" 

215 property = f"_{t}" if not t.startswith("ext_") else t 

216 

217 try: 

218 value = getattr(translator, method)() 

219 except NotImplementedError as e: 

220 raise NotImplementedError( 

221 f"No translation exists for property '{t}'" f" using translator {translator.__class__}" 

222 ) from e 

223 except Exception as e: 

224 err_msg = ( 

225 f"Error calculating property '{t}' using translator {translator.__class__}" f"{file_info}" 

226 ) 

227 if pedantic or t in required: 

228 raise KeyError(err_msg) from e 

229 else: 

230 log.debug("Calculation of property '%s' failed with header: %s", t, header) 

231 log.warning(f"Ignoring {err_msg}: {e}") 

232 continue 

233 

234 definition = self.all_properties[t] 

235 if not self._is_property_ok(definition, value): 

236 err_msg = ( 

237 f"Value calculated for property '{t}' is wrong type " 

238 f"({type(value)} != {definition.str_type}) using translator {translator.__class__}" 

239 f"{file_info}" 

240 ) 

241 if pedantic or t in required: 

242 raise TypeError(err_msg) 

243 else: 

244 log.debug("Calcuation of property '%s' had unexpected type with header: %s", t, header) 

245 log.warning(f"Ignoring {err_msg}") 

246 

247 if value is None and t in required: 

248 raise KeyError(f"Calculation of required property {t} resulted in a value of None") 

249 

250 super().__setattr__(property, value) # allows setting even write-protected extensions 

251 

252 @staticmethod 

253 def _get_all_properties( 

254 extensions: dict[str, PropertyDefinition] | None = None 

255 ) -> dict[str, PropertyDefinition]: 

256 """Return the definitions of all properties. 

257 

258 Parameters 

259 ---------- 

260 extensions : `dict` [`str`: `PropertyDefinition`] 

261 List of extension property definitions, indexed by name (with no 

262 "ext_" prefix). 

263 

264 Returns 

265 ------- 

266 properties : `dict` [`str`: `PropertyDefinition`] 

267 Merged list of all property definitions, indexed by name. Extension 

268 properties will be listed with an ``ext_`` prefix. 

269 """ 

270 properties = dict(PROPERTIES) 

271 if extensions: 

272 properties.update({"ext_" + pp: dd for pp, dd in extensions.items()}) 

273 return properties 

274 

275 def _declare_extensions(self, extensions: dict[str, PropertyDefinition] | None) -> None: 

276 """Declare and set up extension properties. 

277 

278 This should always be called internally as part of the creation of a 

279 new `ObservationInfo`. 

280 

281 The core set of properties each have a python ``property`` that makes 

282 them read-only, and serves as a useful place to hang the docstring. 

283 However, the core set are set up at compile time, whereas the extension 

284 properties have to be configured at run time (because we don't know 

285 what they will be until we look at the header and figure out what 

286 instrument we're dealing with) when we have an instance rather than a 

287 class (and python ``property`` doesn't work on instances; only on 

288 classes). We therefore use a separate scheme for the extension 

289 properties: we write them directly to their associated instance 

290 variable, and we use ``__setattr__`` to protect them as read-only. 

291 Unfortunately, with this scheme, we can't give extension properties a 

292 docstring; but we're setting them up at runtime, so maybe that's not 

293 terribly important. 

294 

295 Parameters 

296 ---------- 

297 extensions : `dict` [`str`: `PropertyDefinition`] 

298 List of extension property definitions, indexed by name (with no 

299 "ext_" prefix). 

300 """ 

301 if not extensions: 

302 extensions = {} 

303 for name in extensions: 

304 super().__setattr__("ext_" + name, None) 

305 self.extensions = extensions 

306 self.all_properties = self._get_all_properties(extensions) 

307 

308 def __setattr__(self, name: str, value: Any) -> Any: 

309 """Set attribute. 

310 

311 This provides read-only protection for the extension properties. The 

312 core set of properties have read-only protection via the use of the 

313 python ``property``. 

314 

315 Parameters 

316 ---------- 

317 name : `str` 

318 Name of attribute to set. 

319 value : `typing.Any` 

320 Value to set it to. 

321 """ 

322 if hasattr(self, "extensions") and name.startswith("ext_") and name[4:] in self.extensions: 

323 raise AttributeError(f"Attribute {name} is read-only") 

324 return super().__setattr__(name, value) 

325 

326 @classmethod 

327 def _is_property_ok(cls, definition: PropertyDefinition, value: Any) -> bool: 

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

329 for the corresponding property. 

330 

331 Parameters 

332 ---------- 

333 definition : `PropertyDefinition` 

334 Property definition. 

335 value : `object` 

336 Value of the property to validate. 

337 

338 Returns 

339 ------- 

340 is_ok : `bool` 

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

342 

343 Notes 

344 ----- 

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

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

347 with the property. 

348 """ 

349 if value is None: 

350 return True 

351 

352 # For AltAz coordinates, they can either arrive as AltAz or 

353 # as SkyCoord(frame=AltAz) so try to find the frame inside 

354 # the SkyCoord. 

355 if issubclass(definition.py_type, AltAz) and isinstance(value, SkyCoord): 

356 value = value.frame 

357 

358 if not isinstance(value, definition.py_type): 

359 return False 

360 

361 return True 

362 

363 @property 

364 def cards_used(self) -> frozenset[str]: 

365 """Header cards used for the translation. 

366 

367 Returns 

368 ------- 

369 used : `frozenset` of `str` 

370 Set of card used. 

371 """ 

372 if not self._translator: 

373 return frozenset() 

374 return self._translator.cards_used() 

375 

376 def stripped_header(self) -> MutableMapping[str, Any]: 

377 """Return a copy of the supplied header with used keywords removed. 

378 

379 Returns 

380 ------- 

381 stripped : `dict`-like 

382 Same class as header supplied to constructor, but with the 

383 headers used to calculate the generic information removed. 

384 """ 

385 hdr = copy.copy(self._header) 

386 used = self.cards_used 

387 for c in used: 

388 del hdr[c] 

389 return hdr 

390 

391 def __str__(self) -> str: 

392 # Put more interesting answers at front of list 

393 # and then do remainder 

394 priority = ("instrument", "telescope", "datetime_begin") 

395 properties = sorted(set(self.all_properties) - set(priority)) 

396 

397 result = "" 

398 for p in itertools.chain(priority, properties): 

399 value = getattr(self, p) 

400 if isinstance(value, astropy.time.Time): 

401 value.format = "isot" 

402 value = str(value.value) 

403 result += f"{p}: {value}\n" 

404 

405 return result 

406 

407 def __eq__(self, other: Any) -> bool: 

408 """Check equality with another object. 

409 

410 Compares equal if standard properties are equal. 

411 

412 Parameters 

413 ---------- 

414 other : `typing.Any` 

415 Thing to compare with. 

416 """ 

417 if not isinstance(other, ObservationInfo): 

418 return NotImplemented 

419 

420 # Compare simplified forms. 

421 # Cannot compare directly because nan will not equate as equal 

422 # whereas they should be equal for our purposes 

423 self_simple = self.to_simple() 

424 other_simple = other.to_simple() 

425 

426 # We don't care about the translator internal detail 

427 self_simple.pop("_translator", None) 

428 other_simple.pop("_translator", None) 

429 

430 for k, self_value in self_simple.items(): 

431 other_value = other_simple[k] 

432 if self_value != other_value: 

433 if math.isnan(self_value) and math.isnan(other_value): 

434 # If both are nan this is fine 

435 continue 

436 return False 

437 return True 

438 

439 def __lt__(self, other: Any) -> bool: 

440 if not isinstance(other, ObservationInfo): 

441 return NotImplemented 

442 return self.datetime_begin < other.datetime_begin 

443 

444 def __gt__(self, other: Any) -> bool: 

445 if not isinstance(other, ObservationInfo): 

446 return NotImplemented 

447 return self.datetime_begin > other.datetime_begin 

448 

449 def __getstate__(self) -> tuple[Any, ...]: 

450 """Get pickleable state. 

451 

452 Returns the properties. Deliberately does not preserve the full 

453 current state; in particular, does not return the full header or 

454 translator. 

455 

456 Returns 

457 ------- 

458 state : `tuple` 

459 Pickled state. 

460 """ 

461 state = dict() 

462 for p in self.all_properties: 

463 state[p] = getattr(self, p) 

464 

465 return state, self.extensions 

466 

467 def __setstate__(self, state: tuple[Any, ...]) -> None: 

468 """Set object state from pickle. 

469 

470 Parameters 

471 ---------- 

472 state : `tuple` 

473 Pickled state. 

474 """ 

475 try: 

476 state, extensions = state 

477 except ValueError: 

478 # Backwards compatibility for pickles generated before DM-34175 

479 extensions = {} 

480 self._declare_extensions(extensions) 

481 for p in self.all_properties: 

482 if p.startswith("ext_"): 

483 # allows setting even write-protected extensions 

484 super().__setattr__(p, state[p]) # type: ignore 

485 else: 

486 property = f"_{p}" 

487 setattr(self, property, state[p]) # type: ignore 

488 

489 def to_simple(self) -> MutableMapping[str, Any]: 

490 """Convert the contents of this object to simple dict form. 

491 

492 The keys of the dict are the standard properties but the values 

493 can be simplified to support JSON serialization. For example a 

494 SkyCoord might be represented as an ICRS RA/Dec tuple rather than 

495 a full SkyCoord representation. 

496 

497 Any properties with `None` value will be skipped. 

498 

499 Can be converted back to an `ObservationInfo` using `from_simple()`. 

500 

501 Returns 

502 ------- 

503 simple : `dict` of [`str`, `~typing.Any`] 

504 Simple dict of all properties. 

505 

506 Notes 

507 ----- 

508 Round-tripping of extension properties requires that the 

509 `ObservationInfo` was created with the help of a registered 

510 `MetadataTranslator` (which contains the extension property 

511 definitions). 

512 """ 

513 simple = {} 

514 if hasattr(self, "_translator") and self._translator and self._translator.name: 

515 simple["_translator"] = self._translator.name 

516 

517 for p in self.all_properties: 

518 property = f"_{p}" if not p.startswith("ext_") else p 

519 value = getattr(self, property) 

520 if value is None: 

521 continue 

522 

523 # Access the function to simplify the property 

524 simplifier = self.all_properties[p].to_simple 

525 

526 if simplifier is None: 

527 simple[p] = value 

528 continue 

529 

530 simple[p] = simplifier(value) 

531 

532 return simple 

533 

534 def to_json(self) -> str: 

535 """Serialize the object to JSON string. 

536 

537 Returns 

538 ------- 

539 j : `str` 

540 The properties of the ObservationInfo in JSON string form. 

541 

542 Notes 

543 ----- 

544 Round-tripping of extension properties requires that the 

545 `ObservationInfo` was created with the help of a registered 

546 `MetadataTranslator` (which contains the extension property 

547 definitions). 

548 """ 

549 return json.dumps(self.to_simple()) 

550 

551 @classmethod 

552 def from_simple(cls, simple: MutableMapping[str, Any]) -> ObservationInfo: 

553 """Convert the entity returned by `to_simple` back into an 

554 `ObservationInfo`. 

555 

556 Parameters 

557 ---------- 

558 simple : `dict` [`str`, `~typing.Any`] 

559 The dict returned by `to_simple()`. 

560 

561 Returns 

562 ------- 

563 obsinfo : `ObservationInfo` 

564 New object constructed from the dict. 

565 

566 Notes 

567 ----- 

568 Round-tripping of extension properties requires that the 

569 `ObservationInfo` was created with the help of a registered 

570 `MetadataTranslator` (which contains the extension property 

571 definitions). 

572 """ 

573 extensions = {} 

574 translator = simple.pop("_translator", None) 

575 if translator: 

576 if translator not in MetadataTranslator.translators: 

577 raise KeyError(f"Unrecognised translator: {translator}") 

578 extensions = MetadataTranslator.translators[translator].extensions 

579 

580 properties = cls._get_all_properties(extensions) 

581 

582 processed: dict[str, Any] = {} 

583 for k, v in simple.items(): 

584 if v is None: 

585 continue 

586 

587 # Access the function to convert from simple form 

588 complexifier = properties[k].from_simple 

589 

590 if complexifier is not None: 

591 v = complexifier(v, **processed) 

592 

593 processed[k] = v 

594 

595 return cls.makeObservationInfo(extensions=extensions, **processed) 

596 

597 @classmethod 

598 def from_json(cls, json_str: str) -> ObservationInfo: 

599 """Create `ObservationInfo` from JSON string. 

600 

601 Parameters 

602 ---------- 

603 json_str : `str` 

604 The JSON representation. 

605 

606 Returns 

607 ------- 

608 obsinfo : `ObservationInfo` 

609 Reconstructed object. 

610 

611 Notes 

612 ----- 

613 Round-tripping of extension properties requires that the 

614 `ObservationInfo` was created with the help of a registered 

615 `MetadataTranslator` (which contains the extension property 

616 definitions). 

617 """ 

618 simple = json.loads(json_str) 

619 return cls.from_simple(simple) 

620 

621 @classmethod 

622 def makeObservationInfo( # noqa: N802 

623 cls, *, extensions: dict[str, PropertyDefinition] | None = None, **kwargs: Any 

624 ) -> ObservationInfo: 

625 """Construct an `ObservationInfo` from the supplied parameters. 

626 

627 Parameters 

628 ---------- 

629 extensions : `dict` [`str`: `PropertyDefinition`], optional 

630 Optional extension definitions, indexed by extension name (without 

631 the ``ext_`` prefix, which will be added by `ObservationInfo`). 

632 **kwargs 

633 Name-value pairs for any properties to be set. In the case of 

634 extension properties, the names should include the ``ext_`` prefix. 

635 

636 Notes 

637 ----- 

638 The supplied parameters should use names matching the property. 

639 The type of the supplied value will be checked against the property. 

640 Any properties not supplied will be assigned a value of `None`. 

641 

642 Raises 

643 ------ 

644 KeyError 

645 Raised if a supplied parameter key is not a known property. 

646 TypeError 

647 Raised if a supplied value does not match the expected type 

648 of the property. 

649 """ 

650 obsinfo = cls(None) 

651 obsinfo._declare_extensions(extensions) 

652 

653 unused = set(kwargs) 

654 

655 for p in obsinfo.all_properties: 

656 if p in kwargs: 

657 property = f"_{p}" if not p.startswith("ext_") else p 

658 value = kwargs[p] 

659 definition = obsinfo.all_properties[p] 

660 if not cls._is_property_ok(definition, value): 

661 raise TypeError( 

662 f"Supplied value {value} for property {p} " 

663 f"should be of class {definition.str_type} not {value.__class__}" 

664 ) 

665 super(cls, obsinfo).__setattr__(property, value) # allows setting write-protected extensions 

666 unused.remove(p) 

667 

668 # Recent additions to ObservationInfo may not be present in 

669 # serializations. In theory they can be derived from other 

670 # values in the default case. This might not be the right thing 

671 # to do. 

672 for k in ("group_counter_start", "group_counter_end"): 

673 if k not in kwargs and "observation_counter" in kwargs: 

674 super(cls, obsinfo).__setattr__(f"_{k}", obsinfo.observation_counter) 

675 if (k := "has_simulated_content") not in kwargs: 

676 super(cls, obsinfo).__setattr__(f"_{k}", False) 

677 

678 if unused: 

679 n = len(unused) 

680 raise KeyError(f"Unrecognized propert{'y' if n == 1 else 'ies'} provided: {', '.join(unused)}") 

681 

682 return obsinfo 

683 

684 

685# Method to add the standard properties 

686def _make_property(property: str, doc: str, return_typedoc: str, return_type: type) -> Callable: 

687 """Create a getter method with associated docstring. 

688 

689 Parameters 

690 ---------- 

691 property : `str` 

692 Name of the property getter to be created. 

693 doc : `str` 

694 Description of this property. 

695 return_typedoc : `str` 

696 Type string of this property (used in the doc string). 

697 return_type : `class` 

698 Type of this property. 

699 

700 Returns 

701 ------- 

702 p : `function` 

703 Getter method for this property. 

704 """ 

705 

706 def getter(self: ObservationInfo) -> Any: 

707 return getattr(self, f"_{property}") 

708 

709 getter.__doc__ = f"""{doc} 

710 

711 Returns 

712 ------- 

713 {property} : `{return_typedoc}` 

714 Access the property. 

715 """ 

716 return getter 

717 

718 

719# Set up the core set of properties 

720# In order to provide read-only protection, each attribute is hidden behind a 

721# python "property" wrapper. 

722for name, definition in PROPERTIES.items(): 

723 setattr(ObservationInfo, f"_{name}", None) 

724 setattr( 

725 ObservationInfo, 

726 name, 

727 property(_make_property(name, definition.doc, definition.str_type, definition.py_type)), 

728 ) 

729 

730 

731def makeObservationInfo( # noqa: N802 

732 *, extensions: dict[str, PropertyDefinition] | None = None, **kwargs: Any 

733) -> ObservationInfo: 

734 """Construct an `ObservationInfo` from the supplied parameters. 

735 

736 Parameters 

737 ---------- 

738 extensions : `dict` [`str`: `PropertyDefinition`], optional 

739 Optional extension definitions, indexed by extension name (without 

740 the ``ext_`` prefix, which will be added by `ObservationInfo`). 

741 **kwargs 

742 Name-value pairs for any properties to be set. In the case of 

743 extension properties, the names should include the ``ext_`` prefix. 

744 

745 Notes 

746 ----- 

747 The supplied parameters should use names matching the property. 

748 The type of the supplied value will be checked against the property. 

749 Any properties not supplied will be assigned a value of `None`. 

750 

751 Raises 

752 ------ 

753 KeyError 

754 Raised if a supplied parameter key is not a known property. 

755 TypeError 

756 Raised if a supplied value does not match the expected type 

757 of the property. 

758 """ 

759 return ObservationInfo.makeObservationInfo(extensions=extensions, **kwargs)