24 from lsstDebug 
import getDebugFrame
 
   26 import lsst.pex.config 
as pexConfig
 
   37 from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
 
   39 from .measurePsf 
import MeasurePsfTask
 
   40 from .repair 
import RepairTask
 
   42 __all__ = [
"CharacterizeImageConfig", 
"CharacterizeImageTask"]
 
   46                                    dimensions=(
"instrument", 
"visit", 
"detector")):
 
   48         doc=
"Input exposure data",
 
   50         storageClass=
"ExposureF",
 
   51         dimensions=[
"instrument", 
"exposure", 
"detector"],
 
   53     characterized = cT.Output(
 
   54         doc=
"Output characterized data.",
 
   56         storageClass=
"ExposureF",
 
   57         dimensions=[
"instrument", 
"visit", 
"detector"],
 
   59     sourceCat = cT.Output(
 
   60         doc=
"Output source catalog.",
 
   62         storageClass=
"SourceCatalog",
 
   63         dimensions=[
"instrument", 
"visit", 
"detector"],
 
   65     backgroundModel = cT.Output(
 
   66         doc=
"Output background model.",
 
   67         name=
"icExpBackground",
 
   68         storageClass=
"Background",
 
   69         dimensions=[
"instrument", 
"visit", 
"detector"],
 
   71     outputSchema = cT.InitOutput(
 
   72         doc=
"Schema of the catalog produced by CharacterizeImage",
 
   74         storageClass=
"SourceCatalog",
 
   77     def adjustQuantum(self, datasetRefMap: pipeBase.InputQuantizedConnection):
 
   81         except pipeBase.ScalarError 
as err:
 
   82             raise pipeBase.ScalarError(
 
   83                 f
"CharacterizeImageTask can at present only be run on visits that are associated with " 
   84                 f
"exactly one exposure.  Either this is not a valid exposure for this pipeline, or the " 
   85                 f
"snap-combination step you probably want hasn't been configured to run between ISR and " 
   86                 f
"this task (as of this writing, that would be because it hasn't been implemented yet)." 
   91                               pipelineConnections=CharacterizeImageConnections):
 
   93     """!Config for CharacterizeImageTask""" 
   94     doMeasurePsf = pexConfig.Field(
 
   97         doc=
"Measure PSF? If False then for all subsequent operations use either existing PSF " 
   98             "model when present, or install simple PSF model when not (see installSimplePsf " 
  101     doWrite = pexConfig.Field(
 
  104         doc=
"Persist results?",
 
  106     doWriteExposure = pexConfig.Field(
 
  109         doc=
"Write icExp and icExpBackground in addition to icSrc? Ignored if doWrite False.",
 
  111     psfIterations = pexConfig.RangeField(
 
  115         doc=
"Number of iterations of detect sources, measure sources, " 
  116             "estimate PSF. If useSimplePsf is True then 2 should be plenty; " 
  117             "otherwise more may be wanted.",
 
  119     background = pexConfig.ConfigurableField(
 
  120         target=SubtractBackgroundTask,
 
  121         doc=
"Configuration for initial background estimation",
 
  123     detection = pexConfig.ConfigurableField(
 
  124         target=SourceDetectionTask,
 
  127     doDeblend = pexConfig.Field(
 
  130         doc=
"Run deblender input exposure" 
  132     deblend = pexConfig.ConfigurableField(
 
  133         target=SourceDeblendTask,
 
  134         doc=
"Split blended source into their components" 
  136     measurement = pexConfig.ConfigurableField(
 
  137         target=SingleFrameMeasurementTask,
 
  138         doc=
"Measure sources" 
  140     doApCorr = pexConfig.Field(
 
  143         doc=
"Run subtasks to measure and apply aperture corrections" 
  145     measureApCorr = pexConfig.ConfigurableField(
 
  146         target=MeasureApCorrTask,
 
  147         doc=
"Subtask to measure aperture corrections" 
  149     applyApCorr = pexConfig.ConfigurableField(
 
  150         target=ApplyApCorrTask,
 
  151         doc=
"Subtask to apply aperture corrections" 
  155     catalogCalculation = pexConfig.ConfigurableField(
 
  156         target=CatalogCalculationTask,
 
  157         doc=
"Subtask to run catalogCalculation plugins on catalog" 
  159     useSimplePsf = pexConfig.Field(
 
  162         doc=
"Replace the existing PSF model with a simplified version that has the same sigma " 
  163         "at the start of each PSF determination iteration? Doing so makes PSF determination " 
  164         "converge more robustly and quickly.",
 
  166     installSimplePsf = pexConfig.ConfigurableField(
 
  167         target=InstallGaussianPsfTask,
 
  168         doc=
"Install a simple PSF model",
 
  170     refObjLoader = pexConfig.ConfigurableField(
 
  171         target=LoadIndexedReferenceObjectsTask,
 
  172         doc=
"reference object loader",
 
  174     ref_match = pexConfig.ConfigurableField(
 
  176         doc=
"Task to load and match reference objects. Only used if measurePsf can use matches. " 
  177         "Warning: matching will only work well if the initial WCS is accurate enough " 
  178         "to give good matches (roughly: good to 3 arcsec across the CCD).",
 
  180     measurePsf = pexConfig.ConfigurableField(
 
  181         target=MeasurePsfTask,
 
  184     repair = pexConfig.ConfigurableField(
 
  186         doc=
"Remove cosmic rays",
 
  188     checkUnitsParseStrict = pexConfig.Field(
 
  189         doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
 
  199         self.
detection.includeThresholdMultiplier = 10.0
 
  200         self.
detection.doTempLocalBackground = 
False 
  213             "base_CircularApertureFlux",
 
  218             raise RuntimeError(
"Must measure PSF to measure aperture correction, " 
  219                                "because flags determined by PSF measurement are used to identify " 
  220                                "sources used to measure aperture correction")
 
  231     r"""!Measure bright sources and use this to estimate background and PSF of an exposure 
  233     @anchor CharacterizeImageTask_ 
  235     @section pipe_tasks_characterizeImage_Contents  Contents 
  237      - @ref pipe_tasks_characterizeImage_Purpose 
  238      - @ref pipe_tasks_characterizeImage_Initialize 
  239      - @ref pipe_tasks_characterizeImage_IO 
  240      - @ref pipe_tasks_characterizeImage_Config 
  241      - @ref pipe_tasks_characterizeImage_Debug 
  244     @section pipe_tasks_characterizeImage_Purpose  Description 
  246     Given an exposure with defects repaired (masked and interpolated over, e.g. as output by IsrTask): 
  247     - detect and measure bright sources 
  249     - measure and subtract background 
  252     @section pipe_tasks_characterizeImage_Initialize  Task initialisation 
  254     @copydoc \_\_init\_\_ 
  256     @section pipe_tasks_characterizeImage_IO  Invoking the Task 
  258     If you want this task to unpersist inputs or persist outputs, then call 
  259     the `runDataRef` method (a thin wrapper around the `run` method). 
  261     If you already have the inputs unpersisted and do not want to persist the output 
  262     then it is more direct to call the `run` method: 
  264     @section pipe_tasks_characterizeImage_Config  Configuration parameters 
  266     See @ref CharacterizeImageConfig 
  268     @section pipe_tasks_characterizeImage_Debug  Debug variables 
  270     The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a flag 
  271     `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug for more about `debug.py`. 
  273     CharacterizeImageTask has a debug dictionary with the following keys: 
  276     <dd>int: if specified, the frame of first debug image displayed (defaults to 1) 
  278     <dd>bool; if True display image after each repair in the measure PSF loop 
  280     <dd>bool; if True display image after each background subtraction in the measure PSF loop 
  282     <dd>bool; if True display image and sources at the end of each iteration of the measure PSF loop 
  283         See @ref lsst.meas.astrom.displayAstrometry for the meaning of the various symbols. 
  285     <dd>bool; if True display image and sources after PSF is measured; 
  286         this will be identical to the final image displayed by measure_iter if measure_iter is true 
  288     <dd>bool; if True display image and sources after final repair 
  290     <dd>bool; if True display image and sources after final measurement 
  293     For example, put something like: 
  297             di = lsstDebug.getInfo(name)  # N.b. lsstDebug.Info(name) would call us recursively 
  298             if name == "lsst.pipe.tasks.characterizeImage": 
  305         lsstDebug.Info = DebugInfo 
  307     into your `debug.py` file and run `calibrateTask.py` with the `--debug` flag. 
  309     Some subtasks may have their own debug variables; see individual Task documentation. 
  314     ConfigClass = CharacterizeImageConfig
 
  315     _DefaultName = 
"characterizeImage" 
  316     RunnerClass = pipeBase.ButlerInitializedTaskRunner
 
  319         inputs = butlerQC.get(inputRefs)
 
  320         if 'exposureIdInfo' not in inputs.keys():
 
  322             exposureIdInfo.expId, exposureIdInfo.expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
 
  324             inputs[
'exposureIdInfo'] = exposureIdInfo
 
  325         outputs = self.
run(**inputs)
 
  326         butlerQC.put(outputs, outputRefs)
 
  328     def __init__(self, butler=None, refObjLoader=None, schema=None, **kwargs):
 
  329         """!Construct a CharacterizeImageTask 
  331         @param[in] butler  A butler object is passed to the refObjLoader constructor in case 
  332             it is needed to load catalogs.  May be None if a catalog-based star selector is 
  333             not used, if the reference object loader constructor does not require a butler, 
  334             or if a reference object loader is passed directly via the refObjLoader argument. 
  335         @param[in] refObjLoader  An instance of LoadReferenceObjectsTasks that supplies an 
  336             external reference catalog to a catalog-based star selector.  May be None if a 
  337             catalog star selector is not used or the loader can be constructed from the 
  339         @param[in,out] schema  initial schema (an lsst.afw.table.SourceTable), or None 
  340         @param[in,out] kwargs  other keyword arguments for lsst.pipe.base.CmdLineTask 
  345             schema = SourceTable.makeMinimalSchema()
 
  347         self.makeSubtask(
"background")
 
  348         self.makeSubtask(
"installSimplePsf")
 
  349         self.makeSubtask(
"repair")
 
  350         self.makeSubtask(
"measurePsf", schema=self.
schema)
 
  351         if self.config.doMeasurePsf 
and self.measurePsf.usesMatches:
 
  353                 self.makeSubtask(
'refObjLoader', butler=butler)
 
  354                 refObjLoader = self.refObjLoader
 
  355             self.makeSubtask(
"ref_match", refObjLoader=refObjLoader)
 
  357         self.makeSubtask(
'detection', schema=self.
schema)
 
  358         if self.config.doDeblend:
 
  359             self.makeSubtask(
"deblend", schema=self.
schema)
 
  360         self.makeSubtask(
'measurement', schema=self.
schema, algMetadata=self.
algMetadata)
 
  361         if self.config.doApCorr:
 
  362             self.makeSubtask(
'measureApCorr', schema=self.
schema)
 
  363             self.makeSubtask(
'applyApCorr', schema=self.
schema)
 
  364         self.makeSubtask(
'catalogCalculation', schema=self.
schema)
 
  367         self.
schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
 
  372         outputCatSchema.getTable().setMetadata(self.
algMetadata)
 
  373         return {
'outputSchema': outputCatSchema}
 
  376     def runDataRef(self, dataRef, exposure=None, background=None, doUnpersist=True):
 
  377         """!Characterize a science image and, if wanted, persist the results 
  379         This simply unpacks the exposure and passes it to the characterize method to do the work. 
  381         @param[in] dataRef: butler data reference for science exposure 
  382         @param[in,out] exposure  exposure to characterize (an lsst.afw.image.ExposureF or similar). 
  383             If None then unpersist from "postISRCCD". 
  384             The following changes are made, depending on the config: 
  385             - set psf to the measured PSF 
  386             - set apCorrMap to the measured aperture correction 
  387             - subtract background 
  388             - interpolate over cosmic rays 
  389             - update detection and cosmic ray mask planes 
  390         @param[in,out] background  initial model of background already subtracted from exposure 
  391             (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted, 
  392             which is typical for image characterization. 
  393             A refined background model is output. 
  394         @param[in] doUnpersist  if True the exposure is read from the repository 
  395             and the exposure and background arguments must be None; 
  396             if False the exposure must be provided. 
  397             True is intended for running as a command-line task, False for running as a subtask 
  399         @return same data as the characterize method 
  402         self.log.
info(
"Processing %s" % (dataRef.dataId))
 
  405             if exposure 
is not None or background 
is not None:
 
  406                 raise RuntimeError(
"doUnpersist true; exposure and background must be None")
 
  407             exposure = dataRef.get(
"postISRCCD", immediate=
True)
 
  408         elif exposure 
is None:
 
  409             raise RuntimeError(
"doUnpersist false; exposure must be provided")
 
  411         exposureIdInfo = dataRef.get(
"expIdInfo")
 
  415             exposureIdInfo=exposureIdInfo,
 
  416             background=background,
 
  419         if self.config.doWrite:
 
  420             dataRef.put(charRes.sourceCat, 
"icSrc")
 
  421             if self.config.doWriteExposure:
 
  422                 dataRef.put(charRes.exposure, 
"icExp")
 
  423                 dataRef.put(charRes.background, 
"icExpBackground")
 
  428     def run(self, exposure, exposureIdInfo=None, background=None):
 
  429         """!Characterize a science image 
  431         Peforms the following operations: 
  432         - Iterate the following config.psfIterations times, or once if config.doMeasurePsf false: 
  433             - detect and measure sources and estimate PSF (see detectMeasureAndEstimatePsf for details) 
  434         - interpolate over cosmic rays 
  435         - perform final measurement 
  437         @param[in,out] exposure  exposure to characterize (an lsst.afw.image.ExposureF or similar). 
  438             The following changes are made: 
  441             - update detection and cosmic ray mask planes 
  442             - subtract background and interpolate over cosmic rays 
  443         @param[in] exposureIdInfo  ID info for exposure (an lsst.obs.base.ExposureIdInfo). 
  444             If not provided, returned SourceCatalog IDs will not be globally unique. 
  445         @param[in,out] background  initial model of background already subtracted from exposure 
  446             (an lsst.afw.math.BackgroundList). May be None if no background has been subtracted, 
  447             which is typical for image characterization. 
  449         @return pipe_base Struct containing these fields, all from the final iteration 
  450         of detectMeasureAndEstimatePsf: 
  451         - exposure: characterized exposure; image is repaired by interpolating over cosmic rays, 
  452             mask is updated accordingly, and the PSF model is set 
  453         - sourceCat: detected sources (an lsst.afw.table.SourceCatalog) 
  454         - background: model of background subtracted from exposure (an lsst.afw.math.BackgroundList) 
  455         - psfCellSet: spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet) 
  459         if not self.config.doMeasurePsf 
and not exposure.hasPsf():
 
  460             self.log.
warn(
"Source catalog detected and measured with placeholder or default PSF")
 
  461             self.installSimplePsf.
run(exposure=exposure)
 
  463         if exposureIdInfo 
is None:
 
  467         background = self.background.
run(exposure).background
 
  469         psfIterations = self.config.psfIterations 
if self.config.doMeasurePsf 
else 1
 
  470         for i 
in range(psfIterations):
 
  473                 exposureIdInfo=exposureIdInfo,
 
  474                 background=background,
 
  477             psf = dmeRes.exposure.getPsf()
 
  478             psfSigma = psf.computeShape().getDeterminantRadius()
 
  479             psfDimensions = psf.computeImage().getDimensions()
 
  480             medBackground = np.median(dmeRes.background.getImage().getArray())
 
  481             self.log.
info(
"iter %s; PSF sigma=%0.2f, dimensions=%s; median background=%0.2f" %
 
  482                           (i + 1, psfSigma, psfDimensions, medBackground))
 
  484         self.
display(
"psf", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
 
  487         self.repair.
run(exposure=dmeRes.exposure)
 
  488         self.
display(
"repair", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
 
  492         self.measurement.
run(measCat=dmeRes.sourceCat, exposure=dmeRes.exposure,
 
  493                              exposureId=exposureIdInfo.expId)
 
  494         if self.config.doApCorr:
 
  495             apCorrMap = self.measureApCorr.
run(exposure=dmeRes.exposure, catalog=dmeRes.sourceCat).apCorrMap
 
  496             dmeRes.exposure.getInfo().setApCorrMap(apCorrMap)
 
  497             self.applyApCorr.
run(catalog=dmeRes.sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
 
  498         self.catalogCalculation.
run(dmeRes.sourceCat)
 
  500         self.
display(
"measure", exposure=dmeRes.exposure, sourceCat=dmeRes.sourceCat)
 
  502         return pipeBase.Struct(
 
  503             exposure=dmeRes.exposure,
 
  504             sourceCat=dmeRes.sourceCat,
 
  505             background=dmeRes.background,
 
  506             psfCellSet=dmeRes.psfCellSet,
 
  508             characterized=dmeRes.exposure,
 
  509             backgroundModel=dmeRes.background
 
  514         """!Perform one iteration of detect, measure and estimate PSF 
  516         Performs the following operations: 
  517         - if config.doMeasurePsf or not exposure.hasPsf(): 
  518             - install a simple PSF model (replacing the existing one, if need be) 
  519         - interpolate over cosmic rays with keepCRs=True 
  520         - estimate background and subtract it from the exposure 
  521         - detect, deblend and measure sources, and subtract a refined background model; 
  522         - if config.doMeasurePsf: 
  525         @param[in,out] exposure  exposure to characterize (an lsst.afw.image.ExposureF or similar) 
  526             The following changes are made: 
  528             - update detection and cosmic ray mask planes 
  529             - subtract background 
  530         @param[in] exposureIdInfo  ID info for exposure (an lsst.obs_base.ExposureIdInfo) 
  531         @param[in,out] background  initial model of background already subtracted from exposure 
  532             (an lsst.afw.math.BackgroundList). 
  534         @return pipe_base Struct containing these fields, all from the final iteration 
  535         of detect sources, measure sources and estimate PSF: 
  536         - exposure  characterized exposure; image is repaired by interpolating over cosmic rays, 
  537             mask is updated accordingly, and the PSF model is set 
  538         - sourceCat  detected sources (an lsst.afw.table.SourceCatalog) 
  539         - background  model of background subtracted from exposure (an lsst.afw.math.BackgroundList) 
  540         - psfCellSet  spatial cells of PSF candidates (an lsst.afw.math.SpatialCellSet) 
  543         if not exposure.hasPsf() 
or (self.config.doMeasurePsf 
and self.config.useSimplePsf):
 
  544             self.log.
warn(
"Source catalog detected and measured with placeholder or default PSF")
 
  545             self.installSimplePsf.
run(exposure=exposure)
 
  548         self.repair.
run(exposure=exposure, keepCRs=
True)
 
  549         self.
display(
"repair_iter", exposure=exposure)
 
  551         if background 
is None:
 
  554         sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits)
 
  555         table = SourceTable.make(self.
schema, sourceIdFactory)
 
  558         detRes = self.detection.
run(table=table, exposure=exposure, doSmooth=
True)
 
  559         sourceCat = detRes.sources
 
  560         if detRes.fpSets.background:
 
  561             for bg 
in detRes.fpSets.background:
 
  562                 background.append(bg)
 
  564         if self.config.doDeblend:
 
  565             self.deblend.
run(exposure=exposure, sources=sourceCat)
 
  567         self.measurement.
run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
 
  569         measPsfRes = pipeBase.Struct(cellSet=
None)
 
  570         if self.config.doMeasurePsf:
 
  571             if self.measurePsf.usesMatches:
 
  572                 matches = self.ref_match.loadAndMatch(exposure=exposure, sourceCat=sourceCat).matches
 
  575             measPsfRes = self.measurePsf.
run(exposure=exposure, sources=sourceCat, matches=matches,
 
  576                                              expId=exposureIdInfo.expId)
 
  577         self.
display(
"measure_iter", exposure=exposure, sourceCat=sourceCat)
 
  579         return pipeBase.Struct(
 
  582             background=background,
 
  583             psfCellSet=measPsfRes.cellSet,
 
  587         """Return a dict of empty catalogs for each catalog dataset produced by this task. 
  591         return {
"icSrc": sourceCat}
 
  593     def display(self, itemName, exposure, sourceCat=None):
 
  594         """Display exposure and sources on next frame, if display of itemName has been requested 
  596         @param[in] itemName  name of item in debugInfo 
  597         @param[in] exposure  exposure to display 
  598         @param[in] sourceCat  source catalog to display