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":" - Create the appropriate directory structure for your measurement (see File Structure for details).
- Place the measurement files in the appropriate place in your created directory (see Measurement Files for details).
- Create a file with any information about the measurement that could prove useful (see Description File for details).
- Create a config file for your measurement (see Config File for details).
- Run the alignment script with
lat_alignment /PATH/TO/CONFIG
- 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":" - Create the appropriate directory structure for your measurement (see File Structure for details).
- Place the measurement files in the appropriate place in your created directory (see Measurement Files for details).
- Create a file with any information about the measurement that could prove useful (see Description File for details).
- Create a config file for your measurement (see Config File for details).
- Run the alignment script with
lat_alignment /PATH/TO/CONFIG
- 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