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

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 lsst.afw.image import Filter
47from .utilities import lookupStaticCalibrations
49import fgcm
51__all__ = ['FgcmMakeLutParametersConfig', 'FgcmMakeLutConfig', 'FgcmMakeLutTask',
52 'FgcmMakeLutRunner']
55class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections,
56 dimensions=('instrument',),
57 defaultTemplates={}):
58 camera = connectionTypes.PrerequisiteInput(
59 doc="Camera instrument",
60 name="camera",
61 storageClass="Camera",
62 dimensions=("instrument",),
63 lookupFunction=lookupStaticCalibrations,
64 isCalibration=True,
65 )
67 transmission_optics = connectionTypes.PrerequisiteInput(
68 doc="Optics transmission curve information",
69 name="transmission_optics",
70 storageClass="TransmissionCurve",
71 dimensions=("instrument",),
72 lookupFunction=lookupStaticCalibrations,
73 isCalibration=True,
74 deferLoad=True,
75 )
77 transmission_sensor = connectionTypes.PrerequisiteInput(
78 doc="Sensor transmission curve information",
79 name="transmission_sensor",
80 storageClass="TransmissionCurve",
81 dimensions=("instrument", "detector",),
82 lookupFunction=lookupStaticCalibrations,
83 isCalibration=True,
84 deferLoad=True,
85 multiple=True,
86 )
88 transmission_filter = connectionTypes.PrerequisiteInput(
89 doc="Filter transmission curve information",
90 name="transmission_filter",
91 storageClass="TransmissionCurve",
92 dimensions=("band", "instrument", "physical_filter",),
93 lookupFunction=lookupStaticCalibrations,
94 isCalibration=True,
95 deferLoad=True,
96 multiple=True,
97 )
99 fgcmLookUpTable = connectionTypes.Output(
100 doc=("Atmosphere + instrument look-up-table for FGCM throughput and "
101 "chromatic corrections."),
102 name="fgcmLookUpTable",
103 storageClass="Catalog",
104 dimensions=("instrument",),
105 )
108class FgcmMakeLutParametersConfig(pexConfig.Config):
109 """Config for parameters if atmosphereTableName not available"""
110 # TODO: When DM-16511 is done, it will be possible to get the
111 # telescope elevation directly from the camera.
112 elevation = pexConfig.Field(
113 doc="Telescope elevation (m)",
114 dtype=float,
115 default=None,
116 )
117 pmbRange = pexConfig.ListField(
118 doc=("Barometric Pressure range (millibar) "
119 "Recommended range depends on the site."),
120 dtype=float,
121 default=None,
122 )
123 pmbSteps = pexConfig.Field(
124 doc="Barometric Pressure number of steps",
125 dtype=int,
126 default=5,
127 )
128 pwvRange = pexConfig.ListField(
129 doc=("Precipitable Water Vapor range (mm) "
130 "Recommended range depends on the site."),
131 dtype=float,
132 default=None,
133 )
134 pwvSteps = pexConfig.Field(
135 doc="Precipitable Water Vapor number of steps",
136 dtype=int,
137 default=15,
138 )
139 o3Range = pexConfig.ListField(
140 doc="Ozone range (dob)",
141 dtype=float,
142 default=[220.0, 310.0],
143 )
144 o3Steps = pexConfig.Field(
145 doc="Ozone number of steps",
146 dtype=int,
147 default=3,
148 )
149 tauRange = pexConfig.ListField(
150 doc="Aerosol Optical Depth range (unitless)",
151 dtype=float,
152 default=[0.002, 0.35],
153 )
154 tauSteps = pexConfig.Field(
155 doc="Aerosol Optical Depth number of steps",
156 dtype=int,
157 default=11,
158 )
159 alphaRange = pexConfig.ListField(
160 doc="Aerosol alpha range (unitless)",
161 dtype=float,
162 default=[0.0, 2.0],
163 )
164 alphaSteps = pexConfig.Field(
165 doc="Aerosol alpha number of steps",
166 dtype=int,
167 default=9,
168 )
169 zenithRange = pexConfig.ListField(
170 doc="Zenith angle range (degree)",
171 dtype=float,
172 default=[0.0, 70.0],
173 )
174 zenithSteps = pexConfig.Field(
175 doc="Zenith angle number of steps",
176 dtype=int,
177 default=21,
178 )
179 # Note that the standard atmosphere parameters depend on the observatory
180 # and elevation, and so these should be set on a per-camera basis.
181 pmbStd = pexConfig.Field(
182 doc=("Standard Atmosphere pressure (millibar); "
183 "Recommended default depends on the site."),
184 dtype=float,
185 default=None,
186 )
187 pwvStd = pexConfig.Field(
188 doc=("Standard Atmosphere PWV (mm); "
189 "Recommended default depends on the site."),
190 dtype=float,
191 default=None,
192 )
193 o3Std = pexConfig.Field(
194 doc="Standard Atmosphere O3 (dob)",
195 dtype=float,
196 default=263.0,
197 )
198 tauStd = pexConfig.Field(
199 doc="Standard Atmosphere aerosol optical depth",
200 dtype=float,
201 default=0.03,
202 )
203 alphaStd = pexConfig.Field(
204 doc="Standard Atmosphere aerosol alpha",
205 dtype=float,
206 default=1.0,
207 )
208 airmassStd = pexConfig.Field(
209 doc=("Standard Atmosphere airmass; "
210 "Recommended default depends on the survey strategy."),
211 dtype=float,
212 default=None,
213 )
214 lambdaNorm = pexConfig.Field(
215 doc="Aerosol Optical Depth normalization wavelength (Angstrom)",
216 dtype=float,
217 default=7750.0,
218 )
219 lambdaStep = pexConfig.Field(
220 doc="Wavelength step for generating atmospheres (nm)",
221 dtype=float,
222 default=0.5,
223 )
224 lambdaRange = pexConfig.ListField(
225 doc="Wavelength range for LUT (Angstrom)",
226 dtype=float,
227 default=[3000.0, 11000.0],
228 )
231class FgcmMakeLutConfig(pipeBase.PipelineTaskConfig,
232 pipelineConnections=FgcmMakeLutConnections):
233 """Config for FgcmMakeLutTask"""
235 filterNames = pexConfig.ListField(
236 doc="Filter names to build LUT ('short' names)",
237 dtype=str,
238 default=None,
239 )
240 stdFilterNames = pexConfig.ListField(
241 doc=("Standard filterNames ('short' names). "
242 "Each filter in filterName will be calibrated to a matched "
243 "stdFilterName. In regular usage, one has g->g, r->r, ... "
244 "In the case of HSC, one would have g->g, r->r2, r2->r2, ... "
245 "which allows replacement (or time-variable) filters to be "
246 "properly cross-calibrated."),
247 dtype=str,
248 default=None,
249 )
250 atmosphereTableName = pexConfig.Field(
251 doc="FGCM name or filename of precomputed atmospheres",
252 dtype=str,
253 default=None,
254 optional=True,
255 )
256 parameters = pexConfig.ConfigField(
257 doc="Atmosphere parameters (required if no atmosphereTableName)",
258 dtype=FgcmMakeLutParametersConfig,
259 default=None,
260 check=None)
262 def validate(self):
263 """
264 Validate the config parameters.
266 This method behaves differently from the parent validate in the case
267 that atmosphereTableName is set. In this case, the config values
268 for standard values, step sizes, and ranges are loaded
269 directly from the specified atmosphereTableName.
270 """
271 # check that filterNames and stdFilterNames are okay
272 self._fields['filterNames'].validate(self)
273 self._fields['stdFilterNames'].validate(self)
275 if self.atmosphereTableName is None:
276 # Validate the parameters
277 self._fields['parameters'].validate(self)
280class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner):
281 """Subclass of TaskRunner for fgcmMakeLutTask
283 fgcmMakeLutTask.run() takes one argument, the butler, and
284 does not run on any data in the repository.
285 This runner does not use any parallelization.
286 """
288 @staticmethod
289 def getTargetList(parsedCmd):
290 """
291 Return a list with one element, the butler.
292 """
293 return [parsedCmd.butler]
295 def __call__(self, butler):
296 """
297 Parameters
298 ----------
299 butler: `lsst.daf.persistence.Butler`
301 Returns
302 -------
303 exitStatus: `list` with `pipeBase.Struct`
304 exitStatus (0: success; 1: failure)
305 """
306 task = self.TaskClass(config=self.config, log=self.log)
308 exitStatus = 0
309 if self.doRaise:
310 task.runDataRef(butler)
311 else:
312 try:
313 task.runDataRef(butler)
314 except Exception as e:
315 exitStatus = 1
316 task.log.fatal("Failed: %s" % e)
317 if not isinstance(e, pipeBase.TaskError):
318 traceback.print_exc(file=sys.stderr)
320 task.writeMetadata(butler)
322 # The task does not return any results:
323 return [pipeBase.Struct(exitStatus=exitStatus)]
325 def run(self, parsedCmd):
326 """
327 Run the task, with no multiprocessing
329 Parameters
330 ----------
331 parsedCmd: ArgumentParser parsed command line
332 """
334 resultList = []
336 if self.precall(parsedCmd):
337 targetList = self.getTargetList(parsedCmd)
338 # make sure that we only get 1
339 resultList = self(targetList[0])
341 return resultList
344class FgcmMakeLutTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
345 """
346 Make Look-Up Table for FGCM.
348 This task computes a look-up-table for the range in expected atmosphere
349 variation and variation in instrumental throughput (as tracked by the
350 transmission_filter products). By pre-computing linearized integrals,
351 the FGCM fit is orders of magnitude faster for stars with a broad range
352 of colors and observing bands, yielding precision at the 1-2 mmag level.
354 Computing a LUT requires running MODTRAN or with a pre-generated
355 atmosphere table packaged with fgcm.
356 """
358 ConfigClass = FgcmMakeLutConfig
359 RunnerClass = FgcmMakeLutRunner
360 _DefaultName = "fgcmMakeLut"
362 def __init__(self, butler=None, initInputs=None, **kwargs):
363 super().__init__(**kwargs)
365 # no saving of metadata for now
366 def _getMetadataName(self):
367 return None
369 @pipeBase.timeMethod
370 def runDataRef(self, butler):
371 """
372 Make a Look-Up Table for FGCM
374 Parameters
375 ----------
376 butler: `lsst.daf.persistence.Butler`
378 Raises
379 ------
380 ValueError : Raised if configured filter name does not match any of the
381 available filter transmission curves.
382 """
383 camera = butler.get('camera')
384 opticsDataRef = butler.dataRef('transmission_optics')
386 sensorDataRefDict = {}
387 for detector in camera:
388 sensorDataRefDict[detector.getId()] = butler.dataRef('transmission_sensor',
389 dataId={'ccd': detector.getId()})
391 filterDataRefDict = {}
392 for filterName in self.config.filterNames:
393 f = Filter(filterName)
394 foundTrans = False
395 # Get all possible aliases and also try the short filterName
396 aliases = f.getAliases()
397 aliases.extend(filterName)
398 for alias in f.getAliases():
399 dataRef = butler.dataRef('transmission_filter', filter=alias)
400 if dataRef.datasetExists():
401 foundTrans = True
402 filterDataRefDict[alias] = dataRef
403 break
404 if not foundTrans:
405 raise ValueError("Cound not find transmission for filter %s via any alias." %
406 (filterName))
408 lutCat = self._fgcmMakeLut(camera,
409 opticsDataRef,
410 sensorDataRefDict,
411 filterDataRefDict)
412 butler.put(lutCat, 'fgcmLookUpTable')
414 def runQuantum(self, butlerQC, inputRefs, outputRefs):
415 camera = butlerQC.get(inputRefs.camera)
417 # Instantiate the instrument to load filter information
418 _ = Instrument.fromName(inputRefs.camera.dataId['instrument'],
419 butlerQC.registry)
420 opticsDataRef = butlerQC.get(inputRefs.transmission_optics)
422 sensorRefs = butlerQC.get(inputRefs.transmission_sensor)
423 sensorDataRefDict = {sensorRef.dataId.byName()['detector']: sensorRef for
424 sensorRef in sensorRefs}
426 filterRefs = butlerQC.get(inputRefs.transmission_filter)
427 filterDataRefDict = {filterRef.dataId.byName()['physical_filter']: filterRef for
428 filterRef in filterRefs}
430 lutCat = self._fgcmMakeLut(camera,
431 opticsDataRef,
432 sensorDataRefDict,
433 filterDataRefDict)
434 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable)
436 def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict,
437 filterDataRefDict):
438 """
439 Make a FGCM Look-up Table
441 Parameters
442 ----------
443 camera : `lsst.afw.cameraGeom.Camera`
444 Camera from the butler.
445 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
446 `lsst.daf.butler.DeferredDatasetHandle`
447 Reference to optics transmission curve.
448 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
449 `lsst.daf.butler.DeferredDatasetHandle`]
450 Dictionary of references to sensor transmission curves. Key will
451 be detector id.
452 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
453 `lsst.daf.butler.DeferredDatasetHandle`]
454 Dictionary of references to filter transmission curves. Key will
455 be physical filter name.
457 Returns
458 -------
459 fgcmLookUpTable : `BaseCatalog`
460 The FGCM look-up table.
461 """
462 # number of ccds from the length of the camera iterator
463 nCcd = len(camera)
464 self.log.info("Found %d ccds for look-up table" % (nCcd))
466 # Load in optics, etc.
467 self._loadThroughputs(camera,
468 opticsDataRef,
469 sensorDataRefDict,
470 filterDataRefDict)
472 lutConfig = self._createLutConfig(nCcd)
474 # make the lut object
475 self.log.info("Making the LUT maker object")
476 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig)
478 # generate the throughput dictionary.
480 # these will be in Angstroms
481 # note that lambdaStep is currently in nm, because of historical
482 # reasons in the code. Convert to Angstroms here.
483 throughputLambda = np.arange(self.fgcmLutMaker.lambdaRange[0],
484 self.fgcmLutMaker.lambdaRange[1]+self.fgcmLutMaker.lambdaStep*10,
485 self.fgcmLutMaker.lambdaStep*10.)
487 self.log.info("Built throughput lambda, %.1f-%.1f, step %.2f" %
488 (throughputLambda[0], throughputLambda[-1],
489 throughputLambda[1] - throughputLambda[0]))
491 throughputDict = {}
492 for i, filterName in enumerate(self.config.filterNames):
493 tDict = {}
494 tDict['LAMBDA'] = throughputLambda
495 for ccdIndex, detector in enumerate(camera):
496 tDict[ccdIndex] = self._getThroughputDetector(detector, filterName, throughputLambda)
497 throughputDict[filterName] = tDict
499 # set the throughputs
500 self.fgcmLutMaker.setThroughputs(throughputDict)
502 # make the LUT
503 self.log.info("Making LUT")
504 self.fgcmLutMaker.makeLUT()
506 # and save the LUT
508 # build the index values
509 comma = ','
510 filterNameString = comma.join(self.config.filterNames)
511 stdFilterNameString = comma.join(self.config.stdFilterNames)
513 atmosphereTableName = 'NoTableWasUsed'
514 if self.config.atmosphereTableName is not None:
515 atmosphereTableName = self.config.atmosphereTableName
517 lutSchema = self._makeLutSchema(filterNameString, stdFilterNameString,
518 atmosphereTableName)
520 lutCat = self._makeLutCat(lutSchema, filterNameString,
521 stdFilterNameString, atmosphereTableName)
522 return lutCat
524 def _createLutConfig(self, nCcd):
525 """
526 Create the fgcmLut config dictionary
528 Parameters
529 ----------
530 nCcd: `int`
531 Number of CCDs in the camera
532 """
534 # create the common stub of the lutConfig
535 lutConfig = {}
536 lutConfig['logger'] = self.log
537 lutConfig['filterNames'] = self.config.filterNames
538 lutConfig['stdFilterNames'] = self.config.stdFilterNames
539 lutConfig['nCCD'] = nCcd
541 # atmosphereTable already validated if available
542 if self.config.atmosphereTableName is not None:
543 lutConfig['atmosphereTableName'] = self.config.atmosphereTableName
544 else:
545 # use the regular paramters (also validated if needed)
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
569 return lutConfig
571 def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict):
572 """Internal method to load throughput data for filters
574 Parameters
575 ----------
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
584 be detector id.
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 name.
590 Raises
591 ------
592 ValueError : Raised if configured filter name does not match any of the
593 available filter transmission curves.
594 """
595 self._opticsTransmission = opticsDataRef.get()
597 self._sensorsTransmission = {}
598 for detector in camera:
599 self._sensorsTransmission[detector.getId()] = sensorDataRefDict[detector.getId()].get()
601 self._filtersTransmission = {}
602 for filterName in self.config.filterNames:
603 f = Filter(filterName)
604 foundTrans = False
605 # Get all possible aliases, and also try the short filterName
606 aliases = f.getAliases()
607 aliases.extend(filterName)
608 for alias in f.getAliases():
609 if alias in filterDataRefDict:
610 self._filtersTransmission[filterName] = filterDataRefDict[alias].get()
611 foundTrans = True
612 break
613 if not foundTrans:
614 raise ValueError("Could not find transmission for filter %s via any alias." %
615 (filterName))
617 def _getThroughputDetector(self, detector, filterName, throughputLambda):
618 """Internal method to get throughput for a detector.
620 Returns the throughput at the center of the detector for a given filter.
622 Parameters
623 ----------
624 detector: `lsst.afw.cameraGeom._detector.Detector`
625 Detector on camera
626 filterName: `str`
627 Short name for filter
628 throughputLambda: `np.array(dtype=np.float64)`
629 Wavelength steps (Angstrom)
631 Returns
632 -------
633 throughput: `np.array(dtype=np.float64)`
634 Throughput (max 1.0) at throughputLambda
635 """
637 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
638 c.scale(1.0/detector.getPixelSize()[0]) # Assumes x and y pixel sizes in arcsec are the same
640 throughput = self._opticsTransmission.sampleAt(position=c,
641 wavelengths=throughputLambda)
643 throughput *= self._sensorsTransmission[detector.getId()].sampleAt(position=c,
644 wavelengths=throughputLambda)
646 throughput *= self._filtersTransmission[filterName].sampleAt(position=c,
647 wavelengths=throughputLambda)
649 # Clip the throughput from 0 to 1
650 throughput = np.clip(throughput, 0.0, 1.0)
652 return throughput
654 def _makeLutSchema(self, filterNameString, stdFilterNameString,
655 atmosphereTableName):
656 """
657 Make the LUT schema
659 Parameters
660 ----------
661 filterNameString: `str`
662 Combined string of all the filterNames
663 stdFilterNameString: `str`
664 Combined string of all the standard filterNames
665 atmosphereTableName: `str`
666 Name of the atmosphere table used to generate LUT
668 Returns
669 -------
670 lutSchema: `afwTable.schema`
671 """
673 lutSchema = afwTable.Schema()
675 lutSchema.addField('tablename', type=str, doc='Atmosphere table name',
676 size=len(atmosphereTableName))
677 lutSchema.addField('elevation', type=float, doc="Telescope elevation used for LUT")
678 lutSchema.addField('filterNames', type=str, doc='filterNames in LUT',
679 size=len(filterNameString))
680 lutSchema.addField('stdFilterNames', type=str, doc='Standard filterNames in LUT',
681 size=len(stdFilterNameString))
682 lutSchema.addField('pmb', type='ArrayD', doc='Barometric Pressure',
683 size=self.fgcmLutMaker.pmb.size)
684 lutSchema.addField('pmbFactor', type='ArrayD', doc='PMB scaling factor',
685 size=self.fgcmLutMaker.pmb.size)
686 lutSchema.addField('pmbElevation', type=np.float64, doc='PMB Scaling at elevation')
687 lutSchema.addField('pwv', type='ArrayD', doc='Preciptable Water Vapor',
688 size=self.fgcmLutMaker.pwv.size)
689 lutSchema.addField('o3', type='ArrayD', doc='Ozone',
690 size=self.fgcmLutMaker.o3.size)
691 lutSchema.addField('tau', type='ArrayD', doc='Aerosol optical depth',
692 size=self.fgcmLutMaker.tau.size)
693 lutSchema.addField('lambdaNorm', type=np.float64, doc='AOD wavelength')
694 lutSchema.addField('alpha', type='ArrayD', doc='Aerosol alpha',
695 size=self.fgcmLutMaker.alpha.size)
696 lutSchema.addField('zenith', type='ArrayD', doc='Zenith angle',
697 size=self.fgcmLutMaker.zenith.size)
698 lutSchema.addField('nCcd', type=np.int32, doc='Number of CCDs')
700 # and the standard values
701 lutSchema.addField('pmbStd', type=np.float64, doc='PMB Standard')
702 lutSchema.addField('pwvStd', type=np.float64, doc='PWV Standard')
703 lutSchema.addField('o3Std', type=np.float64, doc='O3 Standard')
704 lutSchema.addField('tauStd', type=np.float64, doc='Tau Standard')
705 lutSchema.addField('alphaStd', type=np.float64, doc='Alpha Standard')
706 lutSchema.addField('zenithStd', type=np.float64, doc='Zenith angle Standard')
707 lutSchema.addField('lambdaRange', type='ArrayD', doc='Wavelength range',
708 size=2)
709 lutSchema.addField('lambdaStep', type=np.float64, doc='Wavelength step')
710 lutSchema.addField('lambdaStd', type='ArrayD', doc='Standard Wavelength',
711 size=len(self.fgcmLutMaker.filterNames))
712 lutSchema.addField('lambdaStdFilter', type='ArrayD', doc='Standard Wavelength (raw)',
713 size=len(self.fgcmLutMaker.filterNames))
714 lutSchema.addField('i0Std', type='ArrayD', doc='I0 Standard',
715 size=len(self.fgcmLutMaker.filterNames))
716 lutSchema.addField('i1Std', type='ArrayD', doc='I1 Standard',
717 size=len(self.fgcmLutMaker.filterNames))
718 lutSchema.addField('i10Std', type='ArrayD', doc='I10 Standard',
719 size=len(self.fgcmLutMaker.filterNames))
720 lutSchema.addField('i2Std', type='ArrayD', doc='I2 Standard',
721 size=len(self.fgcmLutMaker.filterNames))
722 lutSchema.addField('lambdaB', type='ArrayD', doc='Wavelength for passband (no atm)',
723 size=len(self.fgcmLutMaker.filterNames))
724 lutSchema.addField('atmLambda', type='ArrayD', doc='Atmosphere wavelengths (Angstrom)',
725 size=self.fgcmLutMaker.atmLambda.size)
726 lutSchema.addField('atmStdTrans', type='ArrayD', doc='Standard Atmosphere Throughput',
727 size=self.fgcmLutMaker.atmStdTrans.size)
729 # and the look-up-tables
730 lutSchema.addField('luttype', type=str, size=20, doc='Look-up table type')
731 lutSchema.addField('lut', type='ArrayF', doc='Look-up table for luttype',
732 size=self.fgcmLutMaker.lut['I0'].size)
734 return lutSchema
736 def _makeLutCat(self, lutSchema, filterNameString, stdFilterNameString,
737 atmosphereTableName):
738 """
739 Make the LUT schema
741 Parameters
742 ----------
743 lutSchema: `afwTable.schema`
744 Lut catalog schema
745 filterNameString: `str`
746 Combined string of all the filterNames
747 stdFilterNameString: `str`
748 Combined string of all the standard filterNames
749 atmosphereTableName: `str`
750 Name of the atmosphere table used to generate LUT
752 Returns
753 -------
754 lutCat: `afwTable.BaseCatalog`
755 Lut catalog for persistence
756 """
758 # The somewhat strange format is to make sure that
759 # the rows of the afwTable do not get too large
760 # (see DM-11419)
762 lutCat = afwTable.BaseCatalog(lutSchema)
763 lutCat.table.preallocate(14)
765 # first fill the first index
766 rec = lutCat.addNew()
768 rec['tablename'] = atmosphereTableName
769 rec['elevation'] = self.fgcmLutMaker.atmosphereTable.elevation
770 rec['filterNames'] = filterNameString
771 rec['stdFilterNames'] = stdFilterNameString
772 rec['pmb'][:] = self.fgcmLutMaker.pmb
773 rec['pmbFactor'][:] = self.fgcmLutMaker.pmbFactor
774 rec['pmbElevation'] = self.fgcmLutMaker.pmbElevation
775 rec['pwv'][:] = self.fgcmLutMaker.pwv
776 rec['o3'][:] = self.fgcmLutMaker.o3
777 rec['tau'][:] = self.fgcmLutMaker.tau
778 rec['lambdaNorm'] = self.fgcmLutMaker.lambdaNorm
779 rec['alpha'][:] = self.fgcmLutMaker.alpha
780 rec['zenith'][:] = self.fgcmLutMaker.zenith
781 rec['nCcd'] = self.fgcmLutMaker.nCCD
783 rec['pmbStd'] = self.fgcmLutMaker.pmbStd
784 rec['pwvStd'] = self.fgcmLutMaker.pwvStd
785 rec['o3Std'] = self.fgcmLutMaker.o3Std
786 rec['tauStd'] = self.fgcmLutMaker.tauStd
787 rec['alphaStd'] = self.fgcmLutMaker.alphaStd
788 rec['zenithStd'] = self.fgcmLutMaker.zenithStd
789 rec['lambdaRange'][:] = self.fgcmLutMaker.lambdaRange
790 rec['lambdaStep'] = self.fgcmLutMaker.lambdaStep
791 rec['lambdaStd'][:] = self.fgcmLutMaker.lambdaStd
792 rec['lambdaStdFilter'][:] = self.fgcmLutMaker.lambdaStdFilter
793 rec['i0Std'][:] = self.fgcmLutMaker.I0Std
794 rec['i1Std'][:] = self.fgcmLutMaker.I1Std
795 rec['i10Std'][:] = self.fgcmLutMaker.I10Std
796 rec['i2Std'][:] = self.fgcmLutMaker.I2Std
797 rec['lambdaB'][:] = self.fgcmLutMaker.lambdaB
798 rec['atmLambda'][:] = self.fgcmLutMaker.atmLambda
799 rec['atmStdTrans'][:] = self.fgcmLutMaker.atmStdTrans
801 rec['luttype'] = 'I0'
802 rec['lut'][:] = self.fgcmLutMaker.lut['I0'].flatten()
804 # and add the rest
805 rec = lutCat.addNew()
806 rec['luttype'] = 'I1'
807 rec['lut'][:] = self.fgcmLutMaker.lut['I1'].flatten()
809 derivTypes = ['D_PMB', 'D_LNPWV', 'D_O3', 'D_LNTAU', 'D_ALPHA', 'D_SECZENITH',
810 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1',
811 'D_SECZENITH_I1']
812 for derivType in derivTypes:
813 rec = lutCat.addNew()
814 rec['luttype'] = derivType
815 rec['lut'][:] = self.fgcmLutMaker.lutDeriv[derivType].flatten()
817 return lutCat