Coverage for tests/test_photometryTransform.py: 29%

222 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-25 02:08 -0700

1# This file is part of jointcal. 

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/>. 

21 

22import abc 

23 

24import numpy as np 

25 

26import unittest 

27import lsst.utils.tests 

28 

29import lsst.geom 

30from lsst.jointcal import photometryTransform 

31 

32 

33CHEBYSHEV_T = [ 33 ↛ exitline 33 didn't jump to the function exit

34 lambda x: 1, 

35 lambda x: x, 

36 lambda x: 2*x**2 - 1, 

37 lambda x: (4*x**2 - 3)*x, 

38 lambda x: (8*x**2 - 8)*x**2 + 1, 

39 lambda x: ((16*x**2 - 20)*x**2 + 5)*x, 

40] 

41 

42 

43class PhotometryTransformTestBase: 

44 def setUp(self): 

45 self.value = 5.0 

46 self.valueError = 0.3 

47 self.point = [1., 5.] 

48 

49 

50class SpatiallyInvariantTestBase(PhotometryTransformTestBase): 

51 """Tests for PhotometryTransformSpatiallyInvariant. 

52 Subclasses need to call setUp to define: 

53 self.transform1 == a default initalized PhotometryTransformSpatiallyInvariant. 

54 self.transform2 == a transform initialized with self.t2InitValue. 

55 """ 

56 def setUp(self): 

57 super().setUp() 

58 # initial values for self.transform2 

59 self.t2InitValue = 1000.0 

60 self.t2InitError = 70.0 

61 

62 def _test_transform(self, transform, expect): 

63 result = transform.transform(self.point[0], self.point[1], self.value) 

64 self.assertEqual(result, expect) # yes, I really mean exactly equal 

65 

66 def _test_transformError(self, transform, expect): 

67 result = transform.transformError(self.point[0], self.point[1], self.value, self.valueError) 

68 self.assertFloatsAlmostEqual(result, expect) 

69 

70 def _offsetParams(self, delta, value, expect): 

71 self.transform1.offsetParams(delta) 

72 result = self.transform1.transform(self.point[0], self.point[1], value) 

73 self.assertFloatsAlmostEqual(result, expect) 

74 

75 def _test_offsetParams(self, expect): 

76 """Test offsetting; note that offsetParams offsets by +1.""" 

77 # check that offset by 0 doesn't change anything. 

78 delta = np.zeros(1, dtype=float) 

79 self._offsetParams(delta, self.value, self.value) 

80 

81 # offset by +1 should result in `expect` 

82 delta -= 1 

83 self._offsetParams(delta, self.value, expect) 

84 

85 def test_clone(self): 

86 clone1 = self.transform1.clone() 

87 self.assertEqual(self.transform1.getParameters(), clone1.getParameters()) 

88 clone2 = self.transform2.clone() 

89 self.assertEqual(self.transform2.getParameters(), clone2.getParameters()) 

90 self.assertNotEqual(clone1.getParameters(), clone2.getParameters()) 

91 

92 def _test_computeParameterDerivatives(self, expect): 

93 """The derivative of a spatially invariant transform is always the same. 

94 Should be indepdendent of position 

95 """ 

96 result = self.transform1.computeParameterDerivatives(1, 2, self.value) 

97 self.assertEqual(expect, result) 

98 result = self.transform1.computeParameterDerivatives(-5, -100, self.value) 

99 self.assertEqual(expect, result) 

100 result = self.transform2.computeParameterDerivatives(-1000, 150, self.value) 

101 self.assertEqual(expect, result) 

102 

103 

104class FluxTransformSpatiallyInvariantTestCase(SpatiallyInvariantTestBase, lsst.utils.tests.TestCase): 

105 def setUp(self): 

106 super().setUp() 

107 self.transform1 = photometryTransform.FluxTransformSpatiallyInvariant() 

108 self.transform2 = photometryTransform.FluxTransformSpatiallyInvariant(self.t2InitValue) 

109 

110 def test_transform(self): 

111 self._test_transform(self.transform1, self.value) 

112 self._test_transform(self.transform2, self.value*self.t2InitValue) 

113 

114 def test_transformError(self): 

115 expect = (self.valueError*1) 

116 self._test_transformError(self.transform1, expect) 

117 expect = (self.valueError*self.t2InitValue) 

118 self._test_transformError(self.transform2, expect) 

119 

120 def test_offsetParams(self): 

121 """Offset by +1 means transform by 2.""" 

122 self._test_offsetParams(self.value*2) 

123 

124 def test_computeParameterDerivatives(self): 

125 """Should be indepdendent of position, and equal to the flux.""" 

126 self._test_computeParameterDerivatives(self.value) 

127 

128 

129class MagnitudeTransformSpatiallyInvariantTestCase(SpatiallyInvariantTestBase, lsst.utils.tests.TestCase): 

130 def setUp(self): 

131 super().setUp() 

132 self.transform1 = photometryTransform.MagnitudeTransformSpatiallyInvariant() 

133 self.transform2 = photometryTransform.MagnitudeTransformSpatiallyInvariant(self.t2InitValue) 

134 

135 def test_transform(self): 

136 self._test_transform(self.transform1, self.value) 

137 self._test_transform(self.transform2, self.value + self.t2InitValue) 

138 

139 def test_transformError(self): 

140 expect = self.valueError 

141 self._test_transformError(self.transform1, expect) 

142 expect = self.valueError 

143 self._test_transformError(self.transform2, expect) 

144 

145 def test_offsetParams(self): 

146 """Offset by +1 means transform by +1.""" 

147 self._test_offsetParams(self.value + 1) 

148 

149 def test_computeParameterDerivatives(self): 

150 """Should always be identically 1.""" 

151 self._test_computeParameterDerivatives(1.0) 

152 

153 

154class PhotometryTransformChebyshevTestCase(PhotometryTransformTestBase, abc.ABC): 

155 def setUp(self): 

156 """Call this first, then construct self.transform1 from self.order1, 

157 and self.transform2 from self.coefficients. 

158 """ 

159 super().setUp() 

160 self.bbox = lsst.geom.Box2D(lsst.geom.Point2D(-5, -6), lsst.geom.Point2D(7, 8)) 

161 self.order1 = 2 

162 self.coefficients = np.array([[5, 3], [4, 0]], dtype=float) 

163 

164 # self.transform1 will have 6 parameters, by construction 

165 self.delta = np.arange(6, dtype=float) 

166 # make one of them have opposite sign to check +/- consistency 

167 self.delta[0] = -self.delta[0] 

168 

169 def test_getNpar(self): 

170 self.assertEqual(self.transform1.getNpar(), 6) 

171 self.assertEqual(self.transform2.getNpar(), 3) 

172 

173 def _evaluate_chebyshev(self, x, y): 

174 """Evaluate the chebyshev defined by self.coefficients at (x,y)""" 

175 # sx, sy: transform from self.bbox range to [-1, -1] 

176 cx = (self.bbox.getMinX() + self.bbox.getMaxX())/2.0 

177 cy = (self.bbox.getMinY() + self.bbox.getMaxY())/2.0 

178 sx = 2.0 / self.bbox.getWidth() 

179 sy = 2.0 / self.bbox.getHeight() 

180 result = 0 

181 order = len(self.coefficients) 

182 for j in range(order): 

183 for i in range(0, order-j): 

184 Tx = CHEBYSHEV_T[i](sx*(x - cx)) 

185 Ty = CHEBYSHEV_T[j](sy*(y - cy)) 

186 result += self.coefficients[j, i]*Tx*Ty 

187 return result 

188 

189 def _test_offsetParams(self, expect): 

190 """Test offsetting; note that offsetParams offsets by `-delta`. 

191 

192 Parameters 

193 ---------- 

194 expect1 : `numpy.ndarray`, (N,2) 

195 Expected coefficients from an offset by 0. 

196 expect2 : `numpy.ndarray`, (N,2) 

197 Expected coefficients from an offset by self.delta. 

198 """ 

199 # first offset by all zeros: nothing should change 

200 delta = np.zeros(self.transform1.getNpar(), dtype=float) 

201 self.transform1.offsetParams(delta) 

202 self.assertFloatsAlmostEqual(expect, self.transform1.getCoefficients()) 

203 

204 # now offset by self.delta 

205 expect[0, 0] -= self.delta[0] 

206 expect[0, 1] -= self.delta[1] 

207 expect[0, 2] -= self.delta[2] 

208 expect[1, 0] -= self.delta[3] 

209 expect[1, 1] -= self.delta[4] 

210 expect[2, 0] -= self.delta[5] 

211 self.transform1.offsetParams(self.delta) 

212 self.assertFloatsAlmostEqual(expect, self.transform1.getCoefficients()) 

213 

214 def test_clone(self): 

215 clone1 = self.transform1.clone() 

216 self.assertFloatsEqual(self.transform1.getParameters(), clone1.getParameters()) 

217 self.assertEqual(self.transform1.getOrder(), clone1.getOrder()) 

218 self.assertEqual(self.transform1.getBBox(), clone1.getBBox()) 

219 clone2 = self.transform2.clone() 

220 self.assertFloatsEqual(self.transform2.getParameters(), clone2.getParameters()) 

221 self.assertEqual(self.transform2.getOrder(), clone2.getOrder()) 

222 self.assertEqual(self.transform2.getBBox(), clone2.getBBox()) 

223 

224 @abc.abstractmethod 

225 def _computeChebyshevDerivative(self, Tx, Ty, value): 

226 """Return the derivative of chebyshev component Tx, Ty.""" 

227 pass 

228 

229 def test_computeParameterDerivatives(self): 

230 cx = (self.bbox.getMinX() + self.bbox.getMaxX())/2.0 

231 cy = (self.bbox.getMinY() + self.bbox.getMaxY())/2.0 

232 sx = 2.0 / self.bbox.getWidth() 

233 sy = 2.0 / self.bbox.getHeight() 

234 result = self.transform1.computeParameterDerivatives(self.point[0], self.point[1], self.value) 

235 Tx = np.array([CHEBYSHEV_T[i](sx*(self.point[0] - cx)) for i in range(self.order1+1)], dtype=float) 

236 Ty = np.array([CHEBYSHEV_T[i](sy*(self.point[1] - cy)) for i in range(self.order1+1)], dtype=float) 

237 expect = [] 

238 for j in range(len(Ty)): 

239 for i in range(0, self.order1-j+1): 

240 expect.append(self._computeChebyshevDerivative(Ty[j], Tx[i], self.value)) 

241 self.assertFloatsAlmostEqual(np.array(expect), result) 

242 

243 def testIntegrateBoxOrder0(self): 

244 r"""Test integrating over an "interesting" box. 

245 

246 The values of these integrals were checked in Mathematica. The code 

247 block below can be pasted into Mathematica to re-do those calculations. 

248 

249 .. code-block:: mathematica 

250 

251 f[x_, y_, n_, m_] := \!\( 

252 \*UnderoverscriptBox[\(\[Sum]\), \(i = 0\), \(n\)]\( 

253 \*UnderoverscriptBox[\(\[Sum]\), \(j = 0\), \(m\)] 

254 \*SubscriptBox[\(a\), \(i, j\)]*ChebyshevT[i, x]*ChebyshevT[j, y]\)\) 

255 integrate2dBox[n_, m_, xmin_, xmax_, ymin_, ymax_, x0_, x1_, y0_, 

256 y1_] := \!\( 

257 \*SubsuperscriptBox[\(\[Integral]\), \(y0\), \(y1\)]\( 

258 \*SubsuperscriptBox[\(\[Integral]\), \(x0\), \(x1\)]f[ 

259 \*FractionBox[\(2 x - xmin - xmax\), \(xmax - xmin\)], 

260 \*FractionBox[\(2 y - ymin - ymax\), \(ymax - ymin\)], n, 

261 m] \[DifferentialD]x \[DifferentialD]y\)\) 

262 integrate2dBox[0, 0, -5, 7, -6, 8, 0, 7, 0, 8] 

263 integrate2dBox[0, 0, -5, 7, -6, 8, 2, 6, 3, 5] 

264 integrate2dBox[1, 0, -5, 7, -6, 8, 0, 6, 0, 5] 

265 integrate2dBox[0, 1, -5, 7, -6, 8, 0, 6, 0, 5] 

266 integrate2dBox[1, 1, -5, 7, -6, 8, -1, 5., 2, 7] 

267 integrate2dBox[2, 2, -5, 7, -6, 8, 0, 2, 0, 3] 

268 """ 

269 coeffs = np.array([[3.]], dtype=float) 

270 transform = photometryTransform.FluxTransformChebyshev(coeffs, self.bbox) 

271 

272 # a box that goes from 0,0 to the x/y maximum 

273 box = lsst.geom.Box2D(lsst.geom.Point2D(0, 0), 

274 lsst.geom.Point2D(self.bbox.getMaxX(), self.bbox.getMaxY())) 

275 expect = 56*coeffs[0] 

276 result = transform.integrate(box) 

277 self.assertFloatsAlmostEqual(result, expect) 

278 

279 # Different box 

280 box = lsst.geom.Box2D(lsst.geom.Point2D(2, 3), lsst.geom.Point2D(6, 5)) 

281 expect = 8*coeffs[0] 

282 result = transform.integrate(box) 

283 self.assertFloatsAlmostEqual(result, expect) 

284 

285 def testIntegrateBoxOrder1(self): 

286 """Test integrating 1st order in x or y. 

287 Note that the coefficients are [y,x] ordered. 

288 """ 

289 box = lsst.geom.Box2D(lsst.geom.Point2D(0, 0), lsst.geom.Point2D(6, 5)) 

290 # test 1st order in x: 

291 coeffs = np.array([[2., 5.], [0., 0]], dtype=float) 

292 transform = photometryTransform.FluxTransformChebyshev(coeffs, self.bbox) 

293 # 30*a00 + 10*a10 

294 expect = 30*coeffs[0, 0] + 10*coeffs[0, 1] 

295 result = transform.integrate(box) 

296 self.assertFloatsAlmostEqual(result, expect) 

297 

298 # test 1st order in y: 

299 coeffs = np.array([[2., 0.], [5., 0]], dtype=float) 

300 transform = photometryTransform.FluxTransformChebyshev(coeffs, self.bbox) 

301 # 30*a00 + 45/7*a01 

302 expect = 30*coeffs[0, 0] + 45./7.*coeffs[1, 0] 

303 result = transform.integrate(box) 

304 self.assertFloatsAlmostEqual(result, expect) 

305 

306 def testIntegrateBoxOrder2(self): 

307 """Test integrating 1st order in both x and y. 

308 Note that the coefficients are [y,x] ordered. 

309 """ 

310 # 1st order in both x and y 

311 transform = photometryTransform.FluxTransformChebyshev(2, self.bbox) 

312 # zero, then set the parameters 

313 transform.offsetParams(np.array([1, 0, 0, 0, 0, 0, 0, 0, 0], dtype=float)) 

314 coeffs = np.array([[0, 0, 0], [0, 4, 0], [0, 0, 0]], dtype=float) 

315 transform.offsetParams(-coeffs.flatten()) 

316 

317 # integrate on the smaller box: 

318 box = lsst.geom.Box2D(lsst.geom.Point2D(-1, 2), lsst.geom.Point2D(5, 7)) 

319 # 5/2*(12*a0,0 + 6*a0,1 + 2*a1,0 + a1,1) 

320 expect = 5/2 * (12*coeffs[0, 0] + 6*coeffs[1, 0] + 2*coeffs[0, 1] + coeffs[1, 1]) 

321 

322 result = transform.integrate(box) 

323 self.assertFloatsAlmostEqual(result, expect) 

324 

325 def testIntegrateBoxOrder4(self): 

326 """Test integrating 2nd order in both x and y. 

327 Note that the coefficients are [y,x] ordered. 

328 """ 

329 # for 2nd order in both x and y 

330 box = lsst.geom.Box2D(lsst.geom.Point2D(-3, 0), lsst.geom.Point2D(2, 3)) 

331 coeffs = np.array([[1, 2, 3, 0, 0], [4, 5, 6, 0, 0], [7, 8, 9, 0, 0], 

332 [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], dtype=float) 

333 transform = photometryTransform.FluxTransformChebyshev(coeffs, self.bbox) 

334 

335 # integrating on the full box should match the standard integral 

336 expect = transform.integrate() 

337 result = transform.integrate(self.bbox) 

338 self.assertFloatsAlmostEqual(result, expect, rtol=6e-16) 

339 

340 # 5/3528 * (10584*a00 + 756*a01 - 10152*a02 - 2646*a10 - 189*a11 + 

341 # 2538*a12 - 8036*a20 - 574*a21 + 7708*a22) 

342 expect = 5/3528 * (10584*coeffs[0, 0] + 756*coeffs[1, 0] 

343 - 10152*coeffs[2, 0] - 2646*coeffs[0, 1] 

344 - 189*coeffs[1, 1] + 2538*coeffs[2, 1] 

345 - 8036*coeffs[0, 2] - 574*coeffs[1, 2] 

346 + 7708*coeffs[2, 2]) 

347 result = transform.integrate(box) 

348 self.assertFloatsAlmostEqual(result, expect, rtol=2e-14) 

349 

350 

351class FluxTransformChebyshevTestCase(PhotometryTransformChebyshevTestCase, lsst.utils.tests.TestCase): 

352 def setUp(self): 

353 super().setUp() 

354 self.transform1 = photometryTransform.FluxTransformChebyshev(self.order1, self.bbox) 

355 self.transform2 = photometryTransform.FluxTransformChebyshev(self.coefficients, self.bbox) 

356 

357 def test_transform(self): 

358 result = self.transform1.transform(self.point[0], self.point[1], self.value) 

359 self.assertEqual(result, self.value) # transform1 is the identity 

360 

361 result = self.transform2.transform(self.point[0], self.point[1], self.value) 

362 expect = self.value*self._evaluate_chebyshev(self.point[0], self.point[1]) 

363 self.assertEqual(result, expect) 

364 

365 def test_offsetParams(self): 

366 # an offset by 0 means we will still have 1 only in the 0th parameter 

367 expect = np.zeros((self.order1+1, self.order1+1), dtype=float) 

368 expect[0, 0] = 1 

369 self._test_offsetParams(expect) 

370 

371 def _computeChebyshevDerivative(self, x, y, value): 

372 return x * y * value 

373 

374 

375class MagnitudeTransformChebyshevTestCase(PhotometryTransformChebyshevTestCase, lsst.utils.tests.TestCase): 

376 def setUp(self): 

377 super().setUp() 

378 self.transform1 = photometryTransform.MagnitudeTransformChebyshev(self.order1, self.bbox) 

379 self.transform2 = photometryTransform.MagnitudeTransformChebyshev(self.coefficients, self.bbox) 

380 

381 def test_transform(self): 

382 result = self.transform1.transform(self.point[0], self.point[1], self.value) 

383 self.assertEqual(result, self.value) # transform1 is the identity 

384 

385 result = self.transform2.transform(self.point[0], self.point[1], self.value) 

386 expect = self.value + self._evaluate_chebyshev(self.point[0], self.point[1]) 

387 self.assertEqual(result, expect) 

388 

389 def test_offsetParams(self): 

390 # an offset by 0 means all parameters still 0 

391 expect = np.zeros((self.order1+1, self.order1+1), dtype=float) 

392 self._test_offsetParams(expect) 

393 

394 def _computeChebyshevDerivative(self, x, y, value): 

395 return x * y 

396 

397 

398class MemoryTester(lsst.utils.tests.MemoryTestCase): 

399 pass 

400 

401 

402def setup_module(module): 

403 lsst.utils.tests.init() 

404 

405 

406if __name__ == "__main__": 406 ↛ 407line 406 didn't jump to line 407, because the condition on line 406 was never true

407 lsst.utils.tests.init() 

408 unittest.main()