Coverage for python/lsst/cbp/coordinateConverter.py: 21%
243 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-15 02:50 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-15 02:50 -0700
1# This file is part of cbp.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21"""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 global _RecordErrors, _ErrorList
303 if _RecordErrors:
304 _ErrorList.append((abs(err), pupilPos, focalFieldAngle, beam))
306 except Exception:
307 self._telAzAlt = telAzAlt
308 self._telRot = telRot
309 self._cbpAzAlt = cbpAzAlt
310 raise
312 def setFocalPlanePos(self, pupilPos, focalPlanePos=None, beam=None):
313 """Set the position of a spot on the focal plane.
315 Compute new telescope, camera rotator and CBP positions
316 and thus update beam info.
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)
337 def setDetectorPos(self, pupilPos, detectorPos=None, detector=None, beam=None):
338 """Set the position of a spot on a detector.
340 Compute new telescope, camera rotator and CBP positions
341 and thus update beam info.
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)
370 def offsetDetectorPos(self, pupilOffset=None,
371 detectorOffset=None, beam=None):
372 """Offset the detector position and/or pupil position of a beam.
374 Compute new telescope, camera rotator and CBP positions
375 and thus update beam info.
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)
406 def offsetFocalPlanePos(self, pupilOffset=None,
407 focalPlaneOffset=None, beam=None):
408 """Offset the focal plane position and/or pupil position of a beam.
410 Compute new telescope, camera rotator and CBP positions
411 and thus update beam info.
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)
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.
444 Compute new telescope, camera rotator and CBP positions
445 and thus update beam info.
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)
473 def __getitem__(self, beam):
474 """Dict-like access to beam information.
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)
481 def __iter__(self):
482 """Iterator over beam information.
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]
490 def __len__(self):
491 """The number of beams."""
492 return self.maskInfo.numHoles
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]
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]
506 @property
507 def beamNames(self):
508 """Beam names, in index order (read only)."""
509 return self.maskInfo.holeNames
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)])
521 @cbpAzAltObserved.setter
522 def cbpAzAltObserved(self, cbpObs):
523 """Set the observed az/alt of the CBP.
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)])
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)])
545 @telAzAltObserved.setter
546 def telAzAltObserved(self, telObs):
547 """Set the observed az/alt of the telescope.
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)])
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)
569 @telRotObserved.setter
570 def telRotObserved(self, telRotObserved):
571 """Set the observed angle of the telescope camera rotator.
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)
583 @property
584 def cbpAzAltInternal(self):
585 """Internal az/alt of the CBP (read only),
586 as an `lsst.geom.SpherePoint`.
588 Primarily intended for testing.
589 """
590 return self._cbpAzAlt
592 @property
593 def telAzAltInternal(self):
594 """Internal az/alt of the telescope (read only),
595 as an `lsst.geom.SpherePoint`.
597 Primarily intended for testing.
598 """
599 return self._telAzAlt
601 @property
602 def telRotInternal(self):
603 """Internal angle of the telescope camera rotator (read only),
604 as an `lsst.geom.SpherePoint`.
606 Primarily intended for testing.
607 """
608 return self._telRot
610 def setPupilFieldAngle(self, pupilPos, pupilFieldAngle=None, beam=None):
611 """Set the pupil field angle and pupil position of a beam.
613 Compute new telescope, camera rotator and CBP positions
614 and thus update beam info.
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.
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)
644 telAzAlt = coordUtils.computeAzAltFromBasePupil(
645 vectorBase=self.config.cbpPosition,
646 vectorPupil=cbpVectorInCtrPupil)
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 )
659 self._telRot = self._computeCameraRotatorAngle(telAzAlt=telAzAlt, cbpAzAlt=cbpAzAlt)
660 self._telAzAlt = telAzAlt
661 self._cbpAzAlt = cbpAzAlt
663 def _computeCameraRotatorAngle(self, telAzAlt, cbpAzAlt):
664 """Compute the internal camera rotator angle needed for a given
665 telescope and CBP pointing.
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.
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
699 def _getBeamCbpFieldAngle(self, beam):
700 """Return the field angle of the specified beam
701 in the CBP pupil frame.
703 Parameters
704 ----------
705 beam : `int`, `str` or None
706 Name or index of beam; if None then
707 ``self.maskInfo.defaultBeam``.
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)
718 def _computeBeamVectorInCtrPupilFrame(self, beamPosAtCtr, pupilFieldAngle):
719 """Compute the beam vector to the CBP in the centered pupil frame.
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).
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)
745 def _computeCbpVectorInCtrPupilFrame(self, beamPosAtCtr, beamVectorInCtrPupil):
746 """Compute a vector from telescope to CBP in the telescope's
747 centered pupil frame.
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).
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
770 def _rotateFocalPlaneToPupil(self, focalPlane):
771 """Rotate a position or field angle in telescope focal plane
772 orientation to telescope pupil orientation.
774 Parameters
775 ----------
776 focalPlane : pair of `float`
777 Focal plane position or field angle (x, y any units).
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)
786 def _rotatePupilToFocalPlane(self, pupil):
787 """Rotate a position or field angle in telescope pupil orientation
788 to one in telescope focal plane orientation.
790 Parameters
791 ----------
792 pupil : pair of `float`
793 Pupil plane position or field angle (x, y any units)
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)
802 def getBeamInfo(self, beam, *, holePos=None):
803 """Get beam information for a beam from a specified CBP
804 beam or hole position.
806 You may specify a hole position. This can be useful for unit
807 tests and "what if" scenarios.
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.
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)
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)
831 # Compute focal plane position of the beam.
832 focalPlanePos = self._fieldAngleToFocalPlane.applyForward(Point2D(*pupilFieldAngle))
833 isOnFocalPlane = math.hypot(*focalPlanePos) < self.config.telFocalPlaneDiameter
835 # Vector from telescope centered pupil to actual pupil.
836 pupilOffset = np.array((self.config.telPupilOffset, 0, 0), dtype=float)
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 )
867 def _computeCbpPupilUnitVectorFromHolePos(self, holePos):
868 """Compute the CBP pupil unit vector of a beam.
870 Parameters
871 ----------
872 holePos : pair of `float`
873 Hole position on CBP mask (x, y mm).
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)
884 def _computeTelPupilUnitVectorFromHolePos(self, holePos, telAzAlt, cbpAzAlt):
885 """Compute the telescope pupil unit vector of a beam
886 from its CBP hole position.
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.
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 )
914 def _observedToInternal(self, observed, offset, scale):
915 """Convert an observed angle into an internal angle.
917 Computes ``internal angle = (observed angle / scale) - offset``.
919 Parameters
920 ----------
921 observed : `lsst.geom.Angle`
922 Observed angle.
923 offset : `lsst.geom.Angle`
924 Offset.
925 scale : `float`
926 Scale.
928 Returns
929 -------
930 internal : `lsst.geom.Angle`
931 Internal angle.
932 """
933 return (observed - offset)/scale
935 def _internalToObserved(self, internal, offset, scale):
936 """Convert an internal angle into an observed angle.
938 Computes ``observed angle = (internal angle + offset) * scale``.
940 Parameters
941 ----------
942 internal : `lsst.geom.Angle`
943 Internal angle
944 offset : `lsst.geom.Angle`
945 Offset
946 scale : `float`
947 Scale
949 Returns
950 -------
951 observed : `lsst.geom.Angle`
952 Observed angle.
953 """
954 return internal*scale + offset