Coverage for tests/test_coordUtils.py: 13%

148 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-31 10:29 +0000

1# This file is part of cbp. 

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 itertools 

23import math 

24import unittest 

25 

26import numpy as np 

27 

28from lsst.sphgeom import Vector3d 

29from lsst.geom import SpherePoint, degrees, radians 

30from lsst.cbp import coordUtils 

31import lsst.utils.tests 

32 

33RAD_PER_DEG = math.pi / 180 

34 

35# set True to record and report numeric error in convertVectorFromPupilToBase 

36ReportRecordedErrors = True 

37 

38 

39class CoordUtilsTestCase(lsst.utils.tests.TestCase): 

40 

41 def setUp(self): 

42 if ReportRecordedErrors: 

43 coordUtils.startRecordingErrors() 

44 

45 def tearDown(self): 

46 if ReportRecordedErrors: 

47 errList = coordUtils.getRecordedErrors() 

48 coordUtils.stopRecordingErrors() 

49 if len(errList) > 0: 

50 print("\nWarning: recorded {} numerical errors in computeAzAltFromBasePupil;" 

51 " the worst 5 are:".format(len(errList))) 

52 for err, vectorBase, vectorPupil in errList[-5:]: 

53 print("error={:0.5f} arcsec, vectorBase={}, vectorPupil={}".format( 

54 err, vectorBase, vectorPupil)) 

55 

56 def testGetFlippedPos(self): 

57 floatList = (0, -5.1, 4.3) 

58 for x, y, flipX in itertools.product(floatList, floatList, (False, True)): 

59 with self.subTest(x=x, y=y, flipX=flipX): 

60 if flipX: 

61 desiredResult = (-x, y) 

62 else: 

63 desiredResult = (x, y) 

64 result = coordUtils.getFlippedPos(xyPos=(x, y), flipX=flipX) 

65 self.assertEqual(result, desiredResult) 

66 

67 def testFieldAngleToVector(self): 

68 sp00 = SpherePoint(0, 0, degrees) 

69 degList = (-90, -89.9, -20, 0, 10, 89.9, 90) 

70 for xdeg, ydeg, flipX in itertools.product(degList, degList, (False, True)): 

71 with self.subTest(xdeg=xdeg, ydeg=ydeg, flipX=flipX): 

72 xrad = xdeg * RAD_PER_DEG 

73 signx = -1 if flipX else 1 

74 testOrientation = xdeg != 0 or ydeg != 0 

75 yrad = ydeg * RAD_PER_DEG 

76 fieldAngle = (xrad, yrad) 

77 vector = coordUtils.fieldAngleToVector(fieldAngle, flipX) 

78 self.assertAlmostEqual(np.linalg.norm(vector), 1) 

79 if testOrientation: 

80 # Orientation should match. 

81 orientationFromFieldAngle = math.atan2(yrad, signx*xrad)*radians 

82 # Field angle x = vector y, field angle y = vector z. 

83 orientationFromVector = math.atan2(vector[2], vector[1])*radians 

84 self.assertAnglesAlmostEqual(orientationFromVector, orientationFromFieldAngle) 

85 

86 # Now test as spherical geometry. 

87 sp = SpherePoint(Vector3d(*vector)) 

88 separation = sp00.separation(sp) 

89 predictedSeparation = math.hypot(xrad, yrad)*radians 

90 self.assertAnglesAlmostEqual(predictedSeparation, separation) 

91 if testOrientation: 

92 bearing = sp00.bearingTo(sp) 

93 self.assertAnglesAlmostEqual(orientationFromFieldAngle, bearing) 

94 

95 # Test round trip through vectorToFieldAngle. 

96 fieldAngleFromVector = coordUtils.vectorToFieldAngle(vector, flipX) 

97 np.testing.assert_allclose(fieldAngleFromVector, fieldAngle, atol=1e-15) 

98 

99 def testVectorToFieldAngle(self): 

100 # Note: more sophisticated cases are tested by testFieldAngleToVector. 

101 for flipX in (False, True): 

102 signx = -1 if flipX else 1 

103 for magMultiplier in (0.001, 1, 1000): 

104 for vector, predictedFieldAngleDeg in ( 

105 ((1, 0, 0), (0, 0)), 

106 ((0, 1, 0), (signx*90, 0)), 

107 ((0, 0, 1), (0, 90)), 

108 ): 

109 predictedFieldAngle = [val*RAD_PER_DEG for val in predictedFieldAngleDeg] 

110 scaledVector = np.array(vector) * magMultiplier 

111 fieldAngle = coordUtils.vectorToFieldAngle(scaledVector, flipX) 

112 np.testing.assert_allclose(predictedFieldAngle, fieldAngle, atol=1e-15) 

113 

114 def testComputeShiftedPlanePosZeroFieldAngle(self): 

115 """Test computeShiftedPlanePos with zero field angle 

116 

117 This should result in no change 

118 """ 

119 zeroFieldAngle = (0, 0) 

120 for planePos in ( 

121 (-1000, -2000), 

122 (0, 0), 

123 (5000, 4000), 

124 ): 

125 for shift in (-500, 0, 500): 

126 shiftedPlanePos = coordUtils.computeShiftedPlanePos(planePos, zeroFieldAngle, shift) 

127 self.assertPairsAlmostEqual(planePos, shiftedPlanePos) 

128 

129 def testComputeShiftedPlanePosZeroShift(self): 

130 """Test computeShiftedPlanePos with zero shift 

131 

132 This should result in no change 

133 """ 

134 zeroShift = 0 

135 for planePos in ( 

136 (-1000, -2000), 

137 (0, 0), 

138 (5000, 4000), 

139 ): 

140 for fieldAngle in ( 

141 (0.5, 0.5), 

142 (0, 1), 

143 (-0.5, 0.3), 

144 ): 

145 shiftedPlanePos = coordUtils.computeShiftedPlanePos(planePos, fieldAngle, zeroShift) 

146 self.assertPairsAlmostEqual(planePos, shiftedPlanePos) 

147 

148 def testComputeShiftedPlanePos(self): 

149 """Test computeShiftedPlanePos for the general case 

150 """ 

151 # In the general case the increase in x and y equals the shift 

152 # times y/x, z/x of a vector equivalent to the field angle. 

153 for ratios in ( 

154 (0.0, 0.0), 

155 (0.5, 0.5), 

156 (-0.23, 0.75), 

157 (0.3, 0.1), 

158 ): 

159 vector = (1, ratios[0], ratios[1]) 

160 fieldAngle = coordUtils.vectorToFieldAngle(vector, False) 

161 for planePos in ( 

162 (-1000, -2000), 

163 (0, 0), 

164 (5000, 4000), 

165 ): 

166 for shift3 in (-550, 0, 375): 

167 predictedShiftedPlanePos = [planePos[i] + shift3*ratios[i] for i in range(2)] 

168 shiftedPlanePos = coordUtils.computeShiftedPlanePos(planePos, fieldAngle, shift3) 

169 self.assertPairsAlmostEqual(shiftedPlanePos, predictedShiftedPlanePos) 

170 

171 def testConvertVectorFromPupilToBase(self): 

172 """Test convertVectorFromPupilToBase and convertVectorFromBaseToPupil 

173 """ 

174 cos30 = math.cos(30 * math.pi / 180) 

175 sin30 = math.sin(30 * math.pi / 180) 

176 for magMultiplier in (0.001, 1, 1000): 

177 for azAltDeg, vectorPupil, predictedVectorBase in ( 

178 # At az=0, alt=0: base = pupil. 

179 ((0, 0), (1, 0, 0), (1, 0, 0)), 

180 ((0, 0), (0, 1, 0), (0, 1, 0)), 

181 ((0, 0), (0, 0, -1), (0, 0, -1)), 

182 ((0, 0), (1, -1, 1), (1, -1, 1)), 

183 # At az=90, alt=0: 

184 # base x = pupil -y, 

185 # base y = pupil x, 

186 # base z = pupil z. 

187 ((90, 0), (1, 0, 0), (0, 1, 0)), 

188 ((90, 0), (0, 1, 0), (-1, 0, 0)), 

189 ((90, 0), (0, 0, -1), (0, 0, -1)), 

190 ((90, 0), (1, -1, 1), (1, 1, 1)), 

191 # At az=0, alt=90: 

192 # base x = - pupil z, 

193 # base y = pupil y, 

194 # base z = pupil x. 

195 ((0, 90), (1, 0, 0), (0, 0, 1)), 

196 ((0, 90), (0, 1, 0), (0, 1, 0)), 

197 ((0, 90), (0, 0, -1), (1, 0, 0)), 

198 ((0, 90), (1, -1, 1), (-1, -1, 1)), 

199 # At az=90, alt=90: base x = -pupil y, base y = - pupil z, 

200 # base z = pupil x. 

201 ((90, 90), (1, 0, 0), (0, 0, 1)), 

202 ((90, 90), (0, 1, 0), (-1, 0, 0)), 

203 ((90, 90), (0, 0, -1), (0, 1, 0)), 

204 ((90, 90), (1, -1, 1), (1, -1, 1)), 

205 # At az=0, alt=45: 

206 # base x = cos(30) * pupil x - sin(30) * pupil z 

207 # base y = pupil y 

208 # base z = sin(30) * pupil x + cos(30) * pupil z. 

209 ((0, 30), (1, 0, 0), (cos30, 0, sin30)), 

210 ((0, 30), (0, 1, 0), (0, 1, 0)), 

211 ((0, 30), (0, 0, -1), (sin30, 0, -cos30)), 

212 ((0, 30), (1, -1, 1), (cos30 - sin30, -1, sin30 + cos30)), 

213 # At az=30, alt=0: 

214 # base x = cos(30) * pupil x - sin(30) * pupil y 

215 # base y = sin(30) * pupil x + cos(30) * pupil y 

216 # base z = pupil z 

217 ((30, 0), (1, 0, 0), (cos30, sin30, 0)), 

218 ((30, 0), (0, 1, 0), (-sin30, cos30, 0)), 

219 ((30, 0), (0, 0, -1), (0, 0, -1)), 

220 ((30, 0), (1, -1, 1), (cos30 + sin30, sin30 - cos30, 1)), 

221 ): 

222 vectorPupil = np.array(vectorPupil) * magMultiplier 

223 predictedVectorBase = np.array(predictedVectorBase) * magMultiplier 

224 pupilAzAlt = SpherePoint(*azAltDeg, degrees) 

225 vectorBase = coordUtils.convertVectorFromPupilToBase(vectorPupil=vectorPupil, 

226 pupilAzAlt=pupilAzAlt) 

227 atol = max(magMultiplier, 1) * 1e-15 

228 msg = "azAltDeg={}, vectorPupil={}".format(azAltDeg, vectorPupil) 

229 np.testing.assert_allclose(vectorBase, predictedVectorBase, atol=atol, 

230 err_msg=msg, verbose=True) 

231 

232 vectorPupilRoundTrip = coordUtils.convertVectorFromBaseToPupil(vectorBase=vectorBase, 

233 pupilAzAlt=pupilAzAlt) 

234 np.testing.assert_allclose(vectorPupil, vectorPupilRoundTrip, atol=atol, 

235 err_msg=msg, verbose=True) 

236 

237 def testComputeAzAltFromPupilBaseWithBaseEqualsPupil(self): 

238 """Test computeAzAltFromBasePupil with baseVector=pupilVector, 

239 so the telescope will to internal az, alt=0 

240 """ 

241 zeroSp = SpherePoint(0, 0, radians) 

242 for vector, pupilMagFactor, baseMagFactor in itertools.product( 

243 ((1, 0, 0), (0.1, -1, 0), (0.1, -0.5, 0.5), (0.5, 0, 0.5), (1, 0.7, -0.8)), 

244 (1, 1000), 

245 (1, 1000), 

246 ): 

247 with self.subTest(vector=vector, pupilMagFactor=pupilMagFactor, baseMagFactor=baseMagFactor): 

248 vectorPupil = np.array(vector, dtype=float) * pupilMagFactor 

249 vectorBase = np.array(vector, dtype=float) * baseMagFactor 

250 obs = coordUtils.computeAzAltFromBasePupil(vectorPupil=vectorPupil, 

251 vectorBase=vectorBase) 

252 sep = zeroSp.separation(obs).asRadians() 

253 self.assertLess(sep, 1e-14) 

254 

255 def testComputeAzAltFromPupilBaseWithVectorPupil100(self): 

256 """Test computeAzAltFromBasePupil with vectorPupil = (1, 0, 0), 

257 so internal az/alt points along vectorBase 

258 """ 

259 vectorPupil = (1, 0, 0) 

260 for vectorBase, pupilMagFactor, baseMagFactor in itertools.product( 

261 ((1, 0, 0), (0, -1, 0), (0, -0.5, 0.5), (0.5, 0, 0.5), (1, 0.7, -0.8)), 

262 (1, 1000), 

263 (1, 1000), 

264 ): 

265 with self.subTest(vectorBase=vectorBase, pupilMagFactor=pupilMagFactor, 

266 baseMagFactor=baseMagFactor): 

267 predictedPupilAzalt = SpherePoint(Vector3d(*vectorBase)) 

268 vectorPupilScaled = np.array(vectorPupil, dtype=float) * pupilMagFactor 

269 vectorBaseScaled = np.array(vectorBase, dtype=float) * baseMagFactor 

270 pupilAzAlt = coordUtils.computeAzAltFromBasePupil(vectorPupil=vectorPupilScaled, 

271 vectorBase=vectorBaseScaled) 

272 sep = pupilAzAlt.separation(predictedPupilAzalt) 

273 if sep.asRadians() > 1e-14: 

274 print("Warning: sep={:0.5f} asec for vectorPupilScaled={}, vectorBaseScaled={}".format( 

275 sep.asArcseconds(), vectorPupilScaled, vectorBaseScaled)) 

276 # The worst error I see is 0.0026" 

277 # for vectorBase=(1, 0.7, -0.8). 

278 # That is worrisome, but acceptable. 

279 self.assertLess(sep.asArcseconds(), 0.01) 

280 

281 def testComputeAzAltFromPupilBase(self): 

282 """Test computeAzAltFromBasePupil with general values 

283 """ 

284 # transform the pupil vector back to the base vector 

285 # using the computed internal az/alt position 

286 for vectorPupil, vectorBase, pupilMagFactor, baseMagFactor in itertools.product( 

287 ((1, 0, 0), (2, 1, 0), (2, 0, 1), (2, 0.7, -0.8)), 

288 ((1, 0, 0), (0, 1, 0), (1, -0.7, 0.8)), 

289 (1, 1000), 

290 (1, 1000), 

291 ): 

292 with self.subTest(vectorPupil=vectorPupil, vectorBase=vectorBase, pupilMagFactor=pupilMagFactor, 

293 baseMagFactor=baseMagFactor): 

294 vectorPupilScaled = np.array(vectorPupil, dtype=float) * pupilMagFactor 

295 pupilMag = np.linalg.norm(vectorPupilScaled) 

296 vectorBaseScaled = np.array(vectorBase, dtype=float) * baseMagFactor 

297 pupilAzAlt = coordUtils.computeAzAltFromBasePupil(vectorPupil=vectorPupilScaled, 

298 vectorBase=vectorBaseScaled) 

299 # Check the round trip; note that the magnitude 

300 # of the returned vector will equal 

301 # the magnitude of the input vector. 

302 vectorBaseRoundTrip = coordUtils.convertVectorFromPupilToBase( 

303 vectorPupil=vectorPupilScaled, 

304 pupilAzAlt=pupilAzAlt) 

305 vectorBaseRoundTripMag = np.linalg.norm(vectorBaseRoundTrip) 

306 self.assertAlmostEqual(vectorBaseRoundTripMag, pupilMag, delta=1e-15*pupilMag) 

307 spBase = SpherePoint(Vector3d(*vectorBase)) 

308 spBaseRoundTrip = SpherePoint(Vector3d(*vectorBaseRoundTrip)) 

309 sep = spBase.separation(spBaseRoundTrip) 

310 self.assertLess(sep.asRadians(), 2e-15) 

311 

312 def testRotate2d(self): 

313 for pos, angleDeg, expectedPos in ( 

314 ((1, 2), 0, (1, 2)), 

315 ((1, 0), -30, (math.cos(RAD_PER_DEG*30), -0.5)), 

316 ((0, 1), -30, (0.5, math.cos(RAD_PER_DEG*30))), 

317 ((1, 2), 90, (-2, 1)), 

318 ): 

319 with self.subTest(pos=pos, angleDeg=angleDeg, expectedPos=expectedPos): 

320 angle = angleDeg*degrees 

321 rotatedPos = coordUtils.rotate2d(pos, angle) 

322 self.assertPairsAlmostEqual(rotatedPos, expectedPos) 

323 

324 

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

326 pass 

327 

328 

329def setup_module(module): 

330 lsst.utils.tests.init() 

331 

332 

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

334 lsst.utils.tests.init() 

335 unittest.main()