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

80 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-24 02:29 -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 os.path 

27 

28import numpy as np 

29 

30from ._schema import Schema 

31from ._schemaMapper import SchemaMapper 

32from ._base import BaseCatalog 

33from ._table import SimpleTable 

34from ._simple import SimpleCatalog 

35from ._source import SourceCatalog, SourceTable 

36from ._match import ReferenceMatch 

37 

38from lsst.utils import getPackageDir 

39 

40 

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

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

43 

44 Parameters 

45 ---------- 

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

47 Input source schema that fields will be mapped from. 

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

49 Target schema that fields will be mapped to. 

50 sourcePrefix : `str`, optional 

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

52 targetPrefix : `str`, optional 

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

54 

55 Returns 

56 ------- 

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

58 Mapping between source and target schemas. 

59 """ 

60 m = SchemaMapper(sourceSchema, targetSchema) 

61 for key, field in sourceSchema: 

62 keyName = field.getName() 

63 if sourcePrefix is not None: 

64 if not keyName.startswith(sourcePrefix): 

65 continue 

66 else: 

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

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

69 return m 

70 

71 

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

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

74 

75 Parameters 

76 ---------- 

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

78 Input source schema that fields will be mapped from. 

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

80 Target schema that fields will be mapped to. 

81 sourcePrefix : `str`, optional 

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

83 targetPrefix : `str`, optional 

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

85 

86 Returns 

87 ------- 

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

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

90 """ 

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

92 

93 

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

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

96 

97 Parameters 

98 ---------- 

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

100 Source catalog to be copied from. 

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

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

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

104 Schema of source catalog. 

105 sourcePrefix : `str`, optional 

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

107 targetPrefix : `str`, optional 

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

109 

110 Returns 

111 ------- 

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

113 Target catalog that is edited in place. 

114 """ 

115 if sourceSchema is None: 

116 sourceSchema = catalog.schema 

117 

118 targetSchema = target.schema 

119 target.reserve(len(catalog)) 

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

121 target.addNew() 

122 

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

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

125 

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

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

128 rTo.assign(rFrom, m) 

129 

130 

131def matchesToCatalog(matches, matchMeta): 

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

133 

134 Parameters 

135 ---------- 

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

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

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

139 contain the reference and source catalog entries, and a 

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

141 and source objects). 

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

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

144 

145 Returns 

146 ------- 

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

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

149 referece and source entries, respectively, including alias 

150 maps from reference and source catalogs) 

151 """ 

152 if len(matches) == 0: 

153 raise RuntimeError("No matches provided.") 

154 

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

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

157 

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

159 mergedSchema = makeMergedSchema( 

160 srcSchema, mergedSchema, targetPrefix="src_") 

161 

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

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

164 

165 distKey = mergedSchema.addField( 

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

167 

168 mergedCatalog = BaseCatalog(mergedSchema) 

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

170 sourceSchema=refSchema, targetPrefix="ref_") 

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

172 sourceSchema=srcSchema, targetPrefix="src_") 

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

174 r.set(distKey, m.distance) 

175 

176 # obtain reference catalog name if one is setup 

177 try: 

178 catalogName = os.path.basename(getPackageDir("astrometry_net_data")) 

179 except LookupError: 

180 catalogName = "NOT_SET" 

181 matchMeta.add("REFCAT", catalogName) 

182 mergedCatalog.getTable().setMetadata(matchMeta) 

183 

184 return mergedCatalog 

185 

186 

187def matchesFromCatalog(catalog, sourceSlotConfig=None): 

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

189 

190 Parameters 

191 ---------- 

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

193 Catalog of matches. Must have schema where reference entries 

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

195 ``src_``. 

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

197 Configuration for source slots. 

198 

199 Returns 

200 ------- 

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

202 List of matches. 

203 """ 

204 refSchema = makeMergedSchema( 

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

206 refCatalog = SimpleCatalog(refSchema) 

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

208 

209 srcSchema = makeMergedSchema( 

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

211 srcCatalog = SourceCatalog(srcSchema) 

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

213 

214 if sourceSlotConfig is not None: 

215 sourceSlotConfig.setupSchema(srcCatalog.schema) 

216 

217 matches = [] 

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

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

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

221 

222 return matches 

223 

224 

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

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

227 

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

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

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

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

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

233 respectively). 

234 

235 Parameters 

236 ---------- 

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

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

239 copied to ``outSchema``. 

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

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

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

243 prefix : `str`, optional 

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

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

246 

247 Returns 

248 ------- 

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

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

251 added. 

252 """ 

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

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

255 

256 return outSchema 

257 

258 

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

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

261 

262 Parameters 

263 ---------- 

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

265 Catalog to reindex. 

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

267 Index array. 

268 deep : `bool` 

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

270 

271 Returns 

272 ------- 

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

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

275 """ 

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

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

278 new.extend(records, deep=deep) 

279 return new