Coverage for python/lsst/fgcmcal/fgcmMakeLut.py : 23%

Hot-keys 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
40from lsst.obs.base import Instrument
41import lsst.pex.config as pexConfig
42import lsst.pipe.base as pipeBase
43from lsst.pipe.base import connectionTypes
44import lsst.afw.table as afwTable
45import lsst.afw.cameraGeom as afwCameraGeom
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 @pipeBase.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 # Instantiate the instrument to load filter information
410 _ = Instrument.fromName(inputRefs.camera.dataId['instrument'],
411 butlerQC.registry)
412 opticsDataRef = butlerQC.get(inputRefs.transmission_optics)
414 sensorRefs = butlerQC.get(inputRefs.transmission_sensor)
415 sensorDataRefDict = {sensorRef.dataId.byName()['detector']: sensorRef for
416 sensorRef in sensorRefs}
418 filterRefs = butlerQC.get(inputRefs.transmission_filter)
419 filterDataRefDict = {filterRef.dataId['physical_filter']: filterRef for
420 filterRef in filterRefs}
422 lutCat = self._fgcmMakeLut(camera,
423 opticsDataRef,
424 sensorDataRefDict,
425 filterDataRefDict)
426 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable)
428 def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict,
429 filterDataRefDict):
430 """
431 Make a FGCM Look-up Table
433 Parameters
434 ----------
435 camera : `lsst.afw.cameraGeom.Camera`
436 Camera from the butler.
437 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
438 `lsst.daf.butler.DeferredDatasetHandle`
439 Reference to optics transmission curve.
440 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
441 `lsst.daf.butler.DeferredDatasetHandle`]
442 Dictionary of references to sensor transmission curves. Key will
443 be detector id.
444 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
445 `lsst.daf.butler.DeferredDatasetHandle`]
446 Dictionary of references to filter transmission curves. Key will
447 be physical filter label.
449 Returns
450 -------
451 fgcmLookUpTable : `BaseCatalog`
452 The FGCM look-up table.
453 """
454 # number of ccds from the length of the camera iterator
455 nCcd = len(camera)
456 self.log.info("Found %d ccds for look-up table" % (nCcd))
458 # Load in optics, etc.
459 self._loadThroughputs(camera,
460 opticsDataRef,
461 sensorDataRefDict,
462 filterDataRefDict)
464 lutConfig = self._createLutConfig(nCcd)
466 # make the lut object
467 self.log.info("Making the LUT maker object")
468 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig)
470 # generate the throughput dictionary.
472 # these will be in Angstroms
473 # note that lambdaStep is currently in nm, because of historical
474 # reasons in the code. Convert to Angstroms here.
475 throughputLambda = np.arange(self.fgcmLutMaker.lambdaRange[0],
476 self.fgcmLutMaker.lambdaRange[1]+self.fgcmLutMaker.lambdaStep*10,
477 self.fgcmLutMaker.lambdaStep*10.)
479 self.log.info("Built throughput lambda, %.1f-%.1f, step %.2f" %
480 (throughputLambda[0], throughputLambda[-1],
481 throughputLambda[1] - throughputLambda[0]))
483 throughputDict = {}
484 for i, physicalFilter in enumerate(self.config.physicalFilters):
485 tDict = {}
486 tDict['LAMBDA'] = throughputLambda
487 for ccdIndex, detector in enumerate(camera):
488 tDict[ccdIndex] = self._getThroughputDetector(detector, physicalFilter, throughputLambda)
489 throughputDict[physicalFilter] = tDict
491 # set the throughputs
492 self.fgcmLutMaker.setThroughputs(throughputDict)
494 # make the LUT
495 self.log.info("Making LUT")
496 self.fgcmLutMaker.makeLUT()
498 # and save the LUT
500 # build the index values
501 comma = ','
502 physicalFilterString = comma.join(self.config.physicalFilters)
503 stdPhysicalFilterString = comma.join(self._getStdPhysicalFilterList())
505 atmosphereTableName = 'NoTableWasUsed'
506 if self.config.atmosphereTableName is not None:
507 atmosphereTableName = self.config.atmosphereTableName
509 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString,
510 atmosphereTableName)
512 lutCat = self._makeLutCat(lutSchema, physicalFilterString,
513 stdPhysicalFilterString, atmosphereTableName)
514 return lutCat
516 def _getStdPhysicalFilterList(self):
517 """Get the standard physical filter lists from config.physicalFilters
518 and config.stdPhysicalFilterOverrideMap
520 Returns
521 -------
522 stdPhysicalFilters : `list`
523 """
524 override = self.config.stdPhysicalFilterOverrideMap
525 return [override.get(physicalFilter, physicalFilter) for
526 physicalFilter in self.config.physicalFilters]
528 def _createLutConfig(self, nCcd):
529 """
530 Create the fgcmLut config dictionary
532 Parameters
533 ----------
534 nCcd: `int`
535 Number of CCDs in the camera
536 """
538 # create the common stub of the lutConfig
539 lutConfig = {}
540 lutConfig['logger'] = self.log
541 lutConfig['filterNames'] = self.config.physicalFilters
542 lutConfig['stdFilterNames'] = self._getStdPhysicalFilterList()
543 lutConfig['nCCD'] = nCcd
545 # atmosphereTable already validated if available
546 if self.config.atmosphereTableName is not None:
547 lutConfig['atmosphereTableName'] = self.config.atmosphereTableName
548 else:
549 # use the regular paramters (also validated if needed)
550 lutConfig['elevation'] = self.config.parameters.elevation
551 lutConfig['pmbRange'] = self.config.parameters.pmbRange
552 lutConfig['pmbSteps'] = self.config.parameters.pmbSteps
553 lutConfig['pwvRange'] = self.config.parameters.pwvRange
554 lutConfig['pwvSteps'] = self.config.parameters.pwvSteps
555 lutConfig['o3Range'] = self.config.parameters.o3Range
556 lutConfig['o3Steps'] = self.config.parameters.o3Steps
557 lutConfig['tauRange'] = self.config.parameters.tauRange
558 lutConfig['tauSteps'] = self.config.parameters.tauSteps
559 lutConfig['alphaRange'] = self.config.parameters.alphaRange
560 lutConfig['alphaSteps'] = self.config.parameters.alphaSteps
561 lutConfig['zenithRange'] = self.config.parameters.zenithRange
562 lutConfig['zenithSteps'] = self.config.parameters.zenithSteps
563 lutConfig['pmbStd'] = self.config.parameters.pmbStd
564 lutConfig['pwvStd'] = self.config.parameters.pwvStd
565 lutConfig['o3Std'] = self.config.parameters.o3Std
566 lutConfig['tauStd'] = self.config.parameters.tauStd
567 lutConfig['alphaStd'] = self.config.parameters.alphaStd
568 lutConfig['airmassStd'] = self.config.parameters.airmassStd
569 lutConfig['lambdaRange'] = self.config.parameters.lambdaRange
570 lutConfig['lambdaStep'] = self.config.parameters.lambdaStep
571 lutConfig['lambdaNorm'] = self.config.parameters.lambdaNorm
573 return lutConfig
575 def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict):
576 """Internal method to load throughput data for filters
578 Parameters
579 ----------
580 camera: `lsst.afw.cameraGeom.Camera`
581 Camera from the butler
582 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
583 `lsst.daf.butler.DeferredDatasetHandle`
584 Reference to optics transmission curve.
585 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
586 `lsst.daf.butler.DeferredDatasetHandle`]
587 Dictionary of references to sensor transmission curves. Key will
588 be detector id.
589 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
590 `lsst.daf.butler.DeferredDatasetHandle`]
591 Dictionary of references to filter transmission curves. Key will
592 be physical filter label.
594 Raises
595 ------
596 ValueError : Raised if configured filter name does not match any of the
597 available filter transmission curves.
598 """
599 self._opticsTransmission = opticsDataRef.get()
601 self._sensorsTransmission = {}
602 for detector in camera:
603 self._sensorsTransmission[detector.getId()] = sensorDataRefDict[detector.getId()].get()
605 self._filtersTransmission = {}
606 for physicalFilter in self.config.physicalFilters:
607 self._filtersTransmission[physicalFilter] = filterDataRefDict[physicalFilter].get()
609 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda):
610 """Internal method to get throughput for a detector.
612 Returns the throughput at the center of the detector for a given filter.
614 Parameters
615 ----------
616 detector: `lsst.afw.cameraGeom._detector.Detector`
617 Detector on camera
618 physicalFilter: `str`
619 Physical filter label
620 throughputLambda: `np.array(dtype=np.float64)`
621 Wavelength steps (Angstrom)
623 Returns
624 -------
625 throughput: `np.array(dtype=np.float64)`
626 Throughput (max 1.0) at throughputLambda
627 """
629 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
630 c.scale(1.0/detector.getPixelSize()[0]) # Assumes x and y pixel sizes in arcsec are the same
632 throughput = self._opticsTransmission.sampleAt(position=c,
633 wavelengths=throughputLambda)
635 throughput *= self._sensorsTransmission[detector.getId()].sampleAt(position=c,
636 wavelengths=throughputLambda)
638 throughput *= self._filtersTransmission[physicalFilter].sampleAt(position=c,
639 wavelengths=throughputLambda)
641 # Clip the throughput from 0 to 1
642 throughput = np.clip(throughput, 0.0, 1.0)
644 return throughput
646 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString,
647 atmosphereTableName):
648 """
649 Make the LUT schema
651 Parameters
652 ----------
653 physicalFilterString: `str`
654 Combined string of all the physicalFilters
655 stdPhysicalFilterString: `str`
656 Combined string of all the standard physicalFilters
657 atmosphereTableName: `str`
658 Name of the atmosphere table used to generate LUT
660 Returns
661 -------
662 lutSchema: `afwTable.schema`
663 """
665 lutSchema = afwTable.Schema()
667 lutSchema.addField('tablename', type=str, doc='Atmosphere table name',
668 size=len(atmosphereTableName))
669 lutSchema.addField('elevation', type=float, doc="Telescope elevation used for LUT")
670 lutSchema.addField('physicalFilters', type=str, doc='physicalFilters in LUT',
671 size=len(physicalFilterString))
672 lutSchema.addField('stdPhysicalFilters', type=str, doc='Standard physicalFilters in LUT',
673 size=len(stdPhysicalFilterString))
674 lutSchema.addField('pmb', type='ArrayD', doc='Barometric Pressure',
675 size=self.fgcmLutMaker.pmb.size)
676 lutSchema.addField('pmbFactor', type='ArrayD', doc='PMB scaling factor',
677 size=self.fgcmLutMaker.pmb.size)
678 lutSchema.addField('pmbElevation', type=np.float64, doc='PMB Scaling at elevation')
679 lutSchema.addField('pwv', type='ArrayD', doc='Preciptable Water Vapor',
680 size=self.fgcmLutMaker.pwv.size)
681 lutSchema.addField('o3', type='ArrayD', doc='Ozone',
682 size=self.fgcmLutMaker.o3.size)
683 lutSchema.addField('tau', type='ArrayD', doc='Aerosol optical depth',
684 size=self.fgcmLutMaker.tau.size)
685 lutSchema.addField('lambdaNorm', type=np.float64, doc='AOD wavelength')
686 lutSchema.addField('alpha', type='ArrayD', doc='Aerosol alpha',
687 size=self.fgcmLutMaker.alpha.size)
688 lutSchema.addField('zenith', type='ArrayD', doc='Zenith angle',
689 size=self.fgcmLutMaker.zenith.size)
690 lutSchema.addField('nCcd', type=np.int32, doc='Number of CCDs')
692 # and the standard values
693 lutSchema.addField('pmbStd', type=np.float64, doc='PMB Standard')
694 lutSchema.addField('pwvStd', type=np.float64, doc='PWV Standard')
695 lutSchema.addField('o3Std', type=np.float64, doc='O3 Standard')
696 lutSchema.addField('tauStd', type=np.float64, doc='Tau Standard')
697 lutSchema.addField('alphaStd', type=np.float64, doc='Alpha Standard')
698 lutSchema.addField('zenithStd', type=np.float64, doc='Zenith angle Standard')
699 lutSchema.addField('lambdaRange', type='ArrayD', doc='Wavelength range',
700 size=2)
701 lutSchema.addField('lambdaStep', type=np.float64, doc='Wavelength step')
702 lutSchema.addField('lambdaStd', type='ArrayD', doc='Standard Wavelength',
703 size=len(self.fgcmLutMaker.filterNames))
704 lutSchema.addField('lambdaStdFilter', type='ArrayD', doc='Standard Wavelength (raw)',
705 size=len(self.fgcmLutMaker.filterNames))
706 lutSchema.addField('i0Std', type='ArrayD', doc='I0 Standard',
707 size=len(self.fgcmLutMaker.filterNames))
708 lutSchema.addField('i1Std', type='ArrayD', doc='I1 Standard',
709 size=len(self.fgcmLutMaker.filterNames))
710 lutSchema.addField('i10Std', type='ArrayD', doc='I10 Standard',
711 size=len(self.fgcmLutMaker.filterNames))
712 lutSchema.addField('i2Std', type='ArrayD', doc='I2 Standard',
713 size=len(self.fgcmLutMaker.filterNames))
714 lutSchema.addField('lambdaB', type='ArrayD', doc='Wavelength for passband (no atm)',
715 size=len(self.fgcmLutMaker.filterNames))
716 lutSchema.addField('atmLambda', type='ArrayD', doc='Atmosphere wavelengths (Angstrom)',
717 size=self.fgcmLutMaker.atmLambda.size)
718 lutSchema.addField('atmStdTrans', type='ArrayD', doc='Standard Atmosphere Throughput',
719 size=self.fgcmLutMaker.atmStdTrans.size)
721 # and the look-up-tables
722 lutSchema.addField('luttype', type=str, size=20, doc='Look-up table type')
723 lutSchema.addField('lut', type='ArrayF', doc='Look-up table for luttype',
724 size=self.fgcmLutMaker.lut['I0'].size)
726 return lutSchema
728 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString,
729 atmosphereTableName):
730 """
731 Make the LUT schema
733 Parameters
734 ----------
735 lutSchema: `afwTable.schema`
736 Lut catalog schema
737 physicalFilterString: `str`
738 Combined string of all the physicalFilters
739 stdPhysicalFilterString: `str`
740 Combined string of all the standard physicalFilters
741 atmosphereTableName: `str`
742 Name of the atmosphere table used to generate LUT
744 Returns
745 -------
746 lutCat: `afwTable.BaseCatalog`
747 Lut catalog for persistence
748 """
750 # The somewhat strange format is to make sure that
751 # the rows of the afwTable do not get too large
752 # (see DM-11419)
754 lutCat = afwTable.BaseCatalog(lutSchema)
755 lutCat.table.preallocate(14)
757 # first fill the first index
758 rec = lutCat.addNew()
760 rec['tablename'] = atmosphereTableName
761 rec['elevation'] = self.fgcmLutMaker.atmosphereTable.elevation
762 rec['physicalFilters'] = physicalFilterString
763 rec['stdPhysicalFilters'] = stdPhysicalFilterString
764 rec['pmb'][:] = self.fgcmLutMaker.pmb
765 rec['pmbFactor'][:] = self.fgcmLutMaker.pmbFactor
766 rec['pmbElevation'] = self.fgcmLutMaker.pmbElevation
767 rec['pwv'][:] = self.fgcmLutMaker.pwv
768 rec['o3'][:] = self.fgcmLutMaker.o3
769 rec['tau'][:] = self.fgcmLutMaker.tau
770 rec['lambdaNorm'] = self.fgcmLutMaker.lambdaNorm
771 rec['alpha'][:] = self.fgcmLutMaker.alpha
772 rec['zenith'][:] = self.fgcmLutMaker.zenith
773 rec['nCcd'] = self.fgcmLutMaker.nCCD
775 rec['pmbStd'] = self.fgcmLutMaker.pmbStd
776 rec['pwvStd'] = self.fgcmLutMaker.pwvStd
777 rec['o3Std'] = self.fgcmLutMaker.o3Std
778 rec['tauStd'] = self.fgcmLutMaker.tauStd
779 rec['alphaStd'] = self.fgcmLutMaker.alphaStd
780 rec['zenithStd'] = self.fgcmLutMaker.zenithStd
781 rec['lambdaRange'][:] = self.fgcmLutMaker.lambdaRange
782 rec['lambdaStep'] = self.fgcmLutMaker.lambdaStep
783 rec['lambdaStd'][:] = self.fgcmLutMaker.lambdaStd
784 rec['lambdaStdFilter'][:] = self.fgcmLutMaker.lambdaStdFilter
785 rec['i0Std'][:] = self.fgcmLutMaker.I0Std
786 rec['i1Std'][:] = self.fgcmLutMaker.I1Std
787 rec['i10Std'][:] = self.fgcmLutMaker.I10Std
788 rec['i2Std'][:] = self.fgcmLutMaker.I2Std
789 rec['lambdaB'][:] = self.fgcmLutMaker.lambdaB
790 rec['atmLambda'][:] = self.fgcmLutMaker.atmLambda
791 rec['atmStdTrans'][:] = self.fgcmLutMaker.atmStdTrans
793 rec['luttype'] = 'I0'
794 rec['lut'][:] = self.fgcmLutMaker.lut['I0'].flatten()
796 # and add the rest
797 rec = lutCat.addNew()
798 rec['luttype'] = 'I1'
799 rec['lut'][:] = self.fgcmLutMaker.lut['I1'].flatten()
801 derivTypes = ['D_PMB', 'D_LNPWV', 'D_O3', 'D_LNTAU', 'D_ALPHA', 'D_SECZENITH',
802 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1',
803 'D_SECZENITH_I1']
804 for derivType in derivTypes:
805 rec = lutCat.addNew()
806 rec['luttype'] = derivType
807 rec['lut'][:] = self.fgcmLutMaker.lutDeriv[derivType].flatten()
809 return lutCat