LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
lsst.pipe.tasks.imageDifference.ImageDifferenceFromTemplateConfig Class Reference
Inheritance diagram for lsst.pipe.tasks.imageDifference.ImageDifferenceFromTemplateConfig:

Detailed Description

doAddCalexpBackground = pexConfig.Field(dtype=bool, default=False,
                                        doc="Add background to calexp before processing it.  "
                                            "Useful as ipDiffim does background matching.")
doUseRegister = pexConfig.Field(dtype=bool, default=False,
                                doc="Re-compute astrometry on the template. "
                                "Use image-to-image registration to align template with "
                                "science image (AL only).")
doDebugRegister = pexConfig.Field(dtype=bool, default=False,
                                  doc="Writing debugging data for doUseRegister")
doSelectSources = pexConfig.Field(dtype=bool, default=False,
                                  doc="Select stars to use for kernel fitting (AL only)")
doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=False,
                                     doc="Select stars of extreme color as part "
                                     "of the control sample (AL only)")
doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=False,
                                          doc="Select stars that are variable to be part "
                                              "of the control sample (AL only)")
doSubtract = pexConfig.Field(dtype=bool, default=True, doc="Compute subtracted exposure?")
doPreConvolve = pexConfig.Field(dtype=bool, default=False,
                                doc="Not in use. Superseded by useScoreImageDetection.",
                                deprecated="This option superseded by useScoreImageDetection."
                                " Will be removed after v22.")
useScoreImageDetection = pexConfig.Field(
    dtype=bool, default=False, doc="Calculate the pre-convolved AL likelihood or "
    "the Zogy score image. Use it for source detection (if doDetection=True).")
doWriteScoreExp = pexConfig.Field(
    dtype=bool, default=False, doc="Write AL likelihood or Zogy score exposure?")
doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=False,
                                          doc="Scale variance of the template before PSF matching")
doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=True,
                                        doc="Scale variance of the diffim before PSF matching. "
                                            "You may do either this or template variance scaling, "
                                            "or neither. (Doing both is a waste of CPU.)")
useGaussianForPreConvolution = pexConfig.Field(
    dtype=bool, default=False, doc="Use a simple gaussian PSF model for pre-convolution "
    "(oherwise use exposure PSF)? (AL and if useScoreImageDetection=True only)")
doDetection = pexConfig.Field(dtype=bool, default=True, doc="Detect sources?")
doDecorrelation = pexConfig.Field(dtype=bool, default=True,
                                  doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
                                  "kernel convolution (AL only)? If True, also update the diffim PSF.")
doMerge = pexConfig.Field(dtype=bool, default=True,
                          doc="Merge positive and negative diaSources with grow radius "
                              "set by growFootprint")
doMatchSources = pexConfig.Field(dtype=bool, default=False,
                                 doc="Match diaSources with input calexp sources and ref catalog sources")
doMeasurement = pexConfig.Field(dtype=bool, default=True, doc="Measure diaSources?")
doDipoleFitting = pexConfig.Field(dtype=bool, default=True, doc="Measure dipoles using new algorithm?")
doForcedMeasurement = pexConfig.Field(
    dtype=bool,
    default=True,
    doc="Force photometer diaSource locations on PVI?")
doWriteSubtractedExp = pexConfig.Field(
    dtype=bool, default=True, doc="Write difference exposure (AL and Zogy) ?")
doWriteWarpedExp = pexConfig.Field(
    dtype=bool, default=False, doc="Write WCS, warped template coadd exposure?")
doWriteMatchedExp = pexConfig.Field(dtype=bool, default=False,
                                    doc="Write warped and PSF-matched template coadd exposure?")
doWriteSources = pexConfig.Field(dtype=bool, default=True, doc="Write sources?")
doAddMetrics = pexConfig.Field(dtype=bool, default=False,
                               doc="Add columns to the source table to hold analysis metrics?")

coaddName = pexConfig.Field(
    doc="coadd name: typically one of deep, goodSeeing, or dcr",
    dtype=str,
    default="deep",
)
convolveTemplate = pexConfig.Field(
    doc="Which image gets convolved (default = template)",
    dtype=bool,
    default=True
)
refObjLoader = pexConfig.ConfigurableField(
    target=LoadIndexedReferenceObjectsTask,
    doc="reference object loader",
)
astrometer = pexConfig.ConfigurableField(
    target=AstrometryTask,
    doc="astrometry task; used to match sources to reference objects, but not to fit a WCS",
)
sourceSelector = pexConfig.ConfigurableField(
    target=ObjectSizeStarSelectorTask,
    doc="Source selection algorithm",
)
subtract = subtractAlgorithmRegistry.makeField("Subtraction Algorithm", default="al")
decorrelate = pexConfig.ConfigurableField(
    target=DecorrelateALKernelSpatialTask,
    doc="Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
    "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
    "default of 5.5).",
)
# Old style ImageMapper grid. ZogyTask has its own grid option
doSpatiallyVarying = pexConfig.Field(
    dtype=bool,
    default=False,
    doc="Perform A&L decorrelation on a grid across the "
    "image in order to allow for spatial variations. Zogy does not use this option."
)
detection = pexConfig.ConfigurableField(
    target=SourceDetectionTask,
    doc="Low-threshold detection for final measurement",
)
measurement = pexConfig.ConfigurableField(
    target=DipoleFitTask,
    doc="Enable updated dipole fitting method",
)
doApCorr = lsst.pex.config.Field(
    dtype=bool,
    default=True,
    doc="Run subtask to apply aperture corrections"
)
applyApCorr = lsst.pex.config.ConfigurableField(
    target=ApplyApCorrTask,
    doc="Subtask to apply aperture corrections"
)
forcedMeasurement = pexConfig.ConfigurableField(
    target=ForcedMeasurementTask,
    doc="Subtask to force photometer PVI at diaSource location.",
)
getTemplate = pexConfig.ConfigurableField(
    target=GetCoaddAsTemplateTask,
    doc="Subtask to retrieve template exposure and sources",
)
scaleVariance = pexConfig.ConfigurableField(
    target=ScaleVarianceTask,
    doc="Subtask to rescale the variance of the template "
        "to the statistically expected level"
)
controlStepSize = pexConfig.Field(
    doc="What step size (every Nth one) to select a control sample from the kernelSources",
    dtype=int,
    default=5
)
controlRandomSeed = pexConfig.Field(
    doc="Random seed for shuffing the control sample",
    dtype=int,
    default=10
)
register = pexConfig.ConfigurableField(
    target=RegisterTask,
    doc="Task to enable image-to-image image registration (warping)",
)
kernelSourcesFromRef = pexConfig.Field(
    doc="Select sources to measure kernel from reference catalog if True, template if false",
    dtype=bool,
    default=False
)
templateSipOrder = pexConfig.Field(
    dtype=int, default=2,
    doc="Sip Order for fitting the Template Wcs (default is too high, overfitting)"
)
growFootprint = pexConfig.Field(
    dtype=int, default=2,
    doc="Grow positive and negative footprints by this amount before merging"
)
diaSourceMatchRadius = pexConfig.Field(
    dtype=float, default=0.5,
    doc="Match radius (in arcseconds) for DiaSource to Source association"
)
requiredTemplateFraction = pexConfig.Field(
    dtype=float, default=0.1,
    doc="Do not attempt to run task if template covers less than this fraction of pixels."
    "Setting to 0 will always attempt image subtraction"
)
doSkySources = pexConfig.Field(
    dtype=bool,
    default=False,
    doc="Generate sky sources?",
)
skySources = pexConfig.ConfigurableField(
    target=SkyObjectsTask,
    doc="Generate sky sources",
)

def setDefaults(self):
    # defaults are OK for catalog and diacatalog

    self.subtract['al'].kernel.name = "AL"
    self.subtract['al'].kernel.active.fitForBackground = True
    self.subtract['al'].kernel.active.spatialKernelOrder = 1
    self.subtract['al'].kernel.active.spatialBgOrder = 2

    # DiaSource Detection
    self.detection.thresholdPolarity = "both"
    self.detection.thresholdValue = 5.0
    self.detection.reEstimateBackground = False
    self.detection.thresholdType = "pixel_stdev"

    # Add filtered flux measurement, the correct measurement for pre-convolved images.
    # Enable all measurements, regardless of doPreConvolve, as it makes data harvesting easier.
    # To change that you must modify algorithms.names in the task's applyOverrides method,
    # after the user has set doPreConvolve.
    self.measurement.algorithms.names.add('base_PeakLikelihoodFlux')
    self.measurement.plugins.names |= ['ext_trailedSources_Naive',
                                       'base_LocalPhotoCalib',
                                       'base_LocalWcs']

    self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
    self.forcedMeasurement.copyColumns = {
        "id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
    self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
    self.forcedMeasurement.slots.shape = None

    # For shuffling the control sample
    random.seed(self.controlRandomSeed)

def validate(self):
    pexConfig.Config.validate(self)
    if not self.doSubtract and not self.doDetection:
        raise ValueError("Either doSubtract or doDetection must be enabled.")
    if self.doMeasurement and not self.doDetection:
        raise ValueError("Cannot run source measurement without source detection.")
    if self.doMerge and not self.doDetection:
        raise ValueError("Cannot run source merging without source detection.")
    if self.doSkySources and not self.doDetection:
        raise ValueError("Cannot run sky source creation without source detection.")
    if self.doUseRegister and not self.doSelectSources:
        raise ValueError("doUseRegister=True and doSelectSources=False. "
                         "Cannot run RegisterTask without selecting sources.")
    if hasattr(self.getTemplate, "coaddName"):
        if self.getTemplate.coaddName != self.coaddName:
            raise ValueError("Mis-matched coaddName and getTemplate.coaddName in the config.")
    if self.doScaleDiffimVariance and self.doScaleTemplateVariance:
        raise ValueError("Scaling the diffim variance and scaling the template variance "
                         "are both set. Please choose one or the other.")
    # We cannot allow inconsistencies that would lead to None or not available output products
    if self.subtract.name == 'zogy':
        if self.doWriteMatchedExp:
            raise ValueError("doWriteMatchedExp=True Matched exposure is not "
                             "calculated in zogy subtraction.")
        if self.doAddMetrics:
            raise ValueError("doAddMetrics=True Kernel metrics does not exist in zogy subtraction.")
        if self.doDecorrelation:
            raise ValueError(
                "doDecorrelation=True The decorrelation afterburner does not exist in zogy subtraction.")
        if self.doSelectSources:
            raise ValueError(
                "doSelectSources=True Selecting sources for PSF matching is not a zogy option.")
        if self.useGaussianForPreConvolution:
            raise ValueError(
                "useGaussianForPreConvolution=True This is an AL subtraction only option.")
    else:
        # AL only consistency checks
        if self.useScoreImageDetection and not self.convolveTemplate:
            raise ValueError(
                "convolveTemplate=False and useScoreImageDetection=True "
                "Pre-convolution and matching of the science image is not a supported operation.")
        if self.doWriteSubtractedExp and self.useScoreImageDetection:
            raise ValueError(
                "doWriteSubtractedExp=True and useScoreImageDetection=True "
                "Regular difference image is not calculated. "
                "AL subtraction calculates either the regular difference image or the score image.")
        if self.doWriteScoreExp and not self.useScoreImageDetection:
            raise ValueError(
                "doWriteScoreExp=True and useScoreImageDetection=False "
                "Score image is not calculated. "
                "AL subtraction calculates either the regular difference image or the score image.")
        if self.doAddMetrics and not self.doSubtract:
            raise ValueError("Subtraction must be enabled for kernel metrics calculation.")
        if self.useGaussianForPreConvolution and not self.useScoreImageDetection:
            raise ValueError(
                "useGaussianForPreConvolution=True and useScoreImageDetection=False "
                "Gaussian PSF approximation exists only for AL subtraction w/ pre-convolution.")


class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):

@staticmethod
def getTargetList(parsedCmd, **kwargs):
    return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
                                             **kwargs)


class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
ConfigClass = ImageDifferenceConfig
RunnerClass = ImageDifferenceTaskRunner
_DefaultName = "imageDifference"

def __init__(self, butler=None, **kwargs):
super().__init__(**kwargs)
self.makeSubtask("getTemplate")

self.makeSubtask("subtract")

if self.config.subtract.name == 'al' and self.config.doDecorrelation:
    self.makeSubtask("decorrelate")

if self.config.doScaleTemplateVariance or self.config.doScaleDiffimVariance:
    self.makeSubtask("scaleVariance")

if self.config.doUseRegister:
    self.makeSubtask("register")
self.schema = afwTable.SourceTable.makeMinimalSchema()

if self.config.doSelectSources:
    self.makeSubtask("sourceSelector")
    if self.config.kernelSourcesFromRef:
        self.makeSubtask('refObjLoader', butler=butler)
        self.makeSubtask("astrometer", refObjLoader=self.refObjLoader)

self.algMetadata = dafBase.PropertyList()
if self.config.doDetection:
    self.makeSubtask("detection", schema=self.schema)
if self.config.doMeasurement:
    self.makeSubtask("measurement", schema=self.schema,
                     algMetadata=self.algMetadata)
if self.config.doApCorr:
    self.makeSubtask("applyApCorr", schema=self.measurement.schema)
if self.config.doForcedMeasurement:
    self.schema.addField(
        "ip_diffim_forced_PsfFlux_instFlux", "D",
        "Forced PSF flux measured on the direct image.",
        units="count")
    self.schema.addField(
        "ip_diffim_forced_PsfFlux_instFluxErr", "D",
        "Forced PSF flux error measured on the direct image.",
        units="count")
    self.schema.addField(
        "ip_diffim_forced_PsfFlux_area", "F",
        "Forced PSF flux effective area of PSF.",
        units="pixel")
    self.schema.addField(
        "ip_diffim_forced_PsfFlux_flag", "Flag",
        "Forced PSF flux general failure flag.")
    self.schema.addField(
        "ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
        "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
    self.schema.addField(
        "ip_diffim_forced_PsfFlux_flag_edge", "Flag",
        "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
    self.makeSubtask("forcedMeasurement", refSchema=self.schema)
if self.config.doMatchSources:
    self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
    self.schema.addField("srcMatchId", "L", "unique id of source match")
if self.config.doSkySources:
    self.makeSubtask("skySources")
    self.skySourceKey = self.schema.addField("sky_source", type="Flag", doc="Sky objects.")

# initialize InitOutputs
self.outputSchema = afwTable.SourceCatalog(self.schema)
self.outputSchema.getTable().setMetadata(self.algMetadata)

@staticmethod
def makeIdFactory(expId, expBits):
return ExposureIdInfo(expId, expBits).makeSourceIdFactory()

@lsst.utils.inheritDoc(pipeBase.PipelineTask)
def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
           inputRefs: pipeBase.InputQuantizedConnection,
           outputRefs: pipeBase.OutputQuantizedConnection):
inputs = butlerQC.get(inputRefs)
self.log.info("Processing %s", butlerQC.quantum.dataId)
expId, expBits = butlerQC.quantum.dataId.pack("visit_detector",
                                              returnMaxBits=True)
idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
if self.config.coaddName == 'dcr':
    templateExposures = inputRefs.dcrCoadds
else:
    templateExposures = inputRefs.coaddExposures
templateStruct = self.getTemplate.runQuantum(
    inputs['exposure'], butlerQC, inputRefs.skyMap, templateExposures
)

self.checkTemplateIsSufficient(templateStruct.exposure)

outputs = self.run(exposure=inputs['exposure'],
                   templateExposure=templateStruct.exposure,
                   idFactory=idFactory)
# Consistency with runDataref gen2 handling
if outputs.diaSources is None:
    del outputs.diaSources
butlerQC.put(outputs, outputRefs)

@timeMethod
def runDataRef(self, sensorRef, templateIdList=None):
subtractedExposureName = self.config.coaddName + "Diff_differenceExp"
subtractedExposure = None
selectSources = None
calexpBackgroundExposure = None
self.log.info("Processing %s", sensorRef.dataId)

# We make one IdFactory that will be used by both icSrc and src datasets;
# I don't know if this is the way we ultimately want to do things, but at least
# this ensures the source IDs are fully unique.
idFactory = self.makeIdFactory(expId=int(sensorRef.get("ccdExposureId")),
                               expBits=sensorRef.get("ccdExposureId_bits"))
if self.config.doAddCalexpBackground:
    calexpBackgroundExposure = sensorRef.get("calexpBackground")

# Retrieve the science image we wish to analyze
exposure = sensorRef.get("calexp", immediate=True)

# Retrieve the template image
template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)

if sensorRef.datasetExists("src"):
    self.log.info("Source selection via src product")
    # Sources already exist; for data release processing
    selectSources = sensorRef.get("src")

if not self.config.doSubtract and self.config.doDetection:
    # If we don't do subtraction, we need the subtracted exposure from the repo
    subtractedExposure = sensorRef.get(subtractedExposureName)
# Both doSubtract and doDetection cannot be False

results = self.run(exposure=exposure,
                   selectSources=selectSources,
                   templateExposure=template.exposure,
                   templateSources=template.sources,
                   idFactory=idFactory,
                   calexpBackgroundExposure=calexpBackgroundExposure,
                   subtractedExposure=subtractedExposure)

if self.config.doWriteSources and results.diaSources is not None:
    sensorRef.put(results.diaSources, self.config.coaddName + "Diff_diaSrc")
if self.config.doWriteWarpedExp:
    sensorRef.put(results.warpedExposure, self.config.coaddName + "Diff_warpedExp")
if self.config.doWriteMatchedExp:
    sensorRef.put(results.matchedExposure, self.config.coaddName + "Diff_matchedExp")
if self.config.doAddMetrics and self.config.doSelectSources:
    sensorRef.put(results.selectSources, self.config.coaddName + "Diff_kernelSrc")
if self.config.doWriteSubtractedExp:
    sensorRef.put(results.subtractedExposure, subtractedExposureName)
if self.config.doWriteScoreExp:
    sensorRef.put(results.scoreExposure, self.config.coaddName + "Diff_scoreExp")
return results

@timeMethod
def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
    idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
subtractRes = None
controlSources = None
subtractedExposure = None
scoreExposure = None
diaSources = None
kernelSources = None
# We'll clone exposure if modified but will still need the original
exposureOrig = exposure

if self.config.doAddCalexpBackground:
    mi = exposure.getMaskedImage()
    mi += calexpBackgroundExposure.getImage()

if not exposure.hasPsf():
    raise pipeBase.TaskError("Exposure has no psf")
sciencePsf = exposure.getPsf()

if self.config.doSubtract:
    if self.config.doScaleTemplateVariance:
        self.log.info("Rescaling template variance")
        templateVarFactor = self.scaleVariance.run(
            templateExposure.getMaskedImage())
        self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
        self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
    self.metadata.add("psfMatchingAlgorithm", self.config.subtract.name)

    if self.config.subtract.name == 'zogy':
        subtractRes = self.subtract.run(exposure, templateExposure, doWarping=True)
        scoreExposure = subtractRes.scoreExp
        subtractedExposure = subtractRes.diffExp
        subtractRes.subtractedExposure = subtractedExposure
        subtractRes.matchedExposure = None

    elif self.config.subtract.name == 'al':
        # compute scienceSigmaOrig: sigma of PSF of science image before pre-convolution
        # Just need a rough estimate; average positions are fine
        sciAvgPos = sciencePsf.getAveragePosition()
        scienceSigmaOrig = sciencePsf.computeShape(sciAvgPos).getDeterminantRadius()

        templatePsf = templateExposure.getPsf()
        templateAvgPos = templatePsf.getAveragePosition()
        templateSigma = templatePsf.computeShape(templateAvgPos).getDeterminantRadius()

        # if requested, convolve the science exposure with its PSF
        # (properly, this should be a cross-correlation, but our code does not yet support that)
        # compute scienceSigmaPost: sigma of science exposure with pre-convolution, if done,
        # else sigma of original science exposure
        # TODO: DM-22762 This functional block should be moved into its own method
        preConvPsf = None
        if self.config.useScoreImageDetection:
            self.log.warning("AL likelihood image: pre-convolution of PSF is not implemented.")
            convControl = afwMath.ConvolutionControl()
            # cannot convolve in place, so need a new image anyway
            srcMI = exposure.maskedImage
            exposure = exposure.clone()  # New deep copy
            srcPsf = sciencePsf
            if self.config.useGaussianForPreConvolution:
                self.log.info(
                    "AL likelihood image: Using Gaussian (sigma=%.2f) PSF estimation "
                    "for science image pre-convolution", scienceSigmaOrig)
                # convolve with a simplified PSF model: a double Gaussian
                kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
                preConvPsf = SingleGaussianPsf(kWidth, kHeight, scienceSigmaOrig)
            else:
                # convolve with science exposure's PSF model
                self.log.info(
                    "AL likelihood image: Using the science image PSF for pre-convolution.")
                preConvPsf = srcPsf
            afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
            scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
        else:
            scienceSigmaPost = scienceSigmaOrig

        # If requested, find and select sources from the image
        # else, AL subtraction will do its own source detection
        # TODO: DM-22762 This functional block should be moved into its own method
        if self.config.doSelectSources:
            if selectSources is None:
                self.log.warning("Src product does not exist; running detection, measurement,"
                                 " selection")
                # Run own detection and measurement; necessary in nightly processing
                selectSources = self.subtract.getSelectSources(
                    exposure,
                    sigma=scienceSigmaPost,
                    doSmooth=not self.config.useScoreImageDetection,
                    idFactory=idFactory,
                )

            if self.config.doAddMetrics:
                # Number of basis functions

                nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
                                                 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
                                                 targetFwhmPix=templateSigma*FwhmPerSigma))
                # Modify the schema of all Sources
                # DEPRECATED: This is a data dependent (nparam) output product schema
                # outside the task constructor.
                # NOTE: The pre-determination of nparam at this point
                # may be incorrect as the template psf is warped later in
                # ImagePsfMatchTask.matchExposures()
                kcQa = KernelCandidateQa(nparam)
                selectSources = kcQa.addToSchema(selectSources)
            if self.config.kernelSourcesFromRef:
                # match exposure sources to reference catalog
                astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
                matches = astromRet.matches
            elif templateSources:
                # match exposure sources to template sources
                mc = afwTable.MatchControl()
                mc.findOnlyClosest = False
                matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*geom.arcseconds,
                                              mc)
            else:
                raise RuntimeError("doSelectSources=True and kernelSourcesFromRef=False,"
                                   "but template sources not available. Cannot match science "
                                   "sources with template sources. Run process* on data from "
                                   "which templates are built.")

            kernelSources = self.sourceSelector.run(selectSources, exposure=exposure,
                                                    matches=matches).sourceCat
            random.shuffle(kernelSources, random.random)
            controlSources = kernelSources[::self.config.controlStepSize]
            kernelSources = [k for i, k in enumerate(kernelSources)
                             if i % self.config.controlStepSize]

            if self.config.doSelectDcrCatalog:
                redSelector = DiaCatalogSourceSelectorTask(
                    DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
                                                   grMax=99.999))
                redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
                controlSources.extend(redSources)

                blueSelector = DiaCatalogSourceSelectorTask(
                    DiaCatalogSourceSelectorConfig(grMin=-99.999,
                                                   grMax=self.sourceSelector.config.grMin))
                blueSources = blueSelector.selectStars(exposure, selectSources,
                                                       matches=matches).starCat
                controlSources.extend(blueSources)

            if self.config.doSelectVariableCatalog:
                varSelector = DiaCatalogSourceSelectorTask(
                    DiaCatalogSourceSelectorConfig(includeVariable=True))
                varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
                controlSources.extend(varSources)

            self.log.info("Selected %d / %d sources for Psf matching (%d for control sample)",
                          len(kernelSources), len(selectSources), len(controlSources))

        allresids = {}
        # TODO: DM-22762 This functional block should be moved into its own method
        if self.config.doUseRegister:
            self.log.info("Registering images")

            if templateSources is None:
                # Run detection on the template, which is
                # temporarily background-subtracted
                # sigma of PSF of template image before warping
                templateSources = self.subtract.getSelectSources(
                    templateExposure,
                    sigma=templateSigma,
                    doSmooth=True,
                    idFactory=idFactory
                )

            # Third step: we need to fit the relative astrometry.
            #
            wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
            warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
                                                   exposure.getWcs(), exposure.getBBox())
            templateExposure = warpedExp

            # Create debugging outputs on the astrometric
            # residuals as a function of position.  Persistence
            # not yet implemented; expected on (I believe) #2636.
            if self.config.doDebugRegister:
                # Grab matches to reference catalog
                srcToMatch = {x.second.getId(): x.first for x in matches}

                refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
                inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
                sids = [m.first.getId() for m in wcsResults.matches]
                positions = [m.first.get(refCoordKey) for m in wcsResults.matches]
                residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
                    m.second.get(inCentroidKey))) for m in wcsResults.matches]
                allresids = dict(zip(sids, zip(positions, residuals)))

                cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
                    wcsResults.wcs.pixelToSky(
                        m.second.get(inCentroidKey))) for m in wcsResults.matches]
                colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get("g"))
                                      + 2.5*numpy.log10(srcToMatch[x].get("r"))
                                      for x in sids if x in srcToMatch.keys()])
                dlong = numpy.array([r[0].asArcseconds() for s, r in zip(sids, cresiduals)
                                     if s in srcToMatch.keys()])
                dlat = numpy.array([r[1].asArcseconds() for s, r in zip(sids, cresiduals)
                                    if s in srcToMatch.keys()])
                idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
                idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
                                   & (colors <= self.sourceSelector.config.grMax))
                idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
                rms1Long = IqrToSigma*(
                    (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
                rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
                                      - numpy.percentile(dlat[idx1], 25))
                rms2Long = IqrToSigma*(
                    (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
                rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
                                      - numpy.percentile(dlat[idx2], 25))
                rms3Long = IqrToSigma*(
                    (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
                rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
                                      - numpy.percentile(dlat[idx3], 25))
                self.log.info("Blue star offsets'': %.3f %.3f, %.3f %.3f",
                              numpy.median(dlong[idx1]), rms1Long,
                              numpy.median(dlat[idx1]), rms1Lat)
                self.log.info("Green star offsets'': %.3f %.3f, %.3f %.3f",
                              numpy.median(dlong[idx2]), rms2Long,
                              numpy.median(dlat[idx2]), rms2Lat)
                self.log.info("Red star offsets'': %.3f %.3f, %.3f %.3f",
                              numpy.median(dlong[idx3]), rms3Long,
                              numpy.median(dlat[idx3]), rms3Lat)

                self.metadata.add("RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
                self.metadata.add("RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
                self.metadata.add("RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
                self.metadata.add("RegisterBlueLongOffsetStd", rms1Long)
                self.metadata.add("RegisterGreenLongOffsetStd", rms2Long)
                self.metadata.add("RegisterRedLongOffsetStd", rms3Long)

                self.metadata.add("RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
                self.metadata.add("RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
                self.metadata.add("RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
                self.metadata.add("RegisterBlueLatOffsetStd", rms1Lat)
                self.metadata.add("RegisterGreenLatOffsetStd", rms2Lat)
                self.metadata.add("RegisterRedLatOffsetStd", rms3Lat)

        # warp template exposure to match exposure,
        # PSF match template exposure to exposure,
        # then return the difference

        # Return warped template...  Construct sourceKernelCand list after subtract
        self.log.info("Subtracting images")
        subtractRes = self.subtract.subtractExposures(
            templateExposure=templateExposure,
            scienceExposure=exposure,
            candidateList=kernelSources,
            convolveTemplate=self.config.convolveTemplate,
            doWarping=not self.config.doUseRegister
        )
        if self.config.useScoreImageDetection:
            scoreExposure = subtractRes.subtractedExposure
        else:
            subtractedExposure = subtractRes.subtractedExposure

        if self.config.doDetection:
            self.log.info("Computing diffim PSF")

            # Get Psf from the appropriate input image if it doesn't exist
            if subtractedExposure is not None and not subtractedExposure.hasPsf():
                if self.config.convolveTemplate:
                    subtractedExposure.setPsf(exposure.getPsf())
                else:
                    subtractedExposure.setPsf(templateExposure.getPsf())

        # If doSubtract is False, then subtractedExposure was fetched from disk (above),
        # thus it may have already been decorrelated. Thus, we do not decorrelate if
        # doSubtract is False.

        # NOTE: At this point doSubtract == True
        if self.config.doDecorrelation and self.config.doSubtract:
            preConvKernel = None
            if self.config.useGaussianForPreConvolution:
                preConvKernel = preConvPsf.getLocalKernel()
            if self.config.useScoreImageDetection:
                scoreExposure = self.decorrelate.run(exposureOrig, subtractRes.warpedExposure,
                                                     scoreExposure,
                                                     subtractRes.psfMatchingKernel,
                                                     spatiallyVarying=self.config.doSpatiallyVarying,
                                                     preConvKernel=preConvKernel,
                                                     templateMatched=True,
                                                     preConvMode=True).correctedExposure
            # Note that the subtracted exposure is always decorrelated,
            # even if the score image is used for detection
            subtractedExposure = self.decorrelate.run(exposureOrig, subtractRes.warpedExposure,
                                                      subtractedExposure,
                                                      subtractRes.psfMatchingKernel,
                                                      spatiallyVarying=self.config.doSpatiallyVarying,
                                                      preConvKernel=None,
                                                      templateMatched=self.config.convolveTemplate,
                                                      preConvMode=False).correctedExposure
    # END (if subtractAlgorithm == 'AL')
# END (if self.config.doSubtract)
if self.config.doDetection:
    self.log.info("Running diaSource detection")

    # subtractedExposure - reserved for task return value
    # in zogy, it is always the proper difference image
    # in AL, it may be (yet) pre-convolved and/or decorrelated
    #
    # detectionExposure - controls which exposure to use for detection
    # in-place modifications will appear in task return
    if self.config.useScoreImageDetection:
        # zogy with score image detection enabled
        self.log.info("Detection, diffim rescaling and measurements are "
                      "on AL likelihood or Zogy score image.")
        detectionExposure = scoreExposure
    else:
        # AL or zogy with no score image detection
        detectionExposure = subtractedExposure

    # Rescale difference image variance plane
    if self.config.doScaleDiffimVariance:
        self.log.info("Rescaling diffim variance")
        diffimVarFactor = self.scaleVariance.run(detectionExposure.getMaskedImage())
        self.log.info("Diffim variance scaling factor: %.2f", diffimVarFactor)
        self.metadata.add("scaleDiffimVarianceFactor", diffimVarFactor)

    # Erase existing detection mask planes
    mask = detectionExposure.getMaskedImage().getMask()
    mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))

    table = afwTable.SourceTable.make(self.schema, idFactory)
    table.setMetadata(self.algMetadata)
    results = self.detection.run(
        table=table,
        exposure=detectionExposure,
        doSmooth=not self.config.useScoreImageDetection
    )

    if self.config.doMerge:
        fpSet = results.fpSets.positive
        fpSet.merge(results.fpSets.negative, self.config.growFootprint,
                    self.config.growFootprint, False)
        diaSources = afwTable.SourceCatalog(table)
        fpSet.makeSources(diaSources)
        self.log.info("Merging detections into %d sources", len(diaSources))
    else:
        diaSources = results.sources
    # Inject skySources before measurement.
    if self.config.doSkySources:
        skySourceFootprints = self.skySources.run(
            mask=detectionExposure.mask,
            seed=detectionExposure.info.id)
        if skySourceFootprints:
            for foot in skySourceFootprints:
                s = diaSources.addNew()
                s.setFootprint(foot)
                s.set(self.skySourceKey, True)

    if self.config.doMeasurement:
        newDipoleFitting = self.config.doDipoleFitting
        self.log.info("Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
        if not newDipoleFitting:
            # Just fit dipole in diffim
            self.measurement.run(diaSources, detectionExposure)
        else:
            # Use (matched) template and science image (if avail.) to constrain dipole fitting
            if self.config.doSubtract and 'matchedExposure' in subtractRes.getDict():
                self.measurement.run(diaSources, detectionExposure, exposure,
                                     subtractRes.matchedExposure)
            else:
                self.measurement.run(diaSources, detectionExposure, exposure)
        if self.config.doApCorr:
            self.applyApCorr.run(
                catalog=diaSources,
                apCorrMap=detectionExposure.getInfo().getApCorrMap()
            )

    if self.config.doForcedMeasurement:
        # Run forced psf photometry on the PVI at the diaSource locations.
        # Copy the measured flux and error into the diaSource.
        forcedSources = self.forcedMeasurement.generateMeasCat(
            exposure, diaSources, detectionExposure.getWcs())
        self.forcedMeasurement.run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
        mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
        mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFlux")[0],
                          "ip_diffim_forced_PsfFlux_instFlux", True)
        mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFluxErr")[0],
                          "ip_diffim_forced_PsfFlux_instFluxErr", True)
        mapper.addMapping(forcedSources.schema.find("base_PsfFlux_area")[0],
                          "ip_diffim_forced_PsfFlux_area", True)
        mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag")[0],
                          "ip_diffim_forced_PsfFlux_flag", True)
        mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_noGoodPixels")[0],
                          "ip_diffim_forced_PsfFlux_flag_noGoodPixels", True)
        mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_edge")[0],
                          "ip_diffim_forced_PsfFlux_flag_edge", True)
        for diaSource, forcedSource in zip(diaSources, forcedSources):
            diaSource.assign(forcedSource, mapper)

    # Match with the calexp sources if possible
    if self.config.doMatchSources:
        if selectSources is not None:
            # Create key,val pair where key=diaSourceId and val=sourceId
            matchRadAsec = self.config.diaSourceMatchRadius
            matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()

            srcMatches = afwTable.matchXy(selectSources, diaSources, matchRadPixel)
            srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId()) for
                                 srcMatch in srcMatches])
            self.log.info("Matched %d / %d diaSources to sources",
                          len(srcMatchDict), len(diaSources))
        else:
            self.log.warning("Src product does not exist; cannot match with diaSources")
            srcMatchDict = {}

        # Create key,val pair where key=diaSourceId and val=refId
        refAstromConfig = AstrometryConfig()
        refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
        refAstrometer = AstrometryTask(refAstromConfig)
        astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
        refMatches = astromRet.matches
        if refMatches is None:
            self.log.warning("No diaSource matches with reference catalog")
            refMatchDict = {}
        else:
            self.log.info("Matched %d / %d diaSources to reference catalog",
                          len(refMatches), len(diaSources))
            refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId()) for
                                 refMatch in refMatches])

        # Assign source Ids
        for diaSource in diaSources:
            sid = diaSource.getId()
            if sid in srcMatchDict:
                diaSource.set("srcMatchId", srcMatchDict[sid])
            if sid in refMatchDict:
                diaSource.set("refMatchId", refMatchDict[sid])

    if self.config.doAddMetrics and self.config.doSelectSources:
        self.log.info("Evaluating metrics and control sample")

        kernelCandList = []
        for cell in subtractRes.kernelCellSet.getCellList():
            for cand in cell.begin(False):  # include bad candidates
                kernelCandList.append(cand)

        # Get basis list to build control sample kernels
        basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
        nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())

        controlCandList = (
            diffimTools.sourceTableToCandidateList(controlSources,
                                                   subtractRes.warpedExposure, exposure,
                                                   self.config.subtract.kernel.active,
                                                   self.config.subtract.kernel.active.detectionConfig,
                                                   self.log, doBuild=True, basisList=basisList))

        KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
                                subtractRes.backgroundModel, dof=nparam)
        KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
                                subtractRes.backgroundModel)

        if self.config.doDetection:
            KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
        else:
            KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)

self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
return pipeBase.Struct(
    subtractedExposure=subtractedExposure,
    scoreExposure=scoreExposure,
    warpedExposure=subtractRes.warpedExposure,
    matchedExposure=subtractRes.matchedExposure,
    subtractRes=subtractRes,
    diaSources=diaSources,
    selectSources=selectSources
)

def fitAstrometry(self, templateSources, templateExposure, selectSources):

Definition at line 1339 of file imageDifference.py.


The documentation for this class was generated from the following file: