Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21import abc 

22import datetime 

23import os.path 

24import warnings 

25import yaml 

26from astropy.table import Table 

27from astropy.io import fits 

28 

29from lsst.log import Log 

30from lsst.daf.base import PropertyList 

31 

32 

33__all__ = ["IsrCalib", "IsrProvenance"] 

34 

35 

36class IsrCalib(abc.ABC): 

37 """Generic calibration type. 

38 

39 Subclasses must implement the toDict, fromDict, toTable, fromTable 

40 methods that allow the calibration information to be converted 

41 from dictionaries and afw tables. This will allow the calibration 

42 to be persisted using the base class read/write methods. 

43 

44 The validate method is intended to provide a common way to check 

45 that the calibration is valid (internally consistent) and 

46 appropriate (usable with the intended data). The apply method is 

47 intended to allow the calibration to be applied in a consistent 

48 manner. 

49 

50 Parameters 

51 ---------- 

52 camera : `lsst.afw.cameraGeom.Camera`, optional 

53 Camera to extract metadata from. 

54 detector : `lsst.afw.cameraGeom.Detector`, optional 

55 Detector to extract metadata from. 

56 log : `lsst.log.Log`, optional 

57 Log for messages. 

58 """ 

59 _OBSTYPE = 'generic' 

60 _SCHEMA = 'NO SCHEMA' 

61 _VERSION = 0 

62 

63 def __init__(self, camera=None, detector=None, log=None, **kwargs): 

64 self._instrument = None 

65 self._raftName = None 

66 self._slotName = None 

67 self._detectorName = None 

68 self._detectorSerial = None 

69 self._detectorId = None 

70 self._filter = None 

71 self._calibId = None 

72 self._metadata = PropertyList() 

73 self.setMetadata(PropertyList()) 

74 self.calibInfoFromDict(kwargs) 

75 

76 # Define the required attributes for this calibration. 

77 self.requiredAttributes = set(['_OBSTYPE', '_SCHEMA', '_VERSION']) 

78 self.requiredAttributes.update(['_instrument', '_raftName', '_slotName', 

79 '_detectorName', '_detectorSerial', '_detectorId', 

80 '_filter', '_calibId', '_metadata']) 

81 

82 self.log = log if log else Log.getLogger(__name__.partition(".")[2]) 

83 

84 if detector: 

85 self.fromDetector(detector) 

86 self.updateMetadata(camera=camera, detector=detector) 

87 

88 def __str__(self): 

89 return f"{self.__class__.__name__}(obstype={self._OBSTYPE}, detector={self._detectorName}, )" 

90 

91 def __eq__(self, other): 

92 """Calibration equivalence. 

93 

94 Subclasses will need to check specific sub-properties. The 

95 default is only to check common entries. 

96 """ 

97 if not isinstance(other, self.__class__): 

98 return False 

99 

100 for attr in self._requiredAttributes: 

101 if getattr(self, attr) != getattr(other, attr): 

102 return False 

103 

104 return True 

105 

106 @property 

107 def requiredAttributes(self): 

108 return self._requiredAttributes 

109 

110 @requiredAttributes.setter 

111 def requiredAttributes(self, value): 

112 self._requiredAttributes = value 

113 

114 def getMetadata(self): 

115 """Retrieve metadata associated with this calibration. 

116 

117 Returns 

118 ------- 

119 meta : `lsst.daf.base.PropertyList` 

120 Metadata. The returned `~lsst.daf.base.PropertyList` can be 

121 modified by the caller and the changes will be written to 

122 external files. 

123 """ 

124 return self._metadata 

125 

126 def setMetadata(self, metadata): 

127 """Store a copy of the supplied metadata with this calibration. 

128 

129 Parameters 

130 ---------- 

131 metadata : `lsst.daf.base.PropertyList` 

132 Metadata to associate with the calibration. Will be copied and 

133 overwrite existing metadata. 

134 """ 

135 if metadata is not None: 

136 self._metadata.update(metadata) 

137 

138 # Ensure that we have the obs type required by calibration ingest 

139 self._metadata["OBSTYPE"] = self._OBSTYPE 

140 self._metadata[self._OBSTYPE + "_SCHEMA"] = self._SCHEMA 

141 self._metadata[self._OBSTYPE + "_VERSION"] = self._VERSION 

142 

143 if isinstance(metadata, dict): 

144 self.calibInfoFromDict(metadata) 

145 elif isinstance(metadata, PropertyList): 

146 self.calibInfoFromDict(metadata.toDict()) 

147 

148 def updateMetadata(self, camera=None, detector=None, filterName=None, 

149 setCalibId=False, setCalibInfo=False, setDate=False, 

150 **kwargs): 

151 """Update metadata keywords with new values. 

152 

153 Parameters 

154 ---------- 

155 camera : `lsst.afw.cameraGeom.Camera`, optional 

156 Reference camera to use to set _instrument field. 

157 detector : `lsst.afw.cameraGeom.Detector`, optional 

158 Reference detector to use to set _detector* fields. 

159 filterName : `str`, optional 

160 Filter name to assign to this calibration. 

161 setCalibId : `bool`, optional 

162 Construct the _calibId field from other fields. 

163 setCalibInfo : `bool`, optional 

164 Set calibration parameters from metadata. 

165 setDate : `bool`, optional 

166 Ensure the metadata CALIBDATE fields are set to the current datetime. 

167 kwargs : `dict` or `collections.abc.Mapping`, optional 

168 Set of key=value pairs to assign to the metadata. 

169 """ 

170 mdOriginal = self.getMetadata() 

171 mdSupplemental = dict() 

172 

173 for k, v in kwargs.items(): 

174 if isinstance(v, fits.card.Undefined): 

175 kwargs[k] = None 

176 

177 if setCalibInfo: 

178 self.calibInfoFromDict(kwargs) 

179 

180 if camera: 

181 self._instrument = camera.getName() 

182 

183 if detector: 

184 self._detectorName = detector.getName() 

185 self._detectorSerial = detector.getSerial() 

186 self._detectorId = detector.getId() 

187 if "_" in self._detectorName: 

188 (self._raftName, self._slotName) = self._detectorName.split("_") 

189 

190 if filterName: 

191 # TOD0 DM-28093: I think this whole comment can go away, if we 

192 # always use physicalLabel everywhere in ip_isr. 

193 # If set via: 

194 # exposure.getInfo().getFilter().getName() 

195 # then this will hold the abstract filter. 

196 self._filter = filterName 

197 

198 if setDate: 

199 date = datetime.datetime.now() 

200 mdSupplemental['CALIBDATE'] = date.isoformat() 

201 mdSupplemental['CALIB_CREATION_DATE'] = date.date().isoformat() 

202 mdSupplemental['CALIB_CREATION_TIME'] = date.time().isoformat() 

203 

204 if setCalibId: 

205 values = [] 

206 values.append(f"instrument={self._instrument}") if self._instrument else None 

207 values.append(f"raftName={self._raftName}") if self._raftName else None 

208 values.append(f"detectorName={self._detectorName}") if self._detectorName else None 

209 values.append(f"detector={self._detectorId}") if self._detectorId else None 

210 values.append(f"filter={self._filter}") if self._filter else None 

211 

212 calibDate = mdOriginal.get('CALIBDATE', mdSupplemental.get('CALIBDATE', None)) 

213 values.append(f"calibDate={calibDate}") if calibDate else None 

214 

215 self._calibId = " ".join(values) 

216 

217 self._metadata["INSTRUME"] = self._instrument if self._instrument else None 

218 self._metadata["RAFTNAME"] = self._raftName if self._raftName else None 

219 self._metadata["SLOTNAME"] = self._slotName if self._slotName else None 

220 self._metadata["DETECTOR"] = self._detectorId 

221 self._metadata["DET_NAME"] = self._detectorName if self._detectorName else None 

222 self._metadata["DET_SER"] = self._detectorSerial if self._detectorSerial else None 

223 self._metadata["FILTER"] = self._filter if self._filter else None 

224 self._metadata["CALIB_ID"] = self._calibId if self._calibId else None 

225 

226 mdSupplemental.update(kwargs) 

227 mdOriginal.update(mdSupplemental) 

228 

229 def calibInfoFromDict(self, dictionary): 

230 """Handle common keywords. 

231 

232 This isn't an ideal solution, but until all calibrations 

233 expect to find everything in the metadata, they still need to 

234 search through dictionaries. 

235 

236 Parameters 

237 ---------- 

238 dictionary : `dict` or `lsst.daf.base.PropertyList` 

239 Source for the common keywords. 

240 

241 Raises 

242 ------ 

243 RuntimeError : 

244 Raised if the dictionary does not match the expected OBSTYPE. 

245 

246 """ 

247 

248 def search(haystack, needles): 

249 """Search dictionary 'haystack' for an entry in 'needles' 

250 """ 

251 test = [haystack.get(x) for x in needles] 

252 test = set([x for x in test if x is not None]) 

253 if len(test) == 0: 

254 if 'metadata' in haystack: 

255 return search(haystack['metadata'], needles) 

256 else: 

257 return None 

258 elif len(test) == 1: 

259 value = list(test)[0] 

260 if value == '': 

261 return None 

262 else: 

263 return value 

264 else: 

265 raise ValueError(f"Too many values found: {len(test)} {test} {needles}") 

266 

267 if 'metadata' in dictionary: 

268 metadata = dictionary['metadata'] 

269 

270 if self._OBSTYPE != metadata['OBSTYPE']: 

271 raise RuntimeError(f"Incorrect calibration supplied. Expected {self._OBSTYPE}, " 

272 f"found {metadata['OBSTYPE']}") 

273 

274 self._instrument = search(dictionary, ['INSTRUME', 'instrument']) 

275 self._raftName = search(dictionary, ['RAFTNAME']) 

276 self._slotName = search(dictionary, ['SLOTNAME']) 

277 self._detectorId = search(dictionary, ['DETECTOR', 'detectorId']) 

278 self._detectorName = search(dictionary, ['DET_NAME', 'DETECTOR_NAME', 'detectorName']) 

279 self._detectorSerial = search(dictionary, ['DET_SER', 'DETECTOR_SERIAL', 'detectorSerial']) 

280 self._filter = search(dictionary, ['FILTER', 'filterName']) 

281 self._calibId = search(dictionary, ['CALIB_ID']) 

282 

283 @classmethod 

284 def readText(cls, filename, **kwargs): 

285 """Read calibration representation from a yaml/ecsv file. 

286 

287 Parameters 

288 ---------- 

289 filename : `str` 

290 Name of the file containing the calibration definition. 

291 kwargs : `dict` or collections.abc.Mapping`, optional 

292 Set of key=value pairs to pass to the ``fromDict`` or 

293 ``fromTable`` methods. 

294 

295 Returns 

296 ------- 

297 calib : `~lsst.ip.isr.IsrCalibType` 

298 Calibration class. 

299 

300 Raises 

301 ------ 

302 RuntimeError : 

303 Raised if the filename does not end in ".ecsv" or ".yaml". 

304 """ 

305 if filename.endswith((".ecsv", ".ECSV")): 

306 data = Table.read(filename, format='ascii.ecsv') 

307 return cls.fromTable([data], **kwargs) 

308 

309 elif filename.endswith((".yaml", ".YAML")): 

310 with open(filename, 'r') as f: 

311 data = yaml.load(f, Loader=yaml.CLoader) 

312 return cls.fromDict(data, **kwargs) 

313 else: 

314 raise RuntimeError(f"Unknown filename extension: {filename}") 

315 

316 def writeText(self, filename, format='auto'): 

317 """Write the calibration data to a text file. 

318 

319 Parameters 

320 ---------- 

321 filename : `str` 

322 Name of the file to write. 

323 format : `str` 

324 Format to write the file as. Supported values are: 

325 ``"auto"`` : Determine filetype from filename. 

326 ``"yaml"`` : Write as yaml. 

327 ``"ecsv"`` : Write as ecsv. 

328 Returns 

329 ------- 

330 used : `str` 

331 The name of the file used to write the data. This may 

332 differ from the input if the format is explicitly chosen. 

333 

334 Raises 

335 ------ 

336 RuntimeError : 

337 Raised if filename does not end in a known extension, or 

338 if all information cannot be written. 

339 

340 Notes 

341 ----- 

342 The file is written to YAML/ECSV format and will include any 

343 associated metadata. 

344 

345 """ 

346 if format == 'yaml' or (format == 'auto' and filename.lower().endswith((".yaml", ".YAML"))): 

347 outDict = self.toDict() 

348 path, ext = os.path.splitext(filename) 

349 filename = path + ".yaml" 

350 with open(filename, 'w') as f: 

351 yaml.dump(outDict, f) 

352 elif format == 'ecsv' or (format == 'auto' and filename.lower().endswith((".ecsv", ".ECSV"))): 

353 tableList = self.toTable() 

354 if len(tableList) > 1: 

355 # ECSV doesn't support multiple tables per file, so we 

356 # can only write the first table. 

357 raise RuntimeError(f"Unable to persist {len(tableList)}tables in ECSV format.") 

358 

359 table = tableList[0] 

360 path, ext = os.path.splitext(filename) 

361 filename = path + ".ecsv" 

362 table.write(filename, format="ascii.ecsv") 

363 else: 

364 raise RuntimeError(f"Attempt to write to a file {filename} " 

365 "that does not end in '.yaml' or '.ecsv'") 

366 

367 return filename 

368 

369 @classmethod 

370 def readFits(cls, filename, **kwargs): 

371 """Read calibration data from a FITS file. 

372 

373 Parameters 

374 ---------- 

375 filename : `str` 

376 Filename to read data from. 

377 kwargs : `dict` or collections.abc.Mapping`, optional 

378 Set of key=value pairs to pass to the ``fromTable`` 

379 method. 

380 

381 Returns 

382 ------- 

383 calib : `lsst.ip.isr.IsrCalib` 

384 Calibration contained within the file. 

385 """ 

386 tableList = [] 

387 tableList.append(Table.read(filename, hdu=1)) 

388 extNum = 2 # Fits indices start at 1, we've read one already. 

389 keepTrying = True 

390 

391 while keepTrying: 

392 with warnings.catch_warnings(): 

393 warnings.simplefilter("error") 

394 try: 

395 newTable = Table.read(filename, hdu=extNum) 

396 tableList.append(newTable) 

397 extNum += 1 

398 except Exception: 

399 keepTrying = False 

400 

401 for table in tableList: 

402 for k, v in table.meta.items(): 

403 if isinstance(v, fits.card.Undefined): 

404 table.meta[k] = None 

405 

406 return cls.fromTable(tableList, **kwargs) 

407 

408 def writeFits(self, filename): 

409 """Write calibration data to a FITS file. 

410 

411 Parameters 

412 ---------- 

413 filename : `str` 

414 Filename to write data to. 

415 

416 Returns 

417 ------- 

418 used : `str` 

419 The name of the file used to write the data. 

420 

421 """ 

422 tableList = self.toTable() 

423 with warnings.catch_warnings(): 

424 warnings.filterwarnings("ignore", category=Warning, module="astropy.io") 

425 astropyList = [fits.table_to_hdu(table) for table in tableList] 

426 astropyList.insert(0, fits.PrimaryHDU()) 

427 

428 writer = fits.HDUList(astropyList) 

429 writer.writeto(filename, overwrite=True) 

430 return filename 

431 

432 def fromDetector(self, detector): 

433 """Modify the calibration parameters to match the supplied detector. 

434 

435 Parameters 

436 ---------- 

437 detector : `lsst.afw.cameraGeom.Detector` 

438 Detector to use to set parameters from. 

439 

440 Raises 

441 ------ 

442 NotImplementedError 

443 This needs to be implemented by subclasses for each 

444 calibration type. 

445 """ 

446 raise NotImplementedError("Must be implemented by subclass.") 

447 

448 @classmethod 

449 def fromDict(cls, dictionary, **kwargs): 

450 """Construct a calibration from a dictionary of properties. 

451 

452 Must be implemented by the specific calibration subclasses. 

453 

454 Parameters 

455 ---------- 

456 dictionary : `dict` 

457 Dictionary of properties. 

458 kwargs : `dict` or collections.abc.Mapping`, optional 

459 Set of key=value options. 

460 

461 Returns 

462 ------ 

463 calib : `lsst.ip.isr.CalibType` 

464 Constructed calibration. 

465 

466 Raises 

467 ------ 

468 NotImplementedError : 

469 Raised if not implemented. 

470 """ 

471 raise NotImplementedError("Must be implemented by subclass.") 

472 

473 def toDict(self): 

474 """Return a dictionary containing the calibration properties. 

475 

476 The dictionary should be able to be round-tripped through 

477 `fromDict`. 

478 

479 Returns 

480 ------- 

481 dictionary : `dict` 

482 Dictionary of properties. 

483 

484 Raises 

485 ------ 

486 NotImplementedError : 

487 Raised if not implemented. 

488 """ 

489 raise NotImplementedError("Must be implemented by subclass.") 

490 

491 @classmethod 

492 def fromTable(cls, tableList, **kwargs): 

493 """Construct a calibration from a dictionary of properties. 

494 

495 Must be implemented by the specific calibration subclasses. 

496 

497 Parameters 

498 ---------- 

499 tableList : `list` [`lsst.afw.table.Table`] 

500 List of tables of properties. 

501 kwargs : `dict` or collections.abc.Mapping`, optional 

502 Set of key=value options. 

503 

504 Returns 

505 ------ 

506 calib : `lsst.ip.isr.CalibType` 

507 Constructed calibration. 

508 

509 Raises 

510 ------ 

511 NotImplementedError : 

512 Raised if not implemented. 

513 """ 

514 raise NotImplementedError("Must be implemented by subclass.") 

515 

516 def toTable(self): 

517 """Return a list of tables containing the calibration properties. 

518 

519 The table list should be able to be round-tripped through 

520 `fromDict`. 

521 

522 Returns 

523 ------- 

524 tableList : `list` [`lsst.afw.table.Table`] 

525 List of tables of properties. 

526 

527 Raises 

528 ------ 

529 NotImplementedError : 

530 Raised if not implemented. 

531 """ 

532 raise NotImplementedError("Must be implemented by subclass.") 

533 

534 def validate(self, other=None): 

535 """Validate that this calibration is defined and can be used. 

536 

537 Parameters 

538 ---------- 

539 other : `object`, optional 

540 Thing to validate against. 

541 

542 Returns 

543 ------- 

544 valid : `bool` 

545 Returns true if the calibration is valid and appropriate. 

546 """ 

547 return False 

548 

549 def apply(self, target): 

550 """Method to apply the calibration to the target object. 

551 

552 Parameters 

553 ---------- 

554 target : `object` 

555 Thing to validate against. 

556 

557 Returns 

558 ------- 

559 valid : `bool` 

560 Returns true if the calibration was applied correctly. 

561 

562 Raises 

563 ------ 

564 NotImplementedError : 

565 Raised if not implemented. 

566 """ 

567 raise NotImplementedError("Must be implemented by subclass.") 

568 

569 

570class IsrProvenance(IsrCalib): 

571 """Class for the provenance of data used to construct calibration. 

572 

573 Provenance is not really a calibration, but we would like to 

574 record this when constructing the calibration, and it provides an 

575 example of the base calibration class. 

576 

577 Parameters 

578 ---------- 

579 instrument : `str`, optional 

580 Name of the instrument the data was taken with. 

581 calibType : `str`, optional 

582 Type of calibration this provenance was generated for. 

583 detectorName : `str`, optional 

584 Name of the detector this calibration is for. 

585 detectorSerial : `str`, optional 

586 Identifier for the detector. 

587 

588 """ 

589 _OBSTYPE = 'IsrProvenance' 

590 

591 def __init__(self, calibType="unknown", 

592 **kwargs): 

593 self.calibType = calibType 

594 self.dimensions = set() 

595 self.dataIdList = list() 

596 

597 super().__init__(**kwargs) 

598 

599 self.requiredAttributes.update(['calibType', 'dimensions', 'dataIdList']) 

600 

601 def __str__(self): 

602 return f"{self.__class__.__name__}(obstype={self._OBSTYPE}, calibType={self.calibType}, )" 

603 

604 def __eq__(self, other): 

605 return super().__eq__(other) 

606 

607 def updateMetadata(self, setDate=False, **kwargs): 

608 """Update calibration metadata. 

609 

610 Parameters 

611 ---------- 

612 setDate : `bool, optional 

613 Update the CALIBDATE fields in the metadata to the current 

614 time. Defaults to False. 

615 kwargs : `dict` or `collections.abc.Mapping`, optional 

616 Other keyword parameters to set in the metadata. 

617 """ 

618 kwargs['calibType'] = self.calibType 

619 super().updateMetadata(setDate=setDate, **kwargs) 

620 

621 def fromDataIds(self, dataIdList): 

622 """Update provenance from dataId List. 

623 

624 Parameters 

625 ---------- 

626 dataIdList : `list` [`lsst.daf.butler.DataId`] 

627 List of dataIds used in generating this calibration. 

628 """ 

629 for dataId in dataIdList: 

630 for key in dataId: 

631 if key not in self.dimensions: 

632 self.dimensions.add(key) 

633 self.dataIdList.append(dataId) 

634 

635 @classmethod 

636 def fromTable(cls, tableList): 

637 """Construct provenance from table list. 

638 

639 Parameters 

640 ---------- 

641 tableList : `list` [`lsst.afw.table.Table`] 

642 List of tables to construct the provenance from. 

643 

644 Returns 

645 ------- 

646 provenance : `lsst.ip.isr.IsrProvenance` 

647 The provenance defined in the tables. 

648 """ 

649 table = tableList[0] 

650 metadata = table.meta 

651 inDict = dict() 

652 inDict['metadata'] = metadata 

653 inDict['calibType'] = metadata['calibType'] 

654 inDict['dimensions'] = set() 

655 inDict['dataIdList'] = list() 

656 

657 schema = dict() 

658 for colName in table.columns: 

659 schema[colName.lower()] = colName 

660 inDict['dimensions'].add(colName.lower()) 

661 inDict['dimensions'] = sorted(inDict['dimensions']) 

662 

663 for row in table: 

664 entry = dict() 

665 for dim in sorted(inDict['dimensions']): 

666 entry[dim] = row[schema[dim]] 

667 inDict['dataIdList'].append(entry) 

668 

669 return cls.fromDict(inDict) 

670 

671 @classmethod 

672 def fromDict(cls, dictionary): 

673 """Construct provenance from a dictionary. 

674 

675 Parameters 

676 ---------- 

677 dictionary : `dict` 

678 Dictionary of provenance parameters. 

679 

680 Returns 

681 ------- 

682 provenance : `lsst.ip.isr.IsrProvenance` 

683 The provenance defined in the tables. 

684 """ 

685 calib = cls() 

686 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']: 

687 raise RuntimeError(f"Incorrect calibration supplied. Expected {calib._OBSTYPE}, " 

688 f"found {dictionary['metadata']['OBSTYPE']}") 

689 calib.updateMetadata(setDate=False, setCalibInfo=True, **dictionary['metadata']) 

690 

691 # These properties should be in the metadata, but occasionally 

692 # are found in the dictionary itself. Check both places, 

693 # ending with `None` if neither contains the information. 

694 calib.calibType = dictionary['calibType'] 

695 calib.dimensions = set(dictionary['dimensions']) 

696 calib.dataIdList = dictionary['dataIdList'] 

697 

698 calib.updateMetadata() 

699 return calib 

700 

701 def toDict(self): 

702 """Return a dictionary containing the provenance information. 

703 

704 Returns 

705 ------- 

706 dictionary : `dict` 

707 Dictionary of provenance. 

708 """ 

709 self.updateMetadata() 

710 

711 outDict = {} 

712 

713 metadata = self.getMetadata() 

714 outDict['metadata'] = metadata 

715 outDict['detectorName'] = self._detectorName 

716 outDict['detectorSerial'] = self._detectorSerial 

717 outDict['detectorId'] = self._detectorId 

718 outDict['instrument'] = self._instrument 

719 outDict['calibType'] = self.calibType 

720 outDict['dimensions'] = list(self.dimensions) 

721 outDict['dataIdList'] = self.dataIdList 

722 

723 return outDict 

724 

725 def toTable(self): 

726 """Return a list of tables containing the provenance. 

727 

728 This seems inefficient and slow, so this may not be the best 

729 way to store the data. 

730 

731 Returns 

732 ------- 

733 tableList : `list` [`lsst.afw.table.Table`] 

734 List of tables containing the provenance information 

735 

736 """ 

737 tableList = [] 

738 self.updateMetadata(setDate=True, setCalibInfo=True) 

739 

740 catalog = Table(rows=self.dataIdList, 

741 names=self.dimensions) 

742 filteredMetadata = {k: v for k, v in self.getMetadata().toDict().items() if v is not None} 

743 catalog.meta = filteredMetadata 

744 tableList.append(catalog) 

745 return tableList