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),
320 name=self.config.connections.refCat,
324 self.refObjLoader =
None
326 struct = self.run(handleDict, self.config.physicalFilterMap)
329 if struct.photoCalibCatalogs
is not None:
330 self.log.info(
"Outputting photoCalib catalogs.")
331 for visit, expCatalog
in struct.photoCalibCatalogs:
332 butlerQC.put(expCatalog, photoCalibRefDict[visit])
333 self.log.info(
"Done outputting photoCalib catalogs.")
336 if struct.atmospheres
is not None:
337 self.log.info(
"Outputting atmosphere transmission files.")
338 for visit, atm
in struct.atmospheres:
339 butlerQC.put(atm, atmRefDict[visit])
340 self.log.info(
"Done outputting atmosphere files.")
342 if self.config.doReferenceCalibration:
344 schema = afwTable.Schema()
345 schema.addField(
'offset', type=np.float64,
346 doc=
"Post-process calibration offset (mag)")
347 offsetCat = afwTable.BaseCatalog(schema)
348 offsetCat.resize(len(struct.offsets))
349 offsetCat[
'offset'][:] = struct.offsets
351 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
355 def run(self, handleDict, physicalFilterMap):
356 """Run the output products task.
361 All handles are `lsst.daf.butler.DeferredDatasetHandle`
362 handle dictionary with keys:
365 Camera object (`lsst.afw.cameraGeom.Camera`)
366 ``
"fgcmLookUpTable"``
367 handle
for the FGCM look-up table.
368 ``
"fgcmVisitCatalog"``
369 handle
for visit summary catalog.
370 ``
"fgcmStandardStars"``
371 handle
for the output standard star catalog.
373 handle
for the zeropoint data catalog.
374 ``
"fgcmAtmosphereParameters"``
375 handle
for the atmosphere parameter catalog.
376 ``
"fgcmBuildStarsTableConfig"``
377 Config
for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
378 physicalFilterMap : `dict`
379 Dictionary of mappings
from physical filter to FGCM band.
383 retStruct : `lsst.pipe.base.Struct`
384 Output structure
with keys:
386 offsets : `np.ndarray`
387 Final reference offsets, per band.
388 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
389 Generator that returns (visit, transmissionCurve) tuples.
390 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
391 Generator that returns (visit, exposureCatalog) tuples.
393 stdCat = handleDict['fgcmStandardStars'].get()
394 md = stdCat.getMetadata()
395 bands = md.getArray(
'BANDS')
397 if self.config.doReferenceCalibration:
398 lutCat = handleDict[
'fgcmLookUpTable'].get()
399 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
401 offsets = np.zeros(len(bands))
405 if self.config.doZeropointOutput:
406 zptCat = handleDict[
'fgcmZeropoints'].get()
407 visitCat = handleDict[
'fgcmVisitCatalog'].get()
409 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
414 if self.config.doAtmosphereOutput:
415 atmCat = handleDict[
'fgcmAtmosphereParameters'].get()
416 atmgen = self._outputAtmospheres(handleDict, atmCat)
420 retStruct = pipeBase.Struct(offsets=offsets,
422 retStruct.photoCalibCatalogs = pcgen
426 def generateTractOutputProducts(self, handleDict, tract,
427 visitCat, zptCat, atmCat, stdCat,
428 fgcmBuildStarsConfig):
430 Generate the output products for a given tract,
as specified
in the config.
432 This method
is here to have an alternate entry-point
for
438 All handles are `lsst.daf.butler.DeferredDatasetHandle`
439 handle dictionary
with keys:
442 Camera object (`lsst.afw.cameraGeom.Camera`)
443 ``
"fgcmLookUpTable"``
444 handle
for the FGCM look-up table.
447 visitCat : `lsst.afw.table.BaseCatalog`
448 FGCM visitCat
from `FgcmBuildStarsTask`
449 zptCat : `lsst.afw.table.BaseCatalog`
450 FGCM zeropoint catalog
from `FgcmFitCycleTask`
451 atmCat : `lsst.afw.table.BaseCatalog`
452 FGCM atmosphere parameter catalog
from `FgcmFitCycleTask`
453 stdCat : `lsst.afw.table.SimpleCatalog`
454 FGCM standard star catalog
from `FgcmFitCycleTask`
455 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
456 Configuration object
from `FgcmBuildStarsTask`
460 retStruct : `lsst.pipe.base.Struct`
461 Output structure
with keys:
463 offsets : `np.ndarray`
464 Final reference offsets, per band.
465 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
466 Generator that returns (visit, transmissionCurve) tuples.
467 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
468 Generator that returns (visit, exposureCatalog) tuples.
470 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
472 md = stdCat.getMetadata()
473 bands = md.getArray('BANDS')
475 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
476 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
477 "in fgcmBuildStarsTask.")
479 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
480 self.log.warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
482 if self.config.doReferenceCalibration:
483 lutCat = handleDict[
'fgcmLookUpTable'].get()
484 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
486 offsets = np.zeros(len(bands))
488 if self.config.doZeropointOutput:
489 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
494 if self.config.doAtmosphereOutput:
495 atmgen = self._outputAtmospheres(handleDict, atmCat)
499 retStruct = pipeBase.Struct(offsets=offsets,
501 retStruct.photoCalibCatalogs = pcgen
505 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
507 Compute offsets relative to a reference catalog.
509 This method splits the star catalog into healpix pixels
510 and computes the calibration transfer
for a sample of
511 these pixels to approximate the
'absolute' calibration
512 values (on
for each band) to apply to transfer the
517 stdCat : `lsst.afw.table.SimpleCatalog`
519 lutCat : `lsst.afw.table.SimpleCatalog`
521 physicalFilterMap : `dict`
522 Dictionary of mappings
from physical filter to FGCM band.
523 bands : `list` [`str`]
524 List of band names
from FGCM output
527 offsets : `numpy.array` of floats
528 Per band zeropoint offsets
534 minObs = stdCat[
'ngood'].min(axis=1)
536 goodStars = (minObs >= 1)
537 stdCat = stdCat[goodStars]
539 self.log.info(
"Found %d stars with at least 1 good observation in each band" %
546 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
547 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
548 physicalFilterMapBands = list(physicalFilterMap.values())
549 physicalFilterMapFilters = list(physicalFilterMap.keys())
553 physicalFilterMapIndex = physicalFilterMapBands.index(band)
554 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
556 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
557 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
558 filterLabels.append(afwImage.FilterLabel(band=band,
559 physical=stdPhysicalFilter))
568 sourceMapper = afwTable.SchemaMapper(stdCat.schema)
569 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
570 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
571 doc=
"instrumental flux (counts)")
572 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
573 doc=
"instrumental flux error (counts)")
574 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
582 ipring = hpg.angle_to_pixel(
583 self.config.referencePixelizationNside,
588 h, rev = esutil.stat.histogram(ipring, rev=
True)
590 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
592 self.log.info(
"Found %d pixels (nside=%d) with at least %d good stars" %
594 self.config.referencePixelizationNside,
595 self.config.referencePixelizationMinStars))
597 if gdpix.size < self.config.referencePixelizationNPixels:
598 self.log.warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
599 (gdpix.size, self.config.referencePixelizationNPixels))
602 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
604 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
605 (
'nstar',
'i4', len(bands)),
606 (
'nmatch',
'i4', len(bands)),
607 (
'zp',
'f4', len(bands)),
608 (
'zpErr',
'f4', len(bands))])
609 results[
'hpix'] = ipring[rev[rev[gdpix]]]
612 selected = np.zeros(len(stdCat), dtype=bool)
614 refFluxFields = [
None]*len(bands)
616 for p_index, pix
in enumerate(gdpix):
617 i1a = rev[rev[pix]: rev[pix + 1]]
625 for b_index, filterLabel
in enumerate(filterLabels):
626 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
628 selected, refFluxFields)
629 results[
'nstar'][p_index, b_index] = len(i1a)
630 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
631 results[
'zp'][p_index, b_index] = struct.zp
632 results[
'zpErr'][p_index, b_index] = struct.sigma
635 offsets = np.zeros(len(bands))
637 for b_index, band
in enumerate(bands):
639 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
640 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
643 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
644 self.log.info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
645 band, offsets[b_index], madSigma)
649 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
650 b_index, filterLabel, stdCat, selected, refFluxFields):
652 Compute the zeropoint offset between the fgcm stdCat and the reference
653 stars
for one pixel
in one band
657 sourceMapper : `lsst.afw.table.SchemaMapper`
658 Mapper to go
from stdCat to calibratable catalog
659 badStarKey : `lsst.afw.table.Key`
660 Key
for the field
with bad stars
662 Index of the band
in the star catalog
663 filterLabel : `lsst.afw.image.FilterLabel`
664 filterLabel
with band
and physical filter
665 stdCat : `lsst.afw.table.SimpleCatalog`
667 selected : `numpy.array(dtype=bool)`
668 Boolean array of which stars are
in the pixel
669 refFluxFields : `list`
670 List of names of flux fields
for reference catalog
673 sourceCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
674 sourceCat.reserve(selected.sum())
675 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
676 sourceCat['instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
677 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
678 * sourceCat[
'instFlux'])
682 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
683 for rec
in sourceCat[badStar]:
684 rec.set(badStarKey,
True)
686 exposure = afwImage.ExposureF()
687 exposure.setFilter(filterLabel)
689 if refFluxFields[b_index]
is None:
692 ctr = stdCat[0].getCoord()
693 rad = 0.05*lsst.geom.degrees
694 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
695 refFluxFields[b_index] = refDataTest.fluxField
698 calConfig = copy.copy(self.config.photoCal.value)
699 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
700 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
701 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
703 schema=sourceCat.getSchema())
705 struct = calTask.run(exposure, sourceCat)
709 def _formatCatalog(self, fgcmStarCat, offsets, bands):
711 Turn an FGCM-formatted star catalog, applying zeropoint offsets.
715 fgcmStarCat : `lsst.afw.Table.SimpleCatalog`
716 SimpleCatalog as output by fgcmcal
717 offsets : `list`
with len(self.bands) entries
718 Zeropoint offsets to apply
719 bands : `list` [`str`]
720 List of band names
from FGCM output
724 formattedCat: `lsst.afw.table.SimpleCatalog`
725 SimpleCatalog suitable
for using
as a reference catalog
728 sourceMapper = afwTable.SchemaMapper(fgcmStarCat.schema)
729 minSchema = LoadIndexedReferenceObjectsTask.makeMinimalSchema(bands,
733 sourceMapper.addMinimalSchema(minSchema)
735 sourceMapper.editOutputSchema().addField(
'%s_nGood' % (band), type=np.int32)
736 sourceMapper.editOutputSchema().addField(
'%s_nTotal' % (band), type=np.int32)
737 sourceMapper.editOutputSchema().addField(
'%s_nPsfCandidate' % (band), type=np.int32)
739 formattedCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
740 formattedCat.reserve(len(fgcmStarCat))
741 formattedCat.extend(fgcmStarCat, mapper=sourceMapper)
745 for b, band
in enumerate(bands):
746 mag = fgcmStarCat[
'mag_std_noabs'][:, b].astype(np.float64) + offsets[b]
749 flux = (mag*units.ABmag).to_value(units.nJy)
750 fluxErr = (np.log(10.)/2.5)*flux*fgcmStarCat[
'magErr_std'][:, b].astype(np.float64)
752 formattedCat[
'%s_flux' % (band)][:] = flux
753 formattedCat[
'%s_fluxErr' % (band)][:] = fluxErr
754 formattedCat[
'%s_nGood' % (band)][:] = fgcmStarCat[
'ngood'][:, b]
755 formattedCat[
'%s_nTotal' % (band)][:] = fgcmStarCat[
'ntotal'][:, b]
756 formattedCat[
'%s_nPsfCandidate' % (band)][:] = fgcmStarCat[
'npsfcand'][:, b]
758 addRefCatMetadata(formattedCat)
762 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
763 physicalFilterMap, tract=None):
764 """Output the zeropoints in fgcm_photoCalib format.
768 camera : `lsst.afw.cameraGeom.Camera`
769 Camera from the butler.
770 zptCat : `lsst.afw.table.BaseCatalog`
771 FGCM zeropoint catalog
from `FgcmFitCycleTask`.
772 visitCat : `lsst.afw.table.BaseCatalog`
773 FGCM visitCat
from `FgcmBuildStarsTask`.
774 offsets : `numpy.array`
775 Float array of absolute calibration offsets, one
for each filter.
776 bands : `list` [`str`]
777 List of band names
from FGCM output.
778 physicalFilterMap : `dict`
779 Dictionary of mappings
from physical filter to FGCM band.
780 tract: `int`, optional
781 Tract number to output. Default
is None (
global calibration)
785 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
786 Generator that returns (visit, exposureCatalog) tuples.
791 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
792 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
793 & (zptCat[
'fgcmZptVar'] > 0.0)
794 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE))
797 badVisits = np.unique(zptCat[
'visit'][~selected])
798 goodVisits = np.unique(zptCat[
'visit'][selected])
799 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
800 for allBadVisit
in allBadVisits:
801 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
805 for f
in physicalFilterMap:
807 if physicalFilterMap[f]
in bands:
808 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
812 for ccdIndex, detector
in enumerate(camera):
813 ccdMapping[detector.getId()] = ccdIndex
818 scalingMapping[rec[
'visit']] = rec[
'scaling']
820 if self.config.doComposeWcsJacobian:
821 approxPixelAreaFields = computeApproxPixelAreaFields(camera)
825 zptVisitCatalog =
None
827 metadata = dafBase.PropertyList()
828 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
829 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
831 for rec
in zptCat[selected]:
833 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
840 postCalibrationOffset = offsetMapping[rec[
'filtername']]
841 if self.config.doApplyMeanChromaticCorrection:
842 postCalibrationOffset += rec[
'fgcmDeltaChrom']
844 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
845 rec[
'fgcmfZptChebXyMax'])
847 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
848 rec[
'fgcmfZptChebXyMax'],
849 offset=postCalibrationOffset,
852 if self.config.doComposeWcsJacobian:
854 fgcmField = afwMath.ProductBoundedField([approxPixelAreaFields[rec[
'detector']],
860 fgcmField = afwMath.ProductBoundedField([fgcmSuperStarField, fgcmZptField])
863 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
864 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
865 photoCalib = afwImage.PhotoCalib(calibrationMean=calibCenter,
866 calibrationErr=calibErr,
867 calibration=fgcmField,
871 if rec[
'visit'] != lastVisit:
876 zptVisitCatalog.sort()
877 yield (int(lastVisit), zptVisitCatalog)
880 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
881 zptExpCatSchema.addField(
'visit', type=
'L', doc=
'Visit number')
884 zptVisitCatalog = afwTable.ExposureCatalog(zptExpCatSchema)
885 zptVisitCatalog.setMetadata(metadata)
887 lastVisit = int(rec[
'visit'])
889 catRecord = zptVisitCatalog.addNew()
890 catRecord[
'id'] = int(rec[
'detector'])
891 catRecord[
'visit'] = rec[
'visit']
892 catRecord.setPhotoCalib(photoCalib)
896 zptVisitCatalog.sort()
897 yield (int(lastVisit), zptVisitCatalog)
899 def _getChebyshevBoundedField(self, coefficients, xyMax, offset=0.0, scaling=1.0):
901 Make a ChebyshevBoundedField from fgcm coefficients,
with optional offset
906 coefficients: `numpy.array`
907 Flattened array of chebyshev coefficients
908 xyMax: `list` of length 2
909 Maximum x
and y of the chebyshev bounding box
910 offset: `float`, optional
911 Absolute calibration offset. Default
is 0.0
912 scaling: `float`, optional
913 Flat scaling value
from fgcmBuildStars. Default
is 1.0
917 boundedField: `lsst.afw.math.ChebyshevBoundedField`
920 orderPlus1 = int(np.sqrt(coefficients.size))
921 pars = np.zeros((orderPlus1, orderPlus1))
923 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0.0, 0.0),
924 lsst.geom.Point2I(*xyMax))
926 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
927 * (10.**(offset/-2.5))*scaling)
929 boundedField = afwMath.ChebyshevBoundedField(bbox, pars)
933 def _outputAtmospheres(self, handleDict, atmCat):
935 Output the atmospheres.
940 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
941 The handleDict has the follownig keys:
943 ``"fgcmLookUpTable"``
944 handle
for the FGCM look-up table.
945 atmCat : `lsst.afw.table.BaseCatalog`
946 FGCM atmosphere parameter catalog
from fgcmFitCycleTask.
950 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
951 Generator that returns (visit, transmissionCurve) tuples.
954 lutCat = handleDict[
'fgcmLookUpTable'].get()
956 atmosphereTableName = lutCat[0][
'tablename']
957 elevation = lutCat[0][
'elevation']
958 atmLambda = lutCat[0][
'atmLambda']
963 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
971 modGen = fgcm.ModtranGenerator(elevation)
972 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
973 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
974 except (ValueError, IOError)
as e:
975 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
976 "but modtran not configured to run.")
from e
978 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
980 for i, visit
in enumerate(atmCat[
'visit']):
981 if atmTable
is not None:
983 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
984 pwv=atmCat[i][
'pwv'],
986 tau=atmCat[i][
'tau'],
987 alpha=atmCat[i][
'alpha'],
989 ctranslamstd=[atmCat[i][
'cTrans'],
990 atmCat[i][
'lamStd']])
993 modAtm = modGen(pmb=atmCat[i][
'pmb'],
994 pwv=atmCat[i][
'pwv'],
996 tau=atmCat[i][
'tau'],
997 alpha=atmCat[i][
'alpha'],
999 lambdaRange=lambdaRange,
1000 lambdaStep=lambdaStep,
1001 ctranslamstd=[atmCat[i][
'cTrans'],
1002 atmCat[i][
'lamStd']])
1003 atmVals = modAtm[
'COMBINED']
1006 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
1007 wavelengths=atmLambda,
1008 throughputAtMin=atmVals[0],
1009 throughputAtMax=atmVals[-1])
1011 yield (int(visit), curve)