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
39from astropy.table
import Table
46from lsst.afw.image
import TransmissionCurve
50import lsst.afw.image
as afwImage
55from .utilities
import computeApproxPixelAreaFields
56from .utilities
import FGCM_ILLEGAL_VALUE
60__all__ = [
'FgcmOutputProductsConfig',
'FgcmOutputProductsTask']
64 dimensions=(
"instrument",),
65 defaultTemplates={
"cycleNumber":
"0"}):
66 camera = connectionTypes.PrerequisiteInput(
67 doc=
"Camera instrument",
69 storageClass=
"Camera",
70 dimensions=(
"instrument",),
74 skymap = connectionTypes.Input(
75 doc=
"Skymap used for tract sharding of output catalog.",
76 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
77 storageClass=
"SkyMap",
78 dimensions=(
"skymap",),
81 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
82 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
83 "chromatic corrections."),
84 name=
"fgcmLookUpTable",
85 storageClass=
"Catalog",
86 dimensions=(
"instrument",),
90 fgcmVisitCatalog = connectionTypes.Input(
91 doc=
"Catalog of visit information for fgcm",
92 name=
"fgcmVisitCatalog",
93 storageClass=
"Catalog",
94 dimensions=(
"instrument",),
98 fgcmStandardStars = connectionTypes.Input(
99 doc=
"Catalog of standard star data from fgcm fit",
100 name=
"fgcm_Cycle{cycleNumber}_StandardStars",
101 storageClass=
"SimpleCatalog",
102 dimensions=(
"instrument",),
106 fgcmZeropoints = connectionTypes.Input(
107 doc=
"Catalog of zeropoints from fgcm fit",
108 name=
"fgcm_Cycle{cycleNumber}_Zeropoints",
109 storageClass=
"Catalog",
110 dimensions=(
"instrument",),
114 fgcmAtmosphereParameters = connectionTypes.Input(
115 doc=
"Catalog of atmosphere parameters from fgcm fit",
116 name=
"fgcm_Cycle{cycleNumber}_AtmosphereParameters",
117 storageClass=
"Catalog",
118 dimensions=(
"instrument",),
122 refCat = connectionTypes.PrerequisiteInput(
123 doc=
"Reference catalog to use for photometric calibration",
125 storageClass=
"SimpleCatalog",
126 dimensions=(
"skypix",),
131 fgcmPhotoCalib = connectionTypes.Output(
132 doc=(
"Per-visit photometric calibrations derived from fgcm calibration. "
133 "These catalogs use detector id for the id and are sorted for "
134 "fast lookups of a detector."),
135 name=
"fgcmPhotoCalibCatalog",
136 storageClass=
"ExposureCatalog",
137 dimensions=(
"instrument",
"visit",),
141 fgcmTransmissionAtmosphere = connectionTypes.Output(
142 doc=
"Per-visit atmosphere transmission files produced from fgcm calibration",
143 name=
"transmission_atmosphere_fgcm",
144 storageClass=
"TransmissionCurve",
145 dimensions=(
"instrument",
150 fgcmOffsets = connectionTypes.Output(
151 doc=
"Per-band offsets computed from doReferenceCalibration",
152 name=
"fgcmReferenceCalibrationOffsets",
153 storageClass=
"Catalog",
154 dimensions=(
"instrument",),
158 fgcmTractStars = connectionTypes.Output(
159 doc=
"Per-tract fgcm calibrated stars.",
160 name=
"fgcm_standard_star",
161 storageClass=
"ArrowAstropy",
162 dimensions=(
"instrument",
"tract",
"skymap"),
166 def __init__(self, *, config=None):
167 super().__init__(config=config)
169 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
170 raise ValueError(
"cycleNumber must be of integer format")
172 if not config.doReferenceCalibration:
173 self.prerequisiteInputs.remove(
"refCat")
174 if not config.doAtmosphereOutput:
175 self.inputs.remove(
"fgcmAtmosphereParameters")
176 if not config.doZeropointOutput:
177 self.inputs.remove(
"fgcmZeropoints")
178 if not config.doReferenceCalibration:
179 self.outputs.remove(
"fgcmOffsets")
180 if not config.doTractStars:
182 del self.fgcmTractStars
184 def getSpatialBoundsConnections(self):
185 return (
"fgcmPhotoCalib",)
188class FgcmOutputProductsConfig(pipeBase.PipelineTaskConfig,
189 pipelineConnections=FgcmOutputProductsConnections):
190 """Config for FgcmOutputProductsTask"""
192 physicalFilterMap = pexConfig.DictField(
193 doc=
"Mapping from 'physicalFilter' to band.",
200 doReferenceCalibration = pexConfig.Field(
201 doc=(
"Transfer 'absolute' calibration from reference catalog? "
202 "This afterburner step is unnecessary if reference stars "
203 "were used in the full fit in FgcmFitCycleTask."),
207 doAtmosphereOutput = pexConfig.Field(
208 doc=
"Output atmospheres in transmission_atmosphere_fgcm format",
212 doZeropointOutput = pexConfig.Field(
213 doc=
"Output zeropoints in fgcm_photoCalib format",
217 doComposeWcsJacobian = pexConfig.Field(
218 doc=
"Compose Jacobian of WCS with fgcm calibration for output photoCalib?",
222 doApplyMeanChromaticCorrection = pexConfig.Field(
223 doc=
"Apply the mean chromatic correction to the zeropoints?",
227 doTractStars = pexConfig.Field(
228 doc=
"Output tract-sharded standard stars?",
233 photoCal = pexConfig.ConfigurableField(
235 doc=
"task to perform 'absolute' calibration",
237 referencePixelizationNside = pexConfig.Field(
238 doc=
"Healpix nside to pixelize catalog to compare to reference catalog",
242 referencePixelizationMinStars = pexConfig.Field(
243 doc=(
"Minimum number of stars per healpix pixel to select for comparison"
244 "to the specified reference catalog"),
248 referenceMinMatch = pexConfig.Field(
249 doc=
"Minimum number of stars matched to reference catalog to be used in statistics",
253 referencePixelizationNPixels = pexConfig.Field(
254 doc=(
"Number of healpix pixels to sample to do comparison. "
255 "Doing too many will take a long time and not yield any more "
256 "precise results because the final number is the median offset "
257 "(per band) from the set of pixels."),
262 def setDefaults(self):
263 pexConfig.Config.setDefaults(self)
273 self.photoCal.applyColorTerms =
False
274 self.photoCal.fluxField =
'instFlux'
275 self.photoCal.magErrFloor = 0.003
276 self.photoCal.match.referenceSelection.doSignalToNoise =
True
277 self.photoCal.match.referenceSelection.signalToNoise.minimum = 10.0
278 self.photoCal.match.sourceSelection.doSignalToNoise =
True
279 self.photoCal.match.sourceSelection.signalToNoise.minimum = 10.0
280 self.photoCal.match.sourceSelection.signalToNoise.fluxField =
'instFlux'
281 self.photoCal.match.sourceSelection.signalToNoise.errField =
'instFluxErr'
282 self.photoCal.match.sourceSelection.doFlags =
True
283 self.photoCal.match.sourceSelection.flags.good = []
284 self.photoCal.match.sourceSelection.flags.bad = [
'flag_badStar']
285 self.photoCal.match.sourceSelection.doUnresolved =
False
286 self.photoCal.match.sourceSelection.doRequirePrimary =
False
289class FgcmOutputProductsTask(pipeBase.PipelineTask):
291 Output products from FGCM global calibration.
294 ConfigClass = FgcmOutputProductsConfig
295 _DefaultName =
"fgcmOutputProducts"
297 def __init__(self, **kwargs):
298 super().__init__(**kwargs)
300 def runQuantum(self, butlerQC, inputRefs, outputRefs):
302 handleDict[
'camera'] = butlerQC.get(inputRefs.camera)
303 handleDict[
'fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
304 handleDict[
'fgcmVisitCatalog'] = butlerQC.get(inputRefs.fgcmVisitCatalog)
305 handleDict[
'fgcmStandardStars'] = butlerQC.get(inputRefs.fgcmStandardStars)
307 if self.config.doZeropointOutput:
308 handleDict[
'fgcmZeropoints'] = butlerQC.get(inputRefs.fgcmZeropoints)
309 photoCalibRefDict = {photoCalibRef.dataId[
'visit']:
310 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
312 if self.config.doAtmosphereOutput:
313 handleDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
314 atmRefDict = {atmRef.dataId[
'visit']: atmRef
for
315 atmRef
in outputRefs.fgcmTransmissionAtmosphere}
317 if self.config.doReferenceCalibration:
320 for ref
in inputRefs.refCat],
321 refCats=butlerQC.get(inputRefs.refCat),
322 name=self.config.connections.refCat,
326 self.refObjLoader =
None
328 if self.config.doTractStars:
329 handleDict[
'skymap'] = butlerQC.get(inputRefs.skymap)
330 tractStarRefDict = {tractStarRef.dataId[
"tract"]: tractStarRef
for
331 tractStarRef
in outputRefs.fgcmTractStars}
333 struct = self.run(handleDict, self.config.physicalFilterMap)
336 if struct.photoCalibCatalogs
is not None:
337 self.log.info(
"Outputting photoCalib catalogs.")
338 for visit, expCatalog
in struct.photoCalibCatalogs:
339 butlerQC.put(expCatalog, photoCalibRefDict[visit])
340 self.log.info(
"Done outputting photoCalib catalogs.")
343 if struct.atmospheres
is not None:
344 self.log.info(
"Outputting atmosphere transmission files.")
345 for visit, atm
in struct.atmospheres:
346 butlerQC.put(atm, atmRefDict[visit])
347 self.log.info(
"Done outputting atmosphere files.")
349 if self.config.doReferenceCalibration:
352 schema.addField(
'offset', type=np.float64,
353 doc=
"Post-process calibration offset (mag)")
355 offsetCat.resize(len(struct.offsets))
356 offsetCat[
'offset'][:] = struct.offsets
358 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
360 if self.config.doTractStars:
361 self.log.info(
"Outputting standard stars per-tract.")
362 for tractId, catalog
in struct.tractStars:
363 butlerQC.put(catalog, tractStarRefDict[tractId])
367 def run(self, handleDict, physicalFilterMap):
368 """Run the output products task.
373 All handles are `lsst.daf.butler.DeferredDatasetHandle`
374 handle dictionary with keys:
377 Camera object (`lsst.afw.cameraGeom.Camera`)
378 ``"fgcmLookUpTable"``
379 handle for the FGCM look-up table.
380 ``"fgcmVisitCatalog"``
381 handle for visit summary catalog.
382 ``"fgcmStandardStars"``
383 handle for the output standard star catalog.
385 handle for the zeropoint data catalog.
386 ``"fgcmAtmosphereParameters"``
387 handle for the atmosphere parameter catalog.
388 ``"fgcmBuildStarsTableConfig"``
389 Config for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
391 Skymap for sharding standard stars (optional).
393 physicalFilterMap : `dict`
394 Dictionary of mappings from physical filter to FGCM band.
398 retStruct : `lsst.pipe.base.Struct`
399 Output structure with keys:
401 offsets : `np.ndarray`
402 Final reference offsets, per band.
403 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
404 Generator that returns (visit, transmissionCurve) tuples.
405 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
406 Generator that returns (visit, exposureCatalog) tuples.
408 stdCat = handleDict[
'fgcmStandardStars'].get()
409 md = stdCat.getMetadata()
410 bands = md.getArray(
'BANDS')
412 if self.config.doReferenceCalibration:
413 lutCat = handleDict[
'fgcmLookUpTable'].get()
414 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
416 offsets = np.zeros(len(bands))
418 if self.config.doZeropointOutput:
419 zptCat = handleDict[
'fgcmZeropoints'].get()
420 visitCat = handleDict[
'fgcmVisitCatalog'].get()
422 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
427 if self.config.doAtmosphereOutput:
428 atmCat = handleDict[
'fgcmAtmosphereParameters'].get()
429 atmgen = self._outputAtmospheres(handleDict, atmCat)
433 if self.config.doTractStars:
434 skymap = handleDict[
'skymap']
435 tractstargen = self._outputTractStars(skymap, stdCat)
439 retStruct = pipeBase.Struct(offsets=offsets,
441 tractStars=tractstargen)
442 retStruct.photoCalibCatalogs = pcgen
446 def generateTractOutputProducts(self, handleDict, tract,
447 visitCat, zptCat, atmCat, stdCat,
448 fgcmBuildStarsConfig):
450 Generate the output products for a given tract, as specified in the config.
452 This method is here to have an alternate entry-point for
458 All handles are `lsst.daf.butler.DeferredDatasetHandle`
459 handle dictionary with keys:
462 Camera object (`lsst.afw.cameraGeom.Camera`)
463 ``"fgcmLookUpTable"``
464 handle for the FGCM look-up table.
467 visitCat : `lsst.afw.table.BaseCatalog`
468 FGCM visitCat from `FgcmBuildStarsTask`
469 zptCat : `lsst.afw.table.BaseCatalog`
470 FGCM zeropoint catalog from `FgcmFitCycleTask`
471 atmCat : `lsst.afw.table.BaseCatalog`
472 FGCM atmosphere parameter catalog from `FgcmFitCycleTask`
473 stdCat : `lsst.afw.table.SimpleCatalog`
474 FGCM standard star catalog from `FgcmFitCycleTask`
475 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
476 Configuration object from `FgcmBuildStarsTask`
480 retStruct : `lsst.pipe.base.Struct`
481 Output structure with keys:
483 offsets : `np.ndarray`
484 Final reference offsets, per band.
485 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
486 Generator that returns (visit, transmissionCurve) tuples.
487 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
488 Generator that returns (visit, exposureCatalog) tuples.
490 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
492 md = stdCat.getMetadata()
493 bands = md.getArray(
'BANDS')
495 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
496 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
497 "in fgcmBuildStarsTask.")
499 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
500 self.log.warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
502 if self.config.doReferenceCalibration:
503 lutCat = handleDict[
'fgcmLookUpTable'].get()
504 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
506 offsets = np.zeros(len(bands))
508 if self.config.doZeropointOutput:
509 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
514 if self.config.doAtmosphereOutput:
515 atmgen = self._outputAtmospheres(handleDict, atmCat)
519 retStruct = pipeBase.Struct(offsets=offsets,
521 retStruct.photoCalibCatalogs = pcgen
525 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
527 Compute offsets relative to a reference catalog.
529 This method splits the star catalog into healpix pixels
530 and computes the calibration transfer for a sample of
531 these pixels to approximate the 'absolute' calibration
532 values (on for each band) to apply to transfer the
537 stdCat : `lsst.afw.table.SimpleCatalog`
539 lutCat : `lsst.afw.table.SimpleCatalog`
541 physicalFilterMap : `dict`
542 Dictionary of mappings from physical filter to FGCM band.
543 bands : `list` [`str`]
544 List of band names from FGCM output
547 offsets : `numpy.array` of floats
548 Per band zeropoint offsets
554 minObs = stdCat[
'ngood'].min(axis=1)
556 goodStars = (minObs >= 1)
557 stdCat = stdCat[goodStars]
559 self.log.info(
"Found %d stars with at least 1 good observation in each band" %
566 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
567 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
568 physicalFilterMapBands = list(physicalFilterMap.values())
569 physicalFilterMapFilters = list(physicalFilterMap.keys())
573 physicalFilterMapIndex = physicalFilterMapBands.index(band)
574 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
576 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
577 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
579 physical=stdPhysicalFilter))
589 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
590 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
591 doc=
"instrumental flux (counts)")
592 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
593 doc=
"instrumental flux error (counts)")
594 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
602 ipring = hpg.angle_to_pixel(
603 self.config.referencePixelizationNside,
608 h, rev = fgcm.fgcmUtilities.histogram_rev_sorted(ipring)
610 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
612 self.log.info(
"Found %d pixels (nside=%d) with at least %d good stars" %
614 self.config.referencePixelizationNside,
615 self.config.referencePixelizationMinStars))
617 if gdpix.size < self.config.referencePixelizationNPixels:
618 self.log.warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
619 (gdpix.size, self.config.referencePixelizationNPixels))
622 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
624 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
625 (
'nstar',
'i4', len(bands)),
626 (
'nmatch',
'i4', len(bands)),
627 (
'zp',
'f4', len(bands)),
628 (
'zpErr',
'f4', len(bands))])
629 results[
'hpix'] = ipring[rev[rev[gdpix]]]
632 selected = np.zeros(len(stdCat), dtype=bool)
634 refFluxFields = [
None]*len(bands)
636 for p_index, pix
in enumerate(gdpix):
637 i1a = rev[rev[pix]: rev[pix + 1]]
645 for b_index, filterLabel
in enumerate(filterLabels):
646 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
648 selected, refFluxFields)
649 results[
'nstar'][p_index, b_index] = len(i1a)
650 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
651 results[
'zp'][p_index, b_index] = struct.zp
652 results[
'zpErr'][p_index, b_index] = struct.sigma
655 offsets = np.zeros(len(bands))
657 for b_index, band
in enumerate(bands):
659 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
660 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
663 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
664 self.log.info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
665 band, offsets[b_index], madSigma)
669 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
670 b_index, filterLabel, stdCat, selected, refFluxFields):
672 Compute the zeropoint offset between the fgcm stdCat and the reference
673 stars for one pixel in one band
677 sourceMapper : `lsst.afw.table.SchemaMapper`
678 Mapper to go from stdCat to calibratable catalog
679 badStarKey : `lsst.afw.table.Key`
680 Key for the field with bad stars
682 Index of the band in the star catalog
683 filterLabel : `lsst.afw.image.FilterLabel`
684 filterLabel with band and physical filter
685 stdCat : `lsst.afw.table.SimpleCatalog`
687 selected : `numpy.array(dtype=bool)`
688 Boolean array of which stars are in the pixel
689 refFluxFields : `list`
690 List of names of flux fields for reference catalog
694 sourceCat.reserve(selected.sum())
695 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
696 sourceCat[
'instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
697 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
698 * sourceCat[
'instFlux'])
702 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
703 for rec
in sourceCat[badStar]:
704 rec.set(badStarKey,
True)
706 exposure = afwImage.ExposureF()
707 exposure.setFilter(filterLabel)
709 if refFluxFields[b_index]
is None:
712 ctr = stdCat[0].getCoord()
713 rad = 0.05*lsst.geom.degrees
714 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
715 refFluxFields[b_index] = refDataTest.fluxField
718 calConfig = copy.copy(self.config.photoCal.value)
719 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
720 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
721 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
723 schema=sourceCat.getSchema())
725 struct = calTask.run(exposure, sourceCat)
729 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
730 physicalFilterMap, tract=None):
731 """Output the zeropoints in fgcm_photoCalib format.
735 camera : `lsst.afw.cameraGeom.Camera`
736 Camera from the butler.
737 zptCat : `lsst.afw.table.BaseCatalog`
738 FGCM zeropoint catalog from `FgcmFitCycleTask`.
739 visitCat : `lsst.afw.table.BaseCatalog`
740 FGCM visitCat from `FgcmBuildStarsTask`.
741 offsets : `numpy.array`
742 Float array of absolute calibration offsets, one for each filter.
743 bands : `list` [`str`]
744 List of band names from FGCM output.
745 physicalFilterMap : `dict`
746 Dictionary of mappings from physical filter to FGCM band.
747 tract: `int`, optional
748 Tract number to output. Default is None (global calibration)
752 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
753 Generator that returns (visit, exposureCatalog) tuples.
758 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
759 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
760 & (zptCat[
'fgcmZptVar'] > 0.0)
761 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE)
762 & (zptCat[
'fgcmfZptCheb'][:, 0] > 0.0))
765 badVisits = np.unique(zptCat[
'visit'][~selected])
766 goodVisits = np.unique(zptCat[
'visit'][selected])
767 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
768 for allBadVisit
in allBadVisits:
769 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
773 for f
in physicalFilterMap:
775 if physicalFilterMap[f]
in bands:
776 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
780 for ccdIndex, detector
in enumerate(camera):
781 ccdMapping[detector.getId()] = ccdIndex
786 scalingMapping[rec[
'visit']] = rec[
'scaling']
788 if self.config.doComposeWcsJacobian:
789 approxPixelAreaFields = computeApproxPixelAreaFields(camera)
793 zptVisitCatalog =
None
796 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
797 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
799 for rec
in zptCat[selected]:
801 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
808 postCalibrationOffset = offsetMapping[rec[
'filtername']]
809 if self.config.doApplyMeanChromaticCorrection:
810 postCalibrationOffset += rec[
'fgcmDeltaChrom']
812 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
813 rec[
'fgcmfZptChebXyMax'])
815 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
816 rec[
'fgcmfZptChebXyMax'],
817 offset=postCalibrationOffset,
820 if self.config.doComposeWcsJacobian:
831 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
832 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
834 calibrationErr=calibErr,
835 calibration=fgcmField,
839 if rec[
'visit'] != lastVisit:
844 zptVisitCatalog.sort()
845 yield (int(lastVisit), zptVisitCatalog)
848 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
849 zptExpCatSchema.addField(
'visit', type=
'L', doc=
'Visit number')
853 zptVisitCatalog.setMetadata(metadata)
855 lastVisit = int(rec[
'visit'])
857 catRecord = zptVisitCatalog.addNew()
858 catRecord[
'id'] = int(rec[
'detector'])
859 catRecord[
'visit'] = rec[
'visit']
860 catRecord.setPhotoCalib(photoCalib)
864 zptVisitCatalog.sort()
865 yield (int(lastVisit), zptVisitCatalog)
868 def _getChebyshevBoundedField(coefficients, xyMax, offset=0.0, scaling=1.0):
870 Make a ChebyshevBoundedField from fgcm coefficients, with optional offset
875 coefficients: `numpy.array`
876 Flattened array of chebyshev coefficients
877 xyMax: `list` of length 2
878 Maximum x and y of the chebyshev bounding box
879 offset: `float`, optional
880 Absolute calibration offset. Default is 0.0
881 scaling: `float`, optional
882 Flat scaling value from fgcmBuildStars. Default is 1.0
886 boundedField: `lsst.afw.math.ChebyshevBoundedField`
889 orderPlus1 = int(np.sqrt(coefficients.size))
890 pars = np.zeros((orderPlus1, orderPlus1))
895 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
896 * (10.**(offset/-2.5))*scaling)
902 def _outputAtmospheres(self, handleDict, atmCat):
904 Output the atmospheres.
909 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
910 The handleDict has the follownig keys:
912 ``"fgcmLookUpTable"``
913 handle for the FGCM look-up table.
914 atmCat : `lsst.afw.table.BaseCatalog`
915 FGCM atmosphere parameter catalog from fgcmFitCycleTask.
919 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
920 Generator that returns (visit, transmissionCurve) tuples.
923 lutCat = handleDict[
'fgcmLookUpTable'].get()
925 atmosphereTableName = lutCat[0][
'tablename']
926 elevation = lutCat[0][
'elevation']
927 atmLambda = lutCat[0][
'atmLambda']
932 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
940 modGen = fgcm.ModtranGenerator(elevation)
941 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
942 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
943 except (ValueError, IOError)
as e:
944 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
945 "but modtran not configured to run.")
from e
947 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
949 for i, visit
in enumerate(atmCat[
'visit']):
950 if atmTable
is not None:
952 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
953 pwv=atmCat[i][
'pwv'],
955 tau=atmCat[i][
'tau'],
956 alpha=atmCat[i][
'alpha'],
958 ctranslamstd=[atmCat[i][
'cTrans'],
959 atmCat[i][
'lamStd']])
962 modAtm = modGen(pmb=atmCat[i][
'pmb'],
963 pwv=atmCat[i][
'pwv'],
965 tau=atmCat[i][
'tau'],
966 alpha=atmCat[i][
'alpha'],
968 lambdaRange=lambdaRange,
969 lambdaStep=lambdaStep,
970 ctranslamstd=[atmCat[i][
'cTrans'],
971 atmCat[i][
'lamStd']])
972 atmVals = modAtm[
'COMBINED']
975 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
976 wavelengths=atmLambda,
977 throughputAtMin=atmVals[0],
978 throughputAtMax=atmVals[-1])
980 yield (int(visit), curve)
982 def _outputTractStars(self, skymap, stdCat):
983 """Output the tract-sharded stars.
987 skymap : `lsst.skymap.SkyMap`
988 Skymap for tract id information.
989 stdCat : `lsst.afw.table.SimpleCatalog`
990 FGCM standard star catalog from ``FgcmFitCycleTask``
994 tractgen : `generator` [(`int`, `astropy.table.Table`)]
995 Generator that returns (tractId, Table) tuples.
997 md = stdCat.getMetadata()
998 bands = md.getArray(
'BANDS')
1002 (
"isolated_star_id",
"i8"),
1010 (f
"mag_{band}",
"f4"),
1011 (f
"magErr_{band}",
"f4"),
1012 (f
"ngood_{band}",
"i4"),
1016 tractIds = skymap.findTractIdArray(stdCat[
"coord_ra"], stdCat[
"coord_dec"])
1018 h, rev = esutil.stat.histogram(tractIds, rev=
True)
1020 good, = np.where(h > 0)
1023 i1a = rev[rev[index]: rev[index + 1]]
1024 tractId = tractIds[i1a[0]]
1026 table = Table(np.zeros(len(i1a), dtype=dtype))
1027 table[
"fgcm_id"] = stdCat[
"id"][i1a]
1028 table[
"isolated_star_id"] = stdCat[
"isolated_star_id"][i1a]
1029 table[
"ra"] = np.rad2deg(stdCat[
"coord_ra"][i1a])*units.degree
1030 table[
"dec"] = np.rad2deg(stdCat[
"coord_dec"][i1a])*units.degree
1032 for i, band
in enumerate(bands):
1033 table[f
"mag_{band}"] = stdCat[
"mag_std_noabs"][i1a, i]*units.ABmag
1034 table[f
"magErr_{band}"] = stdCat[
"magErr_std"][i1a, i]*units.ABmag
1035 table[f
"ngood_{band}"] = stdCat[
"ngood"][i1a, i]
1038 bad = (table[f
"mag_{band}"] > 90.0)
1039 table[f
"mag_{band}"][bad] = np.nan
1040 table[f
"magErr_{band}"][bad] = np.nan
1042 yield (int(tractId), table)
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.
Defines the fields and offsets for a table.
A mapping between the keys of two Schemas, used to copy data between them.
Class for storing ordered metadata with comments.
An integer coordinate rectangle.