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 [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\)
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 0x7a6d6f043e00>
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' @ 0x7a6d6ee22510 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.z @ 0x7a6d6ebe8c50> AC_OUT=<SignalNode WE.mech.z @ 0x7a6d6ebe8c50>>,
yaw=<'WE.dofs.yaw' @ 0x7a6d6ee22580 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.yaw @ 0x7a6d6ebe8d10> AC_OUT=<SignalNode WE.mech.yaw @ 0x7a6d6ebe8d10>>,
pitch=<'WE.dofs.pitch' @ 0x7a6d6ee225f0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.pitch @ 0x7a6d6ebe8dd0> AC_OUT=<SignalNode WE.mech.pitch @ 0x7a6d6ebe8dd0>>,
F_z=<'WE.dofs.F_z' @ 0x7a6d6ee22660 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.F_z @ 0x7a6d6ebe8e90> AC_OUT=<SignalNode WE.mech.z @ 0x7a6d6ebe8c50>>,
F_yaw=<'WE.dofs.F_yaw' @ 0x7a6d6ee226d0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_yaw @ 0x7a6d6ebe8f50> AC_OUT=<SignalNode WE.mech.yaw @ 0x7a6d6ebe8d10>>,
F_pitch=<'WE.dofs.F_pitch' @ 0x7a6d6ee22740 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_pitch @ 0x7a6d6ebe9010> AC_OUT=<SignalNode WE.mech.pitch @ 0x7a6d6ebe8dd0>>)
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 @ 0x7a6d6ebf4860>
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
"""
)
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) @ 0x7a6d6ec6e5a0>
dof_model.WI.dofs.z.DC.eval()=-1.594842552110701 degrees
<NI.phi=MICH.DC @ 0x7a6d6ec6f370>
dof_model.NI.dofs.z.DC.eval()=1.594842552110701 degrees
<MICH.DC=1.594842552110701 @ 0x7a6d6d92a8e0>
Symbolic links & Locking
Creating degrees of freedom creates symbolic links between the DC parameter of th
degree of freedom and all the parameters that a degree of freedom is driving. You can
check which parameters are being linked by using the driven_parameters property
for p in dof_model.MICH.driven_parameters:
print(p.full_name, p.value)
WI.phi (-MICH.DC)
NI.phi MICH.DC
You see that the MICH.DC parameter is used in the symbolic expression for the
WI.phi and NI.phi parameters. However, nothing is stopping you from overwriting
the value of those parameters with a numeric value (or a different symbolic value)
broken_dof_model = dof_model.deepcopy()
broken_dof_model.WI.phi = 10
for p in broken_dof_model.MICH.driven_parameters:
print(p.full_name, p.value)
WI.phi 10.0
NI.phi MICH.DC
This means that in the broken model, adjusting the MICH degree of freedom will now
only move one of the mirrors, meaning the degree of freedom now longer works as
intended. There are two ways to handle these mistakes. Firstly, we can use the
check_dof_links method on the Model object to identify broken degrees of
freedom.
Checking links
broken_dof_model.check_dof_links()
BrokenDOFLinkError: (use finesse.tb() to see the full traceback)
Driven parameter 'WI.phi' from 'dof MICH WI.dofs.z -1 NI.dofs.z 1 DC=1.594842552110701'
is not linked, current value: '10.0 degrees'
You can also check the links of a specific degree of freedom with its
check_parameter_links method.
broken_dof_model.MICH.check_parameter_links()
BrokenDOFLinkError: (use finesse.tb() to see the full traceback)
Driven parameter 'WI.phi' from 'dof MICH WI.dofs.z -1 NI.dofs.z 1 DC=1.594842552110701'
is not linked, current value: '10.0 degrees'
Locking
You can lock any parameter in Finesse by calling its lock method. This
prevents the value being changed.
param_model = finesse.Model("l l1 P=1")
# normally allowed to change parameter values
param_model.l1.P = 2
# you can lock the parameter (and provide reason for locking)
param_model.l1.P.lock("demonstration")
# now changing the value will raise an exception
param_model.l1.P = 3
ParameterLocked: (use finesse.tb() to see the full traceback)
Parameter <l1.P=2.0 @ 0x7a6d67ef0ba0> is locked by 'demonstration'
Parameters can be unlocked by using their unlock method.
You can lock all the parameters being driven by a degree of freedom by using the
lock_parameters method.
dof_model.MICH.lock_parameters(verbose=True)
dof_model.WI.phi = 10
Locking parameters for DOF 'MICH'
Locking 'WI.phi'
Locking 'NI.phi'
ParameterLocked: (use finesse.tb() to see the full traceback)
Parameter <WI.phi=(-MICH.DC) @ 0x7a6d6ec6e5a0>'s value is symbolic and locked by `MICH.lock_parameters`
Or do the same for all degrees of freedom in a model by using the
lock_dof_parameters.
dof_model.lock_dof_parameters(verbose=True)
dof_model.WI.phi = 10
Locking parameters for DOF 'DARM'
Locking 'WE.phi'
Locking 'NE.phi'
Locking parameters for DOF 'CARM'
Locking 'WE.phi'
Locking 'NE.phi'
Locking parameters for DOF 'MICH'
Locking 'WI.phi'
Locking 'NI.phi'
ParameterLocked: (use finesse.tb() to see the full traceback)
Parameter <WI.phi=(-MICH.DC) @ 0x7a6d6ec6e5a0>'s value is symbolic and locked by `MICH.lock_parameters, MICH.lock_parameters`
This will prevent you from accidentally breaking your degrees of freedom at any point in
your notebook. The parameters can be unlocked again with the unlock_dof_parameters
method.
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()
{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))
ExternallyControlledException: (use finesse.tb() to see the full traceback)
<WE.phi=(CARM.DC+(-DARM.DC)) @ 0x7a6d6d698450> is being controlled by (<'DARM' @ 0x7a6d6d95fbb0 (DegreeOfFreedom)>, <'CARM' @ 0x7a6d6edb2d50 (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()
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_2.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 @ 0x7a6d67b16e90>
<WE.phi=((CARM.DC+WE_phi.DC)+(-DARM.DC)) @ 0x7a6d67b16740>
Adjusting degree of freedom DC parameter
<WE_phi.DC=45.0 @ 0x7a6d67b16e90>
<WE.phi=((CARM.DC+WE_phi.DC)+(-DARM.DC)) @ 0x7a6d67b16740>
'manual_model.WE.phi.eval()=45.0'
Adjusting mirror tuning directly
<WE_phi.DC=45.0 @ 0x7a6d67b16e90>
<WE.phi=90.0 @ 0x7a6d67b16740>
<WE_phi.DC=45.0 @ 0x7a6d67b16e90>
<WE.phi=90.0 @ 0x7a6d67b16740>