Coverage for tests / test_sourceTable.py: 10%
550 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 01:50 -0700
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 01:50 -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/>.
22import os
23import unittest
24import tempfile
25import pickle
26import math
28import numpy as np
30import lsst.utils.tests
31import lsst.pex.exceptions
32import lsst.geom
33import lsst.afw.table
34import lsst.afw.geom
35import lsst.afw.image
36import lsst.afw.detection
38testPath = os.path.abspath(os.path.dirname(__file__))
41def makeArray(size, dtype):
42 return np.array(np.random.randn(*size), dtype=dtype)
45def makeCov(size, dtype):
46 m = np.array(np.random.randn(size, size), dtype=dtype)
47 return np.dot(m, m.transpose())
50def makeWcs():
51 crval = lsst.geom.SpherePoint(1.606631*lsst.geom.degrees,
52 5.090329*lsst.geom.degrees)
53 crpix = lsst.geom.Point2D(2036.0, 2000.0)
54 cdMatrix = np.array([5.399452e-5, -1.30770e-5, 1.30770e-5, 5.399452e-5])
55 cdMatrix.shape = (2, 2)
56 return lsst.afw.geom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix)
59class SourceTableTestCase(lsst.utils.tests.TestCase):
61 def fillRecord(self, record):
62 record.set(self.instFluxKey, np.random.randn())
63 record.set(self.instFluxErrKey, np.random.randn())
64 record.set(self.centroidKey.getX(), np.random.randn())
65 record.set(self.centroidKey.getY(), np.random.randn())
66 record.set(self.xErrKey, np.random.randn())
67 record.set(self.yErrKey, np.random.randn())
68 record.set(self.shapeKey.getIxx(), np.random.randn())
69 record.set(self.shapeKey.getIyy(), np.random.randn())
70 record.set(self.shapeKey.getIxy(), np.random.randn())
71 record.set(self.xxErrKey, np.random.randn())
72 record.set(self.yyErrKey, np.random.randn())
73 record.set(self.xyErrKey, np.random.randn())
74 record.set(self.psfShapeKey.getIxx(), np.random.randn())
75 record.set(self.psfShapeKey.getIyy(), np.random.randn())
76 record.set(self.psfShapeKey.getIxy(), np.random.randn())
77 record.set(self.fluxFlagKey, np.random.randn() > 0)
78 record.set(self.centroidFlagKey, np.random.randn() > 0)
79 record.set(self.shapeFlagKey, np.random.randn() > 0)
81 def setUp(self):
82 np.random.seed(1)
83 self.schema = lsst.afw.table.SourceTable.makeMinimalSchema()
84 self.instFluxKey = self.schema.addField("a_instFlux", type="D")
85 self.instFluxErrKey = self.schema.addField("a_instFluxErr", type="D")
86 self.fluxFlagKey = self.schema.addField("a_flag", type="Flag")
88 # the meas field is added using a functor key, but the error is added
89 # as scalars, as we lack a ResultKey functor as exists in meas_base
90 self.centroidKey = lsst.afw.table.Point2DKey.addFields(
91 self.schema, "b", "", "pixel")
92 self.xErrKey = self.schema.addField("b_xErr", type="F")
93 self.yErrKey = self.schema.addField("b_yErr", type="F")
94 self.centroidFlagKey = self.schema.addField("b_flag", type="Flag")
96 self.shapeKey = lsst.afw.table.QuadrupoleKey.addFields(
97 self.schema, "c", "", lsst.afw.table.CoordinateType.PIXEL)
98 self.xxErrKey = self.schema.addField("c_xxErr", type="F")
99 self.xyErrKey = self.schema.addField("c_xyErr", type="F")
100 self.yyErrKey = self.schema.addField("c_yyErr", type="F")
101 self.shapeFlagKey = self.schema.addField("c_flag", type="Flag")
103 self.psfShapeKey = lsst.afw.table.QuadrupoleKey.addFields(
104 self.schema, "d", "", lsst.afw.table.CoordinateType.PIXEL)
105 self.psfShapeFlagKey = self.schema.addField("d_flag", type="Flag")
107 self.coordErrKey = lsst.afw.table.CoordKey.addErrorFields(self.schema)
109 self.table = lsst.afw.table.SourceTable.make(self.schema)
110 self.catalog = lsst.afw.table.SourceCatalog(self.table)
111 self.record = self.catalog.addNew()
112 self.fillRecord(self.record)
113 self.record.setId(50)
114 self.fillRecord(self.catalog.addNew())
115 self.fillRecord(self.catalog.addNew())
117 def tearDown(self):
118 del self.schema
119 del self.record
120 del self.table
121 del self.catalog
123 def checkCanonical(self):
124 self.assertEqual(self.record.get(self.instFluxKey),
125 self.record.getPsfInstFlux())
126 self.assertEqual(self.record.get(self.fluxFlagKey),
127 self.record.getPsfFluxFlag())
128 self.assertEqual(self.table.getSchema().getAliasMap().get("slot_Centroid"), "b")
129 self.assertEqual(self.centroidKey.get(self.record),
130 self.record.getCentroid())
131 self.assertFloatsAlmostEqual(
132 math.fabs(self.record.get(self.xErrKey)),
133 math.sqrt(self.record.getCentroidErr()[0, 0]), rtol=1e-6)
134 self.assertFloatsAlmostEqual(
135 math.fabs(self.record.get(self.yErrKey)),
136 math.sqrt(self.record.getCentroidErr()[1, 1]), rtol=1e-6)
137 self.assertEqual(self.table.getSchema().getAliasMap().get("slot_Shape"), "c")
138 self.assertEqual(self.shapeKey.get(self.record),
139 self.record.getShape())
140 self.assertFloatsAlmostEqual(
141 math.fabs(self.record.get(self.xxErrKey)),
142 math.sqrt(self.record.getShapeErr()[0, 0]), rtol=1e-6)
143 self.assertFloatsAlmostEqual(
144 math.fabs(self.record.get(self.yyErrKey)),
145 math.sqrt(self.record.getShapeErr()[1, 1]), rtol=1e-6)
146 self.assertFloatsAlmostEqual(
147 math.fabs(self.record.get(self.xyErrKey)),
148 math.sqrt(self.record.getShapeErr()[2, 2]), rtol=1e-6)
149 self.assertEqual(self.table.getSchema().getAliasMap().get("slot_PsfShape"), "d")
150 self.assertEqual(self.psfShapeKey.get(self.record),
151 self.record.getPsfShape())
153 def testPersisted(self):
154 self.table.definePsfFlux("a")
155 self.table.defineCentroid("b")
156 self.table.defineShape("c")
157 self.table.definePsfShape("d")
158 with lsst.utils.tests.getTempFilePath(".fits") as filename:
159 self.catalog.writeFits(filename)
160 catalog = lsst.afw.table.SourceCatalog.readFits(filename)
161 table = catalog.getTable()
162 record = catalog[0]
163 # I'm using the keys from the non-persisted table. They should work at least in the
164 # current implementation
165 self.assertEqual(record.get(self.instFluxKey), record.getPsfInstFlux())
166 self.assertEqual(record.get(self.fluxFlagKey), record.getPsfFluxFlag())
167 self.assertEqual(table.getSchema().getAliasMap().get("slot_Centroid"), "b")
168 centroid = self.centroidKey.get(self.record)
169 self.assertEqual(centroid, record.getCentroid())
170 self.assertFloatsAlmostEqual(
171 math.fabs(self.record.get(self.xErrKey)),
172 math.sqrt(self.record.getCentroidErr()[0, 0]), rtol=1e-6)
173 self.assertFloatsAlmostEqual(
174 math.fabs(self.record.get(self.yErrKey)),
175 math.sqrt(self.record.getCentroidErr()[1, 1]), rtol=1e-6)
176 shape = self.shapeKey.get(self.record)
177 self.assertEqual(table.getSchema().getAliasMap().get("slot_Shape"), "c")
178 self.assertEqual(shape, record.getShape())
179 self.assertFloatsAlmostEqual(
180 math.fabs(self.record.get(self.xxErrKey)),
181 math.sqrt(self.record.getShapeErr()[0, 0]), rtol=1e-6)
182 self.assertFloatsAlmostEqual(
183 math.fabs(self.record.get(self.yyErrKey)),
184 math.sqrt(self.record.getShapeErr()[1, 1]), rtol=1e-6)
185 self.assertFloatsAlmostEqual(
186 math.fabs(self.record.get(self.xyErrKey)),
187 math.sqrt(self.record.getShapeErr()[2, 2]), rtol=1e-6)
188 psfShape = self.psfShapeKey.get(self.record)
189 self.assertEqual(table.getSchema().getAliasMap().get("slot_PsfShape"), "d")
190 self.assertEqual(psfShape, record.getPsfShape())
192 def testCanonical2(self):
193 self.table.definePsfFlux("a")
194 self.table.defineCentroid("b")
195 self.table.defineShape("c")
196 self.table.definePsfShape("d")
197 self.checkCanonical()
199 def testPickle(self):
200 p = pickle.dumps(self.catalog)
201 new = pickle.loads(p)
203 self.assertEqual(self.catalog.schema.getNames(), new.schema.getNames())
204 self.assertEqual(len(self.catalog), len(new))
205 for r1, r2 in zip(self.catalog, new):
206 # Columns that are easy to test
207 for field in ("a_instFlux", "a_instFluxErr", "id"):
208 k1 = self.catalog.schema.find(field).getKey()
209 k2 = new.schema.find(field).getKey()
210 self.assertEqual(r1[k1], r2[k2])
212 def testCoordUpdate(self):
213 self.table.defineCentroid("b")
214 wcs = makeWcs()
215 self.record.updateCoord(wcs)
216 coord1 = self.record.getCoord()
217 coord2 = wcs.pixelToSky(self.record.get(self.centroidKey))
218 self.assertEqual(coord1, coord2)
220 def testCoordErrors(self):
221 self.table.defineCentroid("b")
222 wcs = makeWcs()
223 self.record.updateCoord(wcs)
225 scale = (1.0 * lsst.geom.arcseconds).asDegrees()
226 center = self.record.getCentroid()
227 skyCenter = wcs.pixelToSky(center)
228 localGnomonicWcs = lsst.afw.geom.makeSkyWcs(
229 center, skyCenter, np.diag((scale, scale)))
230 measurementToLocalGnomonic = wcs.getTransform().then(
231 localGnomonicWcs.getTransform().inverted()
232 )
233 localMatrix = measurementToLocalGnomonic.getJacobian(center)
234 radMatrix = np.radians(localMatrix / 3600)
236 centroidErr = self.record.getCentroidErr()
237 coordErr = radMatrix.dot(centroidErr.dot(radMatrix.T))
238 catCoordErr = self.record.get(self.coordErrKey)
239 np.testing.assert_almost_equal(coordErr, catCoordErr, decimal=16)
241 def testSorting(self):
242 self.assertFalse(self.catalog.isSorted())
243 self.catalog.sort()
244 self.assertTrue(self.catalog.isSorted())
245 r = self.catalog.find(2)
246 self.assertEqual(r["id"], 2)
247 r = self.catalog.find(500)
248 self.assertIsNone(r)
250 def testConversion(self):
251 catalog1 = self.catalog.cast(lsst.afw.table.SourceCatalog)
252 catalog2 = self.catalog.cast(lsst.afw.table.SimpleCatalog)
253 catalog3 = self.catalog.cast(lsst.afw.table.SourceCatalog, deep=True)
254 catalog4 = self.catalog.cast(lsst.afw.table.SimpleCatalog, deep=True)
255 self.assertEqual(self.catalog.table, catalog1.table)
256 self.assertEqual(self.catalog.table, catalog2.table)
257 self.assertNotEqual(self.catalog.table, catalog3.table)
258 self.assertNotEqual(self.catalog.table, catalog3.table)
259 for r, r1, r2, r3, r4 in zip(self.catalog, catalog1, catalog2, catalog3, catalog4):
260 self.assertEqual(r, r1)
261 self.assertEqual(r, r2)
262 self.assertNotEqual(r, r3)
263 self.assertNotEqual(r, r4)
264 self.assertEqual(r.getId(), r3.getId())
265 self.assertEqual(r.getId(), r4.getId())
267 def testColumnView(self):
268 cols1 = self.catalog.getColumnView()
269 cols2 = self.catalog.columns
270 self.assertIs(cols1, cols2)
271 self.assertIsInstance(cols1, lsst.afw.table.SourceColumnView)
272 self.table.definePsfFlux("a")
273 self.table.defineCentroid("b")
274 self.table.defineShape("c")
275 self.table.definePsfShape("d")
276 self.assertFloatsEqual(cols2["a_instFlux"], cols2.getPsfInstFlux())
277 self.assertFloatsEqual(cols2["a_instFluxErr"], cols2.getPsfInstFluxErr())
278 self.assertFloatsEqual(cols2["b_x"], cols2.getX())
279 self.assertFloatsEqual(cols2["b_y"], cols2.getY())
280 self.assertFloatsEqual(cols2["c_xx"], cols2.getIxx())
281 self.assertFloatsEqual(cols2["c_yy"], cols2.getIyy())
282 self.assertFloatsEqual(cols2["c_xy"], cols2.getIxy())
283 self.assertFloatsEqual(cols2["d_xx"], cols2.getPsfIxx())
284 self.assertFloatsEqual(cols2["d_yy"], cols2.getPsfIyy())
285 self.assertFloatsEqual(cols2["d_xy"], cols2.getPsfIxy())
287 # Trying to access slots which have been removed should raise.
288 self.catalog.table.schema.getAliasMap().erase("slot_Centroid")
289 self.catalog.table.schema.getAliasMap().erase("slot_Shape")
290 self.catalog.table.schema.getAliasMap().erase("slot_PsfShape")
291 for quantity in ["X", "Y", "Ixx", "Iyy", "Ixy", "PsfIxx", "PsfIyy", "PsfIxy"]:
292 with self.assertRaises(lsst.pex.exceptions.LogicError):
293 getattr(self.catalog, f"get{quantity}")()
295 def testForwarding(self):
296 """Verify that Catalog forwards unknown methods to its table and/or columns."""
297 self.table.definePsfFlux("a")
298 self.table.defineCentroid("b")
299 self.table.defineShape("c")
300 self.assertFloatsEqual(self.catalog.columns["a_instFlux"],
301 self.catalog["a_instFlux"])
302 self.assertFloatsEqual(self.catalog.columns[self.instFluxKey],
303 self.catalog.get(self.instFluxKey))
304 self.assertFloatsEqual(self.catalog.columns.get(self.instFluxKey),
305 self.catalog.getPsfInstFlux())
306 self.assertEqual(self.instFluxKey, self.catalog.getPsfFluxSlot().getMeasKey())
307 with self.assertRaises(AttributeError):
308 self.catalog.foo()
310 def testBitsColumn(self):
312 allBits = self.catalog.getBits()
313 someBits = self.catalog.getBits(["a_flag", "c_flag"])
314 self.assertEqual(allBits.getMask("a_flag"), 0x1)
315 self.assertEqual(allBits.getMask("b_flag"), 0x2)
316 self.assertEqual(allBits.getMask("c_flag"), 0x4)
317 self.assertEqual(someBits.getMask(self.fluxFlagKey), 0x1)
318 self.assertEqual(someBits.getMask(self.shapeFlagKey), 0x2)
319 np.testing.assert_array_equal((allBits.array & 0x1 != 0), self.catalog["a_flag"])
320 np.testing.assert_array_equal((allBits.array & 0x2 != 0), self.catalog["b_flag"])
321 np.testing.assert_array_equal((allBits.array & 0x4 != 0), self.catalog["c_flag"])
322 np.testing.assert_array_equal((someBits.array & 0x1 != 0), self.catalog["a_flag"])
323 np.testing.assert_array_equal((someBits.array & 0x2 != 0), self.catalog["c_flag"])
325 def testCast(self):
326 baseCat = self.catalog.cast(lsst.afw.table.BaseCatalog)
327 baseCat.cast(lsst.afw.table.SourceCatalog)
329 def testFootprints(self):
330 '''Test round-tripping Footprints (inc. HeavyFootprints) to FITS
331 '''
332 src1 = self.catalog.addNew()
333 src2 = self.catalog.addNew()
334 src3 = self.catalog.addNew()
335 self.fillRecord(src1)
336 self.fillRecord(src2)
337 self.fillRecord(src3)
338 src2.setParent(src1.getId())
340 W, H = 100, 100
341 mim = lsst.afw.image.MaskedImageF(W, H)
342 im = mim.getImage()
343 msk = mim.getMask()
344 var = mim.getVariance()
345 x, y = np.meshgrid(np.arange(W, dtype=int), np.arange(H, dtype=int))
346 im.array[:] = y*1E6 + x*1E3
347 msk.array[:] = (y << 8) | x
348 var.array[:] = y*1E2 + x
349 spanSet = lsst.afw.geom.SpanSet.fromShape(20).shiftedBy(50, 50)
350 circ = lsst.afw.detection.Footprint(spanSet)
351 heavy = lsst.afw.detection.makeHeavyFootprint(circ, mim)
352 src2.setFootprint(heavy)
354 for i, src in enumerate(self.catalog):
355 if src != src2:
356 spanSet = lsst.afw.geom.SpanSet.fromShape(1 + i*2).shiftedBy(50, 50)
357 src.setFootprint(lsst.afw.detection.Footprint(spanSet))
359 # insert this HeavyFootprint into an otherwise blank image (for comparing the results)
360 mim2 = lsst.afw.image.MaskedImageF(W, H)
361 heavy.insert(mim2)
363 with lsst.utils.tests.getTempFilePath(".fits") as fn:
364 self.catalog.writeFits(fn)
366 cat2 = lsst.afw.table.SourceCatalog.readFits(fn)
367 r2 = cat2[-2]
368 h2 = r2.getFootprint()
369 self.assertTrue(h2.isHeavy())
370 mim3 = lsst.afw.image.MaskedImageF(W, H)
371 h2.insert(mim3)
373 self.assertFalse(cat2[-1].getFootprint().isHeavy())
374 self.assertFalse(cat2[-3].getFootprint().isHeavy())
375 self.assertFalse(cat2[0].getFootprint().isHeavy())
376 self.assertFalse(cat2[1].getFootprint().isHeavy())
377 self.assertFalse(cat2[2].getFootprint().isHeavy())
379 if False:
380 # Write out before-n-after FITS images
381 for MI in [mim, mim2, mim3]:
382 f, fn2 = tempfile.mkstemp(prefix='testHeavyFootprint-', suffix='.fits')
383 os.close(f)
384 MI.writeFits(fn2)
385 print('wrote', fn2)
387 self.assertFloatsEqual(mim2.getImage().getArray(), mim3.getImage().getArray())
388 self.assertFloatsEqual(mim2.getMask().getArray(), mim3.getMask().getArray())
389 self.assertFloatsEqual(mim2.getVariance().getArray(), mim3.getVariance().getArray())
391 im3 = mim3.getImage()
392 ma3 = mim3.getMask()
393 va3 = mim3.getVariance()
394 for y in range(H):
395 for x in range(W):
396 if circ.contains(lsst.geom.Point2I(x, y)):
397 self.assertEqual(im[x, y, lsst.afw.image.PARENT], im3[x, y, lsst.afw.image.PARENT])
398 self.assertEqual(msk[x, y, lsst.afw.image.PARENT], ma3[x, y, lsst.afw.image.PARENT])
399 self.assertEqual(var[x, y, lsst.afw.image.PARENT], va3[x, y, lsst.afw.image.PARENT])
400 else:
401 self.assertEqual(im3[x, y, lsst.afw.image.PARENT], 0.)
402 self.assertEqual(ma3[x, y, lsst.afw.image.PARENT], 0.)
403 self.assertEqual(va3[x, y, lsst.afw.image.PARENT], 0.)
405 cat3 = lsst.afw.table.SourceCatalog.readFits(
406 fn, flags=lsst.afw.table.SOURCE_IO_NO_HEAVY_FOOTPRINTS)
407 for src in cat3:
408 self.assertFalse(src.getFootprint().isHeavy())
409 cat4 = lsst.afw.table.SourceCatalog.readFits(
410 fn, flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS)
411 for src in cat4:
412 self.assertIsNone(src.getFootprint())
414 self.catalog.writeFits(
415 fn, flags=lsst.afw.table.SOURCE_IO_NO_HEAVY_FOOTPRINTS)
416 cat5 = lsst.afw.table.SourceCatalog.readFits(fn)
417 for src in cat5:
418 self.assertFalse(src.getFootprint().isHeavy())
420 self.catalog.writeFits(
421 fn, flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS)
422 cat6 = lsst.afw.table.SourceCatalog.readFits(fn)
423 for src in cat6:
424 self.assertIsNone(src.getFootprint())
426 # Insert a source with no Footprint to ensure that a None footprint
427 # is persisted correctly
428 self.catalog.addNew()
430 with lsst.utils.tests.getTempFilePath(".fits") as fn:
431 self.catalog.writeFits(fn)
433 cat2 = lsst.afw.table.SourceCatalog.readFits(fn)
434 self.assertIsNone(cat2[-1].getFootprint())
436 def testFootprintsToNumpy(self):
437 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
438 table = lsst.afw.table.SourceTable.make(schema)
439 catalog = lsst.afw.table.SourceCatalog(table)
440 src1 = catalog.addNew()
441 src2 = catalog.addNew()
442 src3 = catalog.addNew()
444 spans1 = lsst.afw.geom.SpanSet.fromShape(5, lsst.afw.geom.Stencil.BOX).shiftedBy(10, 20)
445 spans2 = lsst.afw.geom.SpanSet.fromShape(5, lsst.afw.geom.Stencil.BOX).shiftedBy(30, 40)
446 spans3 = lsst.afw.geom.SpanSet.fromShape(3, lsst.afw.geom.Stencil.BOX).shiftedBy(40, 50)
448 src1.setFootprint(lsst.afw.detection.Footprint(spans1))
449 src2.setFootprint(lsst.afw.detection.Footprint(spans2))
450 src3.setFootprint(lsst.afw.detection.Footprint(spans3))
452 truth = np.zeros((100, 100), dtype=int)
453 truth[15:26, 5:16] = src1.getId()
454 truth[35:46, 25:36] = src2.getId()
455 truth[47:54, 37:44] = src3.getId()
457 footprintIds = lsst.afw.detection.footprintsToNumpy(catalog, shape=(100, 100), asBool=False)
458 booleanArray = lsst.afw.detection.footprintsToNumpy(catalog, shape=(100, 100), asBool=True)
460 np.testing.assert_array_equal(footprintIds, truth)
461 np.testing.assert_array_equal(booleanArray, truth > 0)
463 def testIdFactory(self):
464 expId = int(1257198)
465 reserved = 32
466 factory = lsst.afw.table.IdFactory.makeSource(expId, reserved)
467 id1 = factory()
468 id2 = factory()
469 self.assertEqual(id2 - id1, 1)
470 factory.notify(0xFFFFFFFF)
471 with self.assertRaises(lsst.pex.exceptions.LengthError):
472 factory()
473 with self.assertRaises(lsst.pex.exceptions.InvalidParameterError):
474 factory.notify(0x1FFFFFFFF)
475 with self.assertRaises(lsst.pex.exceptions.InvalidParameterError):
476 lsst.afw.table.IdFactory.makeSource(0x1FFFFFFFF, reserved)
478 def testFamilies(self):
479 self.catalog.sort()
480 parents = self.catalog.getChildren(0)
481 self.assertEqual(list(parents), list(self.catalog))
482 parentKey = lsst.afw.table.SourceTable.getParentKey()
483 for parent in parents:
484 self.assertEqual(parent.get(parentKey), 0)
485 for i in range(10):
486 child = self.catalog.addNew()
487 self.fillRecord(child)
488 child.set(parentKey, parent.getId())
489 childrenIter = self.catalog.getChildren([parent.getId() for parent in parents],
490 [record.getId() for record in self.catalog])
491 for parent, (children, ids) in zip(parents, childrenIter):
492 self.assertEqual(len(children), 10)
493 self.assertEqual(len(children), len(ids))
494 for child, id in zip(children, ids):
495 self.assertEqual(child.getParent(), parent.getId())
496 self.assertEqual(child.getId(), id)
498 # Check detection of unsorted catalog
499 self.catalog.sort(self.instFluxKey)
500 with self.assertRaises(AssertionError):
501 self.catalog.getChildren(0)
502 self.catalog.sort(parentKey)
503 self.catalog.getChildren(0) # Just care this succeeds
505 def testFitsReadVersion0Compatibility(self):
506 cat = lsst.afw.table.SourceCatalog.readFits(os.path.join(testPath, "data/empty-v0.fits"))
507 self.assertTrue(cat.getPsfFluxSlot().isValid())
508 self.assertTrue(cat.getApFluxSlot().isValid())
509 self.assertTrue(cat.getGaussianFluxSlot().isValid())
510 self.assertTrue(cat.getModelFluxSlot().isValid())
511 self.assertTrue(cat.getCentroidSlot().isValid())
512 self.assertTrue(cat.getShapeSlot().isValid())
513 self.assertEqual(cat.getPsfFluxSlot().getMeasKey(),
514 cat.schema.find("flux_psf").key)
515 self.assertEqual(cat.getApFluxSlot().getMeasKey(),
516 cat.schema.find("flux_sinc").key)
517 self.assertEqual(cat.getGaussianFluxSlot().getMeasKey(),
518 cat.schema.find("flux_naive").key)
519 self.assertEqual(cat.getModelFluxSlot().getMeasKey(),
520 cat.schema.find("cmodel_flux").key)
521 self.assertEqual(cat.getCentroidSlot().getMeasKey().getX(),
522 cat.schema.find("centroid_sdss_x").key)
523 self.assertEqual(cat.getCentroidSlot().getMeasKey().getY(),
524 cat.schema.find("centroid_sdss_y").key)
525 self.assertEqual(cat.getShapeSlot().getMeasKey().getIxx(),
526 cat.schema.find("shape_hsm_moments_xx").key)
527 self.assertEqual(cat.getShapeSlot().getMeasKey().getIyy(),
528 cat.schema.find("shape_hsm_moments_yy").key)
529 self.assertEqual(cat.getShapeSlot().getMeasKey().getIxy(),
530 cat.schema.find("shape_hsm_moments_xy").key)
531 self.assertEqual(cat.getPsfFluxSlot().getErrKey(),
532 cat.schema.find("flux_psf_err").key)
533 self.assertEqual(cat.getApFluxSlot().getErrKey(),
534 cat.schema.find("flux_sinc_err").key)
535 self.assertEqual(cat.getGaussianFluxSlot().getErrKey(),
536 cat.schema.find("flux_naive_err").key)
537 self.assertEqual(cat.getModelFluxSlot().getErrKey(),
538 cat.schema.find("cmodel_flux_err").key)
539 self.assertEqual(
540 cat.getCentroidSlot().getErrKey(),
541 lsst.afw.table.CovarianceMatrix2fKey(cat.schema["centroid_sdss_err"], ["x", "y"]))
542 self.assertEqual(
543 cat.getShapeSlot().getErrKey(),
544 lsst.afw.table.CovarianceMatrix3fKey(cat.schema["shape_hsm_moments_err"], ["xx", "yy", "xy"]))
545 self.assertEqual(cat.getPsfFluxSlot().getFlagKey(),
546 cat.schema.find("flux_psf_flags").key)
547 self.assertEqual(cat.getApFluxSlot().getFlagKey(),
548 cat.schema.find("flux_sinc_flags").key)
549 self.assertEqual(cat.getGaussianFluxSlot().getFlagKey(),
550 cat.schema.find("flux_naive_flags").key)
551 self.assertEqual(cat.getModelFluxSlot().getFlagKey(),
552 cat.schema.find("cmodel_flux_flags").key)
553 self.assertEqual(cat.getCentroidSlot().getFlagKey(),
554 cat.schema.find("centroid_sdss_flags").key)
555 self.assertEqual(cat.getShapeSlot().getFlagKey(),
556 cat.schema.find("shape_hsm_moments_flags").key)
558 def testFitsReadVersion1Compatibility(self):
559 """Test reading of catalogs with version 1 schema
561 Version 1 catalogs need to have added aliases from Sigma->Err and
562 from `_flux`->`_instFlux`.
563 """
564 cat = lsst.afw.table.SourceCatalog.readFits(
565 os.path.join(testPath, "data", "sourceTable-v1.fits"))
566 self.assertEqual(
567 cat.getCentroidSlot().getErrKey(),
568 lsst.afw.table.CovarianceMatrix2fKey(
569 cat.schema["slot_Centroid"],
570 ["x", "y"]))
571 self.assertEqual(
572 cat.getShapeSlot().getErrKey(),
573 lsst.afw.table.CovarianceMatrix3fKey(cat.schema["slot_Shape"], ["xx", "yy", "xy"]))
574 # check the flux->instFlux conversion
575 self.assertEqual(cat.schema["a_flux"].asKey(), cat.schema["a_instFlux"].asKey())
576 self.assertEqual(cat.schema["a_fluxSigma"].asKey(), cat.schema["a_instFluxErr"].asKey())
578 def testFitsReadVersion2CompatibilityRealSourceCatalog(self):
579 """DM-15891: some fields were getting aliases they shouldn't have."""
580 cat = lsst.afw.table.SourceCatalog.readFits(
581 os.path.join(testPath, "data", "sourceCatalog-hsc-v2.fits"))
582 self.assertNotIn('base_SdssShape_flux_xxinstFlux', cat.schema)
583 self.assertIn('base_SdssShape_instFlux_xx_Cov', cat.schema)
584 self.assertNotIn('base_Blendedness_abs_flux_cinstFlux', cat.schema)
586 def testFitsReadVersion2CompatibilityRealCoaddMeasCatalog(self):
587 """DM-16068: some fields were not getting aliases they should have
589 In particular, the alias setting relies on flux fields having their
590 units set properly. Prior to a resolution of DM-16068, the units
591 were not getting set for several CModel flux fields and one deblender
592 field (deblend_psfFlux), and thus were not getting the
593 `_flux`->`_instFlux` aliases set.
595 NOTE: this test will (is meant to) fail on the read until DM-16068 is
596 resolved. The error is:
598 lsst::pex::exceptions::NotFoundError: 'Field or subfield with
599 name 'modelfit_CModel_instFlux' not found with type 'D'.'
600 """
601 cat = lsst.afw.table.SourceCatalog.readFits(
602 os.path.join(testPath, "data", "deepCoadd_meas_HSC_v2.fits"))
603 self.assertIn('modelfit_CModel_instFlux', cat.schema)
604 self.assertIn('modelfit_CModel_instFluxErr', cat.schema)
605 self.assertIn('modelfit_CModel_instFlux_inner', cat.schema)
606 self.assertIn('modelfit_CModel_dev_instFlux_inner', cat.schema)
607 self.assertIn('modelfit_CModel_exp_instFlux_inner', cat.schema)
608 self.assertIn('modelfit_CModel_initial_instFlux_inner', cat.schema)
610 def testFitsVersion2Compatibility(self):
611 """Test reading of catalogs with version 2 schema
613 Version 2 catalogs need to have added aliases from `_flux`->`_instFlux`.
614 """
615 cat = lsst.afw.table.SourceCatalog.readFits(os.path.join(testPath, "data", "sourceTable-v2.fits"))
616 # check the flux->instFlux conversion
617 self.assertEqual(cat.schema["a_flux"].asKey(), cat.schema["a_instFlux"].asKey())
618 self.assertEqual(cat.schema["a_fluxErr"].asKey(), cat.schema["a_instFluxErr"].asKey())
620 def testDM1083(self):
621 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
622 st = lsst.afw.table.SourceTable.make(schema)
623 cat = lsst.afw.table.SourceCatalog(st)
624 tmp = lsst.afw.table.SourceCatalog(cat.getTable())
625 record = tmp.addNew()
626 cat.extend(tmp)
627 self.assertEqual(cat[0].getId(), record.getId())
628 # check that the same record is in both catalogs (not a copy)
629 record.setId(15)
630 self.assertEqual(cat[0].getId(), record.getId())
632 def testSlotUndefine(self):
633 """Test that we can correctly define and undefine a slot after a SourceTable has been created"""
634 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
635 key = schema.addField("a_instFlux", type=np.float64, doc="flux field")
636 table = lsst.afw.table.SourceTable.make(schema)
637 table.definePsfFlux("a")
638 self.assertEqual(table.getPsfFluxSlot().getMeasKey(), key)
639 table.schema.getAliasMap().erase("slot_PsfFlux")
640 self.assertFalse(table.getPsfFluxSlot().isValid())
642 def testOldFootprintPersistence(self):
643 """Test that we can still read SourceCatalogs with (Heavy)Footprints saved by an older
644 version of the pipeline with a different format.
645 """
646 filename = os.path.join(testPath, "data", "old-footprint-persistence.fits")
647 catalog1 = lsst.afw.table.SourceCatalog.readFits(filename)
648 self.assertEqual(len(catalog1), 2)
649 with self.assertRaises(LookupError):
650 catalog1.schema.find("footprint")
651 fp1 = catalog1[0].getFootprint()
652 fp2 = catalog1[1].getFootprint()
653 self.assertEqual(fp1.getArea(), 495)
654 self.assertEqual(fp2.getArea(), 767)
655 self.assertFalse(fp1.isHeavy())
656 self.assertTrue(fp2.isHeavy())
657 self.assertEqual(len(fp1.getSpans()), 29)
658 self.assertEqual(len(fp2.getSpans()), 44)
659 self.assertEqual(len(fp1.getPeaks()), 1)
660 self.assertEqual(len(fp2.getPeaks()), 1)
661 self.assertEqual(fp1.getBBox(),
662 lsst.geom.Box2I(lsst.geom.Point2I(129, 2), lsst.geom.Extent2I(25, 29)))
663 self.assertEqual(fp2.getBBox(),
664 lsst.geom.Box2I(lsst.geom.Point2I(1184, 2), lsst.geom.Extent2I(78, 38)))
665 hfp = lsst.afw.detection.HeavyFootprintF(fp2)
666 self.assertEqual(len(hfp.getImageArray()), fp2.getArea())
667 self.assertEqual(len(hfp.getMaskArray()), fp2.getArea())
668 self.assertEqual(len(hfp.getVarianceArray()), fp2.getArea())
669 catalog2 = lsst.afw.table.SourceCatalog.readFits(
670 filename, flags=lsst.afw.table.SOURCE_IO_NO_HEAVY_FOOTPRINTS)
671 self.assertEqual(list(fp1.getSpans()),
672 list(catalog2[0].getFootprint().getSpans()))
673 self.assertEqual(list(fp2.getSpans()),
674 list(catalog2[1].getFootprint().getSpans()))
675 self.assertFalse(catalog2[1].getFootprint().isHeavy())
676 catalog3 = lsst.afw.table.SourceCatalog.readFits(
677 filename, flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS)
678 self.assertEqual(catalog3[0].getFootprint(), None)
679 self.assertEqual(catalog3[1].getFootprint(), None)
681 def _testFluxSlot(self, slotName):
682 """Demonstrate that we can create & use the named Flux slot."""
683 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
684 baseName = "afw_Test"
685 instFluxKey = schema.addField(f"{baseName}_instFlux", type=np.float64, doc="flux")
686 errKey = schema.addField(f"{baseName}_instFluxErr", type=np.float64, doc="flux uncertainty")
687 flagKey = schema.addField(f"{baseName}_flag", type="Flag", doc="flux flag")
688 catalog = lsst.afw.table.SourceCatalog(schema)
689 table = catalog.table
691 # Initially, the slot is undefined.
692 self.assertFalse(getattr(table, f"get{slotName}Slot")().isValid())
694 # After definition, it maps to the keys defined above.
695 getattr(table, f"define{slotName}")(baseName)
696 self.assertTrue(getattr(table, f"get{slotName}Slot")().isValid())
697 self.assertEqual(getattr(table, f"get{slotName}Slot")().getMeasKey(), instFluxKey)
698 self.assertEqual(getattr(table, f"get{slotName}Slot")().getErrKey(), errKey)
699 self.assertEqual(getattr(table, f"get{slotName}Slot")().getFlagKey(), flagKey)
701 # We should be able to retrieve arbitrary values set in records.
702 record = catalog.addNew()
703 instFlux, err, flag = 10.0, 1.0, False
704 record.set(instFluxKey, instFlux)
705 record.set(errKey, err)
706 record.set(flagKey, flag)
707 instFluxName = slotName.replace("Flux", "InstFlux")
708 self.assertEqual(getattr(record, f"get{instFluxName}")(), instFlux)
709 self.assertEqual(getattr(record, f"get{instFluxName}Err")(), err)
710 self.assertEqual(getattr(record, f"get{slotName}Flag")(), flag)
712 # Should also be able to retrieve them as columns from the catalog
713 self.assertEqual(getattr(catalog, f"get{instFluxName}")()[0], instFlux)
714 self.assertEqual(getattr(catalog, f"get{instFluxName}Err")()[0], err)
716 # And we should be able to delete the slot, breaking the mapping.
717 table.schema.getAliasMap().erase(f"slot_{slotName}")
718 self.assertFalse(getattr(table, f"get{slotName}Slot")().isValid())
719 self.assertNotEqual(getattr(table, f"get{slotName}Slot")().getMeasKey(), instFluxKey)
720 self.assertNotEqual(getattr(table, f"get{slotName}Slot")().getErrKey(), errKey)
721 self.assertNotEqual(getattr(table, f"get{slotName}Slot")().getFlagKey(), flagKey)
723 # When the slot has been deleted, attempting to access it should
724 # throw a LogicError.
725 with self.assertRaises(lsst.pex.exceptions.LogicError):
726 getattr(catalog, f"get{instFluxName}")()
727 with self.assertRaises(lsst.pex.exceptions.LogicError):
728 getattr(catalog, f"get{instFluxName}Err")()
730 def testFluxSlots(self):
731 """Check that all the expected flux slots are present & correct."""
732 for slotName in ["ApFlux", "CalibFlux", "GaussianFlux", "ModelFlux",
733 "PsfFlux"]:
734 self._testFluxSlot(slotName)
736 # But, of course, we should not accept a slot which hasn't be defined.
737 with self.assertRaises(AttributeError):
738 self._testFluxSlot("NotExtantFlux")
740 def testStr(self):
741 """Check that the str() produced on a catalog contains expected things."""
742 string = str(self.catalog)
743 for field in ('id', 'coord_ra', 'coord_dec'):
744 self.assertIn(field, string)
746 def testRepr(self):
747 """Check that the repr() produced on a catalog contains expected things."""
748 string = repr(self.catalog)
749 self.assertIn(str(type(self.catalog)), string)
750 for field in ('id', 'coord_ra', 'coord_dec'):
751 self.assertIn(field, string)
753 def testStrNonContiguous(self):
754 """Check that str() doesn't fail on non-contiguous tables."""
755 del self.catalog[1]
756 string = str(self.catalog)
757 self.assertIn('Non-contiguous afw.Catalog of 2 rows.', string)
758 for field in ('id', 'coord_ra', 'coord_dec'):
759 self.assertIn(field, string)
761 def testRecordStr(self):
762 """Test that str(record) contains expected things."""
763 string = str(self.catalog[0])
764 for field in ('id: 50', 'coord_ra: nan', 'coord_dec: nan'):
765 self.assertIn(field, string)
767 def testRecordRepr(self):
768 """Test that repr(record) contains expected things."""
769 string = repr(self.catalog[0])
770 self.assertIn(str(type(self.catalog[0])), string)
771 for field in ('id: 50', 'coord_ra: nan', 'coord_dec: nan'):
772 self.assertIn(field, string)
774 def testGetNonContiguous(self):
775 """Check that we can index on non-contiguous tables"""
776 # Make a non-contiguous catalog
777 nonContiguous = type(self.catalog)(self.catalog.table)
778 for rr in reversed(self.catalog):
779 nonContiguous.append(rr)
780 num = len(self.catalog)
781 # Check assumptions
782 self.assertFalse(nonContiguous.isContiguous()) # We managed to produce a non-contiguous catalog
783 self.assertEqual(len(set(self.catalog["id"])), num) # ID values are unique
784 # Indexing with boolean array
785 select = np.zeros(num, dtype=bool)
786 select[1] = True
787 self.assertEqual(nonContiguous[np.flip(select, 0)]["id"], self.catalog[select]["id"])
788 # Extracting a number column
789 column = "a_instFlux"
790 array = nonContiguous[column]
791 self.assertFloatsEqual(np.flip(array, 0), self.catalog[column])
792 with self.assertRaises(ValueError):
793 array[1] = 1.2345 # Should be immutable
794 # Extracting a flag column
795 column = "a_flag"
796 array = nonContiguous[column]
797 np.testing.assert_equal(np.flip(array, 0), self.catalog[column])
798 with self.assertRaises(ValueError):
799 array[1] = True # Should be immutable
802class MemoryTester(lsst.utils.tests.MemoryTestCase):
803 pass
806def setup_module(module):
807 lsst.utils.tests.init()
810if __name__ == "__main__": 810 ↛ 811line 810 didn't jump to line 811 because the condition on line 810 was never true
811 lsst.utils.tests.init()
812 unittest.main()