LSST Applications g04e9c324dd+8c5ae1fdc5,g134cb467dc+b203dec576,g18429d2f64+358861cd2c,g199a45376c+0ba108daf9,g1fd858c14a+dd066899e3,g262e1987ae+ebfced1d55,g29ae962dfc+72fd90588e,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+b668f15bc5,g4595892280+3897dae354,g47891489e3+abcf9c3559,g4d44eb3520+fb4ddce128,g53246c7159+8c5ae1fdc5,g67b6fd64d1+abcf9c3559,g67fd3c3899+1f72b5a9f7,g74acd417e5+cb6b47f07b,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+abcf9c3559,g8d7436a09f+bcf525d20c,g8ea07a8fe4+9f5ccc88ac,g90f42f885a+6054cc57f1,g97be763408+06f794da49,g9dd6db0277+1f72b5a9f7,ga681d05dcb+7e36ad54cd,gabf8522325+735880ea63,gac2eed3f23+abcf9c3559,gb89ab40317+abcf9c3559,gbf99507273+8c5ae1fdc5,gd8ff7fe66e+1f72b5a9f7,gdab6d2f7ff+cb6b47f07b,gdc713202bf+1f72b5a9f7,gdfd2d52018+8225f2b331,ge365c994fd+375fc21c71,ge410e46f29+abcf9c3559,geaed405ab2+562b3308c0,gf9a733ac38+8c5ae1fdc5,w.2025.35
LSST Data Management Base Package
Loading...
Searching...
No Matches
fgcmMakeLut.py
Go to the documentation of this file.
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.
24
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.
30
31Computing a LUT requires running MODTRAN or with a pre-generated
32atmosphere table packaged with fgcm.
33"""
34
35import numpy as np
36
37import lsst.pex.config as pexConfig
38import lsst.pipe.base as pipeBase
39from lsst.pipe.base import connectionTypes
40import lsst.afw.table as afwTable
41import lsst.afw.cameraGeom as afwCameraGeom
42from lsst.afw.image import TransmissionCurve
43from .utilities import lookupStaticCalibrations
44from astropy.table import Table
45import astropy.units as units
46
47import fgcm
48
49__all__ = ['FgcmMakeLutParametersConfig', 'FgcmMakeLutConfig', 'FgcmMakeLutTask', 'SensorCorrectionTerms']
50
51
52class SensorCorrectionTerms(pexConfig.Config):
53 refLambda = pexConfig.Field(
54 doc="Reference wavelength for first-order correction terms.",
55 dtype=float,
56 optional=False,
57 )
58 correctionTermDict = pexConfig.DictField(
59 doc="Mapping of detector number to first-order correction term.",
60 keytype=int,
61 itemtype=float,
62 default={},
63 )
64
65
66class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections,
67 dimensions=('instrument',),
68 defaultTemplates={}):
69 camera = connectionTypes.PrerequisiteInput(
70 doc="Camera instrument",
71 name="camera",
72 storageClass="Camera",
73 dimensions=("instrument",),
74 isCalibration=True,
75 )
76
77 transmission_optics = connectionTypes.PrerequisiteInput(
78 doc="Optics transmission curve information",
79 name="transmission_optics",
80 storageClass="TransmissionCurve",
81 dimensions=("instrument",),
82 isCalibration=True,
83 deferLoad=True,
84 )
85
86 transmission_sensor = connectionTypes.PrerequisiteInput(
87 doc="Sensor transmission curve information",
88 name="transmission_sensor",
89 storageClass="TransmissionCurve",
90 dimensions=("instrument", "detector",),
91 lookupFunction=lookupStaticCalibrations,
92 isCalibration=True,
93 deferLoad=True,
94 multiple=True,
95 )
96
97 transmission_filter = connectionTypes.PrerequisiteInput(
98 doc="Filter transmission curve information",
99 name="transmission_filter",
100 storageClass="TransmissionCurve",
101 dimensions=("band", "instrument", "physical_filter",),
102 lookupFunction=lookupStaticCalibrations,
103 isCalibration=True,
104 deferLoad=True,
105 multiple=True,
106 )
107
108 transmission_filter_detector = connectionTypes.PrerequisiteInput(
109 doc="Filter transmission curve per detector",
110 name="transmission_filter_detector",
111 storageClass="TransmissionCurve",
112 dimensions=("instrument", "physical_filter", "detector",),
113 lookupFunction=lookupStaticCalibrations,
114 isCalibration=True,
115 deferLoad=True,
116 multiple=True,
117 )
118
119 fgcmLookUpTable = connectionTypes.Output(
120 doc=("Atmosphere + instrument look-up-table for FGCM throughput and "
121 "chromatic corrections."),
122 name="fgcmLookUpTable",
123 storageClass="Catalog",
124 dimensions=("instrument",),
125 )
126
127 fgcmStandardAtmosphere = connectionTypes.Output(
128 doc="Standard atmosphere used for FGCM calibration.",
129 name="fgcm_standard_atmosphere",
130 storageClass="TransmissionCurve",
131 dimensions=("instrument",),
132 )
133
134 fgcmStandardPassbands = connectionTypes.Output(
135 doc="Standard passbands used for FGCM calibration.",
136 name="fgcm_standard_passband",
137 storageClass="TransmissionCurve",
138 dimensions=("instrument", "physical_filter"),
139 multiple=True,
140 )
141
142 standardPassbands = connectionTypes.Output(
143 doc="Standard passbands, in astropy table format.",
144 name="standard_passband",
145 storageClass="ArrowAstropy",
146 dimensions=("instrument", "band"),
147 multiple=True,
148 )
149
150 def __init__(self, *, config=None):
151 if not config.doOpticsTransmission:
152 del self.transmission_optics
153 if not config.doSensorTransmission:
154 del self.transmission_sensor
155 if not config.doFilterDetectorTransmission:
157 else:
158 del self.transmission_filter
159
160
161class FgcmMakeLutParametersConfig(pexConfig.Config):
162 """Config for parameters if atmosphereTableName not available"""
163 # TODO: When DM-16511 is done, it will be possible to get the
164 # telescope elevation directly from the camera.
165 elevation = pexConfig.Field(
166 doc="Telescope elevation (m)",
167 dtype=float,
168 default=None,
169 )
170 pmbRange = pexConfig.ListField(
171 doc=("Barometric Pressure range (millibar) "
172 "Recommended range depends on the site."),
173 dtype=float,
174 default=None,
175 )
176 pmbSteps = pexConfig.Field(
177 doc="Barometric Pressure number of steps",
178 dtype=int,
179 default=5,
180 )
181 pwvRange = pexConfig.ListField(
182 doc=("Precipitable Water Vapor range (mm) "
183 "Recommended range depends on the site."),
184 dtype=float,
185 default=None,
186 )
187 pwvSteps = pexConfig.Field(
188 doc="Precipitable Water Vapor number of steps",
189 dtype=int,
190 default=15,
191 )
192 o3Range = pexConfig.ListField(
193 doc="Ozone range (dob)",
194 dtype=float,
195 default=[220.0, 310.0],
196 )
197 o3Steps = pexConfig.Field(
198 doc="Ozone number of steps",
199 dtype=int,
200 default=3,
201 )
202 tauRange = pexConfig.ListField(
203 doc="Aerosol Optical Depth range (unitless)",
204 dtype=float,
205 default=[0.002, 0.35],
206 )
207 tauSteps = pexConfig.Field(
208 doc="Aerosol Optical Depth number of steps",
209 dtype=int,
210 default=11,
211 )
212 alphaRange = pexConfig.ListField(
213 doc="Aerosol alpha range (unitless)",
214 dtype=float,
215 default=[0.0, 2.0],
216 )
217 alphaSteps = pexConfig.Field(
218 doc="Aerosol alpha number of steps",
219 dtype=int,
220 default=9,
221 )
222 zenithRange = pexConfig.ListField(
223 doc="Zenith angle range (degree)",
224 dtype=float,
225 default=[0.0, 70.0],
226 )
227 zenithSteps = pexConfig.Field(
228 doc="Zenith angle number of steps",
229 dtype=int,
230 default=21,
231 )
232 # Note that the standard atmosphere parameters depend on the observatory
233 # and elevation, and so these should be set on a per-camera basis.
234 pmbStd = pexConfig.Field(
235 doc=("Standard Atmosphere pressure (millibar); "
236 "Recommended default depends on the site."),
237 dtype=float,
238 default=None,
239 )
240 pwvStd = pexConfig.Field(
241 doc=("Standard Atmosphere PWV (mm); "
242 "Recommended default depends on the site."),
243 dtype=float,
244 default=None,
245 )
246 o3Std = pexConfig.Field(
247 doc="Standard Atmosphere O3 (dob)",
248 dtype=float,
249 default=263.0,
250 )
251 tauStd = pexConfig.Field(
252 doc="Standard Atmosphere aerosol optical depth",
253 dtype=float,
254 default=0.03,
255 )
256 alphaStd = pexConfig.Field(
257 doc="Standard Atmosphere aerosol alpha",
258 dtype=float,
259 default=1.0,
260 )
261 airmassStd = pexConfig.Field(
262 doc=("Standard Atmosphere airmass; "
263 "Recommended default depends on the survey strategy."),
264 dtype=float,
265 default=None,
266 )
267 lambdaNorm = pexConfig.Field(
268 doc="Aerosol Optical Depth normalization wavelength (Angstrom)",
269 dtype=float,
270 default=7750.0,
271 )
272 lambdaStep = pexConfig.Field(
273 doc="Wavelength step for generating atmospheres (nm)",
274 dtype=float,
275 default=0.5,
276 )
277 lambdaRange = pexConfig.ListField(
278 doc="Wavelength range for LUT (Angstrom)",
279 dtype=float,
280 default=[3000.0, 11000.0],
281 )
282
283
284class FgcmMakeLutConfig(pipeBase.PipelineTaskConfig,
285 pipelineConnections=FgcmMakeLutConnections):
286 """Config for FgcmMakeLutTask"""
287 physicalFilters = pexConfig.ListField(
288 doc="List of physicalFilter labels to generate look-up table.",
289 dtype=str,
290 default=[],
291 )
292 stdPhysicalFilterOverrideMap = pexConfig.DictField(
293 doc=("Override mapping from physical filter labels to 'standard' physical "
294 "filter labels. The 'standard' physical filter defines the transmission "
295 "curve that the FGCM standard bandpass will be based on. "
296 "Any filter not listed here will be mapped to "
297 "itself (e.g. g->g or HSC-G->HSC-G). Use this override for cross-"
298 "filter calibration such as HSC-R->HSC-R2 and HSC-I->HSC-I2."),
299 keytype=str,
300 itemtype=str,
301 default={},
302 )
303 atmosphereTableName = pexConfig.Field(
304 doc="FGCM name or filename of precomputed atmospheres",
305 dtype=str,
306 default=None,
307 optional=True,
308 )
309 useScienceDetectors = pexConfig.Field(
310 doc="Only use science detectors in LUT?",
311 dtype=bool,
312 default=True,
313 )
314 doOpticsTransmission = pexConfig.Field(
315 doc="Include optics transmission?",
316 dtype=bool,
317 default=True,
318 )
319 doSensorTransmission = pexConfig.Field(
320 doc="Include sensor transmission?",
321 dtype=bool,
322 default=True,
323 )
324 doFilterDetectorTransmission = pexConfig.Field(
325 doc="Use filter transmissions that are specified per-detector, rather "
326 "than a constant or radially-dependent filter transmission?",
327 dtype=bool,
328 default=False,
329 )
330 sensorCorrectionTermDict = pexConfig.ConfigDictField(
331 doc="Mapping of filter name to sensor correction terms.",
332 keytype=str,
333 itemtype=SensorCorrectionTerms,
334 default={},
335 )
336 parameters = pexConfig.ConfigField(
337 doc="Atmosphere parameters (required if no atmosphereTableName)",
338 dtype=FgcmMakeLutParametersConfig,
339 default=None,
340 check=None)
341
342 def validate(self):
343 """
344 Validate the config parameters.
345
346 This method behaves differently from the parent validate in the case
347 that atmosphereTableName is set. In this case, the config values
348 for standard values, step sizes, and ranges are loaded
349 directly from the specified atmosphereTableName.
350 """
351 # check that filterNames and stdFilterNames are okay
352 self._fields['physicalFilters'].validate(self)
353 self._fields['stdPhysicalFilterOverrideMap'].validate(self)
354
355 if self.atmosphereTableName is None:
356 # Validate the parameters
357 self._fields['parameters'].validate(self)
358
359
360class FgcmMakeLutTask(pipeBase.PipelineTask):
361 """
362 Make Look-Up Table for FGCM.
363
364 This task computes a look-up-table for the range in expected atmosphere
365 variation and variation in instrumental throughput (as tracked by the
366 transmission_filter products). By pre-computing linearized integrals,
367 the FGCM fit is orders of magnitude faster for stars with a broad range
368 of colors and observing bands, yielding precision at the 1-2 mmag level.
369
370 Computing a LUT requires running MODTRAN or with a pre-generated
371 atmosphere table packaged with fgcm.
372 """
373
374 ConfigClass = FgcmMakeLutConfig
375 _DefaultName = "fgcmMakeLut"
376
377 def __init__(self, initInputs=None, **kwargs):
378 super().__init__(**kwargs)
379
380 def runQuantum(self, butlerQC, inputRefs, outputRefs):
381 camera = butlerQC.get(inputRefs.camera)
382
383 if self.config.doOpticsTransmission:
384 opticsHandle = butlerQC.get(inputRefs.transmission_optics)
385 else:
386 opticsHandle = None
387
388 if self.config.doSensorTransmission:
389 sensorHandles = butlerQC.get(inputRefs.transmission_sensor)
390 sensorHandleDict = {sensorHandle.dataId['detector']: sensorHandle for
391 sensorHandle in sensorHandles}
392 else:
393 sensorHandleDict = {}
394
395 if self.config.doFilterDetectorTransmission:
396 filterHandles = butlerQC.get(inputRefs.transmission_filter_detector)
397 filterHandleDict = {
398 (filterHandle.dataId["physical_filter"], filterHandle.dataId["detector"]): filterHandle
399 for filterHandle in filterHandles
400 }
401 else:
402 filterHandles = butlerQC.get(inputRefs.transmission_filter)
403 filterHandleDict = {filterHandle.dataId['physical_filter']: filterHandle for
404 filterHandle in filterHandles}
405
406 filterToBand = {
407 filterHandle.dataId["physical_filter"]: filterHandle.dataId["band"]
408 for filterHandle in filterHandles
409 }
410
411 struct = self._fgcmMakeLut(
412 camera,
413 opticsHandle,
414 sensorHandleDict,
415 filterHandleDict,
416 filterToBand,
417 )
418
419 butlerQC.put(struct.fgcmLookUpTable, outputRefs.fgcmLookUpTable)
420 butlerQC.put(struct.fgcmStandardAtmosphere, outputRefs.fgcmStandardAtmosphere)
421
422 refDict = {passbandRef.dataId['physical_filter']: passbandRef for
423 passbandRef in outputRefs.fgcmStandardPassbands}
424 for physical_filter, passband in struct.fgcmStandardPassbands.items():
425 butlerQC.put(passband, refDict[physical_filter])
426
427 bandRefDict = {passbandRef.dataId["band"]: passbandRef for
428 passbandRef in outputRefs.standardPassbands}
429 for band, passband in struct.standardPassbands.items():
430 butlerQC.put(passband, bandRefDict[band])
431
432 def _fgcmMakeLut(self, camera, opticsHandle, sensorHandleDict,
433 filterHandleDict, filterToBand):
434 """
435 Make a FGCM Look-up Table
436
437 Parameters
438 ----------
439 camera : `lsst.afw.cameraGeom.Camera`
440 Camera from the butler.
441 opticsHandle : `lsst.daf.butler.DeferredDatasetHandle`
442 Reference to optics transmission curve.
443 sensorHandleDict : `dict` of [`int`, `lsst.daf.butler.DeferredDatasetHandle`]
444 Dictionary of references to sensor transmission curves. Key will
445 be detector id.
446 filterHandleDict : `dict` of [`str`, `lsst.daf.butler.DeferredDatasetHandle`]
447 Dictionary of references to filter transmission curves. Key will
448 be physical filter label or tuple of physical filter label and
449 detector.
450 filterToBand : `dict` [`str`: `str`]
451 Mapping of physical filter to band name.
452
453 Returns
454 -------
455 retStruct : `lsst.pipe.base.Struct`
456 Output structure with keys:
457
458 fgcmLookUpTable : `BaseCatalog`
459 The FGCM look-up table.
460 fgcmStandardAtmosphere : `lsst.afw.image.TransmissionCurve`
461 Transmission curve for the FGCM standard atmosphere.
462 fgcmStandardPassbands : `dict` [`str`, `lsst.afw.image.TransmissionCurve`]
463 Dictionary of fgcm standard passbands, with the key as the
464 physical filter name.
465 standardPassbands : `dict` [`str`, `astropy.table.Table`]
466 Dictionary of standard passbands in astropy table format, with
467 the key as the band name.
468 """
469 # number of ccds from the length of the camera iterator
470 nCcd = 0
471 for detector in camera:
472 if self.config.useScienceDetectors:
473 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE:
474 continue
475 nCcd += 1
476
477 self.log.info("Found %d ccds for look-up table" % (nCcd))
478
479 # Load in optics, etc.
480 self._loadThroughputs(camera,
481 opticsHandle,
482 sensorHandleDict,
483 filterHandleDict)
484
485 lutConfig = self._createLutConfig(nCcd)
486
487 # make the lut object
488 self.log.info("Making the LUT maker object")
489 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig)
490
491 # generate the throughput dictionary.
492
493 # these will be in Angstroms
494 # note that lambdaStep is currently in nm, because of historical
495 # reasons in the code. Convert to Angstroms here.
496 throughputLambda = np.arange(self.fgcmLutMaker.lambdaRange[0],
497 self.fgcmLutMaker.lambdaRange[1]+self.fgcmLutMaker.lambdaStep*10,
498 self.fgcmLutMaker.lambdaStep*10.)
499
500 self.log.info("Built throughput lambda, %.1f-%.1f, step %.2f" %
501 (throughputLambda[0], throughputLambda[-1],
502 throughputLambda[1] - throughputLambda[0]))
503
504 throughputDict = {}
505 for i, physicalFilter in enumerate(self.config.physicalFilters):
506 tDict = {}
507 tDict['LAMBDA'] = throughputLambda
508 for ccdIndex, detector in enumerate(camera):
509 if self.config.useScienceDetectors:
510 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE:
511 continue
512 tDict[ccdIndex] = self._getThroughputDetector(detector, physicalFilter, throughputLambda)
513 throughputDict[physicalFilter] = tDict
514
515 # set the throughputs
516 self.fgcmLutMaker.setThroughputs(throughputDict)
517
518 # make the LUT
519 self.log.info("Making LUT")
520 self.fgcmLutMaker.makeLUT()
521
522 # and save the LUT
523
524 # build the index values
525 comma = ','
526 physicalFilterString = comma.join(self.config.physicalFilters)
527 stdPhysicalFilterString = comma.join(self._getStdPhysicalFilterList())
528
529 atmosphereTableName = 'NoTableWasUsed'
530 if self.config.atmosphereTableName is not None:
531 atmosphereTableName = self.config.atmosphereTableName
532
533 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString,
534 atmosphereTableName)
535
536 lutCat = self._makeLutCat(lutSchema, physicalFilterString,
537 stdPhysicalFilterString, atmosphereTableName)
538
539 atmStd = TransmissionCurve.makeSpatiallyConstant(
540 throughput=self.fgcmLutMaker.atmStdTrans.astype(np.float64),
541 wavelengths=self.fgcmLutMaker.atmLambda.astype(np.float64),
542 throughputAtMin=self.fgcmLutMaker.atmStdTrans[0],
543 throughputAtMax=self.fgcmLutMaker.atmStdTrans[1],
544 )
545
546 fgcmStandardPassbands = {}
547 for i, physical_filter in enumerate(self.fgcmLutMaker.filterNames):
548 passband = self.fgcmLutMaker.throughputs[i]['THROUGHPUT_AVG']*self.fgcmLutMaker.atmStdTrans
549 fgcmStandardPassbands[physical_filter] = TransmissionCurve.makeSpatiallyConstant(
550 throughput=passband.astype(np.float64),
551 wavelengths=self.fgcmLutMaker.atmLambda.astype(np.float64),
552 throughputAtMin=passband[0],
553 throughputAtMax=passband[-1],
554 )
555
556 standardPassbands = {}
557 for i, physical_filter in enumerate(self.fgcmLutMaker.filterNames):
558 if physical_filter != self.fgcmLutMaker.stdFilterNames[i]:
559 # This filter does not map onto one of the "standard"
560 # passbands. E.g., for HSC we have HSC-R and HSC-R2 which
561 # both map onto HSC-R2 which sets the "standard".
562 continue
563 band = filterToBand[physical_filter]
564 passband = self.fgcmLutMaker.throughputs[i]['THROUGHPUT_AVG']*self.fgcmLutMaker.atmStdTrans
565 passbandTable = Table(
566 {
567 "wavelength": (self.fgcmLutMaker.atmLambda.astype(np.float64)/10.)*units.nm,
568 "throughput": (passband*100.)*units.percent,
569 },
570 )
571 passbandTable["wavelength"].description = "Wavelength bin centers"
572 standardPassbands[band] = passbandTable
573
574 retStruct = pipeBase.Struct(
575 fgcmLookUpTable=lutCat,
576 fgcmStandardAtmosphere=atmStd,
577 fgcmStandardPassbands=fgcmStandardPassbands,
578 standardPassbands=standardPassbands,
579 )
580
581 return retStruct
582
584 """Get the standard physical filter lists from config.physicalFilters
585 and config.stdPhysicalFilterOverrideMap
586
587 Returns
588 -------
589 stdPhysicalFilters : `list`
590 """
591 override = self.config.stdPhysicalFilterOverrideMap
592 return [override.get(physicalFilter, physicalFilter) for
593 physicalFilter in self.config.physicalFilters]
594
595 def _createLutConfig(self, nCcd):
596 """
597 Create the fgcmLut config dictionary
598
599 Parameters
600 ----------
601 nCcd: `int`
602 Number of CCDs in the camera
603 """
604
605 # create the common stub of the lutConfig
606 lutConfig = {}
607 lutConfig['logger'] = self.log
608 lutConfig['filterNames'] = self.config.physicalFilters
609 lutConfig['stdFilterNames'] = self._getStdPhysicalFilterList()
610 lutConfig['nCCD'] = nCcd
611
612 # atmosphereTable already validated if available
613 if self.config.atmosphereTableName is not None:
614 lutConfig['atmosphereTableName'] = self.config.atmosphereTableName
615 else:
616 # use the regular paramters (also validated if needed)
617 lutConfig['elevation'] = self.config.parameters.elevation
618 lutConfig['pmbRange'] = self.config.parameters.pmbRange
619 lutConfig['pmbSteps'] = self.config.parameters.pmbSteps
620 lutConfig['pwvRange'] = self.config.parameters.pwvRange
621 lutConfig['pwvSteps'] = self.config.parameters.pwvSteps
622 lutConfig['o3Range'] = self.config.parameters.o3Range
623 lutConfig['o3Steps'] = self.config.parameters.o3Steps
624 lutConfig['tauRange'] = self.config.parameters.tauRange
625 lutConfig['tauSteps'] = self.config.parameters.tauSteps
626 lutConfig['alphaRange'] = self.config.parameters.alphaRange
627 lutConfig['alphaSteps'] = self.config.parameters.alphaSteps
628 lutConfig['zenithRange'] = self.config.parameters.zenithRange
629 lutConfig['zenithSteps'] = self.config.parameters.zenithSteps
630 lutConfig['pmbStd'] = self.config.parameters.pmbStd
631 lutConfig['pwvStd'] = self.config.parameters.pwvStd
632 lutConfig['o3Std'] = self.config.parameters.o3Std
633 lutConfig['tauStd'] = self.config.parameters.tauStd
634 lutConfig['alphaStd'] = self.config.parameters.alphaStd
635 lutConfig['airmassStd'] = self.config.parameters.airmassStd
636 lutConfig['lambdaRange'] = self.config.parameters.lambdaRange
637 lutConfig['lambdaStep'] = self.config.parameters.lambdaStep
638 lutConfig['lambdaNorm'] = self.config.parameters.lambdaNorm
639
640 # Add any per-filter correction term updates if necessary.
641 # Note that sensorCTerms is the name of the config field in fgcm.
642 if self.config.sensorCorrectionTermDict:
643 lutConfig['sensorCTerms'] = {}
644 for key, value in self.config.sensorCorrectionTermDict.items():
645 lutConfig['sensorCTerms'][key] = (
646 value.refLambda,
647 dict(value.correctionTermDict),
648 )
649
650 return lutConfig
651
652 def _loadThroughputs(self, camera, opticsHandle, sensorHandleDict, filterHandleDict):
653 """Internal method to load throughput data for filters
654
655 Parameters
656 ----------
657 camera: `lsst.afw.cameraGeom.Camera`
658 Camera from the butler
659 opticsHandle : `lsst.daf.butler.DeferredDatasetHandle`
660 Reference to optics transmission curve.
661 sensorHandleDict : `dict` of [`int`, `lsst.daf.butler.DeferredDatasetHandle`]
662 Dictionary of references to sensor transmission curves. Key will
663 be detector id.
664 filterHandleDict : `dict` of [`str`, `lsst.daf.butler.DeferredDatasetHandle`]
665 Dictionary of references to filter transmission curves. Key will
666 be physical filter label.
667
668 Raises
669 ------
670 ValueError : Raised if configured filter name does not match any of the
671 available filter transmission curves.
672 """
673 if self.config.doOpticsTransmission:
674 self._opticsTransmission = opticsHandle.get()
675 else:
676 self._opticsTransmission = TransmissionCurve.makeSpatiallyConstant(
677 throughput=np.ones(100),
678 wavelengths=np.linspace(
679 self.config.parameters.lambdaRange[0],
680 self.config.parameters.lambdaRange[1],
681 100,
682 ),
683 throughputAtMin=1.0,
684 throughputAtMax=1.0,
685 )
686
688 for detector in camera:
689 if self.config.useScienceDetectors:
690 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE:
691 continue
692 if self.config.doSensorTransmission:
693 self._sensorsTransmission[detector.getId()] = sensorHandleDict[detector.getId()].get()
694 else:
695 self._sensorsTransmission[detector.getId()] = TransmissionCurve.makeSpatiallyConstant(
696 throughput=np.ones(100),
697 wavelengths=np.linspace(
698 self.config.parameters.lambdaRange[0],
699 self.config.parameters.lambdaRange[1],
700 100,
701 ),
702 throughputAtMin=1.0,
703 throughputAtMax=1.0,
704 )
705
707 if self.config.doFilterDetectorTransmission:
708 for physicalFilter in self.config.physicalFilters:
709 for detector in camera:
710 if self.config.useScienceDetectors:
711 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE:
712 continue
713 key = (physicalFilter, detector.getId())
714 self._filtersTransmission[key] = filterHandleDict[key].get()
715 else:
716 for physicalFilter in self.config.physicalFilters:
717 self._filtersTransmission[physicalFilter] = filterHandleDict[physicalFilter].get()
718
719 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda):
720 """Internal method to get throughput for a detector.
721
722 Returns the throughput at the center of the detector for a given filter.
723
724 Parameters
725 ----------
726 detector: `lsst.afw.cameraGeom._detector.Detector`
727 Detector on camera
728 physicalFilter: `str`
729 Physical filter label
730 throughputLambda: `np.array(dtype=np.float64)`
731 Wavelength steps (Angstrom)
732
733 Returns
734 -------
735 throughput: `np.array(dtype=np.float64)`
736 Throughput (max 1.0) at throughputLambda
737 """
738
739 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE)
740 c.scale(1.0/detector.getPixelSize()[0]) # Assumes x and y pixel sizes in arcsec are the same
741
742 throughput = self._opticsTransmission.sampleAt(position=c,
743 wavelengths=throughputLambda)
744
745 throughput *= self._sensorsTransmission[detector.getId()].sampleAt(position=c,
746 wavelengths=throughputLambda)
747
748 if self.config.doFilterDetectorTransmission:
749 throughput *= self._filtersTransmission[(physicalFilter, detector.getId())].sampleAt(
750 position=c,
751 wavelengths=throughputLambda,
752 )
753 else:
754 throughput *= self._filtersTransmission[physicalFilter].sampleAt(
755 position=c,
756 wavelengths=throughputLambda,
757 )
758
759 # Clip the throughput from 0 to 1
760 throughput = np.clip(throughput, 0.0, 1.0)
761
762 return throughput
763
764 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString,
765 atmosphereTableName):
766 """
767 Make the LUT schema
768
769 Parameters
770 ----------
771 physicalFilterString: `str`
772 Combined string of all the physicalFilters
773 stdPhysicalFilterString: `str`
774 Combined string of all the standard physicalFilters
775 atmosphereTableName: `str`
776 Name of the atmosphere table used to generate LUT
777
778 Returns
779 -------
780 lutSchema: `afwTable.schema`
781 """
782
783 lutSchema = afwTable.Schema()
784
785 lutSchema.addField('tablename', type=str, doc='Atmosphere table name',
786 size=len(atmosphereTableName))
787 lutSchema.addField('elevation', type=float, doc="Telescope elevation used for LUT")
788 lutSchema.addField('physicalFilters', type=str, doc='physicalFilters in LUT',
789 size=len(physicalFilterString))
790 lutSchema.addField('stdPhysicalFilters', type=str, doc='Standard physicalFilters in LUT',
791 size=len(stdPhysicalFilterString))
792 lutSchema.addField('pmb', type='ArrayD', doc='Barometric Pressure',
793 size=self.fgcmLutMaker.pmb.size)
794 lutSchema.addField('pmbFactor', type='ArrayD', doc='PMB scaling factor',
795 size=self.fgcmLutMaker.pmb.size)
796 lutSchema.addField('pmbElevation', type=np.float64, doc='PMB Scaling at elevation')
797 lutSchema.addField('pwv', type='ArrayD', doc='Preciptable Water Vapor',
798 size=self.fgcmLutMaker.pwv.size)
799 lutSchema.addField('o3', type='ArrayD', doc='Ozone',
800 size=self.fgcmLutMaker.o3.size)
801 lutSchema.addField('tau', type='ArrayD', doc='Aerosol optical depth',
802 size=self.fgcmLutMaker.tau.size)
803 lutSchema.addField('lambdaNorm', type=np.float64, doc='AOD wavelength')
804 lutSchema.addField('alpha', type='ArrayD', doc='Aerosol alpha',
805 size=self.fgcmLutMaker.alpha.size)
806 lutSchema.addField('zenith', type='ArrayD', doc='Zenith angle',
807 size=self.fgcmLutMaker.zenith.size)
808 lutSchema.addField('nCcd', type=np.int32, doc='Number of CCDs')
809
810 # and the standard values
811 lutSchema.addField('pmbStd', type=np.float64, doc='PMB Standard')
812 lutSchema.addField('pwvStd', type=np.float64, doc='PWV Standard')
813 lutSchema.addField('o3Std', type=np.float64, doc='O3 Standard')
814 lutSchema.addField('tauStd', type=np.float64, doc='Tau Standard')
815 lutSchema.addField('alphaStd', type=np.float64, doc='Alpha Standard')
816 lutSchema.addField('zenithStd', type=np.float64, doc='Zenith angle Standard')
817 lutSchema.addField('lambdaRange', type='ArrayD', doc='Wavelength range',
818 size=2)
819 lutSchema.addField('lambdaStep', type=np.float64, doc='Wavelength step')
820 lutSchema.addField('lambdaStd', type='ArrayD', doc='Standard Wavelength',
821 size=len(self.fgcmLutMaker.filterNames))
822 lutSchema.addField('lambdaStdFilter', type='ArrayD', doc='Standard Wavelength (raw)',
823 size=len(self.fgcmLutMaker.filterNames))
824 lutSchema.addField('i0Std', type='ArrayD', doc='I0 Standard',
825 size=len(self.fgcmLutMaker.filterNames))
826 lutSchema.addField('i1Std', type='ArrayD', doc='I1 Standard',
827 size=len(self.fgcmLutMaker.filterNames))
828 lutSchema.addField('i10Std', type='ArrayD', doc='I10 Standard',
829 size=len(self.fgcmLutMaker.filterNames))
830 lutSchema.addField('i2Std', type='ArrayD', doc='I2 Standard',
831 size=len(self.fgcmLutMaker.filterNames))
832 lutSchema.addField('lambdaB', type='ArrayD', doc='Wavelength for passband (no atm)',
833 size=len(self.fgcmLutMaker.filterNames))
834 lutSchema.addField('atmLambda', type='ArrayD', doc='Atmosphere wavelengths (Angstrom)',
835 size=self.fgcmLutMaker.atmLambda.size)
836 lutSchema.addField('atmStdTrans', type='ArrayD', doc='Standard Atmosphere Throughput',
837 size=self.fgcmLutMaker.atmStdTrans.size)
838
839 # and the look-up-tables
840 lutSchema.addField('luttype', type=str, size=20, doc='Look-up table type')
841 lutSchema.addField('lut', type='ArrayF', doc='Look-up table for luttype',
842 size=self.fgcmLutMaker.lut['I0'].size)
843
844 return lutSchema
845
846 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString,
847 atmosphereTableName):
848 """
849 Make the LUT schema
850
851 Parameters
852 ----------
853 lutSchema: `afwTable.schema`
854 Lut catalog schema
855 physicalFilterString: `str`
856 Combined string of all the physicalFilters
857 stdPhysicalFilterString: `str`
858 Combined string of all the standard physicalFilters
859 atmosphereTableName: `str`
860 Name of the atmosphere table used to generate LUT
861
862 Returns
863 -------
864 lutCat: `afwTable.BaseCatalog`
865 Look-up table catalog for persistence.
866 """
867
868 # The somewhat strange format is to make sure that
869 # the rows of the afwTable do not get too large
870 # (see DM-11419)
871
872 lutCat = afwTable.BaseCatalog(lutSchema)
873 lutCat.table.preallocate(14)
874
875 # first fill the first index
876 rec = lutCat.addNew()
877
878 rec['tablename'] = atmosphereTableName
879 rec['elevation'] = self.fgcmLutMaker.atmosphereTable.elevation
880 rec['physicalFilters'] = physicalFilterString
881 rec['stdPhysicalFilters'] = stdPhysicalFilterString
882 rec['pmb'][:] = self.fgcmLutMaker.pmb
883 rec['pmbFactor'][:] = self.fgcmLutMaker.pmbFactor
884 rec['pmbElevation'] = self.fgcmLutMaker.pmbElevation
885 rec['pwv'][:] = self.fgcmLutMaker.pwv
886 rec['o3'][:] = self.fgcmLutMaker.o3
887 rec['tau'][:] = self.fgcmLutMaker.tau
888 rec['lambdaNorm'] = self.fgcmLutMaker.lambdaNorm
889 rec['alpha'][:] = self.fgcmLutMaker.alpha
890 rec['zenith'][:] = self.fgcmLutMaker.zenith
891 rec['nCcd'] = self.fgcmLutMaker.nCCD
892
893 rec['pmbStd'] = self.fgcmLutMaker.pmbStd
894 rec['pwvStd'] = self.fgcmLutMaker.pwvStd
895 rec['o3Std'] = self.fgcmLutMaker.o3Std
896 rec['tauStd'] = self.fgcmLutMaker.tauStd
897 rec['alphaStd'] = self.fgcmLutMaker.alphaStd
898 rec['zenithStd'] = self.fgcmLutMaker.zenithStd
899 rec['lambdaRange'][:] = self.fgcmLutMaker.lambdaRange
900 rec['lambdaStep'] = self.fgcmLutMaker.lambdaStep
901 rec['lambdaStd'][:] = self.fgcmLutMaker.lambdaStd
902 rec['lambdaStdFilter'][:] = self.fgcmLutMaker.lambdaStdFilter
903 rec['i0Std'][:] = self.fgcmLutMaker.I0Std
904 rec['i1Std'][:] = self.fgcmLutMaker.I1Std
905 rec['i10Std'][:] = self.fgcmLutMaker.I10Std
906 rec['i2Std'][:] = self.fgcmLutMaker.I2Std
907 rec['lambdaB'][:] = self.fgcmLutMaker.lambdaB
908 rec['atmLambda'][:] = self.fgcmLutMaker.atmLambda
909 rec['atmStdTrans'][:] = self.fgcmLutMaker.atmStdTrans
910
911 rec['luttype'] = 'I0'
912 rec['lut'][:] = self.fgcmLutMaker.lut['I0'].flatten()
913
914 # and add the rest
915 rec = lutCat.addNew()
916 rec['luttype'] = 'I1'
917 rec['lut'][:] = self.fgcmLutMaker.lut['I1'].flatten()
918
919 derivTypes = ['D_PMB', 'D_LNPWV', 'D_O3', 'D_LNTAU', 'D_ALPHA', 'D_SECZENITH',
920 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1',
921 'D_SECZENITH_I1']
922 for derivType in derivTypes:
923 rec = lutCat.addNew()
924 rec['luttype'] = derivType
925 rec['lut'][:] = self.fgcmLutMaker.lutDeriv[derivType].flatten()
926
927 return lutCat
Defines the fields and offsets for a table.
Definition Schema.h:51
_makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, atmosphereTableName)
_getThroughputDetector(self, detector, physicalFilter, throughputLambda)
_loadThroughputs(self, camera, opticsHandle, sensorHandleDict, filterHandleDict)
_fgcmMakeLut(self, camera, opticsHandle, sensorHandleDict, filterHandleDict, filterToBand)
runQuantum(self, butlerQC, inputRefs, outputRefs)
__init__(self, initInputs=None, **kwargs)
_makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString, atmosphereTableName)