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 # If set via: 

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

193 # then this will hold the abstract filter. 

194 self._filter = filterName 

195 

196 if setDate: 

197 date = datetime.datetime.now() 

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

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

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

201 

202 if setCalibId: 

203 values = [] 

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

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

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

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

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

209 

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

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

212 

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

214 

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

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

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

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

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

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

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

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

223 

224 mdSupplemental.update(kwargs) 

225 mdOriginal.update(mdSupplemental) 

226 

227 def calibInfoFromDict(self, dictionary): 

228 """Handle common keywords. 

229 

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

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

232 search through dictionaries. 

233 

234 Parameters 

235 ---------- 

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

237 Source for the common keywords. 

238 

239 Raises 

240 ------ 

241 RuntimeError : 

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

243 

244 """ 

245 

246 def search(haystack, needles): 

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

248 """ 

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

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

251 if len(test) == 0: 

252 if 'metadata' in haystack: 

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

254 else: 

255 return None 

256 elif len(test) == 1: 

257 value = list(test)[0] 

258 if value == '': 

259 return None 

260 else: 

261 return value 

262 else: 

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

264 

265 if 'metadata' in dictionary: 

266 metadata = dictionary['metadata'] 

267 

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

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

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

271 

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

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

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

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

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

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

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

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

280 

281 @classmethod 

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

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

284 

285 Parameters 

286 ---------- 

287 filename : `str` 

288 Name of the file containing the calibration definition. 

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

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

291 ``fromTable`` methods. 

292 

293 Returns 

294 ------- 

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

296 Calibration class. 

297 

298 Raises 

299 ------ 

300 RuntimeError : 

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

302 """ 

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

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

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

306 

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

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

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

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

311 else: 

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

313 

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

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

316 

317 Parameters 

318 ---------- 

319 filename : `str` 

320 Name of the file to write. 

321 format : `str` 

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

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

324 ``"yaml"`` : Write as yaml. 

325 ``"ecsv"`` : Write as ecsv. 

326 Returns 

327 ------- 

328 used : `str` 

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

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

331 

332 Raises 

333 ------ 

334 RuntimeError : 

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

336 if all information cannot be written. 

337 

338 Notes 

339 ----- 

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

341 associated metadata. 

342 

343 """ 

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

345 outDict = self.toDict() 

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

347 filename = path + ".yaml" 

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

349 yaml.dump(outDict, f) 

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

351 tableList = self.toTable() 

352 if len(tableList) > 1: 

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

354 # can only write the first table. 

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

356 

357 table = tableList[0] 

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

359 filename = path + ".ecsv" 

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

361 else: 

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

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

364 

365 return filename 

366 

367 @classmethod 

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

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

370 

371 Parameters 

372 ---------- 

373 filename : `str` 

374 Filename to read data from. 

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

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

377 method. 

378 

379 Returns 

380 ------- 

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

382 Calibration contained within the file. 

383 """ 

384 tableList = [] 

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

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

387 keepTrying = True 

388 

389 while keepTrying: 

390 with warnings.catch_warnings(): 

391 warnings.simplefilter("error") 

392 try: 

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

394 tableList.append(newTable) 

395 extNum += 1 

396 except Exception: 

397 keepTrying = False 

398 

399 for table in tableList: 

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

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

402 table.meta[k] = None 

403 

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

405 

406 def writeFits(self, filename): 

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

408 

409 Parameters 

410 ---------- 

411 filename : `str` 

412 Filename to write data to. 

413 

414 Returns 

415 ------- 

416 used : `str` 

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

418 

419 """ 

420 tableList = self.toTable() 

421 with warnings.catch_warnings(): 

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

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

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

425 

426 writer = fits.HDUList(astropyList) 

427 writer.writeto(filename, overwrite=True) 

428 return filename 

429 

430 def fromDetector(self, detector): 

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

432 

433 Parameters 

434 ---------- 

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

436 Detector to use to set parameters from. 

437 

438 Raises 

439 ------ 

440 NotImplementedError 

441 This needs to be implemented by subclasses for each 

442 calibration type. 

443 """ 

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

445 

446 @classmethod 

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

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

449 

450 Must be implemented by the specific calibration subclasses. 

451 

452 Parameters 

453 ---------- 

454 dictionary : `dict` 

455 Dictionary of properties. 

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

457 Set of key=value options. 

458 

459 Returns 

460 ------ 

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

462 Constructed calibration. 

463 

464 Raises 

465 ------ 

466 NotImplementedError : 

467 Raised if not implemented. 

468 """ 

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

470 

471 def toDict(self): 

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

473 

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

475 `fromDict`. 

476 

477 Returns 

478 ------- 

479 dictionary : `dict` 

480 Dictionary of properties. 

481 

482 Raises 

483 ------ 

484 NotImplementedError : 

485 Raised if not implemented. 

486 """ 

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

488 

489 @classmethod 

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

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

492 

493 Must be implemented by the specific calibration subclasses. 

494 

495 Parameters 

496 ---------- 

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

498 List of tables of properties. 

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

500 Set of key=value options. 

501 

502 Returns 

503 ------ 

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

505 Constructed calibration. 

506 

507 Raises 

508 ------ 

509 NotImplementedError : 

510 Raised if not implemented. 

511 """ 

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

513 

514 def toTable(self): 

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

516 

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

518 `fromDict`. 

519 

520 Returns 

521 ------- 

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

523 List of tables of properties. 

524 

525 Raises 

526 ------ 

527 NotImplementedError : 

528 Raised if not implemented. 

529 """ 

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

531 

532 def validate(self, other=None): 

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

534 

535 Parameters 

536 ---------- 

537 other : `object`, optional 

538 Thing to validate against. 

539 

540 Returns 

541 ------- 

542 valid : `bool` 

543 Returns true if the calibration is valid and appropriate. 

544 """ 

545 return False 

546 

547 def apply(self, target): 

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

549 

550 Parameters 

551 ---------- 

552 target : `object` 

553 Thing to validate against. 

554 

555 Returns 

556 ------- 

557 valid : `bool` 

558 Returns true if the calibration was applied correctly. 

559 

560 Raises 

561 ------ 

562 NotImplementedError : 

563 Raised if not implemented. 

564 """ 

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

566 

567 

568class IsrProvenance(IsrCalib): 

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

570 

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

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

573 example of the base calibration class. 

574 

575 Parameters 

576 ---------- 

577 instrument : `str`, optional 

578 Name of the instrument the data was taken with. 

579 calibType : `str`, optional 

580 Type of calibration this provenance was generated for. 

581 detectorName : `str`, optional 

582 Name of the detector this calibration is for. 

583 detectorSerial : `str`, optional 

584 Identifier for the detector. 

585 

586 """ 

587 _OBSTYPE = 'IsrProvenance' 

588 

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

590 **kwargs): 

591 self.calibType = calibType 

592 self.dimensions = set() 

593 self.dataIdList = list() 

594 

595 super().__init__(**kwargs) 

596 

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

598 

599 def __str__(self): 

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

601 

602 def __eq__(self, other): 

603 return super().__eq__(other) 

604 

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

606 """Update calibration metadata. 

607 

608 Parameters 

609 ---------- 

610 setDate : `bool, optional 

611 Update the CALIBDATE fields in the metadata to the current 

612 time. Defaults to False. 

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

614 Other keyword parameters to set in the metadata. 

615 """ 

616 kwargs['calibType'] = self.calibType 

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

618 

619 def fromDataIds(self, dataIdList): 

620 """Update provenance from dataId List. 

621 

622 Parameters 

623 ---------- 

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

625 List of dataIds used in generating this calibration. 

626 """ 

627 for dataId in dataIdList: 

628 for key in dataId: 

629 if key not in self.dimensions: 

630 self.dimensions.add(key) 

631 self.dataIdList.append(dataId) 

632 

633 @classmethod 

634 def fromTable(cls, tableList): 

635 """Construct provenance from table list. 

636 

637 Parameters 

638 ---------- 

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

640 List of tables to construct the provenance from. 

641 

642 Returns 

643 ------- 

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

645 The provenance defined in the tables. 

646 """ 

647 table = tableList[0] 

648 metadata = table.meta 

649 inDict = dict() 

650 inDict['metadata'] = metadata 

651 inDict['calibType'] = metadata['calibType'] 

652 inDict['dimensions'] = set() 

653 inDict['dataIdList'] = list() 

654 

655 schema = dict() 

656 for colName in table.columns: 

657 schema[colName.lower()] = colName 

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

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

660 

661 for row in table: 

662 entry = dict() 

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

664 entry[dim] = row[schema[dim]] 

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

666 

667 return cls.fromDict(inDict) 

668 

669 @classmethod 

670 def fromDict(cls, dictionary): 

671 """Construct provenance from a dictionary. 

672 

673 Parameters 

674 ---------- 

675 dictionary : `dict` 

676 Dictionary of provenance parameters. 

677 

678 Returns 

679 ------- 

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

681 The provenance defined in the tables. 

682 """ 

683 calib = cls() 

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

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

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

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

688 

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

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

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

692 calib.calibType = dictionary['calibType'] 

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

694 calib.dataIdList = dictionary['dataIdList'] 

695 

696 calib.updateMetadata() 

697 return calib 

698 

699 def toDict(self): 

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

701 

702 Returns 

703 ------- 

704 dictionary : `dict` 

705 Dictionary of provenance. 

706 """ 

707 self.updateMetadata() 

708 

709 outDict = {} 

710 

711 metadata = self.getMetadata() 

712 outDict['metadata'] = metadata 

713 outDict['detectorName'] = self._detectorName 

714 outDict['detectorSerial'] = self._detectorSerial 

715 outDict['detectorId'] = self._detectorId 

716 outDict['instrument'] = self._instrument 

717 outDict['calibType'] = self.calibType 

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

719 outDict['dataIdList'] = self.dataIdList 

720 

721 return outDict 

722 

723 def toTable(self): 

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

725 

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

727 way to store the data. 

728 

729 Returns 

730 ------- 

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

732 List of tables containing the provenance information 

733 

734 """ 

735 tableList = [] 

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

737 

738 catalog = Table(rows=self.dataIdList, 

739 names=self.dimensions) 

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

741 catalog.meta = filteredMetadata 

742 tableList.append(catalog) 

743 return tableList