23 """Make the final fgcmcal output products.
25 This task takes the final output from fgcmFitCycle and produces the following
26 outputs for use in the DM stack: the FGCM standard stars in a reference
27 catalog format; the model atmospheres in "transmission_atmosphere_fgcm"
28 format; and the zeropoints in "fgcm_photoCalib" format. Optionally, the
29 task can transfer the 'absolute' calibration from a reference catalog
30 to put the fgcm standard stars in units of Jansky. This is accomplished
31 by matching stars in a sample of healpix pixels, and applying the median
41 from astropy
import units
59 from .utilities
import computeApproxPixelAreaFields
60 from .utilities
import lookupStaticCalibrations
61 from .utilities
import FGCM_ILLEGAL_VALUE
65 __all__ = [
'FgcmOutputProductsConfig',
'FgcmOutputProductsTask',
'FgcmOutputProductsRunner']
69 dimensions=(
"instrument",),
70 defaultTemplates={
"cycleNumber":
"0"}):
71 camera = connectionTypes.PrerequisiteInput(
72 doc=
"Camera instrument",
74 storageClass=
"Camera",
75 dimensions=(
"instrument",),
76 lookupFunction=lookupStaticCalibrations,
80 fgcmLookUpTable = connectionTypes.PrerequisiteInput(
81 doc=(
"Atmosphere + instrument look-up-table for FGCM throughput and "
82 "chromatic corrections."),
83 name=
"fgcmLookUpTable",
84 storageClass=
"Catalog",
85 dimensions=(
"instrument",),
89 fgcmVisitCatalog = connectionTypes.Input(
90 doc=
"Catalog of visit information for fgcm",
91 name=
"fgcmVisitCatalog",
92 storageClass=
"Catalog",
93 dimensions=(
"instrument",),
97 fgcmStandardStars = connectionTypes.Input(
98 doc=
"Catalog of standard star data from fgcm fit",
99 name=
"fgcmStandardStars{cycleNumber}",
100 storageClass=
"SimpleCatalog",
101 dimensions=(
"instrument",),
105 fgcmZeropoints = connectionTypes.Input(
106 doc=
"Catalog of zeropoints from fgcm fit",
107 name=
"fgcmZeropoints{cycleNumber}",
108 storageClass=
"Catalog",
109 dimensions=(
"instrument",),
113 fgcmAtmosphereParameters = connectionTypes.Input(
114 doc=
"Catalog of atmosphere parameters from fgcm fit",
115 name=
"fgcmAtmosphereParameters{cycleNumber}",
116 storageClass=
"Catalog",
117 dimensions=(
"instrument",),
121 refCat = connectionTypes.PrerequisiteInput(
122 doc=
"Reference catalog to use for photometric calibration",
124 storageClass=
"SimpleCatalog",
125 dimensions=(
"skypix",),
130 fgcmPhotoCalib = connectionTypes.Output(
131 doc=(
"Per-visit photometric calibrations derived from fgcm calibration. "
132 "These catalogs use detector id for the id and are sorted for "
133 "fast lookups of a detector."),
134 name=
"fgcmPhotoCalibCatalog",
135 storageClass=
"ExposureCatalog",
136 dimensions=(
"instrument",
"visit",),
140 fgcmTransmissionAtmosphere = connectionTypes.Output(
141 doc=
"Per-visit atmosphere transmission files produced from fgcm calibration",
142 name=
"transmission_atmosphere_fgcm",
143 storageClass=
"TransmissionCurve",
144 dimensions=(
"instrument",
149 fgcmOffsets = connectionTypes.Output(
150 doc=
"Per-band offsets computed from doReferenceCalibration",
151 name=
"fgcmReferenceCalibrationOffsets",
152 storageClass=
"Catalog",
153 dimensions=(
"instrument",),
157 def __init__(self, *, config=None):
158 super().__init__(config=config)
160 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
161 raise ValueError(
"cycleNumber must be of integer format")
162 if config.connections.refCat != config.refObjLoader.ref_dataset_name:
163 raise ValueError(
"connections.refCat must be the same as refObjLoader.ref_dataset_name")
165 if config.doRefcatOutput:
166 raise ValueError(
"FgcmOutputProductsTask (Gen3) does not support doRefcatOutput")
168 if not config.doReferenceCalibration:
169 self.prerequisiteInputs.remove(
"refCat")
170 if not config.doAtmosphereOutput:
171 self.inputs.remove(
"fgcmAtmosphereParameters")
172 if not config.doZeropointOutput:
173 self.inputs.remove(
"fgcmZeropoints")
174 if not config.doReferenceCalibration:
175 self.outputs.remove(
"fgcmOffsets")
178 class FgcmOutputProductsConfig(pipeBase.PipelineTaskConfig,
179 pipelineConnections=FgcmOutputProductsConnections):
180 """Config for FgcmOutputProductsTask"""
182 cycleNumber = pexConfig.Field(
183 doc=
"Final fit cycle from FGCM fit",
187 physicalFilterMap = pexConfig.DictField(
188 doc=
"Mapping from 'physicalFilter' to band.",
195 doReferenceCalibration = pexConfig.Field(
196 doc=(
"Transfer 'absolute' calibration from reference catalog? "
197 "This afterburner step is unnecessary if reference stars "
198 "were used in the full fit in FgcmFitCycleTask."),
202 doRefcatOutput = pexConfig.Field(
203 doc=
"Output standard stars in reference catalog format",
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 refObjLoader = pexConfig.ConfigurableField(
228 target=LoadIndexedReferenceObjectsTask,
229 doc=
"reference object loader for 'absolute' photometric calibration",
231 photoCal = pexConfig.ConfigurableField(
233 doc=
"task to perform 'absolute' calibration",
235 referencePixelizationNside = pexConfig.Field(
236 doc=
"Healpix nside to pixelize catalog to compare to reference catalog",
240 referencePixelizationMinStars = pexConfig.Field(
241 doc=(
"Minimum number of stars per healpix pixel to select for comparison"
242 "to the specified reference catalog"),
246 referenceMinMatch = pexConfig.Field(
247 doc=
"Minimum number of stars matched to reference catalog to be used in statistics",
251 referencePixelizationNPixels = pexConfig.Field(
252 doc=(
"Number of healpix pixels to sample to do comparison. "
253 "Doing too many will take a long time and not yield any more "
254 "precise results because the final number is the median offset "
255 "(per band) from the set of pixels."),
259 datasetConfig = pexConfig.ConfigField(
261 doc=
"Configuration for writing/reading ingested catalog",
265 pexConfig.Config.setDefaults(self)
275 self.photoCal.applyColorTerms =
False
276 self.photoCal.fluxField =
'instFlux'
277 self.photoCal.magErrFloor = 0.003
278 self.photoCal.match.referenceSelection.doSignalToNoise =
True
279 self.photoCal.match.referenceSelection.signalToNoise.minimum = 10.0
280 self.photoCal.match.sourceSelection.doSignalToNoise =
True
281 self.photoCal.match.sourceSelection.signalToNoise.minimum = 10.0
282 self.photoCal.match.sourceSelection.signalToNoise.fluxField =
'instFlux'
283 self.photoCal.match.sourceSelection.signalToNoise.errField =
'instFluxErr'
284 self.photoCal.match.sourceSelection.doFlags =
True
285 self.photoCal.match.sourceSelection.flags.good = []
286 self.photoCal.match.sourceSelection.flags.bad = [
'flag_badStar']
287 self.photoCal.match.sourceSelection.doUnresolved =
False
288 self.datasetConfig.ref_dataset_name =
'fgcm_stars'
289 self.datasetConfig.format_version = 1
295 self.connections.cycleNumber = str(self.cycleNumber)
298 class FgcmOutputProductsRunner(pipeBase.ButlerInitializedTaskRunner):
299 """Subclass of TaskRunner for fgcmOutputProductsTask
301 fgcmOutputProductsTask.run() takes one argument, the butler, and
302 does not run on any data in the repository.
303 This runner does not use any parallelization.
307 def getTargetList(parsedCmd):
309 Return a list with one element, the butler.
311 return [parsedCmd.butler]
313 def __call__(self, butler):
317 butler: `lsst.daf.persistence.Butler`
321 exitStatus: `list` with `pipeBase.Struct`
322 exitStatus (0: success; 1: failure)
323 if self.doReturnResults also
324 results (`np.array` with absolute zeropoint offsets)
326 task = self.TaskClass(butler=butler, config=self.config, log=self.log)
330 results = task.runDataRef(butler)
333 results = task.runDataRef(butler)
334 except Exception
as e:
336 task.log.fatal(
"Failed: %s" % e)
337 if not isinstance(e, pipeBase.TaskError):
338 traceback.print_exc(file=sys.stderr)
340 task.writeMetadata(butler)
342 if self.doReturnResults:
344 return [pipeBase.Struct(exitStatus=exitStatus,
347 return [pipeBase.Struct(exitStatus=exitStatus)]
349 def run(self, parsedCmd):
351 Run the task, with no multiprocessing
355 parsedCmd: `lsst.pipe.base.ArgumentParser` parsed command line
360 if self.precall(parsedCmd):
361 targetList = self.getTargetList(parsedCmd)
363 resultList = self(targetList[0])
368 class FgcmOutputProductsTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
370 Output products from FGCM global calibration.
373 ConfigClass = FgcmOutputProductsConfig
374 RunnerClass = FgcmOutputProductsRunner
375 _DefaultName =
"fgcmOutputProducts"
377 def __init__(self, butler=None, **kwargs):
378 super().__init__(**kwargs)
381 def _getMetadataName(self):
384 def runQuantum(self, butlerQC, inputRefs, outputRefs):
386 dataRefDict[
'camera'] = butlerQC.get(inputRefs.camera)
387 dataRefDict[
'fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
388 dataRefDict[
'fgcmVisitCatalog'] = butlerQC.get(inputRefs.fgcmVisitCatalog)
389 dataRefDict[
'fgcmStandardStars'] = butlerQC.get(inputRefs.fgcmStandardStars)
391 if self.config.doZeropointOutput:
392 dataRefDict[
'fgcmZeropoints'] = butlerQC.get(inputRefs.fgcmZeropoints)
393 photoCalibRefDict = {photoCalibRef.dataId.byName()[
'visit']:
394 photoCalibRef
for photoCalibRef
in outputRefs.fgcmPhotoCalib}
396 if self.config.doAtmosphereOutput:
397 dataRefDict[
'fgcmAtmosphereParameters'] = butlerQC.get(inputRefs.fgcmAtmosphereParameters)
398 atmRefDict = {atmRef.dataId.byName()[
'visit']: atmRef
for
399 atmRef
in outputRefs.fgcmTransmissionAtmosphere}
401 if self.config.doReferenceCalibration:
402 refConfig = self.config.refObjLoader
404 for ref
in inputRefs.refCat],
405 refCats=butlerQC.get(inputRefs.refCat),
409 self.refObjLoader =
None
411 struct = self.run(dataRefDict, self.config.physicalFilterMap, returnCatalogs=
True)
414 if struct.photoCalibCatalogs
is not None:
415 self.log.
info(
"Outputting photoCalib catalogs.")
416 for visit, expCatalog
in struct.photoCalibCatalogs:
417 butlerQC.put(expCatalog, photoCalibRefDict[visit])
418 self.log.
info(
"Done outputting photoCalib catalogs.")
421 if struct.atmospheres
is not None:
422 self.log.
info(
"Outputting atmosphere transmission files.")
423 for visit, atm
in struct.atmospheres:
424 butlerQC.put(atm, atmRefDict[visit])
425 self.log.
info(
"Done outputting atmosphere files.")
427 if self.config.doReferenceCalibration:
430 schema.addField(
'offset', type=np.float64,
431 doc=
"Post-process calibration offset (mag)")
433 offsetCat.resize(len(struct.offsets))
434 offsetCat[
'offset'][:] = struct.offsets
436 butlerQC.put(offsetCat, outputRefs.fgcmOffsets)
441 def runDataRef(self, butler):
443 Make FGCM output products for use in the stack
447 butler: `lsst.daf.persistence.Butler`
449 Final fit cycle number, override config.
453 offsets: `lsst.pipe.base.Struct`
454 A structure with array of zeropoint offsets
459 Raised if any one of the following is true:
461 - butler cannot find "fgcmBuildStars_config" or
462 "fgcmBuildStarsTable_config".
463 - butler cannot find "fgcmFitCycle_config".
464 - "fgcmFitCycle_config" does not refer to
465 `self.config.cycleNumber`.
466 - butler cannot find "fgcmAtmosphereParameters" and
467 `self.config.doAtmosphereOutput` is `True`.
468 - butler cannot find "fgcmStandardStars" and
469 `self.config.doReferenceCalibration` is `True` or
470 `self.config.doRefcatOutput` is `True`.
471 - butler cannot find "fgcmZeropoints" and
472 `self.config.doZeropointOutput` is `True`.
474 if self.config.doReferenceCalibration:
476 self.makeSubtask(
"refObjLoader", butler=butler)
480 if not butler.datasetExists(
'fgcmBuildStarsTable_config')
and \
481 not butler.datasetExists(
'fgcmBuildStars_config'):
482 raise RuntimeError(
"Cannot find fgcmBuildStarsTable_config or fgcmBuildStars_config, "
483 "which is prereq for fgcmOutputProducts")
485 if butler.datasetExists(
'fgcmBuildStarsTable_config'):
486 fgcmBuildStarsConfig = butler.get(
'fgcmBuildStarsTable_config')
488 fgcmBuildStarsConfig = butler.get(
'fgcmBuildStars_config')
489 visitDataRefName = fgcmBuildStarsConfig.visitDataRefName
490 ccdDataRefName = fgcmBuildStarsConfig.ccdDataRefName
491 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
493 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
494 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
495 "in fgcmBuildStarsTask.")
497 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
498 self.log.
warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
501 if (self.config.doAtmosphereOutput
502 and not butler.datasetExists(
'fgcmAtmosphereParameters', fgcmcycle=self.config.cycleNumber)):
503 raise RuntimeError(f
"Atmosphere parameters are missing for cycle {self.config.cycleNumber}.")
505 if not butler.datasetExists(
'fgcmStandardStars',
506 fgcmcycle=self.config.cycleNumber):
507 raise RuntimeError(
"Standard stars are missing for cycle %d." %
508 (self.config.cycleNumber))
510 if (self.config.doZeropointOutput
511 and (
not butler.datasetExists(
'fgcmZeropoints', fgcmcycle=self.config.cycleNumber))):
512 raise RuntimeError(
"Zeropoints are missing for cycle %d." %
513 (self.config.cycleNumber))
517 dataRefDict[
'camera'] = butler.get(
'camera')
518 dataRefDict[
'fgcmLookUpTable'] = butler.dataRef(
'fgcmLookUpTable')
519 dataRefDict[
'fgcmVisitCatalog'] = butler.dataRef(
'fgcmVisitCatalog')
520 dataRefDict[
'fgcmStandardStars'] = butler.dataRef(
'fgcmStandardStars',
521 fgcmcycle=self.config.cycleNumber)
523 if self.config.doZeropointOutput:
524 dataRefDict[
'fgcmZeropoints'] = butler.dataRef(
'fgcmZeropoints',
525 fgcmcycle=self.config.cycleNumber)
526 if self.config.doAtmosphereOutput:
527 dataRefDict[
'fgcmAtmosphereParameters'] = butler.dataRef(
'fgcmAtmosphereParameters',
528 fgcmcycle=self.config.cycleNumber)
530 struct = self.run(dataRefDict, physicalFilterMap, butler=butler, returnCatalogs=
False)
532 if struct.photoCalibs
is not None:
533 self.log.
info(
"Outputting photoCalib files.")
535 for visit, detector, physicalFilter, photoCalib
in struct.photoCalibs:
536 butler.put(photoCalib,
'fgcm_photoCalib',
537 dataId={visitDataRefName: visit,
538 ccdDataRefName: detector,
539 'filter': physicalFilter})
541 self.log.
info(
"Done outputting photoCalib files.")
543 if struct.atmospheres
is not None:
544 self.log.
info(
"Outputting atmosphere transmission files.")
545 for visit, atm
in struct.atmospheres:
546 butler.put(atm,
"transmission_atmosphere_fgcm",
547 dataId={visitDataRefName: visit})
548 self.log.
info(
"Done outputting atmosphere transmissions.")
550 return pipeBase.Struct(offsets=struct.offsets)
552 def run(self, dataRefDict, physicalFilterMap, returnCatalogs=True, butler=None):
553 """Run the output products task.
558 All dataRefs are `lsst.daf.persistence.ButlerDataRef` (gen2) or
559 `lsst.daf.butler.DeferredDatasetHandle` (gen3)
560 dataRef dictionary with keys:
563 Camera object (`lsst.afw.cameraGeom.Camera`)
564 ``"fgcmLookUpTable"``
565 dataRef for the FGCM look-up table.
566 ``"fgcmVisitCatalog"``
567 dataRef for visit summary catalog.
568 ``"fgcmStandardStars"``
569 dataRef for the output standard star catalog.
571 dataRef for the zeropoint data catalog.
572 ``"fgcmAtmosphereParameters"``
573 dataRef for the atmosphere parameter catalog.
574 ``"fgcmBuildStarsTableConfig"``
575 Config for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
576 physicalFilterMap : `dict`
577 Dictionary of mappings from physical filter to FGCM band.
578 returnCatalogs : `bool`, optional
579 Return photoCalibs as per-visit exposure catalogs.
580 butler : `lsst.daf.persistence.Butler`, optional
581 Gen2 butler used for reference star outputs
585 retStruct : `lsst.pipe.base.Struct`
586 Output structure with keys:
588 offsets : `np.ndarray`
589 Final reference offsets, per band.
590 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
591 Generator that returns (visit, transmissionCurve) tuples.
592 photoCalibs : `generator` [(`int`, `int`, `str`, `lsst.afw.image.PhotoCalib`)]
593 Generator that returns (visit, ccd, filtername, photoCalib) tuples.
594 (returned if returnCatalogs is False).
595 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
596 Generator that returns (visit, exposureCatalog) tuples.
597 (returned if returnCatalogs is True).
599 stdCat = dataRefDict[
'fgcmStandardStars'].get()
600 md = stdCat.getMetadata()
601 bands = md.getArray(
'BANDS')
603 if self.config.doReferenceCalibration:
604 lutCat = dataRefDict[
'fgcmLookUpTable'].get()
605 offsets = self._computeReferenceOffsets(stdCat, lutCat, physicalFilterMap, bands)
607 offsets = np.zeros(len(bands))
610 if self.config.doRefcatOutput
and butler
is not None:
611 self._outputStandardStars(butler, stdCat, offsets, bands, self.config.datasetConfig)
615 if self.config.doZeropointOutput:
616 zptCat = dataRefDict[
'fgcmZeropoints'].get()
617 visitCat = dataRefDict[
'fgcmVisitCatalog'].get()
619 pcgen = self._outputZeropoints(dataRefDict[
'camera'], zptCat, visitCat, offsets, bands,
620 physicalFilterMap, returnCatalogs=returnCatalogs)
624 if self.config.doAtmosphereOutput:
625 atmCat = dataRefDict[
'fgcmAtmosphereParameters'].get()
626 atmgen = self._outputAtmospheres(dataRefDict, atmCat)
630 retStruct = pipeBase.Struct(offsets=offsets,
633 retStruct.photoCalibCatalogs = pcgen
635 retStruct.photoCalibs = pcgen
639 def generateTractOutputProducts(self, dataRefDict, tract,
640 visitCat, zptCat, atmCat, stdCat,
641 fgcmBuildStarsConfig,
645 Generate the output products for a given tract, as specified in the config.
647 This method is here to have an alternate entry-point for
653 All dataRefs are `lsst.daf.persistence.ButlerDataRef` (gen2) or
654 `lsst.daf.butler.DeferredDatasetHandle` (gen3)
655 dataRef dictionary with keys:
658 Camera object (`lsst.afw.cameraGeom.Camera`)
659 ``"fgcmLookUpTable"``
660 dataRef for the FGCM look-up table.
663 visitCat : `lsst.afw.table.BaseCatalog`
664 FGCM visitCat from `FgcmBuildStarsTask`
665 zptCat : `lsst.afw.table.BaseCatalog`
666 FGCM zeropoint catalog from `FgcmFitCycleTask`
667 atmCat : `lsst.afw.table.BaseCatalog`
668 FGCM atmosphere parameter catalog from `FgcmFitCycleTask`
669 stdCat : `lsst.afw.table.SimpleCatalog`
670 FGCM standard star catalog from `FgcmFitCycleTask`
671 fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
672 Configuration object from `FgcmBuildStarsTask`
673 returnCatalogs : `bool`, optional
674 Return photoCalibs as per-visit exposure catalogs.
675 butler: `lsst.daf.persistence.Butler`, optional
676 Gen2 butler used for reference star outputs
680 retStruct : `lsst.pipe.base.Struct`
681 Output structure with keys:
683 offsets : `np.ndarray`
684 Final reference offsets, per band.
685 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
686 Generator that returns (visit, transmissionCurve) tuples.
687 photoCalibs : `generator` [(`int`, `int`, `str`, `lsst.afw.image.PhotoCalib`)]
688 Generator that returns (visit, ccd, filtername, photoCalib) tuples.
689 (returned if returnCatalogs is False).
690 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
691 Generator that returns (visit, exposureCatalog) tuples.
692 (returned if returnCatalogs is True).
694 physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap
696 md = stdCat.getMetadata()
697 bands = md.getArray(
'BANDS')
699 if self.config.doComposeWcsJacobian
and not fgcmBuildStarsConfig.doApplyWcsJacobian:
700 raise RuntimeError(
"Cannot compose the WCS jacobian if it hasn't been applied "
701 "in fgcmBuildStarsTask.")
703 if not self.config.doComposeWcsJacobian
and fgcmBuildStarsConfig.doApplyWcsJacobian:
704 self.log.
warning(
"Jacobian was applied in build-stars but doComposeWcsJacobian is not set.")
706 if self.config.doReferenceCalibration:
707 lutCat = dataRefDict[
'fgcmLookUpTable'].get()
708 offsets = self._computeReferenceOffsets(stdCat, lutCat, bands, physicalFilterMap)
710 offsets = np.zeros(len(bands))
712 if self.config.doRefcatOutput
and butler
is not None:
714 datasetConfig = copy.copy(self.config.datasetConfig)
715 datasetConfig.ref_dataset_name =
'%s_%d' % (self.config.datasetConfig.ref_dataset_name,
717 self._outputStandardStars(butler, stdCat, offsets, bands, datasetConfig)
719 if self.config.doZeropointOutput:
720 pcgen = self._outputZeropoints(dataRefDict[
'camera'], zptCat, visitCat, offsets, bands,
721 physicalFilterMap, returnCatalogs=returnCatalogs)
725 if self.config.doAtmosphereOutput:
726 atmgen = self._outputAtmospheres(dataRefDict, atmCat)
730 retStruct = pipeBase.Struct(offsets=offsets,
733 retStruct.photoCalibCatalogs = pcgen
735 retStruct.photoCalibs = pcgen
739 def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap, bands):
741 Compute offsets relative to a reference catalog.
743 This method splits the star catalog into healpix pixels
744 and computes the calibration transfer for a sample of
745 these pixels to approximate the 'absolute' calibration
746 values (on for each band) to apply to transfer the
751 stdCat : `lsst.afw.table.SimpleCatalog`
753 lutCat : `lsst.afw.table.SimpleCatalog`
755 physicalFilterMap : `dict`
756 Dictionary of mappings from physical filter to FGCM band.
757 bands : `list` [`str`]
758 List of band names from FGCM output
761 offsets : `numpy.array` of floats
762 Per band zeropoint offsets
768 minObs = stdCat[
'ngood'].
min(axis=1)
770 goodStars = (minObs >= 1)
771 stdCat = stdCat[goodStars]
773 self.log.
info(
"Found %d stars with at least 1 good observation in each band" %
780 lutPhysicalFilters = lutCat[0][
'physicalFilters'].split(
',')
781 lutStdPhysicalFilters = lutCat[0][
'stdPhysicalFilters'].split(
',')
782 physicalFilterMapBands =
list(physicalFilterMap.values())
783 physicalFilterMapFilters =
list(physicalFilterMap.keys())
787 physicalFilterMapIndex = physicalFilterMapBands.index(band)
788 physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
790 lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
791 stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
793 physical=stdPhysicalFilter))
803 sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
804 sourceMapper.editOutputSchema().addField(
'instFlux', type=np.float64,
805 doc=
"instrumental flux (counts)")
806 sourceMapper.editOutputSchema().addField(
'instFluxErr', type=np.float64,
807 doc=
"instrumental flux error (counts)")
808 badStarKey = sourceMapper.editOutputSchema().addField(
'flag_badStar',
816 theta = np.pi/2. - stdCat[
'coord_dec']
817 phi = stdCat[
'coord_ra']
819 ipring = hp.ang2pix(self.config.referencePixelizationNside, theta, phi)
820 h, rev = esutil.stat.histogram(ipring, rev=
True)
822 gdpix, = np.where(h >= self.config.referencePixelizationMinStars)
824 self.log.
info(
"Found %d pixels (nside=%d) with at least %d good stars" %
826 self.config.referencePixelizationNside,
827 self.config.referencePixelizationMinStars))
829 if gdpix.size < self.config.referencePixelizationNPixels:
830 self.log.
warning(
"Found fewer good pixels (%d) than preferred in configuration (%d)" %
831 (gdpix.size, self.config.referencePixelizationNPixels))
834 gdpix = np.random.choice(gdpix, size=self.config.referencePixelizationNPixels, replace=
False)
836 results = np.zeros(gdpix.size, dtype=[(
'hpix',
'i4'),
837 (
'nstar',
'i4', len(bands)),
838 (
'nmatch',
'i4', len(bands)),
839 (
'zp',
'f4', len(bands)),
840 (
'zpErr',
'f4', len(bands))])
841 results[
'hpix'] = ipring[rev[rev[gdpix]]]
844 selected = np.zeros(len(stdCat), dtype=bool)
846 refFluxFields = [
None]*len(bands)
848 for p_index, pix
in enumerate(gdpix):
849 i1a = rev[rev[pix]: rev[pix + 1]]
857 for b_index, filterLabel
in enumerate(filterLabels):
858 struct = self._computeOffsetOneBand(sourceMapper, badStarKey, b_index,
860 selected, refFluxFields)
861 results[
'nstar'][p_index, b_index] = len(i1a)
862 results[
'nmatch'][p_index, b_index] = len(struct.arrays.refMag)
863 results[
'zp'][p_index, b_index] = struct.zp
864 results[
'zpErr'][p_index, b_index] = struct.sigma
867 offsets = np.zeros(len(bands))
869 for b_index, band
in enumerate(bands):
871 ok, = np.where(results[
'nmatch'][:, b_index] >= self.config.referenceMinMatch)
872 offsets[b_index] = np.median(results[
'zp'][ok, b_index])
875 madSigma = 1.4826*np.median(np.abs(results[
'zp'][ok, b_index] - offsets[b_index]))
876 self.log.
info(
"Reference catalog offset for %s band: %.12f +/- %.12f",
877 band, offsets[b_index], madSigma)
881 def _computeOffsetOneBand(self, sourceMapper, badStarKey,
882 b_index, filterLabel, stdCat, selected, refFluxFields):
884 Compute the zeropoint offset between the fgcm stdCat and the reference
885 stars for one pixel in one band
889 sourceMapper : `lsst.afw.table.SchemaMapper`
890 Mapper to go from stdCat to calibratable catalog
891 badStarKey : `lsst.afw.table.Key`
892 Key for the field with bad stars
894 Index of the band in the star catalog
895 filterLabel : `lsst.afw.image.FilterLabel`
896 filterLabel with band and physical filter
897 stdCat : `lsst.afw.table.SimpleCatalog`
899 selected : `numpy.array(dtype=bool)`
900 Boolean array of which stars are in the pixel
901 refFluxFields : `list`
902 List of names of flux fields for reference catalog
906 sourceCat.reserve(selected.sum())
907 sourceCat.extend(stdCat[selected], mapper=sourceMapper)
908 sourceCat[
'instFlux'] = 10.**(stdCat[
'mag_std_noabs'][selected, b_index]/(-2.5))
909 sourceCat[
'instFluxErr'] = (np.log(10.)/2.5)*(stdCat[
'magErr_std'][selected, b_index]
910 * sourceCat[
'instFlux'])
914 badStar = (stdCat[
'mag_std_noabs'][selected, b_index] > 90.0)
915 for rec
in sourceCat[badStar]:
916 rec.set(badStarKey,
True)
918 exposure = afwImage.ExposureF()
919 exposure.setFilterLabel(filterLabel)
921 if refFluxFields[b_index]
is None:
924 ctr = stdCat[0].getCoord()
925 rad = 0.05*lsst.geom.degrees
926 refDataTest = self.refObjLoader.loadSkyCircle(ctr, rad, filterLabel.bandLabel)
927 refFluxFields[b_index] = refDataTest.fluxField
930 calConfig = copy.copy(self.config.photoCal.value)
931 calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[b_index]
932 calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[b_index] +
'Err'
933 calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
935 schema=sourceCat.getSchema())
937 struct = calTask.run(exposure, sourceCat)
941 def _outputStandardStars(self, butler, stdCat, offsets, bands, datasetConfig):
943 Output standard stars in indexed reference catalog format.
944 This is not currently supported in Gen3.
948 butler : `lsst.daf.persistence.Butler`
949 stdCat : `lsst.afw.table.SimpleCatalog`
950 FGCM standard star catalog from fgcmFitCycleTask
951 offsets : `numpy.array` of floats
952 Per band zeropoint offsets
953 bands : `list` [`str`]
954 List of band names from FGCM output
955 datasetConfig : `lsst.meas.algorithms.DatasetConfig`
956 Config for reference dataset
959 self.log.
info(
"Outputting standard stars to %s" % (datasetConfig.ref_dataset_name))
961 indexer = IndexerRegistry[self.config.datasetConfig.indexer.name](
962 self.config.datasetConfig.indexer.active)
969 conv = stdCat[0][
'coord_ra'].asDegrees()/float(stdCat[0][
'coord_ra'])
970 indices = np.array(indexer.indexPoints(stdCat[
'coord_ra']*conv,
971 stdCat[
'coord_dec']*conv))
973 formattedCat = self._formatCatalog(stdCat, offsets, bands)
976 dataId = indexer.makeDataId(
'master_schema',
977 datasetConfig.ref_dataset_name)
980 butler.put(masterCat,
'ref_cat', dataId=dataId)
983 h, rev = esutil.stat.histogram(indices, rev=
True)
984 gd, = np.where(h > 0)
985 selected = np.zeros(len(formattedCat), dtype=bool)
987 i1a = rev[rev[i]: rev[i + 1]]
996 dataId = indexer.makeDataId(indices[i1a[0]],
997 datasetConfig.ref_dataset_name)
998 butler.put(formattedCat[selected],
'ref_cat', dataId=dataId)
1001 dataId = indexer.makeDataId(
None, datasetConfig.ref_dataset_name)
1002 butler.put(datasetConfig,
'ref_cat_config', dataId=dataId)
1004 self.log.
info(
"Done outputting standard stars.")
1006 def _formatCatalog(self, fgcmStarCat, offsets, bands):
1008 Turn an FGCM-formatted star catalog, applying zeropoint offsets.
1012 fgcmStarCat : `lsst.afw.Table.SimpleCatalog`
1013 SimpleCatalog as output by fgcmcal
1014 offsets : `list` with len(self.bands) entries
1015 Zeropoint offsets to apply
1016 bands : `list` [`str`]
1017 List of band names from FGCM output
1021 formattedCat: `lsst.afw.table.SimpleCatalog`
1022 SimpleCatalog suitable for using as a reference catalog
1026 minSchema = LoadIndexedReferenceObjectsTask.makeMinimalSchema(bands,
1030 sourceMapper.addMinimalSchema(minSchema)
1032 sourceMapper.editOutputSchema().addField(
'%s_nGood' % (band), type=np.int32)
1033 sourceMapper.editOutputSchema().addField(
'%s_nTotal' % (band), type=np.int32)
1034 sourceMapper.editOutputSchema().addField(
'%s_nPsfCandidate' % (band), type=np.int32)
1037 formattedCat.reserve(len(fgcmStarCat))
1038 formattedCat.extend(fgcmStarCat, mapper=sourceMapper)
1042 for b, band
in enumerate(bands):
1043 mag = fgcmStarCat[
'mag_std_noabs'][:, b].astype(np.float64) + offsets[b]
1046 flux = (mag*units.ABmag).to_value(units.nJy)
1047 fluxErr = (np.log(10.)/2.5)*flux*fgcmStarCat[
'magErr_std'][:, b].astype(np.float64)
1049 formattedCat[
'%s_flux' % (band)][:] = flux
1050 formattedCat[
'%s_fluxErr' % (band)][:] = fluxErr
1051 formattedCat[
'%s_nGood' % (band)][:] = fgcmStarCat[
'ngood'][:, b]
1052 formattedCat[
'%s_nTotal' % (band)][:] = fgcmStarCat[
'ntotal'][:, b]
1053 formattedCat[
'%s_nPsfCandidate' % (band)][:] = fgcmStarCat[
'npsfcand'][:, b]
1059 def _outputZeropoints(self, camera, zptCat, visitCat, offsets, bands,
1060 physicalFilterMap, returnCatalogs=True,
1062 """Output the zeropoints in fgcm_photoCalib format.
1066 camera : `lsst.afw.cameraGeom.Camera`
1067 Camera from the butler.
1068 zptCat : `lsst.afw.table.BaseCatalog`
1069 FGCM zeropoint catalog from `FgcmFitCycleTask`.
1070 visitCat : `lsst.afw.table.BaseCatalog`
1071 FGCM visitCat from `FgcmBuildStarsTask`.
1072 offsets : `numpy.array`
1073 Float array of absolute calibration offsets, one for each filter.
1074 bands : `list` [`str`]
1075 List of band names from FGCM output.
1076 physicalFilterMap : `dict`
1077 Dictionary of mappings from physical filter to FGCM band.
1078 returnCatalogs : `bool`, optional
1079 Return photoCalibs as per-visit exposure catalogs.
1080 tract: `int`, optional
1081 Tract number to output. Default is None (global calibration)
1085 photoCalibs : `generator` [(`int`, `int`, `str`, `lsst.afw.image.PhotoCalib`)]
1086 Generator that returns (visit, ccd, filtername, photoCalib) tuples.
1087 (returned if returnCatalogs is False).
1088 photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
1089 Generator that returns (visit, exposureCatalog) tuples.
1090 (returned if returnCatalogs is True).
1095 cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
'CANNOT_COMPUTE_ZEROPOINT']
1096 selected = (((zptCat[
'fgcmFlag'] & cannot_compute) == 0)
1097 & (zptCat[
'fgcmZptVar'] > 0.0)
1098 & (zptCat[
'fgcmZpt'] > FGCM_ILLEGAL_VALUE))
1101 badVisits = np.unique(zptCat[
'visit'][~selected])
1102 goodVisits = np.unique(zptCat[
'visit'][selected])
1103 allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
1104 for allBadVisit
in allBadVisits:
1105 self.log.
warning(f
'No suitable photoCalib for visit {allBadVisit}')
1109 for f
in physicalFilterMap:
1111 if physicalFilterMap[f]
in bands:
1112 offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]
1116 for ccdIndex, detector
in enumerate(camera):
1117 ccdMapping[detector.getId()] = ccdIndex
1121 for rec
in visitCat:
1122 scalingMapping[rec[
'visit']] = rec[
'scaling']
1124 if self.config.doComposeWcsJacobian:
1129 zptVisitCatalog =
None
1132 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
1133 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
1135 for rec
in zptCat[selected]:
1137 scaling = scalingMapping[rec[
'visit']][ccdMapping[rec[
'detector']]]
1144 postCalibrationOffset = offsetMapping[rec[
'filtername']]
1145 if self.config.doApplyMeanChromaticCorrection:
1146 postCalibrationOffset += rec[
'fgcmDeltaChrom']
1148 fgcmSuperStarField = self._getChebyshevBoundedField(rec[
'fgcmfZptSstarCheb'],
1149 rec[
'fgcmfZptChebXyMax'])
1151 fgcmZptField = self._getChebyshevBoundedField((rec[
'fgcmfZptCheb']*units.AB).to_value(units.nJy),
1152 rec[
'fgcmfZptChebXyMax'],
1153 offset=postCalibrationOffset,
1156 if self.config.doComposeWcsJacobian:
1167 calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
1168 calibErr = (np.log(10.0)/2.5)*calibCenter*np.sqrt(rec[
'fgcmZptVar'])
1170 calibrationErr=calibErr,
1171 calibration=fgcmField,
1174 if not returnCatalogs:
1176 yield (int(rec[
'visit']), int(rec[
'detector']), rec[
'filtername'], photoCalib)
1179 if rec[
'visit'] != lastVisit:
1184 zptVisitCatalog.sort()
1185 yield (int(lastVisit), zptVisitCatalog)
1188 zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema()
1189 zptExpCatSchema.addField(
'visit', type=
'I', doc=
'Visit number')
1193 zptVisitCatalog.setMetadata(metadata)
1195 lastVisit = int(rec[
'visit'])
1197 catRecord = zptVisitCatalog.addNew()
1198 catRecord[
'id'] = int(rec[
'detector'])
1199 catRecord[
'visit'] = rec[
'visit']
1200 catRecord.setPhotoCalib(photoCalib)
1205 zptVisitCatalog.sort()
1206 yield (int(lastVisit), zptVisitCatalog)
1208 def _getChebyshevBoundedField(self, coefficients, xyMax, offset=0.0, scaling=1.0):
1210 Make a ChebyshevBoundedField from fgcm coefficients, with optional offset
1215 coefficients: `numpy.array`
1216 Flattened array of chebyshev coefficients
1217 xyMax: `list` of length 2
1218 Maximum x and y of the chebyshev bounding box
1219 offset: `float`, optional
1220 Absolute calibration offset. Default is 0.0
1221 scaling: `float`, optional
1222 Flat scaling value from fgcmBuildStars. Default is 1.0
1226 boundedField: `lsst.afw.math.ChebyshevBoundedField`
1229 orderPlus1 = int(np.sqrt(coefficients.size))
1230 pars = np.zeros((orderPlus1, orderPlus1))
1235 pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1)
1236 * (10.**(offset/-2.5))*scaling)
1242 def _outputAtmospheres(self, dataRefDict, atmCat):
1244 Output the atmospheres.
1248 dataRefDict : `dict`
1249 All dataRefs are `lsst.daf.persistence.ButlerDataRef` (gen2) or
1250 `lsst.daf.butler.DeferredDatasetHandle` (gen3)
1251 dataRef dictionary with keys:
1253 ``"fgcmLookUpTable"``
1254 dataRef for the FGCM look-up table.
1255 atmCat : `lsst.afw.table.BaseCatalog`
1256 FGCM atmosphere parameter catalog from fgcmFitCycleTask.
1260 atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
1261 Generator that returns (visit, transmissionCurve) tuples.
1264 lutCat = dataRefDict[
'fgcmLookUpTable'].get()
1266 atmosphereTableName = lutCat[0][
'tablename']
1267 elevation = lutCat[0][
'elevation']
1268 atmLambda = lutCat[0][
'atmLambda']
1273 atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(atmosphereTableName)
1274 atmTable.loadTable()
1278 if atmTable
is None:
1281 modGen = fgcm.ModtranGenerator(elevation)
1282 lambdaRange = np.array([atmLambda[0], atmLambda[-1]])/10.
1283 lambdaStep = (atmLambda[1] - atmLambda[0])/10.
1284 except (ValueError, IOError)
as e:
1285 raise RuntimeError(
"FGCM look-up-table generated with modtran, "
1286 "but modtran not configured to run.")
from e
1288 zenith = np.degrees(np.arccos(1./atmCat[
'secZenith']))
1290 for i, visit
in enumerate(atmCat[
'visit']):
1291 if atmTable
is not None:
1293 atmVals = atmTable.interpolateAtmosphere(pmb=atmCat[i][
'pmb'],
1294 pwv=atmCat[i][
'pwv'],
1296 tau=atmCat[i][
'tau'],
1297 alpha=atmCat[i][
'alpha'],
1299 ctranslamstd=[atmCat[i][
'cTrans'],
1300 atmCat[i][
'lamStd']])
1303 modAtm = modGen(pmb=atmCat[i][
'pmb'],
1304 pwv=atmCat[i][
'pwv'],
1306 tau=atmCat[i][
'tau'],
1307 alpha=atmCat[i][
'alpha'],
1309 lambdaRange=lambdaRange,
1310 lambdaStep=lambdaStep,
1311 ctranslamstd=[atmCat[i][
'cTrans'],
1312 atmCat[i][
'lamStd']])
1313 atmVals = modAtm[
'COMBINED']
1316 curve = TransmissionCurve.makeSpatiallyConstant(throughput=atmVals,
1317 wavelengths=atmLambda,
1318 throughputAtMin=atmVals[0],
1319 throughputAtMax=atmVals[-1])
1321 yield (int(visit), curve)
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.
Custom catalog class for ExposureRecord/Table.
Defines the fields and offsets for a table.
A mapping between the keys of two Schemas, used to copy data between them.
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Class for storing ordered metadata with comments.
An integer coordinate rectangle.
daf::base::PropertyList * list
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
def computeApproxPixelAreaFields(camera)
def run(self, coaddExposures, bbox, wcs)
def addRefCatMetadata(catalog)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.