Coverage for tests/test_profiles.py: 27%
75 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-26 15:49 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-26 15:49 +0000
1#
2# LSST Data Management System
3# Copyright 2008-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#
23import unittest
24import os
26import numpy as np
28import lsst.utils.tests
29import lsst.geom
30import lsst.afw.geom.ellipses as el
31import lsst.shapelet.tractor
32import lsst.shapelet.tests
33import lsst.afw.image
35# These parameters match those used to generate the check images; see
36# tests/data/generate.py
37GALAXY_RADIUS = 8.0
38PSF_SIGMA = 2.0
39E1 = 0.3
40E2 = -0.2
41PROFILES = [
42 ("exp", 9, 8),
43 ("dev", 9, 8),
44]
46CHECK_COMPONENT_IMAGES = False
49class ProfileTestCase(lsst.shapelet.tests.ShapeletTestCase):
51 def testRadii(self):
52 """Check RadialProfile definitions of moments and half-light radii.
53 """
54 s = np.linspace(-20.0, 20.0, 1000)
55 x, y = np.meshgrid(s, s)
56 r = (x**2 + y**2)**0.5
57 dxdy = (s[1] - s[0])**2
58 for name in ["gaussian", "exp", "ser2", "luv", "lux"]:
59 profile = lsst.shapelet.RadialProfile.get(name)
60 z = profile.evaluate(r) * dxdy
61 # lux and luv don't use the true half-light radius; instead they use the half-light radius
62 # of the exp and dev profiles they approximate
63 if not name.startswith("lu"):
64 self.assertFloatsAlmostEqual(z[r < 1].sum(), 0.5*z.sum(), rtol=0.01)
65 # lhs of this comparison is the moments radius (using a sum approximation to the integral)
66 self.assertFloatsAlmostEqual(((z*x**2).sum() / z.sum())**0.5,
67 profile.getMomentsRadiusFactor(), rtol=0.01)
69 def testGaussian(self):
70 """Test that the Gaussian profile's shapelet 'approximation' is actually exact.
71 """
72 profile = lsst.shapelet.RadialProfile.get("gaussian")
73 r = np.linspace(0.0, 4.0, 100)
74 z1 = profile.evaluate(r)
75 basis = profile.getBasis(1)
76 z2 = lsst.shapelet.tractor.evaluateRadial(basis, r, sbNormalize=True)[0, :]
77 self.assertFloatsAlmostEqual(z1, z2, rtol=1E-8)
79 def testShapeletApproximations(self):
80 psf0 = lsst.shapelet.ShapeletFunction(0, lsst.shapelet.HERMITE, PSF_SIGMA)
81 psf0.getCoefficients()[:] = 1.0 / lsst.shapelet.ShapeletFunction.FLUX_FACTOR
82 psf = lsst.shapelet.MultiShapeletFunction()
83 psf.addComponent(psf0)
84 psf.normalize()
85 ellipse = el.Separable[el.Distortion, el.DeterminantRadius](E1, E2, GALAXY_RADIUS)
86 for name, nComponents, maxRadius in PROFILES:
87 # check1 is the multi-Gaussian approximation, as convolved and evaluated by GalSim,
88 check1 = lsst.afw.image.ImageD(os.path.join("tests", "data", name + "-approx.fits")).getArray()
89 xc = check1.shape[1] // 2
90 yc = check1.shape[0] // 2
91 xb = np.arange(check1.shape[1], dtype=float) - xc
92 yb = np.arange(check1.shape[0], dtype=float) - yc
93 xg, yg = np.meshgrid(xb, yb)
95 basis = lsst.shapelet.RadialProfile.get(name).getBasis(nComponents, maxRadius)
96 builder = lsst.shapelet.MatrixBuilderD.Factory(xg.ravel(), yg.ravel(), basis, psf)()
97 image1 = np.zeros(check1.shape, dtype=float)
98 matrix = image1.reshape(check1.size, 1)
99 builder(matrix, el.Ellipse(ellipse))
100 self.assertFloatsAlmostEqual(check1, image1, plotOnFailure=False, rtol=5E-5, relTo=check1.max())
101 msf = basis.makeFunction(el.Ellipse(ellipse, lsst.geom.Point2D(xc, yc)),
102 np.array([1.0], dtype=float))
103 msf = msf.convolve(psf)
104 image2 = np.zeros(check1.shape, dtype=float)
105 msf.evaluate().addToImage(lsst.afw.image.ImageD(image2, False))
106 self.assertFloatsAlmostEqual(check1, image2, plotOnFailure=False, rtol=5E-5, relTo=check1.max())
108 if name == 'exp':
109 # check2 is the exact profile, again by GalSim.
110 # We only check exp against the exact profile. The other approximations are less
111 # accurate, and we only really need to test one. The real measure of whether these
112 # profiles are good enough is more complicated than what we can do in a unit test.
113 check2 = lsst.afw.image.ImageD(
114 os.path.join("tests", "data", name + "-exact.fits")
115 ).getArray()
116 self.assertFloatsAlmostEqual(check2, image1, plotOnFailure=False,
117 rtol=1E-3, relTo=check2.max())
119 if CHECK_COMPONENT_IMAGES:
120 # This was once useful for debugging test failures, and may be again, but it's
121 # redundant with the above and requires putting more check images in git, so
122 # it's disabled by default.
123 for n, sf in enumerate(msf.getComponents()):
124 check = lsst.afw.image.ImageD(
125 os.path.join("tests", "data", "%s-approx-%0d.fits" % (name, n))
126 ).getArray()
127 image = np.zeros(check1.shape, dtype=float)
128 sf.evaluate().addToImage(lsst.afw.image.ImageD(image, False))
129 self.assertFloatsAlmostEqual(check, image, plotOnFailure=False,
130 rtol=5E-5, relTo=check1.max())
133class MemoryTester(lsst.utils.tests.MemoryTestCase):
134 pass
137def setup_module(module):
138 lsst.utils.tests.init()
141if __name__ == "__main__": 141 ↛ 142line 141 didn't jump to line 142, because the condition on line 141 was never true
142 lsst.utils.tests.init()
143 unittest.main()