Coverage for python / lsst / cbp / coordinateConverter.py: 22%

242 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 08:49 +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"""CoordinateConverter class: coordinate conversions for the CBP.""" 

22 

23__all__ = ["CoordinateConverter"] 

24 

25import math 

26 

27import numpy as np 

28import scipy.optimize 

29 

30from . import coordUtils 

31from lsst.geom import Extent2D, Point2D, SpherePoint, radians 

32from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE, FIELD_ANGLE 

33from .beamInfo import BeamInfo 

34 

35# Record errors from setFocalFieldAngle? 

36_RecordErrors = False 

37# List of errors from the root finder: 

38# error (abs value, radians), pupilPos, focalFieldAngle, beam. 

39_ErrorList = None 

40 

41 

42def startRecordingErrors(): 

43 """Start recording setFocalFieldAngle errors and reset accumulators.""" 

44 global _RecordErrors, _ErrorList 

45 _RecordErrors = True 

46 _ErrorList = [] 

47 

48 

49def stopRecordingErrors(): 

50 """Stop recording errors.""" 

51 global _RecordErrors 

52 _RecordErrors = False 

53 

54 

55def getRecordedErrors(): 

56 """Return setFocalFieldAngle errors. 

57 

58 Returns 

59 ------- 

60 rootFinderErrors : `list` 

61 Each element is a tuple of the following: 

62 - |error| in radians 

63 - pupilPos 

64 - focalFieldAngle 

65 - beam name 

66 """ 

67 return _ErrorList 

68 

69 

70class CoordinateConverter: 

71 """Coordinate conversions for the collimated beam projector (CBP). 

72 

73 This object supports the following tasks: 

74 

75 - Given a desired CBP "beam arrangement" (e.g. a particular beam 

76 should fall on a particular spot on the pupil 

77 and be focused to spot on a particular position of a detector), 

78 compute the necessary telescope and CBP pointing 

79 and the information about where each beam is directed. 

80 - Given the current telescope and CBP pointing and a desired 

81 offset to the resulting CBP beam arrangement, compute the new 

82 telescope and CBP pointing. 

83 

84 See :ref:`how to use this object 

85 <lsst.cbp.coordinateConverter.howToUse>` 

86 for a summary of how to use this object. 

87 

88 Parameters 

89 ---------- 

90 config : `lsst.cbp.CoordinateConverterConfig` 

91 Telescope and CBP :ref:`configuration <lsst.cbp.configuration>`. 

92 maskInfo : `lsst.cbp.MaskInfo` 

93 Information about the CBP mask. 

94 camera : `lsst.afw.cameraGeom.Camera` 

95 Camera geometry. 

96 

97 Notes 

98 ----- 

99 .. _lsst.cbp.coordinateConverter.howToUse: 

100 

101 **How to Use This Object** 

102 

103 Call a :ref:`set method <lsst.cbp.coordinateConverter.setMethods>`, 

104 such as `setDetectorPos` to specify a desired CBP beam arrangement, or 

105 an :ref:`offset method <lsst.cbp.coordinateConverter.offsetMethods>`, 

106 such as `offsetDetectorPos`, to offset the current beam arrangement. 

107 

108 This computes new values for telescope and CBP pointing, as attributes 

109 `telAzAltObserved`, `telRotObserved` and `cbpAzAltObserved`. 

110 Read these attributes and move the telescope and CBP accordingly. 

111 

112 Get information about the beams. There are two ways to do this: 

113 

114 - To get information for all beams, iterate on this object 

115 to get one `lsst.cbp.BeamInfo` per beam. Also the length 

116 of this object is the number of beams. 

117 - To get information for a specific beam, use ``[beamNameOrIndex]``; 

118 see ``__getitem__`` for details. 

119 Also attribute `beamNames` provides an iterable of beam names. 

120 

121 That is basically it. However, it may also help to know the following: 

122 

123 Whenever the telescope, camera rotator or CBP are moved, 

124 you must update the appropriate attribute(s) accordingly. 

125 Otherwise the beam information will be incorrect 

126 and offset commands will not work as expected. 

127 After each such update you can read the new beam information 

128 as usual. 

129 

130 You are free to update configuration information at any time, 

131 by setting the appropriate attribute(s). 

132 The two items you are most likely to update are: 

133 

134 - ``maskInfo``: set this when you change the mask 

135 - ``config.telRotOffset``: set this if you want to correct 

136 the orientation of the spot pattern on the focal plane. 

137 

138 After updating configuration information, read the new beam information 

139 (and possibly new telescope and/or CBP position information, 

140 though few configuration parameters directly affect those) 

141 to see the effect of the change. 

142 

143 .. _lsst.cbp.coordinateConverter.setMethods: 

144 

145 **Set Methods** 

146 

147 The methods `setFocalPlanePos`, `setDetectorPos` and 

148 `setFocalFieldAngle` all allow you to specify the desired 

149 arrangement for one beam: 

150 

151 - The position of the beam on the pupil. 

152 - The position of the spot on the focal plane, expressed in different 

153 ways depending on the method. In most cases you will probably 

154 want to specify the position of the spot in pixels on a sepecified 

155 detector, in which case call `setFocalPlanePos`. 

156 

157 These set methods simply update the pointing of the telescope and CBP 

158 (`telAzAltObserved`, `telRotObserved` and `cbpAzAltObserved`). 

159 If you then move the telescope and CBP as suggested, the beam 

160 should have the arrangement you specified, and the spot pattern of all 

161 the beams should be aligned with the detectors. 

162 

163 .. _lsst.cbp.coordinateConverter.offsetMethods: 

164 

165 **Offset Methods** 

166 

167 The methods `offsetFocalPlanePos`, `offsetDetectorPos` and 

168 `offsetFocalFieldAngle` all allow you to offset the arrangement 

169 for one beam: 

170 

171 - Offset the position of the beam on the pupil. 

172 - Offset the position of the spot on the focal plane, 

173 expressed in different ways depending on the method. 

174 In most cases you will probably want to specify 

175 the offset of the spot in pixels on a sepecified detector, 

176 in which case call `offsetFocalPlanePos`. 

177 

178 These offset methods simply update the pointing of the telescope and 

179 CBP (`telAzAltObserved`, `telRotObserved` and `cbpAzAltObserved`). 

180 If you then move the telescope and CBP as suggested, the beam 

181 should have the arrangement you specified, and the spot pattern of all 

182 the beams should be aligned with the detectors. 

183 

184 """ 

185 

186 def __init__(self, config, maskInfo, cameraGeom): 

187 self.config = config 

188 self.maskInfo = maskInfo 

189 self.cameraGeom = cameraGeom 

190 self._fieldAngleToFocalPlane = cameraGeom.getTransform(FIELD_ANGLE, FOCAL_PLANE) 

191 # Amount to add to default hole position to compute 

192 # telescope rotator angle (pixels); I found that a wide range 

193 # of values works, with (1, 0) comfortably in that range. 

194 self._holeDelta = Extent2D(1, 0) 

195 self._telAzAlt = SpherePoint(np.nan, np.nan, radians) 

196 self._telRot = np.nan*radians 

197 self._cbpAzAlt = SpherePoint(np.nan, np.nan, radians) 

198 

199 def setFocalFieldAngle(self, pupilPos, focalFieldAngle=None, beam=None): 

200 """Set the focal plane field angle of a beam. 

201 

202 Compute new telescope, camera rotator and CBP positions 

203 and thus update beam info. 

204 

205 Parameters 

206 ---------- 

207 pupilPos : pair of `float` 

208 Position of the specified beam on the 

209 :ref:`telescope pupil <lsst.cbp.pupil_position>` (x, y mm). 

210 focalFieldAngle : pair of `float` (optional) 

211 :ref:`Focal plane field angle <lsst.cbp.focal_plane_field_angle>` 

212 of the specified beam (x, y rad); defaults to (0, 0). 

213 beam : `int` or `str` (optional) 

214 Name or index of beam; defaults to 

215 ``self.maskInfo.defaultBeam``. 

216 """ 

217 beam = self.maskInfo.asHoleName(beam) 

218 if focalFieldAngle is None: 

219 focalFieldAngle = Point2D(0, 0) 

220 else: 

221 focalFieldAngle = Point2D(*focalFieldAngle) 

222 

223 # If the field angle is too small to matter and too small to determine 

224 # its orientation, treat it as 0,0 (no iteration required). 

225 if math.hypot(*focalFieldAngle) < 1e-10: 

226 self.setPupilFieldAngle(pupilPos, focalFieldAngle, beam) 

227 return 

228 

229 # Minimize the field angle error as a function of the orientation 

230 # of the field angle; start with rotation = 0 so pupil field angle 

231 # equals focal plane field angle. 

232 # Record initial conditions, in case the miminizer fails to converge. 

233 telAzAlt = self._telAzAlt 

234 telRot = self._telRot 

235 cbpAzAlt = self._cbpAzAlt 

236 try: 

237 class TrialFunctor: 

238 """Functor to compute error in focal plane orientation. 

239 

240 Parameters 

241 ---------- 

242 cco : `lsst.cbp.CoordinateConverter` 

243 A coordinate converter object. 

244 pupilPos : pair of `float` 

245 Position of the specified beam on the 

246 :ref:`telescope pupil <lsst.cbp.pupil_position>` 

247 (x, y mm). 

248 focalFieldAngle : pair of `float` (optional) 

249 :ref:`Focal plane field angle 

250 <lsst.cbp.focal_plane_field_angle>` 

251 of the specified beam (x, y rad); defaults to (0, 0). 

252 beam : `int` or `str` (optional) 

253 Name or index of beam; defaults to 

254 ``self.maskInfo.defaultBeam``. 

255 """ 

256 

257 def __init__(self, cco, pupilPos, focalFieldAngle, beam): 

258 self.cco = cco 

259 self.pupilPos = pupilPos 

260 # desired focal plane field angle 

261 self.focalFieldAngle = focalFieldAngle 

262 self.beam = beam 

263 self.focalFieldAngleOrientation = math.atan2(focalFieldAngle[1], 

264 focalFieldAngle[0])*radians 

265 

266 def __call__(self, rotAngleRadArr): 

267 """Compute the error in focal plane orientation 

268 (in radians) at the specified camera rotation angle. 

269 

270 Parameters 

271 ---------- 

272 rotAngleRadArr : sequence of one `float` 

273 The internal camera rotator angle, in radians. 

274 It is passed in as a sequence of one element, 

275 as required by scipy.optimize. 

276 """ 

277 rotAngleRad = rotAngleRadArr[0] 

278 pupilFieldAngle = coordUtils.rotate2d(pos=self.focalFieldAngle, angle=rotAngleRad*radians) 

279 self.cco.setPupilFieldAngle(pupilPos=self.pupilPos, 

280 pupilFieldAngle=pupilFieldAngle, 

281 beam=self.beam) 

282 measuredFocalFieldAngle = self.cco[beam].focalFieldAngle 

283 measuredFocalFieldAngleOrientation = math.atan2(measuredFocalFieldAngle[1], 

284 measuredFocalFieldAngle[0])*radians 

285 return self.focalFieldAngleOrientation.separation( 

286 measuredFocalFieldAngleOrientation).asRadians() 

287 

288 funcToFindRoot = TrialFunctor(cco=self, pupilPos=pupilPos, 

289 focalFieldAngle=focalFieldAngle, beam=beam) 

290 

291 # Fit the rotator angle using a root finder 

292 with np.errstate(divide="ignore", invalid="ignore"): 

293 iterResult = scipy.optimize.root(fun=funcToFindRoot, x0=np.array([0.0], dtype=float), 

294 options=dict(fatol=1e-11), method="broyden1") 

295 

296 if not iterResult.success: 

297 raise RuntimeError("Iteration failed to converge") 

298 # Call the function again to make sure the final value found 

299 # is the one that is used. 

300 err = funcToFindRoot(iterResult.x) 

301 

302 if _RecordErrors: 

303 _ErrorList.append((abs(err), pupilPos, focalFieldAngle, beam)) 

304 

305 except Exception: 

306 self._telAzAlt = telAzAlt 

307 self._telRot = telRot 

308 self._cbpAzAlt = cbpAzAlt 

309 raise 

310 

311 def setFocalPlanePos(self, pupilPos, focalPlanePos=None, beam=None): 

312 """Set the position of a spot on the focal plane. 

313 

314 Compute new telescope, camera rotator and CBP positions 

315 and thus update beam info. 

316 

317 Parameters 

318 ---------- 

319 pupilPos : pair of `float` 

320 Position of the specified beam on the :ref:`telescope pupil 

321 <lsst.cbp.pupil_position>` (x, y mm) 

322 focalPlanePos : pair of `float` (optional). 

323 :ref:`Focal plane position <lsst.cbp.focal_plane>` of the spot 

324 formed by the specified beam (x, y mm); defaults to (0, 0). 

325 beam : `int` or `str` (optional) 

326 Name or index of beam; defaults to self.maskInfo.defaultBeam. 

327 """ 

328 beam = self.maskInfo.asHoleName(beam) 

329 if focalPlanePos is None: 

330 focalPlanePos = Point2D(0, 0) 

331 else: 

332 focalPlanePos = Point2D(*focalPlanePos) 

333 focalFieldAngle = self._fieldAngleToFocalPlane.applyInverse(focalPlanePos) 

334 self.setFocalFieldAngle(pupilPos=pupilPos, focalFieldAngle=focalFieldAngle, beam=beam) 

335 

336 def setDetectorPos(self, pupilPos, detectorPos=None, detector=None, beam=None): 

337 """Set the position of a spot on a detector. 

338 

339 Compute new telescope, camera rotator and CBP positions 

340 and thus update beam info. 

341 

342 Parameters 

343 ---------- 

344 pupilPos : pair of `float` 

345 Position of the specified beam on the :ref:`telescope pupil 

346 <lsst.cbp.pupil_position>` (x, y mm). 

347 detectorPos : pair of `float` (optional) 

348 Position of the spot formed by the specified beam 

349 on the specified detector (x, y pixels); 

350 defaults to the center of the detector. 

351 detector : `str` (optional 

352 Name of detector; defaults to self.config.defaultDetector. 

353 beam : `int` or `str` (optional) 

354 Name or index of beam; defaults to self.maskInfo.defaultBeam. 

355 """ 

356 beam = self.maskInfo.asHoleName(beam) 

357 if detector is None: 

358 detector = self.config.defaultDetector 

359 detectorInfo = self.cameraGeom[detector] 

360 if detectorPos is None: 

361 detectorPos = detectorInfo.getCenter(PIXELS) 

362 else: 

363 detectorPos = Point2D(*detectorPos) 

364 pixelSys = detectorInfo.makeCameraSys(PIXELS) 

365 pixelsToFieldAngle = self.cameraGeom.getTransform(pixelSys, FIELD_ANGLE) 

366 focalFieldAngle = pixelsToFieldAngle.applyForward(detectorPos) 

367 self.setFocalFieldAngle(pupilPos=pupilPos, focalFieldAngle=focalFieldAngle, beam=beam) 

368 

369 def offsetDetectorPos(self, pupilOffset=None, 

370 detectorOffset=None, beam=None): 

371 """Offset the detector position and/or pupil position of a beam. 

372 

373 Compute new telescope, camera rotator and CBP positions 

374 and thus update beam info. 

375 

376 Parameters 

377 ---------- 

378 pupilOffset : pair of `float` (optional) 

379 Offset of the position of the specified beam on the 

380 :ref:`telescope pupil <lsst.cbp.pupil_position>` (x, y mm); 

381 defaults to (0, 0). 

382 detectorOffset : pair of `float` (optional) 

383 Offset of the position of the specified spot 

384 on the detector it is presently on (x, y pixels); 

385 defaults to (0, 0). 

386 beam : `int` or `str` (optional) 

387 Name or index of beam; defaults to self.maskInfo.defaultBeam. 

388 """ 

389 beamInfo = self[beam] 

390 if not beamInfo.isOnDetector: 

391 raise RuntimeError("This beam is not on a detector") 

392 if pupilOffset is None: 

393 pupilOffset = (0, 0) 

394 pupilOffset = Extent2D(*pupilOffset) 

395 if detectorOffset is None: 

396 detectorOffset = (0, 0) 

397 detectorOffset = Extent2D(*detectorOffset) 

398 newPupilPos = beamInfo.pupilPos + pupilOffset 

399 newDetectorPos = beamInfo.detectorPos + detectorOffset 

400 self.setDetectorPos(pupilPos=newPupilPos, 

401 detectorPos=newDetectorPos, 

402 detector=beamInfo.detectorName, 

403 beam=beam) 

404 

405 def offsetFocalPlanePos(self, pupilOffset=None, 

406 focalPlaneOffset=None, beam=None): 

407 """Offset the focal plane position and/or pupil position of a beam. 

408 

409 Compute new telescope, camera rotator and CBP positions 

410 and thus update beam info. 

411 

412 Parameters 

413 ---------- 

414 pupilOffset : pair of `float` (optional) 

415 Offset of the position of the specified beam on the 

416 :ref:`telescope pupil <lsst.cbp.pupil_position>` (x, y mm); 

417 defaults to (0, 0). 

418 focalPlaneOffset : pair of `float` (optional) 

419 Offset of the position of the specified spot 

420 on the :ref:`focal plane <lsst.cbp.focal_plane>` (x, y mm); 

421 defaults to (0, 0). 

422 beam : `int` or `str` (optional) 

423 Name or index of beam; defaults to self.maskInfo.defaultBeam. 

424 """ 

425 beamInfo = self[beam] 

426 if pupilOffset is None: 

427 pupilOffset = (0, 0) 

428 pupilOffset = Extent2D(*pupilOffset) 

429 if focalPlaneOffset is None: 

430 focalPlaneOffset = (0, 0) 

431 focalPlaneOffset = Extent2D(*focalPlaneOffset) 

432 newPupilPos = beamInfo.pupilPos + pupilOffset 

433 newFocalPlanePos = beamInfo.focalPlanePos + focalPlaneOffset 

434 self.setFocalPlanePos(pupilPos=newPupilPos, 

435 focalPlanePos=newFocalPlanePos, 

436 beam=beam) 

437 

438 def offsetFocalFieldAngle(self, pupilOffset=None, 

439 focalFieldAngleOffset=None, beam=None): 

440 """Offset the focal plane field angle and/or pupil position 

441 of a beam. 

442 

443 Compute new telescope, camera rotator and CBP positions 

444 and thus update beam info. 

445 

446 Parameters 

447 ---------- 

448 pupilOffset : pair of `float` (optional) 

449 Offset of the position of the specified beam on the 

450 :ref:`telescope pupil <lsst.cbp.pupil_position>` (x, y mm); 

451 defaults to (0, 0). 

452 focalFieldAngleOffset : pair of `float` (optional) 

453 Offset of the :ref:`focal plane field angle 

454 <lsst.cbp.focal_plane_field_angle>` of the specified beam 

455 (x, y mm); defaults to (0, 0). 

456 beam : `int` or `str` (optional) 

457 Name or index of beam; defaults to self.maskInfo.defaultBeam. 

458 """ 

459 beamInfo = self[beam] 

460 if pupilOffset is None: 

461 pupilOffset = (0, 0) 

462 pupilOffset = Extent2D(*pupilOffset) 

463 if focalFieldAngleOffset is None: 

464 focalFieldAngleOffset = (0, 0) 

465 focalFieldAngleOffset = Extent2D(*focalFieldAngleOffset) 

466 newPupilPos = beamInfo.pupilPos + pupilOffset 

467 newFocalFieldAngle = beamInfo.focalFieldAngle + focalFieldAngleOffset 

468 self.setFocalFieldAngle(pupilPos=newPupilPos, 

469 focalFieldAngle=newFocalFieldAngle, 

470 beam=beam) 

471 

472 def __getitem__(self, beam): 

473 """Dict-like access to beam information. 

474 

475 Get a BeamInfo for a beam specified by integer index or name. 

476 """ 

477 beam = self.maskInfo.asHoleName(beam) 

478 return self.getBeamInfo(beam=beam) 

479 

480 def __iter__(self): 

481 """Iterator over beam information. 

482 

483 Do not modify this object during iteration, e.g. by modifying 

484 attributes or calling set or offset methods. 

485 """ 

486 for beam in self.beamNames: 

487 yield self[beam] 

488 

489 def __len__(self): 

490 """The number of beams.""" 

491 return self.maskInfo.numHoles 

492 

493 @property 

494 def cbpInBounds(self): 

495 """True if CBP observed altitude is in bounds (read only).""" 

496 alt = self.cbpAzAltObserved[1] 

497 return self.config.cbpAltitudeLimits[0] <= alt <= self.config.cbpAltitudeLimits[1] 

498 

499 @property 

500 def telInBounds(self): 

501 """True if telescope observed altitude is in bounds (read only).""" 

502 alt = self.telAzAltObserved[1] 

503 return self.config.telAltitudeLimits[0] <= alt <= self.config.telAltitudeLimits[1] 

504 

505 @property 

506 def beamNames(self): 

507 """Beam names, in index order (read only).""" 

508 return self.maskInfo.holeNames 

509 

510 @property 

511 def cbpAzAltObserved(self): 

512 """Observed az/alt of the CBP (read/write), 

513 as an lsst.geom.SpherePoint`. 

514 """ 

515 return SpherePoint(*[self._internalToObserved( 

516 internal=self._cbpAzAlt[i], 

517 offset=self.config.cbpAzAltOffset[i], 

518 scale=self.config.cbpAzAltScale[i]) for i in range(2)]) 

519 

520 @cbpAzAltObserved.setter 

521 def cbpAzAltObserved(self, cbpObs): 

522 """Set the observed az/alt of the CBP. 

523 

524 Parameters 

525 ---------- 

526 cbpObs : `lsst.geom.SpherePoint` 

527 Observed az/alt of the CBP. 

528 """ 

529 self._cbpAzAlt = SpherePoint(*[self._observedToInternal( 

530 observed=cbpObs[i], 

531 offset=self.config.cbpAzAltOffset[i], 

532 scale=self.config.cbpAzAltScale[i]) for i in range(2)]) 

533 

534 @property 

535 def telAzAltObserved(self): 

536 """Observed az/alt of the telescope (read/write), 

537 as an `lsst.geom.SpherePoint`. 

538 """ 

539 return SpherePoint(*[self._internalToObserved( 

540 internal=self._telAzAlt[i], 

541 offset=self.config.telAzAltOffset[i], 

542 scale=self.config.telAzAltScale[i]) for i in range(2)]) 

543 

544 @telAzAltObserved.setter 

545 def telAzAltObserved(self, telObs): 

546 """Set the observed az/alt of the telescope. 

547 

548 Parameters 

549 ---------- 

550 telObs : `lsst.geom.SpherePoint` 

551 Observed az/alt of the telescope. 

552 """ 

553 self._telAzAlt = SpherePoint(*[self._observedToInternal( 

554 observed=telObs[i], 

555 offset=self.config.telAzAltOffset[i], 

556 scale=self.config.telAzAltScale[i]) for i in range(2)]) 

557 

558 @property 

559 def telRotObserved(self): 

560 """Observed angle of the telescope camera rotator (read/write), 

561 as an `lsst.geom.Angle`. 

562 """ 

563 return self._internalToObserved( 

564 internal=self._telRot, 

565 offset=self.config.telRotOffset, 

566 scale=self.config.telRotScale) 

567 

568 @telRotObserved.setter 

569 def telRotObserved(self, telRotObserved): 

570 """Set the observed angle of the telescope camera rotator. 

571 

572 Parameters 

573 ---------- 

574 telRotObserved : `lsst.geom.Angle` 

575 The observed angle of the telescope camera rotator. 

576 """ 

577 self._telRot = self._observedToInternal( 

578 observed=telRotObserved, 

579 offset=self.config.telRotOffset, 

580 scale=self.config.telRotScale) 

581 

582 @property 

583 def cbpAzAltInternal(self): 

584 """Internal az/alt of the CBP (read only), 

585 as an `lsst.geom.SpherePoint`. 

586 

587 Primarily intended for testing. 

588 """ 

589 return self._cbpAzAlt 

590 

591 @property 

592 def telAzAltInternal(self): 

593 """Internal az/alt of the telescope (read only), 

594 as an `lsst.geom.SpherePoint`. 

595 

596 Primarily intended for testing. 

597 """ 

598 return self._telAzAlt 

599 

600 @property 

601 def telRotInternal(self): 

602 """Internal angle of the telescope camera rotator (read only), 

603 as an `lsst.geom.SpherePoint`. 

604 

605 Primarily intended for testing. 

606 """ 

607 return self._telRot 

608 

609 def setPupilFieldAngle(self, pupilPos, pupilFieldAngle=None, beam=None): 

610 """Set the pupil field angle and pupil position of a beam. 

611 

612 Compute new telescope, camera rotator and CBP positions 

613 and thus update beam info. 

614 

615 This method is primarily intended for internal use, 

616 to support the other set methods. It is public so it can be 

617 unit-tested. 

618 

619 Parameters 

620 ---------- 

621 pupilPos : pair of `float` 

622 Position of the specified beam on the :ref:`telescope pupil 

623 <lsst.cbp.pupil_position>` (x, y mm). 

624 pupilFieldAngle : pair of `float` (optional) 

625 Pupil field angle of specified beam (x, y rad); 

626 defaults to (0, 0). 

627 beam : `int` or `str` (optional) 

628 Name or index of beam; defaults to self.maskInfo.defaultBeam. 

629 """ 

630 beam = self.maskInfo.asHoleName(beam) 

631 if pupilFieldAngle is None: 

632 pupilFieldAngle = Point2D(0, 0) 

633 else: 

634 pupilFieldAngle = Point2D(*pupilFieldAngle) 

635 beamPosAtCtr = coordUtils.computeShiftedPlanePos(pupilPos, pupilFieldAngle, 

636 -self.config.telPupilOffset) 

637 beamVectorInCtrPupil = self._computeBeamVectorInCtrPupilFrame( 

638 beamPosAtCtr=beamPosAtCtr, pupilFieldAngle=pupilFieldAngle) 

639 cbpVectorInCtrPupil = self._computeCbpVectorInCtrPupilFrame( 

640 beamPosAtCtr=beamPosAtCtr, 

641 beamVectorInCtrPupil=beamVectorInCtrPupil) 

642 

643 telAzAlt = coordUtils.computeAzAltFromBasePupil( 

644 vectorBase=self.config.cbpPosition, 

645 vectorPupil=cbpVectorInCtrPupil) 

646 

647 beamVectorBase = coordUtils.convertVectorFromPupilToBase( 

648 vectorPupil=beamVectorInCtrPupil, 

649 pupilAzAlt=telAzAlt, 

650 ) 

651 beamFieldAngleCbp = self._getBeamCbpFieldAngle(beam) 

652 beamUnitVectorCbpPupil = coordUtils.fieldAngleToVector(beamFieldAngleCbp, self.config.cbpFlipX) 

653 cbpAzAlt = coordUtils.computeAzAltFromBasePupil( 

654 vectorBase=-beamVectorBase, 

655 vectorPupil=beamUnitVectorCbpPupil, 

656 ) 

657 

658 self._telRot = self._computeCameraRotatorAngle(telAzAlt=telAzAlt, cbpAzAlt=cbpAzAlt) 

659 self._telAzAlt = telAzAlt 

660 self._cbpAzAlt = cbpAzAlt 

661 

662 def _computeCameraRotatorAngle(self, telAzAlt, cbpAzAlt): 

663 """Compute the internal camera rotator angle needed for a given 

664 telescope and CBP pointing. 

665 

666 Parameters 

667 ---------- 

668 telAzAlt : `lsst.geom.SpherePoint` 

669 Telescope internal azimuth and altitude. 

670 cbpAzAlt : `lsst.geom.SpherePoint` 

671 CBP internal azimuth and altitude. 

672 

673 Returns 

674 ------- 

675 rotatorAangle : `lsst.geom.Angle` 

676 Internal camera rotator angle. 

677 """ 

678 # Compute focal plane position, ignoring self._telRot, 

679 # for two holes separated by x in the CBP equidistant from the center. 

680 # Compute the angle that would make the spots line up 

681 # with the x axis in the focal plane. 

682 ctrHolePos = Point2D(0, 0) 

683 holeDelta = Extent2D(*coordUtils.getFlippedPos(self._holeDelta, flipX=self.config.cbpFlipX)) 

684 holePos1 = ctrHolePos - holeDelta 

685 holePos2 = ctrHolePos + holeDelta 

686 pupilUnitVector1 = self._computeTelPupilUnitVectorFromHolePos(holePos1, telAzAlt=telAzAlt, 

687 cbpAzAlt=cbpAzAlt) 

688 pupilUnitVector2 = self._computeTelPupilUnitVectorFromHolePos(holePos2, telAzAlt=telAzAlt, 

689 cbpAzAlt=cbpAzAlt) 

690 # Rotation is done in a right-handed system, regardless of telFlipX 

691 pupilFieldAngle1 = coordUtils.vectorToFieldAngle(pupilUnitVector1, flipX=False) 

692 pupilFieldAngle2 = coordUtils.vectorToFieldAngle(pupilUnitVector2, flipX=False) 

693 focalPlane1 = self._fieldAngleToFocalPlane.applyForward(Point2D(*pupilFieldAngle1)) 

694 focalPlane2 = self._fieldAngleToFocalPlane.applyForward(Point2D(*pupilFieldAngle2)) 

695 deltaFocalPlane = np.subtract(focalPlane2, focalPlane1) 

696 return -math.atan2(deltaFocalPlane[1], deltaFocalPlane[0])*radians 

697 

698 def _getBeamCbpFieldAngle(self, beam): 

699 """Return the field angle of the specified beam 

700 in the CBP pupil frame. 

701 

702 Parameters 

703 ---------- 

704 beam : `int`, `str` or None 

705 Name or index of beam; if None then 

706 ``self.maskInfo.defaultBeam``. 

707 

708 Returns 

709 ------- 

710 fieldAngle : a pair of floats, in radians 

711 Field angle of the specified beam in the CBP pupil frame 

712 (x, y radians). 

713 """ 

714 holePos = self.maskInfo.getHolePos(beam) 

715 return tuple(math.atan(pos / self.config.cbpFocalLength) for pos in holePos) 

716 

717 def _computeBeamVectorInCtrPupilFrame(self, beamPosAtCtr, pupilFieldAngle): 

718 """Compute the beam vector to the CBP in the centered pupil frame. 

719 

720 Parameters 

721 ---------- 

722 beamPosAtCtr : pair of `float` 

723 Position of beam on centered pupil (x, y mm). 

724 pupilFieldAngle : pair of `float` 

725 incident angle of beam on pupil (x, y rad). 

726 

727 Returns 

728 ------- 

729 beamVectorinCtrPupil : `numpy.array` of 3 `float` 

730 Beam vector in telescope centered pupil frame (mm). 

731 """ 

732 # beamPosVec is a vector in the telescope pupil frame 

733 # from the center of the centered pupil plane 

734 # to the point on that plane specified by beamPosAtCtr. 

735 # This vector lies in the centered pupil plane. 

736 beamPosVec = coordUtils.pupilPositionToVector(beamPosAtCtr, self.config.telFlipX) 

737 beamUnitVec = coordUtils.fieldAngleToVector(pupilFieldAngle, self.config.telFlipX) 

738 abyz = beamPosVec[1]*beamUnitVec[1] + beamPosVec[2]*beamUnitVec[2] 

739 beamPosMag = np.linalg.norm(beamPosAtCtr) 

740 cbpDistance = self.config.cbpDistance 

741 beamLength = -abyz + math.sqrt(math.fsum((cbpDistance**2, abyz**2, -beamPosMag**2))) 

742 return beamLength*np.array(beamUnitVec) 

743 

744 def _computeCbpVectorInCtrPupilFrame(self, beamPosAtCtr, beamVectorInCtrPupil): 

745 """Compute a vector from telescope to CBP in the telescope's 

746 centered pupil frame. 

747 

748 Parameters 

749 ---------- 

750 beamPosAtCtr : pair of `float` 

751 Position of beam on centered pupil (x, y mm). 

752 beamVectorInCtrPupil : `numpy.array` of 3 `float` 

753 Vector in the telescope pupil frame from ``beamPosAtCtr` 

754 to the center of the CBP (mm). 

755 

756 Returns 

757 ------- 

758 cbpVectorInCtrPupilFrame : `numpy.array` of 3 `float` 

759 Vector in the telescope pupil frame from the center 

760 of the pupil frame to the center of the CBP (mm). 

761 """ 

762 # beamPosVec is a vector in the telescope pupil frame 

763 # from the center of the centered pupil plane 

764 # to the point on that plane specified by beamPosAtCtr. 

765 # This vector lies in the centered pupil plane. 

766 beamPosVec = coordUtils.pupilPositionToVector(beamPosAtCtr, self.config.telFlipX) 

767 return beamVectorInCtrPupil + beamPosVec 

768 

769 def _rotateFocalPlaneToPupil(self, focalPlane): 

770 """Rotate a position or field angle in telescope focal plane 

771 orientation to telescope pupil orientation. 

772 

773 Parameters 

774 ---------- 

775 focalPlane : pair of `float` 

776 Focal plane position or field angle (x, y any units). 

777 

778 Returns 

779 ------- 

780 pupil : pair of `float` 

781 ``focalPlane`` rotated to the pupil plane (x, y, same units). 

782 """ 

783 return coordUtils.rotate2d(pos=focalPlane, angle=-self._telRot) 

784 

785 def _rotatePupilToFocalPlane(self, pupil): 

786 """Rotate a position or field angle in telescope pupil orientation 

787 to one in telescope focal plane orientation. 

788 

789 Parameters 

790 ---------- 

791 pupil : pair of `float` 

792 Pupil plane position or field angle (x, y any units) 

793 

794 Returns 

795 ------- 

796 focalPlane : pair of `float` 

797 ``pupil`` rotated to the focal plane (x, y same units) 

798 """ 

799 return coordUtils.rotate2d(pos=pupil, angle=self._telRot) 

800 

801 def getBeamInfo(self, beam, *, holePos=None): 

802 """Get beam information for a beam from a specified CBP 

803 beam or hole position. 

804 

805 You may specify a hole position. This can be useful for unit 

806 tests and "what if" scenarios. 

807 

808 Parameters 

809 ---------- 

810 beam : `str` 

811 Beam name; ignored if holePos specified. 

812 holePos : pair of `float` (optional) 

813 Hole position on CBP mask (x, y mm); 

814 defaults to the actual hole position of the named beam. 

815 

816 Returns 

817 ------- 

818 beamInfo : `lsst.cbp.BeamInfo` 

819 Information about the specified beam. 

820 """ 

821 if holePos is None: 

822 holePos = self.maskInfo.getHolePos(beam) 

823 

824 # Compute focal plane field angle of the beam. 

825 beamPupilUnitVector = self._computeTelPupilUnitVectorFromHolePos(holePos, telAzAlt=self._telAzAlt, 

826 cbpAzAlt=self._cbpAzAlt) 

827 pupilFieldAngle = coordUtils.vectorToFieldAngle(beamPupilUnitVector, self.config.telFlipX) 

828 focalFieldAngle = self._rotatePupilToFocalPlane(pupilFieldAngle) 

829 

830 # Compute focal plane position of the beam. 

831 focalPlanePos = self._fieldAngleToFocalPlane.applyForward(Point2D(*pupilFieldAngle)) 

832 isOnFocalPlane = math.hypot(*focalPlanePos) < self.config.telFocalPlaneDiameter 

833 

834 # Vector from telescope centered pupil to actual pupil. 

835 pupilOffset = np.array((self.config.telPupilOffset, 0, 0), dtype=float) 

836 

837 # Compute the pupil position of the beam on the actual telescope pupil 

838 # (first compute on the centered pupil, 

839 # then subtract the pupil offset). 

840 cbpPositionPupil = coordUtils.convertVectorFromBaseToPupil( 

841 vectorBase=self.config.cbpPosition, 

842 pupilAzAlt=self._telAzAlt, 

843 ) - pupilOffset 

844 pupilNormalVector = np.array((1, 0, 0), dtype=float) 

845 pupilDistance = np.dot(cbpPositionPupil, pupilNormalVector) / \ 

846 np.dot(beamPupilUnitVector, pupilNormalVector) 

847 pupilPosVector = cbpPositionPupil - beamPupilUnitVector*pupilDistance 

848 # the x component should be zero; y, z -> plane position ±x, y 

849 pupilPos = coordUtils.getFlippedPos((pupilPosVector[1], pupilPosVector[2]), 

850 flipX=self.config.telFlipX) 

851 pupilPosDiameter = math.hypot(*pupilPos) 

852 isOnPupil = self.config.telPupilObscurationDiameter <= pupilPosDiameter and \ 

853 pupilPosDiameter <= self.config.telPupilDiameter 

854 return BeamInfo( 

855 cameraGeom=self.cameraGeom, 

856 name=beam, 

857 holePos=holePos, 

858 isOnPupil=isOnPupil, 

859 isOnFocalPlane=isOnFocalPlane, 

860 focalPlanePos=focalPlanePos, 

861 pupilPos=pupilPos, 

862 focalFieldAngle=focalFieldAngle, 

863 pupilFieldAngle=pupilFieldAngle, 

864 ) 

865 

866 def _computeCbpPupilUnitVectorFromHolePos(self, holePos): 

867 """Compute the CBP pupil unit vector of a beam. 

868 

869 Parameters 

870 ---------- 

871 holePos : pair of `float` 

872 Hole position on CBP mask (x, y mm). 

873 

874 Returns 

875 ------- 

876 cbpPupilUnitVector : `numpy.array` of 3 `float` 

877 The direction of the beam emitted by the CBP 

878 as a unit vector in the CBP pupil frame. 

879 """ 

880 beamCbpFieldAngle = [math.atan(pos/self.config.cbpFocalLength) for pos in holePos] 

881 return coordUtils.fieldAngleToVector(beamCbpFieldAngle, self.config.cbpFlipX) 

882 

883 def _computeTelPupilUnitVectorFromHolePos(self, holePos, telAzAlt, cbpAzAlt): 

884 """Compute the telescope pupil unit vector of a beam 

885 from its CBP hole position. 

886 

887 Parameters 

888 ---------- 

889 holePos : pair of `float` 

890 Hole position on CBP mask (x, y mm). 

891 telAzAlt : `lsst.geom.SpherePoint` 

892 Telescope internal azimuth and altitude. 

893 cbpAzAlt : `lsst.geom.SpherePoint` 

894 CBP internal azimuth and altitude. 

895 

896 Returns 

897 ------- 

898 telPupilUnitVector : `numpy.array` of 3 `float` 

899 The direction of the beam received by the telescope 

900 as a unit vector in the telescope pupil frame. 

901 """ 

902 beamCbpPupilUnitVector = self._computeCbpPupilUnitVectorFromHolePos(holePos) 

903 beamCbpBaseUnitVector = coordUtils.convertVectorFromPupilToBase( 

904 vectorPupil=beamCbpPupilUnitVector, 

905 pupilAzAlt=cbpAzAlt, 

906 ) 

907 beamTelBaseUnitVector = -beamCbpBaseUnitVector 

908 return coordUtils.convertVectorFromBaseToPupil( 

909 vectorBase=beamTelBaseUnitVector, 

910 pupilAzAlt=telAzAlt, 

911 ) 

912 

913 def _observedToInternal(self, observed, offset, scale): 

914 """Convert an observed angle into an internal angle. 

915 

916 Computes ``internal angle = (observed angle / scale) - offset``. 

917 

918 Parameters 

919 ---------- 

920 observed : `lsst.geom.Angle` 

921 Observed angle. 

922 offset : `lsst.geom.Angle` 

923 Offset. 

924 scale : `float` 

925 Scale. 

926 

927 Returns 

928 ------- 

929 internal : `lsst.geom.Angle` 

930 Internal angle. 

931 """ 

932 return (observed - offset)/scale 

933 

934 def _internalToObserved(self, internal, offset, scale): 

935 """Convert an internal angle into an observed angle. 

936 

937 Computes ``observed angle = (internal angle + offset) * scale``. 

938 

939 Parameters 

940 ---------- 

941 internal : `lsst.geom.Angle` 

942 Internal angle 

943 offset : `lsst.geom.Angle` 

944 Offset 

945 scale : `float` 

946 Scale 

947 

948 Returns 

949 ------- 

950 observed : `lsst.geom.Angle` 

951 Observed angle. 

952 """ 

953 return internal*scale + offset