SOTN-001

OFC Control Loop Mathematical Description#

Abstract

This technical note provides a mathematical description of the Optical Feedback Control (OFC) system used in the Vera C. Rubin Observatory’s Active Optics System (AOS), as implemented in ts_ofc, together with the decisions that the ts_mtaos closed loop makes around each OFC call. The measurement side (wavefront estimation in ts_wep) is out of scope; the wavefront error is treated as an input. All quoted defaults come from the summit configuration in ts_config_mttcs.

The document begins with the forward optical model, explaining how the double Zernike sensitivity matrix relates telescope degrees of freedom (DOFs) to wavefront Zernike coefficients and how DOF normalization balances their contributions. It then describes how the measurements are de-rotated and corrected for known terms before inversion.

The core of the note addresses the state estimation inverse problem: recovering DOF states from noisy wavefront measurements. The textbook regularizations (SVD truncation and noise-weighted least squares) are introduced and then contrasted with what ts_ofc actually computes: a least-squares fit confined to the leading v-modes of the full-field optical model. A concrete numerical example gives the matrix dimensions for the deployed 76-measurement, 20-DOF, 12-mode configuration.

A dedicated section defines v-modes, explains their physical interpretation as coordinated DOF adjustments that produce characteristic aberration patterns, and describes the coordinate transformations between DOF space and v-mode space.

The note then covers PID control as implemented, including the leaky integrator, anti-windup clipping and the derivative filter, in both DOF space and v-mode space, and shows how v-mode gains enable tuning by optical importance. A final section describes where OFC sits in the MTAOS closed loop: gain overrides, the large-defocus bypass, mirror stress limits and the pointing correction.

This document serves as a reference for developers working on AOS software and operators seeking to understand the control loop mathematics.

V-Modes and the OFC Control Loop#

This chapter provides a mathematical description of the Optical Feedback Control (OFC) pipeline implemented in ts_ofc, and of the way the ts_mtaos closed loop wraps it. It covers the forward optical model, the preprocessing of the wavefront measurements and the state estimation inverse problem. It then defines the v-modes, explains their physical meaning, and shows how PID gains are applied in both DOF space and v-mode space. It closes with the decisions that MTAOS makes around each OFC call.

Scope and conventions#

What is covered. The core of this note is the mathematics implemented in ts_ofc: how the wavefront error vector \(\mathbf{y}\) is turned into an estimate of the telescope state \(\hat{\mathbf{x}}\), and how a PID controller converts that estimate into a correction. A dedicated section, Where OFC sits in the closed loop, describes only the parts of ts_mtaos that modify the control law or its inputs (gain overrides, image skipping, the large-defocus bypass, stress limits and the pointing correction). The CSC state machine, event publishing and pipeline management are documented in the MTAOS documentation and are not repeated here.

What is not covered. The measurement side of the loop is out of scope: how ts_wep estimates Zernike coefficients from donut images, and how those results are visualized with donut_viz. Here the wavefront error is treated as an input. At the OFC boundary it is:

  • one vector of Zernike coefficients per corner wavefront sensor, in micrometers, for Noll indices Z4 up to Z28 (znmin = 4, znmax = 28), read from the zernikes butler table produced by WEP;

  • the average of the intra- and extra-focal estimates of each corner;

  • expressed in the camera frame (not de-rotated);

  • with sensors whose estimate failed (NaN) dropped before inversion.

In the deployed configuration, MTAOS reads the deviation_columns of that table, that is, the measured Zernikes minus the intrinsic (design) Zernikes. It therefore sets subtract_intrinsics = False so that OFC does not subtract them a second time. This affects the subtraction described in Measurement preprocessing.

Which configuration the numbers come from. ts_ofc ships a set of policy files, but the values actually used on the summit live in ts_config_mttcs:

  • MTAOS/ofc/ holds the OFC data (sensitivity matrix, intrinsic Zernikes, normalization weights, y2 offsets, noise covariance, and the controller files). OFCData loads configurations/init.yaml by default, and MTAOS points OFCData to this directory.

  • MTAOS/v13/_init.yaml (at the time of writing) holds the CSC configuration: which DOFs are used, thresholds, stress limits, and the closed-loop parameters.

Unless stated otherwise, every default quoted in this note comes from those two files. The copies packaged with ts_ofc differ, for example in the gains, and should not be used as a reference.

Notation. Vectors are bold lower case, matrices bold upper case. \(k\) is the closed-loop iteration index. \(r\) is the truncation index (the number of v-modes retained). \(\mathcal{D}\) is the set of active DOF indices (dof_idx); a subscript \(\mathcal{D}\) on a 50-element vector or on a gain selects those entries.

The forward optical model#

The sensitivity matrix relates the telescope’s degrees of freedom (DOFs) to the wavefront error they produce across the focal plane. For each sensor position, it encodes how a unit change in each DOF affects each Zernike coefficient of the wavefront.

If we stack the Zernike coefficients from all sensors into a single vector, the forward model is:

\[\mathbf{y} = \mathbf{A}\,\mathbf{x}\]

where:

  • \(\mathbf{y} \in \mathbb{R}^{m}\) is the wavefront error vector (Zernike coefficients across all sensors, in micrometers), with \(m = n_\mathrm{sensors} \times n_\mathrm{Zernikes}\). For four corner wavefront sensors and Zernikes Z4–Z22 (19 coefficients), \(m = 4 \times 19 = 76\).

  • \(\mathbf{A} \in \mathbb{R}^{m \times n}\) is the sensitivity matrix, evaluated at the sensor field positions and the camera rotation angle.

  • \(\mathbf{x} \in \mathbb{R}^{n}\) is the DOF state vector, with \(n = |\mathcal{D}|\) the number of used DOFs.

The full DOF vector has 50 entries: M2 hexapod (dZ, dX, dY in micrometers; rX, rY in arcseconds), camera hexapod (same), 20 M1M3 bending modes and 20 M2 bending modes (mode amplitudes in micrometers). The deployed configuration (used_dofs in MTAOS/v13/_init.yaml) uses \(n = 20\): the 10 hexapod DOFs, M1M3 bending modes 1–5 (DOF indices 10–14) and M2 bending modes 1–5 (DOF indices 30–34). Earlier configurations used 22 DOFs (7 M1M3 modes). All dimensions in this note assume the deployed 20.

From the double Zernike table to the matrix A#

The sensitivity matrix is not stored as the \(m \times n\) matrix \(\mathbf{A}\) of the forward model. It is stored as a double Zernike coefficient array of shape \(31 \times 29 \times 50\) (sensitivity_matrix/lsst_sensitivity_dz_31_29_50.yaml):

  • the first axis runs over 31 field Zernike terms (Noll indices 0–30), describing how an aberration varies across the field of view;

  • the second axis runs over 29 pupil Zernike terms (Noll indices 0–28), the aberration itself;

  • the third axis runs over the 50 DOFs.

Index 0 of each axis is unused by convention, so the pupil terms are Z1–Z28, which is why znmax = 28. For each DOF the \(31 \times 29\) slice is a double Zernike: a function of field position \((u, v)\) that returns the vector of pupil Zernike coefficients produced by a unit change of that DOF at that field position. This single array describes the optical response over the whole field of view, independently of where the sensors are.

Note

The header comment of the YAML file labels the first two axes the other way round. The generator script generate_sensitivity_matrix.py and the galsim DoubleZernike convention used to evaluate the table both put the field index first, which is the convention used here.

The code turns this 3-D table into a 2-D matrix in two different ways, for two different purposes.

Path 1: evaluate at the sensors (the A that is inverted). This is done for every exposure by SensitivityMatrix.evaluate and StateEstimator.get_sensitivity_matrix:

  1. Evaluate the double Zernike of each DOF at the field position of each sensor (sample_points, rotated by minus the rotator angle, see Measurement preprocessing). The 31-term field axis disappears and is replaced by one pupil Zernike vector per sensor: \((4, 29, 50)\).

  2. Keep pupil terms Z4–Z28, then select zn_selected: \((4, 19, 50)\). The deployed controller file selects Z4–Z22; in the closed loop, MTAOS overrides this with the indices that WEP actually produced.

  3. Flatten the sensor and Zernike axes into a single row index: \((4 \times 19, 50) = (76, 50)\). Rows are sensor-major, so rows 0–18 are Z4–Z22 of the first sensor, rows 19–37 those of the second sensor, and so on. The wavefront vector \(\mathbf{y}\) and the noise covariance \(\mathbf{C}_n\) are ordered the same way.

  4. Select the columns of the active DOFs \(\mathcal{D}\): \((76, 20)\).

The result is the matrix \(\mathbf{A} \in \mathbb{R}^{m \times n}\) of the forward model. This is the matrix that is normalized, projected onto the v-modes, whitened and inverted in State estimation: the inverse problem. Because it is built from the list of sensors that reported, it shrinks automatically when a corner is missing, for example to \(57 \times 20\).

Path 2: flatten the table (the matrix that defines the v-modes). This is done once per configuration by StateEstimator._update_from_ofc_data and involves no evaluation at all. The table is reshaped so that every (field term, pupil term) coefficient pair becomes a row:

\[(31, 29, 50) \;\rightarrow\; (31 \times 29,\, 50) = (899, 50) \;\rightarrow\; (899, 20)\]

where the last step selects the active DOFs. The field axis is kept as coefficients rather than collapsed onto sensor positions, so this matrix describes the response over the whole field. Its SVD defines the v-modes (see Definition of v-modes), which is why they do not depend on the rotator angle, on how many sensors reported, or on the Zernike subset. Rows corresponding to index 0 of either axis, or to the piston, tip and tilt pupil terms, are zero; they are harmless in the SVD.

In short, the per-exposure \(76 \times 20\) matrix answers “what do these sensors see?”, and the \(899 \times 20\) matrix answers “what does the whole field see?”. The first is inverted. The second only chooses the basis in which the inversion is done.

DOF normalization#

The physical DOFs span very different scales. Hexapod translations are in micrometers and hexapod rotations in arcseconds. Bending mode amplitudes are also in micrometers, but their optical leverage varies widely. To give each DOF comparable influence in the inversion, we introduce a diagonal normalization matrix:

\[\mathbf{N} = \operatorname{diag}\!\bigl(w_1, w_2, \ldots, w_n\bigr)\]

where the weights \(w_i\) are read from the normalization_weights file named in the controller configuration. The deployed init.yaml uses range0.5_fwhm-0.15.yaml. Each weight combines a range-based factor and an FWHM-based factor, \(w_i = r_i^{\alpha} f_i^{\beta}\), with the exponents encoded in the file name.

This normalization follows Megias Homar et al. 2024 (ApJ 974, 108; also available as arXiv:2406.04656), Eqs. 9 and 11. The paper calls the matrix \(\boldsymbol{\Omega}\) and defines each diagonal entry as the operational range of the DOF times its effect on the FWHM.

The normalized sensitivity matrix and normalized state are:

\[\tilde{\mathbf{A}} = \mathbf{A}\,\mathbf{N}, \qquad \tilde{\mathbf{x}} = \mathbf{N}^{-1}\,\mathbf{x}\]

so that \(\mathbf{y} = \mathbf{A}\,\mathbf{x} = \tilde{\mathbf{A}}\,\tilde{\mathbf{x}}\). All estimation is done in the normalized space, and the result is de-normalized with \(\mathbf{x} = \mathbf{N}\,\tilde{\mathbf{x}}\) at the end.

Measurement preprocessing#

Before the inversion, the state estimator (StateEstimator.dof_state) brings the measurement and the model into the same frame and removes known contributions from the measurement.

Camera rotation#

WEP reports the Zernikes in the camera frame, while the double Zernike model is defined in the telescope (field) frame. Let \(\theta = \theta_\mathrm{rot} + \theta_\mathrm{off}\), where \(\theta_\mathrm{rot}\) is the camera rotator angle at the time of the exposure (passed by MTAOS as rotation_angle) and \(\theta_\mathrm{off}\) is the rotation_offset of the controller file. The offset is 0 degrees in the deployed init.yaml; the packaged pid_controller.yaml uses 90 degrees. Two things are rotated:

  1. The measurement. Each sensor’s Zernike vector is rotated by \(\theta\) as a pupil Zernike series (galsim.zernike.Zernike.rotate). Rotating a Zernike series mixes the \(\pm m\) pairs (e.g. Z5/Z6, Z7/Z8) and leaves the rotationally symmetric terms (Z4, Z11, Z22) unchanged.

  2. The model. The sensor field angles are rotated by \(-\theta\) before the double Zernike is evaluated, so that \(\mathbf{A}\) describes where the sensors actually were on the sky. The intrinsic Zernikes are evaluated at the same rotated positions.

Subtraction of known terms#

The residual wavefront error used as input to the inversion is:

\[\mathbf{y} = \mathbf{y}_\mathrm{meas} - \mathbf{y}_\mathrm{intrinsic} - \mathbf{y}_2\]

These subtractions operate in measurement space (Zernike coefficients at each sensor, \(\mathbf{y} \in \mathbb{R}^m\)):

  • \(\mathbf{y}_\mathrm{intrinsic}\) contains the design (intrinsic) Zernike coefficients for the current filter and sensor positions. They represent the wavefront error that the optical design produces even when the optics are perfectly aligned (\(\mathbf{x} = \mathbf{0}\)). They are subtracted only when subtract_intrinsics = True. In the deployed closed loop this flag is False, because the deviation_columns read from the WEP table already have the intrinsics removed (see Scope and conventions). Setting the flag to True in that case would subtract them twice.

  • \(\mathbf{y}_2\) is a static per-sensor correction in measurement space, read from the instrument’s y2 file (lsst_y2.yaml). In the deployed file, the corner sensors carry a focus (Z4) offset of -0.11 µm (R00, R04, R40) or -0.19 µm (R44). All four corners also carry -0.07 µm of spherical aberration (Z11) and +0.13 µm of Z14. The science sensors are zero. These values were determined from on-sky data as the residuals at which the best image quality is achieved.

These subtractions ensure that the residual \(\mathbf{y}\) is close to zero when the telescope is at the desired alignment state (\(\mathbf{x} = \mathbf{0}\) in DOF space). The state estimator then inverts \(\mathbf{y}\) to produce the DOF estimate \(\hat{\mathbf{x}}\), and the PID controller drives that estimate to a setpoint of zero (see PID control). The full chain is therefore:

  1. Rotate the measurement and the model into a common frame.

  2. Subtract known contributions in y-space: \(\mathbf{y} = \mathbf{y}_\mathrm{meas} - \mathbf{y}_\mathrm{intrinsic} - \mathbf{y}_2\) (or without the intrinsic term when WEP already removed it).

  3. Estimate the DOF state: \(\hat{\mathbf{x}} = f(\mathbf{y})\).

  4. Let the PID drive \(\hat{\mathbf{x}} \to \mathbf{0}\), the setpoint in x-space.

The y-space subtractions and the x-space setpoint work together. The subtractions define what “zero misalignment” looks like in measurement space, and the PID targets zero in DOF space. If the \(\mathbf{y}_2\) values were wrong, for example if the focus offsets did not correspond to the best image quality, the system would converge to a suboptimal state even though the PID error would be zero.

State estimation: the inverse problem#

Given the residual wavefront error \(\mathbf{y}\), the goal is to estimate the DOF state \(\hat{\mathbf{x}}\). Including measurement noise, the forward model in normalized coordinates reads:

\[\mathbf{y} = \tilde{\mathbf{A}}\,\tilde{\mathbf{x}} + \mathbf{n}\]

where \(\tilde{\mathbf{A}}\) is the normalized sensitivity matrix, \(\tilde{\mathbf{x}}\) the normalized DOF state and \(\mathbf{n}\) measurement noise with covariance \(\mathbf{C}_n = \mathbb{E}[\mathbf{n}\mathbf{n}^T]\). This is an overdetermined linear inverse problem (\(m > n\)), solved by regularized least squares. Two regularization ideas are combined: truncation, which restricts the solution to the well-constrained directions of DOF space, and noise weighting, which down-weights noisy measurements. Both are introduced below in their textbook form, followed by what the code actually does.

Noise covariance#

In the definition of \(\mathbf{C}_n\) above, \(\mathbb{E}[\cdot]\) denotes the expectation operator (the statistical mean), and \(\mathbf{n}\mathbf{n}^T\) is the outer product of the noise vector with itself. The covariance matrix \(\mathbf{C}_n\) therefore contains:

  • Diagonal entries \((\mathbf{C}_n)_{ii} = \mathbb{E}[n_i^2] = \sigma_i^2\): the variance of the \(i\)-th Zernike measurement.

  • Off-diagonal entries \((\mathbf{C}_n)_{ij} = \mathbb{E}[n_i n_j]\): the covariance between Zernikes \(i\) and \(j\).

In practice, \(\mathbf{C}_n\) can be estimated from stability data by computing the sample covariance of the residual Zernikes across \(N\) exposures:

\[\hat{\mathbf{C}}_n = \frac{1}{N-1} \sum_{k=1}^{N} (\mathbf{y}_k - \bar{\mathbf{y}}) (\mathbf{y}_k - \bar{\mathbf{y}})^T\]

where \(\mathbf{y}_k\) are the measured Zernike vectors and \(\bar{\mathbf{y}}\) is their mean. See SITCOMTN-177 for how this covariance is estimated from on-sky data.

Note

The deployed configuration does not name a noise_covariance_filename, and the only file shipped in ts_config_mttcs (default_noise_covariance.yaml) is an identity matrix. The noise-weighting path is therefore implemented but has no effect on the summit today. Running the loop with a non-identity covariance has not been validated yet. It will be validated with the OFC/PID simulator, currently under development, before it is enabled.

Approach 1: SVD with truncation#

The standard least-squares solution uses the pseudo-inverse. Start by computing the singular value decomposition (SVD):

\[\tilde{\mathbf{A}} = \mathbf{U}\,\operatorname{diag}(\boldsymbol{\Sigma})\,\mathbf{V}_h\]

In the thin (economy) SVD of an \(m \times n\) matrix with \(m > n\):

  • \(\mathbf{U} \in \mathbb{R}^{m \times n}\) holds the left singular vectors as columns,

  • \(\boldsymbol{\Sigma} \in \mathbb{R}^{n}\) is the vector of singular values \((\sigma_1, \ldots, \sigma_n)\), ordered \(\sigma_1 \geq \sigma_2 \geq \cdots \geq \sigma_n \geq 0\),

  • \(\mathbf{V}_h \in \mathbb{R}^{n \times n}\) is an orthogonal matrix whose rows are the right singular vectors.

The pseudo-inverse is:

\[\tilde{\mathbf{A}}^{+} = \mathbf{V}_h^T\,\operatorname{diag}(1/\boldsymbol{\Sigma})\,\mathbf{U}^T\]

Truncation regularization. Small singular values amplify noise because \(1/\sigma_i \to \infty\) as \(\sigma_i \to 0\). To regularize, keep only the \(r\) largest singular values:

\[\tilde{\mathbf{A}}_r^{+} = \mathbf{V}_{h,r}^T\,\operatorname{diag}(1/\boldsymbol{\Sigma}_r)\,\mathbf{U}_r^T\]

where \(\mathbf{U}_r\) contains the first \(r\) columns of \(\mathbf{U}\), \(\mathbf{V}_{h,r}\) the first \(r\) rows of \(\mathbf{V}_h\), and \(\boldsymbol{\Sigma}_r\) the first \(r\) singular values. The truncated solution \(\tilde{\mathbf{x}}_\mathrm{trunc} = \tilde{\mathbf{A}}_r^{+}\,\mathbf{y}\) discards the \(n - r\) least-constrained DOF combinations and lies entirely in the subspace spanned by the first \(r\) right singular vectors.

Approach 2: Noise-weighted least squares#

When the measurement noise covariance \(\mathbf{C}_n\) is known and non-uniform, we minimize the weighted least-squares objective:

\[\min_{\tilde{\mathbf{x}}} \; \bigl(\mathbf{y} - \tilde{\mathbf{A}}\,\tilde{\mathbf{x}}\bigr)^T \mathbf{C}_n^{-1} \bigl(\mathbf{y} - \tilde{\mathbf{A}}\,\tilde{\mathbf{x}}\bigr)\]

Equivalently, whiten the problem by defining:

\[\bar{\mathbf{y}} = \mathbf{C}_n^{-1/2}\,\mathbf{y}, \qquad \bar{\mathbf{A}} = \mathbf{C}_n^{-1/2}\,\tilde{\mathbf{A}}\]

and solve the ordinary least-squares problem \(\tilde{\mathbf{x}}_\mathrm{wls} = \bar{\mathbf{A}}^{+}\,\bar{\mathbf{y}}\). Whitening down-weights the Zernike terms and sensors with the largest measurement uncertainty.

What ts_ofc actually computes#

The implementation combines the two ideas, but not by taking the truncated SVD of the per-exposure matrix. Instead, the truncation subspace is fixed once from the full-field optical model, and the least-squares fit is done inside that subspace using the per-exposure matrix. The steps in StateEstimator are as follows.

Step 0 – Define the v-modes (once per configuration). Take the normalized double Zernike sensitivity array, reshaped to one row per (field term, pupil term) coefficient pair (Path 2 in From the double Zernike table to A), and restricted to the active DOFs: \(\tilde{\mathbf{A}}_\mathrm{full} \in \mathbb{R}^{899 \times n}\). Its thin SVD,

\[\tilde{\mathbf{A}}_\mathrm{full} = \mathbf{U}\,\operatorname{diag}(\boldsymbol{\Sigma})\,\mathbf{V}_h ,\]

defines the v-modes as the rows of \(\mathbf{V}_h\). This happens in StateEstimator._update_from_ofc_data and does not depend on which sensors reported, on the rotation angle, or on zn_selected.

Step 1 – Project the per-exposure matrix onto the first r v-modes. With \(\tilde{\mathbf{A}} \in \mathbb{R}^{m \times n}\) the normalized sensitivity matrix evaluated at the (rotated) sensor positions (Path 1 in From the double Zernike table to A):

\[\tilde{\mathbf{A}}_v = \tilde{\mathbf{A}}\,\mathbf{V}_{h,r}^T \;\in\; \mathbb{R}^{m \times r}\]

Column \(i\) of \(\tilde{\mathbf{A}}_v\) is the wavefront pattern seen by the sensors when v-mode \(i\) is commanded with unit amplitude.

Step 2 – Whiten by the noise covariance.

\[\bar{\mathbf{y}} = \mathbf{C}_n^{-1/2}\,\mathbf{y}, \qquad \bar{\mathbf{A}}_v = \mathbf{C}_n^{-1/2}\,\tilde{\mathbf{A}}_v\]

The inverse square root is computed with scipy.linalg.fractional_matrix_power. In the deployed configuration \(\mathbf{C}_n\) is the identity, so this step changes nothing.

Step 3 – Ordinary least squares in v-mode space.

\[\hat{\mathbf{v}} = \arg\min_{\mathbf{v} \in \mathbb{R}^{r}} \bigl\lVert \bar{\mathbf{A}}_v\,\mathbf{v} - \bar{\mathbf{y}} \bigr\rVert^2 = \bar{\mathbf{A}}_v^{+}\,\bar{\mathbf{y}}\]

This is solved with numpy.linalg.lstsq, without any further truncation. The result has \(r\) components, one per retained v-mode.

Step 4 – Return the state in the requested basis.

  • DOF basis (control_vmodes = False, the deployed setting):

    \[\hat{\mathbf{x}} = \mathbf{N}\,\mathbf{V}_{h,r}^T\,\hat{\mathbf{v}} \;\in\; \mathbb{R}^{n}\]

    The \(r\) v-mode amplitudes are expanded into the \(n\) DOFs and then de-normalized. The estimate lies entirely in the subspace spanned by the first \(r\) v-modes.

  • V-mode basis (control_vmodes = True): \(\hat{\mathbf{v}}\) is zero-padded from \(r\) to \(n\) elements and returned as is.

In compact form, for the DOF basis:

\[\hat{\mathbf{x}} = \mathbf{N}\,\mathbf{V}_{h,r}^T\, \bigl(\mathbf{C}_n^{-1/2}\,\tilde{\mathbf{A}}\,\mathbf{V}_{h,r}^T\bigr)^{+}\, \mathbf{C}_n^{-1/2}\,\mathbf{y}\]

Relation to the textbook forms. If the v-modes had been computed from the per-exposure \(\tilde{\mathbf{A}}\) itself and \(\mathbf{C}_n = \mathbf{I}\), the expression above would reduce exactly to the truncated pseudo-inverse of Approach 1. Because the v-modes come from the full-field model instead, the implementation is best read as fitting the measurements with the best linear combination of the first r full-field optical eigenmodes. A practical advantage is that the meaning of “v-mode \(i\)” does not change from one exposure to the next, which is what makes per-mode gains well defined (see PID in v-mode space). Note also that the projection is applied before the whitening. With a non-identity \(\mathbf{C}_n\), the result is therefore not the same as whitening first and truncating afterwards.

For background, see Megias Homar et al. 2024. Eq. 2 gives the SVD pseudo-inverse, Eqs. 4–6 the noise amplification that motivates truncation, Eqs. 7–8 the truncation criterion and the truncated effective response, and Eqs. 9–10 the rescaled (normalized) problem and its de-normalized estimate.

Concrete example#

Consider the deployed configuration, with \(m = 76\) measurements (4 corners × 19 Zernikes), \(n = 20\) DOFs and truncation_index = 12 (\(r = 12\)):

  • \(\mathbf{V}_h \in \mathbb{R}^{20 \times 20}\) from the SVD of the \(899 \times 20\) full-field matrix; \(\mathbf{V}_{h,12} \in \mathbb{R}^{12 \times 20}\).

  • \(\tilde{\mathbf{A}} \in \mathbb{R}^{76 \times 20}\), \(\tilde{\mathbf{A}}_v = \tilde{\mathbf{A}}\,\mathbf{V}_{h,12}^T \in \mathbb{R}^{76 \times 12}\).

  • \(\hat{\mathbf{v}} = \bar{\mathbf{A}}_v^{+}\,\bar{\mathbf{y}} \in \mathbb{R}^{12}\).

  • \(\hat{\mathbf{x}} = \mathbf{N}\,\mathbf{V}_{h,12}^T\,\hat{\mathbf{v}} \in \mathbb{R}^{20}\).

With dimensions written out:

\[\underbrace{\hat{\mathbf{x}}}_{20 \times 1} = \underbrace{\mathbf{N}}_{20 \times 20}\, \underbrace{\mathbf{V}_{h,12}^T}_{20 \times 12}\, \underbrace{\bar{\mathbf{A}}_v^{+}}_{12 \times 76}\, \underbrace{\bar{\mathbf{y}}}_{76 \times 1}\]

The output has 20 components, one per DOF, but the data constrain only 12 independent directions. The amplitudes of v-modes 12–19 are identically zero in the estimate.

Definition of v-modes#

The v-modes are the right singular vectors of the normalized full-field sensitivity matrix \(\tilde{\mathbf{A}}_\mathrm{full} \in \mathbb{R}^{899 \times n}\) (Step 0 above). From its thin SVD,

\[\tilde{\mathbf{A}}_\mathrm{full} = \mathbf{U}\,\operatorname{diag}(\boldsymbol{\Sigma})\,\mathbf{V}_h ,\]

the rows of \(\mathbf{V}_h \in \mathbb{R}^{n \times n}\) form an orthonormal basis for the normalized DOF space. Row \(i\) of \(\mathbf{V}_h\) is v-mode \(i\). These are the “characteristic modes” \(\tilde{\mathbf{v}}_i\) of Megias Homar et al. 2024 (Eq. 2 for the unscaled matrix, Eq. 10 for the rescaled one).

Each v-mode is a linear combination of all (normalized) DOFs. Concretely, v-mode \(i\) is the vector \(\mathbf{v}_i = (\mathbf{V}_h)_{i,:} \in \mathbb{R}^{n}\), which specifies a coordinated adjustment of hexapod positions and bending modes.

The associated singular value \(\sigma_i\) quantifies the optical leverage of v-mode \(i\) over the whole field: the amount of wavefront change (in double Zernike amplitude) produced per unit of that DOF combination. Because the singular values are sorted in decreasing order, v-mode 0 is the most optically important mode and v-mode \(n{-}1\) is the least.

The number of v-modes equals the number of used DOFs. With \(n = 20\) used DOFs, \(\mathbf{V}_h \in \mathbb{R}^{20 \times 20}\) and there are exactly 20 v-modes.

V-modes are a property of the optical model, not of the exposure. They are computed from the whole-field double Zernike array and are therefore independent of the number of sensors that reported, of the rotator angle, and of the Zernike subset in use. They change only when the sensitivity matrix file, the normalization weights, or the set of used DOFs changes.

Coordinate transformations#

The code provides two conversions, both restricted to the \(r\) retained modes.

DOFs to v-modes (StateEstimator.get_vmodes_from_dofs). Given a DOF vector \(\mathbf{x}\):

\[\mathbf{v} = \mathbf{V}_{h,r}\,\mathbf{N}^{-1}\,\mathbf{x}_{\mathcal{D}} \;\in\; \mathbb{R}^{r}\]

V-modes to DOFs (StateEstimator.get_dofs_from_vmodes). Given a v-mode vector \(\mathbf{v}\) (of length \(r\) or longer; only the first \(r\) entries are used):

\[\mathbf{x} = \mathbf{N}\,\mathbf{V}_{h,r}^{T}\,\mathbf{v}_{1:r} \;\in\; \mathbb{R}^{n}\]

Because \(\mathbf{V}_{h,r}\mathbf{V}_{h,r}^{T} = \mathbf{I}_r\), going from v-modes to DOFs and back is lossless. Going from DOFs to v-modes and back is lossless only for DOF vectors that already lie in the span of the first \(r\) v-modes; any component along the discarded modes is removed. With \(r = n\) both directions are exact.

Both methods live in state_estimator.py.

Physical interpretation of v-modes#

Although each v-mode is a linear combination of all DOFs, it produces a characteristic wavefront pattern, described by the corresponding left singular vector \(\mathbf{u}_i = \mathbf{U}_{:,i}\). Since the SVD is taken over the full-field array, \(\mathbf{u}_i\) is a set of double Zernike coefficients. It describes both which pupil aberration the mode produces and how that aberration varies across the field.

If \(\mathbf{u}_i\) has most of its energy in the coefficients of a particular pupil Zernike, say focus (Z4), then v-mode \(i\) is effectively “the focus mode”: the coordinated DOF adjustment that most efficiently changes the telescope’s focus. Similarly, if \(\mathbf{u}_j\) is dominated by spherical aberration (Z11), then v-mode \(j\) is “the spherical mode.”

This is why operators associate specific v-modes with specific aberrations:

  • V-mode \(i\) is named after the dominant pupil Zernike in \(\mathbf{u}_i\).

  • The association is approximate. Each v-mode typically produces a dominant Zernike plus smaller contributions to others, and the name ignores the field dependence.

  • The SVD discovers these “optical eigenmodes” automatically from the sensitivity matrix; they are not prescribed by the user.

Truncation and v-mode importance#

The singular values \(\sigma_i\) determine which v-modes are well-constrained by the wavefront measurements:

  • V-modes \(0\) through \(r{-}1\) (large \(\sigma_i\)): these DOF combinations produce large, easily measured wavefront changes. The state estimator can reliably reconstruct their amplitudes.

  • V-modes \(r\) through \(n{-}1\) (small \(\sigma_i\)): these combinations produce tiny wavefront changes that are buried in measurement noise. They are excluded from the fit.

With truncation_index = 12 and \(n = 20\) DOFs, the first 12 v-modes are retained and the remaining 8 are never estimated. The state vector handed to the controller still has \(n\) entries in either basis. However, its components along the discarded modes are exactly zero, so the controller never commands them.

PID control#

The PID controller computes a correction \(\mathbf{u}_k\) at each iteration \(k\) based on the estimated state. The correction is applied with negative feedback: the telescope moves in the opposite direction of the estimated error.

The overall flow in OFC.calculate_corrections() is:

  1. Estimate the optical state \(\hat{\mathbf{x}}_k\) (or \(\hat{\mathbf{v}}_k\)) via the state estimator.

  2. Pass it to the PID controller, which computes an internal control signal \(\mathbf{u}_k^{\mathrm{pid}}\).

  3. Negate the control signal: uk = -controller.control_step(...).

  4. If controlling in v-mode space, convert \(\mathbf{u}_k\) back to DOFs.

  5. Aggregate the correction into the telescope state.

The negation in step 3 ensures that if the estimated state indicates a positive defocus, the correction drives it back toward zero.

Note

ts_ofc also provides an Optimal Integral Controller (OICController). Instead of a proportional step on the estimated state, it chooses the correction that minimizes a quadratic cost combining the predicted image-quality-weighted wavefront error with a penalty on actuator motion, referenced to the initial state (xref = x00, x0 or 0); see Eq. 12 of Megias Homar et al. 2024. The name key of the controller file selects the controller, and the deployed init.yaml uses name: PID. The OIC is out of scope for this note.

The setpoint: what the PID targets#

The PID controller drives the estimated state toward a setpoint vector \(\mathbf{s} \in \mathbb{R}^{50}\), which specifies the desired value for each of the 50 possible degrees of freedom. In the deployed configuration all setpoint entries are zero:

\[\mathbf{s} = \mathbf{0}\]

Only the entries \(\mathbf{s}_{\mathcal{D}}\) that correspond to the active DOFs are used. A zero setpoint means the controller targets a state where all DOF deviations are zero, that is, the optics are in their nominal alignment. A non-zero setpoint would let the controller target a deliberate offset, for example an intentional defocus for engineering purposes.

The effective target of the closed loop is not simply “zero physical DOFs.” The PID setpoint lives in DOF space, but what “zero” means depends on the measurement preprocessing (see Measurement preprocessing). The system converges when \(\hat{\mathbf{x}} = \mathbf{0}\). This corresponds to a residual \(\mathbf{y} \approx \mathbf{0}\), and therefore to \(\mathbf{y}_\mathrm{meas} \approx \mathbf{y}_\mathrm{intrinsic} + \mathbf{y}_2\). The telescope is thus driven to the state where its measured wavefront matches the intrinsic design prediction plus the empirical \(\mathbf{y}_2\) offsets. That is the state of best image quality, as determined from on-sky calibration.

PID formula (as implemented)#

At each iteration \(k\), the controller (BaseController.calculate_pid_step) receives the estimated state \(\hat{\mathbf{x}}_k\) and computes the terms below. The formulas are the same in DOF space and in v-mode space. Note that Megias Homar et al. 2024 write the PID in incremental (velocity) form in their Eq. 13. The implementation uses the positional form given here, which acts on the error with respect to the setpoint.

Error signal:

\[\mathbf{e}_k = \mathbf{s}_{\mathcal{D}} - \hat{\mathbf{x}}_k\]

Integral term. Two variants exist, selected by use_leaky_integrator. The deployed configuration uses the leaky one.

Leaky integrator (use_leaky_integrator = true). This is a finite-history sum over the last \(n_I\) errors (n_iterms), in which older errors are down-weighted geometrically by \(\lambda\) (i_factor):

\[\mathbf{I}_k^{\mathrm{raw}} = \sum_{j=0}^{\min(k,\,n_I)-1} \lambda^{j}\,\mathbf{e}_{k-j}\]

The newest error has weight 1, the previous one \(\lambda\), the one before that \(\lambda^2\), and so on. With the deployed values \(n_I = 10\) and \(\lambda = 0.5\), a constant error gives a sum bounded by \(\lVert\mathbf{e}\rVert/(1-\lambda) = 2\,\lVert\mathbf{e}\rVert\). The integral therefore cannot wind up indefinitely, even before clipping.

Plain integrator (use_leaky_integrator = false). The errors are accumulated without decay:

\[\mathbf{I}_k^{\mathrm{raw}} = \mathbf{I}_{k-1} + \mathbf{e}_k\]

Anti-windup clipping. Both variants are then clipped:

\[\mathbf{I}_k = \operatorname{clip}\!\bigl( \mathbf{I}_k^{\mathrm{raw}},\; -\mathbf{I}_{\max,\mathcal{D}},\; +\mathbf{I}_{\max,\mathcal{D}} \bigr)\]

where \(\mathbf{I}_{\max}\) is the max_integral vector from the controller file. The deployed init.yaml sets per-DOF values (see Deployed configuration); the code defaults to ones when the key is absent.

Derivative with low-pass filter:

\[\mathbf{d}_k^{\mathrm{raw}} = \mathbf{e}_k - \mathbf{e}_{k-1}\]
\[\mathbf{d}_k = \alpha\,\mathbf{d}_k^{\mathrm{raw}} + (1 - \alpha)\,\mathbf{d}_{k-1}\]

where \(\alpha\) is the derivative_filter_coeff. When \(\alpha = 1\) (the deployed value), no filtering is applied and the derivative is simply the difference between consecutive errors. When \(\alpha < 1\), the derivative is smoothed with an exponential moving average to reduce sensitivity to noise. After a history reset, \(\mathbf{e}_{k-1}\) and \(\mathbf{d}_{k-1}\) are zero.

Control output:

\[\mathbf{u}_k^{\mathrm{pid}} = \mathbf{K}_{p,\mathcal{D}}\,\mathbf{e}_k + \mathbf{K}_{i,\mathcal{D}}\,\mathbf{I}_k + \mathbf{K}_{d,\mathcal{D}}\,\mathbf{d}_k\]

where \(\mathbf{K}_p\), \(\mathbf{K}_i\) and \(\mathbf{K}_d\) are diagonal gain matrices built from kp, ki and kd. Each can be a scalar, applied uniformly to all DOFs, or a 50-element array for per-DOF tuning. The subscript \(\mathcal{D}\) selects the active entries.

The final correction applied to the telescope is:

\[\mathbf{u}_k = -\mathbf{u}_k^{\mathrm{pid}}\]

This negation, applied in OFC.calculate_corrections(), turns the PID output, which points from the state toward the setpoint, into a physical correction that drives the optics back toward the setpoint.

Deployed configuration#

The controller parameters from MTAOS/ofc/configurations/init.yaml in ts_config_mttcs:

Parameter

Deployed value

Description

name

PID

Controller type (PID or OIC)

kp

0.30 (all DOFs)

Proportional gain (scalar or per-DOF array)

ki

0.05 (all DOFs)

Integral gain

kd

0.0

Derivative gain (disabled)

derivative_filter_coeff

1.0

Low-pass filter coefficient (1.0 = no filtering)

use_leaky_integrator

true

Finite-history integral with geometric decay

n_iterms

10

Number of error terms kept in the leaky integral

i_factor

0.5

Decay factor \(\lambda\) per iteration of age

setpoint

all zeros (50 elements)

Target state for each DOF

max_integral

hexapods: 500, 100, 100, 0.01, 0.01; bending modes 1–10: 0.5; modes 11–20: 0

Anti-windup clipping limit per DOF (µm or arcsec)

zn_selected

Z4–Z22

Zernikes used (overridden by WEP output in the closed loop)

normalization_weights_filename

range0.5_fwhm-0.15.yaml

DOF normalization weights

truncation_index

12

Number of v-modes retained (\(r\))

rotation_offset

0.0 deg

Added to the rotator angle before de-rotation

(noise_covariance_filename)

absent

Identity covariance is used

Physical meaning of the gains. \(k_p = 0.30\) means “correct 30% of the estimated error per iteration”. With \(k_i = 0.05\) and the leaky integral bounded by twice the error, the integral term adds at most another 10% of a persistent error. For a constant error, the correction per iteration therefore approaches 40% of that error. With \(k_d = 0\) there is no derivative action. MTAOS can also override kp for an individual image (user gain, elevation scaling) and all three gains after a filter change; see Where OFC sits in the closed loop.

The packaged pid_controller.yaml in ts_ofc (kp = 0.7, ki = 0, kd = 0.1) is not what runs on the summit.

Controller state and history#

The controller maintains internal state between iterations:

  • aggregated_state (\(\mathbf{x}_\mathrm{agg}\)): the cumulative correction applied to the telescope, initialized from dof_state0.

  • previous_error: a queue of the last \(n_I\) errors, used by both the leaky integral and the derivative.

  • integral (\(\mathbf{I}\)): the clipped integral; the unclipped value is kept alongside for diagnostics.

  • filtered_derivative (\(\mathbf{d}_{k-1}\)): filtered derivative from the previous step.

The reset_history() method:

  1. Saves the current aggregated state as the new reference: \(\mathbf{x}_0 \leftarrow \mathbf{x}_\mathrm{agg}\).

  2. Empties the error queue and zeros the integral and derivative terms.

It is called when the closed loop is started or stopped with a new comp_dof_idx configuration, so that the PID restarts from the current telescope state.

PID in DOF space#

When control_vmodes = False (the deployed setting), the state estimator returns the optical state in DOF space:

\[\hat{\mathbf{x}}_k = \mathbf{N}\,\mathbf{V}_{h,r}^T\,\hat{\mathbf{v}}_k\]

The PID operates on this \(n\)-dimensional state vector. Each gain entry corresponds directly to a physical DOF.

Physical meaning of DOF gains. Each diagonal entry of \(\mathbf{K}_p\) sets how aggressively one physical DOF is corrected. For example, \(k_{p,0} = 0.30\) means “correct 30% of the estimated M2 hexapod dZ error per iteration”.

PID in v-mode space#

When control_vmodes = True, the state estimator returns the optical state in v-mode space, zero-padded to \(n\) elements:

\[\hat{\mathbf{v}}_k = \bigl(\hat{v}_{k,1}, \ldots, \hat{v}_{k,r}, 0, \ldots, 0\bigr)\]

The PID then operates on this vector with the same calculate_pid_step() method. The controller does not know whether it is operating on DOFs or v-modes. The error, integral and derivative are all computed in v-mode space:

\[\mathbf{e}^{(v)}_k = \mathbf{s}_{\mathcal{D}} - \hat{\mathbf{v}}_k\]
\[\mathbf{u}^{(v)}_k = \mathbf{K}_{p,\mathcal{D}}\,\mathbf{e}^{(v)}_k + \mathbf{K}_{i,\mathcal{D}}\,\mathbf{I}^{(v)}_k + \mathbf{K}_{d,\mathcal{D}}\,\mathbf{d}^{(v)}_k\]

After the PID produces \(\mathbf{u}^{(v)}_k\), OFC converts it back to physical DOF corrections with get_dofs_from_vmodes():

\[\mathbf{u}_k = -\mathbf{N}\,\mathbf{V}_{h,r}^{T}\,\mathbf{u}^{(v)}_{k,1:r}\]

Only the first \(r\) entries of the v-mode correction are used.

Important implementation detail. The PID uses the same gain arrays, indexed by \(\mathcal{D}\), regardless of the basis. The gain at position \(i\) of the active list therefore applies to v-mode \(i\), with v-modes ordered by singular value. V-mode 0, which has the highest optical leverage, gets the gain of the first active DOF, v-mode 1 the gain of the second, and so on. The setpoint is interpreted positionally in the same way. With an all-zero setpoint this is harmless, but a non-zero setpoint only makes sense in DOF space.

Physical meaning of v-mode gains. Each entry of \(\mathbf{K}_p\) sets how aggressively one optical mode is corrected. For example, if v-mode 0 is the focus mode, giving it a higher gain makes the system correct focus errors more aggressively than higher-order aberrations.

The advantage of v-mode control is the ability to assign different gains based on optical importance:

  • High gain on well-constrained v-modes, which have large \(\sigma_i\) and reliable estimates, for fast convergence.

  • Low gain on poorly constrained v-modes, which have small \(\sigma_i\) and noisy estimates, to avoid injecting noise into the corrections.

Equivalent DOF-space gain#

Composing the three steps (DOF to v-mode, gain, v-mode to DOF), the effective DOF-space proportional gain when controlling in v-mode space is:

\[\mathbf{K}_\mathrm{eff} = \mathbf{N}\,\mathbf{V}_{h,r}^{T}\,\mathbf{K}^{(v)}_{p}\, \mathbf{V}_{h,r}\,\mathbf{N}^{-1}\]

When the v-mode gain is a uniform scalar (\(\mathbf{K}^{(v)}_p = k_p \mathbf{I}_r\)):

\[\mathbf{K}_\mathrm{eff} = k_p\,\mathbf{N}\,\mathbf{P}_r\,\mathbf{N}^{-1}, \qquad \mathbf{P}_r = \mathbf{V}_{h,r}^{T}\,\mathbf{V}_{h,r}\]

where \(\mathbf{P}_r\) is the orthogonal projector onto the retained subspace. Since the DOF-space estimate \(\hat{\mathbf{x}}_k\) already lies in that subspace, \(\mathbf{K}_\mathrm{eff}\hat{\mathbf{x}}_k = k_p\hat{\mathbf{x}}_k\). V-mode control with a uniform gain is therefore identical to DOF-space control with the same scalar gain.

When the v-mode gain has different values per mode, the matrix \(\mathbf{V}_{h,r}^{T}\,\mathbf{K}^{(v)}_p\,\mathbf{V}_{h,r}\) is a full (non-diagonal) matrix that couples the DOFs. This is the intended behavior, since correcting an optical eigenmode such as focus requires coordinated adjustments of several physical DOFs.

For example, a hypothetical two-level gain structure with \(k_p^{(v)} = 0.30\) for v-modes 0–5 and \(k_p^{(v)} = 0.15\) for v-modes 6–11 decomposes as:

\[\mathbf{K}^{(v)}_p = 0.15\,\mathbf{I}_r + 0.15\,\mathbf{P}_{6}\]

where \(\mathbf{P}_{6}\) is the projection onto the first 6 v-modes. In words, a uniform base gain is applied to all retained modes, with an extra boost on the most optically important ones.

Correction aggregation#

After each PID step, the DOF correction \(\mathbf{u}_k\) is added to the telescope state (BaseController.aggregate_state):

\[\mathbf{x}_{k+1} = \mathbf{x}_k + \mathbf{u}_k\]

Only the active entries \(\mathcal{D}\) of the 50-element aggregated state are updated. The per-visit correction \(\mathbf{u}_k\) is also stored on its own (lv_dof) so that it can be undone if a subsystem rejects it.

The aggregated state is then decomposed into physical corrections for the individual components (OFC.get_correction):

  • M2 Hexapod: DOFs 0–4 (dZ, dX, dY, rX, rY), mapped to hexapod command coordinates through the (pseudo-)inverse of the hexapod rotation matrix in comp_dof_idx.

  • Camera Hexapod: DOFs 5–9, same transformation.

  • M1M3 Bending Modes: DOFs 10–29, converted from bending mode amplitudes to actuator forces with the bending-mode-to-force influence matrix (BendModeToForce).

  • M2 Bending Modes: DOFs 30–49, same conversion.

ts_mtaos issues the resulting Correction objects to the subsystem CSCs, after the checks described in the next section.

Where OFC sits in the closed loop#

This section summarizes the parts of the MTAOS closed loop (MTAOS.run_closed_loop) that change what OFC computes or what is finally applied. The full operational description, with diagrams of the main flow and of _execute_ofc, is in the Closed Loop Operations section of the MTAOS documentation. The configuration values below come from MTAOS/v13/_init.yaml in ts_config_mttcs.

Per-image sequence#

For every image ingested by the Observatory Operations Data Service (OODS), the loop:

  1. Checks that the image is one the loop is following, and that it is not stale.

  2. Reads the filter and the mean elevation of the exposure, and the mean rotator angle recorded during the exposure.

  3. Skips the image if the telescope has moved since it was taken: more than rotation_delta_limit (9 deg) in rotator angle or more than elevation_delta_limit_max (9 deg) in elevation relative to the current position.

  4. Runs or polls the wavefront estimation (WEP) and collects the Zernikes.

  5. Computes the gain for this image (see Gain overrides). A zero gain skips the OFC step entirely.

  6. Calls _execute_ofc, which temporarily overrides the controller gains, runs Model.calculate_corrections (large-defocus check, then OFC.calculate_corrections), publishes the DOF and correction events, and restores the gains.

  7. Waits for the shutter to close, then applies the corrections (see Stress limits, force thresholds and pointing).

The OFC data parameters passed with startClosedLoop, for instance truncation_index or comp_dof_idx, are applied once for the whole session and reset the controller history. For each image, only filter_name and rotation_angle are passed through to OFC.

Gain overrides#

Three mechanisms can change the gains that the PID uses for a given image. MTAOS applies all of them on top of the controller file and restores the original gains after the call.

  • Elevation scaling. Model.get_correction_gain compares the elevation of this image with that of the previous processed image. If the change is at or above elevation_delta_limit_max the gain is zero and the image is skipped; at or below elevation_delta_limit_min the gain is the controller’s kp; in between kp is scaled linearly to zero. In the deployed configuration both limits are 9 degrees, so this is a binary skip rather than a ramp. The resulting gain replaces kp only; ki and kd are untouched.

  • User gain. The runOFC command carries a userGain. When it is non-zero, it replaces kp for that call in the same way.

  • Filter-change override. closed_loop_filter_change_gain can replace kp, ki and kd for n_iter successful OFC iterations after a filter change. The deployed default is n_iter = 0, which disables it. An optional override file enables it with n_iter = 4 and kp = 0.75.

Large-defocus bypass#

Before calling OFC, Model.calculate_corrections takes the median of the defocus \(dz\) inferred from the donut radii on the corner sensors. If \(|dz|\) exceeds dz_threshold_min (300 µm):

  • with raise_on_large_defocus = True (deployed), the computation raises an error, no correction is computed, and the failure counts towards max_ofc_consecutive_failures (3);

  • with raise_on_large_defocus = False, the value is clipped to dz_threshold_max (1500 µm). Half of it is applied directly to the M2 hexapod dZ and half to the camera hexapod dZ through offset_dof, bypassing the state estimator and the PID. The offset is still added to the aggregated state.

Stress limits, force thresholds and pointing#

When the corrections are applied (MTAOS.handle_corrections):

  • Mirror stress limits. The bending-mode stresses of the aggregated state are combined as a root sum of squares and multiplied by stress_scale_factor (1.25). If the result exceeds m1m3_stress_limit (137900 Pa) or m2_stress_limit (344737 Pa), the bending modes of that mirror are reduced. The scale approach (deployed) reduces them proportionally; the truncate approach zeroes the highest modes one by one. The aggregated state in the controller is overwritten with the reduced values, and the corrections are recomputed. This is the only place where the applied state can differ from what the PID produced.

  • Force deltas. M1M3 and M2 force commands are skipped when the change from the currently applied forces is below m1m3_min_forces_to_apply or m2_min_forces_to_apply, respectively. Hexapod commands are always issued.

  • Pointing correction. When enable_pointing_correction is true (deployed), the aggregated DOF vector is multiplied by the \(50 \times 2\) pointing_correction_matrix. The hexapod rows give a pointing origin offset in millimeters, and the remaining rows give a collimation offset in arcseconds after the plate-scale factor. Both are sent to the pointing component so that the AOS corrections do not move the field.

  • Undo on failure. If any subsystem rejects its correction, the successful ones are undone and the last-visit correction is subtracted from the aggregated state.

Summary#

The following table summarizes the key properties of DOF-space and v-mode control:

Property

DOF space

V-mode space

State vector

Physical DOFs (hexapod positions, bending modes)

Optical eigenmodes ordered by singular value

Gain meaning

Correction fraction per physical DOF

Correction fraction per optical mode

Cross-coupling

None (diagonal gain matrix)

Coordinated multi-DOF corrections

Advantage

Simple, physically intuitive

Gain tuning by optical importance

Number of modes

\(n\) = number of used DOFs (20)

\(n\) (same; from the SVD of the full-field model)

Truncation

Estimate confined to the first \(r\) v-modes

Same; only the first \(r\) v-mode corrections are converted

Deployed

Yes (control_vmodes = False)

Available (test_control_vmodes.yaml)