Issues with Image Subtraction

Hi everyone!

In DP0, I used the AlardLuptonSubtractTask to do image subtraction for a custom pipeline. I based it on the example in DP0 tutorial 14. However, it seems my code is not working anymore in DP1, and I can’t seem to figure out why. I also couldn’t find a DP1 tutorial on the RSP that covers image subtraction, so I was hoping I could find an answer here. Below is my old code (that worked in DP0 in 2024_42):

# Load data
calexp = butler.get('calexp', dataId=dataId)
template = butler.get('goodSeeingDiff_templateExp', dataId=dataId)
sources = butler.get('src', dataId=dataId)

# Do the subtraction 
config = AlardLuptonSubtractConfig()
config.sourceSelector.value.unresolved.name = 'base_ClassificationExtendedness_value'
alTask = AlardLuptonSubtractTask(config=config)
diff = alTask.run(template, science, sources)

To run with DP1, I changed the first few lines to

calexp = butler.get('visit_image', dataId=dataId)
template = butler.get('template_coadd', dataId=dataId)
sources= butler.get('source', dataId=dataId)

But when I then do alTask.run(template, science, sources), I get the following error:

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[22], line 4
      2 config.sourceSelector.value.unresolved.name = 'base_ClassificationExtendedness_value'
      3 alTask = AlardLuptonSubtractTask(config=config)
----> 4 diff = alTask.run(template, calexp, sources_calexp)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3+fcb1d3bbc8/python/lsst/utils/timer.py:308, in timeMethod.<locals>.decorator_timer.<locals>.timeMethod_wrapper(self, *args, **keyArgs)
    299 logInfo(
    300     obj=self,
    301     prefix=func.__name__ + "Start",
   (...)
    305     stacklevel=stacklevel,
    306 )
    307 try:
--> 308     res = func(self, *args, **keyArgs)
    309 finally:
    310     logInfo(
    311         obj=self,
    312         prefix=func.__name__ + "End",
   (...)
    316         stacklevel=stacklevel,
    317     )

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:373, in AlardLuptonSubtractTask.run(self, template, science, sources, visitSummary)
    331 @timeMethod
    332 def run(self, template, science, sources, visitSummary=None):
    333     """PSF match, subtract, and decorrelate two images.
    334 
    335     Parameters
   (...)
    371         set, is less then the configured requiredTemplateFraction
    372     """
--> 373     self._prepareInputs(template, science, visitSummary=visitSummary)
    375     # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
    376     fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:954, in AlardLuptonSubtractTask._prepareInputs(self, template, science, visitSummary)
    938 def _prepareInputs(self, template, science, visitSummary=None):
    939     """Perform preparatory calculations common to all Alard&Lupton Tasks.
    940 
    941     Parameters
   (...)
    952         lookup.
    953     """
--> 954     self._validateExposures(template, science)
    955     if visitSummary is not None:
    956         self._applyExternalCalibrations(science, visitSummary=visitSummary)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:788, in AlardLuptonSubtractTask._validateExposures(template, science)
    769 @staticmethod
    770 def _validateExposures(template, science):
    771     """Check that the WCS of the two Exposures match, the template bbox
    772     contains the science bbox, and that the bands match.
    773 
   (...)
    786         bounding box, or if the bands do not match.
    787     """
--> 788     assert template.wcs == science.wcs, \
    789         "Template and science exposure WCS are not identical."
    790     templateBBox = template.getBBox()
    791     scienceBBox = science.getBBox()

AssertionError: Template and science exposure WCS are not identical.

This suggests that the template is no longer aligned with the calexp. After plotting both the calexp and template, this is indeed the case. I don’t remember that this was the case in DP0.2. Is there no longer a corresponding template for each calexp/visit_image? Are we supposed to align the templates ourselves now, or did I somehow load the template incorrectly?

I ignored this error for now by just duplicating the science exposure and injecting a source in the duplicate, and hoping to subtract those two. But I quickly ran into another error:

-----------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[26], line 4
      2 config.sourceSelector.value.unresolved.name = 'base_ClassificationExtendedness_value'
      3 alTask = AlardLuptonSubtractTask(config=config)
----> 4 diff = alTask.run(inj_calexp, calexp, sources_calexp)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3+fcb1d3bbc8/python/lsst/utils/timer.py:308, in timeMethod.<locals>.decorator_timer.<locals>.timeMethod_wrapper(self, *args, **keyArgs)
    299 logInfo(
    300     obj=self,
    301     prefix=func.__name__ + "Start",
   (...)
    305     stacklevel=stacklevel,
    306 )
    307 try:
--> 308     res = func(self, *args, **keyArgs)
    309 finally:
    310     logInfo(
    311         obj=self,
    312         prefix=func.__name__ + "End",
   (...)
    316         stacklevel=stacklevel,
    317     )

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:449, in AlardLuptonSubtractTask.run(self, template, science, sources, visitSummary)
    447 sourceMask = science.mask.clone()
    448 sourceMask.array |= template[science.getBBox()].mask.array
--> 449 selectSources = self._sourceSelector(sources, sourceMask)
    450 if convolveTemplate:
    451     self.metadata["convolvedExposure"] = "Template"

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:868, in AlardLuptonSubtractTask._sourceSelector(self, sources, mask)
    844 def _sourceSelector(self, sources, mask):
    845     """Select sources from a catalog that meet the selection criteria.
    846 
    847     Parameters
   (...)
    865         remaining after source selection.
    866     """
--> 868     selected = self.sourceSelector.selectSources(sources).selected
    869     nInitialSelected = np.count_nonzero(selected)
    870     selected *= self._checkMask(mask, sources, self.config.excludeMaskPlanes)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/meas_algorithms/g72a202582f+7a25662ef1/python/lsst/meas/algorithms/sourceSelector.py:720, in ScienceSourceSelectorTask.selectSources(self, sourceCat, matches, exposure)
    718     selected &= self.config.flags.apply(sourceCat)
    719 if self.config.doUnresolved:
--> 720     selected &= self.config.unresolved.apply(sourceCat)
    721 if self.config.doSignalToNoise:
    722     selected &= self.config.signalToNoise.apply(sourceCat)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/meas_algorithms/g72a202582f+7a25662ef1/python/lsst/meas/algorithms/sourceSelector.py:453, in RequireUnresolved.apply(self, catalog)
    437 def apply(self, catalog):
    438     """Apply the flag requirements to a catalog
    439 
    440     Returns whether the source is selected.
   (...)
    451         (True means selected).
    452     """
--> 453     value = catalog[self.name]
    454     return BaseLimit.apply(self, value)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/lib/python3.12/site-packages/astropy/table/table.py:2093, in Table.__getitem__(self, item)
   2091 def __getitem__(self, item):
   2092     if isinstance(item, str):
-> 2093         return self.columns[item]
   2094     elif isinstance(item, (int, np.integer)):
   2095         return self.Row(self, item)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/lib/python3.12/site-packages/astropy/table/table.py:264, in TableColumns.__getitem__(self, item)
    253 """Get items from a TableColumns object.
    254 
    255 ::
   (...)
    261   tc[1:3] # <TableColumns names=('b', 'c')>
    262 """
    263 if isinstance(item, str):
--> 264     return OrderedDict.__getitem__(self, item)
    265 elif isinstance(item, (int, np.integer)):
    266     return list(self.values())[item]

KeyError: 'base_ClassificationExtendedness_value'

I believe this error comes from the fact that sources is now a table, whereas before it was a SourceCatalog. Perhaps butler.get('source', dataId=dataId) is not the DP1 equivalent of butler.get('src', dataId=dataId)?

I would love to hear if anyone else has managed to get image subtraction to work. Any feedback or ideas to solve either of the two issues above is much appreciated :slight_smile: Thanks!

Cheers,
Tobias

Hi @TobiasGeron,

My apologies, we did not make an announcement or make it obvious anywhere yet that difference images cannot be recreated with the DP1 data.

You’ve already found that the template coadds that are available are not aligned with the visit images (previously called “calexps”). You’ve also found the next issue: it’s not possible to recreate difference images without also recreating intermediate tables. As I understand it, those tables are not trivial to reproduce and require addition inputs that are not served with DP1, and require advanced use of pipeline tasks.

It is for these reasons that difference images are being served as a data product for DP1. For future data releases, to save space, the difference images will not be stored. Instead, a tool for on-the-fly difference image recreation will be provided to users. But it is not ready yet.

This is probably the solution / answer to your main question – but I expect probably raises more. I’d like to hear a bit more about your science goals. What is your project, can it be continued with DP0? Could your goals be achieved by injection into difference images?

Hi Melissa,

Thanks for the response, I appreciate it!

Do you have any idea for when this on-the-fly difference image recreation tool will be ready? And will it work similarly to the AlardLuptonSubtractTask in DP0; i.e. can we inject something in the visit image, and run this to generate a new difference image?

Our science case is as follows: sometimes faint transients on top of bright background galaxies are hard to identify with typical subtraction pipelines and a SNR cutoff. E.g. these cases are often registered as detections due to the bright background galaxy, when in reality it is a false positive. To mitigate this, we wrote a pipeline that basically injects a SN with a known magnitude on top of the bright background galaxy in question, and then we do image subtraction and forced photometry to see whether we recover that injected SN. We repeat this until we have a better understanding of the “real” detection threshold for this specific image. We developed this in DP0 and it seem to work well there. However, for this to work we need to do image subtraction :slight_smile: And we were hoping to do this for targets in DP1.

Just so I understand completely: are you saying it is hard to recreate difference images with the DP1 data, or that it is impossible?

I had another go to solve the issues above. I realised that perhaps I could work around not having a SourceCatalog by measuring the sources in the visit image myself again, following tutorial 105.2. This worked fine, I only needed to add an additional column to the SourceCatalog:

isPrimary = schema.addField("detect_isPrimary", type = 'B')

But now I’m stuck again. After using this in the AlardLuptonSubtractTask, I get the following error and not sure how to handle it.

InvalidParameterError                     Traceback (most recent call last)
Cell In[43], line 4
      2 #config.sourceSelector.value.unresolved.name = 'base_ClassificationExtendedness_value'
      3 alTask = AlardLuptonSubtractTask(config=config)
----> 4 diff = alTask.run(inj_calexp, calexp, sources)

File [/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3](https://tobiasgeron.nb.data.lsst.cloud/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3)+fcb1d3bbc8[/python/lsst/utils/timer.py:308](https://tobiasgeron.nb.data.lsst.cloud/python/lsst/utils/timer.py#line=307), in timeMethod.<locals>.decorator_timer.<locals>.timeMethod_wrapper(self, *args, **keyArgs)
    299 logInfo(
    300     obj=self,
    301     prefix=func.__name__ + "Start",
   (...)
    305     stacklevel=stacklevel,
    306 )
    307 try:
--> 308     res = func(self, *args, **keyArgs)
    309 finally:
    310     logInfo(
    311         obj=self,
    312         prefix=func.__name__ + "End",
   (...)
    316         stacklevel=stacklevel,
    317     )

File [/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10](https://tobiasgeron.nb.data.lsst.cloud/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10)+8f7b08dc1c[/python/lsst/ip/diffim/subtractImages.py:373](https://tobiasgeron.nb.data.lsst.cloud/python/lsst/ip/diffim/subtractImages.py#line=372), in AlardLuptonSubtractTask.run(self, template, science, sources, visitSummary)
    331 @timeMethod
    332 def run(self, template, science, sources, visitSummary=None):
    333 """PSF match, subtract, and decorrelate two images.
    334 
    335     Parameters
   (...)
    371         set, is less then the configured requiredTemplateFraction
    372     """
--> 373     self._prepareInputs(template, science, visitSummary=visitSummary)
    375     # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
    376     fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer

File [/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10](https://tobiasgeron.nb.data.lsst.cloud/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10)+8f7b08dc1c[/python/lsst/ip/diffim/subtractImages.py:978](https://tobiasgeron.nb.data.lsst.cloud/python/lsst/ip/diffim/subtractImages.py#line=977), in AlardLuptonSubtractTask._prepareInputs(self, template, science, visitSummary)
    974     self.metadata["scaleScienceVarianceFactor"] = sciVarFactor
    976 # Erase existing detection mask planes.
    977 #  We don't want the detection mask from the science image
--> 978 self.updateMasks(template, science)

File [/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10](https://tobiasgeron.nb.data.lsst.cloud/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10)+8f7b08dc1c[/python/lsst/ip/diffim/subtractImages.py:1022](https://tobiasgeron.nb.data.lsst.cloud/python/lsst/ip/diffim/subtractImages.py#line=1021), in AlardLuptonSubtractTask.updateMasks(self, template, science)
   1020 for maskPlane in renameMaskPlanes:
   1021     self._renameMaskPlanes(template.mask, maskPlane, maskPlane + "_TEMPLATE")
-> 1022 self._clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)

File [/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10](https://tobiasgeron.nb.data.lsst.cloud/opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10)+8f7b08dc1c[/python/lsst/ip/diffim/subtractImages.py:1057](https://tobiasgeron.nb.data.lsst.cloud/python/lsst/ip/diffim/subtractImages.py#line=1056), in AlardLuptonSubtractTask._clearMask(self, mask, clearMaskPlanes)
   1054 if clearMaskPlanes is None:
   1055     clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
-> 1057 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
   1058 mask &= ~bitMaskToClear

InvalidParameterError: 
  File "src[/image/Mask.cc", line 358](https://tobiasgeron.nb.data.lsst.cloud/image/Mask.cc#line=357), in static int lsst::afw::image::Mask<MaskPixelT>::getMaskPlane(const std::string&) [with MaskPixelT = int; std::string = std::__cxx11::basic_string<char>]
    Invalid mask plane name: OTHERDET {0}
lsst::pex::exceptions::InvalidParameterError: 'Invalid mask plane name: OTHERDET'

This also doesn’t solve the misalignment of the template. Thanks again for your help!

Hi Tobias,

The error appears to be because your inj_calexp image has a mask plane with a name that is breaking the mask.getPlaneBitMask() method. Could you report back what the value is of list(inj_calexp .mask.getMaskPlaneDict().keys())?

To warp a coadd to use as a template, you will need to use GetTemplateTask in getTemplate.py . We have had to optimize that task for memory management, so it is not very user- or notebook-friendly. If you want to try to use it, look at the unit tests in test_getTemplate.py for examples. You will have two possible approaches: either wrap the input coadds in structures that have the same API as a Butler DeferredDatasetHandle, or warp the images yourself (see ip_diffim/tests/test_getTemplate.py at 41aed2133cd08b017255869e778fe26e4029ed63 · lsst/ip_diffim · GitHub for an example of how to do this).
Cheers,
Ian

Hi Ian,

Thanks! The output is:

 'CLIPPED',
 'CR',
 'CROSSTALK',
 'DETECTED',
 'DETECTED_NEGATIVE',
 'EDGE',
 'INEXACT_PSF',
 'INJECTED',
 'INJECTED_CORE',
 'INJECTED_TEMPLATE',
 'INTRP',
 'ITL_DIP',
 'NOT_DEBLENDED',
 'NO_DATA',
 'REJECTED',
 'SAT',
 'SAT_TEMPLATE',
 'SENSOR_EDGE',
 'STREAK',
 'SUSPECT',
 'UNMASKEDNAN',
 'VIGNETTED']

However I only created calexp_inj in order to work around the original problem of the template not being oriented correctly with respect to the visit image. In practise, I would actually want to do image subtraction between calexp_inj and the template. Is there any method I’m overlooking that helps with aligning the template back with the calexp?

Thanks!

Cheer,
Tobias

If inj_calexp is meant to be the science image, make sure you supply it second, not first. The API is diff = alTask.run(template, science, isolated_sources_for_psf_matching).

Warping the coadd to make a new template is more complicated, but please see the second part of my last message for two different approaches you could try.

I see! I looked at the examples you linked, and managed to write the following code:

config = afwMath.Warper.ConfigClass()
config.warpingKernelName = "lanczos5"
warper = afwMath.Warper.fromConfig(config)


bbox = calexp.getBBox()
xyTransform = afwGeom.makeWcsPairTransform(template.wcs,calexp.wcs)
warpedPsf = measAlgorithms.WarpedPsf(template.psf, xyTransform)
warped = warper.warpExposure(calexp.wcs, template, destBBox = bbox)
warped.setPsf(warpedPsf)

If I then plot warped, it is indeed oriented similarly to the calexp, so that worked as expected! I then do the following to convert it back to a template:

task = GetTemplateTask()

dataIds = {tract: [{'tract' : tract, 'patch' : patch, 'band' : band}]}
result = task.run(coaddExposures= {tract : [warped]},
                          bbox=bbox,
                          wcs=warped.wcs,
                          dataIds=dataIds,
                          physical_filter=band)

But this gives me the following error:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[49], line 4
      1 task = GetTemplateTask()
      3 dataIds = {tract: [{'tract' : tract, 'patch' : patch, 'band' : band}]}
----> 4 result = task.run(coaddExposures= {tract : [warped]},
      5                           bbox=bbox,
      6                           wcs=warped.wcs,
      7                           dataIds=dataIds,
      8                           physical_filter=band)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3+fcb1d3bbc8/python/lsst/utils/timer.py:308, in timeMethod.<locals>.decorator_timer.<locals>.timeMethod_wrapper(self, *args, **keyArgs)
    299 logInfo(
    300     obj=self,
    301     prefix=func.__name__ + "Start",
   (...)
    305     stacklevel=stacklevel,
    306 )
    307 try:
--> 308     res = func(self, *args, **keyArgs)
    309 finally:
    310     logInfo(
    311         obj=self,
    312         prefix=func.__name__ + "End",
   (...)
    316         stacklevel=stacklevel,
    317     )

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/getTemplate.py:278, in GetTemplateTask.run(self, coaddExposures, bbox, wcs, dataIds, physical_filter)
    275 maskedImages, catalog, totalBox = self._makeExposureCatalog(coaddExposures[tract],
    276                                                             dataIds[tract])
    277 # Combine images from individual patches together.
--> 278 unwarped = self._merge(maskedImages, totalBox, catalog[0].wcs)
    279 potentialInput = self.warper.warpExposure(wcs, unwarped, destBBox=bbox)
    280 if not np.any(np.isfinite(potentialInput.image.array)):

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/getTemplate.py:418, in GetTemplateTask._merge(maskedImages, bbox, wcs)
    416     maskedImage.image *= weight
    417     maskedImage.variance *= weight
--> 418     merged.maskedImage[maskedImage.getBBox()] += maskedImage
    419     weights[maskedImage.getBBox()] += weight
    421 inverseWeights = np.zeros_like(weights.array)

RuntimeError: 
  File "src/image/Mask.cc", line 497, in void lsst::afw::image::Mask<MaskPixelT>::checkMaskDictionaries(const lsst::afw::image::Mask<MaskPixelT>&) [with MaskPixelT = int]
    Mask dictionaries do not match {0}
lsst::pex::exceptions::RuntimeError: 'Mask dictionaries do not match'

Any further advice? I’m assuming I called task.run()incorrectly. I’m also not confident I structured dataIds correctly…

If the steps to create your warped image worked for you, then I recommend just using that as the template for subtractImages. Using the GetTemplateTask is an alternate way to create a template, that is both more capable but also more complicated. So, don’t do both!

Ah I see! I guess the only caveat is that I lose a large area of the sky with this method (see the image below). But I’m only interested in the overlapping part, so perhaps that is not an issue…

Using my warped template, and visit image, I then do the image subtraction as follows:

config = AlardLuptonSubtractConfig()
alTask = AlardLuptonSubtractTask(config=config)
diff = alTask.run(warped, calexp, sources)

But I get the same error as before:

---------------------------------------------------------------------------
InvalidParameterError                     Traceback (most recent call last)
Cell In[49], line 4
      2 config.sourceSelector.value.unresolved.name = 'base_ClassificationExtendedness_value'
      3 alTask = AlardLuptonSubtractTask(config=config)
----> 4 diff = alTask.run(warped, calexp, sources)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3+fcb1d3bbc8/python/lsst/utils/timer.py:308, in timeMethod.<locals>.decorator_timer.<locals>.timeMethod_wrapper(self, *args, **keyArgs)
    299 logInfo(
    300     obj=self,
    301     prefix=func.__name__ + "Start",
   (...)
    305     stacklevel=stacklevel,
    306 )
    307 try:
--> 308     res = func(self, *args, **keyArgs)
    309 finally:
    310     logInfo(
    311         obj=self,
    312         prefix=func.__name__ + "End",
   (...)
    316         stacklevel=stacklevel,
    317     )

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:373, in AlardLuptonSubtractTask.run(self, template, science, sources, visitSummary)
    331 @timeMethod
    332 def run(self, template, science, sources, visitSummary=None):
    333     """PSF match, subtract, and decorrelate two images.
    334 
    335     Parameters
   (...)
    371         set, is less then the configured requiredTemplateFraction
    372     """
--> 373     self._prepareInputs(template, science, visitSummary=visitSummary)
    375     # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
    376     fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:978, in AlardLuptonSubtractTask._prepareInputs(self, template, science, visitSummary)
    974     self.metadata["scaleScienceVarianceFactor"] = sciVarFactor
    976 # Erase existing detection mask planes.
    977 #  We don't want the detection mask from the science image
--> 978 self.updateMasks(template, science)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:1022, in AlardLuptonSubtractTask.updateMasks(self, template, science)
   1020 for maskPlane in renameMaskPlanes:
   1021     self._renameMaskPlanes(template.mask, maskPlane, maskPlane + "_TEMPLATE")
-> 1022 self._clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:1057, in AlardLuptonSubtractTask._clearMask(self, mask, clearMaskPlanes)
   1054 if clearMaskPlanes is None:
   1055     clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
-> 1057 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
   1058 mask &= ~bitMaskToClear

InvalidParameterError: 
  File "src/image/Mask.cc", line 358, in static int lsst::afw::image::Mask<MaskPixelT>::getMaskPlane(const std::string&) [with MaskPixelT = int; std::string = std::__cxx11::basic_string<char>]
    Invalid mask plane name: OTHERDET {0}
lsst::pex::exceptions::InvalidParameterError: 'Invalid mask plane name: OTHERDET'

The output of list(warped.mask.getMaskPlaneDict().keys()) is:

['BAD',
 'CLIPPED',
 'CR',
 'CROSSTALK',
 'DETECTED',
 'DETECTED_NEGATIVE',
 'EDGE',
 'INEXACT_PSF',
 'INJECTED',
 'INJECTED_TEMPLATE',
 'INTRP',
 'ITL_DIP',
 'NOT_DEBLENDED',
 'NO_DATA',
 'OTHERDET',
 'REJECTED',
 'SAT',
 'SAT_TEMPLATE',
 'SENSOR_EDGE',
 'STREAK',
 'SUSPECT',
 'THISDET',
 'UNMASKEDNAN',
 'VIGNETTED']

Let me know if you had any further thoughts on how to solve this… Thanks for your help, I really appreciate it!

Cheers,
Tobias

I am surprised by that error, and I will investigate that issue to see if there is a bug I need to fix. Otherwise, for now I think you will be best off clearing the problematic mask planes. There are a few ways to do this, but one is:

alTask._clearMask(warped, ["OTHERDET", "THISDET"])

If needed, repeat with any other mask planes that trigger the error, including if there are any on the science image.

Thanks! So when I do:

alTask._clearMask(warped.getMask(), ['OTHERDET', 'THISDET'])

I get the following error:

---------------------------------------------------------------------------
InvalidParameterError                     Traceback (most recent call last)
Cell In[145], line 1
----> 1 alTask._clearMask(warped.getMask(), ['OTHERDET', 'THISDET'])

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:1057, in AlardLuptonSubtractTask._clearMask(self, mask, clearMaskPlanes)
   1054 if clearMaskPlanes is None:
   1055     clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
-> 1057 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
   1058 mask &= ~bitMaskToClear

InvalidParameterError: 
  File "src/image/Mask.cc", line 358, in static int lsst::afw::image::Mask<MaskPixelT>::getMaskPlane(const std::string&) [with MaskPixelT = int; std::string = std::__cxx11::basic_string<char>]
    Invalid mask plane name: OTHERDET {0}
lsst::pex::exceptions::InvalidParameterError: 'Invalid mask plane name: OTHERDET'

I can use that line on other masks though; it only does not work on “OTHERDET” and “THISDET”. The same error occurs when I try to use it on the template and difference image. The calexp does not have these masks… But I double checked and “OTHERDET” and “THISDET” do indeed appear when I print out list(warped.mask.getMaskPlaneDict().keys()).

After debugging a bit more I realised that warped.mask.getPlaneBitMask('OTHERDET') and warped.mask.getMaskPlane('OTHERDET') and also do not work; they give both similar errors:

--------------------------------------------------------------------------- 
InvalidParameterError Traceback (most recent call last) 
Cell In[268], line 1 
----> 1 warped.mask.getPlaneBitMask('OTHERDET') 

InvalidParameterError: 
File "src[/image/Mask.cc", line 358](https://tobiasgeron.nb.data.lsst.cloud/image/Mask.cc#line=357), in static int lsst::afw::image::Mask<MaskPixelT>::getMaskPlane(const std::string&) [with MaskPixelT = int; std::string = std::__cxx11::basic_string<char>] 
Invalid mask plane name: OTHERDET {0} 
lsst::pex::exceptions::InvalidParameterError: 'Invalid mask plane name: OTHERDET'

I tried getting around this by finding the bitmask and clearing the mask myself by doing (basically following def_clearMask() in subtractImages.py):

bit_ind = warped.mask.getMaskPlaneDict()['OTHERDET'] #returns 23
bitmask = 1 << bit_ind #returns 8388608
warped.mask &= ~bitmask

which does not produce any errors. However, when I then run diff = alTask.run(warped, calexp, sources) again, it gives me the same error as before:

InvalidParameterError: 
  File "src/image/Mask.cc", line 358, in static int lsst::afw::image::Mask<MaskPixelT>::getMaskPlane(const std::string&) [with MaskPixelT = int; std::string = std::__cxx11::basic_string<char>]
    Invalid mask plane name: OTHERDET {0}
lsst::pex::exceptions::InvalidParameterError: 'Invalid mask plane name: OTHERDET'

I’m starting to wonder whether clearing these masks is not enough, and if they should just not exist at all? Not sure if this is relevant, but the bit indices of OTHERDET and THISDET are also the highest, i.e. the output of warped.mask.getMaskPlaneDict() is:

{'BAD': 0,
 'CLIPPED': 11,
 'CR': 3,
 'CROSSTALK': 12,
 'DETECTED': 5,
 'DETECTED_NEGATIVE': 6,
 'EDGE': 4,
 'INEXACT_PSF': 13,
 'INJECTED': 14,
 'INJECTED_TEMPLATE': 15,
 'INTRP': 2,
 'ITL_DIP': 16,
 'NOT_DEBLENDED': 17,
 'NO_DATA': 8,
 'OTHERDET': 23,
 'REJECTED': 18,
 'SAT': 1,
 'SAT_TEMPLATE': 19,
 'SENSOR_EDGE': 20,
 'STREAK': 10,
 'SUSPECT': 7,
 'THISDET': 22,
 'UNMASKEDNAN': 21,
 'VIGNETTED': 9}

Again, I really appreciate your input! I feel like we’re slowly getting to the root of the problem.

Never mind – I think I figured it out, although it lead me to another issue. I noticed that warped does not have these mask planes from the start, it only gets them after I (unsuccessfully) run diff = alTask.run(warped, calexp, sources). So I went back to how I obtained the sources catalog, and realised it didn’t like how I added extra “detect_isPrimary” field (see a few posts up). After changing it (from type="B" to type="F"), it threw another error that sources also did not have a sky_source field, so I added that as well. My full code to obtain sources now is (mostly copy-pasted from tutorial 105.2):

# Initialize schema
calexp.mask.removeAndClearMaskPlane('DETECTED')
schema = afwTable.SourceTable.makeMinimalSchema()
raerr = schema.addField("coord_raErr", type="F")
decerr = schema.addField("coord_decErr", type="F")
isPrimary = schema.addField("detect_isPrimary", type = 'F') #This sets it all to NaN though.
skySource = schema.addField("sky_source", type = 'F') #This sets it all to NaN though.
algMetadata = dafBase.PropertyList()

# Setup configs and tasks
config = SourceDetectionTask.ConfigClass()
config.thresholdValue = 5
config.thresholdType = "stdev"
sourceDetectionTask = SourceDetectionTask(schema=schema, config=config)
sourceDeblendTask = SourceDeblendTask(schema=schema)
config = SingleFrameMeasurementTask.ConfigClass()
sourceMeasurementTask = SingleFrameMeasurementTask(schema=schema, config=config, algMetadata=algMetadata)
tab = afwTable.SourceTable.make(schema)

# Run tasks
result = sourceDetectionTask.run(tab, calexp)
sources = result.sources
sourceDeblendTask.run(calexp, sources)
sourceMeasurementTask.run(measCat=sources, exposure=calexp)
sources = sources.copy(True)

However, if I now run the subtraction code:

config = AlardLuptonSubtractConfig()
alTask = AlardLuptonSubtractTask(config=config)
diff = alTask.run(warped, calexp, sources)

I get a new error:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[120], line 7
      3 alTask = AlardLuptonSubtractTask(config=config)
      5 #alTask._clearMask(warped, ["OTHERDET", "THISDET"])
----> 7 diff = alTask.run(warped, calexp, sources)

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/utils/g8fa50ef3f3+fcb1d3bbc8/python/lsst/utils/timer.py:308, in timeMethod.<locals>.decorator_timer.<locals>.timeMethod_wrapper(self, *args, **keyArgs)
    299 logInfo(
    300     obj=self,
    301     prefix=func.__name__ + "Start",
   (...)
    305     stacklevel=stacklevel,
    306 )
    307 try:
--> 308     res = func(self, *args, **keyArgs)
    309 finally:
    310     logInfo(
    311         obj=self,
    312         prefix=func.__name__ + "End",
   (...)
    316         stacklevel=stacklevel,
    317     )

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:467, in AlardLuptonSubtractTask.run(self, template, science, sources, visitSummary)
    464     #  checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
    465     raise e
--> 467 self.computeImageMetrics(science, subtractResults.difference, sources)
    469 return subtractResults

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/ip_diffim/g8b8da53e10+8f7b08dc1c/python/lsst/ip/diffim/subtractImages.py:529, in AlardLuptonSubtractTask.computeImageMetrics(self, science, difference, stars)
    526     return science_footprints, difference_footprints, ratio
    528 sky = stars["sky_source"]
--> 529 sky_science, sky_difference, sky_ratio = footprint_mean(stars[sky])
    530 science_footprints, difference_footprints, ratio = footprint_mean(stars[~sky], sky_difference.mean())
    532 self.metadata["differenceFootprintRatioMean"] = ratio.mean()

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.0.0/share/eups/Linux64/afw/g88f4e072da+7005418d1d/python/lsst/afw/table/_base.py:103, in Catalog.__getitem__(self, key)
    101     if key.dtype == bool:
    102         return self.subset(key)
--> 103     raise RuntimeError(f"Unsupported array type for indexing non-contiguous Catalog: {key.dtype}")
    104 elif isinstance(key, str):
    105     key = self.schema.find(key).key

RuntimeError: Unsupported array type for indexing non-contiguous Catalog: float32

I assume this is because of how I setup the “sky_source” field, as it is just all NaNs. I will keep digging, but any advice is appreciated :slight_smile: