23 """Make a look-up-table (LUT) for FGCM calibration.
25 This task computes a look-up-table for the range in expected atmosphere
26 variation and variation in instrumental throughput (as tracked by the
27 transmission_filter products). By pre-computing linearized integrals,
28 the FGCM fit is orders of magnitude faster for stars with a broad range
29 of colors and observing bands, yielding precision at the 1-2 mmag level.
31 Computing a LUT requires running MODTRAN or with a pre-generated
32 atmosphere table packaged with fgcm.
45 from .utilities
import lookupStaticCalibrations
49 __all__ = [
'FgcmMakeLutParametersConfig',
'FgcmMakeLutConfig',
'FgcmMakeLutTask',
54 dimensions=(
'instrument',),
56 camera = connectionTypes.PrerequisiteInput(
57 doc=
"Camera instrument",
59 storageClass=
"Camera",
60 dimensions=(
"instrument",),
61 lookupFunction=lookupStaticCalibrations,
65 transmission_optics = connectionTypes.PrerequisiteInput(
66 doc=
"Optics transmission curve information",
67 name=
"transmission_optics",
68 storageClass=
"TransmissionCurve",
69 dimensions=(
"instrument",),
70 lookupFunction=lookupStaticCalibrations,
75 transmission_sensor = connectionTypes.PrerequisiteInput(
76 doc=
"Sensor transmission curve information",
77 name=
"transmission_sensor",
78 storageClass=
"TransmissionCurve",
79 dimensions=(
"instrument",
"detector",),
80 lookupFunction=lookupStaticCalibrations,
86 transmission_filter = connectionTypes.PrerequisiteInput(
87 doc=
"Filter transmission curve information",
88 name=
"transmission_filter",
89 storageClass=
"TransmissionCurve",
90 dimensions=(
"band",
"instrument",
"physical_filter",),
91 lookupFunction=lookupStaticCalibrations,
97 fgcmLookUpTable = connectionTypes.Output(
98 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
99 "chromatic corrections."),
100 name=
"fgcmLookUpTable",
101 storageClass=
"Catalog",
102 dimensions=(
"instrument",),
107 """Config for parameters if atmosphereTableName not available"""
110 elevation = pexConfig.Field(
111 doc=
"Telescope elevation (m)",
115 pmbRange = pexConfig.ListField(
116 doc=(
"Barometric Pressure range (millibar) "
117 "Recommended range depends on the site."),
121 pmbSteps = pexConfig.Field(
122 doc=
"Barometric Pressure number of steps",
126 pwvRange = pexConfig.ListField(
127 doc=(
"Precipitable Water Vapor range (mm) "
128 "Recommended range depends on the site."),
132 pwvSteps = pexConfig.Field(
133 doc=
"Precipitable Water Vapor number of steps",
137 o3Range = pexConfig.ListField(
138 doc=
"Ozone range (dob)",
140 default=[220.0, 310.0],
142 o3Steps = pexConfig.Field(
143 doc=
"Ozone number of steps",
147 tauRange = pexConfig.ListField(
148 doc=
"Aerosol Optical Depth range (unitless)",
150 default=[0.002, 0.35],
152 tauSteps = pexConfig.Field(
153 doc=
"Aerosol Optical Depth number of steps",
157 alphaRange = pexConfig.ListField(
158 doc=
"Aerosol alpha range (unitless)",
162 alphaSteps = pexConfig.Field(
163 doc=
"Aerosol alpha number of steps",
167 zenithRange = pexConfig.ListField(
168 doc=
"Zenith angle range (degree)",
172 zenithSteps = pexConfig.Field(
173 doc=
"Zenith angle number of steps",
179 pmbStd = pexConfig.Field(
180 doc=(
"Standard Atmosphere pressure (millibar); "
181 "Recommended default depends on the site."),
185 pwvStd = pexConfig.Field(
186 doc=(
"Standard Atmosphere PWV (mm); "
187 "Recommended default depends on the site."),
191 o3Std = pexConfig.Field(
192 doc=
"Standard Atmosphere O3 (dob)",
196 tauStd = pexConfig.Field(
197 doc=
"Standard Atmosphere aerosol optical depth",
201 alphaStd = pexConfig.Field(
202 doc=
"Standard Atmosphere aerosol alpha",
206 airmassStd = pexConfig.Field(
207 doc=(
"Standard Atmosphere airmass; "
208 "Recommended default depends on the survey strategy."),
212 lambdaNorm = pexConfig.Field(
213 doc=
"Aerosol Optical Depth normalization wavelength (Angstrom)",
217 lambdaStep = pexConfig.Field(
218 doc=
"Wavelength step for generating atmospheres (nm)",
222 lambdaRange = pexConfig.ListField(
223 doc=
"Wavelength range for LUT (Angstrom)",
225 default=[3000.0, 11000.0],
230 pipelineConnections=FgcmMakeLutConnections):
231 """Config for FgcmMakeLutTask"""
232 physicalFilters = pexConfig.ListField(
233 doc=
"List of physicalFilter labels to generate look-up table.",
237 stdPhysicalFilterOverrideMap = pexConfig.DictField(
238 doc=(
"Override mapping from physical filter labels to 'standard' physical "
239 "filter labels. The 'standard' physical filter defines the transmission "
240 "curve that the FGCM standard bandpass will be based on. "
241 "Any filter not listed here will be mapped to "
242 "itself (e.g. g->g or HSC-G->HSC-G). Use this override for cross-"
243 "filter calibration such as HSC-R->HSC-R2 and HSC-I->HSC-I2."),
248 atmosphereTableName = pexConfig.Field(
249 doc=
"FGCM name or filename of precomputed atmospheres",
254 parameters = pexConfig.ConfigField(
255 doc=
"Atmosphere parameters (required if no atmosphereTableName)",
256 dtype=FgcmMakeLutParametersConfig,
262 Validate the config parameters.
264 This method behaves differently from the parent validate in the case
265 that atmosphereTableName is set. In this case, the config values
266 for standard values, step sizes, and ranges are loaded
267 directly from the specified atmosphereTableName.
270 self._fields[
'physicalFilters'].
validate(self)
271 self._fields[
'stdPhysicalFilterOverrideMap'].
validate(self)
275 self._fields[
'parameters'].
validate(self)
279 """Subclass of TaskRunner for fgcmMakeLutTask
281 fgcmMakeLutTask.run() takes one argument, the butler, and
282 does not run on any data in the repository.
283 This runner does not use any parallelization.
289 Return a list with one element, the butler.
291 return [parsedCmd.butler]
297 butler: `lsst.daf.persistence.Butler`
301 exitStatus: `list` with `pipeBase.Struct`
302 exitStatus (0: success; 1: failure)
304 task = self.TaskClass(config=self.config, log=self.log)
308 task.runDataRef(butler)
311 task.runDataRef(butler)
312 except Exception
as e:
314 task.log.fatal(
"Failed: %s" % e)
315 if not isinstance(e, pipeBase.TaskError):
316 traceback.print_exc(file=sys.stderr)
318 task.writeMetadata(butler)
321 return [pipeBase.Struct(exitStatus=exitStatus)]
325 Run the task, with no multiprocessing
329 parsedCmd: ArgumentParser parsed command line
334 if self.precall(parsedCmd):
337 resultList = self(targetList[0])
344 Make Look-Up Table for FGCM.
346 This task computes a look-up-table for the range in expected atmosphere
347 variation and variation in instrumental throughput (as tracked by the
348 transmission_filter products). By pre-computing linearized integrals,
349 the FGCM fit is orders of magnitude faster for stars with a broad range
350 of colors and observing bands, yielding precision at the 1-2 mmag level.
352 Computing a LUT requires running MODTRAN or with a pre-generated
353 atmosphere table packaged with fgcm.
356 ConfigClass = FgcmMakeLutConfig
357 RunnerClass = FgcmMakeLutRunner
358 _DefaultName =
"fgcmMakeLut"
360 def __init__(self, butler=None, initInputs=None, **kwargs):
364 def _getMetadataName(self):
370 Make a Look-Up Table for FGCM
374 butler: `lsst.daf.persistence.Butler`
378 ValueError : Raised if configured filter name does not match any of the
379 available filter transmission curves.
381 camera = butler.get(
'camera')
382 opticsDataRef = butler.dataRef(
'transmission_optics')
384 sensorDataRefDict = {}
385 for detector
in camera:
386 sensorDataRefDict[detector.getId()] = butler.dataRef(
'transmission_sensor',
387 dataId={
'ccd': detector.getId()})
389 filterDataRefDict = {}
390 for physicalFilter
in self.config.physicalFilters:
394 dataRef = butler.dataRef(
'transmission_filter', filter=physicalFilter)
395 if not dataRef.datasetExists():
396 raise ValueError(f
"Could not find transmission for filter {physicalFilter}.")
397 filterDataRefDict[physicalFilter] = dataRef
403 butler.put(lutCat,
'fgcmLookUpTable')
406 camera = butlerQC.get(inputRefs.camera)
408 opticsDataRef = butlerQC.get(inputRefs.transmission_optics)
410 sensorRefs = butlerQC.get(inputRefs.transmission_sensor)
411 sensorDataRefDict = {sensorRef.dataId.byName()[
'detector']: sensorRef
for
412 sensorRef
in sensorRefs}
414 filterRefs = butlerQC.get(inputRefs.transmission_filter)
415 filterDataRefDict = {filterRef.dataId[
'physical_filter']: filterRef
for
416 filterRef
in filterRefs}
422 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable)
424 def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict,
427 Make a FGCM Look-up Table
431 camera : `lsst.afw.cameraGeom.Camera`
432 Camera from the butler.
433 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
434 `lsst.daf.butler.DeferredDatasetHandle`
435 Reference to optics transmission curve.
436 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
437 `lsst.daf.butler.DeferredDatasetHandle`]
438 Dictionary of references to sensor transmission curves. Key will
440 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
441 `lsst.daf.butler.DeferredDatasetHandle`]
442 Dictionary of references to filter transmission curves. Key will
443 be physical filter label.
447 fgcmLookUpTable : `BaseCatalog`
448 The FGCM look-up table.
452 self.log.
info(
"Found %d ccds for look-up table" % (nCcd))
463 self.log.
info(
"Making the LUT maker object")
471 throughputLambda = np.arange(self.
fgcmLutMakerfgcmLutMaker.lambdaRange[0],
475 self.log.
info(
"Built throughput lambda, %.1f-%.1f, step %.2f" %
476 (throughputLambda[0], throughputLambda[-1],
477 throughputLambda[1] - throughputLambda[0]))
480 for i, physicalFilter
in enumerate(self.config.physicalFilters):
482 tDict[
'LAMBDA'] = throughputLambda
483 for ccdIndex, detector
in enumerate(camera):
484 tDict[ccdIndex] = self.
_getThroughputDetector_getThroughputDetector(detector, physicalFilter, throughputLambda)
485 throughputDict[physicalFilter] = tDict
488 self.
fgcmLutMakerfgcmLutMaker.setThroughputs(throughputDict)
491 self.log.
info(
"Making LUT")
498 physicalFilterString = comma.join(self.config.physicalFilters)
501 atmosphereTableName =
'NoTableWasUsed'
502 if self.config.atmosphereTableName
is not None:
503 atmosphereTableName = self.config.atmosphereTableName
505 lutSchema = self.
_makeLutSchema_makeLutSchema(physicalFilterString, stdPhysicalFilterString,
508 lutCat = self.
_makeLutCat_makeLutCat(lutSchema, physicalFilterString,
509 stdPhysicalFilterString, atmosphereTableName)
512 def _getStdPhysicalFilterList(self):
513 """Get the standard physical filter lists from config.physicalFilters
514 and config.stdPhysicalFilterOverrideMap
518 stdPhysicalFilters : `list`
520 override = self.config.stdPhysicalFilterOverrideMap
521 return [override.get(physicalFilter, physicalFilter)
for
522 physicalFilter
in self.config.physicalFilters]
524 def _createLutConfig(self, nCcd):
526 Create the fgcmLut config dictionary
531 Number of CCDs in the camera
536 lutConfig[
'logger'] = self.log
537 lutConfig[
'filterNames'] = self.config.physicalFilters
539 lutConfig[
'nCCD'] = nCcd
542 if self.config.atmosphereTableName
is not None:
543 lutConfig[
'atmosphereTableName'] = self.config.atmosphereTableName
546 lutConfig[
'elevation'] = self.config.parameters.elevation
547 lutConfig[
'pmbRange'] = self.config.parameters.pmbRange
548 lutConfig[
'pmbSteps'] = self.config.parameters.pmbSteps
549 lutConfig[
'pwvRange'] = self.config.parameters.pwvRange
550 lutConfig[
'pwvSteps'] = self.config.parameters.pwvSteps
551 lutConfig[
'o3Range'] = self.config.parameters.o3Range
552 lutConfig[
'o3Steps'] = self.config.parameters.o3Steps
553 lutConfig[
'tauRange'] = self.config.parameters.tauRange
554 lutConfig[
'tauSteps'] = self.config.parameters.tauSteps
555 lutConfig[
'alphaRange'] = self.config.parameters.alphaRange
556 lutConfig[
'alphaSteps'] = self.config.parameters.alphaSteps
557 lutConfig[
'zenithRange'] = self.config.parameters.zenithRange
558 lutConfig[
'zenithSteps'] = self.config.parameters.zenithSteps
559 lutConfig[
'pmbStd'] = self.config.parameters.pmbStd
560 lutConfig[
'pwvStd'] = self.config.parameters.pwvStd
561 lutConfig[
'o3Std'] = self.config.parameters.o3Std
562 lutConfig[
'tauStd'] = self.config.parameters.tauStd
563 lutConfig[
'alphaStd'] = self.config.parameters.alphaStd
564 lutConfig[
'airmassStd'] = self.config.parameters.airmassStd
565 lutConfig[
'lambdaRange'] = self.config.parameters.lambdaRange
566 lutConfig[
'lambdaStep'] = self.config.parameters.lambdaStep
567 lutConfig[
'lambdaNorm'] = self.config.parameters.lambdaNorm
571 def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict):
572 """Internal method to load throughput data for filters
576 camera: `lsst.afw.cameraGeom.Camera`
577 Camera from the butler
578 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
579 `lsst.daf.butler.DeferredDatasetHandle`
580 Reference to optics transmission curve.
581 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
582 `lsst.daf.butler.DeferredDatasetHandle`]
583 Dictionary of references to sensor transmission curves. Key will
585 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
586 `lsst.daf.butler.DeferredDatasetHandle`]
587 Dictionary of references to filter transmission curves. Key will
588 be physical filter label.
592 ValueError : Raised if configured filter name does not match any of the
593 available filter transmission curves.
598 for detector
in camera:
599 self.
_sensorsTransmission_sensorsTransmission[detector.getId()] = sensorDataRefDict[detector.getId()].get()
602 for physicalFilter
in self.config.physicalFilters:
603 self.
_filtersTransmission_filtersTransmission[physicalFilter] = filterDataRefDict[physicalFilter].get()
605 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda):
606 """Internal method to get throughput for a detector.
608 Returns the throughput at the center of the detector for a given filter.
612 detector: `lsst.afw.cameraGeom._detector.Detector`
614 physicalFilter: `str`
615 Physical filter label
616 throughputLambda: `np.array(dtype=np.float64)`
617 Wavelength steps (Angstrom)
621 throughput: `np.array(dtype=np.float64)`
622 Throughput (max 1.0) at throughputLambda
625 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
626 c.scale(1.0/detector.getPixelSize()[0])
629 wavelengths=throughputLambda)
631 throughput *= self.
_sensorsTransmission_sensorsTransmission[detector.getId()].sampleAt(position=c,
632 wavelengths=throughputLambda)
634 throughput *= self.
_filtersTransmission_filtersTransmission[physicalFilter].sampleAt(position=c,
635 wavelengths=throughputLambda)
638 throughput = np.clip(throughput, 0.0, 1.0)
642 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString,
643 atmosphereTableName):
649 physicalFilterString: `str`
650 Combined string of all the physicalFilters
651 stdPhysicalFilterString: `str`
652 Combined string of all the standard physicalFilters
653 atmosphereTableName: `str`
654 Name of the atmosphere table used to generate LUT
658 lutSchema: `afwTable.schema`
663 lutSchema.addField(
'tablename', type=str, doc=
'Atmosphere table name',
664 size=len(atmosphereTableName))
665 lutSchema.addField(
'elevation', type=float, doc=
"Telescope elevation used for LUT")
666 lutSchema.addField(
'physicalFilters', type=str, doc=
'physicalFilters in LUT',
667 size=len(physicalFilterString))
668 lutSchema.addField(
'stdPhysicalFilters', type=str, doc=
'Standard physicalFilters in LUT',
669 size=len(stdPhysicalFilterString))
670 lutSchema.addField(
'pmb', type=
'ArrayD', doc=
'Barometric Pressure',
672 lutSchema.addField(
'pmbFactor', type=
'ArrayD', doc=
'PMB scaling factor',
674 lutSchema.addField(
'pmbElevation', type=np.float64, doc=
'PMB Scaling at elevation')
675 lutSchema.addField(
'pwv', type=
'ArrayD', doc=
'Preciptable Water Vapor',
677 lutSchema.addField(
'o3', type=
'ArrayD', doc=
'Ozone',
679 lutSchema.addField(
'tau', type=
'ArrayD', doc=
'Aerosol optical depth',
681 lutSchema.addField(
'lambdaNorm', type=np.float64, doc=
'AOD wavelength')
682 lutSchema.addField(
'alpha', type=
'ArrayD', doc=
'Aerosol alpha',
684 lutSchema.addField(
'zenith', type=
'ArrayD', doc=
'Zenith angle',
686 lutSchema.addField(
'nCcd', type=np.int32, doc=
'Number of CCDs')
689 lutSchema.addField(
'pmbStd', type=np.float64, doc=
'PMB Standard')
690 lutSchema.addField(
'pwvStd', type=np.float64, doc=
'PWV Standard')
691 lutSchema.addField(
'o3Std', type=np.float64, doc=
'O3 Standard')
692 lutSchema.addField(
'tauStd', type=np.float64, doc=
'Tau Standard')
693 lutSchema.addField(
'alphaStd', type=np.float64, doc=
'Alpha Standard')
694 lutSchema.addField(
'zenithStd', type=np.float64, doc=
'Zenith angle Standard')
695 lutSchema.addField(
'lambdaRange', type=
'ArrayD', doc=
'Wavelength range',
697 lutSchema.addField(
'lambdaStep', type=np.float64, doc=
'Wavelength step')
698 lutSchema.addField(
'lambdaStd', type=
'ArrayD', doc=
'Standard Wavelength',
700 lutSchema.addField(
'lambdaStdFilter', type=
'ArrayD', doc=
'Standard Wavelength (raw)',
702 lutSchema.addField(
'i0Std', type=
'ArrayD', doc=
'I0 Standard',
704 lutSchema.addField(
'i1Std', type=
'ArrayD', doc=
'I1 Standard',
706 lutSchema.addField(
'i10Std', type=
'ArrayD', doc=
'I10 Standard',
708 lutSchema.addField(
'i2Std', type=
'ArrayD', doc=
'I2 Standard',
710 lutSchema.addField(
'lambdaB', type=
'ArrayD', doc=
'Wavelength for passband (no atm)',
712 lutSchema.addField(
'atmLambda', type=
'ArrayD', doc=
'Atmosphere wavelengths (Angstrom)',
714 lutSchema.addField(
'atmStdTrans', type=
'ArrayD', doc=
'Standard Atmosphere Throughput',
718 lutSchema.addField(
'luttype', type=str, size=20, doc=
'Look-up table type')
719 lutSchema.addField(
'lut', type=
'ArrayF', doc=
'Look-up table for luttype',
724 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString,
725 atmosphereTableName):
731 lutSchema: `afwTable.schema`
733 physicalFilterString: `str`
734 Combined string of all the physicalFilters
735 stdPhysicalFilterString: `str`
736 Combined string of all the standard physicalFilters
737 atmosphereTableName: `str`
738 Name of the atmosphere table used to generate LUT
742 lutCat: `afwTable.BaseCatalog`
743 Lut catalog for persistence
751 lutCat.table.preallocate(14)
754 rec = lutCat.addNew()
756 rec[
'tablename'] = atmosphereTableName
757 rec[
'elevation'] = self.
fgcmLutMakerfgcmLutMaker.atmosphereTable.elevation
758 rec[
'physicalFilters'] = physicalFilterString
759 rec[
'stdPhysicalFilters'] = stdPhysicalFilterString
761 rec[
'pmbFactor'][:] = self.
fgcmLutMakerfgcmLutMaker.pmbFactor
762 rec[
'pmbElevation'] = self.
fgcmLutMakerfgcmLutMaker.pmbElevation
766 rec[
'lambdaNorm'] = self.
fgcmLutMakerfgcmLutMaker.lambdaNorm
775 rec[
'alphaStd'] = self.
fgcmLutMakerfgcmLutMaker.alphaStd
776 rec[
'zenithStd'] = self.
fgcmLutMakerfgcmLutMaker.zenithStd
777 rec[
'lambdaRange'][:] = self.
fgcmLutMakerfgcmLutMaker.lambdaRange
778 rec[
'lambdaStep'] = self.
fgcmLutMakerfgcmLutMaker.lambdaStep
779 rec[
'lambdaStd'][:] = self.
fgcmLutMakerfgcmLutMaker.lambdaStd
780 rec[
'lambdaStdFilter'][:] = self.
fgcmLutMakerfgcmLutMaker.lambdaStdFilter
785 rec[
'lambdaB'][:] = self.
fgcmLutMakerfgcmLutMaker.lambdaB
786 rec[
'atmLambda'][:] = self.
fgcmLutMakerfgcmLutMaker.atmLambda
787 rec[
'atmStdTrans'][:] = self.
fgcmLutMakerfgcmLutMaker.atmStdTrans
789 rec[
'luttype'] =
'I0'
790 rec[
'lut'][:] = self.
fgcmLutMakerfgcmLutMaker.lut[
'I0'].flatten()
793 rec = lutCat.addNew()
794 rec[
'luttype'] =
'I1'
795 rec[
'lut'][:] = self.
fgcmLutMakerfgcmLutMaker.lut[
'I1'].flatten()
797 derivTypes = [
'D_PMB',
'D_LNPWV',
'D_O3',
'D_LNTAU',
'D_ALPHA',
'D_SECZENITH',
798 'D_PMB_I1',
'D_LNPWV_I1',
'D_O3_I1',
'D_LNTAU_I1',
'D_ALPHA_I1',
800 for derivType
in derivTypes:
801 rec = lutCat.addNew()
802 rec[
'luttype'] = derivType
803 rec[
'lut'][:] = self.
fgcmLutMakerfgcmLutMaker.lutDeriv[derivType].flatten()
Defines the fields and offsets for a table.
def getTargetList(parsedCmd)
def __call__(self, butler)
def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString, atmosphereTableName)
def _getStdPhysicalFilterList(self)
def _createLutConfig(self, nCcd)
def runDataRef(self, butler)
def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict)
def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, atmosphereTableName)
def _getThroughputDetector(self, detector, physicalFilter, throughputLambda)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict)
def __init__(self, butler=None, initInputs=None, **kwargs)