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"""
234 filterNames = pexConfig.ListField(
235 doc="Filter names to build LUT ('short' names)",
236 dtype=str,
237 default=[],
238 deprecated=("This field is no longer used, and has been deprecated by "
239 "DM-28088. It will be removed after v22. Use "
240 "stdPhysicalFilterMap instead.")
241 )
242 stdFilterNames = pexConfig.ListField(
243 doc=("Standard filterNames ('short' names). "
244 "Each filter in filterName will be calibrated to a matched "
245 "stdFilterName. In regular usage, one has g->g, r->r, ... "
246 "In the case of HSC, one would have g->g, r->r2, r2->r2, ... "
247 "which allows replacement (or time-variable) filters to be "
248 "properly cross-calibrated."),
249 dtype=str,
250 default=[],
251 deprecated=("This field is no longer used, and has been deprecated by "
252 "DM-28088. It will be removed after v22. Use "
253 "stdPhysicalFilterMap instead.")
254 )
255 physicalFilters = pexConfig.ListField(
256 doc="List of physicalFilter labels to generate look-up table.",
257 dtype=str,
258 default=[],
259 )
260 stdPhysicalFilterOverrideMap = pexConfig.DictField(
261 doc=("Override mapping from physical filter labels to 'standard' physical "
262 "filter labels. The 'standard' physical filter defines the transmission "
263 "curve that the FGCM standard bandpass will be based on. "
264 "Any filter not listed here will be mapped to "
265 "itself (e.g. g->g or HSC-G->HSC-G). Use this override for cross-"
266 "filter calibration such as HSC-R->HSC-R2 and HSC-I->HSC-I2."),
267 keytype=str,
268 itemtype=str,
269 default={},
270 )
271 atmosphereTableName = pexConfig.Field(
272 doc="FGCM name or filename of precomputed atmospheres",
273 dtype=str,
274 default=None,
275 optional=True,
276 )
277 parameters = pexConfig.ConfigField(
278 doc="Atmosphere parameters (required if no atmosphereTableName)",
279 dtype=FgcmMakeLutParametersConfig,
280 default=None,
281 check=None)
283 def validate(self):
284 """
285 Validate the config parameters.
287 This method behaves differently from the parent validate in the case
288 that atmosphereTableName is set. In this case, the config values
289 for standard values, step sizes, and ranges are loaded
290 directly from the specified atmosphereTableName.
291 """
292 # check that filterNames and stdFilterNames are okay
293 self._fields['physicalFilters'].validate(self)
294 self._fields['stdPhysicalFilterOverrideMap'].validate(self)
296 if self.atmosphereTableName is None:
297 # Validate the parameters
298 self._fields['parameters'].validate(self)
301class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner):
302 """Subclass of TaskRunner for fgcmMakeLutTask
304 fgcmMakeLutTask.run() takes one argument, the butler, and
305 does not run on any data in the repository.
306 This runner does not use any parallelization.
307 """
309 @staticmethod
310 def getTargetList(parsedCmd):
311 """
312 Return a list with one element, the butler.
313 """
314 return [parsedCmd.butler]
316 def __call__(self, butler):
317 """
318 Parameters
319 ----------
320 butler: `lsst.daf.persistence.Butler`
322 Returns
323 -------
324 exitStatus: `list` with `pipeBase.Struct`
325 exitStatus (0: success; 1: failure)
326 """
327 task = self.TaskClass(config=self.config, log=self.log)
329 exitStatus = 0
330 if self.doRaise:
331 task.runDataRef(butler)
332 else:
333 try:
334 task.runDataRef(butler)
335 except Exception as e:
336 exitStatus = 1
337 task.log.fatal("Failed: %s" % e)
338 if not isinstance(e, pipeBase.TaskError):
339 traceback.print_exc(file=sys.stderr)
341 task.writeMetadata(butler)
343 # The task does not return any results:
344 return [pipeBase.Struct(exitStatus=exitStatus)]
346 def run(self, parsedCmd):
347 """
348 Run the task, with no multiprocessing
350 Parameters
351 ----------
352 parsedCmd: ArgumentParser parsed command line
353 """
355 resultList = []
357 if self.precall(parsedCmd):
358 targetList = self.getTargetList(parsedCmd)
359 # make sure that we only get 1
360 resultList = self(targetList[0])
362 return resultList
365class FgcmMakeLutTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
366 """
367 Make Look-Up Table for FGCM.
369 This task computes a look-up-table for the range in expected atmosphere
370 variation and variation in instrumental throughput (as tracked by the
371 transmission_filter products). By pre-computing linearized integrals,
372 the FGCM fit is orders of magnitude faster for stars with a broad range
373 of colors and observing bands, yielding precision at the 1-2 mmag level.
375 Computing a LUT requires running MODTRAN or with a pre-generated
376 atmosphere table packaged with fgcm.
377 """
379 ConfigClass = FgcmMakeLutConfig
380 RunnerClass = FgcmMakeLutRunner
381 _DefaultName = "fgcmMakeLut"
383 def __init__(self, butler=None, initInputs=None, **kwargs):
384 super().__init__(**kwargs)
386 # no saving of metadata for now
387 def _getMetadataName(self):
388 return None
390 @pipeBase.timeMethod
391 def runDataRef(self, butler):
392 """
393 Make a Look-Up Table for FGCM
395 Parameters
396 ----------
397 butler: `lsst.daf.persistence.Butler`
399 Raises
400 ------
401 ValueError : Raised if configured filter name does not match any of the
402 available filter transmission curves.
403 """
404 camera = butler.get('camera')
405 opticsDataRef = butler.dataRef('transmission_optics')
407 sensorDataRefDict = {}
408 for detector in camera:
409 sensorDataRefDict[detector.getId()] = butler.dataRef('transmission_sensor',
410 dataId={'ccd': detector.getId()})
412 filterDataRefDict = {}
413 for physicalFilter in self.config.physicalFilters:
414 # The physical filters map directly to dataId filter names
415 # for gen2 HSC. This is the only camera that will be supported
416 # by Gen2 fgcmcal, so we do not need to worry about other cases.
417 dataRef = butler.dataRef('transmission_filter', filter=physicalFilter)
418 if not dataRef.datasetExists():
419 raise ValueError(f"Could not find transmission for filter {physicalFilter}.")
420 filterDataRefDict[physicalFilter] = dataRef
422 lutCat = self._fgcmMakeLut(camera,
423 opticsDataRef,
424 sensorDataRefDict,
425 filterDataRefDict)
426 butler.put(lutCat, 'fgcmLookUpTable')
428 def runQuantum(self, butlerQC, inputRefs, outputRefs):
429 camera = butlerQC.get(inputRefs.camera)
431 # Instantiate the instrument to load filter information
432 _ = Instrument.fromName(inputRefs.camera.dataId['instrument'],
433 butlerQC.registry)
434 opticsDataRef = butlerQC.get(inputRefs.transmission_optics)
436 sensorRefs = butlerQC.get(inputRefs.transmission_sensor)
437 sensorDataRefDict = {sensorRef.dataId.byName()['detector']: sensorRef for
438 sensorRef in sensorRefs}
440 filterRefs = butlerQC.get(inputRefs.transmission_filter)
441 filterDataRefDict = {filterRef.dataId['physical_filter']: filterRef for
442 filterRef in filterRefs}
444 lutCat = self._fgcmMakeLut(camera,
445 opticsDataRef,
446 sensorDataRefDict,
447 filterDataRefDict)
448 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable)
450 def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict,
451 filterDataRefDict):
452 """
453 Make a FGCM Look-up Table
455 Parameters
456 ----------
457 camera : `lsst.afw.cameraGeom.Camera`
458 Camera from the butler.
459 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
460 `lsst.daf.butler.DeferredDatasetHandle`
461 Reference to optics transmission curve.
462 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
463 `lsst.daf.butler.DeferredDatasetHandle`]
464 Dictionary of references to sensor transmission curves. Key will
465 be detector id.
466 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
467 `lsst.daf.butler.DeferredDatasetHandle`]
468 Dictionary of references to filter transmission curves. Key will
469 be physical filter label.
471 Returns
472 -------
473 fgcmLookUpTable : `BaseCatalog`
474 The FGCM look-up table.
475 """
476 # number of ccds from the length of the camera iterator
477 nCcd = len(camera)
478 self.log.info("Found %d ccds for look-up table" % (nCcd))
480 # Load in optics, etc.
481 self._loadThroughputs(camera,
482 opticsDataRef,
483 sensorDataRefDict,
484 filterDataRefDict)
486 lutConfig = self._createLutConfig(nCcd)
488 # make the lut object
489 self.log.info("Making the LUT maker object")
490 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig)
492 # generate the throughput dictionary.
494 # these will be in Angstroms
495 # note that lambdaStep is currently in nm, because of historical
496 # reasons in the code. Convert to Angstroms here.
497 throughputLambda = np.arange(self.fgcmLutMaker.lambdaRange[0],
498 self.fgcmLutMaker.lambdaRange[1]+self.fgcmLutMaker.lambdaStep*10,
499 self.fgcmLutMaker.lambdaStep*10.)
501 self.log.info("Built throughput lambda, %.1f-%.1f, step %.2f" %
502 (throughputLambda[0], throughputLambda[-1],
503 throughputLambda[1] - throughputLambda[0]))
505 throughputDict = {}
506 for i, physicalFilter in enumerate(self.config.physicalFilters):
507 tDict = {}
508 tDict['LAMBDA'] = throughputLambda
509 for ccdIndex, detector in enumerate(camera):
510 tDict[ccdIndex] = self._getThroughputDetector(detector, physicalFilter, throughputLambda)
511 throughputDict[physicalFilter] = tDict
513 # set the throughputs
514 self.fgcmLutMaker.setThroughputs(throughputDict)
516 # make the LUT
517 self.log.info("Making LUT")
518 self.fgcmLutMaker.makeLUT()
520 # and save the LUT
522 # build the index values
523 comma = ','
524 physicalFilterString = comma.join(self.config.physicalFilters)
525 stdPhysicalFilterString = comma.join(self._getStdPhysicalFilterList())
527 atmosphereTableName = 'NoTableWasUsed'
528 if self.config.atmosphereTableName is not None:
529 atmosphereTableName = self.config.atmosphereTableName
531 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString,
532 atmosphereTableName)
534 lutCat = self._makeLutCat(lutSchema, physicalFilterString,
535 stdPhysicalFilterString, atmosphereTableName)
536 return lutCat
538 def _getStdPhysicalFilterList(self):
539 """Get the standard physical filter lists from config.physicalFilters
540 and config.stdPhysicalFilterOverrideMap
542 Returns
543 -------
544 stdPhysicalFilters : `list`
545 """
546 override = self.config.stdPhysicalFilterOverrideMap
547 return [override.get(physicalFilter, physicalFilter) for
548 physicalFilter in self.config.physicalFilters]
550 def _createLutConfig(self, nCcd):
551 """
552 Create the fgcmLut config dictionary
554 Parameters
555 ----------
556 nCcd: `int`
557 Number of CCDs in the camera
558 """
560 # create the common stub of the lutConfig
561 lutConfig = {}
562 lutConfig['logger'] = self.log
563 lutConfig['filterNames'] = self.config.physicalFilters
564 lutConfig['stdFilterNames'] = self._getStdPhysicalFilterList()
565 lutConfig['nCCD'] = nCcd
567 # atmosphereTable already validated if available
568 if self.config.atmosphereTableName is not None:
569 lutConfig['atmosphereTableName'] = self.config.atmosphereTableName
570 else:
571 # use the regular paramters (also validated if needed)
572 lutConfig['elevation'] = self.config.parameters.elevation
573 lutConfig['pmbRange'] = self.config.parameters.pmbRange
574 lutConfig['pmbSteps'] = self.config.parameters.pmbSteps
575 lutConfig['pwvRange'] = self.config.parameters.pwvRange
576 lutConfig['pwvSteps'] = self.config.parameters.pwvSteps
577 lutConfig['o3Range'] = self.config.parameters.o3Range
578 lutConfig['o3Steps'] = self.config.parameters.o3Steps
579 lutConfig['tauRange'] = self.config.parameters.tauRange
580 lutConfig['tauSteps'] = self.config.parameters.tauSteps
581 lutConfig['alphaRange'] = self.config.parameters.alphaRange
582 lutConfig['alphaSteps'] = self.config.parameters.alphaSteps
583 lutConfig['zenithRange'] = self.config.parameters.zenithRange
584 lutConfig['zenithSteps'] = self.config.parameters.zenithSteps
585 lutConfig['pmbStd'] = self.config.parameters.pmbStd
586 lutConfig['pwvStd'] = self.config.parameters.pwvStd
587 lutConfig['o3Std'] = self.config.parameters.o3Std
588 lutConfig['tauStd'] = self.config.parameters.tauStd
589 lutConfig['alphaStd'] = self.config.parameters.alphaStd
590 lutConfig['airmassStd'] = self.config.parameters.airmassStd
591 lutConfig['lambdaRange'] = self.config.parameters.lambdaRange
592 lutConfig['lambdaStep'] = self.config.parameters.lambdaStep
593 lutConfig['lambdaNorm'] = self.config.parameters.lambdaNorm
595 return lutConfig
597 def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict):
598 """Internal method to load throughput data for filters
600 Parameters
601 ----------
602 camera: `lsst.afw.cameraGeom.Camera`
603 Camera from the butler
604 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or
605 `lsst.daf.butler.DeferredDatasetHandle`
606 Reference to optics transmission curve.
607 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or
608 `lsst.daf.butler.DeferredDatasetHandle`]
609 Dictionary of references to sensor transmission curves. Key will
610 be detector id.
611 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or
612 `lsst.daf.butler.DeferredDatasetHandle`]
613 Dictionary of references to filter transmission curves. Key will
614 be physical filter label.
616 Raises
617 ------
618 ValueError : Raised if configured filter name does not match any of the
619 available filter transmission curves.
620 """
621 self._opticsTransmission = opticsDataRef.get()
623 self._sensorsTransmission = {}
624 for detector in camera:
625 self._sensorsTransmission[detector.getId()] = sensorDataRefDict[detector.getId()].get()
627 self._filtersTransmission = {}
628 for physicalFilter in self.config.physicalFilters:
629 self._filtersTransmission[physicalFilter] = filterDataRefDict[physicalFilter].get()
631 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda):
632 """Internal method to get throughput for a detector.
634 Returns the throughput at the center of the detector for a given filter.
636 Parameters
637 ----------
638 detector: `lsst.afw.cameraGeom._detector.Detector`
639 Detector on camera
640 physicalFilter: `str`
641 Physical filter label
642 throughputLambda: `np.array(dtype=np.float64)`
643 Wavelength steps (Angstrom)
645 Returns
646 -------
647 throughput: `np.array(dtype=np.float64)`
648 Throughput (max 1.0) at throughputLambda
649 """
651 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
652 c.scale(1.0/detector.getPixelSize()[0]) # Assumes x and y pixel sizes in arcsec are the same
654 throughput = self._opticsTransmission.sampleAt(position=c,
655 wavelengths=throughputLambda)
657 throughput *= self._sensorsTransmission[detector.getId()].sampleAt(position=c,
658 wavelengths=throughputLambda)
660 throughput *= self._filtersTransmission[physicalFilter].sampleAt(position=c,
661 wavelengths=throughputLambda)
663 # Clip the throughput from 0 to 1
664 throughput = np.clip(throughput, 0.0, 1.0)
666 return throughput
668 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString,
669 atmosphereTableName):
670 """
671 Make the LUT schema
673 Parameters
674 ----------
675 physicalFilterString: `str`
676 Combined string of all the physicalFilters
677 stdPhysicalFilterString: `str`
678 Combined string of all the standard physicalFilters
679 atmosphereTableName: `str`
680 Name of the atmosphere table used to generate LUT
682 Returns
683 -------
684 lutSchema: `afwTable.schema`
685 """
687 lutSchema = afwTable.Schema()
689 lutSchema.addField('tablename', type=str, doc='Atmosphere table name',
690 size=len(atmosphereTableName))
691 lutSchema.addField('elevation', type=float, doc="Telescope elevation used for LUT")
692 lutSchema.addField('physicalFilters', type=str, doc='physicalFilters in LUT',
693 size=len(physicalFilterString))
694 lutSchema.addField('stdPhysicalFilters', type=str, doc='Standard physicalFilters in LUT',
695 size=len(stdPhysicalFilterString))
696 lutSchema.addField('pmb', type='ArrayD', doc='Barometric Pressure',
697 size=self.fgcmLutMaker.pmb.size)
698 lutSchema.addField('pmbFactor', type='ArrayD', doc='PMB scaling factor',
699 size=self.fgcmLutMaker.pmb.size)
700 lutSchema.addField('pmbElevation', type=np.float64, doc='PMB Scaling at elevation')
701 lutSchema.addField('pwv', type='ArrayD', doc='Preciptable Water Vapor',
702 size=self.fgcmLutMaker.pwv.size)
703 lutSchema.addField('o3', type='ArrayD', doc='Ozone',
704 size=self.fgcmLutMaker.o3.size)
705 lutSchema.addField('tau', type='ArrayD', doc='Aerosol optical depth',
706 size=self.fgcmLutMaker.tau.size)
707 lutSchema.addField('lambdaNorm', type=np.float64, doc='AOD wavelength')
708 lutSchema.addField('alpha', type='ArrayD', doc='Aerosol alpha',
709 size=self.fgcmLutMaker.alpha.size)
710 lutSchema.addField('zenith', type='ArrayD', doc='Zenith angle',
711 size=self.fgcmLutMaker.zenith.size)
712 lutSchema.addField('nCcd', type=np.int32, doc='Number of CCDs')
714 # and the standard values
715 lutSchema.addField('pmbStd', type=np.float64, doc='PMB Standard')
716 lutSchema.addField('pwvStd', type=np.float64, doc='PWV Standard')
717 lutSchema.addField('o3Std', type=np.float64, doc='O3 Standard')
718 lutSchema.addField('tauStd', type=np.float64, doc='Tau Standard')
719 lutSchema.addField('alphaStd', type=np.float64, doc='Alpha Standard')
720 lutSchema.addField('zenithStd', type=np.float64, doc='Zenith angle Standard')
721 lutSchema.addField('lambdaRange', type='ArrayD', doc='Wavelength range',
722 size=2)
723 lutSchema.addField('lambdaStep', type=np.float64, doc='Wavelength step')
724 lutSchema.addField('lambdaStd', type='ArrayD', doc='Standard Wavelength',
725 size=len(self.fgcmLutMaker.filterNames))
726 lutSchema.addField('lambdaStdFilter', type='ArrayD', doc='Standard Wavelength (raw)',
727 size=len(self.fgcmLutMaker.filterNames))
728 lutSchema.addField('i0Std', type='ArrayD', doc='I0 Standard',
729 size=len(self.fgcmLutMaker.filterNames))
730 lutSchema.addField('i1Std', type='ArrayD', doc='I1 Standard',
731 size=len(self.fgcmLutMaker.filterNames))
732 lutSchema.addField('i10Std', type='ArrayD', doc='I10 Standard',
733 size=len(self.fgcmLutMaker.filterNames))
734 lutSchema.addField('i2Std', type='ArrayD', doc='I2 Standard',
735 size=len(self.fgcmLutMaker.filterNames))
736 lutSchema.addField('lambdaB', type='ArrayD', doc='Wavelength for passband (no atm)',
737 size=len(self.fgcmLutMaker.filterNames))
738 lutSchema.addField('atmLambda', type='ArrayD', doc='Atmosphere wavelengths (Angstrom)',
739 size=self.fgcmLutMaker.atmLambda.size)
740 lutSchema.addField('atmStdTrans', type='ArrayD', doc='Standard Atmosphere Throughput',
741 size=self.fgcmLutMaker.atmStdTrans.size)
743 # and the look-up-tables
744 lutSchema.addField('luttype', type=str, size=20, doc='Look-up table type')
745 lutSchema.addField('lut', type='ArrayF', doc='Look-up table for luttype',
746 size=self.fgcmLutMaker.lut['I0'].size)
748 return lutSchema
750 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString,
751 atmosphereTableName):
752 """
753 Make the LUT schema
755 Parameters
756 ----------
757 lutSchema: `afwTable.schema`
758 Lut catalog schema
759 physicalFilterString: `str`
760 Combined string of all the physicalFilters
761 stdPhysicalFilterString: `str`
762 Combined string of all the standard physicalFilters
763 atmosphereTableName: `str`
764 Name of the atmosphere table used to generate LUT
766 Returns
767 -------
768 lutCat: `afwTable.BaseCatalog`
769 Lut catalog for persistence
770 """
772 # The somewhat strange format is to make sure that
773 # the rows of the afwTable do not get too large
774 # (see DM-11419)
776 lutCat = afwTable.BaseCatalog(lutSchema)
777 lutCat.table.preallocate(14)
779 # first fill the first index
780 rec = lutCat.addNew()
782 rec['tablename'] = atmosphereTableName
783 rec['elevation'] = self.fgcmLutMaker.atmosphereTable.elevation
784 rec['physicalFilters'] = physicalFilterString
785 rec['stdPhysicalFilters'] = stdPhysicalFilterString
786 rec['pmb'][:] = self.fgcmLutMaker.pmb
787 rec['pmbFactor'][:] = self.fgcmLutMaker.pmbFactor
788 rec['pmbElevation'] = self.fgcmLutMaker.pmbElevation
789 rec['pwv'][:] = self.fgcmLutMaker.pwv
790 rec['o3'][:] = self.fgcmLutMaker.o3
791 rec['tau'][:] = self.fgcmLutMaker.tau
792 rec['lambdaNorm'] = self.fgcmLutMaker.lambdaNorm
793 rec['alpha'][:] = self.fgcmLutMaker.alpha
794 rec['zenith'][:] = self.fgcmLutMaker.zenith
795 rec['nCcd'] = self.fgcmLutMaker.nCCD
797 rec['pmbStd'] = self.fgcmLutMaker.pmbStd
798 rec['pwvStd'] = self.fgcmLutMaker.pwvStd
799 rec['o3Std'] = self.fgcmLutMaker.o3Std
800 rec['tauStd'] = self.fgcmLutMaker.tauStd
801 rec['alphaStd'] = self.fgcmLutMaker.alphaStd
802 rec['zenithStd'] = self.fgcmLutMaker.zenithStd
803 rec['lambdaRange'][:] = self.fgcmLutMaker.lambdaRange
804 rec['lambdaStep'] = self.fgcmLutMaker.lambdaStep
805 rec['lambdaStd'][:] = self.fgcmLutMaker.lambdaStd
806 rec['lambdaStdFilter'][:] = self.fgcmLutMaker.lambdaStdFilter
807 rec['i0Std'][:] = self.fgcmLutMaker.I0Std
808 rec['i1Std'][:] = self.fgcmLutMaker.I1Std
809 rec['i10Std'][:] = self.fgcmLutMaker.I10Std
810 rec['i2Std'][:] = self.fgcmLutMaker.I2Std
811 rec['lambdaB'][:] = self.fgcmLutMaker.lambdaB
812 rec['atmLambda'][:] = self.fgcmLutMaker.atmLambda
813 rec['atmStdTrans'][:] = self.fgcmLutMaker.atmStdTrans
815 rec['luttype'] = 'I0'
816 rec['lut'][:] = self.fgcmLutMaker.lut['I0'].flatten()
818 # and add the rest
819 rec = lutCat.addNew()
820 rec['luttype'] = 'I1'
821 rec['lut'][:] = self.fgcmLutMaker.lut['I1'].flatten()
823 derivTypes = ['D_PMB', 'D_LNPWV', 'D_O3', 'D_LNTAU', 'D_ALPHA', 'D_SECZENITH',
824 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1',
825 'D_SECZENITH_I1']
826 for derivType in derivTypes:
827 rec = lutCat.addNew()
828 rec['luttype'] = derivType
829 rec['lut'][:] = self.fgcmLutMaker.lutDeriv[derivType].flatten()
831 return lutCat