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\)
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' @ 0x7e1801df2a50 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.z @ 0x7e18022bfc20> AC_OUT=<SignalNode WE.mech.z @ 0x7e18022bfc20>>,
yaw=<'WE.dofs.yaw' @ 0x7e1801df2ac0 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.yaw @ 0x7e18022bfcd0> AC_OUT=<SignalNode WE.mech.yaw @ 0x7e18022bfcd0>>,
pitch=<'WE.dofs.pitch' @ 0x7e1801df2b30 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.pitch @ 0x7e18022bfd80> AC_OUT=<SignalNode WE.mech.pitch @ 0x7e18022bfd80>>,
F_z=<'WE.dofs.F_z' @ 0x7e1801df2ba0 (LocalDegreeOfFreedom) DC=0.0 degrees AC_IN=<SignalNode WE.mech.F_z @ 0x7e18022bfe30> AC_OUT=<SignalNode WE.mech.z @ 0x7e18022bfc20>>,
F_yaw=<'WE.dofs.F_yaw' @ 0x7e1801df2c10 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_yaw @ 0x7e18022bfee0> AC_OUT=<SignalNode WE.mech.yaw @ 0x7e18022bfcd0>>,
F_pitch=<'WE.dofs.F_pitch' @ 0x7e1801df2c80 (LocalDegreeOfFreedom) DC=0.0 radians AC_IN=<SignalNode WE.mech.F_pitch @ 0x7e1801e78050> AC_OUT=<SignalNode WE.mech.pitch @ 0x7e18022bfd80>>)
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 @ 0x7e1801e70b80>
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) @ 0x7e1801d09840>
dof_model.WI.dofs.z.DC.eval()=-1.594842552110701 degrees
<NI.phi=MICH.DC @ 0x7e1801d0a200>
dof_model.NI.dofs.z.DC.eval()=1.594842552110701 degrees
<MICH.DC=1.594842552110701 @ 0x7e1801c4e5c0>
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.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)) @ 0x7e1801d0ac80> is being controlled by (<'DARM' @ 0x7e184819cd70 (DegreeOfFreedom)>, <'CARM' @ 0x7e180197c050 (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()
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 @ 0x7e1800110a00>
<WE.phi=((CARM.DC+WE_phi.DC)+(-DARM.DC)) @ 0x7e1800141480>
Adjusting degree of freedom DC parameter
<WE_phi.DC=45.0 @ 0x7e1800110a00>
<WE.phi=((CARM.DC+WE_phi.DC)+(-DARM.DC)) @ 0x7e1800141480>
'manual_model.WE.phi.eval()=45.0'
Adjusting mirror tuning directly
<WE_phi.DC=45.0 @ 0x7e1800110a00>
<WE.phi=90.0 @ 0x7e1800141480>
<WE_phi.DC=45.0 @ 0x7e1800110a00>
<WE.phi=90.0 @ 0x7e1800141480>