Coverage for tests/test_photometryMapping.py: 26%

190 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-04-12 20:44 +0000

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 numpy as np 

23 

24import abc 

25import unittest 

26import lsst.utils.tests 

27 

28import lsst.geom 

29import lsst.jointcal.photometryMappings 

30import lsst.jointcal.photometryTransform 

31import lsst.jointcal.star 

32 

33 

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

35 lambda x: 1, 

36 lambda x: x, 

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

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

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

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

41] 

42 

43 

44class PhotometryMappingTestBase: 

45 def setUp(self): 

46 self.value = 5.0 

47 self.valueErr = 2.0 

48 

49 baseStar0 = lsst.jointcal.star.BaseStar(0, 0, 1, 2) 

50 self.star0 = lsst.jointcal.star.MeasuredStar(baseStar0) 

51 baseStar1 = lsst.jointcal.star.BaseStar(1, 2, 3, 4) 

52 self.star1 = lsst.jointcal.star.MeasuredStar(baseStar1) 

53 self.star1.setXFocal(2) 

54 self.star1.setYFocal(3) 

55 

56 

57class PhotometryMappingTestCase(PhotometryMappingTestBase, lsst.utils.tests.TestCase): 

58 def setUp(self): 

59 super(PhotometryMappingTestCase, self).setUp() 

60 self.scale = 3 

61 transform = lsst.jointcal.photometryTransform.FluxTransformSpatiallyInvariant(self.scale) 

62 self.mapping = lsst.jointcal.photometryMappings.PhotometryMapping(transform) 

63 

64 def test_getNpar(self): 

65 result = self.mapping.getNpar() 

66 self.assertEqual(result, 1) 

67 

68 def _test_offsetParams(self, delta, expect): 

69 self.mapping.offsetParams(delta) 

70 self.assertFloatsAlmostEqual(expect, self.mapping.getTransform().getParameters()) 

71 

72 def test_transform(self): 

73 result = self.mapping.transform(self.star0, self.value) 

74 self.assertEqual(result, self.value*self.scale) 

75 

76 def test_offsetParams(self): 

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

78 delta = np.array([0.0]) 

79 self._test_offsetParams(delta, np.array([self.scale])) 

80 delta -= 1 

81 self._test_offsetParams(delta, self.scale-delta) 

82 

83 def test_computeParameterDerivatives(self): 

84 """Test that the derivative of a spatially invariant transform is always the same.""" 

85 result = self.mapping.computeParameterDerivatives(self.star0, self.value) 

86 self.assertEqual(self.value, result) 

87 result = self.mapping.computeParameterDerivatives(self.star1, self.value) 

88 self.assertEqual(self.value, result) 

89 transform = lsst.jointcal.FluxTransformSpatiallyInvariant(1000.0) 

90 mapping = lsst.jointcal.PhotometryMapping(transform) 

91 result = mapping.computeParameterDerivatives(self.star0, self.value) 

92 self.assertEqual(self.value, result) 

93 

94 def test_getMappingIndices(self): 

95 """A mapping with one invariant transform has one index""" 

96 self.mapping.setIndex(5) 

97 result = self.mapping.getMappingIndices() 

98 self.assertEqual(result, [5]) 

99 

100 

101class ChipVisitPhotometryMappingTestCase(PhotometryMappingTestBase, abc.ABC): 

102 def setUp(self): 

103 super().setUp() 

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

105 self.order = 1 

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

107 self.chipScale = 2 

108 self.visitScale = 3 

109 self.chipIndex = 5 

110 self.visitIndex = 1000 

111 

112 def _initMappings(self, InvariantTransform, ChebyTransform, ChipVisitMapping): 

113 """Initialize self.mappingInvariants and self.mappingCheby. 

114 Call after setUp(). 

115 

116 Parameters 

117 ---------- 

118 InvariantTransform : `PhotometryTransformSpatiallyInvariant`-type 

119 The PhotometryTransformSpatiallyInvariant-derived class to construct 

120 invariant transforms for. 

121 ChebyTransform : `PhotometryTransform`-type 

122 The PhotometryTransformChebyshev-derived class to construct 

123 2d transforms for. 

124 ChipVisitMapping : `PhotometryMapping`-type 

125 The PhotometryMapping-derived class to construct for both mappings. 

126 """ 

127 # self.mappingInvariants has two trivial transforms in it, to serve 

128 # as a simpler test of functionality. 

129 chipTransform = InvariantTransform(self.chipScale) 

130 chipMapping = lsst.jointcal.PhotometryMapping(chipTransform) 

131 chipMapping.setIndex(self.chipIndex) 

132 visitTransform = InvariantTransform(self.visitScale) 

133 visitMapping = lsst.jointcal.PhotometryMapping(visitTransform) 

134 visitMapping.setIndex(self.visitIndex) 

135 self.mappingInvariants = ChipVisitMapping(chipMapping, visitMapping) 

136 self.mappingInvariants.setWhatToFit(True, True) # default to fitting both 

137 

138 # self.mappingCheby is a more realistic mapping, with two components: 

139 # spatially-invariant per chip and a chebyshev per visit. 

140 # Need a new chipMapping, as it stores shared_ptr to the transform. 

141 chipTransform = InvariantTransform(self.chipScale) 

142 chipMapping = lsst.jointcal.PhotometryMapping(chipTransform) 

143 chipMapping.setIndex(self.chipIndex) 

144 visitTransform2 = ChebyTransform(self.coefficients, self.bbox) 

145 visitMapping2 = lsst.jointcal.PhotometryMapping(visitTransform2) 

146 visitMapping2.setIndex(self.visitIndex) 

147 self.mappingCheby = ChipVisitMapping(chipMapping, visitMapping2) 

148 self.mappingCheby.setWhatToFit(True, True) # default to fitting both 

149 

150 def test_getNpar(self): 

151 result = self.mappingInvariants.getNpar() 

152 self.assertEqual(result, 2) 

153 # order 1 implies 3 parameters, plus one for the chip mapping 

154 result = self.mappingCheby.getNpar() 

155 self.assertEqual(result, 4) 

156 

157 def _evaluate_chebyshev(self, x, y): 

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

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

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

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

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

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

164 result = 0 

165 for j in range(self.order+1): 

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

167 for i in range(0, self.order-j+1): 

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

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

170 return result 

171 

172 def _computeChebyshevDerivative(self, star): 

173 """Return the derivatives w.r.t. the Chebyshev components.""" 

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

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

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

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

178 Tx = np.array([CHEBYSHEV_T[i](sx*(star.getXFocal() - cx)) 

179 for i in range(self.order+1)], dtype=float) 

180 Ty = np.array([CHEBYSHEV_T[i](sy*(star.getYFocal() - cy)) 

181 for i in range(self.order+1)], dtype=float) 

182 expect = [] 

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

184 for i in range(0, self.order-j+1): 

185 expect.append(Ty[j]*Tx[i]) 

186 return np.array(expect) 

187 

188 @abc.abstractmethod 

189 def _computeVisitDerivative(self, star): 

190 """Return the derivative w.r.t. the chebyshev visit component.""" 

191 pass 

192 

193 @abc.abstractmethod 

194 def _computeChipDerivative(self, star): 

195 """Return the derivative w.r.t. the chip component.""" 

196 pass 

197 

198 def test_getMappingIndices(self): 

199 """There are npar indices in a constrained mapping.""" 

200 expect = [self.chipIndex, self.visitIndex] 

201 result = self.mappingInvariants.getMappingIndices() 

202 self.assertEqual(result, expect) 

203 

204 # npar - 1 because the chip mapping has the 1st parameter 

205 expect = [self.chipIndex, ] + list(range(self.visitIndex, 

206 self.visitIndex + self.mappingCheby.getNpar() - 1)) 

207 result = self.mappingCheby.getMappingIndices() 

208 self.assertEqual(result, expect) 

209 

210 def _test_transform_mappingInvariants(self, star, expect): 

211 result = self.mappingInvariants.transform(star, self.value) 

212 self.assertEqual(result, expect) 

213 

214 def _test_transform_mappingCheby(self, star, expect): 

215 result = self.mappingCheby.transform(star, self.value) 

216 self.assertEqual(result, expect) 

217 

218 def _test_computeParameterDerivatives(self, star, expectInvariant): 

219 """Test self.mappingInvariants and self.mappingCheby transforming star. 

220 expectCheby is calculated from _computeChipDerivative and 

221 _computeChebyshevDerivative. 

222 """ 

223 result = self.mappingInvariants.computeParameterDerivatives(star, self.value) 

224 self.assertFloatsAlmostEqual(result, expectInvariant) 

225 

226 # the chip derivative is a single number 

227 expectCheby = [self._computeChipDerivative(self.star1)] 

228 # the Chebyshev Derivatives are a list, so we have to use extend 

229 expectCheby.extend(self._computeVisitDerivative(self.star1)) 

230 expectCheby = np.array(expectCheby) 

231 result = self.mappingCheby.computeParameterDerivatives(star, self.value) 

232 self.assertFloatsAlmostEqual(result, expectCheby) 

233 

234 def _test_setWhatToFit(self, fittingChips, fittingVisits, nPar, indices, derivatives): 

235 """ 

236 Parameters 

237 ---------- 

238 fittingChips : `bool` 

239 Are we fitting the chip component? 

240 Passed to ``self.mappingCheby.setWhatToFit()``. 

241 fittingVisits : `bool` 

242 Are we fitting the visit component? 

243 Passed to ``self.mappingCheby.setWhatToFit()``. 

244 nPar : `int` 

245 Expected result from ``self.mappingCheby.getNpar()``. 

246 indices : `list` 

247 Expected result from ``self.mappingCheby.getMappingIndices()``. 

248 derivatives : `list` 

249 Expected result from ``self.mappingCheby.computeParameterDerivatives()``. 

250 """ 

251 self.mappingCheby.setWhatToFit(fittingChips, fittingVisits) 

252 self.assertEqual(self.mappingCheby.getNpar(), nPar) 

253 self.assertEqual(self.mappingCheby.getMappingIndices(), indices) 

254 result = self.mappingCheby.computeParameterDerivatives(self.star1, self.value) 

255 self.assertFloatsAlmostEqual(result, derivatives) 

256 

257 def test_setWhatToFit(self): 

258 """Test that mapping methods behave correctly when chip and/or visit 

259 fitting is disabled. 

260 

261 The "fit both" case (True, True) is tested by all of the above tests. 

262 """ 

263 # Using mappingCheby so getNpar() will distinguish chips (1 param) from visits (3 params). 

264 

265 # fit nothing means 0 parameters and no indices 

266 self._test_setWhatToFit(False, False, 0, [], []) 

267 

268 # fit just chips means 1 parameter and one index [self.chipIndex] 

269 self._test_setWhatToFit(True, False, 1, [self.chipIndex], 

270 np.array([self._computeChipDerivative(self.star1)])) 

271 

272 # fit just visits means 3 parameters (order 1) and 3 indices starting at self.visitIndex 

273 self._test_setWhatToFit(False, True, 3, list(range(self.visitIndex, self.visitIndex+3)), 

274 np.array([self._computeVisitDerivative(self.star1)])) 

275 

276 

277class ChipVisitFluxMappingTestCase(ChipVisitPhotometryMappingTestCase, lsst.utils.tests.TestCase): 

278 def setUp(self): 

279 super().setUp() 

280 self._initMappings(lsst.jointcal.FluxTransformSpatiallyInvariant, 

281 lsst.jointcal.FluxTransformChebyshev, 

282 lsst.jointcal.ChipVisitFluxMapping) 

283 

284 def _computeVisitDerivative(self, star): 

285 return self._computeChebyshevDerivative(star) * self.value * self.chipScale 

286 

287 def _computeChipDerivative(self, star): 

288 return self.value * self._evaluate_chebyshev(star.getXFocal(), star.getYFocal()) 

289 

290 def test_transform(self): 

291 expect = self.value * self.chipScale * self.visitScale 

292 self._test_transform_mappingInvariants(self.star0, expect) 

293 # The doubly-spatially invariant mapping should be independent of star position. 

294 self._test_transform_mappingInvariants(self.star1, expect) 

295 

296 expect = self.value * self.chipScale * self._evaluate_chebyshev(self.star0.getXFocal(), 

297 self.star0.getYFocal()) 

298 self._test_transform_mappingCheby(self.star0, expect) 

299 expect = self.value * self.chipScale * self._evaluate_chebyshev(self.star1.getXFocal(), 

300 self.star1.getYFocal()) 

301 self._test_transform_mappingCheby(self.star1, expect) 

302 

303 def test_computeParameterDerivatives(self): 

304 expectInvariant = np.array([self.value*self.visitScale, self.value*self.chipScale]) 

305 self._test_computeParameterDerivatives(self.star1, expectInvariant) 

306 

307 

308class ChipVisitMagnitudeMappingTestCase(ChipVisitPhotometryMappingTestCase, lsst.utils.tests.TestCase): 

309 def setUp(self): 

310 super().setUp() 

311 self._initMappings(lsst.jointcal.MagnitudeTransformSpatiallyInvariant, 

312 lsst.jointcal.MagnitudeTransformChebyshev, 

313 lsst.jointcal.ChipVisitMagnitudeMapping) 

314 

315 def _computeVisitDerivative(self, star): 

316 return self._computeChebyshevDerivative(star) 

317 

318 def _computeChipDerivative(self, star): 

319 # Magnitude chip derivative is always identically 1: 

320 # d(M(m))/d(m0)=1 where M(m) = m + m0 

321 return 1.0 

322 

323 def test_transform(self): 

324 expect = self.value + self.chipScale + self.visitScale 

325 self._test_transform_mappingInvariants(self.star0, expect) 

326 # The doubly-spatially invariant mapping should be independent of star position. 

327 self._test_transform_mappingInvariants(self.star1, expect) 

328 

329 expect = self.value + self.chipScale + self._evaluate_chebyshev(self.star0.getXFocal(), 

330 self.star0.getYFocal()) 

331 self._test_transform_mappingCheby(self.star0, expect) 

332 

333 expect = self.value + self.chipScale + self._evaluate_chebyshev(self.star1.getXFocal(), 

334 self.star1.getYFocal()) 

335 self._test_transform_mappingCheby(self.star1, expect) 

336 

337 def test_computeParameterDerivatives(self): 

338 # the parameter derivative of a spatially invariant magnitude transform is always 1. 

339 expectInvariant = np.array([1.0, 1.0]) 

340 self._test_computeParameterDerivatives(self.star1, expectInvariant) 

341 

342 

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

344 pass 

345 

346 

347def setup_module(module): 

348 lsst.utils.tests.init() 

349 

350 

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

352 lsst.utils.tests.init() 

353 unittest.main()