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",
179 physicalFilterMap = pexConfig.DictField(
180 doc=
"Mapping from 'physicalFilter' to band.",
187 doReferenceCalibration = pexConfig.Field(
188 doc=(
"Transfer 'absolute' calibration from reference catalog? "
189 "This afterburner step is unnecessary if reference stars "
190 "were used in the full fit in FgcmFitCycleTask."),
194 doRefcatOutput = pexConfig.Field(
195 doc=
"Output standard stars in reference catalog format",
198 deprecated=
"doRefcatOutput is no longer supported; this config will be removed after v24"
200 doAtmosphereOutput = pexConfig.Field(
201 doc=
"Output atmospheres in transmission_atmosphere_fgcm format",
205 doZeropointOutput = pexConfig.Field(
206 doc=
"Output zeropoints in fgcm_photoCalib format",
210 doComposeWcsJacobian = pexConfig.Field(
211 doc=
"Compose Jacobian of WCS with fgcm calibration for output photoCalib?",
215 doApplyMeanChromaticCorrection = pexConfig.Field(
216 doc=
"Apply the mean chromatic correction to the zeropoints?",
220 refObjLoader = pexConfig.ConfigurableField(
221 target=LoadIndexedReferenceObjectsTask,
222 doc=
"reference object loader for 'absolute' photometric calibration",
223 deprecated=
"refObjLoader is deprecated, and will be removed after v24",
225 photoCal = pexConfig.ConfigurableField(
227 doc=
"task to perform 'absolute' calibration",
229 referencePixelizationNside = pexConfig.Field(
230 doc=
"Healpix nside to pixelize catalog to compare to reference catalog",
234 referencePixelizationMinStars = pexConfig.Field(
235 doc=(
"Minimum number of stars per healpix pixel to select for comparison"
236 "to the specified reference catalog"),
240 referenceMinMatch = pexConfig.Field(
241 doc=
"Minimum number of stars matched to reference catalog to be used in statistics",
245 referencePixelizationNPixels = pexConfig.Field(
246 doc=(
"Number of healpix pixels to sample to do comparison. "
247 "Doing too many will take a long time and not yield any more "
248 "precise results because the final number is the median offset "
249 "(per band) from the set of pixels."),
253 datasetConfig = pexConfig.ConfigField(
255 doc=
"Configuration for writing/reading ingested catalog",
256 deprecated=
"The datasetConfig was only used for gen2; this config will be removed after v24.",
259 def setDefaults(self):
260 pexConfig.Config.setDefaults(self)
270 self.photoCal.applyColorTerms =
False
271 self.photoCal.fluxField =
'instFlux'
272 self.photoCal.magErrFloor = 0.003
273 self.photoCal.match.referenceSelection.doSignalToNoise =
True
274 self.photoCal.match.referenceSelection.signalToNoise.minimum = 10.0
275 self.photoCal.match.sourceSelection.doSignalToNoise =
True
276 self.photoCal.match.sourceSelection.signalToNoise.minimum = 10.0
277 self.photoCal.match.sourceSelection.signalToNoise.fluxField =
'instFlux'
278 self.photoCal.match.sourceSelection.signalToNoise.errField =
'instFluxErr'
279 self.photoCal.match.sourceSelection.doFlags =
True
280 self.photoCal.match.sourceSelection.flags.good = []
281 self.photoCal.match.sourceSelection.flags.bad = [
'flag_badStar']
282 self.photoCal.match.sourceSelection.doUnresolved =
False
288 self.connections.cycleNumber =
str(self.cycleNumber)
291class FgcmOutputProductsTask(pipeBase.PipelineTask):
293 Output products from FGCM
global calibration.
296 ConfigClass = FgcmOutputProductsConfig
297 _DefaultName = "fgcmOutputProducts"
299 def __init__(self, **kwargs):
300 super().__init__(**kwargs)
302 def runQuantum(self, butlerQC, inputRefs, outputRefs):
304 handleDict[
'camera'] = butlerQC.get(inputRefs.camera)
305 handleDict[
'fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
306 handleDict[
'fgcmVisitCatalog'] = butlerQC.get(inputRefs.fgcmVisitCatalog)
307 handleDict[
'fgcmStandardStars'] = butlerQC.get(inputRefs.fgcmStandardStars)
309 if self.config.doZeropointOutput:
310 handleDict[
'fgcmZeropoints'] = butlerQC.get(inputRefs.fgcmZeropoints)
311 photoCalibRefDict = {photoCalibRef.dataId.byName()[
'visit']:
312 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
314 if self.config.doAtmosphereOutput:
315 handleDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
316 atmRefDict = {atmRef.dataId.byName()[
'visit']: atmRef
for
317 atmRef
in outputRefs.fgcmTransmissionAtmosphere}
319 if self.config.doReferenceCalibration:
320 refConfig = LoadReferenceObjectsConfig()
321 self.refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
322 for ref
in inputRefs.refCat],
323 refCats=butlerQC.get(inputRefs.refCat),
327 self.refObjLoader =
None
329 struct = self.run(handleDict, self.config.physicalFilterMap)
332 if struct.photoCalibCatalogs
is not None:
333 self.log.info(
"Outputting photoCalib catalogs.")
334 for visit, expCatalog
in struct.photoCalibCatalogs:
335 butlerQC.put(expCatalog, photoCalibRefDict[visit])
336 self.log.info(
"Done outputting photoCalib catalogs.")
339 if struct.atmospheres
is not None:
340 self.log.info(
"Outputting atmosphere transmission files.")
341 for visit, atm
in struct.atmospheres:
342 butlerQC.put(atm, atmRefDict[visit])
343 self.log.info(
"Done outputting atmosphere files.")
345 if self.config.doReferenceCalibration:
347 schema = afwTable.Schema()
348 schema.addField(
'offset', type=np.float64,
349 doc=
"Post-process calibration offset (mag)")
350 offsetCat = afwTable.BaseCatalog(schema)
351 offsetCat.resize(len(struct.offsets))
352 offsetCat[
'offset'][:] = struct.offsets
354 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
358 def run(self, handleDict, physicalFilterMap):
359 """Run the output products task.
364 All handles are `lsst.daf.butler.DeferredDatasetHandle`
365 handle dictionary with keys:
368 Camera object (`lsst.afw.cameraGeom.Camera`)
369 ``
"fgcmLookUpTable"``
370 handle
for the FGCM look-up table.
371 ``
"fgcmVisitCatalog"``
372 handle
for visit summary catalog.
373 ``
"fgcmStandardStars"``
374 handle
for the output standard star catalog.
376 handle
for the zeropoint data catalog.
377 ``
"fgcmAtmosphereParameters"``
378 handle
for the atmosphere parameter catalog.
379 ``
"fgcmBuildStarsTableConfig"``
380 Config
for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
381 physicalFilterMap : `dict`
382 Dictionary of mappings
from physical filter to FGCM band.
386 retStruct : `lsst.pipe.base.Struct`
387 Output structure
with keys:
389 offsets : `np.ndarray`
390 Final reference offsets, per band.
391 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
392 Generator that returns (visit, transmissionCurve) tuples.
393 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
394 Generator that returns (visit, exposureCatalog) tuples.
396 stdCat = handleDict['fgcmStandardStars'].get()
397 md = stdCat.getMetadata()
398 bands = md.getArray(
'BANDS')
400 if self.config.doReferenceCalibration:
401 lutCat = handleDict[
'fgcmLookUpTable'].get()
402 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
404 offsets = np.zeros(len(bands))
408 if self.config.doZeropointOutput:
409 zptCat = handleDict[
'fgcmZeropoints'].get()
410 visitCat = handleDict[
'fgcmVisitCatalog'].get()
412 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
417 if self.config.doAtmosphereOutput:
418 atmCat = handleDict[
'fgcmAtmosphereParameters'].get()
419 atmgen = self._outputAtmospheres(handleDict, atmCat)
423 retStruct = pipeBase.Struct(offsets=offsets,
425 retStruct.photoCalibCatalogs = pcgen
429 def generateTractOutputProducts(self, handleDict, tract,
430 visitCat, zptCat, atmCat, stdCat,
431 fgcmBuildStarsConfig):
433 Generate the output products for a given tract,
as specified
in the config.
435 This method
is here to have an alternate entry-point
for
441 All handles are `lsst.daf.butler.DeferredDatasetHandle`
442 handle dictionary
with keys:
445 Camera object (`lsst.afw.cameraGeom.Camera`)
446 ``
"fgcmLookUpTable"``
447 handle
for the FGCM look-up table.
450 visitCat : `lsst.afw.table.BaseCatalog`
451 FGCM visitCat
from `FgcmBuildStarsTask`
452 zptCat : `lsst.afw.table.BaseCatalog`
453 FGCM zeropoint catalog
from `FgcmFitCycleTask`
454 atmCat : `lsst.afw.table.BaseCatalog`
455 FGCM atmosphere parameter catalog
from `FgcmFitCycleTask`
456 stdCat : `lsst.afw.table.SimpleCatalog`
457 FGCM standard star catalog
from `FgcmFitCycleTask`
458 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
459 Configuration object
from `FgcmBuildStarsTask`
463 retStruct : `lsst.pipe.base.Struct`
464 Output structure
with keys:
466 offsets : `np.ndarray`
467 Final reference offsets, per band.
468 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
469 Generator that returns (visit, transmissionCurve) tuples.
470 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
471 Generator that returns (visit, exposureCatalog) tuples.
473 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
475 md = stdCat.getMetadata()
476 bands = md.getArray('BANDS')
478 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
479 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
480 "in fgcmBuildStarsTask.")
482 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
483 self.log.warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
485 if self.config.doReferenceCalibration:
486 lutCat = handleDict[
'fgcmLookUpTable'].get()
487 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
489 offsets = np.zeros(len(bands))
491 if self.config.doZeropointOutput:
492 pcgen = self._outputZeropoints(handleDict[
'camera'], zptCat, visitCat, offsets, bands,
497 if self.config.doAtmosphereOutput:
498 atmgen = self._outputAtmospheres(handleDict, atmCat)
502 retStruct = pipeBase.Struct(offsets=offsets,
504 retStruct.photoCalibCatalogs = pcgen
508 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
510 Compute offsets relative to a reference catalog.
512 This method splits the star catalog into healpix pixels
513 and computes the calibration transfer
for a sample of
514 these pixels to approximate the
'absolute' calibration
515 values (on
for each band) to apply to transfer the
520 stdCat : `lsst.afw.table.SimpleCatalog`
522 lutCat : `lsst.afw.table.SimpleCatalog`
524 physicalFilterMap : `dict`
525 Dictionary of mappings
from physical filter to FGCM band.
526 bands : `list` [`str`]
527 List of band names
from FGCM output
530 offsets : `numpy.array` of floats
531 Per band zeropoint offsets
537 minObs = stdCat[
'ngood'].min(axis=1)
539 goodStars = (minObs >= 1)
540 stdCat = stdCat[goodStars]
542 self.log.info(
"Found %d stars with at least 1 good observation in each band" %
549 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
550 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
551 physicalFilterMapBands = list(physicalFilterMap.values())
552 physicalFilterMapFilters = list(physicalFilterMap.keys())
556 physicalFilterMapIndex = physicalFilterMapBands.index(band)
557 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
559 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
560 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
561 filterLabels.append(afwImage.FilterLabel(band=band,
562 physical=stdPhysicalFilter))
571 sourceMapper = afwTable.SchemaMapper(stdCat.schema)
572 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
573 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
574 doc=
"instrumental flux (counts)")
575 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
576 doc=
"instrumental flux error (counts)")
577 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
585 theta = np.pi/2. - stdCat[
'coord_dec']
586 phi = stdCat[
'coord_ra']
588 ipring = hp.ang2pix(self.config.referencePixelizationNside, theta, phi)
589 h, rev = esutil.stat.histogram(ipring, rev=
True)
591 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
593 self.log.info(
"Found %d pixels (nside=%d) with at least %d good stars" %
595 self.config.referencePixelizationNside,
596 self.config.referencePixelizationMinStars))
598 if gdpix.size < self.config.referencePixelizationNPixels:
599 self.log.warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
600 (gdpix.size, self.config.referencePixelizationNPixels))
603 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
605 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
606 (
'nstar',
'i4', len(bands)),
607 (
'nmatch',
'i4', len(bands)),
608 (
'zp',
'f4', len(bands)),
609 (
'zpErr',
'f4', len(bands))])
610 results[
'hpix'] = ipring[rev[rev[gdpix]]]
613 selected = np.zeros(len(stdCat), dtype=bool)
615 refFluxFields = [
None]*len(bands)
617 for p_index, pix
in enumerate(gdpix):
618 i1a = rev[rev[pix]: rev[pix + 1]]
626 for b_index, filterLabel
in enumerate(filterLabels):
627 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
629 selected, refFluxFields)
630 results[
'nstar'][p_index, b_index] = len(i1a)
631 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
632 results[
'zp'][p_index, b_index] = struct.zp
633 results[
'zpErr'][p_index, b_index] = struct.sigma
636 offsets = np.zeros(len(bands))
638 for b_index, band
in enumerate(bands):
640 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
641 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
644 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
645 self.log.info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
646 band, offsets[b_index], madSigma)
650 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
651 b_index, filterLabel, stdCat, selected, refFluxFields):
653 Compute the zeropoint offset between the fgcm stdCat and the reference
654 stars
for one pixel
in one band
658 sourceMapper : `lsst.afw.table.SchemaMapper`
659 Mapper to go
from stdCat to calibratable catalog
660 badStarKey : `lsst.afw.table.Key`
661 Key
for the field
with bad stars
663 Index of the band
in the star catalog
664 filterLabel : `lsst.afw.image.FilterLabel`
665 filterLabel
with band
and physical filter
666 stdCat : `lsst.afw.table.SimpleCatalog`
668 selected : `numpy.array(dtype=bool)`
669 Boolean array of which stars are
in the pixel
670 refFluxFields : `list`
671 List of names of flux fields
for reference catalog
674 sourceCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
675 sourceCat.reserve(selected.sum())
676 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
677 sourceCat['instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
678 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
679 * sourceCat[
'instFlux'])
683 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
684 for rec
in sourceCat[badStar]:
685 rec.set(badStarKey,
True)
687 exposure = afwImage.ExposureF()
688 exposure.setFilterLabel(filterLabel)
690 if refFluxFields[b_index]
is None:
693 ctr = stdCat[0].getCoord()
694 rad = 0.05*lsst.geom.degrees
695 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
696 refFluxFields[b_index] = refDataTest.fluxField
699 calConfig = copy.copy(self.config.photoCal.value)
700 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
701 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
702 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
704 schema=sourceCat.getSchema())
706 struct = calTask.run(exposure, sourceCat)
710 def _formatCatalog(self, fgcmStarCat, offsets, bands):
712 Turn an FGCM-formatted star catalog, applying zeropoint offsets.
716 fgcmStarCat : `lsst.afw.Table.SimpleCatalog`
717 SimpleCatalog as output by fgcmcal
718 offsets : `list`
with len(self.bands) entries
719 Zeropoint offsets to apply
720 bands : `list` [`str`]
721 List of band names
from FGCM output
725 formattedCat: `lsst.afw.table.SimpleCatalog`
726 SimpleCatalog suitable
for using
as a reference catalog
729 sourceMapper = afwTable.SchemaMapper(fgcmStarCat.schema)
730 minSchema = LoadIndexedReferenceObjectsTask.makeMinimalSchema(bands,
734 sourceMapper.addMinimalSchema(minSchema)
736 sourceMapper.editOutputSchema().addField(
'%s_nGood' % (band), type=np.int32)
737 sourceMapper.editOutputSchema().addField(
'%s_nTotal' % (band), type=np.int32)
738 sourceMapper.editOutputSchema().addField(
'%s_nPsfCandidate' % (band), type=np.int32)
740 formattedCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
741 formattedCat.reserve(len(fgcmStarCat))
742 formattedCat.extend(fgcmStarCat, mapper=sourceMapper)
746 for b, band
in enumerate(bands):
747 mag = fgcmStarCat[
'mag_std_noabs'][:, b].astype(np.float64) + offsets[b]
750 flux = (mag*units.ABmag).to_value(units.nJy)
751 fluxErr = (np.log(10.)/2.5)*flux*fgcmStarCat[
'magErr_std'][:, b].astype(np.float64)
753 formattedCat[
'%s_flux' % (band)][:] = flux
754 formattedCat[
'%s_fluxErr' % (band)][:] = fluxErr
755 formattedCat[
'%s_nGood' % (band)][:] = fgcmStarCat[
'ngood'][:, b]
756 formattedCat[
'%s_nTotal' % (band)][:] = fgcmStarCat[
'ntotal'][:, b]
757 formattedCat[
'%s_nPsfCandidate' % (band)][:] = fgcmStarCat[
'npsfcand'][:, b]
759 addRefCatMetadata(formattedCat)
763 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
764 physicalFilterMap, tract=None):
765 """Output the zeropoints in fgcm_photoCalib format.
769 camera : `lsst.afw.cameraGeom.Camera`
770 Camera from the butler.
771 zptCat : `lsst.afw.table.BaseCatalog`
772 FGCM zeropoint catalog
from `FgcmFitCycleTask`.
773 visitCat : `lsst.afw.table.BaseCatalog`
774 FGCM visitCat
from `FgcmBuildStarsTask`.
775 offsets : `numpy.array`
776 Float array of absolute calibration offsets, one
for each filter.
777 bands : `list` [`str`]
778 List of band names
from FGCM output.
779 physicalFilterMap : `dict`
780 Dictionary of mappings
from physical filter to FGCM band.
781 tract: `int`, optional
782 Tract number to output. Default
is None (
global calibration)
786 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
787 Generator that returns (visit, exposureCatalog) tuples.
792 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
793 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
794 & (zptCat[
'fgcmZptVar'] > 0.0)
795 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE))
798 badVisits = np.unique(zptCat[
'visit'][~selected])
799 goodVisits = np.unique(zptCat[
'visit'][selected])
800 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
801 for allBadVisit
in allBadVisits:
802 self.log.warning(f
'No suitable photoCalib for visit {allBadVisit}')
806 for f
in physicalFilterMap:
808 if physicalFilterMap[f]
in bands:
809 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
813 for ccdIndex, detector
in enumerate(camera):
814 ccdMapping[detector.getId()] = ccdIndex
819 scalingMapping[rec[
'visit']] = rec[
'scaling']
821 if self.config.doComposeWcsJacobian:
826 zptVisitCatalog =
None
828 metadata = dafBase.PropertyList()
829 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
830 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
832 for rec
in zptCat[selected]:
834 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
841 postCalibrationOffset = offsetMapping[rec[
'filtername']]
842 if self.config.doApplyMeanChromaticCorrection:
843 postCalibrationOffset += rec[
'fgcmDeltaChrom']
845 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
846 rec[
'fgcmfZptChebXyMax'])
848 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
849 rec[
'fgcmfZptChebXyMax'],
850 offset=postCalibrationOffset,
853 if self.config.doComposeWcsJacobian:
855 fgcmField = afwMath.ProductBoundedField([approxPixelAreaFields[rec[
'detector']],
861 fgcmField = afwMath.ProductBoundedField([fgcmSuperStarField, fgcmZptField])
864 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
865 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
866 photoCalib = afwImage.PhotoCalib(calibrationMean=calibCenter,
867 calibrationErr=calibErr,
868 calibration=fgcmField,
872 if rec[
'visit'] != lastVisit:
877 zptVisitCatalog.sort()
878 yield (int(lastVisit), zptVisitCatalog)
881 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
882 zptExpCatSchema.addField(
'visit', type=
'I', doc=
'Visit number')
885 zptVisitCatalog = afwTable.ExposureCatalog(zptExpCatSchema)
886 zptVisitCatalog.setMetadata(metadata)
888 lastVisit = int(rec[
'visit'])
890 catRecord = zptVisitCatalog.addNew()
891 catRecord[
'id'] = int(rec[
'detector'])
892 catRecord[
'visit'] = rec[
'visit']
893 catRecord.setPhotoCalib(photoCalib)
897 zptVisitCatalog.sort()
898 yield (int(lastVisit), zptVisitCatalog)
900 def _getChebyshevBoundedField(self, coefficients, xyMax, offset=0.0, scaling=1.0):
902 Make a ChebyshevBoundedField from fgcm coefficients,
with optional offset
907 coefficients: `numpy.array`
908 Flattened array of chebyshev coefficients
909 xyMax: `list` of length 2
910 Maximum x
and y of the chebyshev bounding box
911 offset: `float`, optional
912 Absolute calibration offset. Default
is 0.0
913 scaling: `float`, optional
914 Flat scaling value
from fgcmBuildStars. Default
is 1.0
918 boundedField: `lsst.afw.math.ChebyshevBoundedField`
921 orderPlus1 = int(np.sqrt(coefficients.size))
922 pars = np.zeros((orderPlus1, orderPlus1))
924 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0.0, 0.0),
925 lsst.geom.Point2I(*xyMax))
927 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
928 * (10.**(offset/-2.5))*scaling)
930 boundedField = afwMath.ChebyshevBoundedField(bbox, pars)
934 def _outputAtmospheres(self, handleDict, atmCat):
936 Output the atmospheres.
941 All data handles are `lsst.daf.butler.DeferredDatasetHandle`
942 The handleDict has the follownig keys:
944 ``"fgcmLookUpTable"``
945 handle
for the FGCM look-up table.
946 atmCat : `lsst.afw.table.BaseCatalog`
947 FGCM atmosphere parameter catalog
from fgcmFitCycleTask.
951 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
952 Generator that returns (visit, transmissionCurve) tuples.
955 lutCat = handleDict[
'fgcmLookUpTable'].get()
957 atmosphereTableName = lutCat[0][
'tablename']
958 elevation = lutCat[0][
'elevation']
959 atmLambda = lutCat[0][
'atmLambda']
964 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
972 modGen = fgcm.ModtranGenerator(elevation)
973 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
974 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
975 except (ValueError, IOError)
as e:
976 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
977 "but modtran not configured to run.")
from e
979 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
981 for i, visit
in enumerate(atmCat[
'visit']):
982 if atmTable
is not None:
984 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
985 pwv=atmCat[i][
'pwv'],
987 tau=atmCat[i][
'tau'],
988 alpha=atmCat[i][
'alpha'],
990 ctranslamstd=[atmCat[i][
'cTrans'],
991 atmCat[i][
'lamStd']])
994 modAtm = modGen(pmb=atmCat[i][
'pmb'],
995 pwv=atmCat[i][
'pwv'],
997 tau=atmCat[i][
'tau'],
998 alpha=atmCat[i][
'alpha'],
1000 lambdaRange=lambdaRange,
1001 lambdaStep=lambdaStep,
1002 ctranslamstd=[atmCat[i][
'cTrans'],
1003 atmCat[i][
'lamStd']])
1004 atmVals = modAtm[
'COMBINED']
1007 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
1008 wavelengths=atmLambda,
1009 throughputAtMin=atmVals[0],
1010 throughputAtMax=atmVals[-1])
1012 yield (int(visit), curve)
def computeApproxPixelAreaFields(camera)