Coverage for python / lsst / cbp / coordinateConverter.py: 22%
242 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 08:26 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 08:26 +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."""
23__all__ = ["CoordinateConverter"]
25import math
27import numpy as np
28import scipy.optimize
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
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
42def startRecordingErrors():
43 """Start recording setFocalFieldAngle errors and reset accumulators."""
44 global _RecordErrors, _ErrorList
45 _RecordErrors = True
46 _ErrorList = []
49def stopRecordingErrors():
50 """Stop recording errors."""
51 global _RecordErrors
52 _RecordErrors = False
55def getRecordedErrors():
56 """Return setFocalFieldAngle errors.
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
70class CoordinateConverter:
71 """Coordinate conversions for the collimated beam projector (CBP).
73 This object supports the following tasks:
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.
84 See :ref:`how to use this object
85 <lsst.cbp.coordinateConverter.howToUse>`
86 for a summary of how to use this object.
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.
97 Notes
98 -----
99 .. _lsst.cbp.coordinateConverter.howToUse:
101 **How to Use This Object**
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.
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.
112 Get information about the beams. There are two ways to do this:
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.
121 That is basically it. However, it may also help to know the following:
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.
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:
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.
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.
143 .. _lsst.cbp.coordinateConverter.setMethods:
145 **Set Methods**
147 The methods `setFocalPlanePos`, `setDetectorPos` and
148 `setFocalFieldAngle` all allow you to specify the desired
149 arrangement for one beam:
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`.
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.
163 .. _lsst.cbp.coordinateConverter.offsetMethods:
165 **Offset Methods**
167 The methods `offsetFocalPlanePos`, `offsetDetectorPos` and
168 `offsetFocalFieldAngle` all allow you to offset the arrangement
169 for one beam:
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`.
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.
184 """
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)
199 def setFocalFieldAngle(self, pupilPos, focalFieldAngle=None, beam=None):
200 """Set the focal plane field angle of a beam.
202 Compute new telescope, camera rotator and CBP positions
203 and thus update beam info.
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)
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
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.
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 """
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
266 def __call__(self, rotAngleRadArr):
267 """Compute the error in focal plane orientation
268 (in radians) at the specified camera rotation angle.
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()
288 funcToFindRoot = TrialFunctor(cco=self, pupilPos=pupilPos,
289 focalFieldAngle=focalFieldAngle, beam=beam)
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")
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)
302 if _RecordErrors:
303 _ErrorList.append((abs(err), pupilPos, focalFieldAngle, beam))
305 except Exception:
306 self._telAzAlt = telAzAlt
307 self._telRot = telRot
308 self._cbpAzAlt = cbpAzAlt
309 raise
311 def setFocalPlanePos(self, pupilPos, focalPlanePos=None, beam=None):
312 """Set the position of a spot on the focal plane.
314 Compute new telescope, camera rotator and CBP positions
315 and thus update beam info.
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)
336 def setDetectorPos(self, pupilPos, detectorPos=None, detector=None, beam=None):
337 """Set the position of a spot on a detector.
339 Compute new telescope, camera rotator and CBP positions
340 and thus update beam info.
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)
369 def offsetDetectorPos(self, pupilOffset=None,
370 detectorOffset=None, beam=None):
371 """Offset the detector position and/or pupil position of a beam.
373 Compute new telescope, camera rotator and CBP positions
374 and thus update beam info.
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)
405 def offsetFocalPlanePos(self, pupilOffset=None,
406 focalPlaneOffset=None, beam=None):
407 """Offset the focal plane position and/or pupil position of a beam.
409 Compute new telescope, camera rotator and CBP positions
410 and thus update beam info.
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)
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.
443 Compute new telescope, camera rotator and CBP positions
444 and thus update beam info.
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)
472 def __getitem__(self, beam):
473 """Dict-like access to beam information.
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)
480 def __iter__(self):
481 """Iterator over beam information.
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]
489 def __len__(self):
490 """The number of beams."""
491 return self.maskInfo.numHoles
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]
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]
505 @property
506 def beamNames(self):
507 """Beam names, in index order (read only)."""
508 return self.maskInfo.holeNames
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)])
520 @cbpAzAltObserved.setter
521 def cbpAzAltObserved(self, cbpObs):
522 """Set the observed az/alt of the CBP.
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)])
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)])
544 @telAzAltObserved.setter
545 def telAzAltObserved(self, telObs):
546 """Set the observed az/alt of the telescope.
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)])
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)
568 @telRotObserved.setter
569 def telRotObserved(self, telRotObserved):
570 """Set the observed angle of the telescope camera rotator.
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)
582 @property
583 def cbpAzAltInternal(self):
584 """Internal az/alt of the CBP (read only),
585 as an `lsst.geom.SpherePoint`.
587 Primarily intended for testing.
588 """
589 return self._cbpAzAlt
591 @property
592 def telAzAltInternal(self):
593 """Internal az/alt of the telescope (read only),
594 as an `lsst.geom.SpherePoint`.
596 Primarily intended for testing.
597 """
598 return self._telAzAlt
600 @property
601 def telRotInternal(self):
602 """Internal angle of the telescope camera rotator (read only),
603 as an `lsst.geom.SpherePoint`.
605 Primarily intended for testing.
606 """
607 return self._telRot
609 def setPupilFieldAngle(self, pupilPos, pupilFieldAngle=None, beam=None):
610 """Set the pupil field angle and pupil position of a beam.
612 Compute new telescope, camera rotator and CBP positions
613 and thus update beam info.
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.
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)
643 telAzAlt = coordUtils.computeAzAltFromBasePupil(
644 vectorBase=self.config.cbpPosition,
645 vectorPupil=cbpVectorInCtrPupil)
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 )
658 self._telRot = self._computeCameraRotatorAngle(telAzAlt=telAzAlt, cbpAzAlt=cbpAzAlt)
659 self._telAzAlt = telAzAlt
660 self._cbpAzAlt = cbpAzAlt
662 def _computeCameraRotatorAngle(self, telAzAlt, cbpAzAlt):
663 """Compute the internal camera rotator angle needed for a given
664 telescope and CBP pointing.
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.
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
698 def _getBeamCbpFieldAngle(self, beam):
699 """Return the field angle of the specified beam
700 in the CBP pupil frame.
702 Parameters
703 ----------
704 beam : `int`, `str` or None
705 Name or index of beam; if None then
706 ``self.maskInfo.defaultBeam``.
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)
717 def _computeBeamVectorInCtrPupilFrame(self, beamPosAtCtr, pupilFieldAngle):
718 """Compute the beam vector to the CBP in the centered pupil frame.
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).
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)
744 def _computeCbpVectorInCtrPupilFrame(self, beamPosAtCtr, beamVectorInCtrPupil):
745 """Compute a vector from telescope to CBP in the telescope's
746 centered pupil frame.
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).
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
769 def _rotateFocalPlaneToPupil(self, focalPlane):
770 """Rotate a position or field angle in telescope focal plane
771 orientation to telescope pupil orientation.
773 Parameters
774 ----------
775 focalPlane : pair of `float`
776 Focal plane position or field angle (x, y any units).
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)
785 def _rotatePupilToFocalPlane(self, pupil):
786 """Rotate a position or field angle in telescope pupil orientation
787 to one in telescope focal plane orientation.
789 Parameters
790 ----------
791 pupil : pair of `float`
792 Pupil plane position or field angle (x, y any units)
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)
801 def getBeamInfo(self, beam, *, holePos=None):
802 """Get beam information for a beam from a specified CBP
803 beam or hole position.
805 You may specify a hole position. This can be useful for unit
806 tests and "what if" scenarios.
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.
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)
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)
830 # Compute focal plane position of the beam.
831 focalPlanePos = self._fieldAngleToFocalPlane.applyForward(Point2D(*pupilFieldAngle))
832 isOnFocalPlane = math.hypot(*focalPlanePos) < self.config.telFocalPlaneDiameter
834 # Vector from telescope centered pupil to actual pupil.
835 pupilOffset = np.array((self.config.telPupilOffset, 0, 0), dtype=float)
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 )
866 def _computeCbpPupilUnitVectorFromHolePos(self, holePos):
867 """Compute the CBP pupil unit vector of a beam.
869 Parameters
870 ----------
871 holePos : pair of `float`
872 Hole position on CBP mask (x, y mm).
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)
883 def _computeTelPupilUnitVectorFromHolePos(self, holePos, telAzAlt, cbpAzAlt):
884 """Compute the telescope pupil unit vector of a beam
885 from its CBP hole position.
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.
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 )
913 def _observedToInternal(self, observed, offset, scale):
914 """Convert an observed angle into an internal angle.
916 Computes ``internal angle = (observed angle / scale) - offset``.
918 Parameters
919 ----------
920 observed : `lsst.geom.Angle`
921 Observed angle.
922 offset : `lsst.geom.Angle`
923 Offset.
924 scale : `float`
925 Scale.
927 Returns
928 -------
929 internal : `lsst.geom.Angle`
930 Internal angle.
931 """
932 return (observed - offset)/scale
934 def _internalToObserved(self, internal, offset, scale):
935 """Convert an internal angle into an observed angle.
937 Computes ``observed angle = (internal angle + offset) * scale``.
939 Parameters
940 ----------
941 internal : `lsst.geom.Angle`
942 Internal angle
943 offset : `lsst.geom.Angle`
944 Offset
945 scale : `float`
946 Scale
948 Returns
949 -------
950 observed : `lsst.geom.Angle`
951 Observed angle.
952 """
953 return internal*scale + offset