Coverage for tests/test_wcsUtils.py: 11%
222 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-06 01:56 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-06 01:56 -0800
1#
2# LSST Data Management System
3# Copyright 2017 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22import math
23import unittest
25import numpy as np
26from numpy.testing import assert_allclose
28from lsst.pex.exceptions import TypeError
29from lsst.daf.base import PropertyList
30import lsst.geom
31import lsst.afw.geom as afwGeom
32import lsst.utils.tests
33from lsst.afw.geom import createTrivialWcsMetadata, deleteBasicWcsMetadata, \
34 getCdMatrixFromMetadata, getSipMatrixFromMetadata, getImageXY0FromMetadata, \
35 hasSipMatrix, makeSipMatrixMetadata, makeTanSipMetadata, \
36 computePixelToDistortedPixel
39def makeRotationMatrix(angle, scale):
40 angleRad = angle.asRadians()
41 sinAng = math.sin(angleRad)
42 cosAng = math.cos(angleRad)
43 return np.array([
44 ([cosAng, sinAng]),
45 ([-sinAng, cosAng]),
46 ], dtype=float)*scale
49class BaseTestCase(lsst.utils.tests.TestCase):
50 """Base class for testing computePixelToDistortedPixel
51 """
52 def setUp(self):
53 # define the position and size of one CCD in the focal plane
54 self.pixelSizeMm = 0.024 # mm/pixel
55 self.ccdOrientation = 5*lsst.geom.degrees # orientation of pixel w.r.t. focal plane
56 self.plateScale = 0.15*lsst.geom.arcseconds # angle/pixel
57 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(2000, 4000))
58 self.crpix = lsst.geom.Point2D(1000, 2000)
59 self.crval = lsst.geom.SpherePoint(10, 40, lsst.geom.degrees)
60 self.orientation = -45*lsst.geom.degrees
61 self.scale = 1.0*lsst.geom.arcseconds
62 # position of 0,0 pixel position in focal plane
63 self.ccdPositionMm = lsst.geom.Point2D(25.0, 10.0)
64 self.pixelToFocalPlane = self.makeAffineTransform(
65 offset=lsst.geom.Extent2D(self.ccdPositionMm),
66 rotation=self.ccdOrientation,
67 scale=self.pixelSizeMm,
68 )
69 cdMatrix = afwGeom.makeCdMatrix(scale=self.scale, orientation=self.orientation)
70 self.tanWcs = afwGeom.makeSkyWcs(crpix=self.crpix, crval=self.crval, cdMatrix=cdMatrix)
71 self.radPerMm = self.plateScale.asRadians()/self.pixelSizeMm # at center of field
72 bboxD = lsst.geom.Box2D(self.bbox)
73 self.pixelPoints = bboxD.getCorners()
74 self.pixelPoints.append(bboxD.getCenter())
76 def makeAffineTransform(self, offset=(0, 0), rotation=0*lsst.geom.degrees, scale=1.0):
77 """Make an affine TransformPoint2ToPoint2 that first adds the specified offset,
78 then scales and rotates the result
79 """
80 rotScale = lsst.geom.AffineTransform(lsst.geom.LinearTransform.makeScaling(scale)
81 * lsst.geom.LinearTransform.makeRotation(rotation))
82 offset = lsst.geom.AffineTransform(lsst.geom.Extent2D(*offset))
83 # AffineTransform a*b = b.then(a)
84 return afwGeom.makeTransform(rotScale*offset)
87class ComputePixelToDistortedPixelTestCase(BaseTestCase):
88 """Test lsst.afw.geom.computePixelToDistortedPixel
89 """
91 def testNoDistortion(self):
92 """Test computePixelToDistortedPixel without distortion
94 Use an affine transform for pixelToFocalPlane; the transform returned
95 by computePixelToDistortedPixel should be the identity transform
96 """
97 focalPlaneToFieldAngle = self.makeAffineTransform(scale=self.radPerMm)
98 pixelToDistortedPixel = computePixelToDistortedPixel(
99 pixelToFocalPlane=self.pixelToFocalPlane,
100 focalPlaneToFieldAngle=focalPlaneToFieldAngle,
101 )
102 bboxD = lsst.geom.Box2D(self.bbox)
103 pixelPoints = bboxD.getCorners()
104 pixelPoints.append(bboxD.getCenter())
106 assert_allclose(pixelToDistortedPixel.applyForward(pixelPoints), pixelPoints)
107 assert_allclose(pixelToDistortedPixel.applyInverse(pixelPoints), pixelPoints)
109 def testDistortion(self):
110 """Test computePixelToDistortedPixel with distortion
112 Compare the output of computePixelToDistortedPixel to a non-affine
113 transform for pixelToFocalPlane
114 """
115 # Compute a pixelToDistortedSky that matches self.tanWCS at the center of the field;
116 # the amount of distortion is 10s of pixels over the detector
117 fieldAngleToFocalPlane = afwGeom.makeRadialTransform([0.0, 1/self.radPerMm, 0.0, 1000/self.radPerMm])
118 focalPlaneToFieldAngle = fieldAngleToFocalPlane.inverted()
119 focalPlaneToTanFieldAngle = self.makeAffineTransform(scale=self.radPerMm)
120 pixelToDistortedPixel = computePixelToDistortedPixel(
121 pixelToFocalPlane=self.pixelToFocalPlane,
122 focalPlaneToFieldAngle=focalPlaneToFieldAngle,
123 )
125 tanWcsTransform = afwGeom.TransformPoint2ToSpherePoint(self.tanWcs.getFrameDict())
126 pixelToDistortedSky = pixelToDistortedPixel.then(tanWcsTransform)
128 # At the center of the focal plane both WCS should give the same sky position
129 pixelAtCtr = self.pixelToFocalPlane.applyInverse(lsst.geom.Point2D(0, 0))
130 tanSkyAtCtr = self.tanWcs.pixelToSky(pixelAtCtr)
131 skyAtCtr = pixelToDistortedSky.applyForward(pixelAtCtr)
132 self.assertPairsAlmostEqual(tanSkyAtCtr, skyAtCtr)
134 # At all reasonable sky points the following field angles should be almost equal:
135 # sky -> tanWcs.skyToPixel -> pixelToFocalPlane -> focalPlaneToTanFieldAngle
136 # sky -> pixelToDistortedPixel.applyInverse -> pixelToFocalPlane -> focalPlaneToFieldAngle
137 # where focalPlaneToTanFieldAngle is the linear approximation to
138 # focalPlaneToFieldAngle at the center of the field (where tanWcs and pixelToDistortedSky match),
139 # since for a given pointing, field angle gives position on the sky
140 skyPoints = self.tanWcs.pixelToSky(self.pixelPoints)
142 tanFieldAnglePoints = focalPlaneToTanFieldAngle.applyForward(
143 self.pixelToFocalPlane.applyForward(self.tanWcs.skyToPixel(skyPoints)))
144 fieldAnglePoints = focalPlaneToFieldAngle.applyForward(
145 self.pixelToFocalPlane.applyForward(pixelToDistortedSky.applyInverse(skyPoints)))
146 assert_allclose(tanFieldAnglePoints, fieldAnglePoints)
148 # The inverse should also be true: for a set of field angle points
149 # the following sky positions should be almost equal:
150 # fieldAngle -> fieldAngleToTanFocalPlane -> focalPlaneToPixel -> tanWcs.pixelToSky
151 # fieldAngle -> fieldAngleToFocalPlane -> focalPlaneToPixel -> pixelToDistortedPixel.applyForward
152 focalPlaneToPixel = self.pixelToFocalPlane.inverted()
153 fieldAngleToTanFocalPlane = focalPlaneToTanFieldAngle.inverted()
154 tanSkyPoints2 = self.tanWcs.pixelToSky(
155 focalPlaneToPixel.applyForward(
156 fieldAngleToTanFocalPlane.applyForward(fieldAnglePoints)))
158 skyPoints2 = pixelToDistortedSky.applyForward(
159 focalPlaneToPixel.applyForward(
160 fieldAngleToFocalPlane.applyForward(fieldAnglePoints)))
162 self.assertSpherePointListsAlmostEqual(tanSkyPoints2, skyPoints2)
165class DetailTestCase(lsst.utils.tests.TestCase):
166 """Test functions in the detail sub-namespace
167 """
168 def setUp(self):
169 # Actual WCS from processing Suprime-Cam
170 self.width = 2048
171 self.height = 4177
172 metadata = PropertyList()
173 for name, value in (
174 ('NAXIS', 2),
175 ('EQUINOX', 2000.0000000000),
176 ('RADESYS', "ICRS"),
177 ('CRPIX1', -3232.7544925483),
178 ('CRPIX2', 4184.4881091129),
179 ('CD1_1', -5.6123808607273e-05),
180 ('CD1_2', 2.8951544956703e-07),
181 ('CD2_1', 2.7343044348306e-07),
182 ('CD2_2', 5.6100888336445e-05),
183 ('CRVAL1', 5.6066137655191),
184 ('CRVAL2', -0.60804032498548),
185 ('CUNIT1', "deg"),
186 ('CUNIT2', "deg"),
187 ('A_ORDER', 5),
188 ('A_0_2', 1.9749832126246e-08),
189 ('A_0_3', 9.3734869173527e-12),
190 ('A_0_4', 1.8812994578840e-17),
191 ('A_0_5', -2.3524013652433e-19),
192 ('A_1_1', -9.8443908806559e-10),
193 ('A_1_2', -4.9278297504858e-10),
194 ('A_1_3', -2.8491604610001e-16),
195 ('A_1_4', 2.3185723720750e-18),
196 ('A_2_0', 4.9546089730708e-08),
197 ('A_2_1', -8.8592221672777e-12),
198 ('A_2_2', 3.3560100338765e-16),
199 ('A_2_3', 3.0469486185035e-21),
200 ('A_3_0', -4.9332471706700e-10),
201 ('A_3_1', -5.3126029725748e-16),
202 ('A_3_2', 4.7795824885726e-18),
203 ('A_4_0', 1.3128844828963e-16),
204 ('A_4_1', 4.4014452170715e-19),
205 ('A_5_0', 2.1781986904162e-18),
206 ('B_ORDER', 5),
207 ('B_0_2', -1.0607653075899e-08),
208 ('B_0_3', -4.8693887937365e-10),
209 ('B_0_4', -1.0363305097301e-15),
210 ('B_0_5', 1.9621640066919e-18),
211 ('B_1_1', 3.0340657679481e-08),
212 ('B_1_2', -5.0763819284853e-12),
213 ('B_1_3', 2.8987281654754e-16),
214 ('B_1_4', 1.8253389678593e-19),
215 ('B_2_0', -2.4772849184248e-08),
216 ('B_2_1', -4.9775588352207e-10),
217 ('B_2_2', -3.6806326254887e-16),
218 ('B_2_3', 4.4136985315418e-18),
219 ('B_3_0', -1.7807191001742e-11),
220 ('B_3_1', -2.4136396882531e-16),
221 ('B_3_2', 2.9165413645768e-19),
222 ('B_4_0', 4.1029951148438e-16),
223 ('B_4_1', 2.3711874424169e-18),
224 ('B_5_0', 4.9333635889310e-19),
225 ('AP_ORDER', 5),
226 ('AP_0_1', -5.9740855298291e-06),
227 ('AP_0_2', -2.0433429597268e-08),
228 ('AP_0_3', -8.6810071023434e-12),
229 ('AP_0_4', -2.4974690826778e-17),
230 ('AP_0_5', 1.9819631102516e-19),
231 ('AP_1_0', -4.5896648256716e-05),
232 ('AP_1_1', -1.5248993348644e-09),
233 ('AP_1_2', 5.0283116166943e-10),
234 ('AP_1_3', 4.3796281513144e-16),
235 ('AP_1_4', -2.1447889127908e-18),
236 ('AP_2_0', -4.7550300344365e-08),
237 ('AP_2_1', 1.0924172283232e-11),
238 ('AP_2_2', -4.9862026098260e-16),
239 ('AP_2_3', -5.4470851768869e-20),
240 ('AP_3_0', 5.0130654116966e-10),
241 ('AP_3_1', 6.8649554020012e-16),
242 ('AP_3_2', -4.2759588436342e-18),
243 ('AP_4_0', -3.6306802581471e-16),
244 ('AP_4_1', -5.3885285875084e-19),
245 ('AP_5_0', -1.8802693525108e-18),
246 ('BP_ORDER', 5),
247 ('BP_0_1', -2.6627855995942e-05),
248 ('BP_0_2', 1.1143451873584e-08),
249 ('BP_0_3', 4.9323396530135e-10),
250 ('BP_0_4', 1.1785185735421e-15),
251 ('BP_0_5', -1.6169957016415e-18),
252 ('BP_1_0', -5.7914490267576e-06),
253 ('BP_1_1', -3.0565765766244e-08),
254 ('BP_1_2', 5.7727475030971e-12),
255 ('BP_1_3', -4.0586821113726e-16),
256 ('BP_1_4', -2.0662723654322e-19),
257 ('BP_2_0', 2.3705520015164e-08),
258 ('BP_2_1', 5.0530823594352e-10),
259 ('BP_2_2', 3.8904979943489e-16),
260 ('BP_2_3', -3.8346209540986e-18),
261 ('BP_3_0', 1.9505421473262e-11),
262 ('BP_3_1', 1.7583146713289e-16),
263 ('BP_3_2', -3.4876779564534e-19),
264 ('BP_4_0', -3.3690937119054e-16),
265 ('BP_4_1', -2.0853007589561e-18),
266 ('BP_5_0', -5.5344298912288e-19),
267 ('CTYPE1', "RA---TAN-SIP"),
268 ('CTYPE2', "DEC--TAN-SIP"),
269 ):
270 metadata.set(name, value)
271 self.metadata = metadata
273 def testCreateTrivialWcsAsPropertySet(self):
274 wcsName = "Z" # arbitrary
275 xy0 = lsst.geom.Point2I(47, -200) # arbitrary
276 metadata = createTrivialWcsMetadata(wcsName=wcsName, xy0=xy0)
277 desiredNameValueList = ( # names are missing wcsName suffix
278 ("CRPIX1", 1.0),
279 ("CRPIX2", 1.0),
280 ("CRVAL1", xy0[0]),
281 ("CRVAL2", xy0[1]),
282 ("CTYPE1", "LINEAR"),
283 ("CTYPE2", "LINEAR"),
284 ("CUNIT1", "PIXEL"),
285 ("CUNIT2", "PIXEL"),
286 )
287 self.assertEqual(len(metadata.names(True)), len(desiredNameValueList))
288 for name, value in desiredNameValueList:
289 self.assertEqual(metadata.getScalar(name + wcsName), value)
291 def testDeleteBasicWcsMetadata(self):
292 wcsName = "Q" # arbitrary
293 metadata = createTrivialWcsMetadata(wcsName=wcsName, xy0=lsst.geom.Point2I(0, 0))
294 # add the other keywords that will be deleted
295 for i in range(2):
296 for j in range(2):
297 metadata.set(f"CD{i+1}_{j+1}{wcsName}", 0.2) # arbitrary nonzero value
298 metadata.set(f"WCSAXES{wcsName}", 2)
299 # add a keyword that will not be deleted
300 metadata.set("NAXIS1", 100)
301 self.assertEqual(len(metadata.names(True)), 14)
303 # deleting data for a different WCS will delete nothing
304 deleteBasicWcsMetadata(metadata=metadata, wcsName="B")
305 self.assertEqual(len(metadata.names(True)), 14)
307 # deleting data for the right WCS deletes all but one keyword
308 deleteBasicWcsMetadata(metadata=metadata, wcsName=wcsName)
309 self.assertEqual(len(metadata.names(True)), 1)
310 self.assertEqual(metadata.getScalar("NAXIS1"), 100)
312 # try with a smattering of keywords (should silently ignore the missing ones)
313 metadata.set(f"WCSAXES{wcsName}", 2)
314 metadata.set(f"CD1_2{wcsName}", 0.5)
315 metadata.set(f"CRPIX2{wcsName}", 5)
316 metadata.set(f"CRVAL1{wcsName}", 55)
317 deleteBasicWcsMetadata(metadata=metadata, wcsName=wcsName)
318 self.assertEqual(len(metadata.names(True)), 1)
319 self.assertEqual(metadata.getScalar("NAXIS1"), 100)
321 def testGetImageXY0FromMetadata(self):
322 wcsName = "Z" # arbitrary
323 xy0 = lsst.geom.Point2I(47, -200) # arbitrary with a negative value to check rounding
324 metadata = createTrivialWcsMetadata(wcsName=wcsName, xy0=xy0)
326 # reading the wrong wcsName should be treated as no data available
327 xy0WrongWcsName = getImageXY0FromMetadata(metadata=metadata, wcsName="X", strip=True)
328 self.assertEqual(xy0WrongWcsName, lsst.geom.Point2I(0, 0))
329 self.assertEqual(len(metadata.names(True)), 8)
331 # deleting one of the required keywords should be treated as no data available
332 for namePrefixToRemove in ("CRPIX1", "CRPIX2", "CRVAL1", "CRVAL2"):
333 nameToRemove = namePrefixToRemove + wcsName
334 removedValue = metadata.getScalar(nameToRemove)
335 metadata.remove(nameToRemove)
336 xy0MissingWcsKey = getImageXY0FromMetadata(metadata=metadata, wcsName=wcsName, strip=True)
337 self.assertEqual(xy0MissingWcsKey, lsst.geom.Point2I(0, 0))
338 self.assertEqual(len(metadata.names(True)), 7)
339 # restore removed item
340 metadata.set(nameToRemove, removedValue)
341 self.assertEqual(len(metadata.names(True)), 8)
343 # setting CRPIX1, 2 to something other than 1 should be treated as no data available
344 for i in (1, 2):
345 nameToChange = f"CRPIX{i}{wcsName}"
346 metadata.set(nameToChange, 1.1)
347 xy0WrongWcsName = getImageXY0FromMetadata(metadata=metadata, wcsName=wcsName, strip=True)
348 self.assertEqual(xy0WrongWcsName, lsst.geom.Point2I(0, 0))
349 self.assertEqual(len(metadata.names(True)), 8)
350 # restore altered CRPIX value
351 metadata.set(nameToChange, 1.0)
352 self.assertEqual(len(metadata.names(True)), 8)
354 # use the correct WCS name but don't strip
355 xy0RightWcsName = getImageXY0FromMetadata(metadata, wcsName, strip=False)
356 self.assertEqual(xy0RightWcsName, xy0)
357 self.assertEqual(len(metadata.names(True)), 8)
359 # use the correct WCS and strip usable metadata
360 xy0RightWcsName = getImageXY0FromMetadata(metadata, wcsName, strip=True)
361 self.assertEqual(xy0RightWcsName, xy0)
362 self.assertEqual(len(metadata.names(True)), 0)
364 def testGetSipMatrixFromMetadata(self):
365 """Test getSipMatrixFromMetadata and makeSipMatrixMetadata
366 """
367 for badName in ("X", "AA"):
368 self.assertFalse(hasSipMatrix(self.metadata, badName))
369 with self.assertRaises(TypeError):
370 getSipMatrixFromMetadata(self.metadata, badName)
372 for name in ("A", "B", "AP", "BP"):
373 self.assertTrue(hasSipMatrix(self.metadata, name))
374 sipMatrix = getSipMatrixFromMetadata(self.metadata, name)
375 width = self.metadata.getScalar(f"{name}_ORDER") + 1
376 self.assertEqual(sipMatrix.shape, (width, width))
377 for i in range(width):
378 for j in range(width):
379 # SIP matrix terms use 0-based indexing
380 cardName = f"{name}_{i}_{j}"
381 if self.metadata.exists(cardName):
382 self.assertEqual(sipMatrix[i, j], self.metadata.getScalar(cardName))
383 else:
384 self.assertEqual(sipMatrix[i, j], 0.0)
386 metadata = makeSipMatrixMetadata(sipMatrix, name)
387 for name in metadata.names(False):
388 value = metadata.getScalar(name)
389 if (name.endswith("ORDER")):
390 self.assertEqual(width, value + 1)
391 else:
392 self.assertEqual(value, self.metadata.getScalar(name))
393 self.assertNotEqual(value, 0.0) # 0s are omitted
395 # try metadata with only the ORDER keyword; the matrix should be all zeros
396 # except for the invalid case of order < 0
397 for order in (-3, -1, 0, 3):
398 metadata2 = PropertyList()
399 metadata2.set("W_ORDER", order)
400 if order < 0:
401 # invalid order
402 self.assertFalse(hasSipMatrix(metadata2, "W"))
403 with self.assertRaises(TypeError):
404 getSipMatrixFromMetadata(metadata2, "W")
405 else:
406 self.assertTrue(hasSipMatrix(metadata2, "W"))
407 zeroMatrix = getSipMatrixFromMetadata(metadata2, "W")
408 self.assertEqual(zeroMatrix.shape, (order + 1, order + 1))
409 for i in range(order + 1):
410 for j in range(order + 1):
411 self.assertEqual(zeroMatrix[i, j], 0.0)
413 def testGetCdMatrixFromMetadata(self):
414 cdMatrix = getCdMatrixFromMetadata(self.metadata)
415 for i in range(2):
416 for j in range(2):
417 cardName = f"CD{i + 1}_{j + 1}"
418 self.assertEqual(cdMatrix[i, j], self.metadata.getScalar(cardName))
420 metadata = PropertyList()
421 with self.assertRaises(TypeError):
422 getCdMatrixFromMetadata(metadata)
423 metadata.add("CD2_1", 0.56) # just one term, with an arbitrary value
424 cdMatrix2 = getCdMatrixFromMetadata(metadata)
425 for i in range(2):
426 for j in range(2):
427 # CD matrix terms use 1-based indexing
428 cardName = f"CD{i + 1}_{j + 1}"
429 if i == 1 and j == 0:
430 self.assertEqual(cdMatrix2[i, j], 0.56)
431 else:
432 self.assertEqual(cdMatrix2[i, j], 0.0)
434 def testMakeTanSipMetadata(self):
435 """Test makeTanSipMetadata
436 """
437 crpix = lsst.geom.Point2D(self.metadata.getScalar("CRPIX1") - 1,
438 self.metadata.getScalar("CRPIX2") - 1)
439 crval = lsst.geom.SpherePoint(self.metadata.getScalar("CRVAL1"),
440 self.metadata.getScalar("CRVAL2"), lsst.geom.degrees)
441 cdMatrix = getCdMatrixFromMetadata(self.metadata)
442 sipA = getSipMatrixFromMetadata(self.metadata, "A")
443 sipB = getSipMatrixFromMetadata(self.metadata, "B")
444 sipAp = getSipMatrixFromMetadata(self.metadata, "AP")
445 sipBp = getSipMatrixFromMetadata(self.metadata, "BP")
446 forwardMetadata = makeTanSipMetadata(
447 crpix=crpix,
448 crval=crval,
449 cdMatrix=cdMatrix,
450 sipA=sipA,
451 sipB=sipB,
452 )
453 self.assertFalse(forwardMetadata.exists("AP_ORDER"))
454 self.assertFalse(forwardMetadata.exists("BP_ORDER"))
456 fullMetadata = makeTanSipMetadata(
457 crpix=crpix,
458 crval=crval,
459 cdMatrix=cdMatrix,
460 sipA=sipA,
461 sipB=sipB,
462 sipAp=sipAp,
463 sipBp=sipBp,
464 )
465 for cardName in ("CRPIX1", "CRPIX2", "CRVAL1", "CRVAL2", "CTYPE1", "CTYPE2",
466 "CUNIT1", "CUNIT2", "RADESYS"):
467 self.assertTrue(forwardMetadata.exists(cardName))
468 self.assertTrue(fullMetadata.exists(cardName))
469 for name, matrix in (("A", sipA), ("B", sipB)):
470 self.checkSipMetadata(name, matrix, forwardMetadata)
471 self.checkSipMetadata(name, matrix, fullMetadata)
472 for name, matrix in (("AP", sipAp), ("BP", sipBp)):
473 self.checkSipMetadata(name, matrix, fullMetadata)
475 def checkSipMetadata(self, name, sipMatrix, metadata):
476 width = metadata.getScalar(f"{name}_ORDER") + 1
477 self.assertEqual(width, sipMatrix.shape[0])
478 for i in range(width):
479 for j in range(width):
480 cardName = f"{name}_{i}_{j}"
481 value = sipMatrix[i, j]
482 if value != 0 or metadata.exists(cardName):
483 self.assertEqual(value, metadata.getScalar(cardName))
486class MemoryTester(lsst.utils.tests.MemoryTestCase):
487 pass
490def setup_module(module):
491 lsst.utils.tests.init()
494if __name__ == "__main__": 494 ↛ 495line 494 didn't jump to line 495, because the condition on line 494 was never true
495 lsst.utils.tests.init()
496 unittest.main()