Coverage for python / lsst / meas / astrom / match_probabilistic_task.py: 30%

67 statements  

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

1# This file is part of meas_astrom. 

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 typing import Dict, List, Optional, Set, Tuple 

23import warnings 

24 

25import astropy.table 

26import logging 

27import lsst.afw.geom as afwGeom 

28import lsst.geom as geom 

29import lsst.pipe.base as pipeBase 

30import lsst.utils as utils 

31import numpy as np 

32 

33from .matcher_probabilistic import MatchProbabilisticConfig, MatcherProbabilistic 

34 

35 

36__all__ = ["MatchProbabilisticTask", "radec_to_xy"] 

37 

38 

39def radec_to_xy(ra_vec, dec_vec, factor, wcs: afwGeom.SkyWcs): 

40 radec_true = [ 

41 geom.SpherePoint(ra*factor, dec*factor, geom.degrees) 

42 for ra, dec in zip(ra_vec, dec_vec) 

43 ] 

44 return wcs.skyToPixel(radec_true) 

45 

46 

47class MatchProbabilisticTask(pipeBase.Task): 

48 """Run MatchProbabilistic on a reference and target catalog covering the same tract.""" 

49 

50 ConfigClass = MatchProbabilisticConfig 

51 _DefaultName = "matchProbabilistic" 

52 

53 @staticmethod 

54 def _apply_select_bool( 

55 catalog: astropy.table.Table, 

56 columns_true: List[str], 

57 columns_false: List[str], 

58 selection: Optional[np.array], 

59 ) -> np.array: 

60 """Apply additional boolean selection columns. 

61 

62 catalog : `astropy.table.Table` 

63 The catalog to select from. 

64 columns_true : `list` [`str`] 

65 Columns that must be True for selection. 

66 columns_false : `list` [`str`] 

67 Columns that must be False for selection. 

68 selection : `numpy.array` 

69 A prior selection array. Default all true. 

70 

71 Returns 

72 ------- 

73 selection : `numpy.array` 

74 The final selection array. 

75 

76 """ 

77 select_additional = (len(columns_true) + len(columns_false)) > 0 

78 if select_additional: 

79 if selection is None: 

80 selection = np.ones(len(catalog), dtype=bool) 

81 for column in columns_true: 

82 # This is intended for boolean columns, so the behaviour for non-boolean is not obvious 

83 # More config options and/or using a ConfigurableActionField might be best 

84 values = catalog[column] 

85 selection &= (np.isfinite(values) & (values != 0)) 

86 for column in columns_false: 

87 values = catalog[column] 

88 selection &= (values == 0) 

89 return selection 

90 

91 @property 

92 def columns_in_ref(self) -> Set[str]: 

93 return self.config.columns_in_ref 

94 

95 @property 

96 def columns_in_target(self) -> Set[str]: 

97 return self.config.columns_in_target 

98 

99 def match( 

100 self, 

101 catalog_ref: astropy.table.Table, 

102 catalog_target: astropy.table.Table, 

103 select_ref: np.array = None, 

104 select_target: np.array = None, 

105 wcs: afwGeom.SkyWcs = None, 

106 logger: logging.Logger = None, 

107 logging_n_rows: int = None, 

108 ) -> Tuple[astropy.table.Table, astropy.table.Table, Dict[int, str]]: 

109 """Match sources in a reference tract catalog with a target catalog. 

110 

111 Parameters 

112 ---------- 

113 catalog_ref : `astropy.table.Table` 

114 A reference catalog to match objects/sources from. 

115 catalog_target : `astropy.table.Table` 

116 A target catalog to match reference objects/sources to. 

117 select_ref : `numpy.array` 

118 A boolean array of the same length as `catalog_ref` selecting the sources that can be matched. 

119 select_target : `numpy.array` 

120 A boolean array of the same length as `catalog_target` selecting the sources that can be matched. 

121 wcs : `lsst.afw.image.SkyWcs` 

122 A coordinate system to convert catalog positions to sky coordinates. Only used if 

123 `self.config.coords_ref_to_convert` is set. 

124 logger : `logging.Logger` 

125 A Logger for logging. 

126 logging_n_rows : `int` 

127 Number of matches to make before outputting incremental log message. 

128 

129 Returns 

130 ------- 

131 catalog_out_ref : `astropy.table.Table` 

132 Reference matched catalog with indices of target matches. 

133 catalog_out_target : `astropy.table.Table` 

134 Reference matched catalog with indices of target matches. 

135 """ 

136 if logger is None: 

137 logger = self.log 

138 

139 config = self.config 

140 

141 if config.column_ref_order is None: 

142 fluxes = [catalog_ref[key] for key in config.columns_ref_flux] 

143 flux_tot = np.nansum(fluxes, axis=0) 

144 catalog_ref["flux_total"] = flux_tot 

145 if config.mag_brightest_ref != -np.inf or config.mag_faintest_ref != np.inf: 

146 mag_tot = ( 

147 -2.5 * np.log10(flux_tot) + config.coord_format.mag_zeropoint_ref 

148 ) 

149 select_mag = (mag_tot >= config.mag_brightest_ref) & ( 

150 mag_tot <= config.mag_faintest_ref 

151 ) 

152 else: 

153 select_mag = np.isfinite(flux_tot) 

154 if select_ref is None: 

155 select_ref = select_mag 

156 else: 

157 select_ref &= select_mag 

158 

159 with warnings.catch_warnings(): 

160 # We already issued a deprecation warning; no need to repeat it. 

161 warnings.filterwarnings(action="ignore", category=FutureWarning) 

162 select_ref = self._apply_select_bool( 

163 catalog=catalog_ref, 

164 columns_true=config.columns_ref_select_true, 

165 columns_false=config.columns_ref_select_false, 

166 selection=select_ref, 

167 ) 

168 select_target = self._apply_select_bool( 

169 catalog=catalog_target, 

170 columns_true=config.columns_target_select_true, 

171 columns_false=config.columns_target_select_false, 

172 selection=select_target, 

173 ) 

174 

175 logger.info( 

176 "Beginning MatcherProbabilistic.match with %d/%d ref sources selected vs %d/%d target", 

177 len(catalog_ref) if select_ref is None else np.sum(select_ref), 

178 len(catalog_ref), 

179 len(catalog_target) if select_target is None else np.sum(select_target), 

180 len(catalog_target), 

181 ) 

182 

183 catalog_out_ref, catalog_out_target, exceptions = self.matcher.match( 

184 catalog_ref, 

185 catalog_target, 

186 select_ref=select_ref, 

187 select_target=select_target, 

188 logger=logger, 

189 logging_n_rows=logging_n_rows, 

190 wcs=wcs, 

191 radec_to_xy_func=radec_to_xy, 

192 ) 

193 

194 return catalog_out_ref, catalog_out_target, exceptions 

195 

196 @utils.timer.timeMethod 

197 def run( 

198 self, 

199 catalog_ref: astropy.table.Table, 

200 catalog_target: astropy.table.Table, 

201 wcs: afwGeom.SkyWcs = None, 

202 **kwargs, 

203 ) -> pipeBase.Struct: 

204 """Match sources in a reference tract catalog with a target catalog. 

205 

206 Parameters 

207 ---------- 

208 catalog_ref : `astropy.table.Table` 

209 A reference catalog to match objects/sources from. 

210 catalog_target : `astropy.table.Table` 

211 A target catalog to match reference objects/sources to. 

212 wcs : `lsst.afw.image.SkyWcs` 

213 A coordinate system to convert catalog positions to sky coordinates. 

214 Only needed if `config.coords_ref_to_convert` is used to convert 

215 reference catalog sky coordinates to pixel positions. 

216 kwargs : Additional keyword arguments to pass to `match`. 

217 

218 Returns 

219 ------- 

220 retStruct : `lsst.pipe.base.Struct` 

221 A struct with output_ref and output_target attribute containing the 

222 output matched catalogs, as well as a dict 

223 """ 

224 with warnings.catch_warnings(): 

225 # We already issued a deprecation warning; no need to repeat it. 

226 warnings.filterwarnings(action="ignore", category=FutureWarning) 

227 catalog_ref, catalog_target, exceptions = self.match( 

228 catalog_ref, catalog_target, wcs=wcs, **kwargs 

229 ) 

230 

231 return pipeBase.Struct( 

232 cat_output_ref=catalog_ref, 

233 cat_output_target=catalog_target, 

234 exceptions=exceptions, 

235 ) 

236 

237 def __init__(self, **kwargs): 

238 super().__init__(**kwargs) 

239 self.matcher = MatcherProbabilistic(self.config)