Modifying the Input Path (Frequency Noise)¶
In this example, we will modify the input path of the IFO, we will add a 10cm Fabry-Perot pre-mode-cleaner and then explore the frequency noise coupling into DARM.
Warning
At the time of writing the IMC is not in the model, so this is an example rather than a scientific study.
Note
This example blends the use of KatScript with the API interface to demonstrate usage of both. See the other examples for similar code in a KatScript interface
We begin with our usual imports, the same as in the other examples.
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, PowerDetectorDemod2
)
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
We can then go ahead and see the IMC parameters, either by printing them like this or by checking the Default Parameter file
base_factory = FortyMeterFactory()
INPUT_PARAMS = base_factory.params.INPUT
print(f"""The parameters of the IMC I/O couplers are:
MC1:
{INPUT_PARAMS.IMC.MC1}
MC3:
{INPUT_PARAMS.IMC.MC3}
""")
The parameters of the IMC I/O couplers are:
MC1:
Munch({'T': '2000e-6', 'L': 0, 'AOI': 44.835, 'Rc': 8000.0})
MC3:
Munch({'T': '2000e-6', 'L': 0, 'AOI': 44.835, 'Rc': 8000.0})
We can see that the default values are a little high for a 10cm cavity if we want meaningful frequency noise suppression, so let’s go ahead and reduce that by 100x.
Modifying the Optical Path¶
Similar to the approach in Adding the AS beam on the AP Table (and QPDs), we observe the
finesse_40m.factory.base.FortyMeterFactory. We can see
the method finesse_40m.factory.base.FortyMeterFactory.add_input_path()
and inspect it. This method is responsible for adding the laser through to the PRM
so that is the method we will overwrite.
We copy the code into a new class, called myForty and overwrite the method. We can then modify the code to our needs.
class myForty(FortyMeterFactory):
def add_input_path(self, model, INPUT, PRM_AR_port):
"""Setup the input path, starting from the laser and going to the PRM."""
LASER = model.add(fc.Laser("L0", P=INPUT.LASER.power))
f1 = model.add_parameter("f1", INPUT.LASER.f1).ref
f2 = model.add_parameter("f2", 5 * f1).ref
f3 = model.add_parameter("f3", INPUT.LASER.f3).ref
modulators = self.add_modulators(model, INPUT, f1, f2, f3)
### Begin edits - add our PMC
# Add a modulator for PMC locking
model.add(
fc.Modulator(
"mod_PMC",
midx=INPUT.LASER.mod_depth_11,
f=3*f1/2,
mod_type="pm",
order=float(self.options.modulation_order),
)
)
# add cavity
model.parse(f"""
# Mirrors
mirror PMC_M1 R=1-PMC_M1.T T={float(INPUT.IMC.MC1.T)/100}
mirror PMC_M2 R=1-PMC_M1.T T=PMC_M1.T L=0.0 phi=0
# Spaces
space s_cav portA=PMC_M1.p2 portB=PMC_M2.p2 L=0.1 nr=1
# Photodiodes
power_detector_dc PMC_circ node=PMC_M1.p2.o pdtype=none
power_detector_dc PMC_trans node=PMC_M2.p1.o pdtype=none
power_detector_dc PMC_refl node=PMC_M1.p1.o pdtype=none
""")
# connect up new cavity
model.link(LASER.p1, 0.1, model.mod_PMC, 0.1, model.PMC_M1.p1)
if len(modulators) == 0:
LASER_port_out = model.PMC_M2.p1
else:
model.link(model.PMC_M2.p1, *modulators)
LASER_port_out = modulators[-1].p2
# end edits, the following code is copied straight from the old method
IFI = model.add(fc.DirectionalBeamsplitter("IFI"))
if self.options.INPUT.add_IMC_and_IM1:
raise NotImplementedError
else:
model.link(LASER_port_out, IFI.p1)
model.link(IFI.p3, INPUT.IMC.length_MMT2_TT2, PRM_AR_port)
REFL_port = model.add(fc.Nothing("REFL_port"))
model.connect(model.IFI.p4, REFL_port.p1)
self.REFL_port = REFL_port.p1
if self.options.INPUT.set_IMC_mode:
# More edits - disable IMC
raise NotImplementedError("Neither mirror curvature "
"nor telescope is implemented "
"for PMC, modes can't be used")
We have not coded up a mode matching telescope and so we will use plane waves.
Checking the build and locking the IFO¶
We can then go ahead and build the model and put it in PRMI configuration, as in the other examples.
factory = myForty()
factory.reset()
# No HOM
factory.options.INPUT.set_IMC_mode = False
# 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(modes="off") # Disable TEM modes
===== Config =======
{ 'BS_trans_arm': 'X',
'BS_type': 'thick',
'ETMAR': False,
'INPUT': {'add_IMC_and_IM1': False, 'set_IMC_mode': False},
'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 0x7f3c9cf8fdb0>},
'modulation_order': 3,
'suspensions': { 'auxillary_optics': False,
'core_optics': False,
'test_masses': False},
'wedges': False}
===== Building model =======
Contrary to Adding the AS beam on the AP Table (and QPDs), we don’t have HOMs so we can’t use beam tracing to check that everything is connected correctly. Instead we use Networkx to display the nodes in our model.
# Build model
print("===== View Tree =======")
fig = plt.figure(figsize=(5,7))
model.plot_graph(root="PMC_M1", radius = 4,
graphviz=False,
label_font_size=10,attr_font_size=8,edge_font_size=8,
)
===== View Tree =======
<Figure size 576x355.968 with 0 Axes>
<Figure size 576x355.968 with 0 Axes>
Everything looks okay, so lets run a cavity scan. We are expecting 100% transmission since it is a lossless cavity and some sidebands for the modulator that we added.
out = model.run(fac.Xaxis(model.L0.f, "lin", -40e6, 40e6, 1800))
out.plot("PMC_circ", "PMC_refl", "PMC_trans", logy=True)
print(model.modes())
None
Since that was as expected, we now go-ahead and lock the IFO in PRMI configuration
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)
===== Adding locks and configuring operating point =======
===== Writting out data =======
PowerDetector('Px') = 0.0 W
PowerDetector('Pinx') = 14.8 W
PowerDetector('Py') = 0.0 W
PowerDetector('Piny') = 14.8 W
PowerDetector('Pprc') = 29.7 W
PowerDetector('Psrc') = 3.0e-11 W
PowerDetector('PMC_circ') = 39.1 kW
PowerDetector('PMC_trans') = 782.8 mW
PowerDetector('PMC_refl') = 17.2 mW
PowerDetector('Pin') = 782.4 mW
PowerDetector('PreflPRM') = 246.5 mW
PowerDetector('Prefl') = 246.5 mW
PowerDetector('Ppop') = 28.3 mW
PowerDetector('Pas') = 1.8e-07 W
If the error signal looks good, and crosses 0 and 0 offset then we know we are at the right operating point and we can then go ahead and look at our new cavity.
Measure the Response of the IFO to DARM¶
We want to measure the frequency noise coupling into DARM. So, for PRMI configuration DARM is measured by AS55. So first we will measure the response of DARM modulation (in meters/rt(Hz)) to AS55 (in Watts/rt(Hz)). So our transfer function is in Watts/meters.
We can use the Temporary and Change actions in Finesse to quickly run the simulation for the no PMC and PMC cases.
Note
See this paper to get from demodulated watts into a measurable photocurrent Quantum-noise power spectrum of fields with discrete classical components
Note
We could also do this with Frequency Response actions, but then we would need to use a ReadoutRF instead of a detector
Here we make use of the API instead of KatScirpt. The same code can be found in KatScirpt format in 40m Quantum Limited Sensitivity - Simple Michelson.
# Add a signal PD to measure strain, in our case this is AS55
model0 = model.deepcopy()
model0.fsig.f = 1
fsig = model0.fsig.f.ref
model0.add([
PowerDetectorDemod2('signal', model0.AS_port.p1, model0.f2, 0, fsig)
])
# Add some modulation to the inut
model1 = model0.deepcopy()
model1.add([
fc.signal.SignalGenerator("darmx", model1.ITMX.mech.z),
fc.signal.SignalGenerator("darmy", model1.ITMY.mech.z, phase=180)
])
response = model1.run(
fac.Series(
fac.Xaxis(model1.darmx.f, 'log', 0.3, 100e3, 100, name='PMC'),
fac.Temporary(
fac.Change({"PMC_M1.T": 1}),
fac.Xaxis(model1.darmx.f, 'log', 0.3, 100e3, 100, name='No PMC')
)
),progress_bar=True)
Measure the Frequency Noise Coupling¶
We can now go ahead and compute the frequency noise coupling from the laser into AS55. The frequency noise is in Hz/sqrt(Hz), to the transfer function is in Watts/Hz.
# Add some modulation to the input
model2 = model0.deepcopy()
model2.add([
fc.signal.SignalGenerator("fnoise", model2.L0.frq),
])
fnoise = model2.run(
fac.Series(
fac.Xaxis(model2.fnoise.f, 'log', 0.3, 100e3, 100, name='PMC'),
fac.Temporary(
fac.Change({"PMC_M1.T": 1}),
fac.Xaxis(model2.fnoise.f, 'log', 0.3, 100e3, 100, name='No PMC')
)
),progress_bar=True)
Project the Frequency Noise into DARM¶
Lastly we can divide the noise (W/Hz) by the response (W/m) to compute project the noise coupling into DARM (m/Hz).
import numpy as np
fig, ax = plt.subplots(nrows=3, figsize=(7,10))
for k in ["PMC", "No PMC"]:
_tf_noise = np.abs(fnoise[k]['signal'])
_tf_response = 0.5*np.abs(response[k]['signal'])
ax[0].loglog(response[k].x0, _tf_response, label=k)
ax[1].loglog(fnoise[k].x0, _tf_noise, label=k)
ax[2].loglog(fnoise[k].x0, _tf_noise/_tf_response, label=k)
ax[0].set_ylabel("Response [W/m]")
ax[1].set_ylabel("Freq Noise to AS55 [W/Hz]")
ax[2].set_ylabel("Freq Noise to Strain [m/Hz]")
for _ax in ax:
_ax.legend()