Coverage for python / lsst / afw / table / catalogMatches.py: 16%

75 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-14 00:45 -0700

1# This file is part of afw. 

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 

22__all__ = ["makeMergedSchema", "copyIntoCatalog", 

23 "matchesToCatalog", "matchesFromCatalog", "copyAliasMapWithPrefix", 

24 "reindexCatalog"] 

25 

26import numpy as np 

27 

28from ._schema import Schema 

29from ._schemaMapper import SchemaMapper 

30from ._base import BaseCatalog 

31from ._table import SimpleTable 

32from ._simple import SimpleCatalog 

33from ._source import SourceCatalog, SourceTable 

34from ._match import ReferenceMatch 

35 

36 

37def makeMapper(sourceSchema, targetSchema, sourcePrefix=None, targetPrefix=None): 

38 """Create a SchemaMapper between the input source and target schemas. 

39 

40 Parameters 

41 ---------- 

42 sourceSchema : :py:class:`lsst.afw.table.Schema` 

43 Input source schema that fields will be mapped from. 

44 targetSchema : :py:class:`lsst.afw.table.Schema` 

45 Target schema that fields will be mapped to. 

46 sourcePrefix : `str`, optional 

47 If set, only those keys with that prefix will be mapped. 

48 targetPrefix : `str`, optional 

49 If set, prepend it to the mapped (target) key name. 

50 

51 Returns 

52 ------- 

53 SchemaMapper : :py:class:`lsst.afw.table.SchemaMapper` 

54 Mapping between source and target schemas. 

55 """ 

56 m = SchemaMapper(sourceSchema, targetSchema) 

57 for key, field in sourceSchema: 

58 keyName = field.getName() 

59 if sourcePrefix is not None: 

60 if not keyName.startswith(sourcePrefix): 

61 continue 

62 else: 

63 keyName = field.getName().replace(sourcePrefix, "", 1) 

64 m.addMapping(key, (targetPrefix or "") + keyName) 

65 return m 

66 

67 

68def makeMergedSchema(sourceSchema, targetSchema, sourcePrefix=None, targetPrefix=None): 

69 """Return a schema that is a deep copy of a mapping between source and target schemas. 

70 

71 Parameters 

72 ---------- 

73 sourceSchema : :py:class:`lsst.afw.table.Schema` 

74 Input source schema that fields will be mapped from. 

75 targetSchema : :py:class:`lsst.afw.atable.Schema` 

76 Target schema that fields will be mapped to. 

77 sourcePrefix : `str`, optional 

78 If set, only those keys with that prefix will be mapped. 

79 targetPrefix : `str`, optional 

80 If set, prepend it to the mapped (target) key name. 

81 

82 Returns 

83 ------- 

84 schema : :py:class:`lsst.afw.table.Schema` 

85 Schema that is the result of the mapping between source and target schemas. 

86 """ 

87 return makeMapper(sourceSchema, targetSchema, sourcePrefix, targetPrefix).getOutputSchema() 

88 

89 

90def copyIntoCatalog(catalog, target, sourceSchema=None, sourcePrefix=None, targetPrefix=None): 

91 """Copy entries from one Catalog into another. 

92 

93 Parameters 

94 ---------- 

95 catalog : :py:class:`lsst.afw.table.base.Catalog` 

96 Source catalog to be copied from. 

97 target : :py:class:`lsst.afw.table.base.Catalog` 

98 Target catalog to be copied to (edited in place). 

99 sourceSchema : :py:class:`lsst.afw.table.Schema`, optional 

100 Schema of source catalog. 

101 sourcePrefix : `str`, optional 

102 If set, only those keys with that prefix will be copied. 

103 targetPrefix : `str`, optional 

104 If set, prepend it to the copied (target) key name 

105 

106 Returns 

107 ------- 

108 target : :py:class:`lsst.afw.table.base.Catalog` 

109 Target catalog that is edited in place. 

110 """ 

111 if sourceSchema is None: 

112 sourceSchema = catalog.schema 

113 

114 targetSchema = target.schema 

115 target.reserve(len(catalog)) 

116 for i in range(len(target), len(catalog)): 

117 target.addNew() 

118 

119 if len(catalog) != len(target): 

120 raise RuntimeError(f"Length mismatch: {len(catalog)} vs {len(target)}") 

121 

122 m = makeMapper(sourceSchema, targetSchema, sourcePrefix, targetPrefix) 

123 for rFrom, rTo in zip(catalog, target): 

124 rTo.assign(rFrom, m) 

125 

126 

127def matchesToCatalog(matches, matchMeta): 

128 """Denormalise matches into a Catalog of "unpacked matches". 

129 

130 Parameters 

131 ---------- 

132 matches : `~lsst.afw.table.match.SimpleMatch` 

133 Unpacked matches, i.e. a list of Match objects whose schema 

134 has "first" and "second" attributes which, resepectively, 

135 contain the reference and source catalog entries, and a 

136 "distance" field (the measured distance between the reference 

137 and source objects). 

138 matchMeta : `~lsst.daf.base.PropertySet` 

139 Metadata for matches (must have .add attribute). 

140 

141 Returns 

142 ------- 

143 mergedCatalog : :py:class:`lsst.afw.table.BaseCatalog` 

144 Catalog of matches (with ``ref_`` and ``src_`` prefix identifiers for 

145 referece and source entries, respectively, including alias 

146 maps from reference and source catalogs) 

147 """ 

148 if len(matches) == 0: 

149 raise RuntimeError("No matches provided.") 

150 

151 refSchema = matches[0].first.getSchema() 

152 srcSchema = matches[0].second.getSchema() 

153 

154 mergedSchema = makeMergedSchema(refSchema, Schema(), targetPrefix="ref_") 

155 mergedSchema = makeMergedSchema( 

156 srcSchema, mergedSchema, targetPrefix="src_") 

157 

158 mergedSchema = copyAliasMapWithPrefix(refSchema, mergedSchema, prefix="ref_") 

159 mergedSchema = copyAliasMapWithPrefix(srcSchema, mergedSchema, prefix="src_") 

160 

161 distKey = mergedSchema.addField( 

162 "distance", type=np.float64, doc="Distance between ref and src") 

163 

164 mergedCatalog = BaseCatalog(mergedSchema) 

165 copyIntoCatalog([m.first for m in matches], mergedCatalog, 

166 sourceSchema=refSchema, targetPrefix="ref_") 

167 copyIntoCatalog([m.second for m in matches], mergedCatalog, 

168 sourceSchema=srcSchema, targetPrefix="src_") 

169 for m, r in zip(matches, mergedCatalog): 

170 r.set(distKey, m.distance) 

171 

172 # The reference catalog is not known. 

173 catalogName = "NOT_SET" 

174 matchMeta.add("REFCAT", catalogName) 

175 mergedCatalog.getTable().setMetadata(matchMeta) 

176 

177 return mergedCatalog 

178 

179 

180def matchesFromCatalog(catalog, sourceSlotConfig=None): 

181 """Generate a list of ReferenceMatches from a Catalog of "unpacked matches". 

182 

183 Parameters 

184 ---------- 

185 catalog : :py:class:`lsst.afw.table.BaseCatalog` 

186 Catalog of matches. Must have schema where reference entries 

187 are prefixed with ``ref_`` and source entries are prefixed with 

188 ``src_``. 

189 sourceSlotConfig : `lsst.meas.base.baseMeasurement.SourceSlotConfig`, optional 

190 Configuration for source slots. 

191 

192 Returns 

193 ------- 

194 matches : :py:class:`lsst.afw.table.ReferenceMatch` 

195 List of matches. 

196 """ 

197 refSchema = makeMergedSchema( 

198 catalog.schema, SimpleTable.makeMinimalSchema(), sourcePrefix="ref_") 

199 refCatalog = SimpleCatalog(refSchema) 

200 copyIntoCatalog(catalog, refCatalog, sourcePrefix="ref_") 

201 

202 srcSchema = makeMergedSchema( 

203 catalog.schema, SourceTable.makeMinimalSchema(), sourcePrefix="src_") 

204 srcCatalog = SourceCatalog(srcSchema) 

205 copyIntoCatalog(catalog, srcCatalog, sourcePrefix="src_") 

206 

207 if sourceSlotConfig is not None: 

208 sourceSlotConfig.setupSchema(srcCatalog.schema) 

209 

210 matches = [] 

211 distKey = catalog.schema.find("distance").key 

212 for ref, src, cat in zip(refCatalog, srcCatalog, catalog): 

213 matches.append(ReferenceMatch(ref, src, cat[distKey])) 

214 

215 return matches 

216 

217 

218def copyAliasMapWithPrefix(inSchema, outSchema, prefix=""): 

219 """Copy an alias map from one schema into another. 

220 

221 This copies the alias map of one schema into another, optionally 

222 prepending a prefix to both the "from" and "to" names of the alias 

223 (the example use case here is for the "match" catalog created by 

224 `lsst.meas.astrom.denormalizeMatches` where prefixes ``src_`` and 

225 ``ref_`` are added to the source and reference field entries, 

226 respectively). 

227 

228 Parameters 

229 ---------- 

230 inSchema : `lsst.afw.table.Schema` 

231 The input schema whose `lsst.afw.table.AliasMap` is to be 

232 copied to ``outSchema``. 

233 outSchema : `lsst.afw.table.Schema` 

234 The output schema into which the `lsst.afw.table.AliasMap` 

235 from ``inSchema`` is to be copied (modified in place). 

236 prefix : `str`, optional 

237 An optional prefix to add to both the "from" and "to" names 

238 of the alias (default is an empty string). 

239 

240 Returns 

241 ------- 

242 outSchema : `lsst.afw.table.Schema` 

243 The output schema with the alias mappings from `inSchema` 

244 added. 

245 """ 

246 for k, v in inSchema.getAliasMap().items(): 

247 outSchema.getAliasMap().set(prefix + k, prefix + v) 

248 

249 return outSchema 

250 

251 

252def reindexCatalog(catalog, indices, deep=True): 

253 """Apply a numpy index array to an afw Catalog 

254 

255 Parameters 

256 ---------- 

257 catalog : `lsst.afw.table.SourceCatalog` 

258 Catalog to reindex. 

259 indices : `numpy.ndarray` of `int` 

260 Index array. 

261 deep : `bool` 

262 Whether or not to make a deep copy of the original catalog. 

263 

264 Returns 

265 ------- 

266 new : subclass of `lsst.afw.table.BaseCatalog` 

267 Reindexed catalog. Records are shallow copies of those in ``catalog``. 

268 """ 

269 new = SourceCatalog(catalog.table.clone() if deep else catalog.table) 

270 records = [catalog[int(ii)] for ii in indices] 

271 new.extend(records, deep=deep) 

272 return new