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 0lock_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\)
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>
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. By default the degree of freedom element locks
the parameters it controls, via its lock_parameters argument.
If you want to have the freedom to overwrite parameter values that are supposed to be controlled by a degree of freedom, you can manually unlock the parameter and overwrite its value.
broken_dof_model = dof_model.deepcopy()
broken_dof_model.WI.phi.unlock()
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_symbolic_links method on the Model object to identify broken degrees of
freedom.
Checking links
broken_dof_model.check_dof_symbolic_links(verbose=True)
Parameter 'WI.phi' should have value '(0+(-MICH.DC))' but has '10.0'
BrokenDOFLinkError: (use finesse.tb() to see the full traceback)
The following parameters are not correctly linked to their degrees of freedom: 'WI.phi' [MICH]
Restoring links
You can restore a model with broken degrees of freedom at any time with the
restore_dof_symbolic_links method on the Model object. This also locks the
parameters again (configurable via the lock argument).
broken_dof_model.restore_dof_symbolic_links(verbose=True)
broken_dof_model.check_dof_symbolic_links() # will no longer raise an exception
broken_dof_model.WI.phi # has original value
Restored parameter 'WE.phi'' value from '(CARM.DC+(-DARM.DC))' to '(CARM.DC+(-DARM.DC))', imposed by 'DARM, CARM'
Restored parameter 'NE.phi'' value from '(CARM.DC+DARM.DC)' to '(CARM.DC+DARM.DC)', imposed by 'DARM, CARM'
Restored parameter 'WI.phi'' value from '10.0' to '(-MICH.DC)', imposed by 'MICH'
Restored parameter 'NI.phi'' value from 'MICH.DC' to 'MICH.DC', imposed by 'MICH'
<WI.phi=(-MICH.DC) @ 0x799fbb3f65a0>
If you want to restore the symbolic links for just a single parameter, you can call the
restore_from_dof method on the parameter itself.
broken_dof_model.WI.phi.unlock()
broken_dof_model.WI.phi = 10
broken_dof_model.WI.phi.restore_from_dof(verbose=True)
Restored parameter 'WI.phi'' value from '10.0' to '(-MICH.DC)', imposed by 'MICH'
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)
Can not change the value of Parameter 'l1.P' because it is locked by 'demonstration'
You can unlock 'l1.P' using 'l1.P.unlock()'
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_driven_parameters method. Note that this is the default behaviour when creating
a new degree of freedom, unless you set lock_parameters=False at creation.
dof_model.MICH.lock_driven_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)
Can not change the value of Parameter 'WI.phi' because it is locked by 'MICH.lock_parameters'
You can unlock 'WI.phi' using 'WI.phi.unlock()'
You can also unlock all parameters:
dof_model_unlocked = dof_model.deepcopy()
dof_model_unlocked.MICH.unlock_driven_parameters()
dof_model_unlocked.WI.phi = 10
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)
Can not change the value of Parameter 'WI.phi' because it is locked by 'MICH.lock_parameters, MICH.lock_parameters'
You can unlock 'WI.phi' using 'WI.phi.unlock()'
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))
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()
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>