Noise in the PRMI Loop

In this example, I will show the implementation of AC loops in Finesse.

We will:
  • Build the Finesse-40m model with the PRCL loop engaged

  • Measure the open loop transfer functions (OLTF’s)

  • Compare the OLTF to a measurement of the OLTF (taken using the default diaggui template)

  • Download and calibrate a measurement of the closed loop noise measured at the actuator

  • Use python-foton to get the correct filters and calibrate the sensing noise

  • Project both the noise measured at the actuator, and the noise measured at the sensor to the DARM measurement.

Build the 40m Model with PRCL loop

We begin by building our model. If you have not seen this already, please review 40m Quantum Limited Sensitivity - PRMI.

import numpy as np
from scipy.interpolate import interp1d
import foton
import h5py

import finesse
import finesse.analysis.actions as fac
from finesse.detectors import PowerDetector, PowerDetectorDemod1, QuantumNoiseDetectorDemod1
import finesse.components as fc

import finesse_40m
from finesse_40m.logging import setup_logger, get_logger, logging
setup_logger(context="docs") # For documentation only
# call without any arguments in your code
# or, define your own function!

# Disable warnings about missing suspension parameters
get_logger().handlers[0].setLevel(logging.WARNING)

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
    add_PRMI_LSC_AC_feedback, # add loops
    print_AC_loop_info, # AC loop info
)

# Functions for checking the IFO state
from finesse_40m.utils import (
    get_PDs,
    get_readouts,
    fmtpower,
    get_file_path
)

import matplotlib.pyplot as plt
finesse.init_plotting()
import pprint
from munch import Munch
pp = pprint.PrettyPrinter(indent=4, sort_dicts=True).pprint

We can then make our 40m using the factory. See the Default Parameter file for details.

factory = FortyMeterFactory()
factory.reset()

# Enable detectors for LSC, this is required for locking
factory.options.LSC.add_output_detectors = True

# Set close AC loops true so loops actually get enabled
factory.options.LSC.close_AC_loops = True

# Enable power detectors so we can easily
# check the IFO state
factory.options.add_detectors = True

# Enable test masses, as defined in the config file
# NB You should receive some warnings here informing
# you that attributes such as sus_Q_pitch are not defined
# this is okay. These values will be set to reasonable
# default values corresponding to cylindrical core test masses
factory.options.suspensions.test_masses = True
factory.options.suspensions.core_optics = True

We can now build the Finesse model. The verbosity on warnings will be lowered during this step. We assume that the default suspension parameters are ok.

# Build model
print("===== Building model =======")
# Disable warnings about missing suspension parameters
get_logger().handlers[0].setLevel(logging.WARNING)
print_AC_loop_info(factory)
model2 = factory.make()
model2.modes(modes=None)  # basic maxtem
===== Building model =======
===== Local Drives =======
{   'BS': {'BS.mech.F_z': 1},
    'ETMX': {'ETMX.mech.F_z': 1},
    'ETMY': {'ETMY.mech.F_z': 1},
    'ITMX': {'ITMX.mech.F_z': 1},
    'ITMY': {'ITMY.mech.F_z': 1},
    'PRM': {'PRM.mech.F_z': 1},
    'SRM': {'SRM.mech.F_z': 1}}


===== Input Matrix =======
{   'CARM': {'REFL9.I': 1},
    'DARM': {'AS55.Q': 1},
    'MICH': {},
    'MICH2': {'POP45.Q': 1},
    'PRCL': {'POP9.I': 1},
    'SRCL': {'POP45.I': 1},
    'XARM': {},
    'YARM': {}}


===== Output Matrix =======
{   'CARM': {'ETMX': 0.5, 'ETMY': 0.5},
    'DARM': {'ETMX': 1, 'ETMY': -1},
    'MICH': {'BS': np.float64(1.4142135623730951), 'PRM': -1},
    'MICH2': {'BS': np.float64(1.4142135623730951), 'PRM': -1, 'SRM': 1},
    'PRCL': {'PRM': 1},
    'SRCL': {'SRM': 1},
    'XARM': {'ETMX': 1},
    'YARM': {'ETMY': 1}}


===== LSC Controller =======
Munch({'PRCL': ('PRCL_controller', array([ -7.47811493+2.96000660e+02j,  -7.47811493-2.96000660e+02j,
       -94.24777961+1.88495559e+02j, -94.24777961-1.88495559e+02j,
       -15.627557  +2.33526340e+02j, -15.627557  -2.33526340e+02j,
        -6.21305596+2.22180826e+02j,  -6.21305596-2.22180826e+02j,
       -10.40876057+1.85848112e+02j, -10.40876057-1.85848112e+02j,
        -6.10353902+1.47528651e+02j,  -6.10353902-1.47528651e+02j,
       -20.67678576+1.00659801e+02j, -20.67678576-1.00659801e+02j,
       -62.83185307+0.00000000e+00j,  -9.67038767+2.65692031e+01j,
        -9.67038767-2.65692031e+01j, -10.49366637+1.71505674e+01j,
       -10.49366637-1.71505674e+01j,  -6.28318531+3.53127738e-07j,
        -6.28318531-3.53127738e-07j]), array([-1.88495559e+04+0.00000000e+00j, -6.28322932e+03+7.62376505e-02j,
       -6.28322932e+03-7.62376505e-02j, -6.28309728e+03+0.00000000e+00j,
       -7.47811493e-01+2.96094163e+02j, -7.47811493e-01-2.96094163e+02j,
       -4.94186744e-01+2.34048131e+02j, -4.94186744e-01-2.34048131e+02j,
       -1.96474081e+00+2.22258996e+02j, -1.96474081e+00-2.22258996e+02j,
       -3.29153910e+00+1.86110260e+02j, -3.29153910e+00-1.86110260e+02j,
       -1.08537978e+00+1.47650865e+02j, -1.08537978e+00-1.47650865e+02j,
       -6.53857377e-01+1.02759415e+02j, -6.53857377e-01-1.02759415e+02j,
       -1.88495559e+01+6.28318531e+01j, -1.88495559e+01-6.28318531e+01j,
       -1.04936664e+00+2.00787904e+01j, -1.04936664e+00-2.00787904e+01j,
       -6.28318531e+00+0.00000000e+00j,  0.00000000e+00+0.00000000e+00j,
        0.00000000e+00+0.00000000e+00j,  0.00000000e+00+0.00000000e+00j]), -15220658503)})
array([[0, 0]], dtype=int32)

Above we can see the default values for the AC loops. We will need to override the input matrix values, since they correspond to LIGO and not 40m.

Before proceeding further, we must first model the DC loops. This ensures that the interferometer is in the correct operating point. The DC operating point must be correct for our LTI system analysis.

print("===== Adding DC locks and configuring operating point =======")
add_PRMI_locks(model2)
outputs = model2.run(InitialLockPRMI())

print("===== Writting out data =======")
for detector in get_PDs(model2):
    print(f"{detector} = {fmtpower(outputs['after locks'][detector])}")

plot_PRMI_error_signals(model2)
===== Adding DC 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') = 1.5e-11 W
PowerDetector('Pin') = 799.6 mW
PowerDetector('PreflPRM') = 246.0 mW
PowerDetector('Prefl') = 246.0 mW
PowerDetector('Ppop') = 28.9 mW
PowerDetector('Pas') = 2.0e-07 W
../_images/prmi_loop_noise_3_2.svg

The above error signals should pass through zero. REFL11 should look clearly like a PDH error signal. AS55 has a much lower gradient than REFL11, but should still look like an error signal.

We now begin our LTI moddelling. First, we must set fsig as it is required for the FrequencyResponse function. The function finesse_40m.locks.add_PRMI_LSC_AC_feedback() will add the appropriate locks for the PRMI configuration.

# Add some modulation to the inut
model2.fsig.f = 1
fsig = model2.fsig.f.ref

get_logger().handlers[0].setLevel(logging.INFO)
print("===== Add AC Loops =======")
model_bak2 = model2.deepcopy()

add_PRMI_LSC_AC_feedback(model2, factory)

print("===== View Tree =======")
model2.plot_graph(root="PRM", radius = 5,layout='neato',overlap=False,
                graphviz=True,
                size=(20,10),
                #label_font_size=10,attr_font_size=8,edge_font_size=8,
                format='png',

)
===== Add AC Loops =======
2024-11-30 20:07:25,287 - finesse-40m - INFO - Removing DARM from input matrix. Old value was Munch({'AS55.Q': 1})
2024-11-30 20:07:25,288 - finesse-40m - INFO - Removing DARM from output matrix. Old value was Munch({'ETMX': 1, 'ETMY': -1})
2024-11-30 20:07:25,289 - finesse-40m - INFO - Removing CARM from input matrix. Old value was Munch({'REFL9.I': 1})
2024-11-30 20:07:25,289 - finesse-40m - INFO - Removing CARM from output matrix. Old value was Munch({'ETMX': 0.5, 'ETMY': 0.5})
2024-11-30 20:07:25,290 - finesse-40m - INFO - Removing MICH2 from input matrix. Old value was Munch({'POP45.Q': 1})
2024-11-30 20:07:25,290 - finesse-40m - INFO - Removing MICH2 from output matrix. Old value was Munch({'BS': np.float64(1.4142135623730951), 'PRM': -1, 'SRM': 1})
2024-11-30 20:07:25,291 - finesse-40m - INFO - Removing SRCL from input matrix. Old value was Munch({'POP45.I': 1})
2024-11-30 20:07:25,291 - finesse-40m - INFO - Removing SRCL from output matrix. Old value was Munch({'SRM': 1})
2024-11-30 20:07:25,292 - finesse-40m - INFO - Removing XARM from input matrix. Old value was Munch({})
2024-11-30 20:07:25,292 - finesse-40m - INFO - Removing XARM from output matrix. Old value was Munch({'ETMX': 1})
2024-11-30 20:07:25,293 - finesse-40m - INFO - Removing YARM from input matrix. Old value was Munch({})
2024-11-30 20:07:25,294 - finesse-40m - INFO - Removing YARM from output matrix. Old value was Munch({'ETMY': 1})
===== View Tree =======
../_images/prmi_loop_noise_4_13.png

We can now use the plot_graph method in Finesse to observe the what it has built. We should find a signal that goes from REFL11 through a controller and is then applied the mech property of the PRM.

Measure the Open Loop Transfer Functions

We can now measure the open loop transfer functions. At this stage you may be wondering where the controller is specified. The controller is included in the parameter file. It is a pole zero representation of controller and was determined in Elog 18503 <https://nodus.ligo.caltech.edu:8081/40m/18503>. If you go to that elog and download the file AC_response2.html, you can see the exact commands and diagnostic plots used to verify that controller transfer function prior to including it in Finesse-40m.

f_sim = np.logspace(-1.5,4, num=1000)
FullModelOL = model2.run(
    fac.FrequencyResponse(f_sim,[model2.REFL11.I, model2.PRM.mech.F_z, model2.PRM.mech.z],
                        [model2.PRM.mech.F_z, model2.PRM.mech.z, model2.REFL11.I, model2.AS55.I],
                        open_loop=True)
)
FullModelOL.plot(show_unity=True)
(<Figure size 1152x336 with 3 Axes>,
 array([[<Axes: title={'center': 'REFL11.I'}, xlabel='Frequency [Hz]', ylabel='OUTPUT/DOF'>,
         <Axes: title={'center': 'PRM.mech.F_z'}, xlabel='Frequency [Hz]'>,
         <Axes: title={'center': 'PRM.mech.z'}, xlabel='Frequency [Hz]'>]],
       dtype=object))
<Figure size 576x355.968 with 0 Axes>
../_images/prmi_loop_noise_5_2.svg

OLTF Comparison

We can now compare the OLTF simulted by Finesse to a measurement of the OLTF which was taken from diaggui in Elog 18450 <https://nodus.ligo.caltech.edu:8081/40m/18450>. This data is included on the Finesse-40m wiki and can be automatically downloaded by the Finesse-40m package.

# Measured in https://nodus.ligo.caltech.edu:8081/40m/18450
# Analyzed in https://nodus.ligo.caltech.edu:8081/40m/18503
prcl_oltf = np.genfromtxt(str(get_file_path("2024_10_15_1550_OLTF_PRCL_abs_rad.csv")), dtype=float, delimiter=None)
prcl_cohere = np.genfromtxt(get_file_path("2024_10_15_1550_OLTF_COHERE_PRCL_asis.csv"), dtype=float, delimiter=None)

# Find points with good coherence
good_cohere = prcl_cohere[:,1] > 0.95

from matplotlib import pyplot as plt
fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(8,10))
ax[0].semilogx(prcl_oltf[:,0], 20*np.log10(1/prcl_oltf[:,1]), c='b', label='Full Data Set')
ax[0].semilogx(f_sim, 20*np.log10(np.abs(FullModelOL['REFL11.I', 'REFL11.I'])), c='g', label='Finesse Sim.')
ax[1].semilogx(prcl_oltf[:,0], np.rad2deg(-prcl_oltf[:,2]), c='b', label='Full Data Set')
ax[1].semilogx(f_sim, np.unwrap(np.angle(FullModelOL['REFL11.I', 'REFL11.I'], deg=True), period=360), c='g', label='Finesse Sim.')
ax[0].semilogx(prcl_oltf[good_cohere,0], 20*np.log10(1/prcl_oltf[good_cohere,1]), c='r', ls='--',label='Good Coherence')
ax[1].semilogx(prcl_oltf[good_cohere,0], np.rad2deg(-prcl_oltf[good_cohere,2]), c='r', ls='--', label='Good Coherence')
ax[2].semilogx(prcl_cohere[:,0], prcl_cohere[:,1])
ax[2].set_ylabel('Coherence [/1]')
ax[1].set_ylabel('Phase [deg]')
ax[0].set_ylabel('Mangnitude [dB]')
ax[2].set_xlabel('Frequency [Hz]')
ax[0].set_title('PRCL OLTF')
for _ax in ax:
    _ax.grid(visible=True, which='both', axis='both')
    _ax.legend()
ax[0].set_ylim([-20, 60])
ax[0].set_xlim([0.5*prcl_oltf[0,0], 1.5*prcl_oltf[-1,0]])
2024-11-30 20:07:33,150 - finesse-40m - WARNING - Could not find /root/finesse-data/finesse-40m/2024_10_15_1550_OLTF_PRCL_abs_rad.csv.

Will try downloading it using finesse_40m.tools.download('2024_10_15_1550_OLTF_PRCL_abs_rad.csv').
Writing data to /root/finesse-data/finesse-40m
2024-11-30 20:07:33,430 - finesse-40m - INFO - File is gzipped. Unzipping
2024-11-30 20:07:33,432 - finesse-40m - INFO - checksum matches
2024-11-30 20:07:33,434 - finesse-40m - WARNING - Could not find /root/finesse-data/finesse-40m/2024_10_15_1550_OLTF_COHERE_PRCL_asis.csv.

Will try downloading it using finesse_40m.tools.download('2024_10_15_1550_OLTF_COHERE_PRCL_asis.csv').
Writing data to /root/finesse-data/finesse-40m
2024-11-30 20:07:33,688 - finesse-40m - INFO - File is gzipped. Unzipping
2024-11-30 20:07:33,690 - finesse-40m - INFO - checksum matches
(np.float64(5.0), np.float64(600.0))
../_images/prmi_loop_noise_6_12.svg

We can see that the measured OLTF and the simulated OLTF match quite well in the areas with high coherence. Our model is our best guess given what we know about the applied filters and the frequencies where we can take data at high coherence. However, we cannot state the transfer function with any certainty outside the areas where we have made a measurement.

We can now plot the closed loop transfer function very easily using Finesse.

FullModelCL = model2.run(
    fac.FrequencyResponse(f_sim,[model2.REFL11.I, model2.PRM.mech.F_z],
                        [model2.PRM.mech.F_z, model2.PRM.mech.z, model2.REFL11.I, model2.AS55.I],
                        # Note it's AS55_I here not AS55_Q, because of phase differences in Finesse.
                        # This could probably be fixed by "phasing" the RFPDs as is done in
                        # Finesse-LIGO
                        open_loop=False)
)
FullModelCL.plot(show_unity=True)
(<Figure size 960x336 with 2 Axes>,
 array([[<Axes: title={'center': 'REFL11.I'}, xlabel='Frequency [Hz]', ylabel='OUTPUT/DOF'>,
         <Axes: title={'center': 'PRM.mech.F_z'}, xlabel='Frequency [Hz]'>]],
       dtype=object))
<Figure size 576x355.968 with 0 Axes>
../_images/prmi_loop_noise_7_2.svg

The REFL11 to REFL11 trace in the left-most plot is the closed loop transfer function. We can see an actuator bump close to the unity gain frequency which indicates that the sign of the controller is correct.

Measure the Response of the IFO to DARM

We want to measure the cross-coupling of PRCL into DARM. In the PRMI configuration DARM is measured by AS55. Therefore, a calibration must be obtained from DARM modulation (in meters/rt(Hz)) to AS55 (in Watts/rt(Hz)). Therefore the dimensions of the transfer is Watts/meters.

Note

See also Modifying the Input Path (Frequency Noise) for an explicit formulation

We can make use of the finesse_40m.noise_projections.get_short_michelson_quantum_noise() and finesse_40m.noise_projections.get_short_michelson_response() functions.

from finesse_40m.noise_projections import (
    get_short_michelson_quantum_noise,
    get_short_michelson_response
)
response = get_short_michelson_response(model2, freq_low=f_sim[0], freq_high=f_sim[-1], npoints=len(f_sim))
qm_noise = get_short_michelson_quantum_noise(model2, freq_low=f_sim[0], freq_high=f_sim[-1], npoints=len(f_sim))

cltf_PRMFz_to_AS55Q = interp1d(f_sim, FullModelCL['AS55.I', 'PRM.mech.F_z'],fill_value=0,bounds_error=False)
oltf_PRMz_to_AS55Q = interp1d(f_sim, FullModelOL['AS55.I', 'PRM.mech.z'],fill_value=0,bounds_error=False)
oltf_PRMz_to_REFLII = interp1d(f_sim, FullModelOL['REFL11.I', 'PRM.mech.z'],fill_value=0,bounds_error=False)

Calibrate the Closed Loop Actuation Signal

The first step is to pass PRCL_OUT through the output matrix. We can do this using python-foton. When our data was taken, we know FM1, FM2 and FM6 were enabled. We can access these with the indices [0,1,5].

# Load the foton file from the wiki
LSC_PRM = foton.FilterFile(get_file_path('C1LSC.txt'))['LSC_PRM']
# enabled filters FM1, FM2, FM6

display_filter_f = np.logspace(np.log10(200),np.log10(5000), num=2000)
from matplotlib import pyplot as plt
fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8,10))
for i in [0,1,5]:
    _phasor = LSC_PRM[i].freqresp(display_filter_f)
    ls = '-' if i < 5 else '--'
    ax[0].plot(display_filter_f, 20*np.log10(np.abs(_phasor)), ls=ls, label=str(i)+': '+LSC_PRM[i].name)
    ax[1].plot(display_filter_f, np.angle(_phasor, deg=True), ls=ls, label=str(i)+': '+LSC_PRM[i].name)
ax[1].set_ylabel('Phase [deg]')
ax[0].set_ylabel('Mangnitude [dB]')
ax[1].set_xlabel('Frequency [Hz]')
for _ax in ax:
    _ax.grid(visible=True, which='both', axis='both')
    _ax.legend()
ax[1].set_xlim([200, 5000])
ax[0].set_ylim([-10, 5])
2024-11-30 20:08:01,508 - finesse-40m - WARNING - Could not find /root/finesse-data/finesse-40m/C1LSC.txt.

Will try downloading it using finesse_40m.tools.download('C1LSC.txt').
Writing data to /root/finesse-data/finesse-40m
2024-11-30 20:08:01,839 - finesse-40m - INFO - File is gzipped. Unzipping
2024-11-30 20:08:01,844 - finesse-40m - INFO - checksum matches
(-10.0, 5.0)
../_images/prmi_loop_noise_9_6.svg

These filters are simple notches at high frequency due to voilin modes.

Yuta and Hiroki very kindly provide a calibration of PRCL_OUT to PRM motion in Elog 17752 <https://nodus.ligo.caltech.edu:8081/40m/17752>. We can combine this with the measurement of PRCL motion stored as part of the Finesse-40m package to convert all our transfer functions into the unit of meters PRM displacement per square root Hertz.

with h5py.File(get_file_path("PRCL_MICH_Act_Noise.h5"), 'r') as hdf:
    print("Keys: ", list(hdf.keys()))
    print("Attrs: ", [_ for _ in hdf.attrs.items()])
    noise_freq2 = np.array(hdf['LSC-PRCL_OUT_DQ_ASD']) # Naming error when saved
    # Calibration given in https://nodus.ligo.caltech.edu:8081/40m/17752
    PRCL_CL_displacement = (
        np.array(hdf['freq'])
        *((41.4e-9)/np.power(noise_freq2, 2))
        *LSC_PRM[0].freqresp(noise_freq2)
        *LSC_PRM[1].freqresp(noise_freq2)
        *LSC_PRM[5].freqresp(noise_freq2)
    )

plt.loglog(noise_freq2, np.abs(PRCL_CL_displacement))
plt.title('In Loop PRM Motion (Actuation Sig.)')
plt.xlabel('Frequency, Hz')
plt.ylabel('PRM Displacement, m')

prm_force_noise_at_AS55_Q = PRCL_CL_displacement*oltf_PRMz_to_AS55Q(noise_freq2)
2024-11-30 20:08:02,229 - finesse-40m - WARNING - Could not find /root/finesse-data/finesse-40m/PRCL_MICH_Act_Noise.h5.

Will try downloading it using finesse_40m.tools.download('PRCL_MICH_Act_Noise.h5').
Writing data to /root/finesse-data/finesse-40m
2024-11-30 20:08:02,795 - finesse-40m - INFO - file is not gzipped
2024-11-30 20:08:02,800 - finesse-40m - INFO - checksum matches
Keys:  ['LSC-MICH_OUT_DQ_ASD', 'LSC-PRCL_OUT_DQ_ASD', 'freq']
Attrs:  [('author', 'Aaron Goodwin-Jones'), ('description', 'Updated response data'), ('elog', 'https://nodus.ligo.caltech.edu:8081/40m/18450')]
../_images/prmi_loop_noise_10_7.svg

Below the unity gain frequency, the actuation signal is the inverse of the free-running motion and so below ~100 Hz, this is the free running motion of the pendulum. We expect to see about 1 micrometer per root hertz at low frequency and it should fall off as roughly 1/f, which is what we observe.

However, the actuation signal is cancelling the force applied by the ground and so this noise would not show in up in the DARM signal. To get that, we must use the sensing signal.

Calibrate the Sensing Signal

We can calibrate the sensing signal by projecting it through the controller and making sure it overlaps with the actuation signal, which we have just calibrated.

First we get the PRCL filters from C1LSC.txt. In this instance FM1, FM2 FM4, FM5, FM6 and FM9 were in use. First we plot out the filters and inspect them.

LSC_PRCL = foton.FilterFile(get_file_path('C1LSC.txt'))['LSC_PRCL']

fig, ax = plt.subplots(nrows=2, sharex=True, figsize=(8,10))
for i in [0,1,3,4,6,5,7,8,9]:
    _phasor = LSC_PRCL[i].freqresp(noise_freq2)
    ls = '-' if i < 5 else '--'
    ax[0].semilogx(noise_freq2, 20*np.log10(np.abs(_phasor)), ls=ls,
            label=str(i)+': '+LSC_PRCL[i].name)
    ax[1].semilogx(noise_freq2, np.angle(_phasor, deg=True), ls=ls,
            label=str(i)+': '+LSC_PRCL[i].name)
ax[1].set_ylabel('Phase [deg]')
ax[0].set_ylabel('Mangnitude [dB]')
ax[1].set_xlabel('Frequency [Hz]')
for _ax in ax:
    _ax.grid(visible=True, which='both', axis='both')
    _ax.legend()
ax[0].set_ylim([-50,100])
(-50.0, 100.0)
../_images/prmi_loop_noise_11_2.svg

We can now load the data from the wiki and project it to mirror displacement. We find that we need a a DC gain factor of 700.

with h5py.File(get_file_path("PRCL_MICH_sensing_noise.h5"), 'r') as hdf:
    print("Keys: ", list(hdf.keys()))
    print("Attrs: ", [_ for _ in hdf.attrs.items()])
    noise_freq = np.array(hdf['freq']) # Naming error when saved
    PRCL_CL_sensing = np.array(hdf['LSC-REFL11_I_ERR_DQ'])/700
    controller = (
        LSC_PRCL[0].freqresp(noise_freq)
        *LSC_PRCL[1].freqresp(noise_freq)
        *LSC_PRCL[3].freqresp(noise_freq)
        *LSC_PRCL[4].freqresp(noise_freq)
        *LSC_PRCL[5].freqresp(noise_freq)
        *LSC_PRCL[8].freqresp(noise_freq)
        *LSC_PRM[0].freqresp(noise_freq)
        *LSC_PRM[1].freqresp(noise_freq)
        *LSC_PRM[5].freqresp(noise_freq)
        *((41.4e-9)/np.power(noise_freq, 2))
    )

    PRCL_CL_sensing_project_fowards = controller*PRCL_CL_sensing
    PRCL_CL_sensing_project_backwards = PRCL_CL_sensing/oltf_PRMz_to_REFLII(noise_freq)

plt.loglog(noise_freq2, np.abs(PRCL_CL_displacement), label='Calibrated Actuation Data')
plt.loglog(noise_freq, np.abs(PRCL_CL_sensing_project_fowards), ls = '--', label='REFL11 Proj. For. to Disp.')
plt.loglog(noise_freq, np.abs(PRCL_CL_sensing_project_backwards), ls = '--', label='REFL11 Proj. Back. to Disp.')
plt.title('In Loop PRM Motion ')
plt.xlabel('Frequency, Hz')
plt.ylabel('PRM Displacement, m')
plt.legend()
prm_sensing_noise_at_AS55_Q = PRCL_CL_sensing_project_backwards*oltf_PRMz_to_AS55Q(noise_freq)
2024-11-30 20:08:10,252 - finesse-40m - WARNING - Could not find /root/finesse-data/finesse-40m/PRCL_MICH_sensing_noise.h5.

Will try downloading it using finesse_40m.tools.download('PRCL_MICH_sensing_noise.h5').
Writing data to /root/finesse-data/finesse-40m
2024-11-30 20:08:10,821 - finesse-40m - INFO - file is not gzipped
2024-11-30 20:08:10,825 - finesse-40m - INFO - checksum matches
Keys:  ['LSC-AS55_Q_ERR_DQ', 'LSC-REFL11_I_ERR_DQ', 'freq']
Attrs:  [('author', 'Aaron Goodwin-Jones'), ('description', 'Calibration data for PRCL and MICH loops'), ('dt', np.float64(6.103515625e-05)), ('dt_unit', 'seconds'), ('elog', 'https://nodus.ligo.caltech.edu:8081/40m/18508'), ('fft_lenght_seconds', np.int64(10)), ('gwpy version', '3.0.10'), ('start_gps_time', np.float64(1413067207.0)), ('start_time_string', '2024-10-15T22:39:49')]
../_images/prmi_loop_noise_12_7.svg

The actuation noise sensing signal is great for understanding the free-running noise of the PRCL loop below the unity gain frequency. However, we are looking for an estimate of the in-loop PRCL noise projected to the DARM measurement. If we project fowards through the control loop, then the noise is suppressed by the controller.

A more accurate estimate is to take the now calibrated sensing noise, and project backwards through the plant to estimate the in-loop motion of the PRM with respect to the global control frame.

This additional trace is shown above. We can see that below the unity gain frequency the in-loop noise is suppressed.

Once we have that infomation, we can measure the optical cross-coupling in Finesse and project that noise into the DARM measurement.

Estimate the Quantum Noise in the PRCL Loop

Before we project everything to DARM, we will quickly show that the quantum limit of the PRCL sensing noise.

To do that, we add a Signal Generator and Quantum Noise Detector and simulate the quantum noise.

# Measure Noise
model3 = model2.deepcopy()
model3.add([
    fc.SignalGenerator('mySig', model3.PRM.mech.z),
    QuantumNoiseDetectorDemod1('prcl_qmd', model3.REFL_port.p1.i, model3.f1,0,nsr=False)
])
qm_sensing_noise_PRCL = model3.run(
    fac.Xaxis(model3.mySig.f, "log", start=f_sim[0], stop=f_sim[-1], steps=len(f_sim))
)

# Plot
qm_sensing_noise_PRCL.plot('prcl_qmd', logx=True, logy=True)

# Project to PRM Displacement
qm_sensing_noise_PRCL = interp1d(
    qm_sensing_noise_PRCL.x0, qm_sensing_noise_PRCL['prcl_qmd'],fill_value=0,bounds_error=False)(noise_freq)*controller
../_images/prmi_loop_noise_13_0.svg

We can now plot this alongside our other traces.

plt.loglog(noise_freq2, np.abs(PRCL_CL_displacement), label='Calibrated Actuation Data')
plt.loglog(noise_freq, np.abs(PRCL_CL_sensing_project_fowards), ls = '--', label='REFL11 Proj. For. to Disp.')
plt.loglog(noise_freq, np.abs(PRCL_CL_sensing_project_backwards), ls = '--', label='REFL11 Proj. Back. to Disp.')
plt.loglog(noise_freq, np.abs(qm_sensing_noise_PRCL), ls = '--', label='REFL11 QM Noise')
plt.title('In Loop PRM Motion ')
plt.xlabel('Frequency, Hz')
plt.ylabel('PRM Displacement, m')
plt.legend()
<matplotlib.legend.Legend at 0x7ff5e0134230>
../_images/prmi_loop_noise_14_1.svg

We find it is not limiting the measurement.

Project the Sensing 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).

plt.loglog(f_sim, qm_noise(f_sim)/response(f_sim), label='Quantum Noise (MICH)')
plt.loglog(noise_freq2, np.abs(prm_force_noise_at_AS55_Q), label='PRCL Actuation')
plt.loglog(noise_freq, np.abs(prm_sensing_noise_at_AS55_Q), label='PRCL Sensing')
plt.legend()
plt.title('PRCL Noises Projected to DARM Measurement (PRMI)')
plt.ylabel('Sensitivity to 4D S-T Interval, m/rt(Hz)')
plt.xlabel('Frequency, Hz')
Text(0.5, 0, 'Frequency, Hz')
../_images/prmi_loop_noise_15_2.svg