Using degrees of freedom

degree_of_freedom
dof
DegreeOfFreedom
Syntax
dof name *node_amplitude_pairs DC=0

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 [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_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. 4 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
    """
)

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' @ 0x7ca336be9640 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.z @ 0x7ca336be94c0> AC_OUT=<SignalNode WE.mech.z @ 0x7ca336be94c0>>,
          yaw=<'WE.dofs.yaw' @ 0x7ca336be9670 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.yaw @ 0x7ca336be9520> AC_OUT=<SignalNode WE.mech.yaw @ 0x7ca336be9520>>,
          pitch=<'WE.dofs.pitch' @ 0x7ca336be96a0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.pitch @ 0x7ca336be9550> AC_OUT=<SignalNode WE.mech.pitch @ 0x7ca336be9550>>,
          F_z=<'WE.dofs.F_z' @ 0x7ca336be96d0 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.F_z @ 0x7ca336be9580> AC_OUT=<SignalNode WE.mech.z @ 0x7ca336be94c0>>,
          F_yaw=<'WE.dofs.F_yaw' @ 0x7ca336be9700 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_yaw @ 0x7ca336be95b0> AC_OUT=<SignalNode WE.mech.yaw @ 0x7ca336be9520>>,
          F_pitch=<'WE.dofs.F_pitch' @ 0x7ca336be9730 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_pitch @ 0x7ca336be95e0> AC_OUT=<SignalNode WE.mech.pitch @ 0x7ca336be9550>>)

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 @ 0x7ca336c08ac0>

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
    dof CARM WE.dofs.z +1 NE.dofs.z +1
    dof MICH WI.dofs.z -1 NI.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 = 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) @ 0x7ca336c0b280>
dof_model.WI.dofs.z.DC.eval()=-1.594842552110701 degrees
<NI.phi=MICH.DC @ 0x7ca336c0ba00>
dof_model.NI.dofs.z.DC.eval()=1.594842552110701 degrees
<MICH.DC=1.594842552110701 @ 0x7ca3369de800>

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_6_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.run(Xaxis(dof_model.WE.phi, "lin", -180, 180, 200, relative=False))
ExternallyControlledException: 	(use finesse.tb() to see the full traceback)
<WE.phi=(CARM.DC+(-DARM.DC)) @ 0x7ca336ae4280> is being controlled by (<'DARM' @ 0x7ca336c3d4f0 (DegreeOfFreedom)>, <'CARM' @ 0x7ca336c507d0 (DegreeOfFreedom)>), so it cannot be tuned directly.

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

dof_model.parse(
    """
    dof WE_phi WE.dofs.z +1
    """
)
# reset the degree of freedom so both models are in identical state
dof_model.MICH.DC = 0
sol_WEDC = dof_model.run(
    Xaxis(dof_model.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_8_0.svg

When using degrees of freedom, it is safest to never access the component attributes directly. The recommended way to adjust attributes is using actions, which will warn you if you are modifying an attribute controlled by a degree of freedom.

If you want to manually adjust an attribute, using a degree of freedom is recommended because it maintains previously defined symbolic relationships, while manually adjusting the component attribute directly will break it.

manual_model = dof_model.deepcopy()

display(manual_model.WE_phi.DC)
display(manual_model.WE.phi)

out = manual_model.run(Xaxis(manual_model.WE_phi.DC, 'lin', -180, 180, 200))
out.plot()

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

display(manual_model.WE_phi.DC)
display(manual_model.WE.phi)
display(f"{manual_model.WE.phi.eval()=}")

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

display(manual_model.WE_phi.DC)
display(manual_model.WE.phi)

out = manual_model.run(Xaxis(manual_model.WE_phi.DC, 'lin', -180, 180, 200))
out.plot()

manual_model.WE_phi.DC = 45

display(manual_model.WE_phi.DC)
display(manual_model.WE.phi)
<WE_phi.DC=0.0 @ 0x7ca334eb4280>
<WE.phi=((CARM.DC+WE_phi.DC)+(-DARM.DC)) @ 0x7ca334eb4040>
../../_images/degree_of_freedom_9_2.svg
Adjusting degree of freedom DC parameter
<WE_phi.DC=45.0 @ 0x7ca334eb4280>
<WE.phi=((CARM.DC+WE_phi.DC)+(-DARM.DC)) @ 0x7ca334eb4040>
'manual_model.WE.phi.eval()=45.0'
Adjusting mirror tuning directly
<WE_phi.DC=45.0 @ 0x7ca334eb4280>
<WE.phi=90.0 @ 0x7ca334eb4040>
../../_images/degree_of_freedom_9_10.svg
<WE_phi.DC=45.0 @ 0x7ca334eb4280>
<WE.phi=90.0 @ 0x7ca334eb4040>