Coverage for python/lsst/meas/extensions/multiprofit/utils.py: 12%

52 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-09 10:30 +0000

1# This file is part of meas_extensions_multiprofit. 

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 collections import defaultdict 

23from collections.abc import Iterable 

24 

25import numpy as np 

26 

27import lsst.afw.detection as afwDetect 

28import lsst.afw.image as afwImage 

29import lsst.geom as geom 

30 

31 

32def defaultdictNested(): 

33 """Get a nested defaultdict with defaultdict default value. 

34 

35 Returns 

36 ------- 

37 defaultdict : `defaultdict` 

38 A `defaultdict` with `defaultdict` default values. 

39 """ 

40 return defaultdict(defaultdictNested) 

41 

42 

43def get_all_subclasses(cls, children_first: bool = True): 

44 """Return all subclasses of a class recursively. 

45 

46 Parameters 

47 ---------- 

48 cls 

49 The class to get subclasses for. 

50 children_first 

51 If true, return child (direct subclasses) first, followed by their 

52 children (recursively). Otherwise, return each direct child followed 

53 by its own descendants first. 

54 """ 

55 subclasses = {c: None for c in cls.__subclasses__()} 

56 subclasses_return = subclasses.copy() if children_first else {} 

57 for subclass in subclasses: 

58 if children_first: 

59 subclasses_return[subclass] = None 

60 for subsubclass in get_all_subclasses(subclass): 

61 subclasses_return[subsubclass] = None 

62 return list(subclasses_return) 

63 

64 

65# TODO: Allow addition to existing image 

66def get_spanned_image( 

67 exposure: afwImage.Exposure, 

68 footprint: afwDetect.Footprint = None, 

69 bbox: geom.Box2I | None = None, 

70 spans: np.ndarray | None = None, 

71 get_sig_inv: bool = False, 

72 calibrate: bool = True, 

73) -> tuple[np.ndarray, geom.Box2I, np.ndarray]: 

74 """Get an image masked by its spanset. 

75 

76 Parameters 

77 ---------- 

78 exposure 

79 An exposure to extract arrays from. 

80 footprint 

81 The footprint to get spans/bboxes from. Not needed if both of 

82 `bbox` and `spans` are provided. 

83 bbox 

84 The bounding box to subset the exposure with. 

85 Defaults to the footprint's bbox. 

86 spans 

87 A spanset array (inverse mask/selection). 

88 Defaults to the footprint's spans. 

89 get_sig_inv 

90 Whether to get the inverse variance and return its square root. 

91 calibrate 

92 Whether to calibrate the image; set to False if already calibrated. 

93 

94 Returns 

95 ------- 

96 image 

97 The image array, with masked pixels set to zero. 

98 bbox 

99 The bounding box used to subset the exposure. 

100 sig_inv 

101 The inverse sigma array, with masked pixels set to zero. 

102 Set to None if `get_sig_inv` is False. 

103 """ 

104 bbox_is_none = bbox is None 

105 if bbox_is_none: 

106 bbox = footprint.getBBox() 

107 if not (bbox.getHeight() > 0 and bbox.getWidth() > 0): 

108 return None, bbox 

109 if spans is None: 

110 spans = footprint.getSpans().asArray() 

111 sig_inv = afwImage.ImageF(bbox) if get_sig_inv else None 

112 img = afwImage.ImageF(bbox) 

113 img.array[:] = np.nan 

114 if footprint is None: 

115 maskedIm = exposure.maskedImage.subset(bbox) 

116 if not calibrate: 

117 img = maskedIm.image.array 

118 sig_inv.array[spans] = 1 / np.sqrt(maskedIm.variance.array[spans]) 

119 else: 

120 img.array[spans] = footprint.getImageArray() 

121 if get_sig_inv: 

122 # footprint.getVarianceArray() returns zeros 

123 variance = exposure.variance[bbox] 

124 if not calibrate: 

125 sig_inv.array[spans] = 1 / np.sqrt(variance.array[spans]) 

126 if calibrate: 

127 # Have to calibrate with the original image 

128 maskedIm = afwImage.MaskedImageF( 

129 image=exposure.image[bbox], 

130 variance=variance if get_sig_inv else None, 

131 ) 

132 if calibrate: 

133 maskedIm = exposure.photoCalib.calibrateImage(maskedIm) 

134 if footprint is None: 

135 img = maskedIm.image.array 

136 else: 

137 # Apply the calibration to the deblended footprint 

138 # ... hopefully it's multiplicative enough 

139 img.array[spans] *= maskedIm.image.array[spans] / exposure.image[bbox].array[spans] 

140 img = img.array 

141 if get_sig_inv: 

142 sig_inv.array[spans] = 1 / np.sqrt(maskedIm.variance.array[spans]) 

143 # Should not happen but does with footprints having nans 

144 sig_inv.array[~(sig_inv.array >= 0)] = 0 

145 

146 return np.array(img, dtype="float64"), bbox, np.array(sig_inv.array, dtype="float64") 

147 

148 

149def join_and_filter(separator: str, items: Iterable[str], exclusion: str | None = None) -> str: 

150 """Join an iterable of items by a separator, filtering out an exclusion. 

151 

152 Parameters 

153 ---------- 

154 separator 

155 The separator to join items by. 

156 items 

157 Items to join. 

158 exclusion 

159 The pattern to exclude. 

160 

161 Returns 

162 ------- 

163 joined 

164 The joined string. 

165 """ 

166 return separator.join(filter(exclusion, items))