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

243 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-15 17:50 -0800

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 global _RecordErrors, _ErrorList 

303 if _RecordErrors: 

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

305 

306 except Exception: 

307 self._telAzAlt = telAzAlt 

308 self._telRot = telRot 

309 self._cbpAzAlt = cbpAzAlt 

310 raise 

311 

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

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

314 

315 Compute new telescope, camera rotator and CBP positions 

316 and thus update beam info. 

317 

318 Parameters 

319 ---------- 

320 pupilPos : pair of `float` 

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

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

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

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

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

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

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

328 """ 

329 beam = self.maskInfo.asHoleName(beam) 

330 if focalPlanePos is None: 

331 focalPlanePos = Point2D(0, 0) 

332 else: 

333 focalPlanePos = Point2D(*focalPlanePos) 

334 focalFieldAngle = self._fieldAngleToFocalPlane.applyInverse(focalPlanePos) 

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

336 

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

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

339 

340 Compute new telescope, camera rotator and CBP positions 

341 and thus update beam info. 

342 

343 Parameters 

344 ---------- 

345 pupilPos : pair of `float` 

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

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

348 detectorPos : pair of `float` (optional) 

349 Position of the spot formed by the specified beam 

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

351 defaults to the center of the detector. 

352 detector : `str` (optional 

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

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

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

356 """ 

357 beam = self.maskInfo.asHoleName(beam) 

358 if detector is None: 

359 detector = self.config.defaultDetector 

360 detectorInfo = self.cameraGeom[detector] 

361 if detectorPos is None: 

362 detectorPos = detectorInfo.getCenter(PIXELS) 

363 else: 

364 detectorPos = Point2D(*detectorPos) 

365 pixelSys = detectorInfo.makeCameraSys(PIXELS) 

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

367 focalFieldAngle = pixelsToFieldAngle.applyForward(detectorPos) 

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

369 

370 def offsetDetectorPos(self, pupilOffset=None, 

371 detectorOffset=None, beam=None): 

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

373 

374 Compute new telescope, camera rotator and CBP positions 

375 and thus update beam info. 

376 

377 Parameters 

378 ---------- 

379 pupilOffset : pair of `float` (optional) 

380 Offset of the position of the specified beam on the 

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

382 defaults to (0, 0). 

383 detectorOffset : pair of `float` (optional) 

384 Offset of the position of the specified spot 

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

386 defaults to (0, 0). 

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

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

389 """ 

390 beamInfo = self[beam] 

391 if not beamInfo.isOnDetector: 

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

393 if pupilOffset is None: 

394 pupilOffset = (0, 0) 

395 pupilOffset = Extent2D(*pupilOffset) 

396 if detectorOffset is None: 

397 detectorOffset = (0, 0) 

398 detectorOffset = Extent2D(*detectorOffset) 

399 newPupilPos = beamInfo.pupilPos + pupilOffset 

400 newDetectorPos = beamInfo.detectorPos + detectorOffset 

401 self.setDetectorPos(pupilPos=newPupilPos, 

402 detectorPos=newDetectorPos, 

403 detector=beamInfo.detectorName, 

404 beam=beam) 

405 

406 def offsetFocalPlanePos(self, pupilOffset=None, 

407 focalPlaneOffset=None, beam=None): 

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

409 

410 Compute new telescope, camera rotator and CBP positions 

411 and thus update beam info. 

412 

413 Parameters 

414 ---------- 

415 pupilOffset : pair of `float` (optional) 

416 Offset of the position of the specified beam on the 

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

418 defaults to (0, 0). 

419 focalPlaneOffset : pair of `float` (optional) 

420 Offset of the position of the specified spot 

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

422 defaults to (0, 0). 

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

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

425 """ 

426 beamInfo = self[beam] 

427 if pupilOffset is None: 

428 pupilOffset = (0, 0) 

429 pupilOffset = Extent2D(*pupilOffset) 

430 if focalPlaneOffset is None: 

431 focalPlaneOffset = (0, 0) 

432 focalPlaneOffset = Extent2D(*focalPlaneOffset) 

433 newPupilPos = beamInfo.pupilPos + pupilOffset 

434 newFocalPlanePos = beamInfo.focalPlanePos + focalPlaneOffset 

435 self.setFocalPlanePos(pupilPos=newPupilPos, 

436 focalPlanePos=newFocalPlanePos, 

437 beam=beam) 

438 

439 def offsetFocalFieldAngle(self, pupilOffset=None, 

440 focalFieldAngleOffset=None, beam=None): 

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

442 of a beam. 

443 

444 Compute new telescope, camera rotator and CBP positions 

445 and thus update beam info. 

446 

447 Parameters 

448 ---------- 

449 pupilOffset : pair of `float` (optional) 

450 Offset of the position of the specified beam on the 

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

452 defaults to (0, 0). 

453 focalFieldAngleOffset : pair of `float` (optional) 

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

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

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

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

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

459 """ 

460 beamInfo = self[beam] 

461 if pupilOffset is None: 

462 pupilOffset = (0, 0) 

463 pupilOffset = Extent2D(*pupilOffset) 

464 if focalFieldAngleOffset is None: 

465 focalFieldAngleOffset = (0, 0) 

466 focalFieldAngleOffset = Extent2D(*focalFieldAngleOffset) 

467 newPupilPos = beamInfo.pupilPos + pupilOffset 

468 newFocalFieldAngle = beamInfo.focalFieldAngle + focalFieldAngleOffset 

469 self.setFocalFieldAngle(pupilPos=newPupilPos, 

470 focalFieldAngle=newFocalFieldAngle, 

471 beam=beam) 

472 

473 def __getitem__(self, beam): 

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

475 

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

477 """ 

478 beam = self.maskInfo.asHoleName(beam) 

479 return self.getBeamInfo(beam=beam) 

480 

481 def __iter__(self): 

482 """Iterator over beam information. 

483 

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

485 attributes or calling set or offset methods. 

486 """ 

487 for beam in self.beamNames: 

488 yield self[beam] 

489 

490 def __len__(self): 

491 """The number of beams.""" 

492 return self.maskInfo.numHoles 

493 

494 @property 

495 def cbpInBounds(self): 

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

497 alt = self.cbpAzAltObserved[1] 

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

499 

500 @property 

501 def telInBounds(self): 

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

503 alt = self.telAzAltObserved[1] 

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

505 

506 @property 

507 def beamNames(self): 

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

509 return self.maskInfo.holeNames 

510 

511 @property 

512 def cbpAzAltObserved(self): 

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

514 as an lsst.geom.SpherePoint`. 

515 """ 

516 return SpherePoint(*[self._internalToObserved( 

517 internal=self._cbpAzAlt[i], 

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

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

520 

521 @cbpAzAltObserved.setter 

522 def cbpAzAltObserved(self, cbpObs): 

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

524 

525 Parameters 

526 ---------- 

527 cbpObs : `lsst.geom.SpherePoint` 

528 Observed az/alt of the CBP. 

529 """ 

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

531 observed=cbpObs[i], 

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

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

534 

535 @property 

536 def telAzAltObserved(self): 

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

538 as an `lsst.geom.SpherePoint`. 

539 """ 

540 return SpherePoint(*[self._internalToObserved( 

541 internal=self._telAzAlt[i], 

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

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

544 

545 @telAzAltObserved.setter 

546 def telAzAltObserved(self, telObs): 

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

548 

549 Parameters 

550 ---------- 

551 telObs : `lsst.geom.SpherePoint` 

552 Observed az/alt of the telescope. 

553 """ 

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

555 observed=telObs[i], 

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

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

558 

559 @property 

560 def telRotObserved(self): 

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

562 as an `lsst.geom.Angle`. 

563 """ 

564 return self._internalToObserved( 

565 internal=self._telRot, 

566 offset=self.config.telRotOffset, 

567 scale=self.config.telRotScale) 

568 

569 @telRotObserved.setter 

570 def telRotObserved(self, telRotObserved): 

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

572 

573 Parameters 

574 ---------- 

575 telRotObserved : `lsst.geom.Angle` 

576 The observed angle of the telescope camera rotator. 

577 """ 

578 self._telRot = self._observedToInternal( 

579 observed=telRotObserved, 

580 offset=self.config.telRotOffset, 

581 scale=self.config.telRotScale) 

582 

583 @property 

584 def cbpAzAltInternal(self): 

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

586 as an `lsst.geom.SpherePoint`. 

587 

588 Primarily intended for testing. 

589 """ 

590 return self._cbpAzAlt 

591 

592 @property 

593 def telAzAltInternal(self): 

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

595 as an `lsst.geom.SpherePoint`. 

596 

597 Primarily intended for testing. 

598 """ 

599 return self._telAzAlt 

600 

601 @property 

602 def telRotInternal(self): 

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

604 as an `lsst.geom.SpherePoint`. 

605 

606 Primarily intended for testing. 

607 """ 

608 return self._telRot 

609 

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

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

612 

613 Compute new telescope, camera rotator and CBP positions 

614 and thus update beam info. 

615 

616 This method is primarily intended for internal use, 

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

618 unit-tested. 

619 

620 Parameters 

621 ---------- 

622 pupilPos : pair of `float` 

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

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

625 pupilFieldAngle : pair of `float` (optional) 

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

627 defaults to (0, 0). 

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

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

630 """ 

631 beam = self.maskInfo.asHoleName(beam) 

632 if pupilFieldAngle is None: 

633 pupilFieldAngle = Point2D(0, 0) 

634 else: 

635 pupilFieldAngle = Point2D(*pupilFieldAngle) 

636 beamPosAtCtr = coordUtils.computeShiftedPlanePos(pupilPos, pupilFieldAngle, 

637 -self.config.telPupilOffset) 

638 beamVectorInCtrPupil = self._computeBeamVectorInCtrPupilFrame( 

639 beamPosAtCtr=beamPosAtCtr, pupilFieldAngle=pupilFieldAngle) 

640 cbpVectorInCtrPupil = self._computeCbpVectorInCtrPupilFrame( 

641 beamPosAtCtr=beamPosAtCtr, 

642 beamVectorInCtrPupil=beamVectorInCtrPupil) 

643 

644 telAzAlt = coordUtils.computeAzAltFromBasePupil( 

645 vectorBase=self.config.cbpPosition, 

646 vectorPupil=cbpVectorInCtrPupil) 

647 

648 beamVectorBase = coordUtils.convertVectorFromPupilToBase( 

649 vectorPupil=beamVectorInCtrPupil, 

650 pupilAzAlt=telAzAlt, 

651 ) 

652 beamFieldAngleCbp = self._getBeamCbpFieldAngle(beam) 

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

654 cbpAzAlt = coordUtils.computeAzAltFromBasePupil( 

655 vectorBase=-beamVectorBase, 

656 vectorPupil=beamUnitVectorCbpPupil, 

657 ) 

658 

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

660 self._telAzAlt = telAzAlt 

661 self._cbpAzAlt = cbpAzAlt 

662 

663 def _computeCameraRotatorAngle(self, telAzAlt, cbpAzAlt): 

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

665 telescope and CBP pointing. 

666 

667 Parameters 

668 ---------- 

669 telAzAlt : `lsst.geom.SpherePoint` 

670 Telescope internal azimuth and altitude. 

671 cbpAzAlt : `lsst.geom.SpherePoint` 

672 CBP internal azimuth and altitude. 

673 

674 Returns 

675 ------- 

676 rotatorAangle : `lsst.geom.Angle` 

677 Internal camera rotator angle. 

678 """ 

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

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

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

682 # with the x axis in the focal plane. 

683 ctrHolePos = Point2D(0, 0) 

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

685 holePos1 = ctrHolePos - holeDelta 

686 holePos2 = ctrHolePos + holeDelta 

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

688 cbpAzAlt=cbpAzAlt) 

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

690 cbpAzAlt=cbpAzAlt) 

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

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

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

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

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

696 deltaFocalPlane = np.subtract(focalPlane2, focalPlane1) 

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

698 

699 def _getBeamCbpFieldAngle(self, beam): 

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

701 in the CBP pupil frame. 

702 

703 Parameters 

704 ---------- 

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

706 Name or index of beam; if None then 

707 ``self.maskInfo.defaultBeam``. 

708 

709 Returns 

710 ------- 

711 fieldAngle : a pair of floats, in radians 

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

713 (x, y radians). 

714 """ 

715 holePos = self.maskInfo.getHolePos(beam) 

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

717 

718 def _computeBeamVectorInCtrPupilFrame(self, beamPosAtCtr, pupilFieldAngle): 

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

720 

721 Parameters 

722 ---------- 

723 beamPosAtCtr : pair of `float` 

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

725 pupilFieldAngle : pair of `float` 

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

727 

728 Returns 

729 ------- 

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

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

732 """ 

733 # beamPosVec is a vector in the telescope pupil frame 

734 # from the center of the centered pupil plane 

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

736 # This vector lies in the centered pupil plane. 

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

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

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

740 beamPosMag = np.linalg.norm(beamPosAtCtr) 

741 cbpDistance = self.config.cbpDistance 

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

743 return beamLength*np.array(beamUnitVec) 

744 

745 def _computeCbpVectorInCtrPupilFrame(self, beamPosAtCtr, beamVectorInCtrPupil): 

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

747 centered pupil frame. 

748 

749 Parameters 

750 ---------- 

751 beamPosAtCtr : pair of `float` 

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

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

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

755 to the center of the CBP (mm). 

756 

757 Returns 

758 ------- 

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

760 Vector in the telescope pupil frame from the center 

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

762 """ 

763 # beamPosVec is a vector in the telescope pupil frame 

764 # from the center of the centered pupil plane 

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

766 # This vector lies in the centered pupil plane. 

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

768 return beamVectorInCtrPupil + beamPosVec 

769 

770 def _rotateFocalPlaneToPupil(self, focalPlane): 

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

772 orientation to telescope pupil orientation. 

773 

774 Parameters 

775 ---------- 

776 focalPlane : pair of `float` 

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

778 

779 Returns 

780 ------- 

781 pupil : pair of `float` 

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

783 """ 

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

785 

786 def _rotatePupilToFocalPlane(self, pupil): 

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

788 to one in telescope focal plane orientation. 

789 

790 Parameters 

791 ---------- 

792 pupil : pair of `float` 

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

794 

795 Returns 

796 ------- 

797 focalPlane : pair of `float` 

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

799 """ 

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

801 

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

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

804 beam or hole position. 

805 

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

807 tests and "what if" scenarios. 

808 

809 Parameters 

810 ---------- 

811 beam : `str` 

812 Beam name; ignored if holePos specified. 

813 holePos : pair of `float` (optional) 

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

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

816 

817 Returns 

818 ------- 

819 beamInfo : `lsst.cbp.BeamInfo` 

820 Information about the specified beam. 

821 """ 

822 if holePos is None: 

823 holePos = self.maskInfo.getHolePos(beam) 

824 

825 # Compute focal plane field angle of the beam. 

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

827 cbpAzAlt=self._cbpAzAlt) 

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

829 focalFieldAngle = self._rotatePupilToFocalPlane(pupilFieldAngle) 

830 

831 # Compute focal plane position of the beam. 

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

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

834 

835 # Vector from telescope centered pupil to actual pupil. 

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

837 

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

839 # (first compute on the centered pupil, 

840 # then subtract the pupil offset). 

841 cbpPositionPupil = coordUtils.convertVectorFromBaseToPupil( 

842 vectorBase=self.config.cbpPosition, 

843 pupilAzAlt=self._telAzAlt, 

844 ) - pupilOffset 

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

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

847 np.dot(beamPupilUnitVector, pupilNormalVector) 

848 pupilPosVector = cbpPositionPupil - beamPupilUnitVector*pupilDistance 

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

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

851 flipX=self.config.telFlipX) 

852 pupilPosDiameter = math.hypot(*pupilPos) 

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

854 pupilPosDiameter <= self.config.telPupilDiameter 

855 return BeamInfo( 

856 cameraGeom=self.cameraGeom, 

857 name=beam, 

858 holePos=holePos, 

859 isOnPupil=isOnPupil, 

860 isOnFocalPlane=isOnFocalPlane, 

861 focalPlanePos=focalPlanePos, 

862 pupilPos=pupilPos, 

863 focalFieldAngle=focalFieldAngle, 

864 pupilFieldAngle=pupilFieldAngle, 

865 ) 

866 

867 def _computeCbpPupilUnitVectorFromHolePos(self, holePos): 

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

869 

870 Parameters 

871 ---------- 

872 holePos : pair of `float` 

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

874 

875 Returns 

876 ------- 

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

878 The direction of the beam emitted by the CBP 

879 as a unit vector in the CBP pupil frame. 

880 """ 

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

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

883 

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

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

886 from its CBP hole position. 

887 

888 Parameters 

889 ---------- 

890 holePos : pair of `float` 

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

892 telAzAlt : `lsst.geom.SpherePoint` 

893 Telescope internal azimuth and altitude. 

894 cbpAzAlt : `lsst.geom.SpherePoint` 

895 CBP internal azimuth and altitude. 

896 

897 Returns 

898 ------- 

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

900 The direction of the beam received by the telescope 

901 as a unit vector in the telescope pupil frame. 

902 """ 

903 beamCbpPupilUnitVector = self._computeCbpPupilUnitVectorFromHolePos(holePos) 

904 beamCbpBaseUnitVector = coordUtils.convertVectorFromPupilToBase( 

905 vectorPupil=beamCbpPupilUnitVector, 

906 pupilAzAlt=cbpAzAlt, 

907 ) 

908 beamTelBaseUnitVector = -beamCbpBaseUnitVector 

909 return coordUtils.convertVectorFromBaseToPupil( 

910 vectorBase=beamTelBaseUnitVector, 

911 pupilAzAlt=telAzAlt, 

912 ) 

913 

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

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

916 

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

918 

919 Parameters 

920 ---------- 

921 observed : `lsst.geom.Angle` 

922 Observed angle. 

923 offset : `lsst.geom.Angle` 

924 Offset. 

925 scale : `float` 

926 Scale. 

927 

928 Returns 

929 ------- 

930 internal : `lsst.geom.Angle` 

931 Internal angle. 

932 """ 

933 return (observed - offset)/scale 

934 

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

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

937 

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

939 

940 Parameters 

941 ---------- 

942 internal : `lsst.geom.Angle` 

943 Internal angle 

944 offset : `lsst.geom.Angle` 

945 Offset 

946 scale : `float` 

947 Scale 

948 

949 Returns 

950 ------- 

951 observed : `lsst.geom.Angle` 

952 Observed angle. 

953 """ 

954 return internal*scale + offset