Coverage for tests/test_softenedLinearPrior.py: 16%
128 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-18 09:06 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-18 09:06 +0000
1#
2# LSST Data Management System
3#
4# Copyright 2008-2016 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
23import unittest
24import numpy
26import lsst.utils.tests
27import lsst.log
28import lsst.utils.logging
29import lsst.meas.modelfit
31try:
32 import scipy.integrate
33except ImportError:
34 scipy = None
36lsst.utils.logging.trace_set_at("lsst.meas.modelfit.SoftenedLinearPrior", 5)
39class SoftenedLinearPriorTestCase(lsst.utils.tests.TestCase):
41 NUM_DIFF_STEP = 1E-3
43 def setUp(self):
44 # a prior with broad ramps and non-zero slope; broad ramps makes evaluating numerical
45 # derivatives easier, and we want to do that to check the analytic ones
46 numpy.random.seed(500)
47 ctrl = lsst.meas.modelfit.SoftenedLinearPrior.Control()
48 ctrl.logRadiusMinOuter = ctrl.logRadiusMinInner - 2.0
49 ctrl.logRadiusMaxOuter = ctrl.logRadiusMaxInner + 2.0
50 ctrl.ellipticityMaxOuter = ctrl.ellipticityMaxInner + 2.0
51 ctrl.logRadiusMinMaxRatio = 2.0
52 self.prior = lsst.meas.modelfit.SoftenedLinearPrior(ctrl)
53 self.amplitudes = numpy.array([1.0], dtype=lsst.meas.modelfit.Scalar)
55 def tearDown(self):
56 del self.prior
57 del self.amplitudes
59 def evaluatePrior(self, e1, e2, r):
60 b = numpy.broadcast(e1, e2, r)
61 p = numpy.zeros(b.shape, dtype=lsst.meas.modelfit.Scalar)
62 for i, (e1i, e2i, ri) in enumerate(b):
63 p.flat[i] = self.prior.evaluate(numpy.array([e1i, e2i, ri]), self.amplitudes)
64 return p
66 def checkDerivatives(self, e1, e2, r):
67 nonlinear = numpy.array([e1, e2, r], dtype=lsst.meas.modelfit.Scalar)
68 amplitudeGradient = numpy.zeros(1, dtype=lsst.meas.modelfit.Scalar)
69 amplitudeHessian = numpy.zeros((1, 1), dtype=lsst.meas.modelfit.Scalar)
70 crossHessian = numpy.zeros((3, 1), dtype=lsst.meas.modelfit.Scalar)
71 nonlinearGradient = numpy.zeros(3, dtype=lsst.meas.modelfit.Scalar)
72 nonlinearHessian = numpy.zeros((3, 3), dtype=lsst.meas.modelfit.Scalar)
73 self.prior.evaluateDerivatives(nonlinear, self.amplitudes,
74 nonlinearGradient, amplitudeGradient,
75 nonlinearHessian, amplitudeHessian,
76 crossHessian)
77 p = self.prior.evaluate(nonlinear, self.amplitudes)
78 for i in range(3):
79 nonlinearA = nonlinear.copy()
80 nonlinearB = nonlinear.copy()
81 nonlinearA[i] -= self.NUM_DIFF_STEP
82 nonlinearB[i] += self.NUM_DIFF_STEP
83 pA = self.prior.evaluate(nonlinearA, self.amplitudes)
84 pB = self.prior.evaluate(nonlinearB, self.amplitudes)
85 dp = (pB - pA) / (2*self.NUM_DIFF_STEP)
86 self.assertFloatsAlmostEqual(nonlinearGradient[i], dp, rtol=1E-3, atol=1E-8)
87 d2p = (pA + pB - 2*p) / self.NUM_DIFF_STEP**2
88 self.assertFloatsAlmostEqual(nonlinearHessian[i, i], d2p, rtol=1E-3, atol=1E-8)
89 for j in range(i+1, 3):
90 nonlinearAA = nonlinearA.copy()
91 nonlinearAB = nonlinearA.copy()
92 nonlinearBA = nonlinearB.copy()
93 nonlinearBB = nonlinearB.copy()
94 nonlinearAA[j] -= self.NUM_DIFF_STEP
95 nonlinearAB[j] += self.NUM_DIFF_STEP
96 nonlinearBA[j] -= self.NUM_DIFF_STEP
97 nonlinearBB[j] += self.NUM_DIFF_STEP
98 pAA = self.prior.evaluate(nonlinearAA, self.amplitudes)
99 pAB = self.prior.evaluate(nonlinearAB, self.amplitudes)
100 pBA = self.prior.evaluate(nonlinearBA, self.amplitudes)
101 pBB = self.prior.evaluate(nonlinearBB, self.amplitudes)
102 d2p = (pBB - pAB - pBA + pAA) / (4*self.NUM_DIFF_STEP**2)
103 self.assertFloatsAlmostEqual(nonlinearHessian[i, j], d2p, rtol=1E-3, atol=1E-8)
105 def testDerivatives(self):
106 """Test that evaluateDerivatives() returns results similar to finite-differences
107 on evaluate().
108 """
109 ctrl = self.prior.getControl()
110 # a single |e| value for each ellipticity zone
111 ellipticityPoints = numpy.array([0.5*ctrl.ellipticityMaxInner,
112 0.5*(ctrl.ellipticityMaxInner + ctrl.ellipticityMaxOuter)])
113 # a single ln(radius) value for each logRadius zone
114 logRadiusPoints = numpy.array([0.5*(ctrl.logRadiusMinOuter + ctrl.logRadiusMinInner),
115 0.5*(ctrl.logRadiusMinInner + ctrl.logRadiusMaxInner),
116 0.5*(ctrl.logRadiusMaxInner + ctrl.logRadiusMaxOuter)])
117 # a range of position angles
118 thetaPoints = numpy.linspace(0.0, numpy.pi, 5)
119 for theta in thetaPoints:
120 for e in ellipticityPoints:
121 e1 = e*numpy.cos(2.0*theta)
122 e2 = e*numpy.sin(2.0*theta)
123 for r in logRadiusPoints:
124 self.checkDerivatives(e1, e2, r)
126 @unittest.skipIf(scipy is None, "could not import scipy")
127 def testIntegral(self):
128 """Test that the prior is properly normalized.
130 Normally, this test has a very low bar, because it's expensive to compute a high-quality
131 numerical integral to compare with. Even so, the scipy integrator does better than it
132 thinks it does, and we use that smaller tolerance for the test. That means this test
133 could fail if something about the scipy integrator changes, because we're not telling it
134 that it has to get as close as it currently is (because doing so would take way too long).
136 If this class is ever changed, we should do at least one of this test with the tolerances
137 tightened.
138 """
139 ctrl = self.prior.getControl()
140 integral, absErr = scipy.integrate.tplquad(
141 self.evaluatePrior,
142 ctrl.logRadiusMinOuter, ctrl.logRadiusMaxOuter,
143 lambda logR: -ctrl.ellipticityMaxOuter,
144 lambda logR: ctrl.ellipticityMaxOuter,
145 lambda logR, e2: -(ctrl.ellipticityMaxOuter**2 - e2**2)**0.5,
146 lambda logR, e2: (ctrl.ellipticityMaxOuter**2 - e2**2)**0.5,
147 epsabs=1.0,
148 epsrel=1.0,
149 )
150 self.assertFloatsAlmostEqual(integral, 1.0, atol=0.01)
152 def testEllipticityDistribution(self):
153 """Test that the ellipticity distribution is constant in the inner region,
154 mononotically decreasing in the ramp, and zero in the outer region, according
155 to evaluate().
156 """
157 ctrl = self.prior.getControl()
158 # a range of |e| values in each ellipticity zone
159 eInnerPoints = numpy.linspace(0.0, ctrl.ellipticityMaxInner, 5)
160 eRampPoints = numpy.linspace(ctrl.ellipticityMaxInner, ctrl.ellipticityMaxOuter, 5)
161 eOuterPoints = numpy.linspace(ctrl.ellipticityMaxOuter, ctrl.ellipticityMaxOuter + 5.0, 5)
162 # a range of position angles
163 thetaPoints = numpy.linspace(0.0, numpy.pi, 5)
164 # a single ln(radius) value for each logRadius zone
165 logRadiusPoints = numpy.array([0.5*(ctrl.logRadiusMinOuter + ctrl.logRadiusMinInner),
166 0.5*(ctrl.logRadiusMinInner + ctrl.logRadiusMaxInner),
167 0.5*(ctrl.logRadiusMaxInner + ctrl.logRadiusMaxOuter)])
168 for logRadius in logRadiusPoints:
169 for theta in thetaPoints:
170 # All inner points should have the same value
171 pInner = self.evaluatePrior(eInnerPoints*numpy.cos(2*theta),
172 eInnerPoints*numpy.sin(2*theta),
173 logRadius)
174 self.assertFloatsAlmostEqual(pInner.mean(), pInner)
176 # Each ramp point should be greater than the next one
177 pRamp = self.evaluatePrior(eRampPoints*numpy.cos(2*theta),
178 eRampPoints*numpy.sin(2*theta),
179 logRadius)
180 numpy.testing.assert_array_less(pRamp[1:], pRamp[:-1])
182 # Each outer point should be zero
183 pOuter = self.evaluatePrior(eOuterPoints*numpy.cos(2*theta),
184 eOuterPoints*numpy.sin(2*theta),
185 logRadius)
186 self.assertFloatsAlmostEqual(pOuter, 0.0)
188 def testLogRadiusDistribution(self):
189 """Test that the ln(radius) distribution is constant in the inner region,
190 mononotically decreasing in the ramps, and zero in the outer regions, according
191 to evaluate().
192 """
193 ctrl = self.prior.getControl()
194 # a range of ln(radius) values in each logRadius zone
195 rLowerOuterPoints = numpy.linspace(ctrl.logRadiusMinOuter - 2.0, ctrl.logRadiusMinOuter, 5)
196 rLowerRampPoints = numpy.linspace(ctrl.logRadiusMinOuter, ctrl.logRadiusMinInner, 5)
197 rInnerPoints = numpy.linspace(ctrl.logRadiusMinInner, ctrl.logRadiusMaxInner, 5)
198 rUpperRampPoints = numpy.linspace(ctrl.logRadiusMaxInner, ctrl.logRadiusMaxOuter, 5)
199 rUpperOuterPoints = numpy.linspace(ctrl.logRadiusMaxOuter, ctrl.logRadiusMaxOuter + 2.0, 5)
201 # a range of position angles
202 thetaPoints = numpy.linspace(0.0, numpy.pi, 5)
203 # a single |e| value for each ellipticity zone
204 ellipticityPoints = numpy.array([0.5*ctrl.ellipticityMaxInner,
205 0.5*(ctrl.ellipticityMaxInner + ctrl.ellipticityMaxOuter)])
206 for ellipticity in ellipticityPoints:
207 for theta in thetaPoints:
208 e1 = ellipticity*numpy.cos(2*theta)
209 e2 = ellipticity*numpy.sin(2*theta)
210 # Outer points should be zero
211 pLowerOuter = self.evaluatePrior(e1, e2, rLowerOuterPoints)
212 self.assertFloatsAlmostEqual(pLowerOuter, 0.0)
213 # Each ramp point should be less than the next one
214 pLowerRamp = self.evaluatePrior(e1, e2, rLowerRampPoints)
215 numpy.testing.assert_array_less(pLowerRamp[:-1], pLowerRamp[1:])
216 # All adjacent inner points should have the same distance between them (constant slope)
217 pInner = self.evaluatePrior(e1, e2, rInnerPoints)
218 diffs = pInner[1:] - pInner[:-1]
219 self.assertFloatsAlmostEqual(diffs.mean(), diffs)
220 # Each ramp point should be greater than the next one
221 pUpperRamp = self.evaluatePrior(e1, e2, rUpperRampPoints)
222 numpy.testing.assert_array_less(pUpperRamp[1:], pUpperRamp[:-1])
223 # Outer points should be zero
224 pUpperOuter = self.evaluatePrior(e1, e2, rUpperOuterPoints)
225 self.assertFloatsAlmostEqual(pUpperOuter, 0.0)
228class TestMemory(lsst.utils.tests.MemoryTestCase):
229 pass
232def setup_module(module):
233 lsst.utils.tests.init()
236if __name__ == "__main__": 236 ↛ 237line 236 didn't jump to line 237, because the condition on line 236 was never true
237 lsst.utils.tests.init()
238 unittest.main()