Coverage for python/lsst/scarlet/lite/source.py: 35%

68 statements  

« 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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["Source"] 

25 

26from abc import ABC, abstractmethod 

27from typing import TYPE_CHECKING, Any, Callable 

28 

29from .bbox import Box 

30from .component import Component 

31from .image import Image 

32 

33if TYPE_CHECKING: 

34 from .io import ScarletSourceBaseData, ScarletSourceData 

35 

36 

37class SourceBase(ABC): 

38 """Base class for a source 

39 

40 This is primarily to allow `isinstance` checks 

41 without importing the full `Source` class. 

42 """ 

43 

44 metadata: dict[str, Any] | None = None 

45 

46 @abstractmethod 

47 def to_data(self) -> ScarletSourceBaseData: 

48 """Convert to a `ScarletSourceBaseData` for serialization 

49 

50 Returns 

51 ------- 

52 source_data: 

53 The `ScarletSourceData` representation of this source. 

54 """ 

55 

56 

57class Source(SourceBase): 

58 """A container for components associated with the same astrophysical object 

59 

60 A source can have a single component, or multiple components, 

61 and each can be contained in different bounding boxes. 

62 

63 Parameters 

64 ---------- 

65 components: 

66 The components contained in the source. 

67 """ 

68 

69 def __init__(self, components: list[Component], metadata: dict | None = None): 

70 self.components = components 

71 self.flux_weighted_image: Image | None = None 

72 self.metadata = metadata 

73 

74 @property 

75 def n_components(self) -> int: 

76 """The number of components in this source""" 

77 return len(self.components) 

78 

79 @property 

80 def center(self) -> tuple[int, int] | None: 

81 """The center of the source in the full Blend.""" 

82 if not self.is_null and hasattr(self.components[0], "peak"): 

83 return self.components[0].peak # type: ignore 

84 return None 

85 

86 @property 

87 def source_center(self) -> tuple[int, int] | None: 

88 """The center of the source in its local bounding box.""" 

89 _center = self.center 

90 _origin = self.bbox.origin 

91 if _center is not None: 

92 center = ( 

93 _center[0] - _origin[0], 

94 _center[1] - _origin[1], 

95 ) 

96 return center 

97 return None 

98 

99 @property 

100 def is_null(self) -> bool: 

101 """True if the source does not have any components""" 

102 return self.n_components == 0 

103 

104 @property 

105 def bbox(self) -> Box: 

106 """The minimal bounding box to contain all of this sources components 

107 

108 Null sources have a bounding box with shape `(0,0,0)` 

109 """ 

110 if self.n_components == 0: 

111 return Box((0, 0)) 

112 bbox = self.components[0].bbox 

113 for component in self.components[1:]: 

114 bbox = bbox | component.bbox 

115 return bbox 

116 

117 @property 

118 def bands(self) -> tuple: 

119 """The bands in the full source model.""" 

120 if self.is_null: 

121 return () 

122 return self.components[0].bands 

123 

124 def get_model(self, use_flux: bool = False) -> Image: 

125 """Build the model for the source 

126 

127 This is never called during optimization and is only used 

128 to generate a model of the source for investigative purposes. 

129 

130 Parameters 

131 ---------- 

132 use_flux: 

133 Whether to use the re-distributed flux associated with the source 

134 instead of the component models. 

135 

136 Returns 

137 ------- 

138 model: 

139 The full-color model. 

140 """ 

141 if self.n_components == 0: 

142 return 0 # type: ignore 

143 

144 if use_flux: 

145 # Return the redistributed flux 

146 # (calculated by scarlet.lite.measure.weight_sources) 

147 return self.flux_weighted_image # type: ignore 

148 

149 model = self.components[0].get_model() 

150 for component in self.components[1:]: 

151 model = model + component.get_model() 

152 return model 

153 

154 def parameterize(self, parameterization: Callable): 

155 """Convert the component parameter arrays into Parameter instances 

156 

157 Parameters 

158 ---------- 

159 parameterization: 

160 A function to use to convert parameters of a given type into 

161 a `Parameter` in place. It should take a single argument that 

162 is the `Component` or `Source` that is to be parameterized. 

163 """ 

164 for component in self.components: 

165 component.parameterize(parameterization) 

166 

167 def to_data(self) -> ScarletSourceData: 

168 """Convert to a `ScarletSourceData` for serialization 

169 

170 Returns 

171 ------- 

172 source_data: 

173 The `ScarletSourceData` representation of this source. 

174 """ 

175 from .io import ScarletSourceData 

176 

177 component_data = [c.to_data() for c in self.components] 

178 return ScarletSourceData(components=component_data, metadata=self.metadata) 

179 

180 def __str__(self): 

181 return f"Source<{len(self.components)}>" 

182 

183 def __repr__(self): 

184 return f"Source(components={repr(self.components)})>"