Coverage for python / lsst / meas / extensions / scarlet / source.py: 41%
57 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 09:00 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 09:00 +0000
1# This file is part of meas_extensions_scarlet.
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 copy import deepcopy
25from typing import TYPE_CHECKING, Any
27import numpy as np
28from numpy.typing import DTypeLike
30from lsst.afw.detection import Footprint
31from lsst.afw.image import MultibandExposure
32import lsst.scarlet.lite as scl
34if TYPE_CHECKING:
35 from .io import IsolatedSourceData
38__all__ = ["IsolatedSource"]
41class IsolatedSource(scl.source.SourceBase):
42 """A scarlet source representation for isolated sources.
43 """
44 def __init__(self, model: scl.Image, peak: tuple[int, int], metadata: dict | None = None):
45 """Construct an IsolatedSource.
47 Parameters
48 ----------
49 model :
50 The 3D (band, y, x) model of the source.
51 center :
52 The (y, x) coordinates of the peak pixel within the model.
53 metadata :
54 Optional metadata to store with the source.
55 """
56 self.metadata = metadata
57 self.components = [scl.component.CubeComponent(model=model, peak=peak)]
59 @property
60 def component(self) -> scl.component.CubeComponent:
61 """The single component of this isolated source.
63 Returns
64 -------
65 component :
66 The CubeComponent representing this isolated source.
67 """
68 return self.components[0]
70 @staticmethod
71 def from_footprint(
72 footprint: Footprint,
73 mCoadd: MultibandExposure,
74 dtype: DTypeLike,
75 metadata: dict | None = None,
76 ) -> IsolatedSource:
77 """Create an IsolatedSource from a footprint in a multiband coadd.
79 Parameters
80 ----------
81 footprint :
82 The footprint of the source in the multiband coadd.
83 mCoadd :
84 The multiband coadd containing the source.
85 dtype :
86 The desired data type for the source model.
87 metadata :
88 Optional metadata to store with the source.
90 Returns
91 -------
92 source :
93 The isolated source represented as a ScarletSource.
94 """
95 if len(footprint.peaks) != 1:
96 raise ValueError(
97 "Footprint must have exactly one peak to create an IsolatedSource, "
98 f"found {len(footprint.peaks)}"
99 )
100 peak = (footprint.peaks[0].getIy(), footprint.peaks[0].getIx())
101 bbox = footprint.getBBox()
102 x0, y0 = bbox.getMin()
103 width, height = bbox.getDimensions()
104 # Convert the footprint into a boolean array
105 footprint_array = footprint.spans.asArray((height, width), (x0, y0))
106 # Create the 3D model array by multiplying the footprint by each band
107 # of the multiband coadd.
108 model_array = np.ndarray((len(mCoadd.bands), height, width), dtype=dtype)
109 for bidx, band in enumerate(mCoadd.bands):
110 model_array[bidx] = mCoadd[band, bbox].image.array * footprint_array
111 # Create the model
112 model = scl.Image(model_array, bands=mCoadd.bands, yx0=(y0, x0))
113 return IsolatedSource(model=model, peak=peak, metadata=metadata)
115 @property
116 def bbox(self) -> scl.Box:
117 """The bounding box of the source in the full Blend."""
118 return self.component.bbox
120 @property
121 def bands(self) -> list[str]:
122 """The ordered list of bands in the full source model."""
123 return self.component.bands
125 @property
126 def peak(self) -> tuple[int, int]:
127 """The (y, x) coordinates of the peak pixel within the model."""
128 return self.component.peak
130 def get_model(self) -> scl.Image:
131 """Get the full 3D (band, y, x) model of the source.
133 Returns
134 -------
135 model :
136 The 3D (band, y, x) model of the source.
137 """
138 return self.component._model
140 def to_data(self) -> IsolatedSourceData:
141 """Convert to a ScarletSourceData representation.
143 Returns
144 -------
145 source_data :
146 The source represented as a ScarletSourceData.
147 """
148 from .io import IsolatedSourceData
150 span_array = np.any(self.component._model.data != 0, axis=0)
151 return IsolatedSourceData(
152 span_array=span_array,
153 origin=self.bbox.origin,
154 peak=self.peak,
155 )
157 def __copy__(self) -> IsolatedSource:
158 """Create a copy of this IsolatedSource.
160 Returns
161 -------
162 source_copy :
163 A copy of this IsolatedSource.
164 """
165 return IsolatedSource(
166 model=self.component._model,
167 peak=self.component.peak,
168 metadata=self.metadata,
169 )
171 def __deepcopy__(self, memo: dict[int, Any]) -> IsolatedSource:
172 """Create a deep copy of this IsolatedSource.
174 Parameters
175 ----------
176 memo : dict[int, Any]
177 A memoization dictionary used by `copy.deepcopy`.
179 Returns
180 -------
181 source :
182 A deep copy of this IsolatedSource.
183 """
184 if id(self) in memo:
185 return memo[id(self)]
187 source = IsolatedSource.__new__(IsolatedSource)
188 memo[id(self)] = source
189 source.__init__( # type: ignore[misc]
190 model=deepcopy(self.component._model, memo),
191 peak=deepcopy(self.component.peak, memo),
192 metadata=deepcopy(self.metadata, memo),
193 )
194 return source
196 def __getitem__(self, indices: Any) -> IsolatedSource:
197 """Get a sub-source corresponding to the given indices.
199 Parameters
200 ----------
201 indices : Any
202 The indices to use to slice the source model.
204 Returns
205 -------
206 source :
207 A new IsolatedSource that is a sub-source of this one.
208 Raises
209 ------
210 IndexError :
211 If the index includes a `Box` or spatial indices.
212 """
213 component = self.component[indices]
214 return IsolatedSource(
215 model=component._model,
216 peak=component.peak,
217 metadata=self.metadata,
218 )