Coverage for tests/test_sdssShapePsf.py: 21%
103 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-11 03:06 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-11 03:06 -0800
1#
2# LSST Data Management System
3# Copyright 2008-2017 AURA/LSST.
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 <https://www.lsstcorp.org/LegalNotices/>.
21#
23import unittest
25import numpy as np
27import lsst.geom
28import lsst.afw.geom as afwGeom
29import lsst.afw.image as afwImage
30import lsst.afw.math as afwMath
31import lsst.meas.algorithms as measAlg
32import lsst.meas.base as measBase
33import lsst.meas.base.tests as measBaseTests
34import lsst.utils.tests
37class SdssShapePsfTestCase(measBaseTests.AlgorithmTestCase, lsst.utils.tests.TestCase):
38 """Test case to ensure base_SdssShape_psf is being measured at source position
40 Note: this test lives here in meas_algorithms rather than meas_base (where SdssShape
41 lives) due to the need to apply a spatially varying PSF model such that the PSF is
42 different at each position in the image. This varying PSF model is built with
43 meas_algorithms' PcaPsf (which is not accessible from meas_base).
44 """
45 def setUp(self):
46 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-20, -30), lsst.geom.Extent2I(240, 160))
47 self.dataset = measBaseTests.TestDataset(self.bbox)
48 # first two sources are points
49 self.pointCentroid1 = lsst.geom.Point2D(50.1, 49.8)
50 self.pointCentroid2 = lsst.geom.Point2D(-11.6, -1.7)
51 self.dataset.addSource(instFlux=1E5, centroid=self.pointCentroid1)
52 self.dataset.addSource(instFlux=2E5, centroid=self.pointCentroid2)
53 # third source is extended
54 self.extendedCentroid = lsst.geom.Point2D(149.9, 50.3)
55 self.dataset.addSource(instFlux=1E5, centroid=self.extendedCentroid,
56 shape=afwGeom.Quadrupole(8, 9, 3))
57 self.config = self.makeSingleFrameMeasurementConfig("base_SdssShape")
59 def tearDown(self):
60 del self.bbox
61 del self.dataset
62 del self.pointCentroid1
63 del self.pointCentroid2
64 del self.extendedCentroid
65 del self.config
67 def _computeVaryingPsf(self):
68 """Compute a varying PSF as a linear combination of PCA (== Karhunen-Loeve) basis functions
70 We simply desire a PSF that is not constant across the image, so the precise choice of
71 parameters (e.g., sigmas, setSpatialParameters) are not crucial.
72 """
73 kernelSize = 31
74 sigma1 = 1.75
75 sigma2 = 2.0*sigma1
76 basisKernelList = []
77 for sigma in (sigma1, sigma2):
78 basisKernel = afwMath.AnalyticKernel(kernelSize, kernelSize,
79 afwMath.GaussianFunction2D(sigma, sigma))
80 basisImage = afwImage.ImageD(basisKernel.getDimensions())
81 basisKernel.computeImage(basisImage, True)
82 basisImage /= np.sum(basisImage.getArray())
83 if sigma == sigma1:
84 basisImage0 = basisImage
85 else:
86 basisImage -= basisImage0
87 basisKernelList.append(afwMath.FixedKernel(basisImage))
89 order = 1
90 spFunc = afwMath.PolynomialFunction2D(order)
91 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
92 exactKernel.setSpatialParameters([[1.0, 0, 0], [0.0, 0.5E-2, 0.2E-2]])
93 exactPsf = measAlg.PcaPsf(exactKernel)
95 return exactPsf
97 def _runMeasurementTask(self, psf=None):
98 task = self.makeSingleFrameMeasurementTask("base_SdssShape", config=self.config)
99 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=1234)
100 if psf:
101 exposure.setPsf(psf)
102 task.run(catalog, exposure)
103 return exposure, catalog
105 def _checkPsfShape(self, result, psfResult, psfTruth):
106 self.assertFloatsAlmostEqual(psfResult.getIxx(), psfTruth.getIxx(), rtol=1E-4)
107 self.assertFloatsAlmostEqual(psfResult.getIyy(), psfTruth.getIyy(), rtol=1E-4)
108 self.assertFloatsAlmostEqual(psfResult.getIxy(), psfTruth.getIxy(), rtol=1E-4)
109 self.assertFalse(result.getFlag(measBase.SdssShapeAlgorithm.PSF_SHAPE_BAD.number))
111 def testMeasureGoodPsf(self):
112 """Test that we measure shapes and record the PSF shape correctly
114 To ensure this, apply a varying PSF to the image such that different positions
115 can be distinguished by their different PSF model shapes.
116 """
117 # Apply varying PSF model to the exposure
118 varyingPsf = self._computeVaryingPsf()
119 exposure, catalog = self._runMeasurementTask(psf=varyingPsf)
120 key = measBase.SdssShapeResultKey(catalog.schema["base_SdssShape"])
121 # First make sure we did indeed get a varying PSF model across the exposure
122 psf = exposure.getPsf()
123 # Compare truth PSF at positions of two point sources
124 self.assertFloatsNotEqual(psf.computeShape(self.pointCentroid1).getIxx(),
125 psf.computeShape(self.pointCentroid2).getIxx(), rtol=1E-1)
126 self.assertFloatsNotEqual(psf.computeShape(self.pointCentroid1).getIyy(),
127 psf.computeShape(self.pointCentroid2).getIyy(), rtol=1E-1)
128 # Compare truth PSF at average position vs. truth PSF at extended source position
129 self.assertFloatsNotEqual(psf.computeShape(self.extendedCentroid).getIxx(),
130 psf.computeShape(psf.getAveragePosition()).getIxx(), rtol=1E-1)
131 self.assertFloatsNotEqual(psf.computeShape(self.extendedCentroid).getIyy(),
132 psf.computeShape(psf.getAveragePosition()).getIyy(), rtol=1E-1)
133 # Now check the base_SdssShape_psf entries against the PSF truth values
134 for record in catalog:
135 psfTruth = psf.computeShape(lsst.geom.Point2D(record.getX(), record.getY()))
136 result = record.get(key)
137 psfResult = key.getPsfShape(record)
138 self._checkPsfShape(result, psfResult, psfTruth)
140 def testResizedPcaPsf(self):
141 """Test that PcaPsf can resize itself.
143 This test resides here because PcaPsfs do not have their own test module"""
144 psf = self._computeVaryingPsf()
145 dim = psf.computeBBox(psf.getAveragePosition()).getDimensions()
146 for pad in [0, 4, -2]:
147 resizedPsf = psf.resized(dim.getX() + pad, dim.getY() + pad)
148 self.assertEqual(
149 resizedPsf.computeBBox(resizedPsf.getAveragePosition()).getDimensions(),
150 lsst.geom.Extent2I(dim.getX() + pad, dim.getY() + pad)
151 )
152 if psf.getKernel().isSpatiallyVarying():
153 self.assertEqual(resizedPsf.getKernel().getSpatialParameters(),
154 psf.getKernel().getSpatialParameters())
155 else:
156 self.assertEqual(resizedPsf.getKernel().getKernelParameters(),
157 psf.getKernel().getKernelParameters())
158 self._compareKernelImages(resizedPsf, psf)
160 def _compareKernelImages(self, psf1, psf2):
161 """Test that overlapping portions of kernel images are identical
162 """
163 # warning: computeKernelImage modifies kernel parameters if spatially varying
164 im1 = psf1.computeKernelImage(psf1.getAveragePosition())
165 im2 = psf2.computeKernelImage(psf2.getAveragePosition())
166 bboxIntersection = im1.getBBox()
167 bboxIntersection.clip(im2.getBBox())
168 im1Intersection = afwImage.ImageD(im1, bboxIntersection)
169 im2Intersection = afwImage.ImageD(im2, bboxIntersection)
170 scale1 = im1.getArray().sum() / im1Intersection.getArray().sum()
171 scale2 = im2.getArray().sum() / im2Intersection.getArray().sum()
172 im1Arr = scale1 * im1Intersection.getArray()
173 im2Arr = scale2 * im2Intersection.getArray()
174 self.assertTrue(np.allclose(im1Arr, im2Arr),
175 "kernel images %s, %s do not match" % (im1Arr, im2Arr))
178class TestMemory(lsst.utils.tests.MemoryTestCase):
179 pass
182def setup_module(module):
183 lsst.utils.tests.init()
186if __name__ == "__main__": 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true
187 lsst.utils.tests.init()
188 unittest.main()