Using degrees of freedom

degree_of_freedom
dof
DegreeOfFreedom

The degree of freedom element represents degrees of freedom in your optical setup. It is used to control multiple parameters in your model in a coherent way.

Syntax:
dof name *node_amplitude_pairs DC=0 lock_parameters=true
Required:

name: Name of the degree of freedom.

\*node_amplitude_pairs: tuple[str, float | Symbol]: String referring to a parameter or a local degree of freedom, coupled with the amplitude with which this DOF will drive the parameter. The parameter value will be set to the product of this (symbolic) amplitude and the DOF.DC parameter. In case multiple DOFS drive the same parameter, their contributions will be summed.

Optional:

DC: DC value for the degree of freedom, by default 0

lock_parameters: Whether to ‘lock’ the parameters driven by this degree of freedom. This prevents accidental overriding of parameter values that are meant to be controlled by a dof, by default True.

The degree_of_freedom command can be useful when building control loops, but also for tuning interferometers and any other situation where you want to change multiple parameters of the model at the same time.

The typical degrees of freedom in an interferometer are [32]:

  • DARM: Differential ARM length. The output channel of the detector. Generally operated with a small DC offset relative to the dark fringe conditions. \(L_N - L_W\)

  • CARM: Common ARM length. Used to keep the arms on resonance. \(\frac{L_N + L_W}{2}\)

  • MICH: Controls the short arms of the Michelson and determines the fringe at the dark port. \(l_N - l_W\)

../../_images/dof_maggiore.png

Fig. 13 Simplified layout of AdV+

Creating a basic version of this interferometer.

import finesse
import matplotlib.pyplot as plt
from finesse.analysis.actions import Xaxis, Minimize, Noxaxis

finesse.init_plotting()

base_model = finesse.Model()
base_model.parse(
    """
    laser l1 P=1
    bs bs1 R=0.5 T=0.5

    m WI R=0.9 T=0.1
    m NI R=0.9 T=0.1

    m WE R=1 T=0
    m NE R=1 T=0

    link(l1, bs1)
    link(bs1.p2, 1, WI.p1)
    link(bs1.p3, 1, NI.p1)

    link(WI.p2, 1, WE.p1)
    link(NI.p2, 1, NE.p1)

    pd out bs1.p4.o
    """
)
<finesse.model.Model at 0x799fbd8cfb60>

Inspecting

You can inspect the available degrees of freedom for a model component by accessing their .dofs namespace:

base_model.WE.dofs
namespace(z=<'WE.dofs.z' @ 0x799fbd6c8670 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.z @ 0x799fbd689310> AC_OUT=<SignalNode WE.mech.z @ 0x799fbd689310>>,
          yaw=<'WE.dofs.yaw' @ 0x799fbd6c86e0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.yaw @ 0x799fbd6893d0> AC_OUT=<SignalNode WE.mech.yaw @ 0x799fbd6893d0>>,
          pitch=<'WE.dofs.pitch' @ 0x799fbd6c8750 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.pitch @ 0x799fbd689490> AC_OUT=<SignalNode WE.mech.pitch @ 0x799fbd689490>>,
          F_z=<'WE.dofs.F_z' @ 0x799fbd6c87c0 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.F_z @ 0x799fbd689550> AC_OUT=<SignalNode WE.mech.z @ 0x799fbd689310>>,
          F_yaw=<'WE.dofs.F_yaw' @ 0x799fbd6c8830 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_yaw @ 0x799fbd689610> AC_OUT=<SignalNode WE.mech.yaw @ 0x799fbd6893d0>>,
          F_pitch=<'WE.dofs.F_pitch' @ 0x799fbd6c88a0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_pitch @ 0x799fbd6896d0> AC_OUT=<SignalNode WE.mech.pitch @ 0x799fbd689490>>)

Every degree of freedom is coupled to a parameter of the component and has DC and AC attributes. For tuning the interferometer we are interested in the z.DC attribute.

base_model.WE.dofs.z.DC
<WE.phi=0.0 @ 0x799fbd46a4d0>

We can see it is directly coupled to the phi attribute of the mirror. Now we create a copy of this model with the standard degrees of freedom defined.

dof_model = base_model.deepcopy()
dof_model.parse(
    """
    dof DARM WE.dofs.z -1 NE.dofs.z +1 lock_parameters=True  # locks by default, just for demonstration
    dof CARM WE.dofs.z +1 NE.dofs.z +1 lock_parameters=True  # locks by default, just for demonstration
    dof MICH WI.dofs.z -1 NI.dofs.z +1 lock_parameters=True  # locks by default, just for demonstration
    """
)
dof_model_2 = dof_model.deepcopy()

Tuning

We can reference the MICH degree of freedom to tune the dark port of the interferometer.

from finesse.analysis.actions import Minimize

out = dof_model.run(
    Minimize(dof_model.out, dof_model.MICH.DC, xatol=1e-15, fatol=1e-15)
)
dof_model.run(Noxaxis())['out']
np.float64(1.2325951644078312e-32)

Inspecting the mirror tunings, we can see that they are referencing the MICH.DC degree of freedom.

display(dof_model.WI.dofs.z.DC)
print(f"{dof_model.WI.dofs.z.DC.eval()=} degrees")
display(dof_model.NI.dofs.z.DC)
print(f"{dof_model.NI.dofs.z.DC.eval()=} degrees")
display(dof_model.MICH.DC)
<WI.phi=(-MICH.DC) @ 0x799fbd5182b0>
dof_model.WI.dofs.z.DC.eval()=-1.594842552110701 degrees
<NI.phi=MICH.DC @ 0x799fbd519080>
dof_model.NI.dofs.z.DC.eval()=1.594842552110701 degrees
<MICH.DC=1.594842552110701 @ 0x799fbc064860>

Running scans with degrees of freedom

Note that in the base model without the degrees of freedom defined, you can still change the mirror tuning by referencing the phi attribute directly.

sol_phi = base_model.run(
    Xaxis(base_model.WE.phi, "lin", -180, 180, 200, relative=False)
)
sol_phi.plot()
../../_images/degree_of_freedom_15_0.svg
{finesse.detectors.powerdetector.PowerDetector: <Figure size 576x355.968 with 1 Axes>,
 'out': <Figure size 576x355.968 with 1 Axes>}

But in the model with the degrees of freedom defined, you can no longer control the mirror tuning directly.

dof_model_2.run(Xaxis(dof_model.WE.phi, "lin", -180, 180, 200, relative=False))
ControlledByDOFException: 	(use finesse.tb() to see the full traceback)
<WE.phi=(CARM.DC+(-DARM.DC)) @ 0x799fbbe9a4d0> is being controlled by (<'DARM' @ 0x799fbd6d1cd0 (DegreeOfFreedom)>, <'CARM' @ 0x799fbbec0050 (DegreeOfFreedom)>), so it cannot be tuned directly.

It is still possible to move individual mirrors by defining additional degrees of freedom.

dof_model_2.parse(
    """
    dof WE_phi WE.dofs.z +1
    """
)
# reset the degree of freedom so both models are in identical state
dof_model_2.MICH.DC = 0
sol_WEDC = dof_model_2.run(
    Xaxis(dof_model_2.WE_phi.DC, "lin", -180, 180, 200, relative=False)
)

plt.plot(sol_phi.x1, sol_phi["out"], label="WE.phi")
plt.plot(sol_WEDC.x1, sol_WEDC["out"], label="WE_phi.DC", ls="--")
plt.xlabel("WE position")
plt.ylabel("Power")
plt.title("Comparison of scanning with `.phi` and with the dof")
plt.show()
../../_images/degree_of_freedom_17_0.svg

Scanning over a degree of freedom while its symbolic with one or more of its parameters is broken will result in an exception.

manual_model = dof_model_2.deepcopy()
# break the symbolic link with `WE_phi`
manual_model.WE.phi = 90
# try scanning over the degree of freedom
manual_model.run(Xaxis(manual_model.WE_phi.DC, 'lin', -180, 180, 200))
<ArraySolution of xaxis @ 0x799fb8952c20 children=0>