23"""Make the final fgcmcal output products.
25This task takes the final output from fgcmFitCycle and produces the following
26outputs for use in the DM stack: the FGCM standard stars in a reference
27catalog format; the model atmospheres in "transmission_atmosphere_fgcm"
28format; and the zeropoints in "fgcm_photoCalib" format. Optionally, the
29task can transfer the 'absolute' calibration
from a reference catalog
30to put the fgcm standard stars
in units of Jansky. This
is accomplished
31by matching stars
in a sample of healpix pixels,
and applying the median
39from astropy import units
41import lsst.daf.base as dafBase
42import lsst.pex.config as pexConfig
43import lsst.pipe.base as pipeBase
44from lsst.pipe.base import connectionTypes
45from lsst.afw.image import TransmissionCurve
46from lsst.meas.algorithms import ReferenceObjectLoader, LoadReferenceObjectsConfig
47from lsst.pipe.tasks.photoCal import PhotoCalTask
49import lsst.afw.image as afwImage
50import lsst.afw.math as afwMath
51import lsst.afw.table as afwTable
53from .utilities import computeApproxPixelAreaFields
54from .utilities import FGCM_ILLEGAL_VALUE
58__all__ = ['FgcmOutputProductsConfig', 'FgcmOutputProductsTask']
61class FgcmOutputProductsConnections(pipeBase.PipelineTaskConnections,
62 dimensions=("instrument",),
63 defaultTemplates={
"cycleNumber":
"0"}):
64 camera = connectionTypes.PrerequisiteInput(
65 doc=
"Camera instrument",
67 storageClass=
"Camera",
68 dimensions=(
"instrument",),
72 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
73 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
74 "chromatic corrections."),
75 name=
"fgcmLookUpTable",
76 storageClass=
"Catalog",
77 dimensions=(
"instrument",),
81 fgcmVisitCatalog = connectionTypes.Input(
82 doc=
"Catalog of visit information for fgcm",
83 name=
"fgcmVisitCatalog",
84 storageClass=
"Catalog",
85 dimensions=(
"instrument",),
89 fgcmStandardStars = connectionTypes.Input(
90 doc=
"Catalog of standard star data from fgcm fit",
91 name=
"fgcmStandardStars{cycleNumber}",
92 storageClass=
"SimpleCatalog",
93 dimensions=(
"instrument",),
97 fgcmZeropoints = connectionTypes.Input(
98 doc=
"Catalog of zeropoints from fgcm fit",
99 name=
"fgcmZeropoints{cycleNumber}",
100 storageClass=
"Catalog",
101 dimensions=(
"instrument",),
105 fgcmAtmosphereParameters = connectionTypes.Input(
106 doc=
"Catalog of atmosphere parameters from fgcm fit",
107 name=
"fgcmAtmosphereParameters{cycleNumber}",
108 storageClass=
"Catalog",
109 dimensions=(
"instrument",),
113 refCat = connectionTypes.PrerequisiteInput(
114 doc=
"Reference catalog to use for photometric calibration",
116 storageClass=
"SimpleCatalog",
117 dimensions=(
"skypix",),
122 fgcmPhotoCalib = connectionTypes.Output(
123 doc=(
"Per-visit photometric calibrations derived from fgcm calibration. "
124 "These catalogs use detector id for the id and are sorted for "
125 "fast lookups of a detector."),
126 name=
"fgcmPhotoCalibCatalog",
127 storageClass=
"ExposureCatalog",
128 dimensions=(
"instrument",
"visit",),
132 fgcmTransmissionAtmosphere = connectionTypes.Output(
133 doc=
"Per-visit atmosphere transmission files produced from fgcm calibration",
134 name=
"transmission_atmosphere_fgcm",
135 storageClass=
"TransmissionCurve",
136 dimensions=(
"instrument",
141 fgcmOffsets = connectionTypes.Output(
142 doc=
"Per-band offsets computed from doReferenceCalibration",
143 name=
"fgcmReferenceCalibrationOffsets",
144 storageClass=
"Catalog",
145 dimensions=(
"instrument",),
149 def __init__(self, *, config=None):
150 super().__init__(config=config)
152 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
153 raise ValueError(
"cycleNumber must be of integer format")
155 if not config.doReferenceCalibration:
156 self.prerequisiteInputs.remove(
"refCat")
157 if not config.doAtmosphereOutput:
158 self.inputs.remove(
"fgcmAtmosphereParameters")
159 if not config.doZeropointOutput:
160 self.inputs.remove(
"fgcmZeropoints")
161 if not config.doReferenceCalibration:
162 self.outputs.remove(
"fgcmOffsets")
164 def getSpatialBoundsConnections(self):
165 return (
"fgcmPhotoCalib",)
168class FgcmOutputProductsConfig(pipeBase.PipelineTaskConfig,
169 pipelineConnections=FgcmOutputProductsConnections):
170 """Config for FgcmOutputProductsTask"""
172 physicalFilterMap = pexConfig.DictField(
173 doc=
"Mapping from 'physicalFilter' to band.",
180 doReferenceCalibration = pexConfig.Field(
181 doc=(
"Transfer 'absolute' calibration from reference catalog? "
182 "This afterburner step is unnecessary if reference stars "
183 "were used in the full fit in FgcmFitCycleTask."),
187 doAtmosphereOutput = pexConfig.Field(
188 doc=
"Output atmospheres in transmission_atmosphere_fgcm format",
192 doZeropointOutput = pexConfig.Field(
193 doc=
"Output zeropoints in fgcm_photoCalib format",
197 doComposeWcsJacobian = pexConfig.Field(
198 doc=
"Compose Jacobian of WCS with fgcm calibration for output photoCalib?",
202 doApplyMeanChromaticCorrection = pexConfig.Field(
203 doc=
"Apply the mean chromatic correction to the zeropoints?",
207 photoCal = pexConfig.ConfigurableField(
209 doc=
"task to perform 'absolute' calibration",
211 referencePixelizationNside = pexConfig.Field(
212 doc=
"Healpix nside to pixelize catalog to compare to reference catalog",
216 referencePixelizationMinStars = pexConfig.Field(
217 doc=(
"Minimum number of stars per healpix pixel to select for comparison"
218 "to the specified reference catalog"),
222 referenceMinMatch = pexConfig.Field(
223 doc=
"Minimum number of stars matched to reference catalog to be used in statistics",
227 referencePixelizationNPixels = pexConfig.Field(
228 doc=(
"Number of healpix pixels to sample to do comparison. "
229 "Doing too many will take a long time and not yield any more "
230 "precise results because the final number is the median offset "
231 "(per band) from the set of pixels."),
236 def setDefaults(self):
237 pexConfig.Config.setDefaults(self)
247 self.photoCal.applyColorTerms =
False
248 self.photoCal.fluxField =
'instFlux'
249 self.photoCal.magErrFloor = 0.003
250 self.photoCal.match.referenceSelection.doSignalToNoise =
True
251 self.photoCal.match.referenceSelection.signalToNoise.minimum = 10.0
252 self.photoCal.match.sourceSelection.doSignalToNoise =
True
253 self.photoCal.match.sourceSelection.signalToNoise.minimum = 10.0
254 self.photoCal.match.sourceSelection.signalToNoise.fluxField =
'instFlux'
255 self.photoCal.match.sourceSelection.signalToNoise.errField =
'instFluxErr'
256 self.photoCal.match.sourceSelection.doFlags =
True
257 self.photoCal.match.sourceSelection.flags.good = []
258 self.photoCal.match.sourceSelection.flags.bad = [
'flag_badStar']
259 self.photoCal.match.sourceSelection.doUnresolved =
False
260 self.photoCal.match.sourceSelection.doRequirePrimary =
False
263class FgcmOutputProductsTask(pipeBase.PipelineTask):
265 Output products from FGCM
global calibration.
268 ConfigClass = FgcmOutputProductsConfig
269 _DefaultName = "fgcmOutputProducts"
271 def __init__(self, **kwargs):
272 super().__init__(**kwargs)
274 def runQuantum(self, butlerQC, inputRefs, outputRefs):
276 handleDict[
'camera'] = butlerQC.get(inputRefs.camera)
277 handleDict[
'fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
278 handleDict[
'fgcmVisitCatalog'] = butlerQC.get(inputRefs.fgcmVisitCatalog)
279 handleDict[
'fgcmStandardStars'] = butlerQC.get(inputRefs.fgcmStandardStars)
281 if self.config.doZeropointOutput:
282 handleDict[
'fgcmZeropoints'] = butlerQC.get(inputRefs.fgcmZeropoints)
283 photoCalibRefDict = {photoCalibRef.dataId.byName()[
'visit']:
284 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
286 if self.config.doAtmosphereOutput:
287 handleDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
288 atmRefDict = {atmRef.dataId.byName()[
'visit']: atmRef
for
289 atmRef
in outputRefs.fgcmTransmissionAtmosphere}
291 if self.config.doReferenceCalibration:
292 refConfig = LoadReferenceObjectsConfig()
293 self.refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
294 for ref
in inputRefs.refCat],
295 refCats=butlerQC.get(inputRefs.refCat),
296 name=self.config.connections.refCat,
300 self.refObjLoader =
None
302 struct = self.run(handleDict, self.config.physicalFilterMap)
305 if struct.photoCalibCatalogs
is not None:
306 self.log.info(
"Outputting photoCalib catalogs.")
307 for visit, expCatalog
in struct.photoCalibCatalogs:
308 butlerQC.put(expCatalog, photoCalibRefDict[visit])
309 self.log.info(
"Done outputting photoCalib catalogs.")
312 if struct.atmospheres
is not None:
313 self.log.info(
"Outputting atmosphere transmission files.")
314 for visit, atm
in struct.atmospheres:
315 butlerQC.put(atm, atmRefDict[visit])
316 self.log.info(
"Done outputting atmosphere files.")
318 if self.config.doReferenceCalibration:
320 schema = afwTable.Schema()
321 schema.addField(
'offset', type=np.float64,
322 doc=
"Post-process calibration offset (mag)")
323 offsetCat = afwTable.BaseCatalog(schema)
324 offsetCat.resize(len(struct.offsets))
325 offsetCat[
'offset'][:] = struct.offsets
327 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
331 def run(self, handleDict, physicalFilterMap):
332 """Run the output products task.
337 All handles are `lsst.daf.butler.DeferredDatasetHandle`
338 handle dictionary with keys:
341 Camera object (`lsst.afw.cameraGeom.Camera`)
342 ``
"fgcmLookUpTable"``
343 handle
for the FGCM look-up table.
344 ``
"fgcmVisitCatalog"``
345 handle
for visit summary catalog.
346 ``
"fgcmStandardStars"``
347 handle
for the output standard star catalog.
349 handle
for the zeropoint data catalog.
350 ``
"fgcmAtmosphereParameters"``
351 handle
for the atmosphere parameter catalog.
352 ``
"fgcmBuildStarsTableConfig"``
353 Config
for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
354 physicalFilterMap : `dict`
355 Dictionary of mappings
from physical filter to FGCM band.
359 retStruct : `lsst.pipe.base.Struct`
360 Output structure
with keys:
362 offsets : `np.ndarray`
363 Final reference offsets, per band.
364 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
365 Generator that returns (visit, transmissionCurve) tuples.
366 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
367 Generator that returns (visit, exposureCatalog) tuples.
369 stdCat = handleDict['fgcmStandardStars'].get()
370 md = stdCat.getMetadata()
371 bands = md.getArray(
'BANDS')
373 if self.config.doReferenceCalibration:
374 lutCat = handleDict[
'fgcmLookUpTable'].get()
375 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
377 offsets = np.zeros(len(bands))
381 if self.config.doZeropointOutput:
382 zptCat = handleDict[
'fgcmZeropoints'].get()
383 visitCat = handleDict[
'fgcmVisitCatalog'].get()
385 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
390 if self.config.doAtmosphereOutput:
391 atmCat = handleDict[
'fgcmAtmosphereParameters'].get()
392 atmgen = self._outputAtmospheres(handleDict, atmCat)
396 retStruct = pipeBase.Struct(offsets=offsets,
398 retStruct.photoCalibCatalogs = pcgen
402 def generateTractOutputProducts(self, handleDict, tract,
403 visitCat, zptCat, atmCat, stdCat,
404 fgcmBuildStarsConfig):
406 Generate the output products for a given tract,
as specified
in the config.
408 This method
is here to have an alternate entry-point
for
414 All handles are `lsst.daf.butler.DeferredDatasetHandle`
415 handle dictionary
with keys:
418 Camera object (`lsst.afw.cameraGeom.Camera`)
419 ``
"fgcmLookUpTable"``
420 handle
for the FGCM look-up table.
423 visitCat : `lsst.afw.table.BaseCatalog`
424 FGCM visitCat
from `FgcmBuildStarsTask`
425 zptCat : `lsst.afw.table.BaseCatalog`
426 FGCM zeropoint catalog
from `FgcmFitCycleTask`
427 atmCat : `lsst.afw.table.BaseCatalog`
428 FGCM atmosphere parameter catalog
from `FgcmFitCycleTask`
429 stdCat : `lsst.afw.table.SimpleCatalog`
430 FGCM standard star catalog
from `FgcmFitCycleTask`
431 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
432 Configuration object
from `FgcmBuildStarsTask`
436 retStruct : `lsst.pipe.base.Struct`
437 Output structure
with keys:
439 offsets : `np.ndarray`
440 Final reference offsets, per band.
441 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
442 Generator that returns (visit, transmissionCurve) tuples.
443 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
444 Generator that returns (visit, exposureCatalog) tuples.
446 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
448 md = stdCat.getMetadata()
449 bands = md.getArray('BANDS')
451 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
452 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
453 "in fgcmBuildStarsTask.")
455 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
456 self.log.warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
458 if self.config.doReferenceCalibration:
459 lutCat = handleDict[
'fgcmLookUpTable'].get()
460 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
462 offsets = np.zeros(len(bands))
464 if self.config.doZeropointOutput:
465 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
470 if self.config.doAtmosphereOutput:
471 atmgen = self._outputAtmospheres(handleDict, atmCat)
475 retStruct = pipeBase.Struct(offsets=offsets,
477 retStruct.photoCalibCatalogs = pcgen
481 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
483 Compute offsets relative to a reference catalog.
485 This method splits the star catalog into healpix pixels
486 and computes the calibration transfer
for a sample of
487 these pixels to approximate the
'absolute' calibration
488 values (on
for each band) to apply to transfer the
493 stdCat : `lsst.afw.table.SimpleCatalog`
495 lutCat : `lsst.afw.table.SimpleCatalog`
497 physicalFilterMap : `dict`
498 Dictionary of mappings
from physical filter to FGCM band.
499 bands : `list` [`str`]
500 List of band names
from FGCM output
503 offsets : `numpy.array` of floats
504 Per band zeropoint offsets
510 minObs = stdCat[
'ngood'].min(axis=1)
512 goodStars = (minObs >= 1)
513 stdCat = stdCat[goodStars]
515 self.log.info(
"Found %d stars with at least 1 good observation in each band" %
522 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
523 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
524 physicalFilterMapBands = list(physicalFilterMap.values())
525 physicalFilterMapFilters = list(physicalFilterMap.keys())
529 physicalFilterMapIndex = physicalFilterMapBands.index(band)
530 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
532 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
533 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
534 filterLabels.append(afwImage.FilterLabel(band=band,
535 physical=stdPhysicalFilter))
544 sourceMapper = afwTable.SchemaMapper(stdCat.schema)
545 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
546 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
547 doc=
"instrumental flux (counts)")
548 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
549 doc=
"instrumental flux error (counts)")
550 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
558 ipring = hpg.angle_to_pixel(
559 self.config.referencePixelizationNside,
564 h, rev = esutil.stat.histogram(ipring, rev=
True)
566 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
568 self.log.info(
"Found %d pixels (nside=%d) with at least %d good stars" %
570 self.config.referencePixelizationNside,
571 self.config.referencePixelizationMinStars))
573 if gdpix.size < self.config.referencePixelizationNPixels:
574 self.log.warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
575 (gdpix.size, self.config.referencePixelizationNPixels))
578 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
580 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
581 (
'nstar',
'i4', len(bands)),
582 (
'nmatch',
'i4', len(bands)),
583 (
'zp',
'f4', len(bands)),
584 (
'zpErr',
'f4', len(bands))])
585 results[
'hpix'] = ipring[rev[rev[gdpix]]]
588 selected = np.zeros(len(stdCat), dtype=bool)
590 refFluxFields = [
None]*len(bands)
592 for p_index, pix
in enumerate(gdpix):
593 i1a = rev[rev[pix]: rev[pix + 1]]
601 for b_index, filterLabel
in enumerate(filterLabels):
602 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
604 selected, refFluxFields)
605 results[
'nstar'][p_index, b_index] = len(i1a)
606 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
607 results[
'zp'][p_index, b_index] = struct.zp
608 results[
'zpErr'][p_index, b_index] = struct.sigma
611 offsets = np.zeros(len(bands))
613 for b_index, band
in enumerate(bands):
615 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
616 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
619 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
620 self.log.info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
621 band, offsets[b_index], madSigma)
625 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
626 b_index, filterLabel, stdCat, selected, refFluxFields):
628 Compute the zeropoint offset between the fgcm stdCat and the reference
629 stars
for one pixel
in one band
633 sourceMapper : `lsst.afw.table.SchemaMapper`
634 Mapper to go
from stdCat to calibratable catalog
635 badStarKey : `lsst.afw.table.Key`
636 Key
for the field
with bad stars
638 Index of the band
in the star catalog
639 filterLabel : `lsst.afw.image.FilterLabel`
640 filterLabel
with band
and physical filter
641 stdCat : `lsst.afw.table.SimpleCatalog`
643 selected : `numpy.array(dtype=bool)`
644 Boolean array of which stars are
in the pixel
645 refFluxFields : `list`
646 List of names of flux fields
for reference catalog
649 sourceCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
650 sourceCat.reserve(selected.sum())
651 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
652 sourceCat['instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
653 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
654 * sourceCat[
'instFlux'])
658 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
659 for rec
in sourceCat[badStar]:
660 rec.set(badStarKey,
True)
662 exposure = afwImage.ExposureF()
663 exposure.setFilter(filterLabel)
665 if refFluxFields[b_index]
is None:
668 ctr = stdCat[0].getCoord()
669 rad = 0.05*lsst.geom.degrees
670 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
671 refFluxFields[b_index] = refDataTest.fluxField
674 calConfig = copy.copy(self.config.photoCal.value)
675 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
676 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
677 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
679 schema=sourceCat.getSchema())
681 struct = calTask.run(exposure, sourceCat)
685 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
686 physicalFilterMap, tract=None):
687 """Output the zeropoints in fgcm_photoCalib format.
691 camera : `lsst.afw.cameraGeom.Camera`
692 Camera from the butler.
693 zptCat : `lsst.afw.table.BaseCatalog`
694 FGCM zeropoint catalog
from `FgcmFitCycleTask`.
695 visitCat : `lsst.afw.table.BaseCatalog`
696 FGCM visitCat
from `FgcmBuildStarsTask`.
697 offsets : `numpy.array`
698 Float array of absolute calibration offsets, one
for each filter.
699 bands : `list` [`str`]
700 List of band names
from FGCM output.
701 physicalFilterMap : `dict`
702 Dictionary of mappings
from physical filter to FGCM band.
703 tract: `int`, optional
704 Tract number to output. Default
is None (
global calibration)
708 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
709 Generator that returns (visit, exposureCatalog) tuples.
714 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
715 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
716 & (zptCat[
'fgcmZptVar'] > 0.0)
717 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE))
720 badVisits = np.unique(zptCat[
'visit'][~selected])
721 goodVisits = np.unique(zptCat[
'visit'][selected])
722 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
723 for allBadVisit
in allBadVisits:
724 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
728 for f
in physicalFilterMap:
730 if physicalFilterMap[f]
in bands:
731 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
735 for ccdIndex, detector
in enumerate(camera):
736 ccdMapping[detector.getId()] = ccdIndex
741 scalingMapping[rec[
'visit']] = rec[
'scaling']
743 if self.config.doComposeWcsJacobian:
744 approxPixelAreaFields = computeApproxPixelAreaFields(camera)
748 zptVisitCatalog =
None
750 metadata = dafBase.PropertyList()
751 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
752 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
754 for rec
in zptCat[selected]:
756 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
763 postCalibrationOffset = offsetMapping[rec[
'filtername']]
764 if self.config.doApplyMeanChromaticCorrection:
765 postCalibrationOffset += rec[
'fgcmDeltaChrom']
767 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
768 rec[
'fgcmfZptChebXyMax'])
770 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
771 rec[
'fgcmfZptChebXyMax'],
772 offset=postCalibrationOffset,
775 if self.config.doComposeWcsJacobian:
777 fgcmField = afwMath.ProductBoundedField([approxPixelAreaFields[rec[
'detector']],
783 fgcmField = afwMath.ProductBoundedField([fgcmSuperStarField, fgcmZptField])
786 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
787 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
788 photoCalib = afwImage.PhotoCalib(calibrationMean=calibCenter,
789 calibrationErr=calibErr,
790 calibration=fgcmField,
794 if rec[
'visit'] != lastVisit:
799 zptVisitCatalog.sort()
800 yield (int(lastVisit), zptVisitCatalog)
803 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
804 zptExpCatSchema.addField(
'visit', type=
'L', doc=
'Visit number')
807 zptVisitCatalog = afwTable.ExposureCatalog(zptExpCatSchema)
808 zptVisitCatalog.setMetadata(metadata)
810 lastVisit = int(rec[
'visit'])
812 catRecord = zptVisitCatalog.addNew()
813 catRecord[
'id'] = int(rec[
'detector'])
814 catRecord[
'visit'] = rec[
'visit']
815 catRecord.setPhotoCalib(photoCalib)
819 zptVisitCatalog.sort()
820 yield (int(lastVisit), zptVisitCatalog)
822 def _getChebyshevBoundedField(self, coefficients, xyMax, offset=0.0, scaling=1.0):
824 Make a ChebyshevBoundedField from fgcm coefficients,
with optional offset
829 coefficients: `numpy.array`
830 Flattened array of chebyshev coefficients
831 xyMax: `list` of length 2
832 Maximum x
and y of the chebyshev bounding box
833 offset: `float`, optional
834 Absolute calibration offset. Default
is 0.0
835 scaling: `float`, optional
836 Flat scaling value
from fgcmBuildStars. Default
is 1.0
840 boundedField: `lsst.afw.math.ChebyshevBoundedField`
843 orderPlus1 = int(np.sqrt(coefficients.size))
844 pars = np.zeros((orderPlus1, orderPlus1))
846 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0.0, 0.0),
847 lsst.geom.Point2I(*xyMax))
849 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
850 * (10.**(offset/-2.5))*scaling)
852 boundedField = afwMath.ChebyshevBoundedField(bbox, pars)
856 def _outputAtmospheres(self, handleDict, atmCat):
858 Output the atmospheres.
863 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
864 The handleDict has the follownig keys:
866 ``"fgcmLookUpTable"``
867 handle
for the FGCM look-up table.
868 atmCat : `lsst.afw.table.BaseCatalog`
869 FGCM atmosphere parameter catalog
from fgcmFitCycleTask.
873 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
874 Generator that returns (visit, transmissionCurve) tuples.
877 lutCat = handleDict[
'fgcmLookUpTable'].get()
879 atmosphereTableName = lutCat[0][
'tablename']
880 elevation = lutCat[0][
'elevation']
881 atmLambda = lutCat[0][
'atmLambda']
886 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
894 modGen = fgcm.ModtranGenerator(elevation)
895 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
896 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
897 except (ValueError, IOError)
as e:
898 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
899 "but modtran not configured to run.")
from e
901 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
903 for i, visit
in enumerate(atmCat[
'visit']):
904 if atmTable
is not None:
906 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
907 pwv=atmCat[i][
'pwv'],
909 tau=atmCat[i][
'tau'],
910 alpha=atmCat[i][
'alpha'],
912 ctranslamstd=[atmCat[i][
'cTrans'],
913 atmCat[i][
'lamStd']])
916 modAtm = modGen(pmb=atmCat[i][
'pmb'],
917 pwv=atmCat[i][
'pwv'],
919 tau=atmCat[i][
'tau'],
920 alpha=atmCat[i][
'alpha'],
922 lambdaRange=lambdaRange,
923 lambdaStep=lambdaStep,
924 ctranslamstd=[atmCat[i][
'cTrans'],
925 atmCat[i][
'lamStd']])
926 atmVals = modAtm[
'COMBINED']
929 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
930 wavelengths=atmLambda,
931 throughputAtMin=atmVals[0],
932 throughputAtMax=atmVals[-1])
934 yield (int(visit), curve)