Coverage for tests/test_component.py: 18%
124 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-07 01:40 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-07 01:40 -0800
1# This file is part of scarlet_lite.
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/>.
22from __future__ import annotations
24from typing import Callable
26import numpy as np
27from lsst.scarlet.lite import Box, Image, Parameter
28from lsst.scarlet.lite.component import (
29 Component,
30 FactorizedComponent,
31 default_adaprox_parameterization,
32 default_fista_parameterization,
33)
34from lsst.scarlet.lite.operators import Monotonicity
35from numpy.testing import assert_almost_equal, assert_array_equal
36from utils import ScarletTestCase
39class DummyComponent(Component):
40 def resize(self) -> bool:
41 pass
43 def update(self, it: int, input_grad: np.ndarray):
44 pass
46 def get_model(self) -> Image:
47 pass
49 def parameterize(self, parameterization: Callable) -> None:
50 parameterization(self)
52 def to_data(self) -> DummyComponent:
53 pass
56class TestFactorizedComponent(ScarletTestCase):
57 def setUp(self) -> None:
58 spectrum = np.arange(3).astype(np.float32)
59 morph = np.arange(20).reshape(4, 5).astype(np.float32)
60 bands = ("g", "r", "i")
61 bbox = Box((4, 5), (22, 31))
62 self.model_box = Box((100, 100))
63 center = (24, 33)
65 self.component = FactorizedComponent(
66 bands,
67 spectrum,
68 morph,
69 bbox,
70 center,
71 )
73 self.bands = bands
74 self.spectrum = spectrum
75 self.morph = morph
76 self.full_shape = (3, 100, 100)
78 def test_constructor(self):
79 # Test with only required parameters
80 component = FactorizedComponent(
81 self.bands,
82 self.spectrum,
83 self.morph,
84 self.component.bbox,
85 )
87 self.assertIsInstance(component._spectrum, Parameter)
88 assert_array_equal(component.spectrum, self.spectrum)
89 self.assertIsInstance(component._morph, Parameter)
90 assert_array_equal(component.morph, self.morph)
91 self.assertBoxEqual(component.bbox, self.component.bbox)
92 self.assertIsNone(component.peak)
93 self.assertIsNone(component.bg_rms)
94 self.assertEqual(component.bg_thresh, 0.25)
95 self.assertEqual(component.floor, 1e-20)
96 self.assertTupleEqual(component.shape, (3, 4, 5))
98 # Test that parameters are passed through
99 center = self.component.peak
100 bg_rms = np.arange(5) / 10
101 bg_thresh = 0.9
102 floor = 1e-10
104 component = FactorizedComponent(
105 self.bands,
106 self.spectrum,
107 self.morph,
108 self.component.bbox,
109 center,
110 bg_rms,
111 bg_thresh,
112 floor,
113 )
115 self.assertTupleEqual(component.peak, center)
116 assert_array_equal(component.bg_rms, bg_rms) # type: ignore
117 self.assertEqual(component.bg_thresh, bg_thresh)
118 self.assertEqual(component.floor, floor)
119 self.assertEqual(component.get_model().dtype, np.float32)
121 def test_get_model(self):
122 component = self.component
123 assert_array_equal(component.get_model(), self.spectrum[:, None, None] * self.morph[None, :, :])
125 # Insert component into a larger model
126 full_model = np.zeros(self.full_shape)
127 full_model[:, 22:26, 31:36] = self.spectrum[:, None, None] * self.morph[None, :, :]
129 test_model = Image(np.zeros(self.full_shape), bands=self.bands)
130 test_model += component.get_model()
132 assert_array_equal(test_model.data, full_model)
134 def test_gradients(self):
135 component = self.component
136 morph = self.morph
137 spectrum = self.spectrum
139 input_grad = np.array([morph, 2 * morph, 3 * morph])
140 true_spectrum_grad = np.array(
141 [
142 np.sum(morph**2),
143 np.sum(2 * morph**2),
144 np.sum(3 * morph**2),
145 ]
146 )
147 assert_almost_equal(component.grad_spectrum(input_grad, spectrum, morph), true_spectrum_grad)
149 true_morph_grad = np.sum(input_grad * spectrum[:, None, None], axis=0)
150 assert_almost_equal(component.grad_morph(input_grad, morph, spectrum), true_morph_grad)
152 def test_proximal_operators(self):
153 # Test spectrum positivity, morph threshold, and monotonicity
154 spectrum = np.array([-1, 2, 3], dtype=float)
155 morph = np.array([[10, 2, 1], [1, 5, 3], [0.1, 4, -1]], dtype=float)
156 bbox = Box((3, 3), (10, 10))
157 morph_bbox = Box((100, 100))
158 center = (11, 11)
159 monotonicity = Monotonicity((101, 101), fit_radius=0)
161 component = FactorizedComponent(
162 self.bands,
163 spectrum.copy(),
164 morph.copy(),
165 bbox,
166 center,
167 bg_rms=np.array([1, 1, 1]),
168 bg_thresh=0.5,
169 monotonicity=monotonicity,
170 )
172 proxed_spectrum = np.array([1e-20, 2, 3])
173 proxed_morph = np.array([[2.6666666666666667, 2, 1], [1, 5, 3], [0, 4, 0]])
174 proxed_morph = proxed_morph / 5
176 component.prox_spectrum(component.spectrum)
177 component.prox_morph(component.morph)
179 assert_array_equal(component.spectrum, proxed_spectrum)
180 assert_array_equal(component.morph, proxed_morph)
182 component = FactorizedComponent(
183 self.bands,
184 spectrum.copy(),
185 morph.copy(),
186 bbox,
187 None,
188 )
190 proxed_spectrum = np.array([1e-20, 2, 3])
191 proxed_morph = np.array([[10, 2, 1], [1, 5, 3], [0.1, 4, 0]])
192 proxed_morph = proxed_morph / 10
194 component.prox_spectrum(component.spectrum)
195 component.prox_morph(component.morph)
197 assert_array_equal(component.spectrum, proxed_spectrum)
198 assert_array_equal(component.morph, proxed_morph)
200 self.assertFalse(component.resize(morph_bbox))
202 def test_resize(self):
203 spectrum = np.array([1, 2, 3], dtype=float)
204 morph = np.zeros((10, 10), dtype=float)
205 morph[3:6, 5:8] = np.arange(9).reshape(3, 3)
206 bbox = Box((10, 10), (3, 5))
208 morph_bbox = Box((100, 100))
209 monotonicity = Monotonicity((101, 101), fit_radius=0)
211 component = FactorizedComponent(
212 self.bands,
213 spectrum.copy(),
214 morph.copy(),
215 bbox,
216 None,
217 bg_rms=np.array([1, 1, 1]),
218 bg_thresh=0.5,
219 monotonicity=monotonicity,
220 padding=1,
221 )
223 self.assertTupleEqual(component.morph.shape, (10, 10))
224 self.assertIsNone(component.component_center)
226 component.resize(morph_bbox)
227 self.assertTupleEqual(component.morph.shape, (5, 5))
228 self.assertTupleEqual(component.bbox.origin, (5, 9))
229 self.assertTupleEqual(component.bbox.shape, (5, 5))
230 self.assertIsNone(component.component_center)
232 def test_parameterization(self):
233 component = self.component
234 assert_array_equal(component.get_model(), self.spectrum[:, None, None] * self.morph[None, :, :])
236 component.parameterize(default_fista_parameterization)
237 helpers = set(component._morph.helpers.keys())
238 self.assertSetEqual(helpers, {"z"})
239 component.parameterize(default_adaprox_parameterization)
240 helpers = set(component._morph.helpers.keys())
241 self.assertSetEqual(helpers, {"m", "v", "vhat"})
243 params = (tuple("grizy"), Box((5, 5)))
244 with self.assertRaises(NotImplementedError):
245 default_fista_parameterization(DummyComponent(*params))
247 with self.assertRaises(NotImplementedError):
248 default_adaprox_parameterization(DummyComponent(*params))