Adding the AS beam on the AP Table (and QPDs)¶
The default Finesse-40m package only deals with the parts in the vacuum system as these are static. However, you may want to propagate some beams around the in-air tables.
The AP table is an in-air table that contains the reflection from the IMC, the AS beam and the REFL. Finesse does not model in free space and instead using a port-node system to describe the optical transfer function of several elements.
When we observe the finesse_40m.factory.base.FortyMeterFactory. We can see
the method finesse_40m.factory.base.FortyMeterFactory.add_AS_table()
this method is only handling the AS beam path, which is what we will add today.
When we inspect the current code, we can see that all it does is create an empty node, 10cm from the window which it calls the AS port.
Imports¶
import finesse
import finesse.analysis.actions as fac
from finesse.detectors import PowerDetector
import finesse.components as fc
from finesse.detectors import PowerDetector, AmplitudeDetector, MathDetector
from finesse.symbols import Constant
import finesse_40m
from finesse_40m.logging import setup_logger
setup_logger(context="docs") # For documentation only
# call without any arguments in your code
# or, define your own function!
from finesse_40m.actions import InitialLockPRMI
from finesse_40m.factory import FortyMeterFactory
from finesse_40m.locks import (
add_PRMI_locks, # function to add PRMI locks
plot_PRMI_error_signals # function to plot PRMI error sigs
)
# Functions for checking the IFO state
from finesse_40m.utils import (
get_PDs,
get_readouts,
fmtpower
)
import matplotlib.pyplot as plt
finesse.init_plotting()
import pprint
Adding AS WFS¶
In this example, we will create a separate port for AS55 and we will add one WFS head.
We now define the table, by overwriting the
finesse_40m.factory.base.FortyMeterFactory.add_AS_table().
The majority of the library is written using the API format of Finesse, however, for something quick, often people prefer KatScript and so I will show the use of that here.
class myForty(FortyMeterFactory):
def add_AS_table(self, model):
model.parse("""
lens AS_lens 1
bs ASbs1 R=0.5 T=0.5 alpha=45
space s_AS1 AS_lens.p2 ASbs1.p1 L=0.6# connect lens to BS
nothing AS_port # We must define this, it is the point where all "AS" things will be placed by default
space s_AS2 ASbs1.p3 AS_port.p1 L=0.6 # connect transmission of BS1 to a point called "AS_port"
bs ASbs2 R=0.5 T=0.5 alpha=45
space s_AS3 ASbs1.p2 ASbs2.p1 L=0.6 # connect BS1 reflection to a second BS
nothing AS55_port # a port for AS55
space s_AS4 ASbs2.p2 AS55_port.p1 L=0.6 # connect BS2 reflection to the AS55_port port
nothing AS_WFS_Port # a port for a single AS WFS head
space s_AS5 ASbs2.p3 AS_WFS_Port.p1 L=0.6 # connect BS2 reflection to the REFL11 port
# Add a QPD
pd ASqpdx AS_WFS_Port.p1.i pdtype=xsplit
pd ASqpdy AS_WFS_Port.p1.i pdtype=ysplit
""")
model.connect(self.as_window_out, model.AS_lens.p1, L=0.3) # Connect our lens to the beam coming from the window
self.AS_port = model.AS_port.p1 # Provide our AS port for factory functions to use
Note that penultimate line, where I use the API format of Finesse to connect the new model elements to the existing model that is being built by the factory.
Additionally, the last line is very important. This creates as AS port, which is required by the library.
Checking the beam path¶
We can now build this example and run beam tracing to make sure everything is connected correctly.
factory = myForty()
factory.reset()
# Print out build options
print("===== Config =======")
pprint.PrettyPrinter(indent=4, sort_dicts=True).pprint(
factory.options.toDict()
)
# Build model
print("===== Building model =======")
model = factory.make()
model.modes(maxtem=2) # Lets do some WFS modelling.
print("Success")
ps = model.propagate_beam(model.AS2.p1.i,model.AS_WFS_Port.p1)
ps.plot(resolution='all')
===== Config =======
{ 'BS_trans_arm': 'X',
'BS_type': 'thick',
'ETMAR': False,
'INPUT': {'add_IMC_and_IM1': False, 'set_IMC_mode': True},
'LSC': { 'add_AC_loops': False,
'add_DOFs': True,
'add_locks': False,
'add_output_detectors': False,
'add_readouts': True,
'close_AC_loops': False},
'OUTPUT': {'readout': 'AS'},
'PR2_PR3_substrates': True,
'add_11MHz': True,
'add_165MHz': False,
'add_33MHz': True,
'add_55MHz': True,
'add_detectors': True,
'add_transmon': False,
'fake_prc_gouy': False,
'fake_src_gouy': False,
'materials': { 'test_mass_substrate': <finesse.materials.Material object at 0x7f26f266fdb0>},
'modulation_order': 3,
'suspensions': { 'auxillary_optics': False,
'core_optics': False,
'test_masses': False},
'wedges': False}
===== Building model =======
Success
(<Figure size 576x355.968 with 2 Axes>,
array([<Axes: ylabel='Beam size [mm]'>,
<Axes: xlabel='Distance [m]', ylabel='Gouy phase\naccumulation [deg]'>],
dtype=object))
Locking the IFO¶
Before we lock the IFO, we need to put the AS55 sensors in the new location. The locking requires readouts for AC loops and detectors for DC loops.
For the most part we can recycle the existing code in the library, changing only the sensors that we want to change. I’ll mark which things are changed.
class myForty2(myForty):
def add_LSC_readouts(self, model):
f1 = model.f1.ref
f2 = model.f2.ref
output_detectors = self.options.LSC.add_output_detectors
model.add(
fc.ReadoutRF(
"REFL11", self.REFL_port.i, f=f1, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"REFL55", self.REFL_port.i, f=f2, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"POP11", self.POP_port.i, f=f1, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"POP22", self.POP_port.i, f=2 * f1, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"POP44", self.POP_port.i, f=f2 - f1, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"POP55", self.POP_port.i, f=f2, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"POP110", self.POP_port.i, f=2 * f2, output_detectors=output_detectors
)
)
model.add(
fc.ReadoutRF(
"AS55", model.AS55_port.p1.i, f=f2, output_detectors=output_detectors #< -- This line was changed
)
)
model.add(
fc.ReadoutRF(
"AS44", self.AS_port.i, f=f2 - f1, output_detectors=output_detectors
)
)
rout = self.options.OUTPUT.readout
if rout == "AS":
port = self.AS_port.i
elif rout == "HD":
port = self.OMC_output_port.o
elif rout == "BHD":
raise NotImplementedError("BHD not implemented!")
else:
NotImplementedError("Readout of {rout} not implemented for AS readout!")
model.add(fc.ReadoutDC("AS", port, output_detectors=output_detectors))
def add_detectors(self, model):
if self.add_ADs():
f1 = model.f1.ref
f2 = model.f2.ref
def add_amplitude_detectors(port, key):
freqs = []
if self.options.add_11MHz:
freqs.append((f1, "11"))
if self.options.add_55MHz:
freqs.append((f2, "55"))
self._add_all_amplitude_detectors(model, port, key, *freqs)
readout_method = self.options.OUTPUT.readout
if self.add_PDs:
model.add(PowerDetector("Pin", self.PRM_AR_fr.i))
Px = Constant(model.Px)
Py = Constant(model.Py)
model.add(PowerDetector("PreflPRM", self.PRM_AR_fr.o))
model.add(PowerDetector("Prefl", self.REFL_port.i))
model.add(PowerDetector("Ppop", self.POP_port.i))
model.add(PowerDetector("Pas", self.AS_port.i))
if readout_method in ["HD"]:
model.add(PowerDetector("Pas_c", model.OM1.p1.i))
model.add(PowerDetector("Pas", self.OMC_output_port.o))
Pinx = Constant(model.Pinx)
Piny = Constant(model.Piny)
if self.add_ADs():
add_amplitude_detectors(self.POP_port.i, "pop")
if readout_method in ["HD"]:
add_amplitude_detectors(self.OMC_output_port.o, "dcpd")
# Begin edits
#add_amplitude_detectors(self.AS_port.i, "as") # <-- This added everything to the AS port
# instead we add them manually below
model.add(AmplitudeDetector(f"a_c0_as", self.AS_port.i, f=0))
model.add(AmplitudeDetector(f"a_u11_as", self.AS_port.i, f=f1, n=None, m=None))
model.add(AmplitudeDetector(f"a_l11_as", self.AS_port.i, f=-f1, n=None, m=None))
model.add(AmplitudeDetector(f"a_u55_as", model.AS55_port.p1.i, f=f2, n=None, m=None))
model.add(AmplitudeDetector(f"a_l55_as", model.AS55_port.p1.i, f=-f2, n=None, m=None))
# end edits
add_amplitude_detectors(model.PRM.p2.i, "in")
add_amplitude_detectors(self.REFL_port.i, "refl")
add_amplitude_detectors(model.PRM.p1.o, "prc")
freqs = [
(f1, "11"),
(f2, "55"),
(f2, "55", 0, 0),
]
self._add_all_amplitude_detectors(model, model.SRM.p1.i, "src", *freqs)
model.add(AmplitudeDetector("a_carrier_refl", self.REFL_port.i, f=0))
a_carrier_pinx = Constant(model.a_carrier_pinx)
a_carrier_piny = Constant(model.a_carrier_piny)
a_carrier_x = Constant(model.a_carrier_x)
a_carrier_y = Constant(model.a_carrier_y)
# FIXME: there should be some logic below for if 9 and 45 MHz have been added
Pprc_carrier = abs(Constant(model.a_c0_prc)) ** 2
Pin_carrier = abs(Constant(model.a_c0_in)) ** 2
Pprc_11 = (
abs(Constant(model.a_u11_prc)) ** 2
+ abs(Constant(model.a_l11_prc)) ** 2
)
Pprc_55 = (
abs(Constant(model.a_u55_prc)) ** 2
+ abs(Constant(model.a_l55_prc)) ** 2
)
Pin_11 = (
abs(Constant(model.a_u11_in)) ** 2 + abs(Constant(model.a_l11_in)) ** 2
)
Pin_55 = (
abs(Constant(model.a_u55_in)) ** 2 + abs(Constant(model.a_l55_in)) ** 2
)
Pinx_carrier = abs(a_carrier_pinx) ** 2
Piny_carrier = abs(a_carrier_piny) ** 2
model.add(MathDetector("Pprc_carrier", Pprc_carrier))
model.add(MathDetector("Pprc_9", Pprc_11))
model.add(MathDetector("Pprc_45", Pprc_55))
model.add(MathDetector("Pin_carrier", Pin_carrier))
model.add(MathDetector("Pin_9", Pin_11))
model.add(MathDetector("Pin_45", Pin_55))
model.add(MathDetector("Pinx_carrier", Pinx_carrier))
model.add(MathDetector("Piny_carrier", Piny_carrier))
model.add(MathDetector("Pas_carrier", abs(Constant(model.a_c0_src)) ** 2))
model.add(MathDetector("PRG", Pprc_carrier / Pin_carrier))
model.add(MathDetector("PRG11", Pprc_11 / Pin_11))
model.add(MathDetector("PRG55", Pprc_55 / Pin_55))
model.add(MathDetector("AGX", abs(a_carrier_x) ** 2 / Pinx_carrier))
model.add(MathDetector("AGY", abs(a_carrier_y) ** 2 / Piny_carrier))
model.add(MathDetector("cost_prcl", Pprc_11 * (Px + Py) / 2))
# minimize the PRG for the 45 and the carrier through the the OMC for RSE
model.add(
MathDetector(
"cost_srcl",
Constant(model.PRG55)
* 1
/ Constant(model.a_u55_00_src)
* 1
/ Constant(model.a_l55_00_src),
)
)
Now the detectors are connected correctly, we can go ahead and run the locking scripts. We will lock in PRMI configuration.
factory = myForty2()
factory.reset()
# Enable detectors for LSC, this is required for locking
factory.options.LSC.add_output_detectors = True
# Enable power detectors so we can easily
# check the IFO state
factory.options.add_detectors = True
# We will using a PRMI action defined later
# so this must be false
factory.options.LSC.add_locks = False
# Print out build options
print("===== Config =======")
pprint.PrettyPrinter(indent=4, sort_dicts=True).pprint(
factory.options.toDict()
)
# Build model
print("===== Building model =======")
model = factory.make()
model.modes(maxtem=2) # Lets do some WFS modelling.
print("Success")
print("===== Adding locks and configuring operating point =======")
add_PRMI_locks(model)
outputs = model.run(InitialLockPRMI())
print("===== Writting out data =======")
for detector in get_PDs(model):
print(f"{detector} = {fmtpower(outputs['after locks'][detector])}")
plot_PRMI_error_signals(model)
===== Config =======
{ 'BS_trans_arm': 'X',
'BS_type': 'thick',
'ETMAR': False,
'INPUT': {'add_IMC_and_IM1': False, 'set_IMC_mode': True},
'LSC': { 'add_AC_loops': False,
'add_DOFs': True,
'add_locks': False,
'add_output_detectors': True,
'add_readouts': True,
'close_AC_loops': False},
'OUTPUT': {'readout': 'AS'},
'PR2_PR3_substrates': True,
'add_11MHz': True,
'add_165MHz': False,
'add_33MHz': True,
'add_55MHz': True,
'add_detectors': True,
'add_transmon': False,
'fake_prc_gouy': False,
'fake_src_gouy': False,
'materials': { 'test_mass_substrate': <finesse.materials.Material object at 0x7f26f266fdb0>},
'modulation_order': 3,
'suspensions': { 'auxillary_optics': False,
'core_optics': False,
'test_masses': False},
'wedges': False}
===== Building model =======
Success
===== Adding locks and configuring operating point =======
===== Writting out data =======
PowerDetector('Px') = 0.0 W
PowerDetector('Pinx') = 15.1 W
PowerDetector('Py') = 0.0 W
PowerDetector('Piny') = 15.1 W
PowerDetector('Pprc') = 30.2 W
PowerDetector('Psrc') = 3.2e-11 W
PowerDetector('ASqpdx') = 0.0 W
PowerDetector('ASqpdy') = 0.0 W
PowerDetector('Pin') = 799.6 mW
PowerDetector('PreflPRM') = 253.7 mW
PowerDetector('Prefl') = 253.7 mW
PowerDetector('Ppop') = 28.9 mW
PowerDetector('Pas') = 2.1e-07 W
The sensitivity of WFS¶
We can check out WFS are working correctly by tuning the position of AS1. This should produce a linear error signal in the “x” WFS. We expect that as the tuning becomes comparable with the divergance angle, our linearity will break down.
out = model.run("xaxis(AS1.xbeta, lin, -20u, 20u, 100)")
out.plot('ASqpdx', 'ASqpdy')
{finesse.detectors.powerdetector.PowerDetector: <Figure size 576x355.968 with 1 Axes>,
'ASqpdx': <Figure size 576x355.968 with 1 Axes>,
'ASqpdy': <Figure size 576x355.968 with 1 Axes>}
As expected, we can see a linear error signal. We can now explore something more complicated. For instance, what is the effect of tuning the beam-splitter angle on AS WFS? The Y beam will pass from the PRM to the ITMY without being affected by the tuning. However, when going from ITMY to AS1 it will be tilted. Meanwhile, the XARM beam see’s the tilt before reflecting of the ITMX, so it has a longer lever arm and is also affected by the curvature of the ITM.
out = model.run("xaxis(BS.xbeta, lin, -20u, 20u, 100)")
out.plot('ASqpdx', 'ASqpdy')
{finesse.detectors.powerdetector.PowerDetector: <Figure size 576x355.968 with 1 Axes>,
'ASqpdx': <Figure size 576x355.968 with 1 Axes>,
'ASqpdy': <Figure size 576x355.968 with 1 Axes>}