Coverage for python / lsst / meas / extensions / scarlet / source.py: 41%

57 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-24 08:32 +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/>. 

21 

22from __future__ import annotations 

23 

24from copy import deepcopy 

25from typing import TYPE_CHECKING, Any 

26 

27import numpy as np 

28from numpy.typing import DTypeLike 

29 

30from lsst.afw.detection import Footprint 

31from lsst.afw.image import MultibandExposure 

32import lsst.scarlet.lite as scl 

33 

34if TYPE_CHECKING: 

35 from .io import IsolatedSourceData 

36 

37 

38__all__ = ["IsolatedSource"] 

39 

40 

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. 

46 

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)] 

58 

59 @property 

60 def component(self) -> scl.component.CubeComponent: 

61 """The single component of this isolated source. 

62 

63 Returns 

64 ------- 

65 component : 

66 The CubeComponent representing this isolated source. 

67 """ 

68 return self.components[0] 

69 

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. 

78 

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. 

89 

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) 

114 

115 @property 

116 def bbox(self) -> scl.Box: 

117 """The bounding box of the source in the full Blend.""" 

118 return self.component.bbox 

119 

120 @property 

121 def bands(self) -> list[str]: 

122 """The ordered list of bands in the full source model.""" 

123 return self.component.bands 

124 

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 

129 

130 def get_model(self) -> scl.Image: 

131 """Get the full 3D (band, y, x) model of the source. 

132 

133 Returns 

134 ------- 

135 model : 

136 The 3D (band, y, x) model of the source. 

137 """ 

138 return self.component._model 

139 

140 def to_data(self) -> IsolatedSourceData: 

141 """Convert to a ScarletSourceData representation. 

142 

143 Returns 

144 ------- 

145 source_data : 

146 The source represented as a ScarletSourceData. 

147 """ 

148 from .io import IsolatedSourceData 

149 

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 ) 

156 

157 def __copy__(self) -> IsolatedSource: 

158 """Create a copy of this IsolatedSource. 

159 

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 ) 

170 

171 def __deepcopy__(self, memo: dict[int, Any]) -> IsolatedSource: 

172 """Create a deep copy of this IsolatedSource. 

173 

174 Parameters 

175 ---------- 

176 memo : dict[int, Any] 

177 A memoization dictionary used by `copy.deepcopy`. 

178 

179 Returns 

180 ------- 

181 source : 

182 A deep copy of this IsolatedSource. 

183 """ 

184 if id(self) in memo: 

185 return memo[id(self)] 

186 

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 

195 

196 def __getitem__(self, indices: Any) -> IsolatedSource: 

197 """Get a sub-source corresponding to the given indices. 

198 

199 Parameters 

200 ---------- 

201 indices : Any 

202 The indices to use to slice the source model. 

203 

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 )