Coverage for python/lsst/cbp/coordUtils.py: 19%

98 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-23 03:28 -0700

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"""Coordinate conversion functions""" 

22 

23__all__ = ["fieldAngleToVector", "vectorToFieldAngle", "pupilPositionToVector", 

24 "computeShiftedPlanePos", "convertVectorFromBaseToPupil", "convertVectorFromPupilToBase", 

25 "computeAzAltFromBasePupil", "getFlippedPos", "rotate2d"] 

26 

27import math 

28 

29import numpy as np 

30 

31from lsst.sphgeom import Vector3d 

32from lsst.geom import SpherePoint, radians 

33 

34# Error data from computeAzAltFromBasePupil. 

35# Error is determined by calling convertVectorFromPupilToBase 

36# using the az/alt computed by computeAzAltFromBasePupil 

37# and measuring the separation between that base vector and the input. 

38_RecordError = False # record errors? 

39_ErrorLimitArcsec = None # Only record errors larger than this. 

40_ErrorList = None # A list of tuples: (errorArcsec, vectorBase, vectorPupil). 

41 

42ZeroSpherePoint = SpherePoint(0, 0, radians) 

43 

44 

45def startRecordingErrors(errorLimit=1e-11*radians): 

46 """Start recording numeric errors in computeAzAltFromBasePupil 

47 and reset the error list. 

48 

49 Parameters 

50 ---------- 

51 errorLimit : `lsst.geom.Angle` 

52 Only errors larger than this limit are recorded. 

53 """ 

54 global _RecordError, _ErrorLimitArcsec, _ErrorList 

55 _RecordError = True 

56 _ErrorLimitArcsec = errorLimit.asArcseconds() 

57 _ErrorList = [] 

58 

59 

60def stopRecordingErrors(): 

61 """Stop recording numeric errors in computeAzAltFromBasePupil. 

62 """ 

63 global _RecordError 

64 _RecordError = False 

65 

66 

67def getRecordedErrors(): 

68 """Get recorded numeric errors in computeAzAltFromBasePupil, 

69 sorted by increasing error. 

70 

71 Returns 

72 ------- 

73 errorList : `list` of tuples 

74 Recorded error as a list of tuples containing: 

75 - |error| in radians 

76 - vectorBase 

77 - vectorPupil 

78 """ 

79 global _ErrorList 

80 if _ErrorList is None: 

81 raise RuntimeError("Errors were never recorded") 

82 return sorted(_ErrorList, key=lambda elt: elt[0]) 

83 

84 

85def getFlippedPos(xyPos, flipX): 

86 """Get a 2-dimensional position with the x axis properly flipped. 

87 

88 Parameters 

89 ---------- 

90 xyPos : pair of `float` 

91 Position to rotate. 

92 flipX : `bool` 

93 True if the x axis should be flipped (negated). 

94 

95 Returns 

96 ------- 

97 xyResult : pair of `float` 

98 ``xyPos`` with the x axis flipped or not, according to ``flipX`` 

99 """ 

100 return (-xyPos[0], xyPos[1]) if flipX else xyPos 

101 

102 

103def fieldAngleToVector(xyrad, flipX): 

104 """Convert a pupil field angle to a pupil unit vector. 

105 

106 Parameters 

107 ---------- 

108 xyrad : `tuple` of 2 `float` 

109 x,y :ref:`pupil field angle <lsst.cbp.pupil_field_angle>` 

110 (radians). 

111 

112 Returns 

113 ------- 

114 vector : `numpy.array` of 3 `float` 

115 A unit vector in the :ref:`pupil frame <lsst.cbp.pupil_frame>`. 

116 """ 

117 assert len(xyrad) == 2 

118 xyradRightHanded = getFlippedPos(xyrad, flipX=flipX) 

119 amount = math.hypot(*xyradRightHanded)*radians 

120 bearing = math.atan2(xyradRightHanded[1], xyradRightHanded[0])*radians 

121 return np.array(ZeroSpherePoint.offset(bearing=bearing, amount=amount).getVector()) 

122 

123 

124def vectorToFieldAngle(vec, flipX): 

125 """Convert a vector to a pupil field angle. 

126 

127 Parameters 

128 ---------- 

129 vec : sequence of 3 `float` 

130 3-dimensional vector in the :ref:`pupil frame 

131 <lsst.cbp.pupil_frame>`; the magnitude is ignored, 

132 but must be large enough to compute an accurate unit vector. 

133 flipX : bool 

134 Set True if the x axis of the focal plane is flipped 

135 with respect to the pupil. 

136 

137 Returns 

138 ------- 

139 fieldAngle : `tuple` of 2 `float` 

140 x,y :ref:`pupil field angle <lsst.cbp.pupil_field_angle>` 

141 (radians). 

142 """ 

143 sp = SpherePoint(Vector3d(*vec)) 

144 amountRad = ZeroSpherePoint.separation(sp).asRadians() 

145 bearingRad = ZeroSpherePoint.bearingTo(sp).asRadians() 

146 xyrad = (amountRad*math.cos(bearingRad), 

147 amountRad*math.sin(bearingRad)) 

148 return getFlippedPos(xyrad, flipX=flipX) 

149 

150 

151def pupilPositionToVector(xyPos, flipX): 

152 """Convert a pupil plane position to a 3D vector. 

153 

154 Parameters 

155 ---------- 

156 xyPos : sequence of 2 `float` 

157 :ref:`pupil plane position <lsst.cbp.pupil_position>` (mm). 

158 flipX : `bool` 

159 True if the x axis of the position is flipped (negated). 

160 

161 Returns 

162 ------- 

163 vector : sequence of 3 `float` 

164 3-dimensional vector in the 

165 :ref:`pupil frame <lsst.cbp.pupil_frame>`: 

166 x = 0, y = plane position x, z = plane position y. 

167 """ 

168 xyPosRightHanded = getFlippedPos(xyPos, flipX=flipX) 

169 return np.array([0, xyPosRightHanded[0], xyPosRightHanded[1]]) 

170 

171 

172def computeShiftedPlanePos(planePos, fieldAngle, shift): 

173 """Compute the plane position of a vector on a plane 

174 shifted along the optical axis. 

175 

176 Parameters 

177 ---------- 

178 planePos : pair of `float` 

179 Plane position at which the vector intersects the plane (x, y). 

180 fieldAngle : pair of `float` 

181 Field angle of vector (x, y radians). 

182 shift : `float` 

183 Amount by which the new plane is shifted along the optical axis. 

184 If ``shift`` and both components of ``fieldAngle`` 

185 are positive then both axes of the shifted plane position 

186 will be larger (more positive) than ``planePos``. 

187 

188 Returns 

189 ------- 

190 shiftedPlanePos : tuple of `float` 

191 Plane position at which vector intersects the shifted plane (x, y). 

192 

193 Notes 

194 ----- 

195 `flipX` is not an input because for this computation it does not matter 

196 if the x axis is flipped: fieldAngle and planePos are either both 

197 flipped or not, and that cancels out the effect. 

198 """ 

199 # unit vector y = plane x, unit vector z = plane y 

200 # (ignoring flipped x axis, which cancels out) 

201 unitVector = fieldAngleToVector(fieldAngle, False) 

202 dxy = [shift*unitVector[i]/unitVector[0] for i in (1, 2)] 

203 return tuple(planePos[i] + dxy[i] for i in range(2)) 

204 

205 

206def convertVectorFromBaseToPupil(vectorBase, pupilAzAlt): 

207 """Given a vector in base coordinates and the pupil pointing, 

208 compute the vector in pupil coordinates. 

209 

210 Parameters 

211 ---------- 

212 vectorBase : sequence of 3 `float` 

213 3-dimensional vector in the :ref:`base frame 

214 <lsst.cbp.base_frame>`. 

215 pupilAzAlt : `lsst.geom.SpherePoint` 

216 Pointing of the pupil frame as :ref:`internal azimuth, altitude 

217 <lsst.cbp.internal_angles>`. 

218 

219 Returns 

220 ------- 

221 vectorPupil : `np.array` of 3 `float` 

222 3-dimensional vector in the :ref:`pupil frame 

223 <lsst.cbp.pupil_frame>`. 

224 

225 Notes 

226 ----- 

227 This could be implemented as the following Euler angle 

228 rotation matrix, which is: 

229 - first rotate about the z axis by azimuth 

230 - then rotate about the rotated -y axis by altitude 

231 - there is no third rotation 

232 

233 c1*c2 -s1 -c1*s2 

234 c2*s1 c1 s1s2 

235 s1 0 c2 

236 

237 where angle 1 = azimuth, angle 2 = altitude, 

238 sx = sine(angle x) and cx = cosine(angle x). 

239 

240 Knowing this matrix is helpful, e.g. for math inside 

241 computeAzAltFromBasePupil. 

242 """ 

243 vectorMag = np.linalg.norm(vectorBase) 

244 vectorSpBase = SpherePoint(Vector3d(*vectorBase)) 

245 

246 telBase = SpherePoint(0, 0, radians) 

247 

248 # rotate vector around base z axis by -daz 

249 daz = pupilAzAlt[0] - telBase[0] 

250 zaxis = SpherePoint(Vector3d(0, 0, 1)) 

251 vectorSpRot1 = vectorSpBase.rotated(axis=zaxis, amount=-daz) 

252 

253 # rotate vector around pupil -y axis by -dalt 

254 dalt = pupilAzAlt[1] - telBase[1] 

255 negYAxis = SpherePoint(Vector3d(0, -1, 0)) 

256 vectorSpPupil = vectorSpRot1.rotated(axis=negYAxis, amount=-dalt) 

257 return np.array(vectorSpPupil.getVector()) * vectorMag 

258 

259 

260def convertVectorFromPupilToBase(vectorPupil, pupilAzAlt): 

261 """Given a vector in pupil coordinates and the pupil pointing, 

262 compute the vector in base coords. 

263 

264 Parameters 

265 ---------- 

266 vectorPupil : sequence of 3 `float` 

267 3-dimesional vector in the :ref:`pupil frame 

268 <lsst.cbp.pupil_frame>`. 

269 pupilAzAlt : `lsst.geom.SpherePoint` 

270 Pointing of the pupil frame as :ref:`internal azimuth, altitude 

271 <lsst.cbp.internal_angles>`. 

272 

273 Returns 

274 ------- 

275 vectorBase : `np.array` of 3 `float` 

276 3-dimensional vector in the :ref:`base frame 

277 <lsst.cbp.base_frame>`. 

278 """ 

279 vectorMag = np.linalg.norm(vectorPupil) 

280 vectorSpPupil = SpherePoint(Vector3d(*vectorPupil)) 

281 

282 telBase = SpherePoint(0, 0, radians) 

283 

284 # rotate vector around pupil -y axis by dalt 

285 dalt = pupilAzAlt[1] - telBase[1] 

286 negYAxis = SpherePoint(Vector3d(0, -1, 0)) 

287 vectorSpRot1 = vectorSpPupil.rotated(axis=negYAxis, amount=dalt) 

288 

289 # rotate that around base z axis by daz 

290 daz = pupilAzAlt[0] - telBase[0] 

291 zaxis = SpherePoint(Vector3d(0, 0, 1)) 

292 vectorSpBase = vectorSpRot1.rotated(axis=zaxis, amount=daz) 

293 return np.array(vectorSpBase.getVector()) * vectorMag 

294 

295 

296def computeAzAltFromBasePupil(vectorBase, vectorPupil): 

297 """Compute az/alt from a vector in the base frame 

298 and the same vector in the pupil frame. 

299 

300 Parameters 

301 ---------- 

302 vectorBase : `iterable` of three `float` 

303 3-dimensional vector in the :ref:`base frame 

304 <lsst.cbp.base_frame>`. 

305 vectorPupil : `iterable` of `float` 

306 The same vector in the :ref:`pupil frame <lsst.cbp.pupil_frame>`. 

307 This vector should be within 45 degrees or so of the optical axis 

308 for accurate results. 

309 

310 Returns 

311 ------- 

312 pupilAzAlt : `lsst.geom.SpherePoint` 

313 Pointing of the pupil frame as :ref:`internal azimuth, altitude 

314 <lsst.cbp.internal_angles>`. 

315 

316 Raises 

317 ------ 

318 ValueError 

319 If vectorPupil x <= 0 

320 

321 Notes 

322 ----- 

323 The magnitude of each vector is ignored, except that a reasonable 

324 magnitude is required in order to compute an accurate unit vector. 

325 """ 

326 if vectorPupil[0] <= 0: 

327 raise ValueError("vectorPupil x must be > 0: {}".format(vectorPupil)) 

328 

329 # Compute telescope altitude using: 

330 # 

331 # base z = sin(alt) pupil x + cos(alt) pupil z 

332 # 

333 # One way to derive this is from the last row of an Euler rotation 

334 # matrix listed in the comments for convertVectorFromBaseToPupil. 

335 spBase = SpherePoint(Vector3d(*vectorBase)) 

336 spPupil = SpherePoint(Vector3d(*vectorPupil)) 

337 xb, yb, zb = spBase.getVector() 

338 xp, yp, zp = spPupil.getVector() 

339 factor = 1 / math.fsum((xp**2, zp**2)) 

340 addend1 = xp * zb 

341 addend2 = zp * math.sqrt(math.fsum((xp**2, zp**2, -zb**2))) 

342 if zp == 0: 

343 sinAlt = zb/xp 

344 else: 

345 sinAlt = factor*(addend1 - addend2) 

346 alt = math.asin(sinAlt)*radians 

347 

348 # Consider the spherical triangle connecting the telescope pointing 

349 # (pupil frame x axis), the vector, and zenith (the base frame z axis). 

350 # The length of all sides is known: 

351 # - sideA is the side connecting the vector to the pupil frame x axis 

352 # (since 0, 0 is a unit vector pointing along pupil frame x); 

353 # - sideB is the side connecting telescope pointing to the zenith 

354 # - sideC is the side connecting the vector to the zenith 

355 # 

356 # Solve for angleA, the angle between the sides at the zenith; 

357 # that angle is the difference in azimuth between the telescope pointing 

358 # and the azimuth of the base vector. 

359 sideA = SpherePoint(0, 0, radians).separation(spPupil).asRadians() 

360 sideB = math.pi/2 - alt.asRadians() 

361 sideC = math.pi/2 - spBase[1].asRadians() 

362 

363 # sideA can be small or zero so use a half angle formula 

364 # sides B and C will always be well away from 0 and 180 degrees 

365 semiPerimeter = 0.5*math.fsum((sideA, sideB, sideC)) 

366 sinHalfAngleA = math.sqrt(math.sin(semiPerimeter - sideB) * math.sin(semiPerimeter - sideC) 

367 / (math.sin(sideB) * math.sin(sideC))) 

368 daz = 2*math.asin(sinHalfAngleA)*radians 

369 if spPupil[0].wrapCtr() > 0: 

370 daz = -daz 

371 az = spBase[0] + daz 

372 global _RecordError 

373 if _RecordError: # to study sources of numerical imprecision 

374 global _ErrorLimitArcsec, _ErrorList 

375 sp = SpherePoint(az, alt) 

376 vectorBaseRT = convertVectorFromPupilToBase(vectorPupil, sp) 

377 errorArcsec = SpherePoint(Vector3d(*vectorBaseRT)).separation( 

378 SpherePoint(Vector3d(*vectorBase))).asArcseconds() 

379 if errorArcsec > _ErrorLimitArcsec: 

380 _ErrorList.append((errorArcsec, vectorBase, vectorPupil)) 

381 return SpherePoint(az, alt) 

382 

383 

384def rotate2d(pos, angle): 

385 """Rotate a 2-dimensional position by a given angle. 

386 

387 Parameters 

388 ---------- 

389 pos : pair of `float` 

390 Position to rotate. 

391 angle : `lsst.geom.Angle` 

392 Amount of rotation. 

393 

394 Returns 

395 ------- 

396 rotPos : pair of `float` 

397 Rotated position. 

398 

399 Examples 

400 -------- 

401 ``rotate2d((1, 2), 90*lsst.geom.degrees, False)`` returns `(-2, 1)`. 

402 """ 

403 angRad = angle.asRadians() 

404 sinAng = math.sin(angRad) 

405 cosAng = math.cos(angRad) 

406 return ( 

407 cosAng*pos[0] - sinAng*pos[1], 

408 sinAng*pos[0] + cosAng*pos[1] 

409 )