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))
764 badVisits = np.unique(zptCat[
'visit'][~selected])
765 goodVisits = np.unique(zptCat[
'visit'][selected])
766 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
767 for allBadVisit
in allBadVisits:
768 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
772 for f
in physicalFilterMap:
774 if physicalFilterMap[f]
in bands:
775 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
779 for ccdIndex, detector
in enumerate(camera):
780 ccdMapping[detector.getId()] = ccdIndex
785 scalingMapping[rec[
'visit']] = rec[
'scaling']
787 if self.config.doComposeWcsJacobian:
788 approxPixelAreaFields = computeApproxPixelAreaFields(camera)
792 zptVisitCatalog =
None
795 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
796 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
798 for rec
in zptCat[selected]:
800 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
807 postCalibrationOffset = offsetMapping[rec[
'filtername']]
808 if self.config.doApplyMeanChromaticCorrection:
809 postCalibrationOffset += rec[
'fgcmDeltaChrom']
811 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
812 rec[
'fgcmfZptChebXyMax'])
814 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
815 rec[
'fgcmfZptChebXyMax'],
816 offset=postCalibrationOffset,
819 if self.config.doComposeWcsJacobian:
830 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
831 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
833 calibrationErr=calibErr,
834 calibration=fgcmField,
838 if rec[
'visit'] != lastVisit:
843 zptVisitCatalog.sort()
844 yield (int(lastVisit), zptVisitCatalog)
847 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
848 zptExpCatSchema.addField(
'visit', type=
'L', doc=
'Visit number')
852 zptVisitCatalog.setMetadata(metadata)
854 lastVisit = int(rec[
'visit'])
856 catRecord = zptVisitCatalog.addNew()
857 catRecord[
'id'] = int(rec[
'detector'])
858 catRecord[
'visit'] = rec[
'visit']
859 catRecord.setPhotoCalib(photoCalib)
863 zptVisitCatalog.sort()
864 yield (int(lastVisit), zptVisitCatalog)
867 def _getChebyshevBoundedField(coefficients, xyMax, offset=0.0, scaling=1.0):
869 Make a ChebyshevBoundedField from fgcm coefficients, with optional offset
874 coefficients: `numpy.array`
875 Flattened array of chebyshev coefficients
876 xyMax: `list` of length 2
877 Maximum x and y of the chebyshev bounding box
878 offset: `float`, optional
879 Absolute calibration offset. Default is 0.0
880 scaling: `float`, optional
881 Flat scaling value from fgcmBuildStars. Default is 1.0
885 boundedField: `lsst.afw.math.ChebyshevBoundedField`
888 orderPlus1 = int(np.sqrt(coefficients.size))
889 pars = np.zeros((orderPlus1, orderPlus1))
894 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
895 * (10.**(offset/-2.5))*scaling)
901 def _outputAtmospheres(self, handleDict, atmCat):
903 Output the atmospheres.
908 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
909 The handleDict has the follownig keys:
911 ``"fgcmLookUpTable"``
912 handle for the FGCM look-up table.
913 atmCat : `lsst.afw.table.BaseCatalog`
914 FGCM atmosphere parameter catalog from fgcmFitCycleTask.
918 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
919 Generator that returns (visit, transmissionCurve) tuples.
922 lutCat = handleDict[
'fgcmLookUpTable'].get()
924 atmosphereTableName = lutCat[0][
'tablename']
925 elevation = lutCat[0][
'elevation']
926 atmLambda = lutCat[0][
'atmLambda']
931 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
939 modGen = fgcm.ModtranGenerator(elevation)
940 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
941 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
942 except (ValueError, IOError)
as e:
943 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
944 "but modtran not configured to run.")
from e
946 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
948 for i, visit
in enumerate(atmCat[
'visit']):
949 if atmTable
is not None:
951 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
952 pwv=atmCat[i][
'pwv'],
954 tau=atmCat[i][
'tau'],
955 alpha=atmCat[i][
'alpha'],
957 ctranslamstd=[atmCat[i][
'cTrans'],
958 atmCat[i][
'lamStd']])
961 modAtm = modGen(pmb=atmCat[i][
'pmb'],
962 pwv=atmCat[i][
'pwv'],
964 tau=atmCat[i][
'tau'],
965 alpha=atmCat[i][
'alpha'],
967 lambdaRange=lambdaRange,
968 lambdaStep=lambdaStep,
969 ctranslamstd=[atmCat[i][
'cTrans'],
970 atmCat[i][
'lamStd']])
971 atmVals = modAtm[
'COMBINED']
974 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
975 wavelengths=atmLambda,
976 throughputAtMin=atmVals[0],
977 throughputAtMax=atmVals[-1])
979 yield (int(visit), curve)
981 def _outputTractStars(self, skymap, stdCat):
982 """Output the tract-sharded stars.
986 skymap : `lsst.skymap.SkyMap`
987 Skymap for tract id information.
988 stdCat : `lsst.afw.table.SimpleCatalog`
989 FGCM standard star catalog from ``FgcmFitCycleTask``
993 tractgen : `generator` [(`int`, `astropy.table.Table`)]
994 Generator that returns (tractId, Table) tuples.
996 md = stdCat.getMetadata()
997 bands = md.getArray(
'BANDS')
1001 (
"isolated_star_id",
"i8"),
1009 (f
"mag_{band}",
"f4"),
1010 (f
"magErr_{band}",
"f4"),
1011 (f
"ngood_{band}",
"i4"),
1015 tractIds = skymap.findTractIdArray(stdCat[
"coord_ra"], stdCat[
"coord_dec"])
1017 h, rev = esutil.stat.histogram(tractIds, rev=
True)
1019 good, = np.where(h > 0)
1022 i1a = rev[rev[index]: rev[index + 1]]
1023 tractId = tractIds[i1a[0]]
1025 table = Table(np.zeros(len(i1a), dtype=dtype))
1026 table[
"fgcm_id"] = stdCat[
"id"][i1a]
1027 table[
"isolated_star_id"] = stdCat[
"isolated_star_id"][i1a]
1028 table[
"ra"] = np.rad2deg(stdCat[
"coord_ra"][i1a])*units.degree
1029 table[
"dec"] = np.rad2deg(stdCat[
"coord_dec"][i1a])*units.degree
1031 for i, band
in enumerate(bands):
1032 table[f
"mag_{band}"] = stdCat[
"mag_std_noabs"][i1a, i]*units.ABmag
1033 table[f
"magErr_{band}"] = stdCat[
"magErr_std"][i1a, i]*units.ABmag
1034 table[f
"ngood_{band}"] = stdCat[
"ngood"][i1a, i]
1037 bad = (table[f
"mag_{band}"] > 90.0)
1038 table[f
"mag_{band}"][bad] = np.nan
1039 table[f
"magErr_{band}"][bad] = np.nan
1041 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.