Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+05816baf74,g1ec0fe41b4+f536777771,g1fd858c14a+a9301854fb,g35bb328faa+fcb1d3bbc8,g4af146b050+a5c07d5b1d,g4d2262a081+6e5fcc2a4e,g53246c7159+fcb1d3bbc8,g56a49b3a55+9c12191793,g5a012ec0e7+3632fc3ff3,g60b5630c4e+ded28b650d,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8352419a5c+fcb1d3bbc8,g87b7deb4dc+7b42cf88bf,g8852436030+e5453db6e6,g89139ef638+ed4b5058f4,g8e3bb8577d+d38d73bdbd,g9125e01d80+fcb1d3bbc8,g94187f82dc+ded28b650d,g989de1cb63+ed4b5058f4,g9d31334357+ded28b650d,g9f33ca652e+50a8019d8c,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+d9fb1f8026,gb58c049af0+f03b321e39,gb665e3612d+2a0c9e9e84,gb89ab40317+ed4b5058f4,gcf25f946ba+e5453db6e6,gd6cbbdb0b4+bb83cc51f8,gdd1046aedd+ded28b650d,gde0f65d7ad+941d412827,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf23fb2af72+b7cae620c0,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.16
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
calibType.py
Go to the documentation of this file.
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/>.
21__all__ = ["IsrCalib", "IsrProvenance"]
22
23import abc
24import datetime
25import logging
26import os.path
27import warnings
28import yaml
29import numpy as np
30
31from astropy.table import Table
32from astropy.io import fits
33
34from lsst.daf.base import PropertyList
35from lsst.utils.introspection import get_full_type_name
36from lsst.utils import doImport
37
38
39class IsrCalib(abc.ABC):
40 """Generic calibration type.
41
42 Subclasses must implement the toDict, fromDict, toTable, fromTable
43 methods that allow the calibration information to be converted
44 from dictionaries and afw tables. This will allow the calibration
45 to be persisted using the base class read/write methods.
46
47 The validate method is intended to provide a common way to check
48 that the calibration is valid (internally consistent) and
49 appropriate (usable with the intended data). The apply method is
50 intended to allow the calibration to be applied in a consistent
51 manner.
52
53 Parameters
54 ----------
55 camera : `lsst.afw.cameraGeom.Camera`, optional
56 Camera to extract metadata from.
57 detector : `lsst.afw.cameraGeom.Detector`, optional
58 Detector to extract metadata from.
59 log : `logging.Logger`, optional
60 Log for messages.
61 """
62 _OBSTYPE = "generic"
63 _SCHEMA = "NO SCHEMA"
64 _VERSION = 0
65
66 def __init__(self, camera=None, detector=None, log=None, **kwargs):
67 self._instrument = None
68 self._raftName = None
69 self._slotName = None
70 self._detectorName = None
71 self._detectorSerial = None
72 self._detectorId = None
73 self._filter = None
74 self._calibId = None
75 self._seqfile = None
76 self._seqname = None
77 self._seqcksum = None
80 self.calibInfoFromDict(kwargs)
81
82 # Define the required attributes for this calibration. These
83 # are entries that are automatically filled and propagated
84 # from the metadata. The existence of the attribute is
85 # required (they're checked for equivalence), but they do not
86 # necessarily need to have a value (None == None in this
87 # case).
88 self.requiredAttributes = set(["_OBSTYPE", "_SCHEMA", "_VERSION"])
89 self.requiredAttributes.update(["_instrument", "_raftName", "_slotName",
90 "_detectorName", "_detectorSerial", "_detectorId",
91 "_filter", "_calibId", "_seqfile", "_seqname", "_seqcksum",
92 "_metadata"])
93
94 self.log = log if log else logging.getLogger(__name__)
95
96 if detector:
97 self.fromDetector(detector)
98 self.updateMetadata(camera=camera, detector=detector)
99
100 def __str__(self):
101 return f"{self.__class__.__name__}(obstype={self._OBSTYPE}, detector={self._detectorName}, )"
102
103 def __eq__(self, other):
104 """Calibration equivalence.
105
106 Running ``calib.log.setLevel(0)`` enables debug statements to
107 identify problematic fields.
108 """
109 if not isinstance(other, self.__class__):
110 self.log.debug("Incorrect class type: %s %s", self.__class__, other.__class__)
111 return False
112
113 for attr in self._requiredAttributes:
114 attrSelf = getattr(self, attr)
115 attrOther = getattr(other, attr)
116
117 if isinstance(attrSelf, dict):
118 # Dictionary of arrays.
119 if attrSelf.keys() != attrOther.keys():
120 self.log.debug("Dict Key Failure: %s %s %s", attr, attrSelf.keys(), attrOther.keys())
121 return False
122 for key in attrSelf:
123 try:
124 if not np.allclose(attrSelf[key], attrOther[key], equal_nan=True):
125 self.log.debug("Array Failure: %s %s %s", key, attrSelf[key], attrOther[key])
126 return False
127 except TypeError:
128 # If it is not something numpy can handle
129 # (it's not a number or array of numbers),
130 # then it needs to have its own equivalence
131 # operator.
132 if np.all(attrSelf[key] != attrOther[key]):
133 return False
134 elif isinstance(attrSelf, np.ndarray):
135 # Bare array.
136 if isinstance(attrSelf[0], (str, np.str_, np.bytes_)):
137 if not np.all(attrSelf == attrOther):
138 self.log.debug("Array Failure: %s %s %s", attr, attrSelf, attrOther)
139 return False
140 else:
141 if not np.allclose(attrSelf, attrOther, equal_nan=True):
142 self.log.debug("Array Failure: %s %s %s", attr, attrSelf, attrOther)
143 return False
144 elif type(attrSelf) is not type(attrOther):
145 if set([attrSelf, attrOther]) == set([None, ""]):
146 # Fits converts None to "", but None is not "".
147 continue
148 self.log.debug("Type Failure: %s %s %s %s %s", attr, type(attrSelf), type(attrOther),
149 attrSelf, attrOther)
150 return False
151 else:
152 if attrSelf != attrOther:
153 self.log.debug("Value Failure: %s %s %s", attr, attrSelf, attrOther)
154 return False
155
156 return True
157
158 @property
160 return self._requiredAttributes
161
162 @requiredAttributes.setter
163 def requiredAttributes(self, value):
164 self._requiredAttributes = value
165
166 # Property accessor associated with getMetadata().
167 @property
168 def metadata(self):
169 return self._metadata
170
171 def getMetadata(self):
172 """Retrieve metadata associated with this calibration.
173
174 Returns
175 -------
176 meta : `lsst.daf.base.PropertyList`
177 Metadata. The returned `~lsst.daf.base.PropertyList` can be
178 modified by the caller and the changes will be written to
179 external files.
180 """
181 return self._metadata
182
183 def setMetadata(self, metadata):
184 """Store a copy of the supplied metadata with this calibration.
185
186 Parameters
187 ----------
188 metadata : `lsst.daf.base.PropertyList`
189 Metadata to associate with the calibration. Will be copied and
190 overwrite existing metadata.
191 """
192 if metadata is not None:
193 self._metadata.update(metadata)
194
195 # Ensure that we have the obs type required by calibration ingest
196 self._metadata["OBSTYPE"] = self._OBSTYPE
197 self._metadata[self._OBSTYPE + "_SCHEMA"] = self._SCHEMA
198 self._metadata[self._OBSTYPE + "_VERSION"] = self._VERSION
199
200 if isinstance(metadata, dict):
201 self.calibInfoFromDict(metadata)
202 elif isinstance(metadata, PropertyList):
203 self.calibInfoFromDict(metadata.toDict())
204
205 def updateMetadata(self, camera=None, detector=None, filterName=None,
206 setCalibId=False, setCalibInfo=False, setDate=False,
207 **kwargs):
208 """Update metadata keywords with new values.
209
210 Parameters
211 ----------
212 camera : `lsst.afw.cameraGeom.Camera`, optional
213 Reference camera to use to set ``_instrument`` field.
214 detector : `lsst.afw.cameraGeom.Detector`, optional
215 Reference detector to use to set ``_detector*`` fields.
216 filterName : `str`, optional
217 Filter name to assign to this calibration.
218 setCalibId : `bool`, optional
219 Construct the ``_calibId`` field from other fields.
220 setCalibInfo : `bool`, optional
221 Set calibration parameters from metadata.
222 setDate : `bool`, optional
223 Ensure the metadata ``CALIBDATE`` fields are set to the current
224 datetime.
225 kwargs : `dict` or `collections.abc.Mapping`, optional
226 Set of ``key=value`` pairs to assign to the metadata.
227 """
228 mdOriginal = self.getMetadata()
229 mdSupplemental = dict()
230
231 for k, v in kwargs.items():
232 if isinstance(v, fits.card.Undefined):
233 kwargs[k] = None
234
235 if setCalibInfo:
236 self.calibInfoFromDict(kwargs)
237
238 if camera:
239 self._instrument = camera.getName()
240
241 if detector:
242 self._detectorName = detector.getName()
243 self._detectorSerial = detector.getSerial()
244 self._detectorId = detector.getId()
245 if "_" in self._detectorName:
246 (self._raftName, self._slotName) = self._detectorName.split("_")
247
248 if filterName:
249 # TOD0 DM-28093: I think this whole comment can go away, if we
250 # always use physicalLabel everywhere in ip_isr.
251 # If set via:
252 # exposure.getInfo().getFilter().getName()
253 # then this will hold the abstract filter.
254 self._filter = filterName
255
256 if setDate:
257 date = datetime.datetime.now()
258 mdSupplemental["CALIBDATE"] = date.isoformat()
259 mdSupplemental["CALIB_CREATION_DATE"] = date.date().isoformat()
260 mdSupplemental["CALIB_CREATION_TIME"] = date.time().isoformat()
261
262 if setCalibId:
263 values = []
264 values.append(f"instrument={self._instrument}") if self._instrument else None
265 values.append(f"raftName={self._raftName}") if self._raftName else None
266 values.append(f"detectorName={self._detectorName}") if self._detectorName else None
267 values.append(f"detector={self._detectorId}") if self._detectorId else None
268 values.append(f"filter={self._filter}") if self._filter else None
269
270 calibDate = mdOriginal.get("CALIBDATE", mdSupplemental.get("CALIBDATE", None))
271 values.append(f"calibDate={calibDate}") if calibDate else None
272
273 self._calibId = " ".join(values)
274
275 self._metadata["INSTRUME"] = self._instrument if self._instrument else None
276 self._metadata["RAFTNAME"] = self._raftName if self._raftName else None
277 self._metadata["SLOTNAME"] = self._slotName if self._slotName else None
278 self._metadata["DETECTOR"] = self._detectorId
279 self._metadata["DET_NAME"] = self._detectorName if self._detectorName else None
280 self._metadata["DET_SER"] = self._detectorSerial if self._detectorSerial else None
281 self._metadata["FILTER"] = self._filter if self._filter else None
282 self._metadata["CALIB_ID"] = self._calibId if self._calibId else None
283 self._metadata["SEQFILE"] = self._seqfile if self._seqfile else None
284 self._metadata["SEQNAME"] = self._seqname if self._seqname else None
285 self._metadata["SEQCKSUM"] = self._seqcksum if self._seqcksum else None
286 self._metadata["CALIBCLS"] = get_full_type_name(self)
287
288 mdSupplemental.update(kwargs)
289 mdOriginal.update(mdSupplemental)
290
291 def updateMetadataFromExposures(self, exposures):
292 """Extract and unify metadata information.
293
294 Parameters
295 ----------
296 exposures : `list`
297 Exposures or other calibrations to scan.
298 """
299 # This list of keywords is the set of header entries that
300 # should be checked and propagated. Not having an entry is
301 # not a failure, as they may not be defined for the exposures
302 # being used.
303 keywords = ["SEQNAME", "SEQFILE", "SEQCKSUM", "ODP", "AP0_RC"]
304 metadata = {}
305
306 for exp in exposures:
307 try:
308 expMeta = exp.getMetadata()
309 except AttributeError:
310 continue
311 for key in keywords:
312 if key in expMeta:
313 if key in metadata:
314 if metadata[key] != expMeta[key]:
315 self.log.warning("Metadata mismatch! Have: %s Found %s",
316 metadata[key], expMeta[key])
317 else:
318 metadata[key] = expMeta[key]
319
320 self.updateMetadata(**metadata, setCalibInfo=True)
321
322 def calibInfoFromDict(self, dictionary):
323 """Handle common keywords.
324
325 This isn't an ideal solution, but until all calibrations
326 expect to find everything in the metadata, they still need to
327 search through dictionaries.
328
329 Parameters
330 ----------
331 dictionary : `dict` or `lsst.daf.base.PropertyList`
332 Source for the common keywords.
333
334 Raises
335 ------
336 RuntimeError
337 Raised if the dictionary does not match the expected OBSTYPE.
338 """
339
340 def search(haystack, needles):
341 """Search dictionary 'haystack' for an entry in 'needles'
342 """
343 test = [haystack.get(x) for x in needles]
344 test = set([x for x in test if x is not None])
345 if len(test) == 0:
346 if "metadata" in haystack:
347 return search(haystack["metadata"], needles)
348 else:
349 return None
350 elif len(test) == 1:
351 value = list(test)[0]
352 if value == "":
353 return None
354 else:
355 return value
356 else:
357 raise ValueError(f"Too many values found: {len(test)} {test} {needles}")
358
359 if "metadata" in dictionary:
360 metadata = dictionary["metadata"]
361
362 if self._OBSTYPE != metadata["OBSTYPE"]:
363 raise RuntimeError(f"Incorrect calibration supplied. Expected {self._OBSTYPE}, "
364 f"found {metadata['OBSTYPE']}")
365
366 if (value := search(dictionary, ["INSTRUME", "instrument"])) is not None:
367 self._instrument = value
368 if (value := search(dictionary, ["RAFTNAME"])) is not None:
369 self._slotName = value
370 if (value := search(dictionary, ["DETECTOR", "detectorId"])) is not None:
371 self._detectorId = value
372 if (value := search(dictionary, ["DET_NAME", "DETECTOR_NAME", "detectorName"])) is not None:
373 self._detectorName = value
374 if (value := search(dictionary, ["DET_SER", "DETECTOR_SERIAL", "detectorSerial"])) is not None:
375 self._detectorSerial = value
376 if (value := search(dictionary, ["FILTER", "filterName"])) is not None:
377 self._filter = value
378 if (value := search(dictionary, ["CALIB_ID"])) is not None:
379 self._calibId = value
380 if (value := search(dictionary, ["SEQFILE"])) is not None:
381 self._seqfile = value
382 if (value := search(dictionary, ["SEQNAME"])) is not None:
383 self._seqname = value
384 if (value := search(dictionary, ["SEQCKSUM"])) is not None:
385 self._seqcksum = value
386
387 @classmethod
388 def determineCalibClass(cls, metadata, message):
389 """Attempt to find calibration class in metadata.
390
391 Parameters
392 ----------
393 metadata : `dict` or `lsst.daf.base.PropertyList`
394 Metadata possibly containing a calibration class entry.
395 message : `str`
396 Message to include in any errors.
397
398 Returns
399 -------
400 calibClass : `object`
401 The class to use to read the file contents. Should be an
402 `lsst.ip.isr.IsrCalib` subclass.
403
404 Raises
405 ------
406 ValueError
407 Raised if the resulting calibClass is the base
408 `lsst.ip.isr.IsrClass` (which does not implement the
409 content methods).
410 """
411 calibClassName = metadata.get("CALIBCLS")
412 calibClass = doImport(calibClassName) if calibClassName is not None else cls
413 if calibClass is IsrCalib:
414 raise ValueError(f"Cannot use base class to read calibration data: {message}")
415 return calibClass
416
417 @classmethod
418 def readText(cls, filename, **kwargs):
419 """Read calibration representation from a yaml/ecsv file.
420
421 Parameters
422 ----------
423 filename : `str`
424 Name of the file containing the calibration definition.
425 kwargs : `dict` or collections.abc.Mapping`, optional
426 Set of key=value pairs to pass to the ``fromDict`` or
427 ``fromTable`` methods.
428
429 Returns
430 -------
431 calib : `~lsst.ip.isr.IsrCalibType`
432 Calibration class.
433
434 Raises
435 ------
436 RuntimeError
437 Raised if the filename does not end in ".ecsv" or ".yaml".
438 """
439 if filename.endswith((".ecsv", ".ECSV")):
440 data = Table.read(filename, format="ascii.ecsv")
441 calibClass = cls.determineCalibClass(data.meta, "readText/ECSV")
442 return calibClass.fromTable([data], **kwargs)
443 elif filename.endswith((".yaml", ".YAML")):
444 with open(filename, "r") as f:
445 data = yaml.load(f, Loader=yaml.CLoader)
446 calibClass = cls.determineCalibClass(data["metadata"], "readText/YAML")
447 return calibClass.fromDict(data, **kwargs)
448 else:
449 raise RuntimeError(f"Unknown filename extension: {filename}")
450
451 def writeText(self, filename, format="auto"):
452 """Write the calibration data to a text file.
453
454 Parameters
455 ----------
456 filename : `str`
457 Name of the file to write.
458 format : `str`
459 Format to write the file as. Supported values are:
460 ``"auto"`` : Determine filetype from filename.
461 ``"yaml"`` : Write as yaml.
462 ``"ecsv"`` : Write as ecsv.
463
464 Returns
465 -------
466 used : `str`
467 The name of the file used to write the data. This may
468 differ from the input if the format is explicitly chosen.
469
470 Raises
471 ------
472 RuntimeError
473 Raised if filename does not end in a known extension, or
474 if all information cannot be written.
475
476 Notes
477 -----
478 The file is written to YAML/ECSV format and will include any
479 associated metadata.
480 """
481 if format == "yaml" or (format == "auto" and filename.lower().endswith((".yaml", ".YAML"))):
482 outDict = self.toDict()
483 path, ext = os.path.splitext(filename)
484 filename = path + ".yaml"
485 with open(filename, "w") as f:
486 yaml.dump(outDict, f)
487 elif format == "ecsv" or (format == "auto" and filename.lower().endswith((".ecsv", ".ECSV"))):
488 tableList = self.toTable()
489 if len(tableList) > 1:
490 # ECSV doesn't support multiple tables per file, so we
491 # can only write the first table.
492 raise RuntimeError(f"Unable to persist {len(tableList)}tables in ECSV format.")
493
494 table = tableList[0]
495 path, ext = os.path.splitext(filename)
496 filename = path + ".ecsv"
497 table.write(filename, format="ascii.ecsv")
498 else:
499 raise RuntimeError(f"Attempt to write to a file {filename} "
500 "that does not end in '.yaml' or '.ecsv'")
501
502 return filename
503
504 @classmethod
505 def readFits(cls, filename, **kwargs):
506 """Read calibration data from a FITS file.
507
508 Parameters
509 ----------
510 filename : `str`
511 Filename to read data from.
512 kwargs : `dict` or collections.abc.Mapping`, optional
513 Set of key=value pairs to pass to the ``fromTable``
514 method.
515
516 Returns
517 -------
518 calib : `lsst.ip.isr.IsrCalib`
519 Calibration contained within the file.
520 """
521 tableList = []
522 tableList.append(Table.read(filename, hdu=1, mask_invalid=False))
523 extNum = 2 # Fits indices start at 1, we've read one already.
524 keepTrying = True
525
526 while keepTrying:
527 with warnings.catch_warnings():
528 warnings.simplefilter("error")
529 try:
530 newTable = Table.read(filename, hdu=extNum, mask_invalid=False)
531 tableList.append(newTable)
532 extNum += 1
533 except Exception:
534 keepTrying = False
535
536 for table in tableList:
537 for k, v in table.meta.items():
538 if isinstance(v, fits.card.Undefined):
539 table.meta[k] = None
540
541 calibClass = cls.determineCalibClass(tableList[0].meta, "readFits")
542 return calibClass.fromTable(tableList, **kwargs)
543
544 def writeFits(self, filename):
545 """Write calibration data to a FITS file.
546
547 Parameters
548 ----------
549 filename : `str`
550 Filename to write data to.
551
552 Returns
553 -------
554 used : `str`
555 The name of the file used to write the data.
556 """
557 tableList = self.toTable()
558 with warnings.catch_warnings():
559 warnings.filterwarnings("ignore", category=Warning, module="astropy.io")
560 astropyList = [fits.table_to_hdu(table) for table in tableList]
561 astropyList.insert(0, fits.PrimaryHDU())
562
563 writer = fits.HDUList(astropyList)
564 writer.writeto(filename, overwrite=True)
565 return filename
566
567 def fromDetector(self, detector):
568 """Modify the calibration parameters to match the supplied detector.
569
570 Parameters
571 ----------
572 detector : `lsst.afw.cameraGeom.Detector`
573 Detector to use to set parameters from.
574
575 Raises
576 ------
577 NotImplementedError
578 Raised if not implemented by a subclass.
579 This needs to be implemented by subclasses for each
580 calibration type.
581 """
582 raise NotImplementedError("Must be implemented by subclass.")
583
584 @classmethod
585 def fromDict(cls, dictionary, **kwargs):
586 """Construct a calibration from a dictionary of properties.
587
588 Must be implemented by the specific calibration subclasses.
589
590 Parameters
591 ----------
592 dictionary : `dict`
593 Dictionary of properties.
594 kwargs : `dict` or collections.abc.Mapping`, optional
595 Set of key=value options.
596
597 Returns
598 -------
599 calib : `lsst.ip.isr.CalibType`
600 Constructed calibration.
601
602 Raises
603 ------
604 NotImplementedError
605 Raised if not implemented.
606 """
607 raise NotImplementedError("Must be implemented by subclass.")
608
609 def toDict(self):
610 """Return a dictionary containing the calibration properties.
611
612 The dictionary should be able to be round-tripped through
613 `fromDict`.
614
615 Returns
616 -------
617 dictionary : `dict`
618 Dictionary of properties.
619
620 Raises
621 ------
622 NotImplementedError
623 Raised if not implemented.
624 """
625 raise NotImplementedError("Must be implemented by subclass.")
626
627 @classmethod
628 def fromTable(cls, tableList, **kwargs):
629 """Construct a calibration from a dictionary of properties.
630
631 Must be implemented by the specific calibration subclasses.
632
633 Parameters
634 ----------
635 tableList : `list` [`lsst.afw.table.Table`]
636 List of tables of properties.
637 kwargs : `dict` or collections.abc.Mapping`, optional
638 Set of key=value options.
639
640 Returns
641 -------
642 calib : `lsst.ip.isr.CalibType`
643 Constructed calibration.
644
645 Raises
646 ------
647 NotImplementedError
648 Raised if not implemented.
649 """
650 raise NotImplementedError("Must be implemented by subclass.")
651
652 def toTable(self):
653 """Return a list of tables containing the calibration properties.
654
655 The table list should be able to be round-tripped through
656 `fromDict`.
657
658 Returns
659 -------
660 tableList : `list` [`lsst.afw.table.Table`]
661 List of tables of properties.
662
663 Raises
664 ------
665 NotImplementedError
666 Raised if not implemented.
667 """
668 raise NotImplementedError("Must be implemented by subclass.")
669
670 def validate(self, other=None):
671 """Validate that this calibration is defined and can be used.
672
673 Parameters
674 ----------
675 other : `object`, optional
676 Thing to validate against.
677
678 Returns
679 -------
680 valid : `bool`
681 Returns true if the calibration is valid and appropriate.
682 """
683 return False
684
685 def apply(self, target):
686 """Method to apply the calibration to the target object.
687
688 Parameters
689 ----------
690 target : `object`
691 Thing to validate against.
692
693 Returns
694 -------
695 valid : `bool`
696 Returns true if the calibration was applied correctly.
697
698 Raises
699 ------
700 NotImplementedError
701 Raised if not implemented.
702 """
703 raise NotImplementedError("Must be implemented by subclass.")
704
705
707 """Class for the provenance of data used to construct calibration.
708
709 Provenance is not really a calibration, but we would like to
710 record this when constructing the calibration, and it provides an
711 example of the base calibration class.
712
713 Parameters
714 ----------
715 instrument : `str`, optional
716 Name of the instrument the data was taken with.
717 calibType : `str`, optional
718 Type of calibration this provenance was generated for.
719 detectorName : `str`, optional
720 Name of the detector this calibration is for.
721 detectorSerial : `str`, optional
722 Identifier for the detector.
723
724 """
725 _OBSTYPE = "IsrProvenance"
726
727 def __init__(self, calibType="unknown",
728 **kwargs):
729 self.calibType = calibType
730 self.dimensions = set()
731 self.dataIdList = list()
732
733 super().__init__(**kwargs)
734
735 self.requiredAttributes.update(["calibType", "dimensions", "dataIdList"])
736
737 def __str__(self):
738 return f"{self.__class__.__name__}(obstype={self._OBSTYPE}, calibType={self.calibType}, )"
739
740 def __eq__(self, other):
741 return super().__eq__(other)
742
743 def updateMetadata(self, setDate=False, **kwargs):
744 """Update calibration metadata.
745
746 Parameters
747 ----------
748 setDate : `bool`, optional
749 Update the ``CALIBDATE`` fields in the metadata to the current
750 time. Defaults to False.
751 kwargs : `dict` or `collections.abc.Mapping`, optional
752 Other keyword parameters to set in the metadata.
753 """
754 kwargs["calibType"] = self.calibType
755 super().updateMetadata(setDate=setDate, **kwargs)
756
757 def fromDataIds(self, dataIdList):
758 """Update provenance from dataId List.
759
760 Parameters
761 ----------
762 dataIdList : `list` [`lsst.daf.butler.DataId`]
763 List of dataIds used in generating this calibration.
764 """
765 for dataId in dataIdList:
766 for key in dataId:
767 if key not in self.dimensions:
768 self.dimensions.add(key)
769 self.dataIdList.append(dataId)
770
771 @classmethod
772 def fromTable(cls, tableList):
773 """Construct provenance from table list.
774
775 Parameters
776 ----------
777 tableList : `list` [`lsst.afw.table.Table`]
778 List of tables to construct the provenance from.
779
780 Returns
781 -------
782 provenance : `lsst.ip.isr.IsrProvenance`
783 The provenance defined in the tables.
784 """
785 table = tableList[0]
786 metadata = table.meta
787 inDict = dict()
788 inDict["metadata"] = metadata
789 inDict["calibType"] = metadata["calibType"]
790 inDict["dimensions"] = set()
791 inDict["dataIdList"] = list()
792
793 schema = dict()
794 for colName in table.columns:
795 schema[colName.lower()] = colName
796 inDict["dimensions"].add(colName.lower())
797 inDict["dimensions"] = sorted(inDict["dimensions"])
798
799 for row in table:
800 entry = dict()
801 for dim in sorted(inDict["dimensions"]):
802 entry[dim] = row[schema[dim]]
803 inDict["dataIdList"].append(entry)
804
805 return cls.fromDict(inDict)
806
807 @classmethod
808 def fromDict(cls, dictionary):
809 """Construct provenance from a dictionary.
810
811 Parameters
812 ----------
813 dictionary : `dict`
814 Dictionary of provenance parameters.
815
816 Returns
817 -------
818 provenance : `lsst.ip.isr.IsrProvenance`
819 The provenance defined in the tables.
820 """
821 calib = cls()
822 if calib._OBSTYPE != dictionary["metadata"]["OBSTYPE"]:
823 raise RuntimeError(f"Incorrect calibration supplied. Expected {calib._OBSTYPE}, "
824 f"found {dictionary['metadata']['OBSTYPE']}")
825 calib.updateMetadata(setDate=False, setCalibInfo=True, **dictionary["metadata"])
826
827 # These properties should be in the metadata, but occasionally
828 # are found in the dictionary itself. Check both places,
829 # ending with `None` if neither contains the information.
830 calib.calibType = dictionary["calibType"]
831 calib.dimensions = set(dictionary["dimensions"])
832 calib.dataIdList = dictionary["dataIdList"]
833
834 calib.updateMetadata()
835 return calib
836
837 def toDict(self):
838 """Return a dictionary containing the provenance information.
839
840 Returns
841 -------
842 dictionary : `dict`
843 Dictionary of provenance.
844 """
845 self.updateMetadata()
846
847 outDict = {}
848
849 metadata = self.getMetadata()
850 outDict["metadata"] = metadata
851 outDict["detectorName"] = self._detectorName
852 outDict["detectorSerial"] = self._detectorSerial
853 outDict["detectorId"] = self._detectorId
854 outDict["instrument"] = self._instrument
855 outDict["calibType"] = self.calibType
856 outDict["dimensions"] = list(self.dimensions)
857 outDict["dataIdList"] = self.dataIdList
858
859 return outDict
860
861 def toTable(self):
862 """Return a list of tables containing the provenance.
863
864 This seems inefficient and slow, so this may not be the best
865 way to store the data.
866
867 Returns
868 -------
869 tableList : `list` [`lsst.afw.table.Table`]
870 List of tables containing the provenance information
871 """
872 tableList = []
873 self.updateMetadata(setDate=True, setCalibInfo=True)
874
875 catalog = Table(rows=self.dataIdList,
876 names=self.dimensions)
877 filteredMetadata = {k: v for k, v in self.getMetadata().toDict().items() if v is not None}
878 catalog.meta = filteredMetadata
879 tableList.append(catalog)
880 return tableList
Class for storing ordered metadata with comments.
fromDict(cls, dictionary, **kwargs)
Definition calibType.py:585
writeText(self, filename, format="auto")
Definition calibType.py:451
readText(cls, filename, **kwargs)
Definition calibType.py:418
calibInfoFromDict(self, dictionary)
Definition calibType.py:322
updateMetadataFromExposures(self, exposures)
Definition calibType.py:291
determineCalibClass(cls, metadata, message)
Definition calibType.py:388
__init__(self, camera=None, detector=None, log=None, **kwargs)
Definition calibType.py:66
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:207
fromTable(cls, tableList, **kwargs)
Definition calibType.py:628
readFits(cls, filename, **kwargs)
Definition calibType.py:505
__init__(self, calibType="unknown", **kwargs)
Definition calibType.py:728
updateMetadata(self, setDate=False, **kwargs)
Definition calibType.py:743