Closing control loops

Discuss the difference in Finesse between DC and AC closed loop control.

DC control loops - ‘lock’

DC control loops are used to manually adjust the degrees of freedom (DOFs) in an optical system based on error signal measurements, until the error signals are within a specified tolerance. Several Finesse Actions have been developed to facilitate such locking. In particular, the Action RunLocks can be used to move a system to lock. Below we describe how this Action works and show an example of its use.

Methods Used for DC Locking

RunLocks provides two methods that the user may choose from to attempt to achieve lock in a Finesse model. Such a model must include user-defined locks specified using the lock command. These locks specify the error signals that will be used in locking, the unique DOF associated with each error signal, and the gain relating each error signal/DOF pair. To describe how RunLocks works, let us denote the set of values of the error signals as the vector \(\vec{E}\), the set of values of the DOFs as the vector \(\vec{F}\), and the set of gains as \(\vec{G}\).

In the first method that a user may call when running RunLocks, henceforth named the ‘proportional’ method, each DOF responds proportionally to the error signal with which it was paired in the model. That is, it is assumed that each error signal \(E_i\) depends only on the DOF \(F_i\):

\(\Delta E_i = G_i \Delta F_i.\)

Therefore, to attempt to lock the model with this method, the value the DOFs are iteratively updated according to the following equation, where \(j\) is the iteration step:

\(F_{i, j+1} = F_{i,j} - \frac{E_{i,j}}{G_i}.\)

The iteration continues until all error signals are within tolerances or until a user-defined maximum number of iterations is reached. This method may be called within RunLocks by using the option method="proportional".

However, this method has its limitations because, in general, DOFs tend to affect multiple error signals. This can be observed by constructing a ‘gain matrix,’ which shows the derivatives of all error signals with respect to all DOFs. This gain matrix should be diagonal if the proportional method is to be used. For a real system, however, some off-diagonal elements may be even larger than the diagonal elements. Note that when using RunLocks, we assume that the model is close enough to lock that a change in any DOF induces either a linear or a negligible change in the error signals; i.e., all gains are constant or near zero throughout the locking process. The Action CheckLinearity may be used to check this assumption. If this assumption holds, we can write the sensing matrix as shown below:

\[\begin{bmatrix} \Delta E_1 \\ \Delta E_2 \\ ... \\ \Delta E_n \end{bmatrix} = \begin{bmatrix} G_{11} & G_{12} & ... & G_{1n} \\ G_{21} & G_{22} & ... & G_{2n} \\ ... & ... & ... & ... \\ G_{n1} & G_{n2} & ... & G_{nn} \end{bmatrix} \begin{bmatrix} \Delta F_1 \\ \Delta F_2 \\ ... \\ \Delta F_n \end{bmatrix} . \]

In this way, the sensing matrix can be thought as a constant Jacobian matrix. Based on this intuition, the second method that RunLocks may use applies Newton’s method for multidimensional root-finding. This method may be used with the option method="newton". At each iteration step, the DOFs are changed all together according to the following equation:

\(\vec{F}_{j+1} = \vec{F}_{j} - \textbf{G}^{-1} \vec{E}_{j}.\)

Theoretically, this method should achieve lock much faster and for a wider range of initial conditions than the proportional method. Below, we show this to be true in practice.

Example: Locking a Dual-Recycled Interferometer

Here, we present a simplified example for the locking of a dual-recycled interferometer, similar to Advanced Virgo. We compare the performance of the two locking methods of RunLocks by attempting to lock this interferometer to the dark fringe. We’ll first set up the optical geometry. This geometry uses the same three modulation frequencies as used in Advanced Virgo for the construction of error signals.

import numpy as np
import matplotlib.pyplot as plt
import finesse
from finesse.analysis.actions import RunLocks
finesse.configure(plotting=True)

kat = finesse.Model()
kat.parse(
    """
    ###########################################################################
    ###   Variables
    ###########################################################################
    var Larm 2999.8
    var Lprc 12
    var Lsrc 12
    var Mtm  40
    var prmT 0.04835
    var itmT 0.01377
    var srmT 0.4
    var lmichx 5.3662
    var lmichy 5.244
    var f1 6270777
    var f2 9*f1
    var f3 4/3*f1

    ###########################################################################
    ###   Input optics
    ###########################################################################
    l L0 125
    s l_mod1 L0.p1 eo1.p1
    mod eo1 f1 0.1 order=3
    s l_mod2 eo1.p2 eo2.p1
    mod eo2 f2 0.1 order=3
    s l_mod3 eo2.p2 eo3.p1
    mod eo3 f3 0.1 order=3
    s l_in eo3.p2 prm.p1 L=53
    #Power recycling mirror
    m prm T=prmT L=30u Rc=-1430
    s l_prc prm.p2 bs.p1 L=Lprc
    # Central beamsplitter
    bs bs T=0.5 L=0 alpha=45

    ###########################################################################
    ###   X arm
    ###########################################################################
    s lx bs.p3 itmx.p1 L=lmichx
    m itmx T=itmT L=37.5u phi=90 Rc=-1424.6
    s LX itmx.p2 etmx.p1 L=Larm
    m etmx T=5u L=37.5u phi=90 Rc=1695.0

    ###########################################################################
    ###   Y arm
    ###########################################################################
    s ly bs.p2 itmy.p1 L=lmichy
    m itmy T=itmT L=37.5u phi=0 Rc=-1424.5
    s LY itmy.p2 etmy.p1 L=Larm
    m etmy T=5u L=37.5u phi=0.0 Rc=1696.0

    ###########################################################################
    ###   Signal Recyling Cavity
    ###########################################################################
    s l_src bs.p4 srm.p1 L=Lsrc
    m srm T=srmT L=30u Rc=1430.0

    ###########################################################################
    ###   Cavities
    ###########################################################################
    # Arms
    cav cavX itmx.p2.o priority=3
    cav cavY itmy.p2.o priority=3
    # PRC
    cav cavPRW prm.p2.o via=itmx.p1.i priority=2
    cav cavPRN prm.p2.o via=itmy.p1.i priority=2
    # SRC
    cav cavSRW srm.p1.o via=itmx.p1.i priority=1
    cav cavSRN srm.p1.o via=itmy.p1.i priority=1
    """
)

Now we introduce the degrees of freedom (DOFs) that can be adjusted to reach lock, the error signals (measured via demodulation at the signal recycling and power recycling mirrors), and the locks relating the DOFs to the error signals. Gains are initially set at unity, but get set to appropriate values calculated with finite differences during RunLocks (in the case of Newton’s method, the entire sensing matrix will be calculated).

kat.parse(
    """
    ###########################################################################
    ###   Degrees of Freedom, Readouts, and Locks
    ###########################################################################
    dof DARM etmx.dofs.z -1 etmy.dofs.z +1
    dof CARM etmx.dofs.z +1 etmy.dofs.z +1
    dof MICH itmx.dofs.z -1 etmx.dofs.z -1 itmy.dofs.z +1 etmy.dofs.z +1
    dof PRCL prm.dofs.z +1
    dof SRCL srm.dofs.z -1

    readout_rf rd_sr_f1 srm.p2.o f=eo1.f phase=0 output_detectors=True
    readout_rf rd_pr_f1 prm.p1.o f=eo1.f phase=0
    readout_rf rd_pr_f2 prm.p1.o f=eo2.f phase=0
    readout_rf rd_pr_f3 prm.p1.o f=eo3.f phase=0

    lock DARM_lock rd_sr_f1.outputs.I DARM.DC 1 1e-9
    lock CARM_lock rd_pr_f1.outputs.I CARM.DC 1 1e-9
    lock MICH_lock rd_pr_f2.outputs.Q MICH.DC 1 1e-9
    lock PRCL_lock rd_pr_f3.outputs.I PRCL.DC 1 1e-9
    lock SRCL_lock rd_pr_f2.outputs.I SRCL.DC 1 1e-9
    """
)

At this point, it would typically be necessary to perform ‘pre-tuning’ steps to move the model close enough to lock for the DC control loops to succeed. Here, however, because the geometry is relatively simple and higher-order modes are not considered, the model is very close to lock as defined. We can therefore kick the model away from lock by changing one or more DOFs and then observe whether the control loops succeed in bringing the model back to lock. Below, we explore simultaneous deviations in the common arm length CARM and the differential arm length DARM, varying each from their set points over the ranges \((-0.08^\circ, 0.08^\circ)\). Here, the units of degrees mean that we are deviating the microscopic path lengths of the optical components to reach a desired phase shift: e.g., a change of \(0.01^\circ\) for a laser wavelength of 1064 nm corresponds to a length change of 29.6 pm. For each deviation, we store the number of iterations of the locking procedure required to reach lock. If the locking does not converge within the maximum number of iterations specified (100), we return None. For more complex models, it is often required to set max_iterations to 1,000 or even 10,000. But for this simple model, if convergence is not achieved within 100 iterations, it will not be achieved within 10,000.

num_points = 21
DARM_limit, CARM_limit = 0.08, 0.08
DARM_values = np.linspace(-1*DARM_limit, DARM_limit, num_points)
CARM_values = np.linspace(-1*CARM_limit, CARM_limit, num_points)
# Array to store results.
iter_list = np.zeros((2,num_points,num_points))
for ind_D, val_D in enumerate(DARM_values):
    for ind_C, val_C in enumerate(CARM_values):
        # Reset parameters to their set points
        kat_paired = kat.deepcopy()
        kat_newton = kat.deepcopy()
        # Deviate DARM and CARM from their setpoints
        kat_paired.DARM.DC = val_D
        kat_paired.CARM.DC = val_C
        kat_newton.DARM.DC = val_D
        kat_newton.CARM.DC = val_C
        # Try to lock with the paired method
        sol_paired = kat_paired.run(
            RunLocks(
                method="proportional", exception_on_fail=False,
                no_warning=True, max_iterations=100,
                display_progress=False, optimize_phase=True
            )
        )
        if sol_paired.iters > 99:
            iter_list[0, ind_D, ind_C] = None
        else:
            iter_list[0, ind_D, ind_C] = sol_paired.iters

        # Try to lock with Newton's method
        sol_newton = kat_newton.run(
            RunLocks(
                method="newton", exception_on_fail=False,
                no_warning=True, max_iterations=100,
                display_progress=False, optimize_phase=True
            )
        )
        if sol_newton.iters > 99:
            iter_list[1, ind_D, ind_C] = None
        else:
            iter_list[1, ind_D, ind_C] = sol_newton.iters

We then display the number of iterations required for each deviation. A white pixel indicates that the locking exceeded the maximum number of iterations.

fig, axs = plt.subplots(1,2)
im1 = axs[0].imshow(np.log10(iter_list[0]), extent=[-1*CARM_limit,CARM_limit,-1*DARM_limit,DARM_limit],
                    aspect=CARM_limit/DARM_limit, vmin=0, vmax=2)
im1 = axs[1].imshow(np.log10(iter_list[1]), extent=[-1*CARM_limit,CARM_limit,-1*DARM_limit,DARM_limit],
                    aspect=CARM_limit/DARM_limit, vmin=0, vmax=2)
axs[0].set_xlabel("CARM Deviation (deg)", fontsize=10)
axs[1].set_xlabel("CARM Deviation (deg)", fontsize=10)
axs[0].set_ylabel("DARM Deviation (deg)", fontsize=10)
axs[1].set_ylabel("DARM Deviation (deg)", fontsize=10)
axs[0].set_title("Paired Method",fontsize=12)
axs[1].set_title("Newton's Method")
fig.subplots_adjust(right=0.8, wspace=0.75)
cbar_ax = fig.add_axes([0.1, 0.1, 0.7, 0.4])
cbar_ax.axis('off')
cbar = fig.colorbar(im1,ax=cbar_ax, location="bottom",
             label=r"$\log_{10}$(Number of Iterations)")
../../_images/loops_3_0.svg

We observe that Newton’s method converges for a much larger fraction of this parameter space than the paired method. Moreover, for pixels in which the paired method converges, Newton’s method requires far fewer iterations.

AC control loops

AC control loops are about shaping feedback and looking and control loop stability, etc.