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
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=
"fgcm_Cycle{cycleNumber}_StandardStars",
91 storageClass=
"SimpleCatalog",
92 dimensions=(
"instrument",),
96 fgcmZeropoints = connectionTypes.Input(
97 doc=
"Catalog of zeropoints from fgcm fit",
98 name=
"fgcm_Cycle{cycleNumber}_Zeropoints",
99 storageClass=
"Catalog",
100 dimensions=(
"instrument",),
104 fgcmAtmosphereParameters = connectionTypes.Input(
105 doc=
"Catalog of atmosphere parameters from fgcm fit",
106 name=
"fgcm_Cycle{cycleNumber}_AtmosphereParameters",
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:
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:
320 schema.addField(
'offset', type=np.float64,
321 doc=
"Post-process calibration offset (mag)")
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]
534 physical=stdPhysicalFilter))
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
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
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:
785 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
786 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
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')
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))
848 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
849 * (10.**(offset/-2.5))*scaling)
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)
A group of labels for a filter in an exposure or coadd.
The photometric calibration of an exposure.
A BoundedField based on 2-d Chebyshev polynomials of the first kind.
A BoundedField that lazily multiplies a sequence of other BoundedFields.
Custom catalog class for ExposureRecord/Table.
Defines the fields and offsets for a table.
A mapping between the keys of two Schemas, used to copy data between them.
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Class for storing ordered metadata with comments.
An integer coordinate rectangle.