diff --git a/4.0.0/objects.inv b/4.0.0/objects.inv index db7475e..8468189 100644 Binary files a/4.0.0/objects.inv and b/4.0.0/objects.inv differ diff --git a/4.0.0/reference/adjustments/index.html b/4.0.0/reference/adjustments/index.html index 4af1f61..3114af0 100644 --- a/4.0.0/reference/adjustments/index.html +++ b/4.0.0/reference/adjustments/index.html @@ -605,62 +605,187 @@

-

Function to minimize when calculating adjustments

-

@param pars: The parameters to fit for: - dx: Translation in x - dy: Translation in y - dz: Translation in z - thetha_0: Angle to rotate about first adjustor axis - thetha_1: Angle to rotate about second adjustor axis - z_t: Additional translation to tension the center point -@param can_points: The cannonical positions of the points to align -@param points: The measured positions of the points to align -@param adjustors: The measured positions of the adjustors

-

@return norm: The norm of (cannonical positions - transformed positions)

+

Function to minimize when calculating adjustments.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ pars + + NDArray[float32] + +
+

The parameters to fit for:

+
    +
  • dx: Translation in x
  • +
  • dy: Translation in y
  • +
  • dz: Translation in z
  • +
  • thetha_0: Angle to rotate about first adjustor axis
  • +
  • thetha_1: Angle to rotate about second adjustor axis
  • +
  • z_t: Additional translation to tension the center point
  • +
+
+
+ required +
+ can_points + + NDArray[float32] + +
+

The cannonical positions of the points to align.

+
+
+ required +
+ points + + NDArray[float32] + +
+

The measured positions of the points to align.

+
+
+ required +
+ adjustors + + NDArray[float32] + +
+

The measured positions of the adjustors.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
norm + float32 + +
+

The norm of \(cannonical_positions - transformed_positions\).

+
+
Source code in lat_alignment/adjustments.py -
 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
-100
-101
-102
-103
-104
-105
-106
-107
-108
def adjustment_fit_func(
-    pars: ndarray, can_points: ndarray, points: ndarray, adjustors: ndarray
-) -> float64:
-    """
-    Function to minimize when calculating adjustments
-
-    @param pars: The parameters to fit for:
-                    dx: Translation in x
-                    dy: Translation in y
-                    dz: Translation in z
-                    thetha_0: Angle to rotate about first adjustor axis
-                    thetha_1: Angle to rotate about second adjustor axis
-                    z_t: Additional translation to tension the center point
-    @param can_points: The cannonical positions of the points to align
-    @param points: The measured positions of the points to align
-    @param adjustors: The measured positions of the adjustors
-
-    @return norm: The norm of (cannonical positions - transformed positions)
+              
126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
def adjustment_fit_func(
+    pars: NDArray[np.float32],
+    can_points: NDArray[np.float32],
+    points: NDArray[np.float32],
+    adjustors: NDArray[np.float32],
+) -> np.float32:
+    r"""
+    Function to minimize when calculating adjustments.
+
+    Parameters
+    ----------
+    pars : NDArray[np.float32]
+        The parameters to fit for:
+
+        * dx: Translation in x
+        * dy: Translation in y
+        * dz: Translation in z
+        * thetha_0: Angle to rotate about first adjustor axis
+        * thetha_1: Angle to rotate about second adjustor axis
+        * z_t: Additional translation to tension the center point
+    can_points : NDArray[np.float32]
+        The cannonical positions of the points to align.
+    points : NDArray[np.float32]
+        The measured positions of the points to align.
+    adjustors : NDArray[np.float32]
+        The measured positions of the adjustors.
+
+    Returns
+    -------
+    norm : np.float32
+        The norm of $cannonical_positions - transformed_positions$.
     """
     dx, dy, dz, thetha_0, thetha_1, z_t = pars
     points, adjustors = translate_panel(points, adjustors, dx, dy, dz)
@@ -684,75 +809,283 @@ 

-

Calculate adjustments needed to align panel

-

@param can_points: The cannonical position of the points to align -@param points: The measured positions of the points to align -@param adjustors: The measured positions of the adjustors -@param **kwargs: Arguments to be passed to scipy.optimize.minimize

-

@return dx: The required translation of panel in x -@return dy: The required translation of panel in y -@return d_adj: The amount to move each adjustor -@return dx_err: The error in the fit for dx -@return dy_err: The error in the fit for dy -@return d_adj_err: The error in the fit for d_adj

+

Calculate adjustments needed to align panel.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ can_points + + NDArray[float32] + +
+

The cannonical position of the points to align.

+
+
+ required +
+ points + + NDArray[float32] + +
+

The measured positions of the points to align.

+
+
+ required +
+ adjustors + + NDArray[float32] + +
+

The measured positions of the adjustors.

+
+
+ required +
+ **kwargs + + +
+

Arguments to be passed to scipy.optimize.minimize.

+
+
+ {} +
+ dx + + float32 + +
+

The required translation of panel in x.

+
+
+ required +
+ dy + + float32 + +
+

The required translation of panel in y.

+
+
+ required +
+ d_adj + + NDArray[float32] + +
+

The amount to move each adjustor.

+
+
+ required +
+ dx_err + + float32 + +
+

The error in the fit for dx.

+
+
+ required +
+ dy_err + + float32 + +
+

The error in the fit for dy.

+
+
+ required +
+ d_adj_err + + NDArray[float32] + +
+

The error in the fit for d_adj.

+
+
+ required +
Source code in lat_alignment/adjustments.py -
111
-112
-113
-114
-115
-116
-117
-118
-119
-120
-121
-122
-123
-124
-125
-126
-127
-128
-129
-130
-131
-132
-133
-134
-135
-136
-137
-138
-139
-140
-141
-142
-143
-144
-145
-146
-147
-148
-149
def calc_adjustments(
-    can_points: ndarray, points: ndarray, adjustors: ndarray, **kwargs
-) -> Tuple[float64, float64, ndarray, float64, float64, ndarray]:
+              
165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
def calc_adjustments(
+    can_points: NDArray[np.float32],
+    points: NDArray[np.float32],
+    adjustors: NDArray[np.float32],
+    **kwargs,
+) -> Tuple[
+    np.float32,
+    np.float32,
+    NDArray[np.float32],
+    np.float32,
+    np.float32,
+    NDArray[np.float32],
+]:
     """
-    Calculate adjustments needed to align panel
-
-    @param can_points: The cannonical position of the points to align
-    @param points: The measured positions of the points to align
-    @param adjustors: The measured positions of the adjustors
-    @param **kwargs: Arguments to be passed to scipy.optimize.minimize
-
-    @return dx: The required translation of panel in x
-    @return dy: The required translation of panel in y
-    @return d_adj: The amount to move each adjustor
-    @return dx_err: The error in the fit for dx
-    @return dy_err: The error in the fit for dy
-    @return d_adj_err: The error in the fit for d_adj
+    Calculate adjustments needed to align panel.
+
+    Parameters
+    ----------
+    can_points : NDArray[np.float32]
+        The cannonical position of the points to align.
+    points : NDArray[np.float32]
+        The measured positions of the points to align.
+    adjustors : NDArray[np.float32]
+        The measured positions of the adjustors.
+    **kwargs
+        Arguments to be passed to `scipy.optimize.minimize`.
+
+    dx : np.float32
+        The required translation of panel in x.
+    dy : np.float32
+        The required translation of panel in y.
+    d_adj : NDArray[np.float32]
+        The amount to move each adjustor.
+    dx_err : np.float32
+        The error in the fit for `dx`.
+    dy_err : np.float32
+        The error in the fit for `dy`.
+    d_adj_err : NDArray[np.float32]
+        The error in the fit for `d_adj`.
     """
     res = opt.minimize(
         adjustment_fit_func, np.zeros(6), (can_points, points, adjustors), **kwargs
@@ -785,7 +1118,7 @@ 

- rotate(point, end_point1, end_point2, thetha) + rotate(point, end_point1, end_point2, theta)

@@ -793,16 +1126,112 @@

Rotate a point about an axis

-

@param point: The point to rotate -@param end_point1: A point on the axis of rotation -@param end_point2: Another point on the axis of rotation -@param thetha: Angle in radians to rotate by

-

@return point: The rotated point

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ point + + NDArray[float32] + +
+

The point to rotate

+
+
+ required +
+ end_point1 + + NDArray[float32] + +
+

A point on the axis of rotation

+
+
+ required +
+ end_point2 + + NDArray[float32] + +
+

Another point on the axis of rotation

+
+
+ required +
+ theta + + float32 + +
+

Angle in radians to rotate by

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
point + NDArray[float32] + +
+

The rotated point

+
+
Source code in lat_alignment/adjustments.py -
14
-15
+              
15
 16
 17
 18
@@ -819,23 +1248,48 @@ 

29 30 31 -32

def rotate(
-    point: ndarray, end_point1: ndarray, end_point2: ndarray, thetha: float64
-) -> ndarray:
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
def rotate(
+    point: NDArray[np.float32],
+    end_point1: NDArray[np.float32],
+    end_point2: NDArray[np.float32],
+    theta: np.float32,
+) -> NDArray[np.float32]:
     """
     Rotate a point about an axis
 
-    @param point: The point to rotate
-    @param end_point1: A point on the axis of rotation
-    @param end_point2: Another point on the axis of rotation
-    @param thetha: Angle in radians to rotate by
-
-    @return point: The rotated point
+    Parameters
+    ----------
+    point : NDArray[np.float32]
+        The point to rotate
+    end_point1 : NDArray[np.float32]
+        A point on the axis of rotation
+    end_point2 : NDArray[np.float32]
+        Another point on the axis of rotation
+    theta: NDArray[np.float32]
+        Angle in radians to rotate by
+
+    Returns
+    -------
+    point : NDArray[np.float32]
+        The rotated point
     """
     origin = np.mean((end_point1, end_point2))
     point_0 = point - origin
     ax = end_point2 - end_point1
-    ax = rot.from_rotvec(thetha * ax / np.linalg.norm(ax))
+    ax = rot.from_rotvec(theta * ax / np.linalg.norm(ax))
     point_0 = ax.apply(point_0)
     return point_0 + origin
 
@@ -855,30 +1309,123 @@

-

Rotate panel about axes created by adjustors

-

@param points: Points on panel to rotate -@param adjustors: Adjustor positions -@param thetha_0: Angle to rotate about first adjustor axis -@param thetha_1: Angle to rotate about second adjustor axis

-

@return rot_points: The rotated points -@return rot_adjustors: The rotated adjustors

+

Rotate panel about axes created by adjustors.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ points + + NDArray[float32] + +
+

Points on panel to rotate.

+
+
+ required +
+ adjustors + + NDArray[float32] + +
+

Adjustor positions.

+
+
+ required +
+ thetha_0 + + float32 + +
+

Angle to rotate about first adjustor axis

+
+
+ required +
+ thetha_1 + + np.float32. + +
+

Angle to rotate about second adjustor axis

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
rot_points + NDArray[float32] + +
+

The rotated points.

+
+
rot_adjustors + NDArray[float32] + +
+

The rotated adjustors.

+
+
Source code in lat_alignment/adjustments.py -
35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-48
+              
48
 49
 50
 51
@@ -893,22 +1440,61 @@ 

60 61 62 -63

def rotate_panel(
-    points: ndarray, adjustors: ndarray, thetha_0: float64, thetha_1: float64
-) -> Tuple[ndarray, ndarray]:
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
def rotate_panel(
+    points: NDArray[np.float32],
+    adjustors: NDArray[np.float32],
+    thetha_0: np.float32,
+    thetha_1: np.float32,
+) -> Tuple[NDArray[np.float32], NDArray[np.float32]]:
     """
-    Rotate panel about axes created by adjustors
-
-    @param points: Points on panel to rotate
-    @param adjustors: Adjustor positions
-    @param thetha_0: Angle to rotate about first adjustor axis
-    @param thetha_1: Angle to rotate about second adjustor axis
-
-    @return rot_points: The rotated points
-    @return rot_adjustors: The rotated adjustors
+    Rotate panel about axes created by adjustors.
+
+    Parameters
+    ----------
+    points : NDArray[np.float32]
+        Points on panel to rotate.
+    adjustors : NDArray[np.float32]
+        Adjustor positions.
+    thetha_0 : np.float32
+        Angle to rotate about first adjustor axis
+    thetha_1 : np.float32.
+        Angle to rotate about second adjustor axis
+
+    Returns
+    -------
+    rot_points : NDArray[np.float32]
+        The rotated points.
+    rot_adjustors : NDArray[np.float32]
+        The rotated adjustors.
     """
-    rot_points = np.zeros(points.shape)
-    rot_adjustors = np.zeros(adjustors.shape)
+    rot_points = np.zeros(points.shape, np.float32)
+    rot_adjustors = np.zeros(adjustors.shape, np.float32)
 
     n_points = len(points)
     n_adjustors = len(adjustors)
@@ -939,47 +1525,198 @@ 

-

Translate panel

-

@param points: The points on panel to translate -@param adjustors: Adjustor positions -@param dx: Translation in x -@param dy: Translation in y -@param dz: Translation in z

-

@return points: The translated points -@return adjustors: The translated adjustors

+

Translate a panel.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ points + + NDArray[float32] + +
+

The points on panel to translate.

+
+
+ required +
+ adjustors + + NDArray[float32] + +
+

Adjustor positions.

+
+
+ required +
+ dx + + float32 + +
+

Translation in x.

+
+
+ required +
+ dy + + float32 + +
+

Translation in y.

+
+
+ required +
+ dz + + float32 + +
+

Translation in z.

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
points + NDArray[float32] + +
+

The translated points.

+
+
adjustors + NDArray[float32] + +
+

The translated adjustors.

+
+
Source code in lat_alignment/adjustments.py -
66
-67
-68
-69
-70
-71
-72
-73
-74
-75
-76
-77
-78
-79
-80
-81
-82
def translate_panel(
-    points: ndarray, adjustors: ndarray, dx: float64, dy: float64, dz: float64
-) -> Tuple[ndarray, ndarray]:
+              
 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
def translate_panel(
+    points: NDArray[np.float32],
+    adjustors: NDArray[np.float32],
+    dx: np.float32,
+    dy: np.float32,
+    dz: np.float32,
+) -> Tuple[NDArray[np.float32], NDArray[np.float32]]:
     """
-    Translate panel
-
-    @param points: The points on panel to translate
-    @param adjustors: Adjustor positions
-    @param dx: Translation in x
-    @param dy: Translation in y
-    @param dz: Translation in z
-
-    @return points: The translated points
-    @return adjustors: The translated adjustors
+    Translate a panel.
+
+    Parameters
+    ----------
+    points : NDArray[np.float32]
+        The points on panel to translate.
+    adjustors : NDArray[np.float32]
+        Adjustor positions.
+    dx : np.float32
+        Translation in x.
+    dy : np.float32
+        Translation in y.
+    dz : np.float32
+        Translation in z.
+
+    Returns
+    -------
+    points : NDArray[np.float32]
+        The translated points.
+    adjustors : NDArray[np.float32]
+        The translated adjustors.
     """
     translation = np.array((dx, dy, dz))
     return points + translation, adjustors + translation
diff --git a/4.0.0/reference/alignment/index.html b/4.0.0/reference/alignment/index.html
index 5d82f23..380b57e 100644
--- a/4.0.0/reference/alignment/index.html
+++ b/4.0.0/reference/alignment/index.html
@@ -325,6 +325,15 @@
     
   
   
+
+      
+        
  • + + + adjust_panel + + +
  • @@ -458,6 +467,15 @@ + + +
  • + + + adjust_panel + + +
  • @@ -484,6 +502,10 @@

    alignment

    +

    Main driver script for running the alignment. +You typically want to use the lat_alignment entrypoint rather than +calling this directly.

    + @@ -501,6 +523,189 @@

    alignment

    +
    + + +

    + adjust_panel(panel, mnum, cfg) + +

    + + +
    + +

    Helper function to get the adjustments for a single panel.

    + + +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + panel + + Panel + +
    +

    The mirror panel to adjust.

    +
    +
    + required +
    + mnum + + int + +
    +

    The mirror number. +1 for the primary and 2 for the secondary.

    +
    +
    + required +
    + cfg + + dict + +
    +

    The configuration dictionairy.

    +
    +
    + required +
    + + +

    Returns:

    + + + + + + + + + + + + + +
    Name TypeDescription
    adjustments + NDArray[float32] + +
    +

    The adjustments to make for the panel. +This is a 17 element array with the following structure: +[mnum, panel_row, panel_col, dx, dy, d_adj1, ..., d_adj5, dx_err, dy_err, d_adj1_err, ..., d_adj5_err].

    +
    +
    + +
    + Source code in lat_alignment/alignment.py +
    21
    +22
    +23
    +24
    +25
    +26
    +27
    +28
    +29
    +30
    +31
    +32
    +33
    +34
    +35
    +36
    +37
    +38
    +39
    +40
    +41
    +42
    +43
    +44
    +45
    +46
    +47
    +48
    +49
    +50
    +51
    +52
    +53
    +54
    +55
    +56
    +57
    def adjust_panel(panel: mir.Panel, mnum: int, cfg: dict) -> NDArray[np.float32]:
    +    """
    +    Helper function to get the adjustments for a single panel.
    +
    +    Parameters
    +    ----------
    +    panel : mir.Panel
    +        The mirror panel to adjust.
    +    mnum : int
    +        The mirror number.
    +        1 for the primary and 2 for the secondary.
    +    cfg : dict
    +        The configuration dictionairy.
    +
    +    Returns
    +    -------
    +    adjustments : NDArray[np.float32]
    +        The adjustments to make for the panel.
    +        This is a 17 element array with the following structure:
    +        `[mnum, panel_row, panel_col, dx, dy, d_adj1, ..., d_adj5, dx_err, dy_err, d_adj1_err, ..., d_adj5_err]`.
    +    """
    +    adjustments = np.zeros(17, np.float32)
    +    adjustments[0] = mnum
    +    adjustments[1] = panel.row
    +    adjustments[2] = panel.col
    +    meas_adj = panel.meas_adj.copy()
    +    meas_adj[:, 2] += panel.meas_adj_resid
    +    meas_surface = panel.meas_surface.copy()
    +    meas_surface[:, 2] += panel.meas_adj_resid
    +    dy, dy, d_adj, dx_err, dy_err, d_adj_err = adj.calc_adjustments(
    +        panel.can_surface, meas_surface, meas_adj, **cfg.get("adjust", {})
    +    )
    +    adjustments[3:] = np.array(
    +        [dy, dy] + list(d_adj) + [dx_err, dy_err] + list(d_adj_err)
    +    )
    +
    +    return adjustments
    +
    +
    +
    + +
    +
    diff --git a/4.0.0/reference/fitting/index.html b/4.0.0/reference/fitting/index.html index 8ad2ae8..668a82a 100644 --- a/4.0.0/reference/fitting/index.html +++ b/4.0.0/reference/fitting/index.html @@ -626,22 +626,102 @@

    Ideally the points should be in the mirror's local coordinate system.

    -
    - Paramaters -

    points : NDArray[np.floating] - Array of points to compare against the mirror. - Should have shape (npoint, 3). -a : NDArray[np.floating] - Coeffecients of the mirror function. - Use a_primary for the primary mirror and a_secondary for the secondary. -compensate : float, default: 0.0 - Amount to compensate the mirror surface by. - This is useful to model things like the surface traced out by an SMR. -to_points : bool, default: True - If True, the transform will be inverted to align the model to the points. -**kwargs - Additional arguments to pass on to scipy.optimize.minimize.

    -
    +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + points + + NDArray[floating] + +
    +

    Array of points to compare against the mirror. +Should have shape (npoint, 3).

    +
    +
    + required +
    + a + + NDArray[floating] + +
    +

    Coeffecients of the mirror function. +Use a_primary for the primary mirror and a_secondary for the secondary.

    +
    +
    + required +
    + compensate + + float + +
    +

    Amount to compensate the mirror surface by. +This is useful to model things like the surface traced out by an SMR.

    +
    +
    + 0.0 +
    + to_points + + bool + +
    +

    If True, the transform will be inverted to align the model to the points.

    +
    +
    + True +
    + **kwargs + + +
    +

    Additional arguments to pass on to scipy.optimize.minimize.

    +
    +
    + {} +
    +

    Returns:

    @@ -679,8 +759,7 @@

    Source code in lat_alignment/fitting.py -

     72
    - 73
    +              
     73
      74
      75
      76
    @@ -736,18 +815,19 @@ 

    126 127 128 -129

    def mirror_fit(
    +129
    +130
    def mirror_fit(
         points: NDArray[np.floating],
         a: NDArray[np.floating],
         compensate: float = 0,
         to_points: bool = True,
    -    **kwargs
    +    **kwargs,
     ) -> tuple[NDArray[np.floating], float]:
         """
         Fit points against the mirror surface.
         Ideally the points should be in the mirror's local coordinate system.
     
    -    Paramaters
    +    Parameters
         ----------
         points : NDArray[np.floating]
             Array of points to compare against the mirror.
    @@ -815,18 +895,71 @@ 

    Essentially just a curvature weighted chisq.

    -
    - Paramaters -

    points : NDArray[np.floating] - Array of points to compare against the mirror. - Should have shape (npoint, 3). -a : NDArray[np.floating] - Coeffecients of the mirror function. - Use a_primary for the primary mirror and a_secondary for the secondary. -compensate : float, default: 0.0 - Amount to compensate the mirror surface by. - This is useful to model things like the surface traced out by an SMR.

    -
    +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + points + + NDArray[floating] + +
    +

    Array of points to compare against the mirror. +Should have shape (npoint, 3).

    +
    +
    + required +
    + a + + NDArray[floating] + +
    +

    Coeffecients of the mirror function. +Use a_primary for the primary mirror and a_secondary for the secondary.

    +
    +
    + required +
    + compensate + + float + +
    +

    Amount to compensate the mirror surface by. +This is useful to model things like the surface traced out by an SMR.

    +
    +
    + 0.0 +
    +

    Returns:

    @@ -852,8 +985,7 @@

    Source code in lat_alignment/fitting.py -

    13
    -14
    +              
    14
     15
     16
     17
    @@ -880,14 +1012,15 @@ 

    38 39 40 -41

    def mirror_objective(
    +41
    +42
    def mirror_objective(
         points: NDArray[np.floating], a: NDArray[np.floating], compensate: float = 0
     ) -> float:
         """
         Objective function to minimize when fitting to mirror surface.
         Essentially just a curvature weighted chisq.
     
    -    Paramaters
    +    Parameters
         ----------
         points : NDArray[np.floating]
             Array of points to compare against the mirror.
    @@ -930,16 +1063,55 @@ 

    This is the transform we are fitting for.

    -
    - Paramaters -

    transform_pars : NDArray[np.floating] - Flattened affine transform and shift, has to be 1d for use with minimizers. - Should have shape (12,) where the first 9 elements are the flattened affine transform, - and the last 3 are the shift in (x, y, z) applied after the affine transform. -points : NDArray[np.floating] - Array of points to compare against the mirror. - Should have shape (npoint, 3).

    -
    +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + transform_pars + + NDArray[floating] + +
    +

    Flattened affine transform and shift, has to be 1d for use with minimizers. +Should have shape (12,) where the first 9 elements are the flattened affine transform, +and the last 3 are the shift in (x, y, z) applied after the affine transform.

    +
    +
    + required +
    + points + + NDArray[floating] + +
    +

    Array of points to compare against the mirror. +Should have shape (npoint, 3).

    +
    +
    + required +
    +

    Returns:

    @@ -966,8 +1138,7 @@

    Source code in lat_alignment/fitting.py -

    44
    -45
    +              
    45
     46
     47
     48
    @@ -991,14 +1162,15 @@ 

    66 67 68 -69

    def mirror_transform(
    +69
    +70
    def mirror_transform(
         transform_pars: NDArray[np.floating], points: NDArray[np.floating]
     ) -> NDArray[np.floating]:
         """
         Function to apply an affine transform to the mirror.
         This is the transform we are fitting for.
     
    -    Paramaters
    +    Parameters
         ----------
         transform_pars : NDArray[np.floating]
             Flattened affine transform and shift, has to be 1d for use with minimizers.
    @@ -1037,11 +1209,36 @@ 

    Compute auto correlation of residuals from fit.

    -
    - Paramaters -

    residuals : NDArray[np.floating] - Residuals between measured point cloud and fit model.

    -
    +

    Parameters:

    + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + residuals + + NDArray[floating] + +
    +

    Residuals between measured point cloud and fit model.

    +
    +
    + required +
    +

    Returns:

    @@ -1077,8 +1274,7 @@

    Source code in lat_alignment/fitting.py -

    215
    -216
    +              
    216
     217
     218
     219
    @@ -1111,13 +1307,14 @@ 

    246 247 248 -249

    def res_auto_corr(
    +249
    +250
    def res_auto_corr(
         residuals: NDArray[np.floating],
     ) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
         """
         Compute auto correlation of residuals from fit.
     
    -    Paramaters
    +    Parameters
         ----------
         residuals : NDArray[np.floating]
             Residuals between measured point cloud and fit model.
    @@ -1166,13 +1363,51 @@ 

    Fit a power law model of tension to a point cloud of residuals.

    -
    - Paramaters -

    residuals : NDArray[np.floating] - Residuals between measured point cloud and fit model. -**kwargs - Arguments to be passed to scipy.optimize.minimize

    -
    +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + residuals + + NDArray[floating] + +
    +

    Residuals between measured point cloud and fit model.

    +
    +
    + required +
    + **kwargs + + +
    +

    Arguments to be passed to scipy.optimize.minimize

    +
    +
    + {} +
    +

    Returns:

    @@ -1208,8 +1443,7 @@

    Source code in lat_alignment/fitting.py -

    175
    -176
    +              
    176
     177
     178
     179
    @@ -1245,13 +1479,14 @@ 

    209 210 211 -212

    def tension_fit(
    +212
    +213
    def tension_fit(
         residuals: NDArray[np.floating], **kwargs
     ) -> tuple[NDArray[np.floating], float]:
         """
         Fit a power law model of tension to a point cloud of residuals.
     
    -    Paramaters
    +    Parameters
         ----------
         residuals : NDArray[np.floating]
             Residuals between measured point cloud and fit model.
    @@ -1304,24 +1539,119 @@ 

    Currently the model used is a radial power law.

    -
    - Paramaters -

    x0 : float - Center of the power law in x. -y0 : float - Center of the power law in y. -t : float. - Amplitude of power law, - nominally the offset due to tensioning in the center of panel. -a : float - Base of power law. -b : float - Exponential scale factor of power law -points : NDArray[np.floating] - Points to compute power law at. - Only the x and y coordinates are used (first two collumns). - So should be (npoint, 2) but (npoint, ndim>2) is also fine.

    -
    +

    Parameters:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionDefault
    + x0 + + float + +
    +

    Center of the power law in x.

    +
    +
    + required +
    + y0 + + float + +
    +

    Center of the power law in y.

    +
    +
    + required +
    + t + + float. + +
    +

    Amplitude of power law, +nominally the offset due to tensioning in the center of panel.

    +
    +
    + required +
    + a + + float + +
    +

    Base of power law.

    +
    +
    + required +
    + b + + float + +
    +

    Exponential scale factor of power law

    +
    +
    + required +
    + points + + NDArray[floating] + +
    +

    Points to compute power law at. +Only the x and y coordinates are used (first two collumns). +So should be (npoint, 2) but (npoint, ndim>2) is also fine.

    +
    +
    + required +
    +

    Returns:

    @@ -1348,8 +1678,7 @@

    Source code in lat_alignment/fitting.py -

    132
    -133
    +              
    133
     134
     135
     136
    @@ -1388,7 +1717,8 @@ 

    169 170 171 -172

    def tension_model(
    +172
    +173
    def tension_model(
         x0: float, y0: float, t: float, a: float, b: float, points: NDArray[np.floating]
     ) -> NDArray[np.floating]:
         """
    @@ -1396,7 +1726,7 @@ 

    Currently the model used is a radial power law. - Paramaters + Parameters ---------- x0 : float Center of the power law in x. diff --git a/4.0.0/reference/transforms/index.html b/4.0.0/reference/transforms/index.html index ee7ab1e..e72c07a 100644 --- a/4.0.0/reference/transforms/index.html +++ b/4.0.0/reference/transforms/index.html @@ -714,8 +714,7 @@

    Source code in lat_alignment/transforms.py -
    342
    -343
    +              
    343
     344
     345
     346
    @@ -823,7 +822,8 @@ 

    448 449 450 -451

    def align_photo(
    +451
    +452
    def align_photo(
         labels: NDArray[np.str_],
         coords: NDArray[np.float32],
         *,
    @@ -1048,8 +1048,7 @@ 

    Source code in lat_alignment/transforms.py -
    247
    -248
    +              
    248
     249
     250
     251
    @@ -1140,7 +1139,8 @@ 

    336 337 338 -339

    def coord_transform(
    +339
    +340
    def coord_transform(
         coords: NDArray[np.float32], cfrom: str, cto: str
     ) -> NDArray[np.float32]:
         """
    diff --git a/4.0.0/search/search_index.json b/4.0.0/search/search_index.json
    index 0c56b1b..8b83762 100644
    --- a/4.0.0/search/search_index.json
    +++ b/4.0.0/search/search_index.json
    @@ -1 +1 @@
    -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"LAT Alignment","text":"

    Tools for LAT mirror alignment

    "},{"location":"#installation","title":"Installation","text":"

    Technically after cloning this repository you can just run python lat_alignment/alignment.py PATH/TO/CONFIG, but it is recommended that you install this as a package instead.

    To do this just run: pip install -e . from the root of this repository.

    This has two main benefits over running the script directly: 1. It will handle dependencies for you. 2. This sets up an entrypoint called lat_alignment so that you can call the code from anywhere. This is nice because now you can call the code from the measurement directory where you are most likely editing files, saving you the hassle of having to cd or wrangle long file paths.

    "},{"location":"#usage","title":"Usage","text":"
    1. Create the appropriate directory structure for your measurement (see File Structure for details).
    2. Place the measurement files in the appropriate place in your created directory (see Measurement Files for details).
    3. Create a file with any information about the measurement that could prove useful (see Description File for details).
    4. Create a config file for your measurement (see Config File for details).
    5. Run the alignment script with lat_alignment /PATH/TO/CONFIG
    6. Follow the instructions in the output to align panels. This output will both be printed in the terminal and written to an output file (see Output File)
    "},{"location":"#file-structure","title":"File Structure","text":"

    Measurements should be organized in the following file structure

    measurements\n|\n\u2514\u2500\u2500\u2500YYYYMMDD_num\n|   |config.txt\n|   |description.txt\n|   |output.txt\n|   |adjusters.yaml\n|   |\n|   \u2514\u2500\u2500\u2500M1\n|   |   |XX-XXXXXX.txt\n|   |   |XX-XXXXXX.txt\n|   |   |...\n|   |\n|   \u2514\u2500\u2500\u2500M2\n|   |   |XX-XXXXXX.txt\n|   |   |XX-XXXXXX.txt\n|   |   |...\n|   |\n|   \u2514\u2500\u2500\u2500plots\n|       \u2514\u2500\u2500\u2500M1\n|       |   |XX-XXXXXX_surface.png\n|       |   |XX-XXXXXX_hist.png\n|       |   |XX-XXXXXX_ps.png\n|       |   |...\n|       |\n|       \u2514\u2500\u2500\u2500M2\n|           |XX-XXXXXX_surface.png\n|           |XX-XXXXXX_hist.png\n|           |XX-XXXXXX_ps.png\n|           |...\n|       \n\u2514\u2500\u2500\u2500YYYYMMDD_num\n|   |config.txt\n|   |description.txt\n|   |adjusters.yaml\n|   |\n|   \u2514\u2500\u2500\u2500M1\n|   |   |XX-XXXXXX.txt\n|   |   |XX-XXXXXX.txt\n|   |   |...\n|   |\n|   \u2514\u2500\u2500\u2500M2\n|       |XX-XXXXXX.txt\n|       |XX-XXXXXX.txt\n|       |...\n|   |\n|   \u2514\u2500\u2500\u2500plots\n|       \u2514\u2500\u2500\u2500M1\n|       |   |XX-XXXXXX_surface.png\n|       |   |XX-XXXXXX_hist.png\n|       |   |XX-XXXXXX_ps.png\n|       |   |...\n|       |\n|       \u2514\u2500\u2500\u2500M2\n|           |XX-XXXXXX_surface.png\n|           |XX-XXXXXX_hist.png\n|           |XX-XXXXXX_ps.png\n|           |...\n|...\n
    "},{"location":"#measurement-directories","title":"Measurement Directories","text":"

    Each directory YYYYMMDD_num refers to a specific measurement session. Where YYYYMMDD refers to the date of the measurement and num refers to which number measurement on that date it was. For example the second measurement taken on January 1st, 2022 would be 20220101_02.

    This is the file path that should be provided to alignment.py as the measurement_dir argument.

    "},{"location":"#config-file","title":"Config File","text":"

    The file config.yaml contains configuration options. Below is an annotated example with all possible options.

    # The measurement directory\n# If not provided the dirctory containing the config will be used\nmeasurement_dir: PATH/TO/MEASUREMENT\n\n# The path the the dirctory containing the cannonical adjuster locations\n# If not provided the can_points directory in the root of this repository is used\ncannonical_points: PATH/TO/CAN/POINTS\n\n# Coordinate system of measurements\n# Possible vaules are [\"cad\", \"global\", \"primary\", \"secondary\"]\ncoordinates: cad # default value\n\n# Amount to shift the origin of the measurements by\n# Should be a 3 element list\norigin_shift: [0, 0, 0] # default value\n\n# FARO compensation\ncompensation: 0.0 # default value\n\n# Set to True to apply common mode subtraction\ncm_sub: False # default value\n\n# Set to True to make plots if panels \nplots: False # default value\n\n# Where to save log\n# If not provided log is saved to a file called output.txt\n# in the measurement_dir for this measurement\nlog_file: null # Set to null to only print output and not save\n\n# Path to a yaml file with the current adjuster positions\n# If null (None) then all adjusters are assumed to be at 0\n# You probably want to point this to the file generated\n# in the previous alignment run if you have it\nadj_path: null # default value\n\n# Path to where to store the adjuster postions after aligning\n# If null (None) will store in a file called adjusters.yaml\n# in the measurement_dir for this measurement\nadj_out: null # default value\n\n# Defines the allowed adjuster range in mm\nadj_low: -1 # default value\nadj_high: 1 # default value\n

    If you are using all default values make a blank config with touch config.yaml

    "},{"location":"#description-file","title":"Description File","text":"

    Each measurement directory should contain a file description.txt with information on the measurement. Any information that could provide useful context when looking at the measurement/alignment after the fact should be included here (ie: who performed the measurement, where the measurement was taken, etc.).

    "},{"location":"#output-file","title":"Output File","text":"

    Output generated by alignment.py. By default this is saved at measurement_dir/output.txt

    Note that this file gets overwritten when lat_alignment is run, so if you want to store multiple copies with different configs or something rename them or change the log_file in the config.

    "},{"location":"#adjuster-positions","title":"Adjuster Positions","text":"

    Positions of adjusters after applying the calculated adjustments. This is a yaml file nominally saved at measurement_dir/adjusters/yaml

    Each element in the file is in the format:

    PANEL_NUMBER: [X, Y, ADJ_1, ADJ_2, ADJ_3, ADJ_4, ADJ_5] \n
    "},{"location":"#mirror-directories","title":"Mirror Directories","text":"

    Directories containing the measurements files within each root measurement directory. M1 contains the measurements for the primary mirror and M2 contains the measurements for the secondary mirror. If you don't have measurements for one of the mirrors you do not need to create an empty directory for it.

    "},{"location":"#measurement-files","title":"Measurement Files","text":"

    Files containing the point cloud measurements for a given panel. Should live in the mirror directory that the panel belongs to. Files should be named XX-XXXXXX.txt where XX-XXXXXX is the panel number. The numbering system is as follows: * First four digits (XX-XX) are the telescope number. For the LAT this is 01-01 * Fifth digit is the mirror number. This is 1 for the primary and 2 for the secondary. * Sixth digit is the panel row * Seventh digit is the panel column * Eight digit is the panel number (current, spare, replacement, etc.)

    "},{"location":"#plot-directory","title":"Plot Directory","text":"

    If the plots option is set to True then the root measurement will contain a directory called plots. Within this directory will be directories for each mirror measured, M1 for the primary and M2 for the secondary. Each of these will contain three plots per panel measured: * XX-XXXXXX_surface.png, a plot of the panel's surface in the mirror's coordinate system. * XX-XXXXXX_hist.png, a histogram of the residuals from the panel's fit. * XX-XXXXXX_ps.png, a plot of the power spectrum of the residuals from the panel's fit.

    Where XX-XXXXXX is the panel number.

    "},{"location":"#coordinate-systems","title":"Coordinate Systems","text":"

    The relevant coordinate systems are marked in the diagram below:

    Where the orange circle marks the global coordinate system, the green circle marks the primary coordinate system, and the blue circle marks the secondary coordinate system.

    Additionally there is a cad coordinate system that is defined as the coordinate system from the SolidWorks model. It is given by the following transformation from the global coordinate system:

    x -> y - 200 mm\ny -> x\nz -> -z\n

    It is currently unclear why the 200 mm offset exists.

    Note that the files in the can_points directory are in the cad coordinate system.

    All measurements should be done in one of these four coordinate systems modulo a known shift in the origin.

    "},{"location":"#bugs-and-feature-requests","title":"Bugs and Feature Requests","text":"

    For low priority bugs and feature requests submit an issue on the git repo.

    For higher priority issues (or questions that require an expedient answer) email, Slack, or call me.

    "},{"location":"#contributing","title":"Contributing","text":"

    If you wish to contribute to this repository (either code or adding measurement files) contact me via email or Slack.

    If you are contributing code please do so by creating a branch and submitting a pull request. Try to keep things as close to PEP8 as possible.

    "},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
    • adjustments
    • alignment
    • fitting
    • io
    • mirror
    • transforms
    "},{"location":"reference/adjustments/","title":"adjustments","text":"

    Calculate adjustments needed to align LAT mirror panel

    Author: Saianeesh Keshav Haridas

    "},{"location":"reference/adjustments/#lat_alignment.adjustments.adjustment_fit_func","title":"adjustment_fit_func(pars, can_points, points, adjustors)","text":"

    Function to minimize when calculating adjustments

    @param pars: The parameters to fit for: dx: Translation in x dy: Translation in y dz: Translation in z thetha_0: Angle to rotate about first adjustor axis thetha_1: Angle to rotate about second adjustor axis z_t: Additional translation to tension the center point @param can_points: The cannonical positions of the points to align @param points: The measured positions of the points to align @param adjustors: The measured positions of the adjustors

    @return norm: The norm of (cannonical positions - transformed positions)

    Source code in lat_alignment/adjustments.py
    def adjustment_fit_func(\n    pars: ndarray, can_points: ndarray, points: ndarray, adjustors: ndarray\n) -> float64:\n    \"\"\"\n    Function to minimize when calculating adjustments\n\n    @param pars: The parameters to fit for:\n                    dx: Translation in x\n                    dy: Translation in y\n                    dz: Translation in z\n                    thetha_0: Angle to rotate about first adjustor axis\n                    thetha_1: Angle to rotate about second adjustor axis\n                    z_t: Additional translation to tension the center point\n    @param can_points: The cannonical positions of the points to align\n    @param points: The measured positions of the points to align\n    @param adjustors: The measured positions of the adjustors\n\n    @return norm: The norm of (cannonical positions - transformed positions)\n    \"\"\"\n    dx, dy, dz, thetha_0, thetha_1, z_t = pars\n    points, adjustors = translate_panel(points, adjustors, dx, dy, dz)\n    points, adjustors = rotate_panel(points, adjustors, thetha_0, thetha_1)\n    points[-1, -1] += z_t\n    return np.linalg.norm(can_points - points)\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.calc_adjustments","title":"calc_adjustments(can_points, points, adjustors, **kwargs)","text":"

    Calculate adjustments needed to align panel

    @param can_points: The cannonical position of the points to align @param points: The measured positions of the points to align @param adjustors: The measured positions of the adjustors @param **kwargs: Arguments to be passed to scipy.optimize.minimize

    @return dx: The required translation of panel in x @return dy: The required translation of panel in y @return d_adj: The amount to move each adjustor @return dx_err: The error in the fit for dx @return dy_err: The error in the fit for dy @return d_adj_err: The error in the fit for d_adj

    Source code in lat_alignment/adjustments.py
    def calc_adjustments(\n    can_points: ndarray, points: ndarray, adjustors: ndarray, **kwargs\n) -> Tuple[float64, float64, ndarray, float64, float64, ndarray]:\n    \"\"\"\n    Calculate adjustments needed to align panel\n\n    @param can_points: The cannonical position of the points to align\n    @param points: The measured positions of the points to align\n    @param adjustors: The measured positions of the adjustors\n    @param **kwargs: Arguments to be passed to scipy.optimize.minimize\n\n    @return dx: The required translation of panel in x\n    @return dy: The required translation of panel in y\n    @return d_adj: The amount to move each adjustor\n    @return dx_err: The error in the fit for dx\n    @return dy_err: The error in the fit for dy\n    @return d_adj_err: The error in the fit for d_adj\n    \"\"\"\n    res = opt.minimize(\n        adjustment_fit_func, np.zeros(6), (can_points, points, adjustors), **kwargs\n    )\n\n    dx, dy, dz, thetha_0, thetha_1, z_t = res.x\n    _points, _adjustors = translate_panel(points, adjustors, dx, dy, dz)\n    _points, _adjustors = rotate_panel(_points, _adjustors, thetha_0, thetha_1)\n    _adjustors[-1, -1] += z_t\n    d_adj = _adjustors - adjustors\n\n    ftol = 2.220446049250313e-09\n    if \"ftol\" in kwargs:\n        ftol = kwargs[\"ftol\"]\n    perr = np.sqrt(ftol * np.diag(res.hess_inv))\n    dx_err, dy_err, dz_err, thetha_0_err, thetha_1_err, z_t_err = perr\n    _points, _adjustors = translate_panel(points, adjustors, dx_err, dy_err, dz_err)\n    _points, _adjustors = rotate_panel(_points, _adjustors, thetha_0_err, thetha_1_err)\n    _adjustors[-1, -1] += z_t_err\n    d_adj_err = _adjustors - adjustors\n\n    return dx, dy, d_adj[:, 2], dx_err, dy_err, d_adj_err[:, 2]\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.rotate","title":"rotate(point, end_point1, end_point2, thetha)","text":"

    Rotate a point about an axis

    @param point: The point to rotate @param end_point1: A point on the axis of rotation @param end_point2: Another point on the axis of rotation @param thetha: Angle in radians to rotate by

    @return point: The rotated point

    Source code in lat_alignment/adjustments.py
    def rotate(\n    point: ndarray, end_point1: ndarray, end_point2: ndarray, thetha: float64\n) -> ndarray:\n    \"\"\"\n    Rotate a point about an axis\n\n    @param point: The point to rotate\n    @param end_point1: A point on the axis of rotation\n    @param end_point2: Another point on the axis of rotation\n    @param thetha: Angle in radians to rotate by\n\n    @return point: The rotated point\n    \"\"\"\n    origin = np.mean((end_point1, end_point2))\n    point_0 = point - origin\n    ax = end_point2 - end_point1\n    ax = rot.from_rotvec(thetha * ax / np.linalg.norm(ax))\n    point_0 = ax.apply(point_0)\n    return point_0 + origin\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.rotate_panel","title":"rotate_panel(points, adjustors, thetha_0, thetha_1)","text":"

    Rotate panel about axes created by adjustors

    @param points: Points on panel to rotate @param adjustors: Adjustor positions @param thetha_0: Angle to rotate about first adjustor axis @param thetha_1: Angle to rotate about second adjustor axis

    @return rot_points: The rotated points @return rot_adjustors: The rotated adjustors

    Source code in lat_alignment/adjustments.py
    def rotate_panel(\n    points: ndarray, adjustors: ndarray, thetha_0: float64, thetha_1: float64\n) -> Tuple[ndarray, ndarray]:\n    \"\"\"\n    Rotate panel about axes created by adjustors\n\n    @param points: Points on panel to rotate\n    @param adjustors: Adjustor positions\n    @param thetha_0: Angle to rotate about first adjustor axis\n    @param thetha_1: Angle to rotate about second adjustor axis\n\n    @return rot_points: The rotated points\n    @return rot_adjustors: The rotated adjustors\n    \"\"\"\n    rot_points = np.zeros(points.shape)\n    rot_adjustors = np.zeros(adjustors.shape)\n\n    n_points = len(points)\n    n_adjustors = len(adjustors)\n\n    for i in range(n_points):\n        rot_points[i] = rotate(points[i], adjustors[1], adjustors[2], thetha_0)\n        rot_points[i] = rotate(rot_points[i], adjustors[0], adjustors[3], thetha_1)\n    for i in range(n_adjustors):\n        rot_adjustors[i] = rotate(adjustors[i], adjustors[1], adjustors[2], thetha_0)\n        rot_adjustors[i] = rotate(\n            rot_adjustors[i], adjustors[0], adjustors[3], thetha_1\n        )\n    return rot_points, rot_adjustors\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.translate_panel","title":"translate_panel(points, adjustors, dx, dy, dz)","text":"

    Translate panel

    @param points: The points on panel to translate @param adjustors: Adjustor positions @param dx: Translation in x @param dy: Translation in y @param dz: Translation in z

    @return points: The translated points @return adjustors: The translated adjustors

    Source code in lat_alignment/adjustments.py
    def translate_panel(\n    points: ndarray, adjustors: ndarray, dx: float64, dy: float64, dz: float64\n) -> Tuple[ndarray, ndarray]:\n    \"\"\"\n    Translate panel\n\n    @param points: The points on panel to translate\n    @param adjustors: Adjustor positions\n    @param dx: Translation in x\n    @param dy: Translation in y\n    @param dz: Translation in z\n\n    @return points: The translated points\n    @return adjustors: The translated adjustors\n    \"\"\"\n    translation = np.array((dx, dy, dz))\n    return points + translation, adjustors + translation\n
    "},{"location":"reference/alignment/","title":"alignment","text":""},{"location":"reference/fitting/","title":"fitting","text":"

    Functions for fitting against the mirror surface.

    "},{"location":"reference/fitting/#lat_alignment.fitting.mirror_fit","title":"mirror_fit(points, a, compensate=0, to_points=True, **kwargs)","text":"

    Fit points against the mirror surface. Ideally the points should be in the mirror's local coordinate system.

    Paramaters

    points : NDArray[np.floating] Array of points to compare against the mirror. Should have shape (npoint, 3). a : NDArray[np.floating] Coeffecients of the mirror function. Use a_primary for the primary mirror and a_secondary for the secondary. compensate : float, default: 0.0 Amount to compensate the mirror surface by. This is useful to model things like the surface traced out by an SMR. to_points : bool, default: True If True, the transform will be inverted to align the model to the points. **kwargs Additional arguments to pass on to scipy.optimize.minimize.

    Returns:

    Name Type Description transform_pars NDArray[floating]

    Flattened affine transform and shift, has to be 1d for use with minimizers. Will have shape (12,) where the first 9 elements are the flattened affine transform, and the last 3 are the shift in (x, y, z) applied after the affine transform.

    rms float

    The RMS error between the transformed points and the model.

    Source code in lat_alignment/fitting.py
    def mirror_fit(\n    points: NDArray[np.floating],\n    a: NDArray[np.floating],\n    compensate: float = 0,\n    to_points: bool = True,\n    **kwargs\n) -> tuple[NDArray[np.floating], float]:\n    \"\"\"\n    Fit points against the mirror surface.\n    Ideally the points should be in the mirror's local coordinate system.\n\n    Paramaters\n    ----------\n    points : NDArray[np.floating]\n        Array of points to compare against the mirror.\n        Should have shape (npoint, 3).\n    a : NDArray[np.floating]\n        Coeffecients of the mirror function.\n        Use a_primary for the primary mirror and a_secondary for the secondary.\n    compensate : float, default: 0.0\n        Amount to compensate the mirror surface by.\n        This is useful to model things like the surface traced out by an SMR.\n    to_points : bool, default: True\n        If True, the transform will be inverted to align the model to the points.\n    **kwargs\n        Additional arguments to pass on to scipy.optimize.minimize.\n\n    Returns\n    -------\n    transform_pars : NDArray[np.floating]\n        Flattened affine transform and shift, has to be 1d for use with minimizers.\n        Will have shape (12,) where the first 9 elements are the flattened affine transform,\n        and the last 3 are the shift in (x, y, z) applied after the affine transform.\n    rms : float\n        The RMS error between the transformed points and the model.\n    \"\"\"\n\n    def _fit_func(transform_pars, points, a, compensate):\n        points_transformed = mirror_transform(transform_pars, points)\n        chisq = mirror_objective(points_transformed, a, compensate)\n        return chisq\n\n    x0 = np.concatenate((np.eye(3).ravel(), np.zeros(3)))\n    res = opt.minimize(_fit_func, x0, args=(points, a, compensate), **kwargs)\n\n    transform_pars = res.x\n    transformed = mirror_transform(transform_pars, points)\n    z = mr.mirror(transformed[:, 0], transformed[:, 1], a, compensate)\n    rms = np.sqrt(np.mean((z - transformed[:, 2]) ** 2))\n\n    if to_points:\n        aff = transform_pars[:9].reshape((3, 3))\n        sft = transform_pars[9:]\n        aff = np.linalg.inv(aff)\n        sft = (-1 * sft) @ aff\n        transform_pars = np.concatenate((aff.ravel(), sft))\n\n    return transform_pars, rms\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.mirror_objective","title":"mirror_objective(points, a, compensate=0)","text":"

    Objective function to minimize when fitting to mirror surface. Essentially just a curvature weighted chisq.

    Paramaters

    points : NDArray[np.floating] Array of points to compare against the mirror. Should have shape (npoint, 3). a : NDArray[np.floating] Coeffecients of the mirror function. Use a_primary for the primary mirror and a_secondary for the secondary. compensate : float, default: 0.0 Amount to compensate the mirror surface by. This is useful to model things like the surface traced out by an SMR.

    Returns:

    Name Type Description chisq float

    The value to minimize when fitting to.

    Source code in lat_alignment/fitting.py
    def mirror_objective(\n    points: NDArray[np.floating], a: NDArray[np.floating], compensate: float = 0\n) -> float:\n    \"\"\"\n    Objective function to minimize when fitting to mirror surface.\n    Essentially just a curvature weighted chisq.\n\n    Paramaters\n    ----------\n    points : NDArray[np.floating]\n        Array of points to compare against the mirror.\n        Should have shape (npoint, 3).\n    a : NDArray[np.floating]\n        Coeffecients of the mirror function.\n        Use a_primary for the primary mirror and a_secondary for the secondary.\n    compensate : float, default: 0.0\n        Amount to compensate the mirror surface by.\n        This is useful to model things like the surface traced out by an SMR.\n\n    Returns\n    -------\n    chisq : float\n        The value to minimize when fitting to.\n    \"\"\"\n    surface = mr.mirror(points[:, 0], points[:, 1], a, compensate)\n    norm = mr.mirror_norm(points[:, 0], points[:, 1], a)\n    res = (points[:, 2] - surface) * (norm[2] ** 2)\n\n    return res @ res.T\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.mirror_transform","title":"mirror_transform(transform_pars, points)","text":"

    Function to apply an affine transform to the mirror. This is the transform we are fitting for.

    Paramaters

    transform_pars : NDArray[np.floating] Flattened affine transform and shift, has to be 1d for use with minimizers. Should have shape (12,) where the first 9 elements are the flattened affine transform, and the last 3 are the shift in (x, y, z) applied after the affine transform. points : NDArray[np.floating] Array of points to compare against the mirror. Should have shape (npoint, 3).

    Returns:

    Name Type Description points_transformed NDArray[floating]

    Array of transformed points. Will have shape (npoint, 3).

    Source code in lat_alignment/fitting.py
    def mirror_transform(\n    transform_pars: NDArray[np.floating], points: NDArray[np.floating]\n) -> NDArray[np.floating]:\n    \"\"\"\n    Function to apply an affine transform to the mirror.\n    This is the transform we are fitting for.\n\n    Paramaters\n    ----------\n    transform_pars : NDArray[np.floating]\n        Flattened affine transform and shift, has to be 1d for use with minimizers.\n        Should have shape (12,) where the first 9 elements are the flattened affine transform,\n        and the last 3 are the shift in (x, y, z) applied after the affine transform.\n    points : NDArray[np.floating]\n        Array of points to compare against the mirror.\n        Should have shape (npoint, 3).\n\n    Returns\n    -------\n    points_transformed : NDArray[np.floating]\n        Array of transformed points.\n        Will have shape (npoint, 3).\n    \"\"\"\n    aff = transform_pars[:9].reshape((3, 3))\n    sft = transform_pars[9:]\n    return points @ aff + sft\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.res_auto_corr","title":"res_auto_corr(residuals)","text":"

    Compute auto correlation of residuals from fit.

    Paramaters

    residuals : NDArray[np.floating] Residuals between measured point cloud and fit model.

    Returns:

    Name Type Description ac NDArray[floating]

    Auto correlation, really just the deviations in mm at each distance scale.

    ac_dists NDArray[floating]

    Distance scale of each value in ac.

    Source code in lat_alignment/fitting.py
    def res_auto_corr(\n    residuals: NDArray[np.floating],\n) -> tuple[NDArray[np.floating], NDArray[np.floating]]:\n    \"\"\"\n    Compute auto correlation of residuals from fit.\n\n    Paramaters\n    ----------\n    residuals : NDArray[np.floating]\n        Residuals between measured point cloud and fit model.\n\n    Returns\n    -------\n    ac : NDArray[np.floating]\n        Auto correlation, really just the deviations in mm at each distance scale.\n    ac_dists : NDArray[np.floating]\n        Distance scale of each value in ac.\n    \"\"\"\n    dists = np.zeros((len(residuals), len(residuals)))\n    res_diff = np.zeros((len(residuals), len(residuals)))\n\n    for i in range(len(residuals)):\n        res1 = residuals[i]\n        for j in range(i):\n            res2 = residuals[j]\n            dist = np.linalg.norm((res1[0] - res2[0], res1[1] - res2[1]))\n            dists[i, j] = dist\n            res_diff[i, j] = abs(res1[2] - res2[2])\n    tri_i = np.tril_indices(len(residuals), k=-1)\n    dists = dists[tri_i]\n    res_diff = res_diff[tri_i]\n    ac, bin_e, _ = binned_statistic(dists, res_diff, bins=100)\n    ac_dists = bin_e[:-1] + np.diff(bin_e) / 2.0\n\n    return ac, ac_dists\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.tension_fit","title":"tension_fit(residuals, **kwargs)","text":"

    Fit a power law model of tension to a point cloud of residuals.

    Paramaters

    residuals : NDArray[np.floating] Residuals between measured point cloud and fit model. **kwargs Arguments to be passed to scipy.optimize.minimize

    Returns:

    Name Type Description tension_pars NDArray[floating]

    The fit parameters, see docstring of tension_model for details.

    rms float

    The rms between the input residuals and the fit model.

    Source code in lat_alignment/fitting.py
    def tension_fit(\n    residuals: NDArray[np.floating], **kwargs\n) -> tuple[NDArray[np.floating], float]:\n    \"\"\"\n    Fit a power law model of tension to a point cloud of residuals.\n\n    Paramaters\n    ----------\n    residuals : NDArray[np.floating]\n        Residuals between measured point cloud and fit model.\n    **kwargs\n        Arguments to be passed to scipy.optimize.minimize\n\n    Returns\n    -------\n    tension_pars : NDArray[np.floating]\n        The fit parameters, see docstring of tension_model for details.\n    rms : float\n        The rms between the input residuals and the fit model.\n    \"\"\"\n\n    def min_func(pars, residuals):\n        _z = tension_model(*pars[:5], residuals)\n        return np.sqrt(np.mean((residuals[:, 2] - _z) ** 2))\n\n    if \"bounds\" not in kwargs:\n        ptp = np.ptp(residuals[:, 2])\n        bounds = [\n            (np.min(residuals[:, 0]), np.max(residuals[:, 0])),\n            (np.min(residuals[:, 1]), np.max(residuals[:, 1])),\n            (-1 * ptp, ptp),\n            (1e-10, np.inf),\n            (0, np.inf),\n        ]\n        kwargs[\"bounds\"] = bounds\n    x0 = [np.mean(residuals[:, 0]), np.mean(residuals[:, 1]), 0, 1, 0]\n    res = opt.minimize(min_func, x0, (residuals,), **kwargs)\n    return res.x, res.fun\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.tension_model","title":"tension_model(x0, y0, t, a, b, points)","text":"

    Function to model incorrect panel tensioning. Currently the model used is a radial power law.

    Paramaters

    x0 : float Center of the power law in x. y0 : float Center of the power law in y. t : float. Amplitude of power law, nominally the offset due to tensioning in the center of panel. a : float Base of power law. b : float Exponential scale factor of power law points : NDArray[np.floating] Points to compute power law at. Only the x and y coordinates are used (first two collumns). So should be (npoint, 2) but (npoint, ndim>2) is also fine.

    Returns:

    Name Type Description z NDArray[floating]

    Power law model at each xy. Will have shape (npoint,).

    Source code in lat_alignment/fitting.py
    def tension_model(\n    x0: float, y0: float, t: float, a: float, b: float, points: NDArray[np.floating]\n) -> NDArray[np.floating]:\n    \"\"\"\n    Function to model incorrect panel tensioning.\n    Currently the model used is a radial power law.\n\n\n    Paramaters\n    ----------\n    x0 : float\n        Center of the power law in x.\n    y0 : float\n        Center of the power law in y.\n    t : float.\n        Amplitude of power law,\n        nominally the offset due to tensioning in the center of panel.\n    a : float\n        Base of power law.\n    b : float\n        Exponential scale factor of power law\n    points : NDArray[np.floating]\n        Points to compute power law at.\n        Only the x and y coordinates are used (first two collumns).\n        So should be (npoint, 2) but (npoint, ndim>2) is also fine.\n\n    Returns\n    -------\n    z : NDArray[np.floating]\n        Power law model at each xy.\n        Will have shape (npoint,).\n    \"\"\"\n    # Avoid divide by 0 error\n    if a == 0:\n        return np.zeros(len(points))\n\n    # Compute radius at each point\n    r = np.sqrt((points[:, 0] - x0) ** 2 + (points[:, 1] - y0) ** 2)\n\n    # Return power law\n    return t * (a ** (-b * r))\n
    "},{"location":"reference/io/","title":"io","text":""},{"location":"reference/io/#lat_alignment.io.load_adjusters","title":"load_adjusters(path, mirror)","text":"

    Get nominal adjuster locations from file.

    Parameters:

    Name Type Description Default path str

    Path to the data file.

    required mirror str

    The mirror that these points belong to. Should be either: 'primary' or 'secondary'.

    'primary'

    Returns:

    Name Type Description adjusters dict[tuple[int, int], NDArray[float32]]

    Nominal adjuster locations. This is indexed by a (row, col) tuple. Each entry is (5, 3) array where each row is an adjuster.

    Source code in lat_alignment/io.py
    def load_adjusters(\n    path: str, mirror: str\n) -> dict[tuple[int, int], NDArray[np.float32]]:\n    \"\"\"\n    Get nominal adjuster locations from file.\n\n    Parameters\n    ----------\n    path : str\n        Path to the data file.\n    mirror : str, default: 'primary'\n        The mirror that these points belong to.\n        Should be either: 'primary' or 'secondary'.\n\n    Returns\n    -------\n    adjusters : dict[tuple[int, int], NDArray[np.float32]]\n        Nominal adjuster locations.\n        This is indexed by a (row, col) tuple.\n        Each entry is `(5, 3)` array where each row is an adjuster.\n    \"\"\"\n    if mirror not in [\"primary\", \"secondary\"]:\n        raise ValueError(f\"Invalid mirror: {mirror}\")\n\n    def _transform(coords):\n        coords = np.atleast_2d(coords)\n        coords -= np.array([120, 0, 0])  # cancel out shift\n        return coord_transform(coords, \"va_global\", f\"opt_{mirror}\")\n\n    # TODO: cleaner transform call\n    adjusters = defaultdict(list)\n    c_points = np.genfromtxt(path, dtype=str)\n    for point in c_points:\n        row = point[0][6]\n        col = point[0][7]\n        adjusters[(row, col)] += [_transform(np.array(point[2:], dtype=np.float32))[0]]\n    adjusters = {rc: np.vstack(pts) for rc, pts in adjusters.items()}\n\n    return adjusters\n
    "},{"location":"reference/io/#lat_alignment.io.load_corners","title":"load_corners(path)","text":"

    Get panel corners from file.

    Parameters:

    Name Type Description Default path str

    Path to the data file.

    required

    Returns:

    Name Type Description corners dict[tuple[int, int], ndarray[float32]]

    The corners. This is indexed by a (row, col) tuple. Each entry is (4, 3) array where each row is a corner.

    Source code in lat_alignment/io.py
    def load_corners(path: str) -> dict[tuple[int, int], NDArray[np.float32]]:\n    \"\"\"\n    Get panel corners from file.\n\n    Parameters\n    ----------\n    path : str\n        Path to the data file.\n\n    Returns\n    -------\n    corners : dict[tuple[int, int], ndarray[np.float32]]\n        The corners. This is indexed by a (row, col) tuple.\n        Each entry is `(4, 3)` array where each row is a corner.\n    \"\"\"\n    with open(path) as file:\n        corners_raw = yaml.safe_load(file)\n\n    corners = {\n        (panel[7], panel[9]): np.vstack(\n            [np.array(coord.split(), np.float32) for coord in coords]\n        )\n        for panel, coords in corners_raw.items()\n    }\n    return corners\n
    "},{"location":"reference/io/#lat_alignment.io.load_photo","title":"load_photo(path, align=True, err_thresh=2, plot=True, **kwargs)","text":"

    Load photogrammetry data. Assuming first column is target names and next three are (x, y , z).

    Parameters:

    Name Type Description Default path str

    The path to the photogrammetry data.

    required align bool

    If True align using the invar points.

    True err_thresh float

    How many times the median photogrammetry error a target need to have to be cut.

    2 plot bool

    If True display a scatter plot of targets.

    True **kwargs

    Arguments to pass to align_photo.

    {}

    Returns:

    Name Type Description data dict[str, NDArray[float32]]

    The photogrammetry data. Dict is indexed by the target names.

    Source code in lat_alignment/io.py
    def load_photo(\n    path: str, align: bool = True, err_thresh: float = 2, plot: bool = True, **kwargs\n) -> dict[str, NDArray[np.float32]]:\n    \"\"\"\n    Load photogrammetry data.\n    Assuming first column is target names and next three are (x, y , z).\n\n    Parameters\n    ----------\n    path : str\n        The path to the photogrammetry data.\n    align : bool, default: True\n        If True align using the invar points.\n    err_thresh : float, default: 2\n        How many times the median photogrammetry error\n        a target need to have to be cut.\n    plot: bool, default: True\n        If True display a scatter plot of targets.\n    **kwargs\n        Arguments to pass to `align_photo`.\n\n    Returns\n    -------\n    data : dict[str, NDArray[np.float32]]\n        The photogrammetry data.\n        Dict is indexed by the target names.\n    \"\"\"\n    labels = np.genfromtxt(path, dtype=str, delimiter=\",\", usecols=(0,))\n    coords = np.genfromtxt(path, dtype=np.float32, delimiter=\",\", usecols=(1, 2, 3))\n    errs = np.genfromtxt(path, dtype=np.float32, delimiter=\",\", usecols=(4, 5, 6))\n    msk = (np.char.find(labels, \"TARGET\") >= 0) + (np.char.find(labels, \"CODE\") >= 0)\n\n    labels, coords, errs = labels[msk], coords[msk], errs[msk]\n    err = np.linalg.norm(errs, axis=-1)\n\n    if align:\n        labels, coords, msk = align_photo(labels, coords, **kwargs)\n        err = err[msk]\n    trg_msk = np.char.find(labels, \"TARGET\") >= 0\n    labels = labels[trg_msk]\n    coords = coords[trg_msk]\n    err = err[trg_msk]\n\n    err_msk = err < err_thresh * np.median(err)\n    labels, coords, err = labels[err_msk], coords[err_msk], err[err_msk]\n\n    # Lets find and remove doubles\n    # Dumb brute force\n    edm = make_edm(coords[:, :2])\n    np.fill_diagonal(edm, np.nan)\n    to_kill = []\n    for i in range(len(edm)):\n        if i in to_kill:\n            continue\n        imin = np.nanargmin(edm[i])\n        if edm[i][imin] > 20:\n            continue\n        if err[i] < err[imin]:\n            to_kill += [imin]\n        else:\n            to_kill += [i]\n    msk = ~np.isin(np.arange(len(coords), dtype=int), to_kill)\n    labels, coords = labels[msk], coords[msk]\n\n    if plot:\n        plt.scatter(coords[:, 0], coords[:, 1], c=coords[:, 2], marker=\"x\")\n        plt.colorbar()\n        plt.show()\n\n    data = {label: coord for label, coord in zip(labels, coords)}\n    return data\n
    "},{"location":"reference/mirror/","title":"mirror","text":"

    Functions to describe the mirror surface.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel","title":"Panel dataclass","text":"

    Dataclass for storing a mirror panel.

    Attributes:

    Name Type Description mirror str

    Which mirror this panel is for. Should be 'primary' or 'secondary'.

    row int

    The row of the panel.

    col int

    The column of the panel.

    corners NDArray[float32]

    Array of panel corners. Should have shape (4, 3).

    measurements NDArray[float32]

    The measurement data for this panel. Should be in the mirror's internal coords. Should have shape (npoint, 3).

    nom_adj NDArray[float32]

    The nominal position of the adjusters in the mirror internal coordinates. Should have shape (5, 3).

    compensate float, default: 0

    The amount (in mm) to compensate the model surface by. This is to account for things like the Faro SMR.

    Source code in lat_alignment/mirror.py
    @dataclass\nclass Panel:\n    \"\"\"\n    Dataclass for storing a mirror panel.\n\n    Attributes\n    ----------\n    mirror : str\n        Which mirror this panel is for.\n        Should be 'primary' or 'secondary'.\n    row : int\n        The row of the panel.\n    col : int\n        The column of the panel.\n    corners : NDArray[np.float32]\n        Array of panel corners.\n        Should have shape `(4, 3)`.\n    measurements : NDArray[np.float32]\n        The measurement data for this panel.\n        Should be in the mirror's internal coords.\n        Should have shape `(npoint, 3)`.\n    nom_adj : NDArray[np.float32]\n        The nominal position of the adjusters in the mirror internal coordinates.\n        Should have shape `(5, 3)`.\n    compensate : float, default: 0\n        The amount (in mm) to compensate the model surface by.\n        This is to account for things like the Faro SMR.\n    \"\"\"\n\n    mirror: str\n    row: int\n    col: int\n    corners: NDArray[np.float32]\n    measurements: NDArray[np.float32]\n    nom_adj: NDArray[np.float32]\n    compensate: float = field(default=0.0)\n    adjuster_radius: float = field(default=50.0)\n\n    def __post_init__(self):\n        self.measurements = np.atleast_2d(self.measurements)\n\n    def __setattr__(self, name, value):\n        if (\n            name == \"nom_adj\"\n            or name == \"mirror\"\n            or name == \"measurements\"\n            or name == \"compensate\"\n        ):\n            self.__dict__.pop(\"can_surface\", None)\n            self.__dict__.pop(\"model\", None)\n            self.__dict__.pop(\"residuals\", None)\n            self.__dict__.pop(\"transformed_residuals\", None)\n            self.__dict__.pop(\"res_norm\", None)\n            self.__dict__.pop(\"rms\", None)\n            self.__dict__.pop(\"meas_surface\", None)\n            self.__dict__.pop(\"meas_adj\", None)\n            self.__dict__.pop(\"meas_adj_resid\", None)\n            self.__dict__.pop(\"model_transformed\", None)\n            self.__dict__.pop(\"_transform\", None)\n        elif name == \"adjuster_radius\":\n            self.__dict__.pop(\"meas_adj_resid\", None)\n        return super().__setattr__(name, value)\n\n    @cached_property\n    def model(self):\n        \"\"\"\n        The modeled mirror surface at the locations of the measurementss.\n        \"\"\"\n        model = self.measurements.copy()\n        model[:, 2] = mirror_surface(model[:, 0], model[:, 1], a[self.mirror])\n        if self.compensate != 0.0:\n            compensation = self.compensate * mirror_norm(\n                model[:, 0], model[:0], a[self.mirror]\n            )\n            model += compensation\n        return model\n\n    @cached_property\n    def _transform(self):\n        return get_rigid(self.model, self.measurements, center_dst=True, method=\"mean\")\n\n    @property\n    def rot(self):\n        \"\"\"\n        Rotation that aligns the model to the measurements.\n        \"\"\"\n        return self._transform[0]\n\n    @property\n    def shift(self):\n        \"\"\"\n        Shift that aligns the model to the measurements.\n        \"\"\"\n        return self._transform[1]\n\n    @cached_property\n    def can_surface(self):\n        \"\"\"\n        Get the cannonical points to define the panel surface.\n        These are the adjuster positions projected only the mirror surface.\n        Note that this is in the nominal coordinates not the measured ones.\n        \"\"\"\n        can_z = mirror_surface(self.nom_adj[:, 0], self.nom_adj[:, 1], a[self.mirror])\n        points = self.nom_adj.copy()\n        points[:, 2] = can_z\n        return points\n\n    @cached_property\n    def meas_surface(self):\n        \"\"\"\n        The cannonical surface transformed to be in the measured coordinates.\n        \"\"\"\n        return apply_transform(self.can_surface, self.rot, self.shift)\n\n    @cached_property\n    def meas_adj(self):\n        \"\"\"\n        The adjuster points transformed to be in the measured coordinates.\n        \"\"\"\n        return apply_transform(self.nom_adj, self.rot, self.shift)\n\n    @cached_property\n    def meas_adj_resid(self):\n        \"\"\"\n        A correction that can be applied to `meas_adj` where we compute\n        the average residual of measured points from the transformed model\n        that are within `adjuster_radius` of the adjuster point in `xy`.\n        \"\"\"\n        resid = np.zeros(len(self.meas_adj))\n        for i, adj in enumerate(self.meas_adj):\n            dists = np.linalg.norm(self.measurements[:, :2] - adj[:2], axis=-1)\n            msk = dists <= self.adjuster_radius\n            if np.sum(msk) == 0:\n                continue\n            resid[i] = np.mean(self.transformed_residuals[msk, 2])\n\n        return resid\n\n    @cached_property\n    def model_transformed(self):\n        \"\"\"\n        The model transformed to be in the measured coordinates.\n        \"\"\"\n        return apply_transform(self.model, self.rot, self.shift)\n\n    @cached_property\n    def residuals(self):\n        \"\"\"\n        Get residuals between model and measurements.\n        \"\"\"\n        return self.measurements - self.model\n\n    @cached_property\n    def transformed_residuals(self):\n        \"\"\"\n        Get residuals between transformed model and measurements.\n        \"\"\"\n        return self.measurements - self.model_transformed\n\n    @cached_property\n    def res_norm(self):\n        \"\"\"\n        Get norm of residuals between transformed model and measurements.\n        \"\"\"\n        return np.linalg.norm(self.residuals, axis=-1)\n\n    @cached_property\n    def rms(self):\n        \"\"\"\n        Get rms between model and measurements.\n        \"\"\"\n        return np.sqrt(np.mean(self.residuals[:, 2].ravel() ** 2))\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.can_surface","title":"can_surface cached property","text":"

    Get the cannonical points to define the panel surface. These are the adjuster positions projected only the mirror surface. Note that this is in the nominal coordinates not the measured ones.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.meas_adj","title":"meas_adj cached property","text":"

    The adjuster points transformed to be in the measured coordinates.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.meas_adj_resid","title":"meas_adj_resid cached property","text":"

    A correction that can be applied to meas_adj where we compute the average residual of measured points from the transformed model that are within adjuster_radius of the adjuster point in xy.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.meas_surface","title":"meas_surface cached property","text":"

    The cannonical surface transformed to be in the measured coordinates.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.model","title":"model cached property","text":"

    The modeled mirror surface at the locations of the measurementss.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.model_transformed","title":"model_transformed cached property","text":"

    The model transformed to be in the measured coordinates.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.res_norm","title":"res_norm cached property","text":"

    Get norm of residuals between transformed model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.residuals","title":"residuals cached property","text":"

    Get residuals between model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.rms","title":"rms cached property","text":"

    Get rms between model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.rot","title":"rot property","text":"

    Rotation that aligns the model to the measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.shift","title":"shift property","text":"

    Shift that aligns the model to the measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.transformed_residuals","title":"transformed_residuals cached property","text":"

    Get residuals between transformed model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.gen_panels","title":"gen_panels(mirror, measurements, corners, adjusters, compensate=0.0, adjuster_radius=50.0)","text":"

    Use a set of measurements to generate panel objects.

    Parameters:

    Name Type Description Default mirror str

    The mirror these panels belong to. Should be 'primary' or 'secondary'.

    required measurements dict[str, NDArray[float32]]

    The photogrammetry data. Dict is data indexed by the target names.

    required corners dict[tuple[int, int], ndarray[float32]]

    The corners. This is indexed by a (row, col) tuple. Each entry is (4, 3) array where each row is a corner.

    required adjusters dict[tuple[int, int], NDArray[float32]]

    Nominal adjuster locations. This is indexed by a (row, col) tuple. Each entry is (5, 3) array where each row is an adjuster.

    required compensate float

    Amount (in mm) to compensate the model surface by. This is to account for things like the faro SMR.

    0.0 adjuster_radius float

    The radius in XY of points that an adjuster should use to compute a secondary correction on its position. Should be in mm.

    50.0

    Returns:

    Name Type Description panels list[Panels]

    A list of panels with the transforme initialized to the identity.

    Source code in lat_alignment/mirror.py
    def gen_panels(\n    mirror: str,\n    measurements: dict[str, NDArray[np.float32]],\n    corners: dict[tuple[int, int], NDArray[np.float32]],\n    adjusters: dict[tuple[int, int], NDArray[np.float32]],\n    compensate: float = 0.0,\n    adjuster_radius: float = 50.0,\n) -> list[Panel]:\n    \"\"\"\n    Use a set of measurements to generate panel objects.\n\n    Parameters\n    ----------\n    mirror : str\n        The mirror these panels belong to.\n        Should be 'primary' or 'secondary'.\n    measurements : dict[str, NDArray[np.float32]]\n        The photogrammetry data.\n        Dict is data indexed by the target names.\n    corners : dict[tuple[int, int], ndarray[np.float32]]\n        The corners. This is indexed by a (row, col) tuple.\n        Each entry is `(4, 3)` array where each row is a corner.\n    adjusters : dict[tuple[int, int], NDArray[np.float32]]\n        Nominal adjuster locations.\n        This is indexed by a (row, col) tuple.\n        Each entry is `(5, 3)` array where each row is an adjuster.\n    compensate : float, default: 0.0\n        Amount (in mm) to compensate the model surface by.\n        This is to account for things like the faro SMR.\n    adjuster_radius : float, default: 50.0\n        The radius in XY of points that an adjuster should use to\n        compute a secondary correction on its position.\n        Should be in mm.\n\n    Returns\n    -------\n    panels : list[Panels]\n        A list of panels with the transforme initialized to the identity.\n    \"\"\"\n    points = defaultdict(list)\n    # dumb brute force\n    corr = np.arange(4, dtype=int)\n    for _, point in measurements.items():\n        for rc, crns in corners.items():\n            x = crns[:, 0] > point[0]\n            y = crns[:, 1] > point[1]\n            val = x.astype(int) + 2 * y.astype(int)\n            if np.array_equal(np.sort(val), corr):\n                points[rc] += [point]\n                break\n\n    # Now init the objects\n    panels = []\n    for (row, col), meas in points.items():\n        meas = np.vstack(meas, dtype=np.float32)\n        panel = Panel(\n            mirror,\n            row,\n            col,\n            corners[(row, col)],\n            meas,\n            adjusters[(row, col)],\n            compensate,\n            adjuster_radius,\n        )\n        panels += [panel]\n    return panels\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.mirror_norm","title":"mirror_norm(x, y, a)","text":"

    Analytic form of the vector normal to the mirror surface.

    Parameters:

    Name Type Description Default x NDArray[float32]

    X positions to calculate at in mm.

    required y NDArray[float32]

    Y positions to calculate at in mm. Should have the same shape as x.

    required a NDArray[float32]

    Coeffecients of the mirror function. Use a_primary for the primary mirror. Use a_secondary for the secondary mirror.

    required

    Returns:

    Name Type Description normals NDArray[float32]

    Unit vector normal to the mirror surface at each input coordinate. Has shape shape(x) + (3,).

    Source code in lat_alignment/mirror.py
    def mirror_norm(\n    x: NDArray[np.float32], y: NDArray[np.float32], a: NDArray[np.float32]\n) -> NDArray[np.float32]:\n    \"\"\"\n    Analytic form of the vector normal to the mirror surface.\n\n    Parameters\n    ----------\n    x : NDArray[np.float32]\n        X positions to calculate at in mm.\n    y : NDArray[np.float32]\n        Y positions to calculate at in mm.\n        Should have the same shape as `x`.\n    a : NDArray[np.float32]\n        Coeffecients of the mirror function.\n        Use `a_primary` for the primary mirror.\n        Use `a_secondary` for the secondary mirror.\n\n    Returns\n    -------\n    normals : NDArray[np.float32]\n        Unit vector normal to the mirror surface at each input coordinate.\n        Has shape `shape(x) + (3,)`.\n    \"\"\"\n    Rn = 3000.0\n\n    x_n = np.zeros_like(x)\n    y_n = np.zeros_like(y)\n    for i in range(a.shape[0]):\n        for j in range(a.shape[1]):\n            if i != 0:\n                x_n += a[i, j] * (x ** (i - 1)) / (Rn**i) * (y / Rn) ** j\n            if j != 0:\n                y_n += a[i, j] * (x / Rn) ** i * (y ** (j - 1)) / (Rn**j)\n\n    z_n = -1 * np.ones_like(x_n)\n    normals = np.array((x_n, y_n, z_n)).T\n    normals /= np.linalg.norm(normals, axis=-1)[:, np.newaxis]\n    return normals\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.mirror_surface","title":"mirror_surface(x, y, a)","text":"

    Analytic form of the mirror surface.

    Parameters:

    Name Type Description Default x NDArray[float32]

    X positions to calculate at in mm.

    required y NDArray[float32]

    Y positions to calculate at in mm. Should have the same shape as x.

    required a NDArray[float32]

    Coeffecients of the mirror function. Use a_primary for the primary mirror. Use a_secondary for the secondary mirror.

    required

    Returns:

    Name Type Description z NDArray[float32]

    Z position of the mirror at each input coordinate. Has the same shape as x.

    Source code in lat_alignment/mirror.py
    def mirror_surface(\n    x: NDArray[np.float32], y: NDArray[np.float32], a: NDArray[np.float32]\n) -> NDArray[np.float32]:\n    \"\"\"\n    Analytic form of the mirror surface.\n\n    Parameters\n    ----------\n    x : NDArray[np.float32]\n        X positions to calculate at in mm.\n    y : NDArray[np.float32]\n        Y positions to calculate at in mm.\n        Should have the same shape as `x`.\n    a : NDArray[np.float32]\n        Coeffecients of the mirror function.\n        Use `a_primary` for the primary mirror.\n        Use `a_secondary` for the secondary mirror.\n\n    Returns\n    -------\n    z : NDArray[np.float32]\n        Z position of the mirror at each input coordinate.\n        Has the same shape as `x`.\n    \"\"\"\n    z = np.zeros_like(x)\n    Rn = 3000.0\n    for i in range(a.shape[0]):\n        for j in range(a.shape[1]):\n            z += a[i, j] * (x / Rn) ** i * (y / Rn) ** j\n    return z\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.plot_panels","title":"plot_panels(panels, title_str, vmax=None)","text":"

    Make a plot containing panel residuals and histogram. TODO: Correlation?

    Parameters:

    Name Type Description Default panels list[Panel]

    The panels to plot.

    required title_str str

    The title string, rms will me appended.

    required vmax Optional[float]

    The max of the colorbar. vmin will be -1 times this. Set to None to compute automatically. Should be in um.

    None

    Returns:

    Name Type Description figure Figure

    The figure with panels plotted on it.

    Source code in lat_alignment/mirror.py
    def plot_panels(\n    panels: list[Panel], title_str: str, vmax: Optional[float] = None\n) -> Figure:\n    \"\"\"\n    Make a plot containing panel residuals and histogram.\n    TODO: Correlation?\n\n    Parameters\n    ----------\n    panels : list[Panel]\n        The panels to plot.\n    title_str : str\n        The title string, rms will me appended.\n    vmax : Optional[float], default: None\n        The max of the colorbar. vmin will be -1 times this.\n        Set to None to compute automatically.\n        Should be in um.\n\n    Returns\n    -------\n    figure : Figure\n        The figure with panels plotted on it.\n    \"\"\"\n    res_all = np.vstack([panel.residuals for panel in panels]) * 1000\n    model_all = np.vstack([panel.model for panel in panels])\n    if vmax is None:\n        vmax = np.max(np.abs(res_all[:, 2]))\n    if vmax is None:\n        raise ValueError(\"vmax still None?\")\n    gs = gridspec.GridSpec(2, 2, width_ratios=[20, 1], height_ratios=[2, 1])\n    fig = plt.figure()\n    ax0 = plt.subplot(gs[0])\n    cax = plt.subplot(gs[1])\n    ax1 = plt.subplot(gs[2:])\n    cb = None\n    for panel in panels:\n        ax0.tricontourf(\n            panel.model[:, 0],\n            panel.model[:, 1],\n            panel.residuals[:, 2] * 1000,\n            vmin=-1 * vmax,\n            vmax=vmax,\n            cmap=\"coolwarm\",\n            alpha=0.6,\n        )\n        cb = ax0.scatter(\n            panel.model[:, 0],\n            panel.model[:, 1],\n            s=40,\n            c=panel.residuals[:, 2] * 1000,\n            vmin=-1 * vmax,\n            vmax=vmax,\n            cmap=\"coolwarm\",\n            marker=\"o\",\n            alpha=0.9,\n            linewidth=2,\n            edgecolor=\"black\",\n        )\n        ax0.scatter(\n            panel.meas_adj[:, 0],\n            panel.meas_adj[:, 1],\n            marker=\"x\",\n            linewidth=1,\n            color=\"black\",\n        )\n    ax0.tricontourf(\n        model_all[:, 0],\n        model_all[:, 1],\n        res_all[:, 2],\n        vmin=-1 * vmax,\n        vmax=vmax,\n        cmap=\"coolwarm\",\n        alpha=0.2,\n    )\n    ax0.set_xlabel(\"x (mm)\")\n    ax0.set_ylabel(\"y (mm)\")\n    ax0.set_xlim(-3300, 3300)  # ack hardcoded!\n    ax0.set_ylim(-3300, 3300)\n    if cb is not None:\n        fig.colorbar(cb, cax)\n    ax0.set_aspect(\"equal\")\n    for panel in panels:\n        ax0.add_patch(\n            Polygon(panel.corners[[0, 1, 3, 2], :2], fill=False, color=\"black\")\n        )\n\n    ax1.hist(res_all[:, 2], bins=len(panels))\n    ax1.set_xlabel(\"z residual (um)\")\n\n    points = np.array([len(panel.measurements) for panel in panels])\n    rms = np.array([panel.rms for panel in panels])\n    tot_rms = 1000 * np.sum(rms * points) / np.sum(points)\n    fig.suptitle(f\"{title_str}, RMS={tot_rms:.2f} um\")\n\n    plt.show()\n\n    return fig\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.remove_cm","title":"remove_cm(meas, mirror, compensate=0, thresh=10, cut_thresh=50, niters=10, verbose=False)","text":"

    Fit for the common mode transformation from the model to the measurements of all panels and them remove it. Note that we only remove the shift component of the common mode, rotations are ignored.

    Parameters:

    Name Type Description Default meas dict[str, NDArray[float32]]

    The photogrammetry data. Dict is data indexed by the target names.

    required mirror str

    The mirror this data belong to. Should be 'primary' or 'secondary'.

    required compensate float

    Compensation to apply to model. This is to account for the radius of a Faro SMR.

    0 thresh float

    How many times higher than the median residual a point needs to have to be considered an outlier.

    10 niters int

    How many iterations of common mode fitting to do.

    10 verbose bool

    If True print the transformation for each iteration.

    False

    Returns:

    Name Type Description kept_panels list[Panel]

    The panels that were successfully fit.

    Source code in lat_alignment/mirror.py
    def remove_cm(\n    meas,\n    mirror,\n    compensate: float = 0,\n    thresh: float = 10,\n    cut_thresh: float = 50,\n    niters: int = 10,\n    verbose=False,\n) -> dict[str, NDArray[np.float32]]:\n    \"\"\"\n    Fit for the common mode transformation from the model to the measurements of all panels and them remove it.\n    Note that we only remove the shift component of the common mode, rotations are ignored.\n\n    Parameters\n    ----------\n    meas : dict[str, NDArray[np.float32]]\n        The photogrammetry data.\n        Dict is data indexed by the target names.\n    mirror : str\n        The mirror this data belong to.\n        Should be 'primary' or 'secondary'.\n    compensate : float, default: 0\n        Compensation to apply to model.\n        This is to account for the radius of a Faro SMR.\n    thresh : float, default: 10\n        How many times higher than the median residual a point needs to have to be\n        considered an outlier.\n    niters : int, default: 10\n        How many iterations of common mode fitting to do.\n    verbose : bool, default: False\n        If True print the transformation for each iteration.\n\n    Returns\n    -------\n    kept_panels : list[Panel]\n        The panels that were successfully fit.\n    \"\"\"\n\n    def _cm(x, panel):\n        panel.measurements[:] -= x[1:4]\n        rot = Rotation.from_euler(\"xyz\", x[4:])\n        panel.measurements = rot.apply(panel.measurements)\n        panel.measurements *= x[0]\n\n    def _opt(x, panel):\n        p2 = deepcopy(panel)\n        _cm(x, p2)\n        return p2.rms\n\n    # make a fake panel for the full mirror\n    corners = np.array(\n        ([-3300, -3300, 0], [-3300, 3300, 0], [3300, 3300, 0], [3300, -3300, 0])\n    )  # ack hardcoded\n    labels = np.array(list(meas.keys()))\n    data = np.array(list(meas.values()))\n    corr = np.arange(4, dtype=int)\n    x = np.vstack([corners[:, 0] > dat[0] for dat in data])\n    y = np.vstack([corners[:, 1] > dat[1] for dat in data])\n    val = x.astype(int) + 2 * y.astype(int)\n    val = np.sort(val, axis=-1)\n    msk = (val == corr).all(-1)\n    data = data[msk]\n    labels = labels[msk]\n    panel = Panel(\n        mirror,\n        -1,\n        -1,\n        np.zeros((4, 3), \"float32\"),\n        data,\n        np.zeros((5, 3), \"float32\"),\n        compensate,\n    )\n    data = data.copy()\n    data_clean = data.copy()\n\n    x0 = np.hstack([np.ones(1), np.zeros(6)])\n    bounds = [(-0.95, 1.05)] + [(-100, 100)] * 3 + [(0, 2 * np.pi)] * 3\n\n    for i in range(niters):\n        if len(panel.measurements) < 3:\n            raise ValueError\n        print(f\"iter {i} for common mode fit\")\n        cut = panel.res_norm > thresh * np.median(panel.res_norm)\n        if np.sum(cut) > 0:\n            # print(f\"\\tRemoving {np.sum(cut)} points from mirror\")\n            panel.measurements = panel.measurements[~cut]\n            # labels = labels[~cut]\n            data = data[~cut]\n\n        if verbose:\n            print(f\"\\tRemoving a naive common mode shift of {panel.shift}\")\n        panel.measurements -= panel.shift\n        panel.measurements @= panel.rot.T\n\n        res = minimize(_opt, x0, (panel,), bounds=bounds)\n        if verbose:\n            print(\n                f\"\\tRemoving a fit common mode with scale {res.x[0]}, shift {res.x[1:4]}, and rotation {res.x[4:]}\"\n            )\n        _cm(res.x, panel)\n\n        if verbose:\n            print(\n                f\"\\tRemoving a secondary common mode shift of {panel.shift} and rotation of {decompose_rotation(panel.rot)}\"\n            )\n        panel.measurements -= panel.shift\n        panel.measurements @= panel.rot.T\n\n    aff, sft = get_affine(\n        data, panel.measurements, method=\"mean\", weights=np.ones(len(data))\n    )\n    scale, shear, rot = decompose_affine(aff)\n    rot = decompose_rotation(rot)\n    print(\n        f\"Full common mode is:\\n\\tshift = {sft} mm\\n\\tscale = {scale}\\n\\tshear = {shear}\\n\\trot = {np.rad2deg(rot)} deg\"\n    )\n\n    panel.measurements = apply_transform(data_clean, aff, sft)\n    cut = panel.res_norm > cut_thresh * np.median(panel.res_norm)\n    if np.sum(cut) > 0:\n        print(f\"Removing {np.sum(cut)} points from mirror\")\n        panel.measurements = panel.measurements[~cut]\n\n    return {l: d for l, d in zip(labels, panel.measurements)}\n
    "},{"location":"reference/transforms/","title":"transforms","text":"

    Functions for coordinate transforms.

    There are 6 relevant coordinate systems here, belonging to two sets of three. Each set is a global, a primary, and a secondary coordinate system; where primary and secondary are internal to those mirrors. The two sets of coordinates are the optical coordinates and the coordinates used by vertex. We denote these six coordinate systems as follows:

    - opt_global\n- opt_primary\n- opt_secondary\n- va_global\n- va_primary\n- va_secondary\n
    "},{"location":"reference/transforms/#lat_alignment.transforms.align_photo","title":"align_photo(labels, coords, *, mirror='primary', reference=None, max_dist=100.0)","text":"

    Align photogrammetry data and then put it into mirror coordinates.

    Parameters:

    Name Type Description Default labels NDArray[str_]

    The labels of each photogrammetry point. Should have shape (npoint,).

    required coords NDArray[float32]

    The coordinates of each photogrammetry point. Should have shape (npoint, 3).

    required mirror str

    The mirror that these points belong to. Should be either: 'primary' or 'secondary'.

    'primary' reference Optional[list[tuple[tuple[float, float, float], list[str]]]]

    List of reference points to use. Each point given should be a tuple with two elements. The first element is a tuple with the (x, y, z) coordinates of the point in the global coordinate system. The second is a list of nearby coded targets that can be used to identify the point. If None the default reference for each mirror is used.

    None max_dist float

    Max distance in mm that the reference poing can be from the target point used to locate it.

    100

    Returns:

    Name Type Description labels NDArray[str_]

    The labels of each photogrammetry point. Invar points are not included.

    coords_transformed NDArray[float32]

    The transformed coordinates. Invar points are not included.

    msk NDArray[bool_]

    Mask to removes invar points

    Source code in lat_alignment/transforms.py
    def align_photo(\n    labels: NDArray[np.str_],\n    coords: NDArray[np.float32],\n    *,\n    mirror: str = \"primary\",\n    reference: Optional[list[tuple[tuple[float, float, float], list[str]]]] = None,\n    max_dist: float = 100.0,\n) -> tuple[NDArray[np.str_], NDArray[np.float32], NDArray[np.bool_]]:\n    \"\"\"\n    Align photogrammetry data and then put it into mirror coordinates.\n\n    Parameters\n    ----------\n    labels : NDArray[np.str_]\n        The labels of each photogrammetry point.\n        Should have shape `(npoint,)`.\n    coords : NDArray[np.float32]\n        The coordinates of each photogrammetry point.\n        Should have shape `(npoint, 3)`.\n    mirror : str, default: 'primary'\n        The mirror that these points belong to.\n        Should be either: 'primary' or 'secondary'.\n    reference : Optional[list[tuple[tuple[float, float, float], list[str]]]], default: None\n        List of reference points to use.\n        Each point given should be a tuple with two elements.\n        The first element is a tuple with the (x, y, z) coordinates\n        of the point in the global coordinate system.\n        The second is a list of nearby coded targets that can be used\n        to identify the point.\n        If `None` the default reference for each mirror is used.\n    max_dist : float, default: 100\n        Max distance in mm that the reference poing can be from the target\n        point used to locate it.\n\n    Returns\n    -------\n    labels : NDArray[np.str_]\n        The labels of each photogrammetry point.\n        Invar points are not included.\n    coords_transformed : NDArray[np.float32]\n        The transformed coordinates.\n        Invar points are not included.\n    msk : NDArray[np.bool_]\n        Mask to removes invar points\n    \"\"\"\n    if mirror not in [\"primary\", \"secondary\"]:\n        raise ValueError(f\"Invalid mirror: {mirror}\")\n    if mirror == \"primary\":\n        transform = partial(coord_transform, cfrom=\"va_global\", cto=\"opt_primary\")\n    else:\n        transform = partial(coord_transform, cfrom=\"va_global\", cto=\"opt_secondary\")\n    if reference is None:\n        reference = DEFAULT_REF[mirror]\n    if reference is None or len(reference) == 0:\n        raise ValueError(\"Invalid or empty reference\")\n\n    # Lets find the points we can use\n    trg_idx = np.where(np.char.find(labels, \"TARGET\") >= 0)[0]\n    ref = []\n    pts = []\n    invars = []\n    for rpoint, codes in reference:\n        have = np.isin(codes, labels)\n        if np.sum(have) == 0:\n            continue\n        coded = coords[np.where(labels == codes[np.where(have)[0][0]])[0][0]]\n        print(codes[np.where(have)[0][0]])\n        # Find the closest point\n        dist = np.linalg.norm(coords[trg_idx] - coded, axis=-1)\n        if np.min(dist) > max_dist:\n            continue\n        print(np.min(dist))\n        ref += [rpoint]\n        pts += [coords[trg_idx][np.argmin(dist)]]\n        invars += [labels[trg_idx][np.argmin(dist)]]\n    if len(ref) < 4:\n        raise ValueError(f\"Only {len(ref)} reference points found! Can't align!\")\n    msk = [0, 1, 3]\n    pts = np.vstack(pts)[msk]\n    ref = np.vstack(ref)[msk]\n    pts = np.vstack((pts, np.mean(pts, 0)))\n    ref = np.vstack((ref, np.mean(ref, 0)))\n    ref = transform(ref)\n    print(\"Reference points in mirror coords:\")\n    print(ref[:-1])\n    print(make_edm(ref) / make_edm(pts))\n    print(make_edm(ref) - make_edm(pts))\n    print(np.nanmedian(make_edm(ref) / make_edm(pts)))\n    pts *= np.nanmedian(make_edm(ref) / make_edm(pts))\n    print(make_edm(ref) / make_edm(pts))\n    print(make_edm(ref) - make_edm(pts))\n    print(np.nanmedian(make_edm(ref) / make_edm(pts)))\n\n    rot, sft = get_rigid(pts, ref, method=\"mean\")\n    pts_t = apply_transform(pts, rot, sft)\n    import matplotlib.pyplot as plt\n\n    plt.scatter(pts_t[:, 0], pts_t[:, 1], color=\"b\")\n    plt.scatter(ref[:, 0], ref[:, 1], color=\"r\")\n    plt.show()\n    print(pts_t[:-1])\n    print(pts_t - ref)\n    print(\n        f\"RMS of reference points after alignment: {np.sqrt(np.mean((pts_t - ref)**2))}\"\n    )\n    coords_transformed = apply_transform(coords, rot, sft)\n\n    msk = ~np.isin(labels, invars)\n\n    return labels[msk], coords_transformed[msk], msk\n
    "},{"location":"reference/transforms/#lat_alignment.transforms.coord_transform","title":"coord_transform(coords, cfrom, cto)","text":"

    Transform between the six defined mirror coordinates:

    - opt_global\n- opt_primary\n- opt_secondary\n- va_global\n- va_primary\n- va_secondary\n

    Parameters:

    Name Type Description Default coords NDArray[float32]

    Coordinates to transform. Should be a (npoint, 3) array.

    required cfrom str

    The coordinate system that coords is currently in.

    required cto str

    The coordinate system to put coords into.

    required

    Returns:

    Name Type Description coords_transformed NDArray[float32]

    coords transformed into cto.

    Source code in lat_alignment/transforms.py
    def coord_transform(\n    coords: NDArray[np.float32], cfrom: str, cto: str\n) -> NDArray[np.float32]:\n    \"\"\"\n    Transform between the six defined mirror coordinates:\n\n        - opt_global\n        - opt_primary\n        - opt_secondary\n        - va_global\n        - va_primary\n        - va_secondary\n\n    Parameters\n    ----------\n    coords : NDArray[np.float32]\n        Coordinates to transform.\n        Should be a `(npoint, 3)` array.\n    cfrom : str\n        The coordinate system that `coords` is currently in.\n    cto : str\n        The coordinate system to put `coords` into.\n\n    Returns\n    -------\n    coords_transformed : NDArray[np.float32]\n        `coords` transformed into `cto`.\n    \"\"\"\n    if cfrom == cto:\n        return coords\n    match f\"{cfrom}-{cto}\":\n        case \"opt_global-opt_primary\":\n            return _opt_global_to_opt_primary(coords)\n        case \"opt_global-opt_secondary\":\n            return _opt_global_to_opt_secondary(coords)\n        case \"opt_primary-opt_global\":\n            return _opt_primary_to_opt_global(coords)\n        case \"opt_secondary-opt_global\":\n            return _opt_secondary_to_opt_global(coords)\n        case \"opt_primary-opt_secondary\":\n            return _opt_primary_to_opt_secondary(coords)\n        case \"opt_secondary-opt_primary\":\n            return _opt_secondary_to_opt_primary(coords)\n        case \"va_global-va_primary\":\n            return _va_global_to_va_primary(coords)\n        case \"va_global-va_secondary\":\n            return _va_global_to_va_secondary(coords)\n        case \"va_primary-va_global\":\n            return _va_primary_to_va_global(coords)\n        case \"va_secondary-va_global\":\n            return _va_secondary_to_va_global(coords)\n        case \"va_primary-va_secondary\":\n            return _va_primary_to_va_secondary(coords)\n        case \"va_secondary-va_primary\":\n            return _va_secondary_to_va_primary(coords)\n        case \"opt_global-va_global\":\n            return _opt_global_to_va_global(coords)\n        case \"opt_global-va_primary\":\n            return _opt_global_to_va_primary(coords)\n        case \"opt_global-va_secondary\":\n            return _opt_global_to_va_secondary(coords)\n        case \"opt_primary-va_global\":\n            return _opt_primary_to_va_global(coords)\n        case \"opt_primary-va_primary\":\n            return _opt_primary_to_va_primary(coords)\n        case \"opt_primary-va_secondary\":\n            return _opt_primary_to_va_secondary(coords)\n        case \"opt_secondary-va_global\":\n            return _opt_secondary_to_va_global(coords)\n        case \"opt_secondary-va_primary\":\n            return _opt_secondary_to_va_primary(coords)\n        case \"opt_secondary-va_secondary\":\n            return _opt_secondary_to_va_secondary(coords)\n        case \"va_global-opt_global\":\n            return _va_global_to_opt_global(coords)\n        case \"va_global-opt_primary\":\n            return _va_global_to_opt_primary(coords)\n        case \"va_global-opt_secondary\":\n            return _va_global_to_opt_secondary(coords)\n        case \"va_primary-opt_global\":\n            return _va_primary_to_opt_global(coords)\n        case \"va_primary-opt_primary\":\n            return _va_primary_to_opt_primary(coords)\n        case \"va_primary-opt_secondary\":\n            return _va_primary_to_opt_secondary(coords)\n        case \"va_secondary-opt_global\":\n            return _va_secondary_to_opt_global(coords)\n        case \"va_secondary-opt_primary\":\n            return _va_secondary_to_opt_primary(coords)\n        case \"va_secondary-opt_secondary\":\n            return _va_secondary_to_opt_secondary(coords)\n        case _:\n            raise ValueError(\"Invalid coordinate system provided!\")\n
    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"LAT Alignment","text":"

    Tools for LAT mirror alignment

    "},{"location":"#installation","title":"Installation","text":"

    Technically after cloning this repository you can just run python lat_alignment/alignment.py PATH/TO/CONFIG, but it is recommended that you install this as a package instead.

    To do this just run: pip install -e . from the root of this repository.

    This has two main benefits over running the script directly: 1. It will handle dependencies for you. 2. This sets up an entrypoint called lat_alignment so that you can call the code from anywhere. This is nice because now you can call the code from the measurement directory where you are most likely editing files, saving you the hassle of having to cd or wrangle long file paths.

    "},{"location":"#usage","title":"Usage","text":"
    1. Create the appropriate directory structure for your measurement (see File Structure for details).
    2. Place the measurement files in the appropriate place in your created directory (see Measurement Files for details).
    3. Create a file with any information about the measurement that could prove useful (see Description File for details).
    4. Create a config file for your measurement (see Config File for details).
    5. Run the alignment script with lat_alignment /PATH/TO/CONFIG
    6. Follow the instructions in the output to align panels. This output will both be printed in the terminal and written to an output file (see Output File)
    "},{"location":"#file-structure","title":"File Structure","text":"

    Measurements should be organized in the following file structure

    measurements\n|\n\u2514\u2500\u2500\u2500YYYYMMDD_num\n|   |config.txt\n|   |description.txt\n|   |output.txt\n|   |adjusters.yaml\n|   |\n|   \u2514\u2500\u2500\u2500M1\n|   |   |XX-XXXXXX.txt\n|   |   |XX-XXXXXX.txt\n|   |   |...\n|   |\n|   \u2514\u2500\u2500\u2500M2\n|   |   |XX-XXXXXX.txt\n|   |   |XX-XXXXXX.txt\n|   |   |...\n|   |\n|   \u2514\u2500\u2500\u2500plots\n|       \u2514\u2500\u2500\u2500M1\n|       |   |XX-XXXXXX_surface.png\n|       |   |XX-XXXXXX_hist.png\n|       |   |XX-XXXXXX_ps.png\n|       |   |...\n|       |\n|       \u2514\u2500\u2500\u2500M2\n|           |XX-XXXXXX_surface.png\n|           |XX-XXXXXX_hist.png\n|           |XX-XXXXXX_ps.png\n|           |...\n|       \n\u2514\u2500\u2500\u2500YYYYMMDD_num\n|   |config.txt\n|   |description.txt\n|   |adjusters.yaml\n|   |\n|   \u2514\u2500\u2500\u2500M1\n|   |   |XX-XXXXXX.txt\n|   |   |XX-XXXXXX.txt\n|   |   |...\n|   |\n|   \u2514\u2500\u2500\u2500M2\n|       |XX-XXXXXX.txt\n|       |XX-XXXXXX.txt\n|       |...\n|   |\n|   \u2514\u2500\u2500\u2500plots\n|       \u2514\u2500\u2500\u2500M1\n|       |   |XX-XXXXXX_surface.png\n|       |   |XX-XXXXXX_hist.png\n|       |   |XX-XXXXXX_ps.png\n|       |   |...\n|       |\n|       \u2514\u2500\u2500\u2500M2\n|           |XX-XXXXXX_surface.png\n|           |XX-XXXXXX_hist.png\n|           |XX-XXXXXX_ps.png\n|           |...\n|...\n
    "},{"location":"#measurement-directories","title":"Measurement Directories","text":"

    Each directory YYYYMMDD_num refers to a specific measurement session. Where YYYYMMDD refers to the date of the measurement and num refers to which number measurement on that date it was. For example the second measurement taken on January 1st, 2022 would be 20220101_02.

    This is the file path that should be provided to alignment.py as the measurement_dir argument.

    "},{"location":"#config-file","title":"Config File","text":"

    The file config.yaml contains configuration options. Below is an annotated example with all possible options.

    # The measurement directory\n# If not provided the dirctory containing the config will be used\nmeasurement_dir: PATH/TO/MEASUREMENT\n\n# The path the the dirctory containing the cannonical adjuster locations\n# If not provided the can_points directory in the root of this repository is used\ncannonical_points: PATH/TO/CAN/POINTS\n\n# Coordinate system of measurements\n# Possible vaules are [\"cad\", \"global\", \"primary\", \"secondary\"]\ncoordinates: cad # default value\n\n# Amount to shift the origin of the measurements by\n# Should be a 3 element list\norigin_shift: [0, 0, 0] # default value\n\n# FARO compensation\ncompensation: 0.0 # default value\n\n# Set to True to apply common mode subtraction\ncm_sub: False # default value\n\n# Set to True to make plots if panels \nplots: False # default value\n\n# Where to save log\n# If not provided log is saved to a file called output.txt\n# in the measurement_dir for this measurement\nlog_file: null # Set to null to only print output and not save\n\n# Path to a yaml file with the current adjuster positions\n# If null (None) then all adjusters are assumed to be at 0\n# You probably want to point this to the file generated\n# in the previous alignment run if you have it\nadj_path: null # default value\n\n# Path to where to store the adjuster postions after aligning\n# If null (None) will store in a file called adjusters.yaml\n# in the measurement_dir for this measurement\nadj_out: null # default value\n\n# Defines the allowed adjuster range in mm\nadj_low: -1 # default value\nadj_high: 1 # default value\n

    If you are using all default values make a blank config with touch config.yaml

    "},{"location":"#description-file","title":"Description File","text":"

    Each measurement directory should contain a file description.txt with information on the measurement. Any information that could provide useful context when looking at the measurement/alignment after the fact should be included here (ie: who performed the measurement, where the measurement was taken, etc.).

    "},{"location":"#output-file","title":"Output File","text":"

    Output generated by alignment.py. By default this is saved at measurement_dir/output.txt

    Note that this file gets overwritten when lat_alignment is run, so if you want to store multiple copies with different configs or something rename them or change the log_file in the config.

    "},{"location":"#adjuster-positions","title":"Adjuster Positions","text":"

    Positions of adjusters after applying the calculated adjustments. This is a yaml file nominally saved at measurement_dir/adjusters/yaml

    Each element in the file is in the format:

    PANEL_NUMBER: [X, Y, ADJ_1, ADJ_2, ADJ_3, ADJ_4, ADJ_5] \n
    "},{"location":"#mirror-directories","title":"Mirror Directories","text":"

    Directories containing the measurements files within each root measurement directory. M1 contains the measurements for the primary mirror and M2 contains the measurements for the secondary mirror. If you don't have measurements for one of the mirrors you do not need to create an empty directory for it.

    "},{"location":"#measurement-files","title":"Measurement Files","text":"

    Files containing the point cloud measurements for a given panel. Should live in the mirror directory that the panel belongs to. Files should be named XX-XXXXXX.txt where XX-XXXXXX is the panel number. The numbering system is as follows: * First four digits (XX-XX) are the telescope number. For the LAT this is 01-01 * Fifth digit is the mirror number. This is 1 for the primary and 2 for the secondary. * Sixth digit is the panel row * Seventh digit is the panel column * Eight digit is the panel number (current, spare, replacement, etc.)

    "},{"location":"#plot-directory","title":"Plot Directory","text":"

    If the plots option is set to True then the root measurement will contain a directory called plots. Within this directory will be directories for each mirror measured, M1 for the primary and M2 for the secondary. Each of these will contain three plots per panel measured: * XX-XXXXXX_surface.png, a plot of the panel's surface in the mirror's coordinate system. * XX-XXXXXX_hist.png, a histogram of the residuals from the panel's fit. * XX-XXXXXX_ps.png, a plot of the power spectrum of the residuals from the panel's fit.

    Where XX-XXXXXX is the panel number.

    "},{"location":"#coordinate-systems","title":"Coordinate Systems","text":"

    The relevant coordinate systems are marked in the diagram below:

    Where the orange circle marks the global coordinate system, the green circle marks the primary coordinate system, and the blue circle marks the secondary coordinate system.

    Additionally there is a cad coordinate system that is defined as the coordinate system from the SolidWorks model. It is given by the following transformation from the global coordinate system:

    x -> y - 200 mm\ny -> x\nz -> -z\n

    It is currently unclear why the 200 mm offset exists.

    Note that the files in the can_points directory are in the cad coordinate system.

    All measurements should be done in one of these four coordinate systems modulo a known shift in the origin.

    "},{"location":"#bugs-and-feature-requests","title":"Bugs and Feature Requests","text":"

    For low priority bugs and feature requests submit an issue on the git repo.

    For higher priority issues (or questions that require an expedient answer) email, Slack, or call me.

    "},{"location":"#contributing","title":"Contributing","text":"

    If you wish to contribute to this repository (either code or adding measurement files) contact me via email or Slack.

    If you are contributing code please do so by creating a branch and submitting a pull request. Try to keep things as close to PEP8 as possible.

    "},{"location":"reference/SUMMARY/","title":"SUMMARY","text":"
    • adjustments
    • alignment
    • fitting
    • io
    • mirror
    • transforms
    "},{"location":"reference/adjustments/","title":"adjustments","text":"

    Calculate adjustments needed to align LAT mirror panel

    Author: Saianeesh Keshav Haridas

    "},{"location":"reference/adjustments/#lat_alignment.adjustments.adjustment_fit_func","title":"adjustment_fit_func(pars, can_points, points, adjustors)","text":"

    Function to minimize when calculating adjustments.

    Parameters:

    Name Type Description Default pars NDArray[float32]

    The parameters to fit for:

    • dx: Translation in x
    • dy: Translation in y
    • dz: Translation in z
    • thetha_0: Angle to rotate about first adjustor axis
    • thetha_1: Angle to rotate about second adjustor axis
    • z_t: Additional translation to tension the center point
    required can_points NDArray[float32]

    The cannonical positions of the points to align.

    required points NDArray[float32]

    The measured positions of the points to align.

    required adjustors NDArray[float32]

    The measured positions of the adjustors.

    required

    Returns:

    Name Type Description norm float32

    The norm of \\(cannonical_positions - transformed_positions\\).

    Source code in lat_alignment/adjustments.py
    def adjustment_fit_func(\n    pars: NDArray[np.float32],\n    can_points: NDArray[np.float32],\n    points: NDArray[np.float32],\n    adjustors: NDArray[np.float32],\n) -> np.float32:\n    r\"\"\"\n    Function to minimize when calculating adjustments.\n\n    Parameters\n    ----------\n    pars : NDArray[np.float32]\n        The parameters to fit for:\n\n        * dx: Translation in x\n        * dy: Translation in y\n        * dz: Translation in z\n        * thetha_0: Angle to rotate about first adjustor axis\n        * thetha_1: Angle to rotate about second adjustor axis\n        * z_t: Additional translation to tension the center point\n    can_points : NDArray[np.float32]\n        The cannonical positions of the points to align.\n    points : NDArray[np.float32]\n        The measured positions of the points to align.\n    adjustors : NDArray[np.float32]\n        The measured positions of the adjustors.\n\n    Returns\n    -------\n    norm : np.float32\n        The norm of $cannonical_positions - transformed_positions$.\n    \"\"\"\n    dx, dy, dz, thetha_0, thetha_1, z_t = pars\n    points, adjustors = translate_panel(points, adjustors, dx, dy, dz)\n    points, adjustors = rotate_panel(points, adjustors, thetha_0, thetha_1)\n    points[-1, -1] += z_t\n    return np.linalg.norm(can_points - points)\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.calc_adjustments","title":"calc_adjustments(can_points, points, adjustors, **kwargs)","text":"

    Calculate adjustments needed to align panel.

    Parameters:

    Name Type Description Default can_points NDArray[float32]

    The cannonical position of the points to align.

    required points NDArray[float32]

    The measured positions of the points to align.

    required adjustors NDArray[float32]

    The measured positions of the adjustors.

    required **kwargs

    Arguments to be passed to scipy.optimize.minimize.

    {} dx float32

    The required translation of panel in x.

    required dy float32

    The required translation of panel in y.

    required d_adj NDArray[float32]

    The amount to move each adjustor.

    required dx_err float32

    The error in the fit for dx.

    required dy_err float32

    The error in the fit for dy.

    required d_adj_err NDArray[float32]

    The error in the fit for d_adj.

    required Source code in lat_alignment/adjustments.py
    def calc_adjustments(\n    can_points: NDArray[np.float32],\n    points: NDArray[np.float32],\n    adjustors: NDArray[np.float32],\n    **kwargs,\n) -> Tuple[\n    np.float32,\n    np.float32,\n    NDArray[np.float32],\n    np.float32,\n    np.float32,\n    NDArray[np.float32],\n]:\n    \"\"\"\n    Calculate adjustments needed to align panel.\n\n    Parameters\n    ----------\n    can_points : NDArray[np.float32]\n        The cannonical position of the points to align.\n    points : NDArray[np.float32]\n        The measured positions of the points to align.\n    adjustors : NDArray[np.float32]\n        The measured positions of the adjustors.\n    **kwargs\n        Arguments to be passed to `scipy.optimize.minimize`.\n\n    dx : np.float32\n        The required translation of panel in x.\n    dy : np.float32\n        The required translation of panel in y.\n    d_adj : NDArray[np.float32]\n        The amount to move each adjustor.\n    dx_err : np.float32\n        The error in the fit for `dx`.\n    dy_err : np.float32\n        The error in the fit for `dy`.\n    d_adj_err : NDArray[np.float32]\n        The error in the fit for `d_adj`.\n    \"\"\"\n    res = opt.minimize(\n        adjustment_fit_func, np.zeros(6), (can_points, points, adjustors), **kwargs\n    )\n\n    dx, dy, dz, thetha_0, thetha_1, z_t = res.x\n    _points, _adjustors = translate_panel(points, adjustors, dx, dy, dz)\n    _points, _adjustors = rotate_panel(_points, _adjustors, thetha_0, thetha_1)\n    _adjustors[-1, -1] += z_t\n    d_adj = _adjustors - adjustors\n\n    ftol = 2.220446049250313e-09\n    if \"ftol\" in kwargs:\n        ftol = kwargs[\"ftol\"]\n    perr = np.sqrt(ftol * np.diag(res.hess_inv))\n    dx_err, dy_err, dz_err, thetha_0_err, thetha_1_err, z_t_err = perr\n    _points, _adjustors = translate_panel(points, adjustors, dx_err, dy_err, dz_err)\n    _points, _adjustors = rotate_panel(_points, _adjustors, thetha_0_err, thetha_1_err)\n    _adjustors[-1, -1] += z_t_err\n    d_adj_err = _adjustors - adjustors\n\n    return dx, dy, d_adj[:, 2], dx_err, dy_err, d_adj_err[:, 2]\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.rotate","title":"rotate(point, end_point1, end_point2, theta)","text":"

    Rotate a point about an axis

    Parameters:

    Name Type Description Default point NDArray[float32]

    The point to rotate

    required end_point1 NDArray[float32]

    A point on the axis of rotation

    required end_point2 NDArray[float32]

    Another point on the axis of rotation

    required theta float32

    Angle in radians to rotate by

    required

    Returns:

    Name Type Description point NDArray[float32]

    The rotated point

    Source code in lat_alignment/adjustments.py
    def rotate(\n    point: NDArray[np.float32],\n    end_point1: NDArray[np.float32],\n    end_point2: NDArray[np.float32],\n    theta: np.float32,\n) -> NDArray[np.float32]:\n    \"\"\"\n    Rotate a point about an axis\n\n    Parameters\n    ----------\n    point : NDArray[np.float32]\n        The point to rotate\n    end_point1 : NDArray[np.float32]\n        A point on the axis of rotation\n    end_point2 : NDArray[np.float32]\n        Another point on the axis of rotation\n    theta: NDArray[np.float32]\n        Angle in radians to rotate by\n\n    Returns\n    -------\n    point : NDArray[np.float32]\n        The rotated point\n    \"\"\"\n    origin = np.mean((end_point1, end_point2))\n    point_0 = point - origin\n    ax = end_point2 - end_point1\n    ax = rot.from_rotvec(theta * ax / np.linalg.norm(ax))\n    point_0 = ax.apply(point_0)\n    return point_0 + origin\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.rotate_panel","title":"rotate_panel(points, adjustors, thetha_0, thetha_1)","text":"

    Rotate panel about axes created by adjustors.

    Parameters:

    Name Type Description Default points NDArray[float32]

    Points on panel to rotate.

    required adjustors NDArray[float32]

    Adjustor positions.

    required thetha_0 float32

    Angle to rotate about first adjustor axis

    required thetha_1 np.float32.

    Angle to rotate about second adjustor axis

    required

    Returns:

    Name Type Description rot_points NDArray[float32]

    The rotated points.

    rot_adjustors NDArray[float32]

    The rotated adjustors.

    Source code in lat_alignment/adjustments.py
    def rotate_panel(\n    points: NDArray[np.float32],\n    adjustors: NDArray[np.float32],\n    thetha_0: np.float32,\n    thetha_1: np.float32,\n) -> Tuple[NDArray[np.float32], NDArray[np.float32]]:\n    \"\"\"\n    Rotate panel about axes created by adjustors.\n\n    Parameters\n    ----------\n    points : NDArray[np.float32]\n        Points on panel to rotate.\n    adjustors : NDArray[np.float32]\n        Adjustor positions.\n    thetha_0 : np.float32\n        Angle to rotate about first adjustor axis\n    thetha_1 : np.float32.\n        Angle to rotate about second adjustor axis\n\n    Returns\n    -------\n    rot_points : NDArray[np.float32]\n        The rotated points.\n    rot_adjustors : NDArray[np.float32]\n        The rotated adjustors.\n    \"\"\"\n    rot_points = np.zeros(points.shape, np.float32)\n    rot_adjustors = np.zeros(adjustors.shape, np.float32)\n\n    n_points = len(points)\n    n_adjustors = len(adjustors)\n\n    for i in range(n_points):\n        rot_points[i] = rotate(points[i], adjustors[1], adjustors[2], thetha_0)\n        rot_points[i] = rotate(rot_points[i], adjustors[0], adjustors[3], thetha_1)\n    for i in range(n_adjustors):\n        rot_adjustors[i] = rotate(adjustors[i], adjustors[1], adjustors[2], thetha_0)\n        rot_adjustors[i] = rotate(\n            rot_adjustors[i], adjustors[0], adjustors[3], thetha_1\n        )\n    return rot_points, rot_adjustors\n
    "},{"location":"reference/adjustments/#lat_alignment.adjustments.translate_panel","title":"translate_panel(points, adjustors, dx, dy, dz)","text":"

    Translate a panel.

    Parameters:

    Name Type Description Default points NDArray[float32]

    The points on panel to translate.

    required adjustors NDArray[float32]

    Adjustor positions.

    required dx float32

    Translation in x.

    required dy float32

    Translation in y.

    required dz float32

    Translation in z.

    required

    Returns:

    Name Type Description points NDArray[float32]

    The translated points.

    adjustors NDArray[float32]

    The translated adjustors.

    Source code in lat_alignment/adjustments.py
    def translate_panel(\n    points: NDArray[np.float32],\n    adjustors: NDArray[np.float32],\n    dx: np.float32,\n    dy: np.float32,\n    dz: np.float32,\n) -> Tuple[NDArray[np.float32], NDArray[np.float32]]:\n    \"\"\"\n    Translate a panel.\n\n    Parameters\n    ----------\n    points : NDArray[np.float32]\n        The points on panel to translate.\n    adjustors : NDArray[np.float32]\n        Adjustor positions.\n    dx : np.float32\n        Translation in x.\n    dy : np.float32\n        Translation in y.\n    dz : np.float32\n        Translation in z.\n\n    Returns\n    -------\n    points : NDArray[np.float32]\n        The translated points.\n    adjustors : NDArray[np.float32]\n        The translated adjustors.\n    \"\"\"\n    translation = np.array((dx, dy, dz))\n    return points + translation, adjustors + translation\n
    "},{"location":"reference/alignment/","title":"alignment","text":"

    Main driver script for running the alignment. You typically want to use the lat_alignment entrypoint rather than calling this directly.

    "},{"location":"reference/alignment/#lat_alignment.alignment.adjust_panel","title":"adjust_panel(panel, mnum, cfg)","text":"

    Helper function to get the adjustments for a single panel.

    Parameters:

    Name Type Description Default panel Panel

    The mirror panel to adjust.

    required mnum int

    The mirror number. 1 for the primary and 2 for the secondary.

    required cfg dict

    The configuration dictionairy.

    required

    Returns:

    Name Type Description adjustments NDArray[float32]

    The adjustments to make for the panel. This is a 17 element array with the following structure: [mnum, panel_row, panel_col, dx, dy, d_adj1, ..., d_adj5, dx_err, dy_err, d_adj1_err, ..., d_adj5_err].

    Source code in lat_alignment/alignment.py
    def adjust_panel(panel: mir.Panel, mnum: int, cfg: dict) -> NDArray[np.float32]:\n    \"\"\"\n    Helper function to get the adjustments for a single panel.\n\n    Parameters\n    ----------\n    panel : mir.Panel\n        The mirror panel to adjust.\n    mnum : int\n        The mirror number.\n        1 for the primary and 2 for the secondary.\n    cfg : dict\n        The configuration dictionairy.\n\n    Returns\n    -------\n    adjustments : NDArray[np.float32]\n        The adjustments to make for the panel.\n        This is a 17 element array with the following structure:\n        `[mnum, panel_row, panel_col, dx, dy, d_adj1, ..., d_adj5, dx_err, dy_err, d_adj1_err, ..., d_adj5_err]`.\n    \"\"\"\n    adjustments = np.zeros(17, np.float32)\n    adjustments[0] = mnum\n    adjustments[1] = panel.row\n    adjustments[2] = panel.col\n    meas_adj = panel.meas_adj.copy()\n    meas_adj[:, 2] += panel.meas_adj_resid\n    meas_surface = panel.meas_surface.copy()\n    meas_surface[:, 2] += panel.meas_adj_resid\n    dy, dy, d_adj, dx_err, dy_err, d_adj_err = adj.calc_adjustments(\n        panel.can_surface, meas_surface, meas_adj, **cfg.get(\"adjust\", {})\n    )\n    adjustments[3:] = np.array(\n        [dy, dy] + list(d_adj) + [dx_err, dy_err] + list(d_adj_err)\n    )\n\n    return adjustments\n
    "},{"location":"reference/fitting/","title":"fitting","text":"

    Functions for fitting against the mirror surface.

    "},{"location":"reference/fitting/#lat_alignment.fitting.mirror_fit","title":"mirror_fit(points, a, compensate=0, to_points=True, **kwargs)","text":"

    Fit points against the mirror surface. Ideally the points should be in the mirror's local coordinate system.

    Parameters:

    Name Type Description Default points NDArray[floating]

    Array of points to compare against the mirror. Should have shape (npoint, 3).

    required a NDArray[floating]

    Coeffecients of the mirror function. Use a_primary for the primary mirror and a_secondary for the secondary.

    required compensate float

    Amount to compensate the mirror surface by. This is useful to model things like the surface traced out by an SMR.

    0.0 to_points bool

    If True, the transform will be inverted to align the model to the points.

    True **kwargs

    Additional arguments to pass on to scipy.optimize.minimize.

    {}

    Returns:

    Name Type Description transform_pars NDArray[floating]

    Flattened affine transform and shift, has to be 1d for use with minimizers. Will have shape (12,) where the first 9 elements are the flattened affine transform, and the last 3 are the shift in (x, y, z) applied after the affine transform.

    rms float

    The RMS error between the transformed points and the model.

    Source code in lat_alignment/fitting.py
    def mirror_fit(\n    points: NDArray[np.floating],\n    a: NDArray[np.floating],\n    compensate: float = 0,\n    to_points: bool = True,\n    **kwargs,\n) -> tuple[NDArray[np.floating], float]:\n    \"\"\"\n    Fit points against the mirror surface.\n    Ideally the points should be in the mirror's local coordinate system.\n\n    Parameters\n    ----------\n    points : NDArray[np.floating]\n        Array of points to compare against the mirror.\n        Should have shape (npoint, 3).\n    a : NDArray[np.floating]\n        Coeffecients of the mirror function.\n        Use a_primary for the primary mirror and a_secondary for the secondary.\n    compensate : float, default: 0.0\n        Amount to compensate the mirror surface by.\n        This is useful to model things like the surface traced out by an SMR.\n    to_points : bool, default: True\n        If True, the transform will be inverted to align the model to the points.\n    **kwargs\n        Additional arguments to pass on to scipy.optimize.minimize.\n\n    Returns\n    -------\n    transform_pars : NDArray[np.floating]\n        Flattened affine transform and shift, has to be 1d for use with minimizers.\n        Will have shape (12,) where the first 9 elements are the flattened affine transform,\n        and the last 3 are the shift in (x, y, z) applied after the affine transform.\n    rms : float\n        The RMS error between the transformed points and the model.\n    \"\"\"\n\n    def _fit_func(transform_pars, points, a, compensate):\n        points_transformed = mirror_transform(transform_pars, points)\n        chisq = mirror_objective(points_transformed, a, compensate)\n        return chisq\n\n    x0 = np.concatenate((np.eye(3).ravel(), np.zeros(3)))\n    res = opt.minimize(_fit_func, x0, args=(points, a, compensate), **kwargs)\n\n    transform_pars = res.x\n    transformed = mirror_transform(transform_pars, points)\n    z = mr.mirror(transformed[:, 0], transformed[:, 1], a, compensate)\n    rms = np.sqrt(np.mean((z - transformed[:, 2]) ** 2))\n\n    if to_points:\n        aff = transform_pars[:9].reshape((3, 3))\n        sft = transform_pars[9:]\n        aff = np.linalg.inv(aff)\n        sft = (-1 * sft) @ aff\n        transform_pars = np.concatenate((aff.ravel(), sft))\n\n    return transform_pars, rms\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.mirror_objective","title":"mirror_objective(points, a, compensate=0)","text":"

    Objective function to minimize when fitting to mirror surface. Essentially just a curvature weighted chisq.

    Parameters:

    Name Type Description Default points NDArray[floating]

    Array of points to compare against the mirror. Should have shape (npoint, 3).

    required a NDArray[floating]

    Coeffecients of the mirror function. Use a_primary for the primary mirror and a_secondary for the secondary.

    required compensate float

    Amount to compensate the mirror surface by. This is useful to model things like the surface traced out by an SMR.

    0.0

    Returns:

    Name Type Description chisq float

    The value to minimize when fitting to.

    Source code in lat_alignment/fitting.py
    def mirror_objective(\n    points: NDArray[np.floating], a: NDArray[np.floating], compensate: float = 0\n) -> float:\n    \"\"\"\n    Objective function to minimize when fitting to mirror surface.\n    Essentially just a curvature weighted chisq.\n\n    Parameters\n    ----------\n    points : NDArray[np.floating]\n        Array of points to compare against the mirror.\n        Should have shape (npoint, 3).\n    a : NDArray[np.floating]\n        Coeffecients of the mirror function.\n        Use a_primary for the primary mirror and a_secondary for the secondary.\n    compensate : float, default: 0.0\n        Amount to compensate the mirror surface by.\n        This is useful to model things like the surface traced out by an SMR.\n\n    Returns\n    -------\n    chisq : float\n        The value to minimize when fitting to.\n    \"\"\"\n    surface = mr.mirror(points[:, 0], points[:, 1], a, compensate)\n    norm = mr.mirror_norm(points[:, 0], points[:, 1], a)\n    res = (points[:, 2] - surface) * (norm[2] ** 2)\n\n    return res @ res.T\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.mirror_transform","title":"mirror_transform(transform_pars, points)","text":"

    Function to apply an affine transform to the mirror. This is the transform we are fitting for.

    Parameters:

    Name Type Description Default transform_pars NDArray[floating]

    Flattened affine transform and shift, has to be 1d for use with minimizers. Should have shape (12,) where the first 9 elements are the flattened affine transform, and the last 3 are the shift in (x, y, z) applied after the affine transform.

    required points NDArray[floating]

    Array of points to compare against the mirror. Should have shape (npoint, 3).

    required

    Returns:

    Name Type Description points_transformed NDArray[floating]

    Array of transformed points. Will have shape (npoint, 3).

    Source code in lat_alignment/fitting.py
    def mirror_transform(\n    transform_pars: NDArray[np.floating], points: NDArray[np.floating]\n) -> NDArray[np.floating]:\n    \"\"\"\n    Function to apply an affine transform to the mirror.\n    This is the transform we are fitting for.\n\n    Parameters\n    ----------\n    transform_pars : NDArray[np.floating]\n        Flattened affine transform and shift, has to be 1d for use with minimizers.\n        Should have shape (12,) where the first 9 elements are the flattened affine transform,\n        and the last 3 are the shift in (x, y, z) applied after the affine transform.\n    points : NDArray[np.floating]\n        Array of points to compare against the mirror.\n        Should have shape (npoint, 3).\n\n    Returns\n    -------\n    points_transformed : NDArray[np.floating]\n        Array of transformed points.\n        Will have shape (npoint, 3).\n    \"\"\"\n    aff = transform_pars[:9].reshape((3, 3))\n    sft = transform_pars[9:]\n    return points @ aff + sft\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.res_auto_corr","title":"res_auto_corr(residuals)","text":"

    Compute auto correlation of residuals from fit.

    Parameters:

    Name Type Description Default residuals NDArray[floating]

    Residuals between measured point cloud and fit model.

    required

    Returns:

    Name Type Description ac NDArray[floating]

    Auto correlation, really just the deviations in mm at each distance scale.

    ac_dists NDArray[floating]

    Distance scale of each value in ac.

    Source code in lat_alignment/fitting.py
    def res_auto_corr(\n    residuals: NDArray[np.floating],\n) -> tuple[NDArray[np.floating], NDArray[np.floating]]:\n    \"\"\"\n    Compute auto correlation of residuals from fit.\n\n    Parameters\n    ----------\n    residuals : NDArray[np.floating]\n        Residuals between measured point cloud and fit model.\n\n    Returns\n    -------\n    ac : NDArray[np.floating]\n        Auto correlation, really just the deviations in mm at each distance scale.\n    ac_dists : NDArray[np.floating]\n        Distance scale of each value in ac.\n    \"\"\"\n    dists = np.zeros((len(residuals), len(residuals)))\n    res_diff = np.zeros((len(residuals), len(residuals)))\n\n    for i in range(len(residuals)):\n        res1 = residuals[i]\n        for j in range(i):\n            res2 = residuals[j]\n            dist = np.linalg.norm((res1[0] - res2[0], res1[1] - res2[1]))\n            dists[i, j] = dist\n            res_diff[i, j] = abs(res1[2] - res2[2])\n    tri_i = np.tril_indices(len(residuals), k=-1)\n    dists = dists[tri_i]\n    res_diff = res_diff[tri_i]\n    ac, bin_e, _ = binned_statistic(dists, res_diff, bins=100)\n    ac_dists = bin_e[:-1] + np.diff(bin_e) / 2.0\n\n    return ac, ac_dists\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.tension_fit","title":"tension_fit(residuals, **kwargs)","text":"

    Fit a power law model of tension to a point cloud of residuals.

    Parameters:

    Name Type Description Default residuals NDArray[floating]

    Residuals between measured point cloud and fit model.

    required **kwargs

    Arguments to be passed to scipy.optimize.minimize

    {}

    Returns:

    Name Type Description tension_pars NDArray[floating]

    The fit parameters, see docstring of tension_model for details.

    rms float

    The rms between the input residuals and the fit model.

    Source code in lat_alignment/fitting.py
    def tension_fit(\n    residuals: NDArray[np.floating], **kwargs\n) -> tuple[NDArray[np.floating], float]:\n    \"\"\"\n    Fit a power law model of tension to a point cloud of residuals.\n\n    Parameters\n    ----------\n    residuals : NDArray[np.floating]\n        Residuals between measured point cloud and fit model.\n    **kwargs\n        Arguments to be passed to scipy.optimize.minimize\n\n    Returns\n    -------\n    tension_pars : NDArray[np.floating]\n        The fit parameters, see docstring of tension_model for details.\n    rms : float\n        The rms between the input residuals and the fit model.\n    \"\"\"\n\n    def min_func(pars, residuals):\n        _z = tension_model(*pars[:5], residuals)\n        return np.sqrt(np.mean((residuals[:, 2] - _z) ** 2))\n\n    if \"bounds\" not in kwargs:\n        ptp = np.ptp(residuals[:, 2])\n        bounds = [\n            (np.min(residuals[:, 0]), np.max(residuals[:, 0])),\n            (np.min(residuals[:, 1]), np.max(residuals[:, 1])),\n            (-1 * ptp, ptp),\n            (1e-10, np.inf),\n            (0, np.inf),\n        ]\n        kwargs[\"bounds\"] = bounds\n    x0 = [np.mean(residuals[:, 0]), np.mean(residuals[:, 1]), 0, 1, 0]\n    res = opt.minimize(min_func, x0, (residuals,), **kwargs)\n    return res.x, res.fun\n
    "},{"location":"reference/fitting/#lat_alignment.fitting.tension_model","title":"tension_model(x0, y0, t, a, b, points)","text":"

    Function to model incorrect panel tensioning. Currently the model used is a radial power law.

    Parameters:

    Name Type Description Default x0 float

    Center of the power law in x.

    required y0 float

    Center of the power law in y.

    required t float.

    Amplitude of power law, nominally the offset due to tensioning in the center of panel.

    required a float

    Base of power law.

    required b float

    Exponential scale factor of power law

    required points NDArray[floating]

    Points to compute power law at. Only the x and y coordinates are used (first two collumns). So should be (npoint, 2) but (npoint, ndim>2) is also fine.

    required

    Returns:

    Name Type Description z NDArray[floating]

    Power law model at each xy. Will have shape (npoint,).

    Source code in lat_alignment/fitting.py
    def tension_model(\n    x0: float, y0: float, t: float, a: float, b: float, points: NDArray[np.floating]\n) -> NDArray[np.floating]:\n    \"\"\"\n    Function to model incorrect panel tensioning.\n    Currently the model used is a radial power law.\n\n\n    Parameters\n    ----------\n    x0 : float\n        Center of the power law in x.\n    y0 : float\n        Center of the power law in y.\n    t : float.\n        Amplitude of power law,\n        nominally the offset due to tensioning in the center of panel.\n    a : float\n        Base of power law.\n    b : float\n        Exponential scale factor of power law\n    points : NDArray[np.floating]\n        Points to compute power law at.\n        Only the x and y coordinates are used (first two collumns).\n        So should be (npoint, 2) but (npoint, ndim>2) is also fine.\n\n    Returns\n    -------\n    z : NDArray[np.floating]\n        Power law model at each xy.\n        Will have shape (npoint,).\n    \"\"\"\n    # Avoid divide by 0 error\n    if a == 0:\n        return np.zeros(len(points))\n\n    # Compute radius at each point\n    r = np.sqrt((points[:, 0] - x0) ** 2 + (points[:, 1] - y0) ** 2)\n\n    # Return power law\n    return t * (a ** (-b * r))\n
    "},{"location":"reference/io/","title":"io","text":""},{"location":"reference/io/#lat_alignment.io.load_adjusters","title":"load_adjusters(path, mirror)","text":"

    Get nominal adjuster locations from file.

    Parameters:

    Name Type Description Default path str

    Path to the data file.

    required mirror str

    The mirror that these points belong to. Should be either: 'primary' or 'secondary'.

    'primary'

    Returns:

    Name Type Description adjusters dict[tuple[int, int], NDArray[float32]]

    Nominal adjuster locations. This is indexed by a (row, col) tuple. Each entry is (5, 3) array where each row is an adjuster.

    Source code in lat_alignment/io.py
    def load_adjusters(\n    path: str, mirror: str\n) -> dict[tuple[int, int], NDArray[np.float32]]:\n    \"\"\"\n    Get nominal adjuster locations from file.\n\n    Parameters\n    ----------\n    path : str\n        Path to the data file.\n    mirror : str, default: 'primary'\n        The mirror that these points belong to.\n        Should be either: 'primary' or 'secondary'.\n\n    Returns\n    -------\n    adjusters : dict[tuple[int, int], NDArray[np.float32]]\n        Nominal adjuster locations.\n        This is indexed by a (row, col) tuple.\n        Each entry is `(5, 3)` array where each row is an adjuster.\n    \"\"\"\n    if mirror not in [\"primary\", \"secondary\"]:\n        raise ValueError(f\"Invalid mirror: {mirror}\")\n\n    def _transform(coords):\n        coords = np.atleast_2d(coords)\n        coords -= np.array([120, 0, 0])  # cancel out shift\n        return coord_transform(coords, \"va_global\", f\"opt_{mirror}\")\n\n    # TODO: cleaner transform call\n    adjusters = defaultdict(list)\n    c_points = np.genfromtxt(path, dtype=str)\n    for point in c_points:\n        row = point[0][6]\n        col = point[0][7]\n        adjusters[(row, col)] += [_transform(np.array(point[2:], dtype=np.float32))[0]]\n    adjusters = {rc: np.vstack(pts) for rc, pts in adjusters.items()}\n\n    return adjusters\n
    "},{"location":"reference/io/#lat_alignment.io.load_corners","title":"load_corners(path)","text":"

    Get panel corners from file.

    Parameters:

    Name Type Description Default path str

    Path to the data file.

    required

    Returns:

    Name Type Description corners dict[tuple[int, int], ndarray[float32]]

    The corners. This is indexed by a (row, col) tuple. Each entry is (4, 3) array where each row is a corner.

    Source code in lat_alignment/io.py
    def load_corners(path: str) -> dict[tuple[int, int], NDArray[np.float32]]:\n    \"\"\"\n    Get panel corners from file.\n\n    Parameters\n    ----------\n    path : str\n        Path to the data file.\n\n    Returns\n    -------\n    corners : dict[tuple[int, int], ndarray[np.float32]]\n        The corners. This is indexed by a (row, col) tuple.\n        Each entry is `(4, 3)` array where each row is a corner.\n    \"\"\"\n    with open(path) as file:\n        corners_raw = yaml.safe_load(file)\n\n    corners = {\n        (panel[7], panel[9]): np.vstack(\n            [np.array(coord.split(), np.float32) for coord in coords]\n        )\n        for panel, coords in corners_raw.items()\n    }\n    return corners\n
    "},{"location":"reference/io/#lat_alignment.io.load_photo","title":"load_photo(path, align=True, err_thresh=2, plot=True, **kwargs)","text":"

    Load photogrammetry data. Assuming first column is target names and next three are (x, y , z).

    Parameters:

    Name Type Description Default path str

    The path to the photogrammetry data.

    required align bool

    If True align using the invar points.

    True err_thresh float

    How many times the median photogrammetry error a target need to have to be cut.

    2 plot bool

    If True display a scatter plot of targets.

    True **kwargs

    Arguments to pass to align_photo.

    {}

    Returns:

    Name Type Description data dict[str, NDArray[float32]]

    The photogrammetry data. Dict is indexed by the target names.

    Source code in lat_alignment/io.py
    def load_photo(\n    path: str, align: bool = True, err_thresh: float = 2, plot: bool = True, **kwargs\n) -> dict[str, NDArray[np.float32]]:\n    \"\"\"\n    Load photogrammetry data.\n    Assuming first column is target names and next three are (x, y , z).\n\n    Parameters\n    ----------\n    path : str\n        The path to the photogrammetry data.\n    align : bool, default: True\n        If True align using the invar points.\n    err_thresh : float, default: 2\n        How many times the median photogrammetry error\n        a target need to have to be cut.\n    plot: bool, default: True\n        If True display a scatter plot of targets.\n    **kwargs\n        Arguments to pass to `align_photo`.\n\n    Returns\n    -------\n    data : dict[str, NDArray[np.float32]]\n        The photogrammetry data.\n        Dict is indexed by the target names.\n    \"\"\"\n    labels = np.genfromtxt(path, dtype=str, delimiter=\",\", usecols=(0,))\n    coords = np.genfromtxt(path, dtype=np.float32, delimiter=\",\", usecols=(1, 2, 3))\n    errs = np.genfromtxt(path, dtype=np.float32, delimiter=\",\", usecols=(4, 5, 6))\n    msk = (np.char.find(labels, \"TARGET\") >= 0) + (np.char.find(labels, \"CODE\") >= 0)\n\n    labels, coords, errs = labels[msk], coords[msk], errs[msk]\n    err = np.linalg.norm(errs, axis=-1)\n\n    if align:\n        labels, coords, msk = align_photo(labels, coords, **kwargs)\n        err = err[msk]\n    trg_msk = np.char.find(labels, \"TARGET\") >= 0\n    labels = labels[trg_msk]\n    coords = coords[trg_msk]\n    err = err[trg_msk]\n\n    err_msk = err < err_thresh * np.median(err)\n    labels, coords, err = labels[err_msk], coords[err_msk], err[err_msk]\n\n    # Lets find and remove doubles\n    # Dumb brute force\n    edm = make_edm(coords[:, :2])\n    np.fill_diagonal(edm, np.nan)\n    to_kill = []\n    for i in range(len(edm)):\n        if i in to_kill:\n            continue\n        imin = np.nanargmin(edm[i])\n        if edm[i][imin] > 20:\n            continue\n        if err[i] < err[imin]:\n            to_kill += [imin]\n        else:\n            to_kill += [i]\n    msk = ~np.isin(np.arange(len(coords), dtype=int), to_kill)\n    labels, coords = labels[msk], coords[msk]\n\n    if plot:\n        plt.scatter(coords[:, 0], coords[:, 1], c=coords[:, 2], marker=\"x\")\n        plt.colorbar()\n        plt.show()\n\n    data = {label: coord for label, coord in zip(labels, coords)}\n    return data\n
    "},{"location":"reference/mirror/","title":"mirror","text":"

    Functions to describe the mirror surface.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel","title":"Panel dataclass","text":"

    Dataclass for storing a mirror panel.

    Attributes:

    Name Type Description mirror str

    Which mirror this panel is for. Should be 'primary' or 'secondary'.

    row int

    The row of the panel.

    col int

    The column of the panel.

    corners NDArray[float32]

    Array of panel corners. Should have shape (4, 3).

    measurements NDArray[float32]

    The measurement data for this panel. Should be in the mirror's internal coords. Should have shape (npoint, 3).

    nom_adj NDArray[float32]

    The nominal position of the adjusters in the mirror internal coordinates. Should have shape (5, 3).

    compensate float, default: 0

    The amount (in mm) to compensate the model surface by. This is to account for things like the Faro SMR.

    Source code in lat_alignment/mirror.py
    @dataclass\nclass Panel:\n    \"\"\"\n    Dataclass for storing a mirror panel.\n\n    Attributes\n    ----------\n    mirror : str\n        Which mirror this panel is for.\n        Should be 'primary' or 'secondary'.\n    row : int\n        The row of the panel.\n    col : int\n        The column of the panel.\n    corners : NDArray[np.float32]\n        Array of panel corners.\n        Should have shape `(4, 3)`.\n    measurements : NDArray[np.float32]\n        The measurement data for this panel.\n        Should be in the mirror's internal coords.\n        Should have shape `(npoint, 3)`.\n    nom_adj : NDArray[np.float32]\n        The nominal position of the adjusters in the mirror internal coordinates.\n        Should have shape `(5, 3)`.\n    compensate : float, default: 0\n        The amount (in mm) to compensate the model surface by.\n        This is to account for things like the Faro SMR.\n    \"\"\"\n\n    mirror: str\n    row: int\n    col: int\n    corners: NDArray[np.float32]\n    measurements: NDArray[np.float32]\n    nom_adj: NDArray[np.float32]\n    compensate: float = field(default=0.0)\n    adjuster_radius: float = field(default=50.0)\n\n    def __post_init__(self):\n        self.measurements = np.atleast_2d(self.measurements)\n\n    def __setattr__(self, name, value):\n        if (\n            name == \"nom_adj\"\n            or name == \"mirror\"\n            or name == \"measurements\"\n            or name == \"compensate\"\n        ):\n            self.__dict__.pop(\"can_surface\", None)\n            self.__dict__.pop(\"model\", None)\n            self.__dict__.pop(\"residuals\", None)\n            self.__dict__.pop(\"transformed_residuals\", None)\n            self.__dict__.pop(\"res_norm\", None)\n            self.__dict__.pop(\"rms\", None)\n            self.__dict__.pop(\"meas_surface\", None)\n            self.__dict__.pop(\"meas_adj\", None)\n            self.__dict__.pop(\"meas_adj_resid\", None)\n            self.__dict__.pop(\"model_transformed\", None)\n            self.__dict__.pop(\"_transform\", None)\n        elif name == \"adjuster_radius\":\n            self.__dict__.pop(\"meas_adj_resid\", None)\n        return super().__setattr__(name, value)\n\n    @cached_property\n    def model(self):\n        \"\"\"\n        The modeled mirror surface at the locations of the measurementss.\n        \"\"\"\n        model = self.measurements.copy()\n        model[:, 2] = mirror_surface(model[:, 0], model[:, 1], a[self.mirror])\n        if self.compensate != 0.0:\n            compensation = self.compensate * mirror_norm(\n                model[:, 0], model[:0], a[self.mirror]\n            )\n            model += compensation\n        return model\n\n    @cached_property\n    def _transform(self):\n        return get_rigid(self.model, self.measurements, center_dst=True, method=\"mean\")\n\n    @property\n    def rot(self):\n        \"\"\"\n        Rotation that aligns the model to the measurements.\n        \"\"\"\n        return self._transform[0]\n\n    @property\n    def shift(self):\n        \"\"\"\n        Shift that aligns the model to the measurements.\n        \"\"\"\n        return self._transform[1]\n\n    @cached_property\n    def can_surface(self):\n        \"\"\"\n        Get the cannonical points to define the panel surface.\n        These are the adjuster positions projected only the mirror surface.\n        Note that this is in the nominal coordinates not the measured ones.\n        \"\"\"\n        can_z = mirror_surface(self.nom_adj[:, 0], self.nom_adj[:, 1], a[self.mirror])\n        points = self.nom_adj.copy()\n        points[:, 2] = can_z\n        return points\n\n    @cached_property\n    def meas_surface(self):\n        \"\"\"\n        The cannonical surface transformed to be in the measured coordinates.\n        \"\"\"\n        return apply_transform(self.can_surface, self.rot, self.shift)\n\n    @cached_property\n    def meas_adj(self):\n        \"\"\"\n        The adjuster points transformed to be in the measured coordinates.\n        \"\"\"\n        return apply_transform(self.nom_adj, self.rot, self.shift)\n\n    @cached_property\n    def meas_adj_resid(self):\n        \"\"\"\n        A correction that can be applied to `meas_adj` where we compute\n        the average residual of measured points from the transformed model\n        that are within `adjuster_radius` of the adjuster point in `xy`.\n        \"\"\"\n        resid = np.zeros(len(self.meas_adj))\n        for i, adj in enumerate(self.meas_adj):\n            dists = np.linalg.norm(self.measurements[:, :2] - adj[:2], axis=-1)\n            msk = dists <= self.adjuster_radius\n            if np.sum(msk) == 0:\n                continue\n            resid[i] = np.mean(self.transformed_residuals[msk, 2])\n\n        return resid\n\n    @cached_property\n    def model_transformed(self):\n        \"\"\"\n        The model transformed to be in the measured coordinates.\n        \"\"\"\n        return apply_transform(self.model, self.rot, self.shift)\n\n    @cached_property\n    def residuals(self):\n        \"\"\"\n        Get residuals between model and measurements.\n        \"\"\"\n        return self.measurements - self.model\n\n    @cached_property\n    def transformed_residuals(self):\n        \"\"\"\n        Get residuals between transformed model and measurements.\n        \"\"\"\n        return self.measurements - self.model_transformed\n\n    @cached_property\n    def res_norm(self):\n        \"\"\"\n        Get norm of residuals between transformed model and measurements.\n        \"\"\"\n        return np.linalg.norm(self.residuals, axis=-1)\n\n    @cached_property\n    def rms(self):\n        \"\"\"\n        Get rms between model and measurements.\n        \"\"\"\n        return np.sqrt(np.mean(self.residuals[:, 2].ravel() ** 2))\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.can_surface","title":"can_surface cached property","text":"

    Get the cannonical points to define the panel surface. These are the adjuster positions projected only the mirror surface. Note that this is in the nominal coordinates not the measured ones.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.meas_adj","title":"meas_adj cached property","text":"

    The adjuster points transformed to be in the measured coordinates.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.meas_adj_resid","title":"meas_adj_resid cached property","text":"

    A correction that can be applied to meas_adj where we compute the average residual of measured points from the transformed model that are within adjuster_radius of the adjuster point in xy.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.meas_surface","title":"meas_surface cached property","text":"

    The cannonical surface transformed to be in the measured coordinates.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.model","title":"model cached property","text":"

    The modeled mirror surface at the locations of the measurementss.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.model_transformed","title":"model_transformed cached property","text":"

    The model transformed to be in the measured coordinates.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.res_norm","title":"res_norm cached property","text":"

    Get norm of residuals between transformed model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.residuals","title":"residuals cached property","text":"

    Get residuals between model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.rms","title":"rms cached property","text":"

    Get rms between model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.rot","title":"rot property","text":"

    Rotation that aligns the model to the measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.shift","title":"shift property","text":"

    Shift that aligns the model to the measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.Panel.transformed_residuals","title":"transformed_residuals cached property","text":"

    Get residuals between transformed model and measurements.

    "},{"location":"reference/mirror/#lat_alignment.mirror.gen_panels","title":"gen_panels(mirror, measurements, corners, adjusters, compensate=0.0, adjuster_radius=50.0)","text":"

    Use a set of measurements to generate panel objects.

    Parameters:

    Name Type Description Default mirror str

    The mirror these panels belong to. Should be 'primary' or 'secondary'.

    required measurements dict[str, NDArray[float32]]

    The photogrammetry data. Dict is data indexed by the target names.

    required corners dict[tuple[int, int], ndarray[float32]]

    The corners. This is indexed by a (row, col) tuple. Each entry is (4, 3) array where each row is a corner.

    required adjusters dict[tuple[int, int], NDArray[float32]]

    Nominal adjuster locations. This is indexed by a (row, col) tuple. Each entry is (5, 3) array where each row is an adjuster.

    required compensate float

    Amount (in mm) to compensate the model surface by. This is to account for things like the faro SMR.

    0.0 adjuster_radius float

    The radius in XY of points that an adjuster should use to compute a secondary correction on its position. Should be in mm.

    50.0

    Returns:

    Name Type Description panels list[Panels]

    A list of panels with the transforme initialized to the identity.

    Source code in lat_alignment/mirror.py
    def gen_panels(\n    mirror: str,\n    measurements: dict[str, NDArray[np.float32]],\n    corners: dict[tuple[int, int], NDArray[np.float32]],\n    adjusters: dict[tuple[int, int], NDArray[np.float32]],\n    compensate: float = 0.0,\n    adjuster_radius: float = 50.0,\n) -> list[Panel]:\n    \"\"\"\n    Use a set of measurements to generate panel objects.\n\n    Parameters\n    ----------\n    mirror : str\n        The mirror these panels belong to.\n        Should be 'primary' or 'secondary'.\n    measurements : dict[str, NDArray[np.float32]]\n        The photogrammetry data.\n        Dict is data indexed by the target names.\n    corners : dict[tuple[int, int], ndarray[np.float32]]\n        The corners. This is indexed by a (row, col) tuple.\n        Each entry is `(4, 3)` array where each row is a corner.\n    adjusters : dict[tuple[int, int], NDArray[np.float32]]\n        Nominal adjuster locations.\n        This is indexed by a (row, col) tuple.\n        Each entry is `(5, 3)` array where each row is an adjuster.\n    compensate : float, default: 0.0\n        Amount (in mm) to compensate the model surface by.\n        This is to account for things like the faro SMR.\n    adjuster_radius : float, default: 50.0\n        The radius in XY of points that an adjuster should use to\n        compute a secondary correction on its position.\n        Should be in mm.\n\n    Returns\n    -------\n    panels : list[Panels]\n        A list of panels with the transforme initialized to the identity.\n    \"\"\"\n    points = defaultdict(list)\n    # dumb brute force\n    corr = np.arange(4, dtype=int)\n    for _, point in measurements.items():\n        for rc, crns in corners.items():\n            x = crns[:, 0] > point[0]\n            y = crns[:, 1] > point[1]\n            val = x.astype(int) + 2 * y.astype(int)\n            if np.array_equal(np.sort(val), corr):\n                points[rc] += [point]\n                break\n\n    # Now init the objects\n    panels = []\n    for (row, col), meas in points.items():\n        meas = np.vstack(meas, dtype=np.float32)\n        panel = Panel(\n            mirror,\n            row,\n            col,\n            corners[(row, col)],\n            meas,\n            adjusters[(row, col)],\n            compensate,\n            adjuster_radius,\n        )\n        panels += [panel]\n    return panels\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.mirror_norm","title":"mirror_norm(x, y, a)","text":"

    Analytic form of the vector normal to the mirror surface.

    Parameters:

    Name Type Description Default x NDArray[float32]

    X positions to calculate at in mm.

    required y NDArray[float32]

    Y positions to calculate at in mm. Should have the same shape as x.

    required a NDArray[float32]

    Coeffecients of the mirror function. Use a_primary for the primary mirror. Use a_secondary for the secondary mirror.

    required

    Returns:

    Name Type Description normals NDArray[float32]

    Unit vector normal to the mirror surface at each input coordinate. Has shape shape(x) + (3,).

    Source code in lat_alignment/mirror.py
    def mirror_norm(\n    x: NDArray[np.float32], y: NDArray[np.float32], a: NDArray[np.float32]\n) -> NDArray[np.float32]:\n    \"\"\"\n    Analytic form of the vector normal to the mirror surface.\n\n    Parameters\n    ----------\n    x : NDArray[np.float32]\n        X positions to calculate at in mm.\n    y : NDArray[np.float32]\n        Y positions to calculate at in mm.\n        Should have the same shape as `x`.\n    a : NDArray[np.float32]\n        Coeffecients of the mirror function.\n        Use `a_primary` for the primary mirror.\n        Use `a_secondary` for the secondary mirror.\n\n    Returns\n    -------\n    normals : NDArray[np.float32]\n        Unit vector normal to the mirror surface at each input coordinate.\n        Has shape `shape(x) + (3,)`.\n    \"\"\"\n    Rn = 3000.0\n\n    x_n = np.zeros_like(x)\n    y_n = np.zeros_like(y)\n    for i in range(a.shape[0]):\n        for j in range(a.shape[1]):\n            if i != 0:\n                x_n += a[i, j] * (x ** (i - 1)) / (Rn**i) * (y / Rn) ** j\n            if j != 0:\n                y_n += a[i, j] * (x / Rn) ** i * (y ** (j - 1)) / (Rn**j)\n\n    z_n = -1 * np.ones_like(x_n)\n    normals = np.array((x_n, y_n, z_n)).T\n    normals /= np.linalg.norm(normals, axis=-1)[:, np.newaxis]\n    return normals\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.mirror_surface","title":"mirror_surface(x, y, a)","text":"

    Analytic form of the mirror surface.

    Parameters:

    Name Type Description Default x NDArray[float32]

    X positions to calculate at in mm.

    required y NDArray[float32]

    Y positions to calculate at in mm. Should have the same shape as x.

    required a NDArray[float32]

    Coeffecients of the mirror function. Use a_primary for the primary mirror. Use a_secondary for the secondary mirror.

    required

    Returns:

    Name Type Description z NDArray[float32]

    Z position of the mirror at each input coordinate. Has the same shape as x.

    Source code in lat_alignment/mirror.py
    def mirror_surface(\n    x: NDArray[np.float32], y: NDArray[np.float32], a: NDArray[np.float32]\n) -> NDArray[np.float32]:\n    \"\"\"\n    Analytic form of the mirror surface.\n\n    Parameters\n    ----------\n    x : NDArray[np.float32]\n        X positions to calculate at in mm.\n    y : NDArray[np.float32]\n        Y positions to calculate at in mm.\n        Should have the same shape as `x`.\n    a : NDArray[np.float32]\n        Coeffecients of the mirror function.\n        Use `a_primary` for the primary mirror.\n        Use `a_secondary` for the secondary mirror.\n\n    Returns\n    -------\n    z : NDArray[np.float32]\n        Z position of the mirror at each input coordinate.\n        Has the same shape as `x`.\n    \"\"\"\n    z = np.zeros_like(x)\n    Rn = 3000.0\n    for i in range(a.shape[0]):\n        for j in range(a.shape[1]):\n            z += a[i, j] * (x / Rn) ** i * (y / Rn) ** j\n    return z\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.plot_panels","title":"plot_panels(panels, title_str, vmax=None)","text":"

    Make a plot containing panel residuals and histogram. TODO: Correlation?

    Parameters:

    Name Type Description Default panels list[Panel]

    The panels to plot.

    required title_str str

    The title string, rms will me appended.

    required vmax Optional[float]

    The max of the colorbar. vmin will be -1 times this. Set to None to compute automatically. Should be in um.

    None

    Returns:

    Name Type Description figure Figure

    The figure with panels plotted on it.

    Source code in lat_alignment/mirror.py
    def plot_panels(\n    panels: list[Panel], title_str: str, vmax: Optional[float] = None\n) -> Figure:\n    \"\"\"\n    Make a plot containing panel residuals and histogram.\n    TODO: Correlation?\n\n    Parameters\n    ----------\n    panels : list[Panel]\n        The panels to plot.\n    title_str : str\n        The title string, rms will me appended.\n    vmax : Optional[float], default: None\n        The max of the colorbar. vmin will be -1 times this.\n        Set to None to compute automatically.\n        Should be in um.\n\n    Returns\n    -------\n    figure : Figure\n        The figure with panels plotted on it.\n    \"\"\"\n    res_all = np.vstack([panel.residuals for panel in panels]) * 1000\n    model_all = np.vstack([panel.model for panel in panels])\n    if vmax is None:\n        vmax = np.max(np.abs(res_all[:, 2]))\n    if vmax is None:\n        raise ValueError(\"vmax still None?\")\n    gs = gridspec.GridSpec(2, 2, width_ratios=[20, 1], height_ratios=[2, 1])\n    fig = plt.figure()\n    ax0 = plt.subplot(gs[0])\n    cax = plt.subplot(gs[1])\n    ax1 = plt.subplot(gs[2:])\n    cb = None\n    for panel in panels:\n        ax0.tricontourf(\n            panel.model[:, 0],\n            panel.model[:, 1],\n            panel.residuals[:, 2] * 1000,\n            vmin=-1 * vmax,\n            vmax=vmax,\n            cmap=\"coolwarm\",\n            alpha=0.6,\n        )\n        cb = ax0.scatter(\n            panel.model[:, 0],\n            panel.model[:, 1],\n            s=40,\n            c=panel.residuals[:, 2] * 1000,\n            vmin=-1 * vmax,\n            vmax=vmax,\n            cmap=\"coolwarm\",\n            marker=\"o\",\n            alpha=0.9,\n            linewidth=2,\n            edgecolor=\"black\",\n        )\n        ax0.scatter(\n            panel.meas_adj[:, 0],\n            panel.meas_adj[:, 1],\n            marker=\"x\",\n            linewidth=1,\n            color=\"black\",\n        )\n    ax0.tricontourf(\n        model_all[:, 0],\n        model_all[:, 1],\n        res_all[:, 2],\n        vmin=-1 * vmax,\n        vmax=vmax,\n        cmap=\"coolwarm\",\n        alpha=0.2,\n    )\n    ax0.set_xlabel(\"x (mm)\")\n    ax0.set_ylabel(\"y (mm)\")\n    ax0.set_xlim(-3300, 3300)  # ack hardcoded!\n    ax0.set_ylim(-3300, 3300)\n    if cb is not None:\n        fig.colorbar(cb, cax)\n    ax0.set_aspect(\"equal\")\n    for panel in panels:\n        ax0.add_patch(\n            Polygon(panel.corners[[0, 1, 3, 2], :2], fill=False, color=\"black\")\n        )\n\n    ax1.hist(res_all[:, 2], bins=len(panels))\n    ax1.set_xlabel(\"z residual (um)\")\n\n    points = np.array([len(panel.measurements) for panel in panels])\n    rms = np.array([panel.rms for panel in panels])\n    tot_rms = 1000 * np.sum(rms * points) / np.sum(points)\n    fig.suptitle(f\"{title_str}, RMS={tot_rms:.2f} um\")\n\n    plt.show()\n\n    return fig\n
    "},{"location":"reference/mirror/#lat_alignment.mirror.remove_cm","title":"remove_cm(meas, mirror, compensate=0, thresh=10, cut_thresh=50, niters=10, verbose=False)","text":"

    Fit for the common mode transformation from the model to the measurements of all panels and them remove it. Note that we only remove the shift component of the common mode, rotations are ignored.

    Parameters:

    Name Type Description Default meas dict[str, NDArray[float32]]

    The photogrammetry data. Dict is data indexed by the target names.

    required mirror str

    The mirror this data belong to. Should be 'primary' or 'secondary'.

    required compensate float

    Compensation to apply to model. This is to account for the radius of a Faro SMR.

    0 thresh float

    How many times higher than the median residual a point needs to have to be considered an outlier.

    10 niters int

    How many iterations of common mode fitting to do.

    10 verbose bool

    If True print the transformation for each iteration.

    False

    Returns:

    Name Type Description kept_panels list[Panel]

    The panels that were successfully fit.

    Source code in lat_alignment/mirror.py
    def remove_cm(\n    meas,\n    mirror,\n    compensate: float = 0,\n    thresh: float = 10,\n    cut_thresh: float = 50,\n    niters: int = 10,\n    verbose=False,\n) -> dict[str, NDArray[np.float32]]:\n    \"\"\"\n    Fit for the common mode transformation from the model to the measurements of all panels and them remove it.\n    Note that we only remove the shift component of the common mode, rotations are ignored.\n\n    Parameters\n    ----------\n    meas : dict[str, NDArray[np.float32]]\n        The photogrammetry data.\n        Dict is data indexed by the target names.\n    mirror : str\n        The mirror this data belong to.\n        Should be 'primary' or 'secondary'.\n    compensate : float, default: 0\n        Compensation to apply to model.\n        This is to account for the radius of a Faro SMR.\n    thresh : float, default: 10\n        How many times higher than the median residual a point needs to have to be\n        considered an outlier.\n    niters : int, default: 10\n        How many iterations of common mode fitting to do.\n    verbose : bool, default: False\n        If True print the transformation for each iteration.\n\n    Returns\n    -------\n    kept_panels : list[Panel]\n        The panels that were successfully fit.\n    \"\"\"\n\n    def _cm(x, panel):\n        panel.measurements[:] -= x[1:4]\n        rot = Rotation.from_euler(\"xyz\", x[4:])\n        panel.measurements = rot.apply(panel.measurements)\n        panel.measurements *= x[0]\n\n    def _opt(x, panel):\n        p2 = deepcopy(panel)\n        _cm(x, p2)\n        return p2.rms\n\n    # make a fake panel for the full mirror\n    corners = np.array(\n        ([-3300, -3300, 0], [-3300, 3300, 0], [3300, 3300, 0], [3300, -3300, 0])\n    )  # ack hardcoded\n    labels = np.array(list(meas.keys()))\n    data = np.array(list(meas.values()))\n    corr = np.arange(4, dtype=int)\n    x = np.vstack([corners[:, 0] > dat[0] for dat in data])\n    y = np.vstack([corners[:, 1] > dat[1] for dat in data])\n    val = x.astype(int) + 2 * y.astype(int)\n    val = np.sort(val, axis=-1)\n    msk = (val == corr).all(-1)\n    data = data[msk]\n    labels = labels[msk]\n    panel = Panel(\n        mirror,\n        -1,\n        -1,\n        np.zeros((4, 3), \"float32\"),\n        data,\n        np.zeros((5, 3), \"float32\"),\n        compensate,\n    )\n    data = data.copy()\n    data_clean = data.copy()\n\n    x0 = np.hstack([np.ones(1), np.zeros(6)])\n    bounds = [(-0.95, 1.05)] + [(-100, 100)] * 3 + [(0, 2 * np.pi)] * 3\n\n    for i in range(niters):\n        if len(panel.measurements) < 3:\n            raise ValueError\n        print(f\"iter {i} for common mode fit\")\n        cut = panel.res_norm > thresh * np.median(panel.res_norm)\n        if np.sum(cut) > 0:\n            # print(f\"\\tRemoving {np.sum(cut)} points from mirror\")\n            panel.measurements = panel.measurements[~cut]\n            # labels = labels[~cut]\n            data = data[~cut]\n\n        if verbose:\n            print(f\"\\tRemoving a naive common mode shift of {panel.shift}\")\n        panel.measurements -= panel.shift\n        panel.measurements @= panel.rot.T\n\n        res = minimize(_opt, x0, (panel,), bounds=bounds)\n        if verbose:\n            print(\n                f\"\\tRemoving a fit common mode with scale {res.x[0]}, shift {res.x[1:4]}, and rotation {res.x[4:]}\"\n            )\n        _cm(res.x, panel)\n\n        if verbose:\n            print(\n                f\"\\tRemoving a secondary common mode shift of {panel.shift} and rotation of {decompose_rotation(panel.rot)}\"\n            )\n        panel.measurements -= panel.shift\n        panel.measurements @= panel.rot.T\n\n    aff, sft = get_affine(\n        data, panel.measurements, method=\"mean\", weights=np.ones(len(data))\n    )\n    scale, shear, rot = decompose_affine(aff)\n    rot = decompose_rotation(rot)\n    print(\n        f\"Full common mode is:\\n\\tshift = {sft} mm\\n\\tscale = {scale}\\n\\tshear = {shear}\\n\\trot = {np.rad2deg(rot)} deg\"\n    )\n\n    panel.measurements = apply_transform(data_clean, aff, sft)\n    cut = panel.res_norm > cut_thresh * np.median(panel.res_norm)\n    if np.sum(cut) > 0:\n        print(f\"Removing {np.sum(cut)} points from mirror\")\n        panel.measurements = panel.measurements[~cut]\n\n    return {l: d for l, d in zip(labels, panel.measurements)}\n
    "},{"location":"reference/transforms/","title":"transforms","text":"

    Functions for coordinate transforms.

    There are 6 relevant coordinate systems here, belonging to two sets of three. Each set is a global, a primary, and a secondary coordinate system; where primary and secondary are internal to those mirrors. The two sets of coordinates are the optical coordinates and the coordinates used by vertex. We denote these six coordinate systems as follows:

    - opt_global\n- opt_primary\n- opt_secondary\n- va_global\n- va_primary\n- va_secondary\n
    "},{"location":"reference/transforms/#lat_alignment.transforms.align_photo","title":"align_photo(labels, coords, *, mirror='primary', reference=None, max_dist=100.0)","text":"

    Align photogrammetry data and then put it into mirror coordinates.

    Parameters:

    Name Type Description Default labels NDArray[str_]

    The labels of each photogrammetry point. Should have shape (npoint,).

    required coords NDArray[float32]

    The coordinates of each photogrammetry point. Should have shape (npoint, 3).

    required mirror str

    The mirror that these points belong to. Should be either: 'primary' or 'secondary'.

    'primary' reference Optional[list[tuple[tuple[float, float, float], list[str]]]]

    List of reference points to use. Each point given should be a tuple with two elements. The first element is a tuple with the (x, y, z) coordinates of the point in the global coordinate system. The second is a list of nearby coded targets that can be used to identify the point. If None the default reference for each mirror is used.

    None max_dist float

    Max distance in mm that the reference poing can be from the target point used to locate it.

    100

    Returns:

    Name Type Description labels NDArray[str_]

    The labels of each photogrammetry point. Invar points are not included.

    coords_transformed NDArray[float32]

    The transformed coordinates. Invar points are not included.

    msk NDArray[bool_]

    Mask to removes invar points

    Source code in lat_alignment/transforms.py
    def align_photo(\n    labels: NDArray[np.str_],\n    coords: NDArray[np.float32],\n    *,\n    mirror: str = \"primary\",\n    reference: Optional[list[tuple[tuple[float, float, float], list[str]]]] = None,\n    max_dist: float = 100.0,\n) -> tuple[NDArray[np.str_], NDArray[np.float32], NDArray[np.bool_]]:\n    \"\"\"\n    Align photogrammetry data and then put it into mirror coordinates.\n\n    Parameters\n    ----------\n    labels : NDArray[np.str_]\n        The labels of each photogrammetry point.\n        Should have shape `(npoint,)`.\n    coords : NDArray[np.float32]\n        The coordinates of each photogrammetry point.\n        Should have shape `(npoint, 3)`.\n    mirror : str, default: 'primary'\n        The mirror that these points belong to.\n        Should be either: 'primary' or 'secondary'.\n    reference : Optional[list[tuple[tuple[float, float, float], list[str]]]], default: None\n        List of reference points to use.\n        Each point given should be a tuple with two elements.\n        The first element is a tuple with the (x, y, z) coordinates\n        of the point in the global coordinate system.\n        The second is a list of nearby coded targets that can be used\n        to identify the point.\n        If `None` the default reference for each mirror is used.\n    max_dist : float, default: 100\n        Max distance in mm that the reference poing can be from the target\n        point used to locate it.\n\n    Returns\n    -------\n    labels : NDArray[np.str_]\n        The labels of each photogrammetry point.\n        Invar points are not included.\n    coords_transformed : NDArray[np.float32]\n        The transformed coordinates.\n        Invar points are not included.\n    msk : NDArray[np.bool_]\n        Mask to removes invar points\n    \"\"\"\n    if mirror not in [\"primary\", \"secondary\"]:\n        raise ValueError(f\"Invalid mirror: {mirror}\")\n    if mirror == \"primary\":\n        transform = partial(coord_transform, cfrom=\"va_global\", cto=\"opt_primary\")\n    else:\n        transform = partial(coord_transform, cfrom=\"va_global\", cto=\"opt_secondary\")\n    if reference is None:\n        reference = DEFAULT_REF[mirror]\n    if reference is None or len(reference) == 0:\n        raise ValueError(\"Invalid or empty reference\")\n\n    # Lets find the points we can use\n    trg_idx = np.where(np.char.find(labels, \"TARGET\") >= 0)[0]\n    ref = []\n    pts = []\n    invars = []\n    for rpoint, codes in reference:\n        have = np.isin(codes, labels)\n        if np.sum(have) == 0:\n            continue\n        coded = coords[np.where(labels == codes[np.where(have)[0][0]])[0][0]]\n        print(codes[np.where(have)[0][0]])\n        # Find the closest point\n        dist = np.linalg.norm(coords[trg_idx] - coded, axis=-1)\n        if np.min(dist) > max_dist:\n            continue\n        print(np.min(dist))\n        ref += [rpoint]\n        pts += [coords[trg_idx][np.argmin(dist)]]\n        invars += [labels[trg_idx][np.argmin(dist)]]\n    if len(ref) < 4:\n        raise ValueError(f\"Only {len(ref)} reference points found! Can't align!\")\n    msk = [0, 1, 3]\n    pts = np.vstack(pts)[msk]\n    ref = np.vstack(ref)[msk]\n    pts = np.vstack((pts, np.mean(pts, 0)))\n    ref = np.vstack((ref, np.mean(ref, 0)))\n    ref = transform(ref)\n    print(\"Reference points in mirror coords:\")\n    print(ref[:-1])\n    print(make_edm(ref) / make_edm(pts))\n    print(make_edm(ref) - make_edm(pts))\n    print(np.nanmedian(make_edm(ref) / make_edm(pts)))\n    pts *= np.nanmedian(make_edm(ref) / make_edm(pts))\n    print(make_edm(ref) / make_edm(pts))\n    print(make_edm(ref) - make_edm(pts))\n    print(np.nanmedian(make_edm(ref) / make_edm(pts)))\n\n    rot, sft = get_rigid(pts, ref, method=\"mean\")\n    pts_t = apply_transform(pts, rot, sft)\n    import matplotlib.pyplot as plt\n\n    plt.scatter(pts_t[:, 0], pts_t[:, 1], color=\"b\")\n    plt.scatter(ref[:, 0], ref[:, 1], color=\"r\")\n    plt.show()\n    print(pts_t[:-1])\n    print(pts_t - ref)\n    print(\n        f\"RMS of reference points after alignment: {np.sqrt(np.mean((pts_t - ref)**2))}\"\n    )\n    coords_transformed = apply_transform(coords, rot, sft)\n\n    msk = ~np.isin(labels, invars)\n\n    return labels[msk], coords_transformed[msk], msk\n
    "},{"location":"reference/transforms/#lat_alignment.transforms.coord_transform","title":"coord_transform(coords, cfrom, cto)","text":"

    Transform between the six defined mirror coordinates:

    - opt_global\n- opt_primary\n- opt_secondary\n- va_global\n- va_primary\n- va_secondary\n

    Parameters:

    Name Type Description Default coords NDArray[float32]

    Coordinates to transform. Should be a (npoint, 3) array.

    required cfrom str

    The coordinate system that coords is currently in.

    required cto str

    The coordinate system to put coords into.

    required

    Returns:

    Name Type Description coords_transformed NDArray[float32]

    coords transformed into cto.

    Source code in lat_alignment/transforms.py
    def coord_transform(\n    coords: NDArray[np.float32], cfrom: str, cto: str\n) -> NDArray[np.float32]:\n    \"\"\"\n    Transform between the six defined mirror coordinates:\n\n        - opt_global\n        - opt_primary\n        - opt_secondary\n        - va_global\n        - va_primary\n        - va_secondary\n\n    Parameters\n    ----------\n    coords : NDArray[np.float32]\n        Coordinates to transform.\n        Should be a `(npoint, 3)` array.\n    cfrom : str\n        The coordinate system that `coords` is currently in.\n    cto : str\n        The coordinate system to put `coords` into.\n\n    Returns\n    -------\n    coords_transformed : NDArray[np.float32]\n        `coords` transformed into `cto`.\n    \"\"\"\n    if cfrom == cto:\n        return coords\n    match f\"{cfrom}-{cto}\":\n        case \"opt_global-opt_primary\":\n            return _opt_global_to_opt_primary(coords)\n        case \"opt_global-opt_secondary\":\n            return _opt_global_to_opt_secondary(coords)\n        case \"opt_primary-opt_global\":\n            return _opt_primary_to_opt_global(coords)\n        case \"opt_secondary-opt_global\":\n            return _opt_secondary_to_opt_global(coords)\n        case \"opt_primary-opt_secondary\":\n            return _opt_primary_to_opt_secondary(coords)\n        case \"opt_secondary-opt_primary\":\n            return _opt_secondary_to_opt_primary(coords)\n        case \"va_global-va_primary\":\n            return _va_global_to_va_primary(coords)\n        case \"va_global-va_secondary\":\n            return _va_global_to_va_secondary(coords)\n        case \"va_primary-va_global\":\n            return _va_primary_to_va_global(coords)\n        case \"va_secondary-va_global\":\n            return _va_secondary_to_va_global(coords)\n        case \"va_primary-va_secondary\":\n            return _va_primary_to_va_secondary(coords)\n        case \"va_secondary-va_primary\":\n            return _va_secondary_to_va_primary(coords)\n        case \"opt_global-va_global\":\n            return _opt_global_to_va_global(coords)\n        case \"opt_global-va_primary\":\n            return _opt_global_to_va_primary(coords)\n        case \"opt_global-va_secondary\":\n            return _opt_global_to_va_secondary(coords)\n        case \"opt_primary-va_global\":\n            return _opt_primary_to_va_global(coords)\n        case \"opt_primary-va_primary\":\n            return _opt_primary_to_va_primary(coords)\n        case \"opt_primary-va_secondary\":\n            return _opt_primary_to_va_secondary(coords)\n        case \"opt_secondary-va_global\":\n            return _opt_secondary_to_va_global(coords)\n        case \"opt_secondary-va_primary\":\n            return _opt_secondary_to_va_primary(coords)\n        case \"opt_secondary-va_secondary\":\n            return _opt_secondary_to_va_secondary(coords)\n        case \"va_global-opt_global\":\n            return _va_global_to_opt_global(coords)\n        case \"va_global-opt_primary\":\n            return _va_global_to_opt_primary(coords)\n        case \"va_global-opt_secondary\":\n            return _va_global_to_opt_secondary(coords)\n        case \"va_primary-opt_global\":\n            return _va_primary_to_opt_global(coords)\n        case \"va_primary-opt_primary\":\n            return _va_primary_to_opt_primary(coords)\n        case \"va_primary-opt_secondary\":\n            return _va_primary_to_opt_secondary(coords)\n        case \"va_secondary-opt_global\":\n            return _va_secondary_to_opt_global(coords)\n        case \"va_secondary-opt_primary\":\n            return _va_secondary_to_opt_primary(coords)\n        case \"va_secondary-opt_secondary\":\n            return _va_secondary_to_opt_secondary(coords)\n        case _:\n            raise ValueError(\"Invalid coordinate system provided!\")\n
    "}]} \ No newline at end of file