Source injection with modified PSF

Hello,

I have used the tutorials to try injecting sources into a visit image and that works as expected. However, something I would like to try is injecting a source using a manually modified PSF. Essentially this would test the situation where the PSF produced from the pipelines is slightly incorrect, and how that would propagate into measurements. Currently, it seems to only be possible to pass a lsst.afw.detection.Psf object which I think is a wrapper over Piff if I understood the docs correctly. But I would really like to just pass a 2D array or some other simple object to be used by the injection system. Here’s a cutout of the code showing what I tried:

my_injection_catalog = Table(
    {
        'injection_id': [9999],
        'ra': [inject_ra_dec[0]],
        'dec': [inject_ra_dec[1]],
        "seed": ["123"],
        'source_type': ['Star'],
        'mag': [21.8],
    }
)

inject_config = VisitInjectConfig()

inject_task = VisitInjectTask(config=inject_config)

psf = visit_img.psf.clone()
sn_point = geom.Point2D(ij[0], ij[1])
psf_pix = psf.computeImage(sn_point)
psf_pix.array[10] = 0 # some modification to the PSF

injected_output = inject_task.run(
    injection_catalogs=my_injection_catalog,
    input_exposure=visit_img.clone(),
    psf=psf_pix,
    photo_calib=visit_img.photoCalib,
    wcs=visit_img.wcs,
)
injected_exposure = injected_output.output_exposure
injected_catalog = injected_output.output_catalog

Which didn’t work, it produced this error:

TypeError                                 Traceback (most recent call last)
Cell In[20], line 31
     28 psf_pix = psf.computeImage(sn_point)
     29 psf_pix.array[10] = 0 # some modification to the PSF
---> 31 injected_output = inject_task.run(
     32     injection_catalogs=my_injection_catalog,
     33     input_exposure=visit_img.clone(),
     34     psf=psf_pix,
     35     photo_calib=visit_img.photoCalib,
     36     wcs=visit_img.wcs,
     37 )
     38 injected_exposure = injected_output.output_exposure
     39 injected_catalog = injected_output.output_catalog

File /opt/lsst/software/stack/conda/envs/lsst-scipipe-10.1.0/share/eups/Linux64/source_injection/g373392b20a+78a5bceb3f/python/lsst/source/injection/inject_base.py:216, in BaseInjectTask.run(self, injection_catalogs, input_exposure, psf, photo_calib, wcs)
    214 original_photo_calib = input_exposure.getPhotoCalib()
    215 original_wcs = input_exposure.getWcs()
--> 216 input_exposure.setPsf(psf)
    217 input_exposure.setPhotoCalib(photo_calib)
    218 input_exposure.setWcs(wcs)

TypeError: setPsf(): incompatible function arguments. The following argument types are supported:
    1. (self: lsst.afw.image._exposure.ExposureF, psf: lsst.afw.detection.Psf) -> None

Invoked with: <lsst.afw.image._exposure.ExposureF object at 0x7db0270bd1b0>, lsst.afw.image._image.ImageD=[[-2.14661988e-04 -8.14682781e-05  1.94507816e-05  1.86314248e-04
...

If I have missed something obvious and there is a way to manually modify the injection PSF that would be great. Otherwise, perhaps this could be a feature to add for the next update?

Thanks!

There might be a way to modify the Piff model parameters since it is ultimately an interpolated pixel grid, but I don’t know off-hand how. You’ll likely want to construct a lsst.meas.algorithms.KernelPsf instead.

Ah, that might be what I’m looking for! Is there any documentation to describe how to use it? I checked the tutorials and didn’t find anything: Search - DP1
I also checked the py-api docs but that just had function names: KernelPsf — LSST Science Pipelines

The documentation for all C++ classes is in the doxygen pages. Unfortunately, there’s no easy way to have pybind11 convert these to Python docstrings, so they will remain separate for the foreseeable future.

C++ bound __init__ functions do still have auto-generated docstrings and throw errors if incorrect (kw)args are passed in, so in practice you can sometimes work out correct usage just by trying to construct bound classes in an interactive session.

Ok, I’ve run into a bit of a wall. Looking at the doxygen for KernelPsf it needs a Kernel and a Point2D. I tried just passing a 2D array as the kernel, that didn’t work. I tried doing kernel = visit_image.psf.computeKernelPsf(point) where I gave it a Point2D object, but that didn’t work either. I also tried importing lsst.afw.math.Kernel to make a kernel object, but even with the doxygen I was unable to instantiate that object. I just kept getting TypeError: lsst.afw.math._math.Kernel: No constructor defined!. So I’m not really sure how to construct the input that KernelPsf wants.

Hi Connor -

I can’t guarantee that this is the optimal way to do things, but the following works. (It’s just a tiny modification to what you were already trying.) After modifying the PSF array, you need to turn it back into a KernelPsf object.

from lsst.meas.algorithms import KernelPsf
from lsst.afw.math import FixedKernel

# get the PSF image
psf = visit_img.getPsf()
sn_point = geom.Point2D(300, 300)
psf_img = psf.computeImage(sn_point)

# modify pixels
psf_img.array[10] = 0.0

# renormalize
psf_img.array = psf_img.array/np.sum(psf_img.array)

# wrap in a kernel and then a Psf
kernel = FixedKernel(psf_img)
modified_psf = KernelPsf(kernel)

Passing this modified_psf to the injection task worked for me, and I can confirm that the images are different between using the unmodified and modified versions:

3 Likes

math.Kernel is a virtual base class, as indicated in the doxygen docs. As @jeffcarlin suggested, FixedKernel is the concrete class you’ll likely want to use.

@jeffcarlin that worked perfectly, thanks!