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 lookupStaticCalibrations
55from .utilities import FGCM_ILLEGAL_VALUE
59__all__ = ['FgcmOutputProductsConfig', 'FgcmOutputProductsTask']
62class FgcmOutputProductsConnections(pipeBase.PipelineTaskConnections,
63 dimensions=("instrument",),
64 defaultTemplates={
"cycleNumber":
"0"}):
65 camera = connectionTypes.PrerequisiteInput(
66 doc=
"Camera instrument",
68 storageClass=
"Camera",
69 dimensions=(
"instrument",),
70 lookupFunction=lookupStaticCalibrations,
74 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
75 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
76 "chromatic corrections."),
77 name=
"fgcmLookUpTable",
78 storageClass=
"Catalog",
79 dimensions=(
"instrument",),
83 fgcmVisitCatalog = connectionTypes.Input(
84 doc=
"Catalog of visit information for fgcm",
85 name=
"fgcmVisitCatalog",
86 storageClass=
"Catalog",
87 dimensions=(
"instrument",),
91 fgcmStandardStars = connectionTypes.Input(
92 doc=
"Catalog of standard star data from fgcm fit",
93 name=
"fgcmStandardStars{cycleNumber}",
94 storageClass=
"SimpleCatalog",
95 dimensions=(
"instrument",),
99 fgcmZeropoints = connectionTypes.Input(
100 doc=
"Catalog of zeropoints from fgcm fit",
101 name=
"fgcmZeropoints{cycleNumber}",
102 storageClass=
"Catalog",
103 dimensions=(
"instrument",),
107 fgcmAtmosphereParameters = connectionTypes.Input(
108 doc=
"Catalog of atmosphere parameters from fgcm fit",
109 name=
"fgcmAtmosphereParameters{cycleNumber}",
110 storageClass=
"Catalog",
111 dimensions=(
"instrument",),
115 refCat = connectionTypes.PrerequisiteInput(
116 doc=
"Reference catalog to use for photometric calibration",
118 storageClass=
"SimpleCatalog",
119 dimensions=(
"skypix",),
124 fgcmPhotoCalib = connectionTypes.Output(
125 doc=(
"Per-visit photometric calibrations derived from fgcm calibration. "
126 "These catalogs use detector id for the id and are sorted for "
127 "fast lookups of a detector."),
128 name=
"fgcmPhotoCalibCatalog",
129 storageClass=
"ExposureCatalog",
130 dimensions=(
"instrument",
"visit",),
134 fgcmTransmissionAtmosphere = connectionTypes.Output(
135 doc=
"Per-visit atmosphere transmission files produced from fgcm calibration",
136 name=
"transmission_atmosphere_fgcm",
137 storageClass=
"TransmissionCurve",
138 dimensions=(
"instrument",
143 fgcmOffsets = connectionTypes.Output(
144 doc=
"Per-band offsets computed from doReferenceCalibration",
145 name=
"fgcmReferenceCalibrationOffsets",
146 storageClass=
"Catalog",
147 dimensions=(
"instrument",),
151 def __init__(self, *, config=None):
152 super().__init__(config=config)
154 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
155 raise ValueError(
"cycleNumber must be of integer format")
157 if not config.doReferenceCalibration:
158 self.prerequisiteInputs.remove(
"refCat")
159 if not config.doAtmosphereOutput:
160 self.inputs.remove(
"fgcmAtmosphereParameters")
161 if not config.doZeropointOutput:
162 self.inputs.remove(
"fgcmZeropoints")
163 if not config.doReferenceCalibration:
164 self.outputs.remove(
"fgcmOffsets")
167class FgcmOutputProductsConfig(pipeBase.PipelineTaskConfig,
168 pipelineConnections=FgcmOutputProductsConnections):
169 """Config for FgcmOutputProductsTask"""
171 physicalFilterMap = pexConfig.DictField(
172 doc=
"Mapping from 'physicalFilter' to band.",
179 doReferenceCalibration = pexConfig.Field(
180 doc=(
"Transfer 'absolute' calibration from reference catalog? "
181 "This afterburner step is unnecessary if reference stars "
182 "were used in the full fit in FgcmFitCycleTask."),
186 doAtmosphereOutput = pexConfig.Field(
187 doc=
"Output atmospheres in transmission_atmosphere_fgcm format",
191 doZeropointOutput = pexConfig.Field(
192 doc=
"Output zeropoints in fgcm_photoCalib format",
196 doComposeWcsJacobian = pexConfig.Field(
197 doc=
"Compose Jacobian of WCS with fgcm calibration for output photoCalib?",
201 doApplyMeanChromaticCorrection = pexConfig.Field(
202 doc=
"Apply the mean chromatic correction to the zeropoints?",
206 photoCal = pexConfig.ConfigurableField(
208 doc=
"task to perform 'absolute' calibration",
210 referencePixelizationNside = pexConfig.Field(
211 doc=
"Healpix nside to pixelize catalog to compare to reference catalog",
215 referencePixelizationMinStars = pexConfig.Field(
216 doc=(
"Minimum number of stars per healpix pixel to select for comparison"
217 "to the specified reference catalog"),
221 referenceMinMatch = pexConfig.Field(
222 doc=
"Minimum number of stars matched to reference catalog to be used in statistics",
226 referencePixelizationNPixels = pexConfig.Field(
227 doc=(
"Number of healpix pixels to sample to do comparison. "
228 "Doing too many will take a long time and not yield any more "
229 "precise results because the final number is the median offset "
230 "(per band) from the set of pixels."),
235 def setDefaults(self):
236 pexConfig.Config.setDefaults(self)
246 self.photoCal.applyColorTerms =
False
247 self.photoCal.fluxField =
'instFlux'
248 self.photoCal.magErrFloor = 0.003
249 self.photoCal.match.referenceSelection.doSignalToNoise =
True
250 self.photoCal.match.referenceSelection.signalToNoise.minimum = 10.0
251 self.photoCal.match.sourceSelection.doSignalToNoise =
True
252 self.photoCal.match.sourceSelection.signalToNoise.minimum = 10.0
253 self.photoCal.match.sourceSelection.signalToNoise.fluxField =
'instFlux'
254 self.photoCal.match.sourceSelection.signalToNoise.errField =
'instFluxErr'
255 self.photoCal.match.sourceSelection.doFlags =
True
256 self.photoCal.match.sourceSelection.flags.good = []
257 self.photoCal.match.sourceSelection.flags.bad = [
'flag_badStar']
258 self.photoCal.match.sourceSelection.doUnresolved =
False
259 self.photoCal.match.sourceSelection.doRequirePrimary =
False
262class FgcmOutputProductsTask(pipeBase.PipelineTask):
264 Output products from FGCM
global calibration.
267 ConfigClass = FgcmOutputProductsConfig
268 _DefaultName = "fgcmOutputProducts"
270 def __init__(self, **kwargs):
271 super().__init__(**kwargs)
273 def runQuantum(self, butlerQC, inputRefs, outputRefs):
275 handleDict[
'camera'] = butlerQC.get(inputRefs.camera)
276 handleDict[
'fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
277 handleDict[
'fgcmVisitCatalog'] = butlerQC.get(inputRefs.fgcmVisitCatalog)
278 handleDict[
'fgcmStandardStars'] = butlerQC.get(inputRefs.fgcmStandardStars)
280 if self.config.doZeropointOutput:
281 handleDict[
'fgcmZeropoints'] = butlerQC.get(inputRefs.fgcmZeropoints)
282 photoCalibRefDict = {photoCalibRef.dataId.byName()[
'visit']:
283 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
285 if self.config.doAtmosphereOutput:
286 handleDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
287 atmRefDict = {atmRef.dataId.byName()[
'visit']: atmRef
for
288 atmRef
in outputRefs.fgcmTransmissionAtmosphere}
290 if self.config.doReferenceCalibration:
291 refConfig = LoadReferenceObjectsConfig()
292 self.refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
293 for ref
in inputRefs.refCat],
294 refCats=butlerQC.get(inputRefs.refCat),
295 name=self.config.connections.refCat,
299 self.refObjLoader =
None
301 struct = self.run(handleDict, self.config.physicalFilterMap)
304 if struct.photoCalibCatalogs
is not None:
305 self.log.info(
"Outputting photoCalib catalogs.")
306 for visit, expCatalog
in struct.photoCalibCatalogs:
307 butlerQC.put(expCatalog, photoCalibRefDict[visit])
308 self.log.info(
"Done outputting photoCalib catalogs.")
311 if struct.atmospheres
is not None:
312 self.log.info(
"Outputting atmosphere transmission files.")
313 for visit, atm
in struct.atmospheres:
314 butlerQC.put(atm, atmRefDict[visit])
315 self.log.info(
"Done outputting atmosphere files.")
317 if self.config.doReferenceCalibration:
319 schema = afwTable.Schema()
320 schema.addField(
'offset', type=np.float64,
321 doc=
"Post-process calibration offset (mag)")
322 offsetCat = afwTable.BaseCatalog(schema)
323 offsetCat.resize(len(struct.offsets))
324 offsetCat[
'offset'][:] = struct.offsets
326 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
330 def run(self, handleDict, physicalFilterMap):
331 """Run the output products task.
336 All handles are `lsst.daf.butler.DeferredDatasetHandle`
337 handle dictionary with keys:
340 Camera object (`lsst.afw.cameraGeom.Camera`)
341 ``
"fgcmLookUpTable"``
342 handle
for the FGCM look-up table.
343 ``
"fgcmVisitCatalog"``
344 handle
for visit summary catalog.
345 ``
"fgcmStandardStars"``
346 handle
for the output standard star catalog.
348 handle
for the zeropoint data catalog.
349 ``
"fgcmAtmosphereParameters"``
350 handle
for the atmosphere parameter catalog.
351 ``
"fgcmBuildStarsTableConfig"``
352 Config
for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
353 physicalFilterMap : `dict`
354 Dictionary of mappings
from physical filter to FGCM band.
358 retStruct : `lsst.pipe.base.Struct`
359 Output structure
with keys:
361 offsets : `np.ndarray`
362 Final reference offsets, per band.
363 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
364 Generator that returns (visit, transmissionCurve) tuples.
365 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
366 Generator that returns (visit, exposureCatalog) tuples.
368 stdCat = handleDict['fgcmStandardStars'].get()
369 md = stdCat.getMetadata()
370 bands = md.getArray(
'BANDS')
372 if self.config.doReferenceCalibration:
373 lutCat = handleDict[
'fgcmLookUpTable'].get()
374 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
376 offsets = np.zeros(len(bands))
380 if self.config.doZeropointOutput:
381 zptCat = handleDict[
'fgcmZeropoints'].get()
382 visitCat = handleDict[
'fgcmVisitCatalog'].get()
384 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
389 if self.config.doAtmosphereOutput:
390 atmCat = handleDict[
'fgcmAtmosphereParameters'].get()
391 atmgen = self._outputAtmospheres(handleDict, atmCat)
395 retStruct = pipeBase.Struct(offsets=offsets,
397 retStruct.photoCalibCatalogs = pcgen
401 def generateTractOutputProducts(self, handleDict, tract,
402 visitCat, zptCat, atmCat, stdCat,
403 fgcmBuildStarsConfig):
405 Generate the output products for a given tract,
as specified
in the config.
407 This method
is here to have an alternate entry-point
for
413 All handles are `lsst.daf.butler.DeferredDatasetHandle`
414 handle dictionary
with keys:
417 Camera object (`lsst.afw.cameraGeom.Camera`)
418 ``
"fgcmLookUpTable"``
419 handle
for the FGCM look-up table.
422 visitCat : `lsst.afw.table.BaseCatalog`
423 FGCM visitCat
from `FgcmBuildStarsTask`
424 zptCat : `lsst.afw.table.BaseCatalog`
425 FGCM zeropoint catalog
from `FgcmFitCycleTask`
426 atmCat : `lsst.afw.table.BaseCatalog`
427 FGCM atmosphere parameter catalog
from `FgcmFitCycleTask`
428 stdCat : `lsst.afw.table.SimpleCatalog`
429 FGCM standard star catalog
from `FgcmFitCycleTask`
430 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
431 Configuration object
from `FgcmBuildStarsTask`
435 retStruct : `lsst.pipe.base.Struct`
436 Output structure
with keys:
438 offsets : `np.ndarray`
439 Final reference offsets, per band.
440 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
441 Generator that returns (visit, transmissionCurve) tuples.
442 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
443 Generator that returns (visit, exposureCatalog) tuples.
445 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
447 md = stdCat.getMetadata()
448 bands = md.getArray('BANDS')
450 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
451 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
452 "in fgcmBuildStarsTask.")
454 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
455 self.log.warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
457 if self.config.doReferenceCalibration:
458 lutCat = handleDict[
'fgcmLookUpTable'].get()
459 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
461 offsets = np.zeros(len(bands))
463 if self.config.doZeropointOutput:
464 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
469 if self.config.doAtmosphereOutput:
470 atmgen = self._outputAtmospheres(handleDict, atmCat)
474 retStruct = pipeBase.Struct(offsets=offsets,
476 retStruct.photoCalibCatalogs = pcgen
480 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
482 Compute offsets relative to a reference catalog.
484 This method splits the star catalog into healpix pixels
485 and computes the calibration transfer
for a sample of
486 these pixels to approximate the
'absolute' calibration
487 values (on
for each band) to apply to transfer the
492 stdCat : `lsst.afw.table.SimpleCatalog`
494 lutCat : `lsst.afw.table.SimpleCatalog`
496 physicalFilterMap : `dict`
497 Dictionary of mappings
from physical filter to FGCM band.
498 bands : `list` [`str`]
499 List of band names
from FGCM output
502 offsets : `numpy.array` of floats
503 Per band zeropoint offsets
509 minObs = stdCat[
'ngood'].min(axis=1)
511 goodStars = (minObs >= 1)
512 stdCat = stdCat[goodStars]
514 self.log.info(
"Found %d stars with at least 1 good observation in each band" %
521 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
522 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
523 physicalFilterMapBands = list(physicalFilterMap.values())
524 physicalFilterMapFilters = list(physicalFilterMap.keys())
528 physicalFilterMapIndex = physicalFilterMapBands.index(band)
529 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
531 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
532 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
533 filterLabels.append(afwImage.FilterLabel(band=band,
534 physical=stdPhysicalFilter))
543 sourceMapper = afwTable.SchemaMapper(stdCat.schema)
544 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
545 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
546 doc=
"instrumental flux (counts)")
547 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
548 doc=
"instrumental flux error (counts)")
549 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
557 ipring = hpg.angle_to_pixel(
558 self.config.referencePixelizationNside,
563 h, rev = esutil.stat.histogram(ipring, rev=
True)
565 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
567 self.log.info(
"Found %d pixels (nside=%d) with at least %d good stars" %
569 self.config.referencePixelizationNside,
570 self.config.referencePixelizationMinStars))
572 if gdpix.size < self.config.referencePixelizationNPixels:
573 self.log.warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
574 (gdpix.size, self.config.referencePixelizationNPixels))
577 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
579 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
580 (
'nstar',
'i4', len(bands)),
581 (
'nmatch',
'i4', len(bands)),
582 (
'zp',
'f4', len(bands)),
583 (
'zpErr',
'f4', len(bands))])
584 results[
'hpix'] = ipring[rev[rev[gdpix]]]
587 selected = np.zeros(len(stdCat), dtype=bool)
589 refFluxFields = [
None]*len(bands)
591 for p_index, pix
in enumerate(gdpix):
592 i1a = rev[rev[pix]: rev[pix + 1]]
600 for b_index, filterLabel
in enumerate(filterLabels):
601 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
603 selected, refFluxFields)
604 results[
'nstar'][p_index, b_index] = len(i1a)
605 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
606 results[
'zp'][p_index, b_index] = struct.zp
607 results[
'zpErr'][p_index, b_index] = struct.sigma
610 offsets = np.zeros(len(bands))
612 for b_index, band
in enumerate(bands):
614 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
615 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
618 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
619 self.log.info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
620 band, offsets[b_index], madSigma)
624 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
625 b_index, filterLabel, stdCat, selected, refFluxFields):
627 Compute the zeropoint offset between the fgcm stdCat and the reference
628 stars
for one pixel
in one band
632 sourceMapper : `lsst.afw.table.SchemaMapper`
633 Mapper to go
from stdCat to calibratable catalog
634 badStarKey : `lsst.afw.table.Key`
635 Key
for the field
with bad stars
637 Index of the band
in the star catalog
638 filterLabel : `lsst.afw.image.FilterLabel`
639 filterLabel
with band
and physical filter
640 stdCat : `lsst.afw.table.SimpleCatalog`
642 selected : `numpy.array(dtype=bool)`
643 Boolean array of which stars are
in the pixel
644 refFluxFields : `list`
645 List of names of flux fields
for reference catalog
648 sourceCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
649 sourceCat.reserve(selected.sum())
650 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
651 sourceCat['instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
652 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
653 * sourceCat[
'instFlux'])
657 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
658 for rec
in sourceCat[badStar]:
659 rec.set(badStarKey,
True)
661 exposure = afwImage.ExposureF()
662 exposure.setFilter(filterLabel)
664 if refFluxFields[b_index]
is None:
667 ctr = stdCat[0].getCoord()
668 rad = 0.05*lsst.geom.degrees
669 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
670 refFluxFields[b_index] = refDataTest.fluxField
673 calConfig = copy.copy(self.config.photoCal.value)
674 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
675 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
676 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
678 schema=sourceCat.getSchema())
680 struct = calTask.run(exposure, sourceCat)
684 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
685 physicalFilterMap, tract=None):
686 """Output the zeropoints in fgcm_photoCalib format.
690 camera : `lsst.afw.cameraGeom.Camera`
691 Camera from the butler.
692 zptCat : `lsst.afw.table.BaseCatalog`
693 FGCM zeropoint catalog
from `FgcmFitCycleTask`.
694 visitCat : `lsst.afw.table.BaseCatalog`
695 FGCM visitCat
from `FgcmBuildStarsTask`.
696 offsets : `numpy.array`
697 Float array of absolute calibration offsets, one
for each filter.
698 bands : `list` [`str`]
699 List of band names
from FGCM output.
700 physicalFilterMap : `dict`
701 Dictionary of mappings
from physical filter to FGCM band.
702 tract: `int`, optional
703 Tract number to output. Default
is None (
global calibration)
707 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
708 Generator that returns (visit, exposureCatalog) tuples.
713 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
714 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
715 & (zptCat[
'fgcmZptVar'] > 0.0)
716 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE))
719 badVisits = np.unique(zptCat[
'visit'][~selected])
720 goodVisits = np.unique(zptCat[
'visit'][selected])
721 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
722 for allBadVisit
in allBadVisits:
723 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
727 for f
in physicalFilterMap:
729 if physicalFilterMap[f]
in bands:
730 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
734 for ccdIndex, detector
in enumerate(camera):
735 ccdMapping[detector.getId()] = ccdIndex
740 scalingMapping[rec[
'visit']] = rec[
'scaling']
742 if self.config.doComposeWcsJacobian:
743 approxPixelAreaFields = computeApproxPixelAreaFields(camera)
747 zptVisitCatalog =
None
749 metadata = dafBase.PropertyList()
750 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
751 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
753 for rec
in zptCat[selected]:
755 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
762 postCalibrationOffset = offsetMapping[rec[
'filtername']]
763 if self.config.doApplyMeanChromaticCorrection:
764 postCalibrationOffset += rec[
'fgcmDeltaChrom']
766 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
767 rec[
'fgcmfZptChebXyMax'])
769 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
770 rec[
'fgcmfZptChebXyMax'],
771 offset=postCalibrationOffset,
774 if self.config.doComposeWcsJacobian:
776 fgcmField = afwMath.ProductBoundedField([approxPixelAreaFields[rec[
'detector']],
782 fgcmField = afwMath.ProductBoundedField([fgcmSuperStarField, fgcmZptField])
785 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
786 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
787 photoCalib = afwImage.PhotoCalib(calibrationMean=calibCenter,
788 calibrationErr=calibErr,
789 calibration=fgcmField,
793 if rec[
'visit'] != lastVisit:
798 zptVisitCatalog.sort()
799 yield (int(lastVisit), zptVisitCatalog)
802 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
803 zptExpCatSchema.addField(
'visit', type=
'L', doc=
'Visit number')
806 zptVisitCatalog = afwTable.ExposureCatalog(zptExpCatSchema)
807 zptVisitCatalog.setMetadata(metadata)
809 lastVisit = int(rec[
'visit'])
811 catRecord = zptVisitCatalog.addNew()
812 catRecord[
'id'] = int(rec[
'detector'])
813 catRecord[
'visit'] = rec[
'visit']
814 catRecord.setPhotoCalib(photoCalib)
818 zptVisitCatalog.sort()
819 yield (int(lastVisit), zptVisitCatalog)
821 def _getChebyshevBoundedField(self, coefficients, xyMax, offset=0.0, scaling=1.0):
823 Make a ChebyshevBoundedField from fgcm coefficients,
with optional offset
828 coefficients: `numpy.array`
829 Flattened array of chebyshev coefficients
830 xyMax: `list` of length 2
831 Maximum x
and y of the chebyshev bounding box
832 offset: `float`, optional
833 Absolute calibration offset. Default
is 0.0
834 scaling: `float`, optional
835 Flat scaling value
from fgcmBuildStars. Default
is 1.0
839 boundedField: `lsst.afw.math.ChebyshevBoundedField`
842 orderPlus1 = int(np.sqrt(coefficients.size))
843 pars = np.zeros((orderPlus1, orderPlus1))
845 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0.0, 0.0),
846 lsst.geom.Point2I(*xyMax))
848 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
849 * (10.**(offset/-2.5))*scaling)
851 boundedField = afwMath.ChebyshevBoundedField(bbox, pars)
855 def _outputAtmospheres(self, handleDict, atmCat):
857 Output the atmospheres.
862 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
863 The handleDict has the follownig keys:
865 ``"fgcmLookUpTable"``
866 handle
for the FGCM look-up table.
867 atmCat : `lsst.afw.table.BaseCatalog`
868 FGCM atmosphere parameter catalog
from fgcmFitCycleTask.
872 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
873 Generator that returns (visit, transmissionCurve) tuples.
876 lutCat = handleDict[
'fgcmLookUpTable'].get()
878 atmosphereTableName = lutCat[0][
'tablename']
879 elevation = lutCat[0][
'elevation']
880 atmLambda = lutCat[0][
'atmLambda']
885 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
893 modGen = fgcm.ModtranGenerator(elevation)
894 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
895 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
896 except (ValueError, IOError)
as e:
897 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
898 "but modtran not configured to run.")
from e
900 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
902 for i, visit
in enumerate(atmCat[
'visit']):
903 if atmTable
is not None:
905 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
906 pwv=atmCat[i][
'pwv'],
908 tau=atmCat[i][
'tau'],
909 alpha=atmCat[i][
'alpha'],
911 ctranslamstd=[atmCat[i][
'cTrans'],
912 atmCat[i][
'lamStd']])
915 modAtm = modGen(pmb=atmCat[i][
'pmb'],
916 pwv=atmCat[i][
'pwv'],
918 tau=atmCat[i][
'tau'],
919 alpha=atmCat[i][
'alpha'],
921 lambdaRange=lambdaRange,
922 lambdaStep=lambdaStep,
923 ctranslamstd=[atmCat[i][
'cTrans'],
924 atmCat[i][
'lamStd']])
925 atmVals = modAtm[
'COMBINED']
928 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
929 wavelengths=atmLambda,
930 throughputAtMin=atmVals[0],
931 throughputAtMax=atmVals[-1])
933 yield (int(visit), curve)