Coverage for python/lsst/fgcmcal/fgcmMakeLut.py: 23%
Shortcuts 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
Shortcuts 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# See COPYRIGHT file at the top of the source tree.
2#
3# This file is part of fgcmcal.
4#
5# Developed for the LSST Data Management System.
6# This product includes software developed by the LSST Project
7# (https://www.lsst.org).
8# See the COPYRIGHT file at the top-level directory of this distribution
9# for details of code ownership.
10#
11# This program is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program. If not, see <https://www.gnu.org/licenses/>.
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.
33"""
35import sys
36import traceback
38import numpy as np
40import lsst.pex.config as pexConfig
41import lsst.pipe.base as pipeBase
42from lsst.pipe.base import connectionTypes
43import lsst.afw.table as afwTable
44import lsst.afw.cameraGeom as afwCameraGeom
45from lsst.utils.timer import timeMethod
46from .utilities import lookupStaticCalibrations
48import fgcm
50__all__ = ['FgcmMakeLutParametersConfig', 'FgcmMakeLutConfig', 'FgcmMakeLutTask',
51 'FgcmMakeLutRunner']
54class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections,
55 dimensions=('instrument',),
56 defaultTemplates={}):
57 camera = connectionTypes.PrerequisiteInput(
58 doc="Camera instrument",
59 name="camera",
60 storageClass="Camera",
61 dimensions=("instrument",),
62 lookupFunction=lookupStaticCalibrations,
63 isCalibration=True,
64 )
66 transmission_optics = connectionTypes.PrerequisiteInput(
67 doc="Optics transmission curve information",
68 name="transmission_optics",
69 storageClass="TransmissionCurve",
70 dimensions=("instrument",),
71 lookupFunction=lookupStaticCalibrations,
72 isCalibration=True,
73 deferLoad=True,
74 )
76 transmission_sensor = connectionTypes.PrerequisiteInput(
77 doc="Sensor transmission curve information",
78 name="transmission_sensor",
79 storageClass="TransmissionCurve",
80 dimensions=("instrument", "detector",),
81 lookupFunction=lookupStaticCalibrations,
82 isCalibration=True,
83 deferLoad=True,
84 multiple=True,
85 )
87 transmission_filter = connectionTypes.PrerequisiteInput(
88 doc="Filter transmission curve information",
89 name="transmission_filter",
90 storageClass="TransmissionCurve",
91 dimensions=("band", "instrument", "physical_filter",),
92 lookupFunction=lookupStaticCalibrations,
93 isCalibration=True,
94 deferLoad=True,
95 multiple=True,
96 )
98 fgcmLookUpTable = connectionTypes.Output(
99 doc=("Atmosphere + instrument look-up-table for FGCM throughput and "
100 "chromatic corrections."),
101 name="fgcmLookUpTable",
102 storageClass="Catalog",
103 dimensions=("instrument",),
104 )
107class FgcmMakeLutParametersConfig(pexConfig.Config):
108 """Config for parameters if atmosphereTableName not available"""
109 # TODO: When DM-16511 is done, it will be possible to get the
110 # telescope elevation directly from the camera.
111 elevation = pexConfig.Field(
112 doc="Telescope elevation (m)",
113 dtype=float,
114 default=None,
115 )
116 pmbRange = pexConfig.ListField(
117 doc=("Barometric Pressure range (millibar) "
118 "Recommended range depends on the site."),
119 dtype=float,
120 default=None,
121 )
122 pmbSteps = pexConfig.Field(
123 doc="Barometric Pressure number of steps",
124 dtype=int,
125 default=5,
126 )
127 pwvRange = pexConfig.ListField(
128 doc=("Precipitable Water Vapor range (mm) "
129 "Recommended range depends on the site."),
130 dtype=float,
131 default=None,
132 )
133 pwvSteps = pexConfig.Field(
134 doc="Precipitable Water Vapor number of steps",
135 dtype=int,
136 default=15,
137 )
138 o3Range = pexConfig.ListField(
139 doc="Ozone range (dob)",
140 dtype=float,
141 default=[220.0, 310.0],
142 )
143 o3Steps = pexConfig.Field(
144 doc="Ozone number of steps",
145 dtype=int,
146 default=3,
147 )
148 tauRange = pexConfig.ListField(
149 doc="Aerosol Optical Depth range (unitless)",
150 dtype=float,
151 default=[0.002, 0.35],
152 )
153 tauSteps = pexConfig.Field(
154 doc="Aerosol Optical Depth number of steps",
155 dtype=int,
156 default=11,
157 )
158 alphaRange = pexConfig.ListField(
159 doc="Aerosol alpha range (unitless)",
160 dtype=float,
161 default=[0.0, 2.0],
162 )
163 alphaSteps = pexConfig.Field(
164 doc="Aerosol alpha number of steps",
165 dtype=int,
166 default=9,
167 )
168 zenithRange = pexConfig.ListField(
169 doc="Zenith angle range (degree)",
170 dtype=float,
171 default=[0.0, 70.0],
172 )
173 zenithSteps = pexConfig.Field(
174 doc="Zenith angle number of steps",
175 dtype=int,
176 default=21,
177 )
178 # Note that the standard atmosphere parameters depend on the observatory
179 # and elevation, and so these should be set on a per-camera basis.
180 pmbStd = pexConfig.Field(
181 doc=("Standard Atmosphere pressure (millibar); "
182 "Recommended default depends on the site."),
183 dtype=float,
184 default=None,
185 )
186 pwvStd = pexConfig.Field(
187 doc=("Standard Atmosphere PWV (mm); "
188 "Recommended default depends on the site."),
189 dtype=float,
190 default=None,
191 )
192 o3Std = pexConfig.Field(
193 doc="Standard Atmosphere O3 (dob)",
194 dtype=float,
195 default=263.0,
196 )
197 tauStd = pexConfig.Field(
198 doc="Standard Atmosphere aerosol optical depth",
199 dtype=float,
200 default=0.03,
201 )
202 alphaStd = pexConfig.Field(
203 doc="Standard Atmosphere aerosol alpha",
204 dtype=float,
205 default=1.0,
206 )
207 airmassStd = pexConfig.Field(
208 doc=("Standard Atmosphere airmass; "
209 "Recommended default depends on the survey strategy."),
210 dtype=float,
211 default=None,
212 )
213 lambdaNorm = pexConfig.Field(
214 doc="Aerosol Optical Depth normalization wavelength (Angstrom)",
215 dtype=float,
216 default=7750.0,
217 )
218 lambdaStep = pexConfig.Field(
219 doc="Wavelength step for generating atmospheres (nm)",
220 dtype=float,
221 default=0.5,
222 )
223 lambdaRange = pexConfig.ListField(
224 doc="Wavelength range for LUT (Angstrom)",
225 dtype=float,
226 default=[3000.0, 11000.0],
227 )
230class FgcmMakeLutConfig(pipeBase.PipelineTaskConfig,
231 pipelineConnections=FgcmMakeLutConnections):
232 """Config for FgcmMakeLutTask"""
233 physicalFilters = pexConfig.ListField(
234 doc="List of physicalFilter labels to generate look-up table.",
235 dtype=str,
236 default=[],
237 )
238 stdPhysicalFilterOverrideMap = pexConfig.DictField(
239 doc=("Override mapping from physical filter labels to 'standard' physical "
240 "filter labels. The 'standard' physical filter defines the transmission "
241 "curve that the FGCM standard bandpass will be based on. "
242 "Any filter not listed here will be mapped to "
243 "itself (e.g. g->g or HSC-G->HSC-G). Use this override for cross-"
244 "filter calibration such as HSC-R->HSC-R2 and HSC-I->HSC-I2."),
245 keytype=str,
246 itemtype=str,
247 default={},
248 )
249 atmosphereTableName = pexConfig.Field(
250 doc="FGCM name or filename of precomputed atmospheres",
251 dtype=str,
252 default=None,
253 optional=True,
254 )
255 parameters = pexConfig.ConfigField(
256 doc="Atmosphere parameters (required if no atmosphereTableName)",
257 dtype=FgcmMakeLutParametersConfig,
258 default=None,
259 check=None)
261 def validate(self):
262 """
263 Validate the config parameters.
265 This method behaves differently from the parent validate in the case
266 that atmosphereTableName is set. In this case, the config values
267 for standard values, step sizes, and ranges are loaded
268 directly from the specified atmosphereTableName.
269 """
270 # check that filterNames and stdFilterNames are okay
271 self._fields['physicalFilters'].validate(self)
272 self._fields['stdPhysicalFilterOverrideMap'].validate(self)
274 if self.atmosphereTableName is None:
275 # Validate the parameters
276 self._fields['parameters'].validate(self)
279class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner):
280 """Subclass of TaskRunner for fgcmMakeLutTask
282 fgcmMakeLutTask.run() takes one argument, the butler, and
283 does not run on any data in the repository.
284 This runner does not use any parallelization.
285 """
287 @staticmethod
288 def getTargetList(parsedCmd):
289 """
290 Return a list with one element, the butler.
291 """
292 return [parsedCmd.butler]
294 def __call__(self, butler):
295 """
296 Parameters
297 ----------
298 butler: `lsst.daf.persistence.Butler`
300 Returns
301 -------
302 exitStatus: `list` with `pipeBase.Struct`
303 exitStatus (0: success; 1: failure)
304 """
305 task = self.TaskClass(config=self.config, log=self.log)
307 exitStatus = 0
308 if self.doRaise:
309 task.runDataRef(butler)
310 else:
311 try:
312 task.runDataRef(butler)
313 except Exception as e:
314 exitStatus = 1
315 task.log.fatal("Failed: %s" % e)
316 if not isinstance(e, pipeBase.TaskError):
317 traceback.print_exc(file=sys.stderr)
319 task.writeMetadata(butler)
321 # The task does not return any results:
322 return [pipeBase.Struct(exitStatus=exitStatus)]
324 def run(self, parsedCmd):
325 """
326 Run the task, with no multiprocessing
328 Parameters
329 ----------
330 parsedCmd: ArgumentParser parsed command line
331 """
333 resultList = []
335 if self.precall(parsedCmd):
336 targetList = self.getTargetList(parsedCmd)
337 # make sure that we only get 1
338 resultList = self(targetList[0])
340 return resultList
343class FgcmMakeLutTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
344 """
345 Make Look-Up Table for FGCM.
347 This task computes a look-up-table for the range in expected atmosphere
348 variation and variation in instrumental throughput (as tracked by the
349 transmission_filter products). By pre-computing linearized integrals,
350 the FGCM fit is orders of magnitude faster for stars with a broad range
351 of colors and observing bands, yielding precision at the 1-2 mmag level.
353 Computing a LUT requires running MODTRAN or with a pre-generated
354 atmosphere table packaged with fgcm.
355 """
357 ConfigClass = FgcmMakeLutConfig
358 RunnerClass = FgcmMakeLutRunner
359 _DefaultName = "fgcmMakeLut"
361 def __init__(self, butler=None, initInputs=None, **kwargs):
362 super().__init__(**kwargs)
364 # no saving of metadata for now
365 def _getMetadataName(self):
366 return None
368 @timeMethod
369 def runDataRef(self, butler):
370 """
371 Make a Look-Up Table for FGCM
373 Parameters
374 ----------
375 butler: `lsst.daf.persistence.Butler`
377 Raises
378 ------
379 ValueError : Raised if configured filter name does not match any of the
380 available filter transmission curves.
381 """
382 camera = butler.get('camera')
383 opticsDataRef = butler.dataRef('transmission_optics')
385 sensorDataRefDict = {}
386 for detector in camera:
387 sensorDataRefDict[detector.getId()] = butler.dataRef('transmission_sensor',
388 dataId={'ccd': detector.getId()})
390 filterDataRefDict = {}
391 for physicalFilter in self.config.physicalFilters:
392 # The physical filters map directly to dataId filter names
393 # for gen2 HSC. This is the only camera that will be supported
394 # by Gen2 fgcmcal, so we do not need to worry about other cases.
395 dataRef = butler.dataRef('transmission_filter', filter=physicalFilter)
396 if not dataRef.datasetExists():
397 raise ValueError(f"Could not find transmission for filter {physicalFilter}.")
398 filterDataRefDict[physicalFilter] = dataRef
400 lutCat = self._fgcmMakeLut(camera,
401 opticsDataRef,
402 sensorDataRefDict,
403 filterDataRefDict)
404 butler.put(lutCat, 'fgcmLookUpTable')
406 def runQuantum(self, butlerQC, inputRefs, outputRefs):
407 camera = butlerQC.get(inputRefs.camera)
409 opticsDataRef = butlerQC.get(inputRefs.transmission_optics)
411 sensorRefs = butlerQC.get(inputRefs.transmission_sensor)
412 sensorDataRefDict = {sensorRef.dataId.byName()['detector']: sensorRef for
413 sensorRef in sensorRefs}
415 filterRefs = butlerQC.get(inputRefs.transmission_filter)
416 filterDataRefDict = {filterRef.dataId['physical_filter']: filterRef for
417 filterRef in filterRefs}
419 lutCat = self._fgcmMakeLut(camera,
420 opticsDataRef,
421 sensorDataRefDict,
422 filterDataRefDict)
423 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable)
425 def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict,
426 filterDataRefDict):
427 """
428 Make a FGCM Look-up Table
430 Parameters
431 ----------
432 camera : `lsst.afw.cameraGeom.Camera`
433 Camera from the butler.
434 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
435 `lsst.daf.butler.DeferredDatasetHandle`
436 Reference to optics transmission curve.
437 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
438 `lsst.daf.butler.DeferredDatasetHandle`]
439 Dictionary of references to sensor transmission curves. Key will
440 be detector id.
441 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
442 `lsst.daf.butler.DeferredDatasetHandle`]
443 Dictionary of references to filter transmission curves. Key will
444 be physical filter label.
446 Returns
447 -------
448 fgcmLookUpTable : `BaseCatalog`
449 The FGCM look-up table.
450 """
451 # number of ccds from the length of the camera iterator
452 nCcd = len(camera)
453 self.log.info("Found %d ccds for look-up table" % (nCcd))
455 # Load in optics, etc.
456 self._loadThroughputs(camera,
457 opticsDataRef,
458 sensorDataRefDict,
459 filterDataRefDict)
461 lutConfig = self._createLutConfig(nCcd)
463 # make the lut object
464 self.log.info("Making the LUT maker object")
465 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig)
467 # generate the throughput dictionary.
469 # these will be in Angstroms
470 # note that lambdaStep is currently in nm, because of historical
471 # reasons in the code. Convert to Angstroms here.
472 throughputLambda = np.arange(self.fgcmLutMaker.lambdaRange[0],
473 self.fgcmLutMaker.lambdaRange[1]+self.fgcmLutMaker.lambdaStep*10,
474 self.fgcmLutMaker.lambdaStep*10.)
476 self.log.info("Built throughput lambda, %.1f-%.1f, step %.2f" %
477 (throughputLambda[0], throughputLambda[-1],
478 throughputLambda[1] - throughputLambda[0]))
480 throughputDict = {}
481 for i, physicalFilter in enumerate(self.config.physicalFilters):
482 tDict = {}
483 tDict['LAMBDA'] = throughputLambda
484 for ccdIndex, detector in enumerate(camera):
485 tDict[ccdIndex] = self._getThroughputDetector(detector, physicalFilter, throughputLambda)
486 throughputDict[physicalFilter] = tDict
488 # set the throughputs
489 self.fgcmLutMaker.setThroughputs(throughputDict)
491 # make the LUT
492 self.log.info("Making LUT")
493 self.fgcmLutMaker.makeLUT()
495 # and save the LUT
497 # build the index values
498 comma = ','
499 physicalFilterString = comma.join(self.config.physicalFilters)
500 stdPhysicalFilterString = comma.join(self._getStdPhysicalFilterList())
502 atmosphereTableName = 'NoTableWasUsed'
503 if self.config.atmosphereTableName is not None:
504 atmosphereTableName = self.config.atmosphereTableName
506 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString,
507 atmosphereTableName)
509 lutCat = self._makeLutCat(lutSchema, physicalFilterString,
510 stdPhysicalFilterString, atmosphereTableName)
511 return lutCat
513 def _getStdPhysicalFilterList(self):
514 """Get the standard physical filter lists from config.physicalFilters
515 and config.stdPhysicalFilterOverrideMap
517 Returns
518 -------
519 stdPhysicalFilters : `list`
520 """
521 override = self.config.stdPhysicalFilterOverrideMap
522 return [override.get(physicalFilter, physicalFilter) for
523 physicalFilter in self.config.physicalFilters]
525 def _createLutConfig(self, nCcd):
526 """
527 Create the fgcmLut config dictionary
529 Parameters
530 ----------
531 nCcd: `int`
532 Number of CCDs in the camera
533 """
535 # create the common stub of the lutConfig
536 lutConfig = {}
537 lutConfig['logger'] = self.log
538 lutConfig['filterNames'] = self.config.physicalFilters
539 lutConfig['stdFilterNames'] = self._getStdPhysicalFilterList()
540 lutConfig['nCCD'] = nCcd
542 # atmosphereTable already validated if available
543 if self.config.atmosphereTableName is not None:
544 lutConfig['atmosphereTableName'] = self.config.atmosphereTableName
545 else:
546 # use the regular paramters (also validated if needed)
547 lutConfig['elevation'] = self.config.parameters.elevation
548 lutConfig['pmbRange'] = self.config.parameters.pmbRange
549 lutConfig['pmbSteps'] = self.config.parameters.pmbSteps
550 lutConfig['pwvRange'] = self.config.parameters.pwvRange
551 lutConfig['pwvSteps'] = self.config.parameters.pwvSteps
552 lutConfig['o3Range'] = self.config.parameters.o3Range
553 lutConfig['o3Steps'] = self.config.parameters.o3Steps
554 lutConfig['tauRange'] = self.config.parameters.tauRange
555 lutConfig['tauSteps'] = self.config.parameters.tauSteps
556 lutConfig['alphaRange'] = self.config.parameters.alphaRange
557 lutConfig['alphaSteps'] = self.config.parameters.alphaSteps
558 lutConfig['zenithRange'] = self.config.parameters.zenithRange
559 lutConfig['zenithSteps'] = self.config.parameters.zenithSteps
560 lutConfig['pmbStd'] = self.config.parameters.pmbStd
561 lutConfig['pwvStd'] = self.config.parameters.pwvStd
562 lutConfig['o3Std'] = self.config.parameters.o3Std
563 lutConfig['tauStd'] = self.config.parameters.tauStd
564 lutConfig['alphaStd'] = self.config.parameters.alphaStd
565 lutConfig['airmassStd'] = self.config.parameters.airmassStd
566 lutConfig['lambdaRange'] = self.config.parameters.lambdaRange
567 lutConfig['lambdaStep'] = self.config.parameters.lambdaStep
568 lutConfig['lambdaNorm'] = self.config.parameters.lambdaNorm
570 return lutConfig
572 def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict):
573 """Internal method to load throughput data for filters
575 Parameters
576 ----------
577 camera: `lsst.afw.cameraGeom.Camera`
578 Camera from the butler
579 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
580 `lsst.daf.butler.DeferredDatasetHandle`
581 Reference to optics transmission curve.
582 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
583 `lsst.daf.butler.DeferredDatasetHandle`]
584 Dictionary of references to sensor transmission curves. Key will
585 be detector id.
586 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
587 `lsst.daf.butler.DeferredDatasetHandle`]
588 Dictionary of references to filter transmission curves. Key will
589 be physical filter label.
591 Raises
592 ------
593 ValueError : Raised if configured filter name does not match any of the
594 available filter transmission curves.
595 """
596 self._opticsTransmission = opticsDataRef.get()
598 self._sensorsTransmission = {}
599 for detector in camera:
600 self._sensorsTransmission[detector.getId()] = sensorDataRefDict[detector.getId()].get()
602 self._filtersTransmission = {}
603 for physicalFilter in self.config.physicalFilters:
604 self._filtersTransmission[physicalFilter] = filterDataRefDict[physicalFilter].get()
606 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda):
607 """Internal method to get throughput for a detector.
609 Returns the throughput at the center of the detector for a given filter.
611 Parameters
612 ----------
613 detector: `lsst.afw.cameraGeom._detector.Detector`
614 Detector on camera
615 physicalFilter: `str`
616 Physical filter label
617 throughputLambda: `np.array(dtype=np.float64)`
618 Wavelength steps (Angstrom)
620 Returns
621 -------
622 throughput: `np.array(dtype=np.float64)`
623 Throughput (max 1.0) at throughputLambda
624 """
626 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
627 c.scale(1.0/detector.getPixelSize()[0]) # Assumes x and y pixel sizes in arcsec are the same
629 throughput = self._opticsTransmission.sampleAt(position=c,
630 wavelengths=throughputLambda)
632 throughput *= self._sensorsTransmission[detector.getId()].sampleAt(position=c,
633 wavelengths=throughputLambda)
635 throughput *= self._filtersTransmission[physicalFilter].sampleAt(position=c,
636 wavelengths=throughputLambda)
638 # Clip the throughput from 0 to 1
639 throughput = np.clip(throughput, 0.0, 1.0)
641 return throughput
643 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString,
644 atmosphereTableName):
645 """
646 Make the LUT schema
648 Parameters
649 ----------
650 physicalFilterString: `str`
651 Combined string of all the physicalFilters
652 stdPhysicalFilterString: `str`
653 Combined string of all the standard physicalFilters
654 atmosphereTableName: `str`
655 Name of the atmosphere table used to generate LUT
657 Returns
658 -------
659 lutSchema: `afwTable.schema`
660 """
662 lutSchema = afwTable.Schema()
664 lutSchema.addField('tablename', type=str, doc='Atmosphere table name',
665 size=len(atmosphereTableName))
666 lutSchema.addField('elevation', type=float, doc="Telescope elevation used for LUT")
667 lutSchema.addField('physicalFilters', type=str, doc='physicalFilters in LUT',
668 size=len(physicalFilterString))
669 lutSchema.addField('stdPhysicalFilters', type=str, doc='Standard physicalFilters in LUT',
670 size=len(stdPhysicalFilterString))
671 lutSchema.addField('pmb', type='ArrayD', doc='Barometric Pressure',
672 size=self.fgcmLutMaker.pmb.size)
673 lutSchema.addField('pmbFactor', type='ArrayD', doc='PMB scaling factor',
674 size=self.fgcmLutMaker.pmb.size)
675 lutSchema.addField('pmbElevation', type=np.float64, doc='PMB Scaling at elevation')
676 lutSchema.addField('pwv', type='ArrayD', doc='Preciptable Water Vapor',
677 size=self.fgcmLutMaker.pwv.size)
678 lutSchema.addField('o3', type='ArrayD', doc='Ozone',
679 size=self.fgcmLutMaker.o3.size)
680 lutSchema.addField('tau', type='ArrayD', doc='Aerosol optical depth',
681 size=self.fgcmLutMaker.tau.size)
682 lutSchema.addField('lambdaNorm', type=np.float64, doc='AOD wavelength')
683 lutSchema.addField('alpha', type='ArrayD', doc='Aerosol alpha',
684 size=self.fgcmLutMaker.alpha.size)
685 lutSchema.addField('zenith', type='ArrayD', doc='Zenith angle',
686 size=self.fgcmLutMaker.zenith.size)
687 lutSchema.addField('nCcd', type=np.int32, doc='Number of CCDs')
689 # and the standard values
690 lutSchema.addField('pmbStd', type=np.float64, doc='PMB Standard')
691 lutSchema.addField('pwvStd', type=np.float64, doc='PWV Standard')
692 lutSchema.addField('o3Std', type=np.float64, doc='O3 Standard')
693 lutSchema.addField('tauStd', type=np.float64, doc='Tau Standard')
694 lutSchema.addField('alphaStd', type=np.float64, doc='Alpha Standard')
695 lutSchema.addField('zenithStd', type=np.float64, doc='Zenith angle Standard')
696 lutSchema.addField('lambdaRange', type='ArrayD', doc='Wavelength range',
697 size=2)
698 lutSchema.addField('lambdaStep', type=np.float64, doc='Wavelength step')
699 lutSchema.addField('lambdaStd', type='ArrayD', doc='Standard Wavelength',
700 size=len(self.fgcmLutMaker.filterNames))
701 lutSchema.addField('lambdaStdFilter', type='ArrayD', doc='Standard Wavelength (raw)',
702 size=len(self.fgcmLutMaker.filterNames))
703 lutSchema.addField('i0Std', type='ArrayD', doc='I0 Standard',
704 size=len(self.fgcmLutMaker.filterNames))
705 lutSchema.addField('i1Std', type='ArrayD', doc='I1 Standard',
706 size=len(self.fgcmLutMaker.filterNames))
707 lutSchema.addField('i10Std', type='ArrayD', doc='I10 Standard',
708 size=len(self.fgcmLutMaker.filterNames))
709 lutSchema.addField('i2Std', type='ArrayD', doc='I2 Standard',
710 size=len(self.fgcmLutMaker.filterNames))
711 lutSchema.addField('lambdaB', type='ArrayD', doc='Wavelength for passband (no atm)',
712 size=len(self.fgcmLutMaker.filterNames))
713 lutSchema.addField('atmLambda', type='ArrayD', doc='Atmosphere wavelengths (Angstrom)',
714 size=self.fgcmLutMaker.atmLambda.size)
715 lutSchema.addField('atmStdTrans', type='ArrayD', doc='Standard Atmosphere Throughput',
716 size=self.fgcmLutMaker.atmStdTrans.size)
718 # and the look-up-tables
719 lutSchema.addField('luttype', type=str, size=20, doc='Look-up table type')
720 lutSchema.addField('lut', type='ArrayF', doc='Look-up table for luttype',
721 size=self.fgcmLutMaker.lut['I0'].size)
723 return lutSchema
725 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString,
726 atmosphereTableName):
727 """
728 Make the LUT schema
730 Parameters
731 ----------
732 lutSchema: `afwTable.schema`
733 Lut catalog schema
734 physicalFilterString: `str`
735 Combined string of all the physicalFilters
736 stdPhysicalFilterString: `str`
737 Combined string of all the standard physicalFilters
738 atmosphereTableName: `str`
739 Name of the atmosphere table used to generate LUT
741 Returns
742 -------
743 lutCat: `afwTable.BaseCatalog`
744 Lut catalog for persistence
745 """
747 # The somewhat strange format is to make sure that
748 # the rows of the afwTable do not get too large
749 # (see DM-11419)
751 lutCat = afwTable.BaseCatalog(lutSchema)
752 lutCat.table.preallocate(14)
754 # first fill the first index
755 rec = lutCat.addNew()
757 rec['tablename'] = atmosphereTableName
758 rec['elevation'] = self.fgcmLutMaker.atmosphereTable.elevation
759 rec['physicalFilters'] = physicalFilterString
760 rec['stdPhysicalFilters'] = stdPhysicalFilterString
761 rec['pmb'][:] = self.fgcmLutMaker.pmb
762 rec['pmbFactor'][:] = self.fgcmLutMaker.pmbFactor
763 rec['pmbElevation'] = self.fgcmLutMaker.pmbElevation
764 rec['pwv'][:] = self.fgcmLutMaker.pwv
765 rec['o3'][:] = self.fgcmLutMaker.o3
766 rec['tau'][:] = self.fgcmLutMaker.tau
767 rec['lambdaNorm'] = self.fgcmLutMaker.lambdaNorm
768 rec['alpha'][:] = self.fgcmLutMaker.alpha
769 rec['zenith'][:] = self.fgcmLutMaker.zenith
770 rec['nCcd'] = self.fgcmLutMaker.nCCD
772 rec['pmbStd'] = self.fgcmLutMaker.pmbStd
773 rec['pwvStd'] = self.fgcmLutMaker.pwvStd
774 rec['o3Std'] = self.fgcmLutMaker.o3Std
775 rec['tauStd'] = self.fgcmLutMaker.tauStd
776 rec['alphaStd'] = self.fgcmLutMaker.alphaStd
777 rec['zenithStd'] = self.fgcmLutMaker.zenithStd
778 rec['lambdaRange'][:] = self.fgcmLutMaker.lambdaRange
779 rec['lambdaStep'] = self.fgcmLutMaker.lambdaStep
780 rec['lambdaStd'][:] = self.fgcmLutMaker.lambdaStd
781 rec['lambdaStdFilter'][:] = self.fgcmLutMaker.lambdaStdFilter
782 rec['i0Std'][:] = self.fgcmLutMaker.I0Std
783 rec['i1Std'][:] = self.fgcmLutMaker.I1Std
784 rec['i10Std'][:] = self.fgcmLutMaker.I10Std
785 rec['i2Std'][:] = self.fgcmLutMaker.I2Std
786 rec['lambdaB'][:] = self.fgcmLutMaker.lambdaB
787 rec['atmLambda'][:] = self.fgcmLutMaker.atmLambda
788 rec['atmStdTrans'][:] = self.fgcmLutMaker.atmStdTrans
790 rec['luttype'] = 'I0'
791 rec['lut'][:] = self.fgcmLutMaker.lut['I0'].flatten()
793 # and add the rest
794 rec = lutCat.addNew()
795 rec['luttype'] = 'I1'
796 rec['lut'][:] = self.fgcmLutMaker.lut['I1'].flatten()
798 derivTypes = ['D_PMB', 'D_LNPWV', 'D_O3', 'D_LNTAU', 'D_ALPHA', 'D_SECZENITH',
799 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1',
800 'D_SECZENITH_I1']
801 for derivType in derivTypes:
802 rec = lutCat.addNew()
803 rec['luttype'] = derivType
804 rec['lut'][:] = self.fgcmLutMaker.lutDeriv[derivType].flatten()
806 return lutCat