23"""Make the final fgcmcal output products.
25This task takes the final output from fgcmFitCycle and produces the following
26outputs for use in the DM stack: the FGCM standard stars in a reference
27catalog format; the model atmospheres in "transmission_atmosphere_fgcm"
28format; and the zeropoints in "fgcm_photoCalib" format. Optionally, the
29task can transfer the 'absolute' calibration
from a reference catalog
30to put the fgcm standard stars
in units of Jansky. This
is accomplished
31by matching stars
in a sample of healpix pixels,
and applying the median
39from astropy import units
41import lsst.daf.base as dafBase
42import lsst.pex.config as pexConfig
43import lsst.pipe.base as pipeBase
44from lsst.pipe.base import connectionTypes
45from lsst.afw.image import TransmissionCurve
46from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask
47from lsst.meas.algorithms import ReferenceObjectLoader, LoadReferenceObjectsConfig
48from lsst.pipe.tasks.photoCal import PhotoCalTask
50import lsst.afw.image as afwImage
51import lsst.afw.math as afwMath
52import lsst.afw.table as afwTable
53from lsst.meas.algorithms import DatasetConfig
54from lsst.meas.algorithms.ingestIndexReferenceTask import addRefCatMetadata
56from .utilities import computeApproxPixelAreaFields
57from .utilities import lookupStaticCalibrations
58from .utilities import FGCM_ILLEGAL_VALUE
62__all__ = ['FgcmOutputProductsConfig', 'FgcmOutputProductsTask']
65class FgcmOutputProductsConnections(pipeBase.PipelineTaskConnections,
66 dimensions=("instrument",),
67 defaultTemplates={
"cycleNumber":
"0"}):
68 camera = connectionTypes.PrerequisiteInput(
69 doc=
"Camera instrument",
71 storageClass=
"Camera",
72 dimensions=(
"instrument",),
73 lookupFunction=lookupStaticCalibrations,
77 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
78 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
79 "chromatic corrections."),
80 name=
"fgcmLookUpTable",
81 storageClass=
"Catalog",
82 dimensions=(
"instrument",),
86 fgcmVisitCatalog = connectionTypes.Input(
87 doc=
"Catalog of visit information for fgcm",
88 name=
"fgcmVisitCatalog",
89 storageClass=
"Catalog",
90 dimensions=(
"instrument",),
94 fgcmStandardStars = connectionTypes.Input(
95 doc=
"Catalog of standard star data from fgcm fit",
96 name=
"fgcmStandardStars{cycleNumber}",
97 storageClass=
"SimpleCatalog",
98 dimensions=(
"instrument",),
102 fgcmZeropoints = connectionTypes.Input(
103 doc=
"Catalog of zeropoints from fgcm fit",
104 name=
"fgcmZeropoints{cycleNumber}",
105 storageClass=
"Catalog",
106 dimensions=(
"instrument",),
110 fgcmAtmosphereParameters = connectionTypes.Input(
111 doc=
"Catalog of atmosphere parameters from fgcm fit",
112 name=
"fgcmAtmosphereParameters{cycleNumber}",
113 storageClass=
"Catalog",
114 dimensions=(
"instrument",),
118 refCat = connectionTypes.PrerequisiteInput(
119 doc=
"Reference catalog to use for photometric calibration",
121 storageClass=
"SimpleCatalog",
122 dimensions=(
"skypix",),
127 fgcmPhotoCalib = connectionTypes.Output(
128 doc=(
"Per-visit photometric calibrations derived from fgcm calibration. "
129 "These catalogs use detector id for the id and are sorted for "
130 "fast lookups of a detector."),
131 name=
"fgcmPhotoCalibCatalog",
132 storageClass=
"ExposureCatalog",
133 dimensions=(
"instrument",
"visit",),
137 fgcmTransmissionAtmosphere = connectionTypes.Output(
138 doc=
"Per-visit atmosphere transmission files produced from fgcm calibration",
139 name=
"transmission_atmosphere_fgcm",
140 storageClass=
"TransmissionCurve",
141 dimensions=(
"instrument",
146 fgcmOffsets = connectionTypes.Output(
147 doc=
"Per-band offsets computed from doReferenceCalibration",
148 name=
"fgcmReferenceCalibrationOffsets",
149 storageClass=
"Catalog",
150 dimensions=(
"instrument",),
154 def __init__(self, *, config=None):
155 super().__init__(config=config)
157 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
158 raise ValueError(
"cycleNumber must be of integer format")
160 if not config.doReferenceCalibration:
161 self.prerequisiteInputs.remove(
"refCat")
162 if not config.doAtmosphereOutput:
163 self.inputs.remove(
"fgcmAtmosphereParameters")
164 if not config.doZeropointOutput:
165 self.inputs.remove(
"fgcmZeropoints")
166 if not config.doReferenceCalibration:
167 self.outputs.remove(
"fgcmOffsets")
170class FgcmOutputProductsConfig(pipeBase.PipelineTaskConfig,
171 pipelineConnections=FgcmOutputProductsConnections):
172 """Config for FgcmOutputProductsTask"""
174 cycleNumber = pexConfig.Field(
175 doc=
"Final fit cycle from FGCM fit",
178 deprecated=(
"This config is no longer used, and will be removed after v25. "
179 "Please set config.connections.cycleNumber directly instead."),
181 physicalFilterMap = pexConfig.DictField(
182 doc=
"Mapping from 'physicalFilter' to band.",
189 doReferenceCalibration = pexConfig.Field(
190 doc=(
"Transfer 'absolute' calibration from reference catalog? "
191 "This afterburner step is unnecessary if reference stars "
192 "were used in the full fit in FgcmFitCycleTask."),
196 doRefcatOutput = pexConfig.Field(
197 doc=
"Output standard stars in reference catalog format",
200 deprecated=
"doRefcatOutput is no longer supported; this config will be removed after v24"
202 doAtmosphereOutput = pexConfig.Field(
203 doc=
"Output atmospheres in transmission_atmosphere_fgcm format",
207 doZeropointOutput = pexConfig.Field(
208 doc=
"Output zeropoints in fgcm_photoCalib format",
212 doComposeWcsJacobian = pexConfig.Field(
213 doc=
"Compose Jacobian of WCS with fgcm calibration for output photoCalib?",
217 doApplyMeanChromaticCorrection = pexConfig.Field(
218 doc=
"Apply the mean chromatic correction to the zeropoints?",
222 refObjLoader = pexConfig.ConfigurableField(
223 target=LoadIndexedReferenceObjectsTask,
224 doc=
"reference object loader for 'absolute' photometric calibration",
225 deprecated=
"refObjLoader is deprecated, and will be removed after v24",
227 photoCal = pexConfig.ConfigurableField(
229 doc=
"task to perform 'absolute' calibration",
231 referencePixelizationNside = pexConfig.Field(
232 doc=
"Healpix nside to pixelize catalog to compare to reference catalog",
236 referencePixelizationMinStars = pexConfig.Field(
237 doc=(
"Minimum number of stars per healpix pixel to select for comparison"
238 "to the specified reference catalog"),
242 referenceMinMatch = pexConfig.Field(
243 doc=
"Minimum number of stars matched to reference catalog to be used in statistics",
247 referencePixelizationNPixels = pexConfig.Field(
248 doc=(
"Number of healpix pixels to sample to do comparison. "
249 "Doing too many will take a long time and not yield any more "
250 "precise results because the final number is the median offset "
251 "(per band) from the set of pixels."),
255 datasetConfig = pexConfig.ConfigField(
257 doc=
"Configuration for writing/reading ingested catalog",
258 deprecated=
"The datasetConfig was only used for gen2; this config will be removed after v24.",
261 def setDefaults(self):
262 pexConfig.Config.setDefaults(self)
272 self.photoCal.applyColorTerms =
False
273 self.photoCal.fluxField =
'instFlux'
274 self.photoCal.magErrFloor = 0.003
275 self.photoCal.match.referenceSelection.doSignalToNoise =
True
276 self.photoCal.match.referenceSelection.signalToNoise.minimum = 10.0
277 self.photoCal.match.sourceSelection.doSignalToNoise =
True
278 self.photoCal.match.sourceSelection.signalToNoise.minimum = 10.0
279 self.photoCal.match.sourceSelection.signalToNoise.fluxField =
'instFlux'
280 self.photoCal.match.sourceSelection.signalToNoise.errField =
'instFluxErr'
281 self.photoCal.match.sourceSelection.doFlags =
True
282 self.photoCal.match.sourceSelection.flags.good = []
283 self.photoCal.match.sourceSelection.flags.bad = [
'flag_badStar']
284 self.photoCal.match.sourceSelection.doUnresolved =
False
287class FgcmOutputProductsTask(pipeBase.PipelineTask):
289 Output products from FGCM
global calibration.
292 ConfigClass = FgcmOutputProductsConfig
293 _DefaultName = "fgcmOutputProducts"
295 def __init__(self, **kwargs):
296 super().__init__(**kwargs)
298 def runQuantum(self, butlerQC, inputRefs, outputRefs):
300 handleDict[
'camera'] = butlerQC.get(inputRefs.camera)
301 handleDict[
'fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
302 handleDict[
'fgcmVisitCatalog'] = butlerQC.get(inputRefs.fgcmVisitCatalog)
303 handleDict[
'fgcmStandardStars'] = butlerQC.get(inputRefs.fgcmStandardStars)
305 if self.config.doZeropointOutput:
306 handleDict[
'fgcmZeropoints'] = butlerQC.get(inputRefs.fgcmZeropoints)
307 photoCalibRefDict = {photoCalibRef.dataId.byName()[
'visit']:
308 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
310 if self.config.doAtmosphereOutput:
311 handleDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
312 atmRefDict = {atmRef.dataId.byName()[
'visit']: atmRef
for
313 atmRef
in outputRefs.fgcmTransmissionAtmosphere}
315 if self.config.doReferenceCalibration:
316 refConfig = LoadReferenceObjectsConfig()
317 self.refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
318 for ref
in inputRefs.refCat],
319 refCats=butlerQC.get(inputRefs.refCat),
323 self.refObjLoader =
None
325 struct = self.run(handleDict, self.config.physicalFilterMap)
328 if struct.photoCalibCatalogs
is not None:
329 self.log.info(
"Outputting photoCalib catalogs.")
330 for visit, expCatalog
in struct.photoCalibCatalogs:
331 butlerQC.put(expCatalog, photoCalibRefDict[visit])
332 self.log.info(
"Done outputting photoCalib catalogs.")
335 if struct.atmospheres
is not None:
336 self.log.info(
"Outputting atmosphere transmission files.")
337 for visit, atm
in struct.atmospheres:
338 butlerQC.put(atm, atmRefDict[visit])
339 self.log.info(
"Done outputting atmosphere files.")
341 if self.config.doReferenceCalibration:
343 schema = afwTable.Schema()
344 schema.addField(
'offset', type=np.float64,
345 doc=
"Post-process calibration offset (mag)")
346 offsetCat = afwTable.BaseCatalog(schema)
347 offsetCat.resize(len(struct.offsets))
348 offsetCat[
'offset'][:] = struct.offsets
350 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
354 def run(self, handleDict, physicalFilterMap):
355 """Run the output products task.
360 All handles are `lsst.daf.butler.DeferredDatasetHandle`
361 handle dictionary with keys:
364 Camera object (`lsst.afw.cameraGeom.Camera`)
365 ``
"fgcmLookUpTable"``
366 handle
for the FGCM look-up table.
367 ``
"fgcmVisitCatalog"``
368 handle
for visit summary catalog.
369 ``
"fgcmStandardStars"``
370 handle
for the output standard star catalog.
372 handle
for the zeropoint data catalog.
373 ``
"fgcmAtmosphereParameters"``
374 handle
for the atmosphere parameter catalog.
375 ``
"fgcmBuildStarsTableConfig"``
376 Config
for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
377 physicalFilterMap : `dict`
378 Dictionary of mappings
from physical filter to FGCM band.
382 retStruct : `lsst.pipe.base.Struct`
383 Output structure
with keys:
385 offsets : `np.ndarray`
386 Final reference offsets, per band.
387 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
388 Generator that returns (visit, transmissionCurve) tuples.
389 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
390 Generator that returns (visit, exposureCatalog) tuples.
392 stdCat = handleDict['fgcmStandardStars'].get()
393 md = stdCat.getMetadata()
394 bands = md.getArray(
'BANDS')
396 if self.config.doReferenceCalibration:
397 lutCat = handleDict[
'fgcmLookUpTable'].get()
398 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
400 offsets = np.zeros(len(bands))
404 if self.config.doZeropointOutput:
405 zptCat = handleDict[
'fgcmZeropoints'].get()
406 visitCat = handleDict[
'fgcmVisitCatalog'].get()
408 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
413 if self.config.doAtmosphereOutput:
414 atmCat = handleDict[
'fgcmAtmosphereParameters'].get()
415 atmgen = self._outputAtmospheres(handleDict, atmCat)
419 retStruct = pipeBase.Struct(offsets=offsets,
421 retStruct.photoCalibCatalogs = pcgen
425 def generateTractOutputProducts(self, handleDict, tract,
426 visitCat, zptCat, atmCat, stdCat,
427 fgcmBuildStarsConfig):
429 Generate the output products for a given tract,
as specified
in the config.
431 This method
is here to have an alternate entry-point
for
437 All handles are `lsst.daf.butler.DeferredDatasetHandle`
438 handle dictionary
with keys:
441 Camera object (`lsst.afw.cameraGeom.Camera`)
442 ``
"fgcmLookUpTable"``
443 handle
for the FGCM look-up table.
446 visitCat : `lsst.afw.table.BaseCatalog`
447 FGCM visitCat
from `FgcmBuildStarsTask`
448 zptCat : `lsst.afw.table.BaseCatalog`
449 FGCM zeropoint catalog
from `FgcmFitCycleTask`
450 atmCat : `lsst.afw.table.BaseCatalog`
451 FGCM atmosphere parameter catalog
from `FgcmFitCycleTask`
452 stdCat : `lsst.afw.table.SimpleCatalog`
453 FGCM standard star catalog
from `FgcmFitCycleTask`
454 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
455 Configuration object
from `FgcmBuildStarsTask`
459 retStruct : `lsst.pipe.base.Struct`
460 Output structure
with keys:
462 offsets : `np.ndarray`
463 Final reference offsets, per band.
464 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
465 Generator that returns (visit, transmissionCurve) tuples.
466 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
467 Generator that returns (visit, exposureCatalog) tuples.
469 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
471 md = stdCat.getMetadata()
472 bands = md.getArray('BANDS')
474 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
475 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
476 "in fgcmBuildStarsTask.")
478 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
479 self.log.warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
481 if self.config.doReferenceCalibration:
482 lutCat = handleDict[
'fgcmLookUpTable'].get()
483 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
485 offsets = np.zeros(len(bands))
487 if self.config.doZeropointOutput:
488 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
493 if self.config.doAtmosphereOutput:
494 atmgen = self._outputAtmospheres(handleDict, atmCat)
498 retStruct = pipeBase.Struct(offsets=offsets,
500 retStruct.photoCalibCatalogs = pcgen
504 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
506 Compute offsets relative to a reference catalog.
508 This method splits the star catalog into healpix pixels
509 and computes the calibration transfer
for a sample of
510 these pixels to approximate the
'absolute' calibration
511 values (on
for each band) to apply to transfer the
516 stdCat : `lsst.afw.table.SimpleCatalog`
518 lutCat : `lsst.afw.table.SimpleCatalog`
520 physicalFilterMap : `dict`
521 Dictionary of mappings
from physical filter to FGCM band.
522 bands : `list` [`str`]
523 List of band names
from FGCM output
526 offsets : `numpy.array` of floats
527 Per band zeropoint offsets
533 minObs = stdCat[
'ngood'].min(axis=1)
535 goodStars = (minObs >= 1)
536 stdCat = stdCat[goodStars]
538 self.log.info(
"Found %d stars with at least 1 good observation in each band" %
545 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
546 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
547 physicalFilterMapBands = list(physicalFilterMap.values())
548 physicalFilterMapFilters = list(physicalFilterMap.keys())
552 physicalFilterMapIndex = physicalFilterMapBands.index(band)
553 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
555 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
556 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
557 filterLabels.append(afwImage.FilterLabel(band=band,
558 physical=stdPhysicalFilter))
567 sourceMapper = afwTable.SchemaMapper(stdCat.schema)
568 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
569 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
570 doc=
"instrumental flux (counts)")
571 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
572 doc=
"instrumental flux error (counts)")
573 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
581 ipring = hpg.angle_to_pixel(
582 self.config.referencePixelizationNside,
587 h, rev = esutil.stat.histogram(ipring, rev=
True)
589 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
591 self.log.info(
"Found %d pixels (nside=%d) with at least %d good stars" %
593 self.config.referencePixelizationNside,
594 self.config.referencePixelizationMinStars))
596 if gdpix.size < self.config.referencePixelizationNPixels:
597 self.log.warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
598 (gdpix.size, self.config.referencePixelizationNPixels))
601 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
603 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
604 (
'nstar',
'i4', len(bands)),
605 (
'nmatch',
'i4', len(bands)),
606 (
'zp',
'f4', len(bands)),
607 (
'zpErr',
'f4', len(bands))])
608 results[
'hpix'] = ipring[rev[rev[gdpix]]]
611 selected = np.zeros(len(stdCat), dtype=bool)
613 refFluxFields = [
None]*len(bands)
615 for p_index, pix
in enumerate(gdpix):
616 i1a = rev[rev[pix]: rev[pix + 1]]
624 for b_index, filterLabel
in enumerate(filterLabels):
625 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
627 selected, refFluxFields)
628 results[
'nstar'][p_index, b_index] = len(i1a)
629 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
630 results[
'zp'][p_index, b_index] = struct.zp
631 results[
'zpErr'][p_index, b_index] = struct.sigma
634 offsets = np.zeros(len(bands))
636 for b_index, band
in enumerate(bands):
638 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
639 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
642 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
643 self.log.info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
644 band, offsets[b_index], madSigma)
648 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
649 b_index, filterLabel, stdCat, selected, refFluxFields):
651 Compute the zeropoint offset between the fgcm stdCat and the reference
652 stars
for one pixel
in one band
656 sourceMapper : `lsst.afw.table.SchemaMapper`
657 Mapper to go
from stdCat to calibratable catalog
658 badStarKey : `lsst.afw.table.Key`
659 Key
for the field
with bad stars
661 Index of the band
in the star catalog
662 filterLabel : `lsst.afw.image.FilterLabel`
663 filterLabel
with band
and physical filter
664 stdCat : `lsst.afw.table.SimpleCatalog`
666 selected : `numpy.array(dtype=bool)`
667 Boolean array of which stars are
in the pixel
668 refFluxFields : `list`
669 List of names of flux fields
for reference catalog
672 sourceCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
673 sourceCat.reserve(selected.sum())
674 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
675 sourceCat['instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
676 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
677 * sourceCat[
'instFlux'])
681 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
682 for rec
in sourceCat[badStar]:
683 rec.set(badStarKey,
True)
685 exposure = afwImage.ExposureF()
686 exposure.setFilter(filterLabel)
688 if refFluxFields[b_index]
is None:
691 ctr = stdCat[0].getCoord()
692 rad = 0.05*lsst.geom.degrees
693 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
694 refFluxFields[b_index] = refDataTest.fluxField
697 calConfig = copy.copy(self.config.photoCal.value)
698 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
699 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
700 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
702 schema=sourceCat.getSchema())
704 struct = calTask.run(exposure, sourceCat)
708 def _formatCatalog(self, fgcmStarCat, offsets, bands):
710 Turn an FGCM-formatted star catalog, applying zeropoint offsets.
714 fgcmStarCat : `lsst.afw.Table.SimpleCatalog`
715 SimpleCatalog as output by fgcmcal
716 offsets : `list`
with len(self.bands) entries
717 Zeropoint offsets to apply
718 bands : `list` [`str`]
719 List of band names
from FGCM output
723 formattedCat: `lsst.afw.table.SimpleCatalog`
724 SimpleCatalog suitable
for using
as a reference catalog
727 sourceMapper = afwTable.SchemaMapper(fgcmStarCat.schema)
728 minSchema = LoadIndexedReferenceObjectsTask.makeMinimalSchema(bands,
732 sourceMapper.addMinimalSchema(minSchema)
734 sourceMapper.editOutputSchema().addField(
'%s_nGood' % (band), type=np.int32)
735 sourceMapper.editOutputSchema().addField(
'%s_nTotal' % (band), type=np.int32)
736 sourceMapper.editOutputSchema().addField(
'%s_nPsfCandidate' % (band), type=np.int32)
738 formattedCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
739 formattedCat.reserve(len(fgcmStarCat))
740 formattedCat.extend(fgcmStarCat, mapper=sourceMapper)
744 for b, band
in enumerate(bands):
745 mag = fgcmStarCat[
'mag_std_noabs'][:, b].astype(np.float64) + offsets[b]
748 flux = (mag*units.ABmag).to_value(units.nJy)
749 fluxErr = (np.log(10.)/2.5)*flux*fgcmStarCat[
'magErr_std'][:, b].astype(np.float64)
751 formattedCat[
'%s_flux' % (band)][:] = flux
752 formattedCat[
'%s_fluxErr' % (band)][:] = fluxErr
753 formattedCat[
'%s_nGood' % (band)][:] = fgcmStarCat[
'ngood'][:, b]
754 formattedCat[
'%s_nTotal' % (band)][:] = fgcmStarCat[
'ntotal'][:, b]
755 formattedCat[
'%s_nPsfCandidate' % (band)][:] = fgcmStarCat[
'npsfcand'][:, b]
757 addRefCatMetadata(formattedCat)
761 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
762 physicalFilterMap, tract=None):
763 """Output the zeropoints in fgcm_photoCalib format.
767 camera : `lsst.afw.cameraGeom.Camera`
768 Camera from the butler.
769 zptCat : `lsst.afw.table.BaseCatalog`
770 FGCM zeropoint catalog
from `FgcmFitCycleTask`.
771 visitCat : `lsst.afw.table.BaseCatalog`
772 FGCM visitCat
from `FgcmBuildStarsTask`.
773 offsets : `numpy.array`
774 Float array of absolute calibration offsets, one
for each filter.
775 bands : `list` [`str`]
776 List of band names
from FGCM output.
777 physicalFilterMap : `dict`
778 Dictionary of mappings
from physical filter to FGCM band.
779 tract: `int`, optional
780 Tract number to output. Default
is None (
global calibration)
784 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
785 Generator that returns (visit, exposureCatalog) tuples.
790 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
791 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
792 & (zptCat[
'fgcmZptVar'] > 0.0)
793 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE))
796 badVisits = np.unique(zptCat[
'visit'][~selected])
797 goodVisits = np.unique(zptCat[
'visit'][selected])
798 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
799 for allBadVisit
in allBadVisits:
800 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
804 for f
in physicalFilterMap:
806 if physicalFilterMap[f]
in bands:
807 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
811 for ccdIndex, detector
in enumerate(camera):
812 ccdMapping[detector.getId()] = ccdIndex
817 scalingMapping[rec[
'visit']] = rec[
'scaling']
819 if self.config.doComposeWcsJacobian:
824 zptVisitCatalog =
None
826 metadata = dafBase.PropertyList()
827 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
828 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
830 for rec
in zptCat[selected]:
832 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
839 postCalibrationOffset = offsetMapping[rec[
'filtername']]
840 if self.config.doApplyMeanChromaticCorrection:
841 postCalibrationOffset += rec[
'fgcmDeltaChrom']
843 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
844 rec[
'fgcmfZptChebXyMax'])
846 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
847 rec[
'fgcmfZptChebXyMax'],
848 offset=postCalibrationOffset,
851 if self.config.doComposeWcsJacobian:
853 fgcmField = afwMath.ProductBoundedField([approxPixelAreaFields[rec[
'detector']],
859 fgcmField = afwMath.ProductBoundedField([fgcmSuperStarField, fgcmZptField])
862 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
863 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
864 photoCalib = afwImage.PhotoCalib(calibrationMean=calibCenter,
865 calibrationErr=calibErr,
866 calibration=fgcmField,
870 if rec[
'visit'] != lastVisit:
875 zptVisitCatalog.sort()
876 yield (int(lastVisit), zptVisitCatalog)
879 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
880 zptExpCatSchema.addField(
'visit', type=
'L', doc=
'Visit number')
883 zptVisitCatalog = afwTable.ExposureCatalog(zptExpCatSchema)
884 zptVisitCatalog.setMetadata(metadata)
886 lastVisit = int(rec[
'visit'])
888 catRecord = zptVisitCatalog.addNew()
889 catRecord[
'id'] = int(rec[
'detector'])
890 catRecord[
'visit'] = rec[
'visit']
891 catRecord.setPhotoCalib(photoCalib)
895 zptVisitCatalog.sort()
896 yield (int(lastVisit), zptVisitCatalog)
898 def _getChebyshevBoundedField(self, coefficients, xyMax, offset=0.0, scaling=1.0):
900 Make a ChebyshevBoundedField from fgcm coefficients,
with optional offset
905 coefficients: `numpy.array`
906 Flattened array of chebyshev coefficients
907 xyMax: `list` of length 2
908 Maximum x
and y of the chebyshev bounding box
909 offset: `float`, optional
910 Absolute calibration offset. Default
is 0.0
911 scaling: `float`, optional
912 Flat scaling value
from fgcmBuildStars. Default
is 1.0
916 boundedField: `lsst.afw.math.ChebyshevBoundedField`
919 orderPlus1 = int(np.sqrt(coefficients.size))
920 pars = np.zeros((orderPlus1, orderPlus1))
922 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0.0, 0.0),
923 lsst.geom.Point2I(*xyMax))
925 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
926 * (10.**(offset/-2.5))*scaling)
928 boundedField = afwMath.ChebyshevBoundedField(bbox, pars)
932 def _outputAtmospheres(self, handleDict, atmCat):
934 Output the atmospheres.
939 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
940 The handleDict has the follownig keys:
942 ``"fgcmLookUpTable"``
943 handle
for the FGCM look-up table.
944 atmCat : `lsst.afw.table.BaseCatalog`
945 FGCM atmosphere parameter catalog
from fgcmFitCycleTask.
949 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
950 Generator that returns (visit, transmissionCurve) tuples.
953 lutCat = handleDict[
'fgcmLookUpTable'].get()
955 atmosphereTableName = lutCat[0][
'tablename']
956 elevation = lutCat[0][
'elevation']
957 atmLambda = lutCat[0][
'atmLambda']
962 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
970 modGen = fgcm.ModtranGenerator(elevation)
971 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
972 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
973 except (ValueError, IOError)
as e:
974 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
975 "but modtran not configured to run.")
from e
977 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
979 for i, visit
in enumerate(atmCat[
'visit']):
980 if atmTable
is not None:
982 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
983 pwv=atmCat[i][
'pwv'],
985 tau=atmCat[i][
'tau'],
986 alpha=atmCat[i][
'alpha'],
988 ctranslamstd=[atmCat[i][
'cTrans'],
989 atmCat[i][
'lamStd']])
992 modAtm = modGen(pmb=atmCat[i][
'pmb'],
993 pwv=atmCat[i][
'pwv'],
995 tau=atmCat[i][
'tau'],
996 alpha=atmCat[i][
'alpha'],
998 lambdaRange=lambdaRange,
999 lambdaStep=lambdaStep,
1000 ctranslamstd=[atmCat[i][
'cTrans'],
1001 atmCat[i][
'lamStd']])
1002 atmVals = modAtm[
'COMBINED']
1005 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
1006 wavelengths=atmLambda,
1007 throughputAtMin=atmVals[0],
1008 throughputAtMax=atmVals[-1])
1010 yield (int(visit), curve)
def computeApproxPixelAreaFields(camera)