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.
40 import lsst.pex.config
as pexConfig
49 __all__ = [
'FgcmMakeLutParametersConfig',
'FgcmMakeLutConfig',
'FgcmMakeLutTask',
54 """Config for parameters if atmosphereTableName not available"""
57 elevation = pexConfig.Field(
58 doc=
"Telescope elevation (m)",
62 pmbRange = pexConfig.ListField(
63 doc=(
"Barometric Pressure range (millibar) "
64 "Recommended range depends on the site."),
68 pmbSteps = pexConfig.Field(
69 doc=
"Barometric Pressure number of steps",
73 pwvRange = pexConfig.ListField(
74 doc=(
"Precipitable Water Vapor range (mm) "
75 "Recommended range depends on the site."),
79 pwvSteps = pexConfig.Field(
80 doc=
"Precipitable Water Vapor number of steps",
84 o3Range = pexConfig.ListField(
85 doc=
"Ozone range (dob)",
87 default=[220.0, 310.0],
89 o3Steps = pexConfig.Field(
90 doc=
"Ozone number of steps",
94 tauRange = pexConfig.ListField(
95 doc=
"Aerosol Optical Depth range (unitless)",
97 default=[0.002, 0.35],
99 tauSteps = pexConfig.Field(
100 doc=
"Aerosol Optical Depth number of steps",
104 alphaRange = pexConfig.ListField(
105 doc=
"Aerosol alpha range (unitless)",
109 alphaSteps = pexConfig.Field(
110 doc=
"Aerosol alpha number of steps",
114 zenithRange = pexConfig.ListField(
115 doc=
"Zenith angle range (degree)",
119 zenithSteps = pexConfig.Field(
120 doc=
"Zenith angle number of steps",
126 pmbStd = pexConfig.Field(
127 doc=(
"Standard Atmosphere pressure (millibar); "
128 "Recommended default depends on the site."),
132 pwvStd = pexConfig.Field(
133 doc=(
"Standard Atmosphere PWV (mm); "
134 "Recommended default depends on the site."),
138 o3Std = pexConfig.Field(
139 doc=
"Standard Atmosphere O3 (dob)",
143 tauStd = pexConfig.Field(
144 doc=
"Standard Atmosphere aerosol optical depth",
148 alphaStd = pexConfig.Field(
149 doc=
"Standard Atmosphere aerosol alpha",
153 airmassStd = pexConfig.Field(
154 doc=(
"Standard Atmosphere airmass; "
155 "Recommended default depends on the survey strategy."),
159 lambdaNorm = pexConfig.Field(
160 doc=
"Aerosol Optical Depth normalization wavelength (Angstrom)",
164 lambdaStep = pexConfig.Field(
165 doc=
"Wavelength step for generating atmospheres (nm)",
169 lambdaRange = pexConfig.ListField(
170 doc=
"Wavelength range for LUT (Angstrom)",
172 default=[3000.0, 11000.0],
177 """Config for FgcmMakeLutTask"""
179 filterNames = pexConfig.ListField(
180 doc=
"Filter names to build LUT ('short' names)",
184 stdFilterNames = pexConfig.ListField(
185 doc=(
"Standard filterNames ('short' names). "
186 "Each filter in filterName will be calibrated to a matched "
187 "stdFilterName. In regular usage, one has g->g, r->r, ... "
188 "In the case of HSC, one would have g->g, r->r2, r2->r2, ... "
189 "which allows replacement (or time-variable) filters to be "
190 "properly cross-calibrated."),
194 atmosphereTableName = pexConfig.Field(
195 doc=
"FGCM name or filename of precomputed atmospheres",
200 parameters = pexConfig.ConfigField(
201 doc=
"Atmosphere parameters (required if no atmosphereTableName)",
202 dtype=FgcmMakeLutParametersConfig,
208 Validate the config parameters.
210 This method behaves differently from the parent validate in the case
211 that atmosphereTableName is set. In this case, the config values
212 for standard values, step sizes, and ranges are loaded
213 directly from the specified atmosphereTableName.
216 self._fields[
'filterNames'].
validate(self)
217 self._fields[
'stdFilterNames'].
validate(self)
224 raise RuntimeError(
"Could not find atmosphereTableName: %s" %
228 self._fields[
'parameters'].
validate(self)
232 """Subclass of TaskRunner for fgcmMakeLutTask
234 fgcmMakeLutTask.run() takes one argument, the butler, and
235 does not run on any data in the repository.
236 This runner does not use any parallelization.
242 Return a list with one element, the butler.
244 return [parsedCmd.butler]
250 butler: `lsst.daf.persistence.Butler`
254 exitStatus: `list` with `pipeBase.Struct`
255 exitStatus (0: success; 1: failure)
257 task = self.TaskClass(config=self.config, log=self.log)
261 task.runDataRef(butler)
264 task.runDataRef(butler)
265 except Exception
as e:
267 task.log.fatal(
"Failed: %s" % e)
268 if not isinstance(e, pipeBase.TaskError):
269 traceback.print_exc(file=sys.stderr)
271 task.writeMetadata(butler)
274 return [pipeBase.Struct(exitStatus=exitStatus)]
278 Run the task, with no multiprocessing
282 parsedCmd: ArgumentParser parsed command line
287 if self.precall(parsedCmd):
290 resultList = self(targetList[0])
297 Make Look-Up Table for FGCM.
299 This task computes a look-up-table for the range in expected atmosphere
300 variation and variation in instrumental throughput (as tracked by the
301 transmission_filter products). By pre-computing linearized integrals,
302 the FGCM fit is orders of magnitude faster for stars with a broad range
303 of colors and observing bands, yielding precision at the 1-2 mmag level.
305 Computing a LUT requires running MODTRAN or with a pre-generated
306 atmosphere table packaged with fgcm.
309 ConfigClass = FgcmMakeLutConfig
310 RunnerClass = FgcmMakeLutRunner
311 _DefaultName =
"fgcmMakeLut"
315 Instantiate an fgcmMakeLutTask.
319 butler: `lsst.daf.persistence.Butler`
322 pipeBase.CmdLineTask.__init__(self, **kwargs)
325 def _getMetadataName(self):
331 Make a Look-Up Table for FGCM
335 butler: `lsst.daf.persistence.Butler`
340 def _fgcmMakeLut(self, butler):
342 Make a FGCM Look-up Table
346 butler: `lsst.daf.persistence.Butler`
350 camera = butler.get(
'camera')
354 self.log.
info(
"Found %d ccds for look-up table" % (nCcd))
362 self.log.
info(
"Making the LUT maker object")
370 throughputLambda = np.arange(self.
fgcmLutMaker.lambdaRange[0],
374 self.log.
info(
"Built throughput lambda, %.1f-%.1f, step %.2f" %
375 (throughputLambda[0], throughputLambda[-1],
376 throughputLambda[1]-throughputLambda[0]))
379 for i, filterName
in enumerate(self.config.filterNames):
381 tDict[
'LAMBDA'] = throughputLambda
382 for ccdIndex, detector
in enumerate(camera):
384 throughputDict[filterName] = tDict
390 self.log.
info(
"Making LUT")
397 filterNameString = comma.join(self.config.filterNames)
398 stdFilterNameString = comma.join(self.config.stdFilterNames)
400 atmosphereTableName =
'NoTableWasUsed'
401 if self.config.atmosphereTableName
is not None:
402 atmosphereTableName = self.config.atmosphereTableName
404 lutSchema = self.
_makeLutSchema(filterNameString, stdFilterNameString,
407 lutCat = self.
_makeLutCat(lutSchema, filterNameString,
408 stdFilterNameString, atmosphereTableName)
409 butler.put(lutCat,
'fgcmLookUpTable')
411 def _createLutConfig(self, nCcd):
413 Create the fgcmLut config dictionary
418 Number of CCDs in the camera
423 lutConfig[
'logger'] = self.log
424 lutConfig[
'filterNames'] = self.config.filterNames
425 lutConfig[
'stdFilterNames'] = self.config.stdFilterNames
426 lutConfig[
'nCCD'] = nCcd
429 if self.config.atmosphereTableName
is not None:
430 lutConfig[
'atmosphereTableName'] = self.config.atmosphereTableName
433 lutConfig[
'elevation'] = self.config.parameters.elevation
434 lutConfig[
'pmbRange'] = self.config.parameters.pmbRange
435 lutConfig[
'pmbSteps'] = self.config.parameters.pmbSteps
436 lutConfig[
'pwvRange'] = self.config.parameters.pwvRange
437 lutConfig[
'pwvSteps'] = self.config.parameters.pwvSteps
438 lutConfig[
'o3Range'] = self.config.parameters.o3Range
439 lutConfig[
'o3Steps'] = self.config.parameters.o3Steps
440 lutConfig[
'tauRange'] = self.config.parameters.tauRange
441 lutConfig[
'tauSteps'] = self.config.parameters.tauSteps
442 lutConfig[
'alphaRange'] = self.config.parameters.alphaRange
443 lutConfig[
'alphaSteps'] = self.config.parameters.alphaSteps
444 lutConfig[
'zenithRange'] = self.config.parameters.zenithRange
445 lutConfig[
'zenithSteps'] = self.config.parameters.zenithSteps
446 lutConfig[
'pmbStd'] = self.config.parameters.pmbStd
447 lutConfig[
'pwvStd'] = self.config.parameters.pwvStd
448 lutConfig[
'o3Std'] = self.config.parameters.o3Std
449 lutConfig[
'tauStd'] = self.config.parameters.tauStd
450 lutConfig[
'alphaStd'] = self.config.parameters.alphaStd
451 lutConfig[
'airmassStd'] = self.config.parameters.airmassStd
452 lutConfig[
'lambdaRange'] = self.config.parameters.lambdaRange
453 lutConfig[
'lambdaStep'] = self.config.parameters.lambdaStep
454 lutConfig[
'lambdaNorm'] = self.config.parameters.lambdaNorm
458 def _loadThroughputs(self, butler, camera):
459 """Internal method to load throughput data for filters
463 butler: `lsst.daf.persistence.butler.Butler`
464 A butler with the transmission info
465 camera: `lsst.afw.cameraGeom.Camera`
470 for detector
in camera:
472 dataId={
'ccd': detector.getId()})
474 for filterName
in self.config.filterNames:
478 aliases = f.getAliases()
479 aliases.extend(filterName)
480 for alias
in f.getAliases():
483 dataId={
'filter': alias})
489 raise ValueError(
"Could not find transmission for filter %s via any alias." % (filterName))
491 def _getThroughputDetector(self, detector, filterName, throughputLambda):
492 """Internal method to get throughput for a detector.
494 Returns the throughput at the center of the detector for a given filter.
498 detector: `lsst.afw.cameraGeom._detector.Detector`
501 Short name for filter
502 throughputLambda: `np.array(dtype=np.float64)`
503 Wavelength steps (Angstrom)
507 throughput: `np.array(dtype=np.float64)`
508 Throughput (max 1.0) at throughputLambda
511 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
512 c.scale(1.0/detector.getPixelSize()[0])
515 wavelengths=throughputLambda)
518 wavelengths=throughputLambda)
521 wavelengths=throughputLambda)
524 throughput = np.clip(throughput, 0.0, 1.0)
528 def _makeLutSchema(self, filterNameString, stdFilterNameString,
529 atmosphereTableName):
535 filterNameString: `str`
536 Combined string of all the filterNames
537 stdFilterNameString: `str`
538 Combined string of all the standard filterNames
539 atmosphereTableName: `str`
540 Name of the atmosphere table used to generate LUT
544 lutSchema: `afwTable.schema`
549 lutSchema.addField(
'tablename', type=str, doc=
'Atmosphere table name',
550 size=len(atmosphereTableName))
551 lutSchema.addField(
'elevation', type=float, doc=
"Telescope elevation used for LUT")
552 lutSchema.addField(
'filterNames', type=str, doc=
'filterNames in LUT',
553 size=len(filterNameString))
554 lutSchema.addField(
'stdFilterNames', type=str, doc=
'Standard filterNames in LUT',
555 size=len(stdFilterNameString))
556 lutSchema.addField(
'pmb', type=
'ArrayD', doc=
'Barometric Pressure',
558 lutSchema.addField(
'pmbFactor', type=
'ArrayD', doc=
'PMB scaling factor',
560 lutSchema.addField(
'pmbElevation', type=np.float64, doc=
'PMB Scaling at elevation')
561 lutSchema.addField(
'pwv', type=
'ArrayD', doc=
'Preciptable Water Vapor',
563 lutSchema.addField(
'o3', type=
'ArrayD', doc=
'Ozone',
565 lutSchema.addField(
'tau', type=
'ArrayD', doc=
'Aerosol optical depth',
567 lutSchema.addField(
'lambdaNorm', type=np.float64, doc=
'AOD wavelength')
568 lutSchema.addField(
'alpha', type=
'ArrayD', doc=
'Aerosol alpha',
570 lutSchema.addField(
'zenith', type=
'ArrayD', doc=
'Zenith angle',
572 lutSchema.addField(
'nCcd', type=np.int32, doc=
'Number of CCDs')
575 lutSchema.addField(
'pmbStd', type=np.float64, doc=
'PMB Standard')
576 lutSchema.addField(
'pwvStd', type=np.float64, doc=
'PWV Standard')
577 lutSchema.addField(
'o3Std', type=np.float64, doc=
'O3 Standard')
578 lutSchema.addField(
'tauStd', type=np.float64, doc=
'Tau Standard')
579 lutSchema.addField(
'alphaStd', type=np.float64, doc=
'Alpha Standard')
580 lutSchema.addField(
'zenithStd', type=np.float64, doc=
'Zenith angle Standard')
581 lutSchema.addField(
'lambdaRange', type=
'ArrayD', doc=
'Wavelength range',
583 lutSchema.addField(
'lambdaStep', type=np.float64, doc=
'Wavelength step')
584 lutSchema.addField(
'lambdaStd', type=
'ArrayD', doc=
'Standard Wavelength',
586 lutSchema.addField(
'lambdaStdFilter', type=
'ArrayD', doc=
'Standard Wavelength (raw)',
588 lutSchema.addField(
'i0Std', type=
'ArrayD', doc=
'I0 Standard',
590 lutSchema.addField(
'i1Std', type=
'ArrayD', doc=
'I1 Standard',
592 lutSchema.addField(
'i10Std', type=
'ArrayD', doc=
'I10 Standard',
594 lutSchema.addField(
'i2Std', type=
'ArrayD', doc=
'I2 Standard',
596 lutSchema.addField(
'lambdaB', type=
'ArrayD', doc=
'Wavelength for passband (no atm)',
598 lutSchema.addField(
'atmLambda', type=
'ArrayD', doc=
'Atmosphere wavelengths (Angstrom)',
600 lutSchema.addField(
'atmStdTrans', type=
'ArrayD', doc=
'Standard Atmosphere Throughput',
604 lutSchema.addField(
'luttype', type=str, size=20, doc=
'Look-up table type')
605 lutSchema.addField(
'lut', type=
'ArrayF', doc=
'Look-up table for luttype',
610 def _makeLutCat(self, lutSchema, filterNameString, stdFilterNameString,
611 atmosphereTableName):
617 lutSchema: `afwTable.schema`
619 filterNameString: `str`
620 Combined string of all the filterNames
621 stdFilterNameString: `str`
622 Combined string of all the standard filterNames
623 atmosphereTableName: `str`
624 Name of the atmosphere table used to generate LUT
628 lutCat: `afwTable.BaseCatalog`
629 Lut catalog for persistence
637 lutCat.table.preallocate(14)
640 rec = lutCat.addNew()
642 rec[
'tablename'] = atmosphereTableName
643 rec[
'elevation'] = self.
fgcmLutMaker.atmosphereTable.elevation
644 rec[
'filterNames'] = filterNameString
645 rec[
'stdFilterNames'] = stdFilterNameString
666 rec[
'lambdaStdFilter'][:] = self.
fgcmLutMaker.lambdaStdFilter
675 rec[
'luttype'] =
'I0'
679 rec = lutCat.addNew()
680 rec[
'luttype'] =
'I1'
683 derivTypes = [
'D_PMB',
'D_LNPWV',
'D_O3',
'D_LNTAU',
'D_ALPHA',
'D_SECZENITH',
684 'D_PMB_I1',
'D_LNPWV_I1',
'D_O3_I1',
'D_LNTAU_I1',
'D_ALPHA_I1',
686 for derivType
in derivTypes:
687 rec = lutCat.addNew()
688 rec[
'luttype'] = derivType
689 rec[
'lut'][:] = self.
fgcmLutMaker.lutDeriv[derivType].flatten()