23"""Make a look-up-table (LUT) for FGCM calibration.
25This task computes a look-up-table for the range in expected atmosphere
26variation and variation in instrumental throughput (as tracked by the
27transmission_filter products). By pre-computing linearized integrals,
28the FGCM fit is orders of magnitude faster
for stars
with a broad range
29of colors
and observing bands, yielding precision at the 1-2 mmag level.
31Computing a LUT requires running MODTRAN
or with a pre-generated
32atmosphere table packaged
with fgcm.
37import lsst.pex.config as pexConfig
38import lsst.pipe.base as pipeBase
39from lsst.pipe.base import connectionTypes
40import lsst.afw.table as afwTable
41import lsst.afw.cameraGeom as afwCameraGeom
42from lsst.afw.image import TransmissionCurve
43from .utilities import lookupStaticCalibrations
47__all__ = ['FgcmMakeLutParametersConfig', 'FgcmMakeLutConfig', 'FgcmMakeLutTask']
50class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections,
51 dimensions=('instrument',),
53 camera = connectionTypes.PrerequisiteInput(
54 doc=
"Camera instrument",
56 storageClass=
"Camera",
57 dimensions=(
"instrument",),
58 lookupFunction=lookupStaticCalibrations,
62 transmission_optics = connectionTypes.PrerequisiteInput(
63 doc=
"Optics transmission curve information",
64 name=
"transmission_optics",
65 storageClass=
"TransmissionCurve",
66 dimensions=(
"instrument",),
67 lookupFunction=lookupStaticCalibrations,
72 transmission_sensor = connectionTypes.PrerequisiteInput(
73 doc=
"Sensor transmission curve information",
74 name=
"transmission_sensor",
75 storageClass=
"TransmissionCurve",
76 dimensions=(
"instrument",
"detector",),
77 lookupFunction=lookupStaticCalibrations,
83 transmission_filter = connectionTypes.PrerequisiteInput(
84 doc=
"Filter transmission curve information",
85 name=
"transmission_filter",
86 storageClass=
"TransmissionCurve",
87 dimensions=(
"band",
"instrument",
"physical_filter",),
88 lookupFunction=lookupStaticCalibrations,
94 fgcmLookUpTable = connectionTypes.Output(
95 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
96 "chromatic corrections."),
97 name=
"fgcmLookUpTable",
98 storageClass=
"Catalog",
99 dimensions=(
"instrument",),
102 fgcmStandardAtmosphere = connectionTypes.Output(
103 doc=
"Standard atmosphere used for FGCM calibration.",
104 name=
"fgcm_standard_atmosphere",
105 storageClass=
"TransmissionCurve",
106 dimensions=(
"instrument",),
109 fgcmStandardPassbands = connectionTypes.Output(
110 doc=
"Standard passbands used for FGCM calibration.",
111 name=
"fgcm_standard_passband",
112 storageClass=
"TransmissionCurve",
113 dimensions=(
"instrument",
"physical_filter"),
119 """Config for parameters if atmosphereTableName not available"""
122 elevation = pexConfig.Field(
123 doc=
"Telescope elevation (m)",
127 pmbRange = pexConfig.ListField(
128 doc=(
"Barometric Pressure range (millibar) "
129 "Recommended range depends on the site."),
133 pmbSteps = pexConfig.Field(
134 doc=
"Barometric Pressure number of steps",
138 pwvRange = pexConfig.ListField(
139 doc=(
"Precipitable Water Vapor range (mm) "
140 "Recommended range depends on the site."),
144 pwvSteps = pexConfig.Field(
145 doc=
"Precipitable Water Vapor number of steps",
149 o3Range = pexConfig.ListField(
150 doc=
"Ozone range (dob)",
152 default=[220.0, 310.0],
154 o3Steps = pexConfig.Field(
155 doc=
"Ozone number of steps",
159 tauRange = pexConfig.ListField(
160 doc=
"Aerosol Optical Depth range (unitless)",
162 default=[0.002, 0.35],
164 tauSteps = pexConfig.Field(
165 doc=
"Aerosol Optical Depth number of steps",
169 alphaRange = pexConfig.ListField(
170 doc=
"Aerosol alpha range (unitless)",
174 alphaSteps = pexConfig.Field(
175 doc=
"Aerosol alpha number of steps",
179 zenithRange = pexConfig.ListField(
180 doc=
"Zenith angle range (degree)",
184 zenithSteps = pexConfig.Field(
185 doc=
"Zenith angle number of steps",
191 pmbStd = pexConfig.Field(
192 doc=(
"Standard Atmosphere pressure (millibar); "
193 "Recommended default depends on the site."),
197 pwvStd = pexConfig.Field(
198 doc=(
"Standard Atmosphere PWV (mm); "
199 "Recommended default depends on the site."),
203 o3Std = pexConfig.Field(
204 doc=
"Standard Atmosphere O3 (dob)",
208 tauStd = pexConfig.Field(
209 doc=
"Standard Atmosphere aerosol optical depth",
213 alphaStd = pexConfig.Field(
214 doc=
"Standard Atmosphere aerosol alpha",
218 airmassStd = pexConfig.Field(
219 doc=(
"Standard Atmosphere airmass; "
220 "Recommended default depends on the survey strategy."),
224 lambdaNorm = pexConfig.Field(
225 doc=
"Aerosol Optical Depth normalization wavelength (Angstrom)",
229 lambdaStep = pexConfig.Field(
230 doc=
"Wavelength step for generating atmospheres (nm)",
234 lambdaRange = pexConfig.ListField(
235 doc=
"Wavelength range for LUT (Angstrom)",
237 default=[3000.0, 11000.0],
242 pipelineConnections=FgcmMakeLutConnections):
243 """Config for FgcmMakeLutTask"""
244 physicalFilters = pexConfig.ListField(
245 doc=
"List of physicalFilter labels to generate look-up table.",
249 stdPhysicalFilterOverrideMap = pexConfig.DictField(
250 doc=(
"Override mapping from physical filter labels to 'standard' physical "
251 "filter labels. The 'standard' physical filter defines the transmission "
252 "curve that the FGCM standard bandpass will be based on. "
253 "Any filter not listed here will be mapped to "
254 "itself (e.g. g->g or HSC-G->HSC-G). Use this override for cross-"
255 "filter calibration such as HSC-R->HSC-R2 and HSC-I->HSC-I2."),
260 atmosphereTableName = pexConfig.Field(
261 doc=
"FGCM name or filename of precomputed atmospheres",
266 parameters = pexConfig.ConfigField(
267 doc=
"Atmosphere parameters (required if no atmosphereTableName)",
268 dtype=FgcmMakeLutParametersConfig,
274 Validate the config parameters.
276 This method behaves differently from the parent validate
in the case
277 that atmosphereTableName
is set. In this case, the config values
278 for standard values, step sizes,
and ranges are loaded
279 directly
from the specified atmosphereTableName.
282 self._fields[
'physicalFilters'].
validate(self)
283 self._fields[
'stdPhysicalFilterOverrideMap'].
validate(self)
287 self._fields[
'parameters'].
validate(self)
292 Make Look-Up Table for FGCM.
294 This task computes a look-up-table
for the range
in expected atmosphere
295 variation
and variation
in instrumental throughput (
as tracked by the
296 transmission_filter products). By pre-computing linearized integrals,
297 the FGCM fit
is orders of magnitude faster
for stars
with a broad range
298 of colors
and observing bands, yielding precision at the 1-2 mmag level.
300 Computing a LUT requires running MODTRAN
or with a pre-generated
301 atmosphere table packaged
with fgcm.
304 ConfigClass = FgcmMakeLutConfig
305 _DefaultName = "fgcmMakeLut"
307 def __init__(self, butler=None, initInputs=None, **kwargs):
308 super().__init__(**kwargs)
311 camera = butlerQC.get(inputRefs.camera)
313 opticsHandle = butlerQC.get(inputRefs.transmission_optics)
315 sensorHandles = butlerQC.get(inputRefs.transmission_sensor)
316 sensorHandleDict = {sensorHandle.dataId.byName()[
'detector']: sensorHandle
for
317 sensorHandle
in sensorHandles}
319 filterHandles = butlerQC.get(inputRefs.transmission_filter)
320 filterHandleDict = {filterHandle.dataId[
'physical_filter']: filterHandle
for
321 filterHandle
in filterHandles}
328 butlerQC.put(struct.fgcmLookUpTable, outputRefs.fgcmLookUpTable)
329 butlerQC.put(struct.fgcmStandardAtmosphere, outputRefs.fgcmStandardAtmosphere)
331 refDict = {passbandRef.dataId[
'physical_filter']: passbandRef
for
332 passbandRef
in outputRefs.fgcmStandardPassbands}
333 for physical_filter, passband
in struct.fgcmStandardPassbands.items():
334 butlerQC.put(passband, refDict[physical_filter])
336 def _fgcmMakeLut(self, camera, opticsHandle, sensorHandleDict,
339 Make a FGCM Look-up Table
344 Camera from the butler.
345 opticsHandle : `lsst.daf.butler.DeferredDatasetHandle`
346 Reference to optics transmission curve.
347 sensorHandleDict : `dict` of [`int`, `lsst.daf.butler.DeferredDatasetHandle`]
348 Dictionary of references to sensor transmission curves. Key will
350 filterHandleDict : `dict` of [`str`, `lsst.daf.butler.DeferredDatasetHandle`]
351 Dictionary of references to filter transmission curves. Key will
352 be physical filter label.
356 retStruct : `lsst.pipe.base.Struct`
357 Output structure
with keys:
359 fgcmLookUpTable : `BaseCatalog`
360 The FGCM look-up table.
362 Transmission curve
for the FGCM standard atmosphere.
364 Dictionary of standard passbands,
with the key
as the
365 physical filter name.
369 self.log.info(
"Found %d ccds for look-up table" % (nCcd))
380 self.log.info(
"Making the LUT maker object")
388 throughputLambda = np.arange(self.
fgcmLutMaker.lambdaRange[0],
392 self.log.info(
"Built throughput lambda, %.1f-%.1f, step %.2f" %
393 (throughputLambda[0], throughputLambda[-1],
394 throughputLambda[1] - throughputLambda[0]))
397 for i, physicalFilter
in enumerate(self.config.physicalFilters):
399 tDict[
'LAMBDA'] = throughputLambda
400 for ccdIndex, detector
in enumerate(camera):
402 throughputDict[physicalFilter] = tDict
408 self.log.info(
"Making LUT")
415 physicalFilterString = comma.join(self.config.physicalFilters)
418 atmosphereTableName =
'NoTableWasUsed'
419 if self.config.atmosphereTableName
is not None:
420 atmosphereTableName = self.config.atmosphereTableName
422 lutSchema = self.
_makeLutSchema(physicalFilterString, stdPhysicalFilterString,
425 lutCat = self.
_makeLutCat(lutSchema, physicalFilterString,
426 stdPhysicalFilterString, atmosphereTableName)
428 atmStd = TransmissionCurve.makeSpatiallyConstant(
429 throughput=self.
fgcmLutMaker.atmStdTrans.astype(np.float64),
430 wavelengths=self.
fgcmLutMaker.atmLambda.astype(np.float64),
435 fgcmStandardPassbands = {}
436 for i, physical_filter
in enumerate(self.
fgcmLutMaker.filterNames):
438 fgcmStandardPassbands[physical_filter] = TransmissionCurve.makeSpatiallyConstant(
439 throughput=passband.astype(np.float64),
440 wavelengths=self.
fgcmLutMaker.atmLambda.astype(np.float64),
441 throughputAtMin=passband[0],
442 throughputAtMax=passband[-1],
445 retStruct = pipeBase.Struct(
446 fgcmLookUpTable=lutCat,
447 fgcmStandardAtmosphere=atmStd,
448 fgcmStandardPassbands=fgcmStandardPassbands,
453 def _getStdPhysicalFilterList(self):
454 """Get the standard physical filter lists from config.physicalFilters
455 and config.stdPhysicalFilterOverrideMap
459 stdPhysicalFilters : `list`
461 override = self.config.stdPhysicalFilterOverrideMap
462 return [override.get(physicalFilter, physicalFilter)
for
463 physicalFilter
in self.config.physicalFilters]
465 def _createLutConfig(self, nCcd):
467 Create the fgcmLut config dictionary
472 Number of CCDs in the camera
477 lutConfig[
'logger'] = self.log
478 lutConfig[
'filterNames'] = self.config.physicalFilters
480 lutConfig[
'nCCD'] = nCcd
483 if self.config.atmosphereTableName
is not None:
484 lutConfig[
'atmosphereTableName'] = self.config.atmosphereTableName
487 lutConfig[
'elevation'] = self.config.parameters.elevation
488 lutConfig[
'pmbRange'] = self.config.parameters.pmbRange
489 lutConfig[
'pmbSteps'] = self.config.parameters.pmbSteps
490 lutConfig[
'pwvRange'] = self.config.parameters.pwvRange
491 lutConfig[
'pwvSteps'] = self.config.parameters.pwvSteps
492 lutConfig[
'o3Range'] = self.config.parameters.o3Range
493 lutConfig[
'o3Steps'] = self.config.parameters.o3Steps
494 lutConfig[
'tauRange'] = self.config.parameters.tauRange
495 lutConfig[
'tauSteps'] = self.config.parameters.tauSteps
496 lutConfig[
'alphaRange'] = self.config.parameters.alphaRange
497 lutConfig[
'alphaSteps'] = self.config.parameters.alphaSteps
498 lutConfig[
'zenithRange'] = self.config.parameters.zenithRange
499 lutConfig[
'zenithSteps'] = self.config.parameters.zenithSteps
500 lutConfig[
'pmbStd'] = self.config.parameters.pmbStd
501 lutConfig[
'pwvStd'] = self.config.parameters.pwvStd
502 lutConfig[
'o3Std'] = self.config.parameters.o3Std
503 lutConfig[
'tauStd'] = self.config.parameters.tauStd
504 lutConfig[
'alphaStd'] = self.config.parameters.alphaStd
505 lutConfig[
'airmassStd'] = self.config.parameters.airmassStd
506 lutConfig[
'lambdaRange'] = self.config.parameters.lambdaRange
507 lutConfig[
'lambdaStep'] = self.config.parameters.lambdaStep
508 lutConfig[
'lambdaNorm'] = self.config.parameters.lambdaNorm
512 def _loadThroughputs(self, camera, opticsHandle, sensorHandleDict, filterHandleDict):
513 """Internal method to load throughput data for filters
518 Camera from the butler
519 opticsHandle : `lsst.daf.butler.DeferredDatasetHandle`
520 Reference to optics transmission curve.
521 sensorHandleDict : `dict` of [`int`, `lsst.daf.butler.DeferredDatasetHandle`]
522 Dictionary of references to sensor transmission curves. Key will
524 filterHandleDict : `dict` of [`str`, `lsst.daf.butler.DeferredDatasetHandle`]
525 Dictionary of references to filter transmission curves. Key will
526 be physical filter label.
530 ValueError : Raised
if configured filter name does
not match any of the
531 available filter transmission curves.
536 for detector
in camera:
540 for physicalFilter
in self.config.physicalFilters:
543 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda):
544 """Internal method to get throughput for a detector.
546 Returns the throughput at the center of the detector for a given filter.
550 detector: `lsst.afw.cameraGeom._detector.Detector`
552 physicalFilter: `str`
553 Physical filter label
554 throughputLambda: `np.array(dtype=np.float64)`
555 Wavelength steps (Angstrom)
559 throughput: `np.array(dtype=np.float64)`
560 Throughput (max 1.0) at throughputLambda
563 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
564 c.scale(1.0/detector.getPixelSize()[0])
567 wavelengths=throughputLambda)
570 wavelengths=throughputLambda)
573 wavelengths=throughputLambda)
576 throughput = np.clip(throughput, 0.0, 1.0)
580 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString,
581 atmosphereTableName):
587 physicalFilterString: `str`
588 Combined string of all the physicalFilters
589 stdPhysicalFilterString: `str`
590 Combined string of all the standard physicalFilters
591 atmosphereTableName: `str`
592 Name of the atmosphere table used to generate LUT
596 lutSchema: `afwTable.schema`
601 lutSchema.addField('tablename', type=str, doc=
'Atmosphere table name',
602 size=len(atmosphereTableName))
603 lutSchema.addField(
'elevation', type=float, doc=
"Telescope elevation used for LUT")
604 lutSchema.addField(
'physicalFilters', type=str, doc=
'physicalFilters in LUT',
605 size=len(physicalFilterString))
606 lutSchema.addField(
'stdPhysicalFilters', type=str, doc=
'Standard physicalFilters in LUT',
607 size=len(stdPhysicalFilterString))
608 lutSchema.addField(
'pmb', type=
'ArrayD', doc=
'Barometric Pressure',
610 lutSchema.addField(
'pmbFactor', type=
'ArrayD', doc=
'PMB scaling factor',
612 lutSchema.addField(
'pmbElevation', type=np.float64, doc=
'PMB Scaling at elevation')
613 lutSchema.addField(
'pwv', type=
'ArrayD', doc=
'Preciptable Water Vapor',
615 lutSchema.addField(
'o3', type=
'ArrayD', doc=
'Ozone',
617 lutSchema.addField(
'tau', type=
'ArrayD', doc=
'Aerosol optical depth',
619 lutSchema.addField(
'lambdaNorm', type=np.float64, doc=
'AOD wavelength')
620 lutSchema.addField(
'alpha', type=
'ArrayD', doc=
'Aerosol alpha',
622 lutSchema.addField(
'zenith', type=
'ArrayD', doc=
'Zenith angle',
624 lutSchema.addField(
'nCcd', type=np.int32, doc=
'Number of CCDs')
627 lutSchema.addField(
'pmbStd', type=np.float64, doc=
'PMB Standard')
628 lutSchema.addField(
'pwvStd', type=np.float64, doc=
'PWV Standard')
629 lutSchema.addField(
'o3Std', type=np.float64, doc=
'O3 Standard')
630 lutSchema.addField(
'tauStd', type=np.float64, doc=
'Tau Standard')
631 lutSchema.addField(
'alphaStd', type=np.float64, doc=
'Alpha Standard')
632 lutSchema.addField(
'zenithStd', type=np.float64, doc=
'Zenith angle Standard')
633 lutSchema.addField(
'lambdaRange', type=
'ArrayD', doc=
'Wavelength range',
635 lutSchema.addField(
'lambdaStep', type=np.float64, doc=
'Wavelength step')
636 lutSchema.addField(
'lambdaStd', type=
'ArrayD', doc=
'Standard Wavelength',
638 lutSchema.addField(
'lambdaStdFilter', type=
'ArrayD', doc=
'Standard Wavelength (raw)',
640 lutSchema.addField(
'i0Std', type=
'ArrayD', doc=
'I0 Standard',
642 lutSchema.addField(
'i1Std', type=
'ArrayD', doc=
'I1 Standard',
644 lutSchema.addField(
'i10Std', type=
'ArrayD', doc=
'I10 Standard',
646 lutSchema.addField(
'i2Std', type=
'ArrayD', doc=
'I2 Standard',
648 lutSchema.addField(
'lambdaB', type=
'ArrayD', doc=
'Wavelength for passband (no atm)',
650 lutSchema.addField(
'atmLambda', type=
'ArrayD', doc=
'Atmosphere wavelengths (Angstrom)',
652 lutSchema.addField(
'atmStdTrans', type=
'ArrayD', doc=
'Standard Atmosphere Throughput',
656 lutSchema.addField(
'luttype', type=str, size=20, doc=
'Look-up table type')
657 lutSchema.addField(
'lut', type=
'ArrayF', doc=
'Look-up table for luttype',
662 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString,
663 atmosphereTableName):
669 lutSchema: `afwTable.schema`
671 physicalFilterString: `str`
672 Combined string of all the physicalFilters
673 stdPhysicalFilterString: `str`
674 Combined string of all the standard physicalFilters
675 atmosphereTableName: `str`
676 Name of the atmosphere table used to generate LUT
681 Look-up table catalog for persistence.
689 lutCat.table.preallocate(14)
692 rec = lutCat.addNew()
694 rec[
'tablename'] = atmosphereTableName
695 rec[
'elevation'] = self.
fgcmLutMaker.atmosphereTable.elevation
696 rec[
'physicalFilters'] = physicalFilterString
697 rec[
'stdPhysicalFilters'] = stdPhysicalFilterString
718 rec[
'lambdaStdFilter'][:] = self.
fgcmLutMaker.lambdaStdFilter
727 rec[
'luttype'] =
'I0'
731 rec = lutCat.addNew()
732 rec[
'luttype'] =
'I1'
735 derivTypes = [
'D_PMB',
'D_LNPWV',
'D_O3',
'D_LNTAU',
'D_ALPHA',
'D_SECZENITH',
736 'D_PMB_I1',
'D_LNPWV_I1',
'D_O3_I1',
'D_LNTAU_I1',
'D_ALPHA_I1',
738 for derivType
in derivTypes:
739 rec = lutCat.addNew()
740 rec[
'luttype'] = derivType
741 rec[
'lut'][:] = self.
fgcmLutMaker.lutDeriv[derivType].flatten()
An immutable representation of a camera.
A spatially-varying transmission curve as a function of wavelength.
Defines the fields and offsets for a table.
def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString, atmosphereTableName)
def _fgcmMakeLut(self, camera, opticsHandle, sensorHandleDict, filterHandleDict)
def _getStdPhysicalFilterList(self)
def _createLutConfig(self, nCcd)
def _loadThroughputs(self, camera, opticsHandle, sensorHandleDict, filterHandleDict)
def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, atmosphereTableName)
def _getThroughputDetector(self, detector, physicalFilter, throughputLambda)
def runQuantum(self, butlerQC, inputRefs, outputRefs)