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
38from astropy
import units
40import lsst.daf.base
as dafBase
41import lsst.pex.config
as pexConfig
42import lsst.pipe.base
as pipeBase
43from lsst.pipe.base
import connectionTypes
44from lsst.afw.image
import TransmissionCurve
45from lsst.meas.algorithms
import ReferenceObjectLoader, LoadReferenceObjectsConfig
46from lsst.pipe.tasks.photoCal
import PhotoCalTask
48import lsst.afw.image
as afwImage
49import lsst.afw.math
as afwMath
50import lsst.afw.table
as afwTable
52from .utilities
import computeApproxPixelAreaFields
53from .utilities
import FGCM_ILLEGAL_VALUE
57__all__ = [
'FgcmOutputProductsConfig',
'FgcmOutputProductsTask']
61 dimensions=(
"instrument",),
62 defaultTemplates={
"cycleNumber":
"0"}):
63 camera = connectionTypes.PrerequisiteInput(
64 doc=
"Camera instrument",
66 storageClass=
"Camera",
67 dimensions=(
"instrument",),
71 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
72 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
73 "chromatic corrections."),
74 name=
"fgcmLookUpTable",
75 storageClass=
"Catalog",
76 dimensions=(
"instrument",),
80 fgcmVisitCatalog = connectionTypes.Input(
81 doc=
"Catalog of visit information for fgcm",
82 name=
"fgcmVisitCatalog",
83 storageClass=
"Catalog",
84 dimensions=(
"instrument",),
88 fgcmStandardStars = connectionTypes.Input(
89 doc=
"Catalog of standard star data from fgcm fit",
90 name=
"fgcmStandardStars{cycleNumber}",
91 storageClass=
"SimpleCatalog",
92 dimensions=(
"instrument",),
96 fgcmZeropoints = connectionTypes.Input(
97 doc=
"Catalog of zeropoints from fgcm fit",
98 name=
"fgcmZeropoints{cycleNumber}",
99 storageClass=
"Catalog",
100 dimensions=(
"instrument",),
104 fgcmAtmosphereParameters = connectionTypes.Input(
105 doc=
"Catalog of atmosphere parameters from fgcm fit",
106 name=
"fgcmAtmosphereParameters{cycleNumber}",
107 storageClass=
"Catalog",
108 dimensions=(
"instrument",),
112 refCat = connectionTypes.PrerequisiteInput(
113 doc=
"Reference catalog to use for photometric calibration",
115 storageClass=
"SimpleCatalog",
116 dimensions=(
"skypix",),
121 fgcmPhotoCalib = connectionTypes.Output(
122 doc=(
"Per-visit photometric calibrations derived from fgcm calibration. "
123 "These catalogs use detector id for the id and are sorted for "
124 "fast lookups of a detector."),
125 name=
"fgcmPhotoCalibCatalog",
126 storageClass=
"ExposureCatalog",
127 dimensions=(
"instrument",
"visit",),
131 fgcmTransmissionAtmosphere = connectionTypes.Output(
132 doc=
"Per-visit atmosphere transmission files produced from fgcm calibration",
133 name=
"transmission_atmosphere_fgcm",
134 storageClass=
"TransmissionCurve",
135 dimensions=(
"instrument",
140 fgcmOffsets = connectionTypes.Output(
141 doc=
"Per-band offsets computed from doReferenceCalibration",
142 name=
"fgcmReferenceCalibrationOffsets",
143 storageClass=
"Catalog",
144 dimensions=(
"instrument",),
148 def __init__(self, *, config=None):
149 super().__init__(config=config)
151 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
152 raise ValueError(
"cycleNumber must be of integer format")
154 if not config.doReferenceCalibration:
155 self.prerequisiteInputs.remove(
"refCat")
156 if not config.doAtmosphereOutput:
157 self.inputs.remove(
"fgcmAtmosphereParameters")
158 if not config.doZeropointOutput:
159 self.inputs.remove(
"fgcmZeropoints")
160 if not config.doReferenceCalibration:
161 self.outputs.remove(
"fgcmOffsets")
163 def getSpatialBoundsConnections(self):
164 return (
"fgcmPhotoCalib",)
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[
'visit']:
283 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
285 if self.config.doAtmosphereOutput:
286 handleDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
287 atmRefDict = {atmRef.dataId[
'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 = fgcm.fgcmUtilities.histogram_rev_sorted(ipring)
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)