Using degrees of freedom

dof name *node_amplitude_pairs DC=0

The degree_of_freedom command can be useful when building control loops, but also for tuning interferometes 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 [26]:

  • 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_N\)

  • 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\)


Fig. 4 Simplified layout of AdV+ [26]

Creating a basic version of this interferometer.

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


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

    m ITMy R=0.9 T=0.1
    m ITMx R=0.9 T=0.1

    m ETMy R=1 T=0
    m ETMx R=1 T=0

    link(l1, bs1)
    link(bs1.p2, 1, ITMy.p1)
    link(bs1.p3, 1, ITMx.p1)

    link(ITMy.p2, 1, ETMy.p1)
    link(ITMx.p2, 1, ETMx.p1)

    pd out bs1.p4.o

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

namespace(z=<'ETMy.dofs.z' @ 0x7ddec207b380 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode ETMy.mech.z @ 0x7ddec207b560> AC_OUT=<SignalNode ETMy.mech.z @ 0x7ddec207b560>>,
          yaw=<'ETMy.dofs.yaw' @ 0x7ddec207ac60 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode ETMy.mech.yaw @ 0x7ddec207b260> AC_OUT=<SignalNode ETMy.mech.yaw @ 0x7ddec207b260>>,
          pitch=<'ETMy.dofs.pitch' @ 0x7ddec207a390 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode ETMy.mech.pitch @ 0x7ddec207b320> AC_OUT=<SignalNode ETMy.mech.pitch @ 0x7ddec207b320>>,
          F_z=<'ETMy.dofs.F_z' @ 0x7ddec207a300 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode ETMy.mech.F_z @ 0x7ddec207b4a0> AC_OUT=<SignalNode ETMy.mech.z @ 0x7ddec207b560>>,
          F_yaw=<'ETMy.dofs.F_yaw' @ 0x7ddec207a540 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode ETMy.mech.F_yaw @ 0x7ddec207b620> AC_OUT=<SignalNode ETMy.mech.yaw @ 0x7ddec207b260>>,
          F_pitch=<'ETMy.dofs.F_pitch' @ 0x7ddec207a3c0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode ETMy.mech.F_pitch @ 0x7ddec207b470> AC_OUT=<SignalNode ETMy.mech.pitch @ 0x7ddec207b320>>)

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.

<ETMy.phi=0.0 @ 0x7ddec2247a00>

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 DARM ETMy.dofs.z -1 ETMx.dofs.z +1
    dof CARM ETMy.dofs.z +1 ETMx.dofs.z +1
    dof MICH ITMy.dofs.z -1 ITMx.dofs.z +1

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

from finesse.analysis.actions import Minimize

out =
    Minimize(dof_model.out, dof_model.MICH.DC, xatol=1e-15, fatol=1e-15)

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

print(f"{dof_model.ITMy.dofs.z.DC.eval()=} degrees")
print(f"{dof_model.ITMx.dofs.z.DC.eval()=} degrees")
<ITMy.phi=(-MICH.DC) @ 0x7ddec20c2140>
dof_model.ITMy.dofs.z.DC.eval()=-1.594842552110701 degrees
<ITMx.phi=MICH.DC @ 0x7ddec20c28c0>
dof_model.ITMx.dofs.z.DC.eval()=1.594842552110701 degrees
<MICH.DC=1.594842552110701 @ 0x7ddec1e8d540>

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 =
    Xaxis(base_model.ETMy.phi, "lin", -180, 180, 200, relative=False)
{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., "lin", -180, 180, 200, relative=False))
ExternallyControlledException: 	(use finesse.tb() to see the full traceback)
<ETMy.phi=(CARM.DC+(-DARM.DC)) @ 0x7ddec20c3100> is being controlled by (<'DARM' @ 0x7ddec20e4650 (DegreeOfFreedom)>, <'CARM' @ 0x7ddec20ee0c0 (DegreeOfFreedom)>), so it cannot be tuned directly.

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

    dof ETMy_phi ETMy.dofs.z +1
# reset the degree of freedom so both models are in identical state
dof_model.MICH.DC = 0
sol_ETMyDC =
    Xaxis(dof_model.ETMy_phi.DC, "lin", -180, 180, 200, relative=False)

plt.plot(sol_phi.x1, sol_phi["out"], label="ETMy.phi")
plt.plot(sol_ETMyDC.x1, sol_ETMyDC["out"], label="ETMy_phi.DC", ls="--")
plt.xlabel("ETMy position")
plt.title("Comparison of scanning with `.phi` and with the dof")

When using degrees of freedom, it is safest to never access the component attributes directly. Manually adjusting the value of a degree of freedom maintains all the symbolic relationships, but manually adjusting the component attribute directly will break it.

manual_model = dof_model.deepcopy()


out =, 'lin', -180, 180, 200))

print("Adjusting degree of freedom DC parameter")
manual_model.ETMy_phi.DC = 45


print("Adjusting mirror tuning directly")
manual_model.ETMy.phi = 90


out =, 'lin', -180, 180, 200))

manual_model.ETMy_phi.DC = 45

<ETMy_phi.DC=0.0 @ 0x7ddec04a2b00>
<ETMy.phi=((CARM.DC+ETMy_phi.DC)+(-DARM.DC)) @ 0x7ddec04a28c0>
Adjusting degree of freedom DC parameter
<ETMy_phi.DC=45.0 @ 0x7ddec04a2b00>
<ETMy.phi=((CARM.DC+ETMy_phi.DC)+(-DARM.DC)) @ 0x7ddec04a28c0>
Adjusting mirror tuning directly
<ETMy_phi.DC=45.0 @ 0x7ddec04a2b00>
<ETMy.phi=90.0 @ 0x7ddec04a28c0>
<ETMy_phi.DC=45.0 @ 0x7ddec04a2b00>
<ETMy.phi=90.0 @ 0x7ddec04a28c0>