Coverage for python/lsst/scarlet/lite/utils.py: 48%

47 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 

22import sys 

23 

24import numpy as np 

25import numpy.typing as npt 

26from scipy.special import erfc 

27 

28ScalarLike = bool | int | float | complex 

29ScalarTypes = (bool, int, float, complex) 

30 

31 

32sqrt2 = np.sqrt(2) 

33sqrt_pi = np.sqrt(np.pi) 

34 

35 

36def integrated_gaussian_value(x: np.ndarray, sigma: float) -> np.ndarray: 

37 """A Gaussian function evaluated at `x` 

38 

39 Parameters 

40 ---------- 

41 x: 

42 The coordinates to evaluate the integrated Gaussian 

43 (ie. the centers of pixels). 

44 sigma: 

45 The standard deviation of the Gaussian. 

46 

47 Returns 

48 ------- 

49 gaussian: 

50 A Gaussian function integrated over `x` 

51 """ 

52 lhs = erfc((x - 0.5) / (sqrt2 * sigma)) 

53 rhs = erfc((x + 0.5) / (sqrt2 * sigma)) 

54 return sqrt_pi * 0.5 * sigma * (lhs - rhs) 

55 

56 

57def integrated_circular_gaussian( 

58 x: np.ndarray | None = None, y: np.ndarray | None = None, sigma: float = 0.8 

59) -> np.ndarray: 

60 """Create a circular Gaussian that is integrated over pixels 

61 

62 This is typically used for the model PSF, 

63 working well with the default parameters. 

64 

65 Parameters 

66 ---------- 

67 x, y: 

68 The x,y-coordinates to evaluate the integrated Gaussian. 

69 If `X` and `Y` are `None` then they will both be given the 

70 default value `numpy.arange(-7, 8)`, resulting in a 

71 `15x15` centered image. 

72 sigma: 

73 The standard deviation of the Gaussian. 

74 

75 Returns 

76 ------- 

77 image: 

78 A Gaussian function integrated over `X` and `Y`. 

79 """ 

80 if x is None: 

81 if y is None: 

82 x = np.arange(-7, 8) 

83 y = x 

84 else: 

85 raise ValueError( 

86 f"Either X and Y must be specified, or neither must be specified, got {x=} and {y=}" 

87 ) 

88 elif y is None: 

89 raise ValueError(f"Either X and Y must be specified, or neither must be specified, got {x=} and {y=}") 

90 

91 _x = integrated_gaussian_value(np.abs(x), sigma)[None, :] 

92 _y = integrated_gaussian_value(np.abs(y), sigma)[:, None] 

93 result = _x * _y 

94 return result / np.sum(result) 

95 

96 

97def get_circle_mask(diameter: int, dtype: npt.DTypeLike = np.float64): 

98 """Get a boolean image of a circle 

99 

100 Parameters 

101 ---------- 

102 diameter: 

103 The diameter of the circle and width 

104 of the image. 

105 dtype: 

106 The `dtype` of the image. 

107 

108 Returns 

109 ------- 

110 circle: 

111 A boolean array with ones for the pixels with centers 

112 inside of the circle and zeros 

113 outside of the circle. 

114 """ 

115 c = (diameter - 1) / 2 

116 # The center of the circle and its radius are 

117 # off by half a pixel for circles with 

118 # even numbered diameter 

119 if diameter % 2 == 0: 

120 radius = diameter / 2 

121 else: 

122 radius = c 

123 _x = np.arange(diameter) 

124 x, y = np.meshgrid(_x, _x) 

125 r = np.sqrt((x - c) ** 2 + (y - c) ** 2) 

126 

127 circle = np.ones((diameter, diameter), dtype=dtype) 

128 circle[r > radius] = 0 

129 return circle 

130 

131 

132INTRINSIC_SPECIAL_ATTRIBUTES = frozenset( 

133 ( 

134 "__qualname__", 

135 "__module__", 

136 "__metaclass__", 

137 "__dict__", 

138 "__weakref__", 

139 "__class__", 

140 "__subclasshook__", 

141 "__name__", 

142 "__doc__", 

143 ) 

144) 

145 

146 

147def is_attribute_safe_to_transfer(name, value): 

148 """Return True if an attribute is safe to monkeypatch-transfer to another 

149 class. 

150 This rejects special methods that are defined automatically for all 

151 classes, leaving only those explicitly defined in a class decorated by 

152 `continueClass` or registered with an instance of `TemplateMeta`. 

153 """ 

154 if name.startswith("__") and ( 

155 value is getattr(object, name, None) or name in INTRINSIC_SPECIAL_ATTRIBUTES 

156 ): 

157 return False 

158 return True 

159 

160 

161def continue_class(cls): 

162 """Re-open the decorated class, adding any new definitions into the 

163 original. 

164 For example: 

165 .. code-block:: python 

166 class Foo: 

167 pass 

168 @continueClass 

169 class Foo: 

170 def run(self): 

171 return None 

172 is equivalent to: 

173 .. code-block:: python 

174 class Foo: 

175 def run(self): 

176 return None 

177 .. warning:: 

178 Python's built-in `super` function does not behave properly in classes 

179 decorated with `continue_class`. Base class methods must be invoked 

180 directly using their explicit types instead. 

181 

182 This is copied directly from lsst.utils. If any additional functions are 

183 used from that repo we should remove this function and make lsst.utils 

184 a dependency. But for now, it is easier to copy this single wrapper 

185 than to include lsst.utils and all of its dependencies. 

186 """ 

187 orig = getattr(sys.modules[cls.__module__], cls.__name__) 

188 for name in dir(cls): 

189 # Common descriptors like classmethod and staticmethod can only be 

190 # accessed without invoking their magic if we use __dict__; if we use 

191 # getattr on those we'll get e.g. a bound method instance on the dummy 

192 # class rather than a classmethod instance we can put on the target 

193 # class. 

194 attr = cls.__dict__.get(name, None) or getattr(cls, name) 

195 if is_attribute_safe_to_transfer(name, attr): 

196 setattr(orig, name, attr) 

197 return orig