Coverage for tests/test_inject_engine.py: 97%
83 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 02:51 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 02:51 -0700
1# This file is part of source_injection.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import logging
23import unittest
24from types import GeneratorType
26import galsim
27import numpy as np
28from galsim import BoundsI, GSObject
30import lsst.utils.tests
31from lsst.geom import Point2D, SpherePoint, degrees
32from lsst.source.injection.inject_engine import (
33 generate_galsim_objects,
34 get_gain_map,
35 infer_gain_from_image,
36 inject_galsim_objects_into_exposure,
37 make_galsim_object,
38)
39from lsst.source.injection.utils.test_utils import make_test_exposure, make_test_injection_catalog
40from lsst.utils.tests import TestCase
43class InjectEngineTestCase(TestCase):
44 """Test the inject_engine.py module."""
46 def setUp(self):
47 """Set up synthetic source injection inputs.
49 This method sets up a noisy synthetic image with Gaussian PSFs injected
50 into the frame, an example source injection catalog, and a generator of
51 GalSim objects intended for injection.
52 """
53 self.exposure = make_test_exposure()
54 self.injection_catalog = make_test_injection_catalog(
55 self.exposure.getWcs(),
56 self.exposure.getBBox(),
57 )
58 self.galsim_objects = generate_galsim_objects(
59 injection_catalog=self.injection_catalog,
60 photo_calib=self.exposure.photoCalib,
61 wcs=self.exposure.wcs,
62 fits_alignment="wcs",
63 stamp_prefix="",
64 )
66 def tearDown(self):
67 del self.exposure
68 del self.injection_catalog
69 del self.galsim_objects
71 def test_make_galsim_object(self):
72 source_data = self.injection_catalog[0]
73 sky_coords = SpherePoint(float(source_data["ra"]), float(source_data["dec"]), degrees)
74 pixel_coords = self.exposure.wcs.skyToPixel(sky_coords)
75 inst_flux = self.exposure.photoCalib.magnitudeToInstFlux(source_data["mag"], pixel_coords)
76 object = make_galsim_object(
77 source_data=source_data,
78 source_type=source_data["source_type"],
79 inst_flux=inst_flux,
80 )
81 self.assertIsInstance(object, GSObject)
82 self.assertIsInstance(object, getattr(galsim, source_data["source_type"]))
84 def test_generate_galsim_objects(self):
85 self.assertTrue(isinstance(self.galsim_objects, GeneratorType))
86 for galsim_object in self.galsim_objects:
87 self.assertIsInstance(galsim_object, tuple)
88 self.assertEqual(len(galsim_object), 4)
89 self.assertIsInstance(galsim_object[0], SpherePoint) # RA/Dec
90 self.assertIsInstance(galsim_object[1], Point2D) # x/y
91 self.assertIsInstance(galsim_object[2], int) # draw size
92 self.assertIsInstance(galsim_object[3], GSObject) # GSObject
94 def test_infer_gain_nonexistent_mask(self):
95 """Test that infer_gain_from_image returns a gain value and logs a
96 warning when provided with nonexistent mask plane names.
97 """
98 logger = logging.getLogger(__name__)
99 with self.assertLogs(logger, level="WARNING") as cm:
100 gain = infer_gain_from_image(
101 self.exposure,
102 bad_mask_names=["NONEXISTENT_MASK1", "NONEXISTENT_MASK2"],
103 logger=logger,
104 )
105 self.assertTrue(any("NONEXISTENT_MASK1" in msg for msg in cm.output))
106 self.assertIsInstance(gain, float)
107 self.assertTrue(np.isfinite(gain))
108 gain_no_mask = infer_gain_from_image(self.exposure, bad_mask_names=[])
109 self.assertAlmostEqual(gain, gain_no_mask)
111 def test_infer_gain_no_valid_pixels(self):
112 """Test that infer_gain_from_image returns NaN (rather than raising)
113 when a region has no valid pixels to fit.
114 """
115 logger = logging.getLogger(__name__)
116 # Every pixel flagged with a bad mask plane.
117 all_bad = make_test_exposure()
118 all_bad.mask.addMaskPlane("BAD")
119 all_bad.mask.array[:] = all_bad.mask.getPlaneBitMask("BAD")
120 with self.assertLogs(logger, level="WARNING"):
121 gain = infer_gain_from_image(all_bad, bad_mask_names=["BAD"], logger=logger)
122 self.assertTrue(np.isnan(gain))
124 # Every variance pixel non-finite.
125 all_nan = make_test_exposure()
126 all_nan.variance.array[:] = np.nan
127 self.assertTrue(np.isnan(infer_gain_from_image(all_nan, bad_mask_names=[])))
129 def test_get_gain_map_no_valid_pixels(self):
130 """Test that get_gain_map produces a finite, positive map even when no
131 region can be fit, falling back to unit gain.
132 """
133 all_bad = make_test_exposure()
134 all_bad.mask.addMaskPlane("BAD")
135 all_bad.mask.array[:] = all_bad.mask.getPlaneBitMask("BAD")
136 gain_map = get_gain_map(all_bad, bad_mask_names=["BAD"])
137 self.assertTrue(np.all(np.isfinite(gain_map.array)))
138 self.assertTrue(np.all(gain_map.array > 0))
140 def test_inject_galsim_objects_into_exposure(self):
141 flux0 = np.sum(self.exposure.image.array)
142 injected_outputs = inject_galsim_objects_into_exposure(
143 exposure=self.exposure,
144 objects=self.galsim_objects,
145 mask_plane_name="INJECTED",
146 calib_flux_radius=12.0,
147 draw_size_max=1000,
148 add_noise=False,
149 )
150 pc = self.exposure.getPhotoCalib()
151 inst_fluxes = [float(pc.magnitudeToInstFlux(mag)) for mag in self.injection_catalog["mag"]]
152 self.assertAlmostEqual(
153 np.sum(self.exposure.image.array) - flux0,
154 np.sum(inst_fluxes),
155 delta=0.00015 * np.sum(inst_fluxes),
156 )
157 self.assertEqual(len(injected_outputs[0]), len(self.injection_catalog["ra"]))
158 self.assertTrue(all(isinstance(injected_output, list) for injected_output in injected_outputs))
159 self.assertTrue(all(isinstance(item, int) for item in injected_outputs[0])) # draw sizes
160 self.assertTrue(all(isinstance(item, BoundsI) for item in injected_outputs[1])) # common bounds
161 self.assertTrue(all(isinstance(item, bool) for item in injected_outputs[2])) # FFT size errors
162 self.assertTrue(all(isinstance(item, bool) for item in injected_outputs[3])) # PSF compute errors
165class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
166 """Test memory usage of functions in this script."""
168 pass
171def setup_module(module):
172 """Configure pytest."""
173 lsst.utils.tests.init()
176if __name__ == "__main__": 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 lsst.utils.tests.init()
178 unittest.main()