40m Custom Suspensions

Overview of Suspensions in Finesse LIGO-40m

The default finesse_40m.factory.base.FortyMeterFactory uses three functions to define the suspension plant for the interferometer.

The first is finesse_40m.factory.base.FortyMeterFactory.add_suspensions() which reads the options and decides whether to suspend the test masses, core optics, and auxillary optics.

This method then calls finesse_40m.factory.base.FortyMeterFactory.SOS_plant_ZPK() with the appropriate options from the Default Parameter file. In the parameter file, one can specify the mass, quality factor, optic thickness and optic diameter, without amending any code at all.

finesse_40m.factory.base.FortyMeterFactory.SOS_plant_ZPK() then calls finesse_40m.factory.base.FortyMeterFactory.SOS_ZPK_dof() for the longitudinal, pitch and yaw degrees of freedom. finesse_40m.factory.base.FortyMeterFactory.SOS_ZPK_dof() then computes the relevent inertia’s and uses this to compute the ZPK transfer function.

In the 40m, most of the optics are 75mm in diameter, 25mm in thickness with a mass of 264g. Since this is common, it isn’t set in the parameter file but is assumed in the _SOS_ZPK function.

The default ZPK is no zero’s and a pair of complex conjugate poles, with the gain equalling the reciprocal of the mass or MOI.

In most cases, you should be able to achieve what you want without modifying the suspension code. In this example, we will compare the transfer function of force applied to the PRM motion to the michelson degree of freedom (AS55), in the PRMI configuration.

Example - PRM to AS55_Q Transfer Function

This example is inspired by some unusual results found in the 40m noise budgeting work. For the noise budget, it was desirable to split up the actuation of Force to meters and meters to watts at the output into separate TFs. However, when doing this the meters to watts transfer function looks strange.

We start by doing our usual imports

from matplotlib import pyplot as plt
import finesse
import finesse.analysis.actions as fac
import finesse.components as fc
import finesse_40m
from finesse_40m.logging import setup_logger
setup_logger()
from finesse_40m.actions import InitialLockPRMI
from finesse_40m.factory import FortyMeterFactory
from finesse_40m.locks import add_PRMI_locks
from finesse_40m.utils import get_PDs, get_readouts, fmtpower, get_file_path

Defining the factories

We can then define some factories, which will describe the plant in different optical configurations.

The first configuration has only the PRM suspend and only in 1 degree of freedom. The second has only the PRM, but with all three degrees of freedom, the last configuration has all the core optics suspended.

class myForty1(FortyMeterFactory):
    def add_suspensions(self, model, params):
        """Suspend only the PRM
        """
        self.SOS_plant_ZPK(model, model.PRM, params.PRC.PRM)

    def SOS_plant_ZPK(self, model, optic, param):
        """ Longitudinal only """
        plant = {}
        plant["z", "F_z"] = self.SOS_ZPK_dof(optic, param, "pos")
        #plant["pitch", "F_pitch"] = self.SOS_ZPK_dof(optic, param, "pitch")
        #plant["yaw", "F_yaw"] = self.SOS_ZPK_dof(optic, param, "yaw")

        model.add(fc.mechanical.SuspensionZPK(optic.name + "_sus", optic.mech, plant))

class myForty2(FortyMeterFactory):
    def SOS_plant_ZPK(self, model, optic, param):
        """ Longitudinal only """
        plant = {}
        plant["z", "F_z"] = self.SOS_ZPK_dof(optic, param, "pos")
        #plant["pitch", "F_pitch"] = self.SOS_ZPK_dof(optic, param, "pitch")
        #plant["yaw", "F_yaw"] = self.SOS_ZPK_dof(optic, param, "yaw")

        model.add(fc.mechanical.SuspensionZPK(optic.name + "_sus", optic.mech, plant))

Building the Models

We can now go ahead and build the models. In the 40m Quantum Limited Sensitivity - PRMI example, I show the explicit checking of the error signals, so I will skip that step here and just assert that the power in the PRC is more than 14W.

models = []
names = ("PRM 1D", "Core Opt. 1D", "Core Opt. 3D")
for factory, name in zip([myForty1(), myForty2(), FortyMeterFactory()],names):
    print("====== model", name, "======")
    factory.reset()
    factory.options.LSC.add_output_detectors = True
    factory.options.add_detectors = True
    factory.options.LSC.add_locks = False
    factory.options.suspensions.test_masses = False
    factory.options.suspensions.core_optics = True

    # Build model
    print("===== Building model =======")
    model = factory.make()
    model.modes(modes=None)  # basic maxtem

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

    assert outputs['after locks'][model.Pinx] > 14, "Lock failed"
    # usually one checks the error signal, however, this snipit is in a lot of examples

    models.append(model)
====== model PRM 1D ======
===== Building model =======
===== Adding locks and configuring operating point =======
====== model Core Opt. 1D ======
===== Building model =======
===== Adding locks and configuring operating point =======
====== model Core Opt. 3D ======
===== Building model =======
===== Adding locks and configuring operating point =======

Running the Simulations

We can now loop over the models and modulate the motion of the PRM and see what happens at AS55_Q. We will do this applying a force and also modulating the position directly.

outs = {}
for _model, name in zip(models, names):
    modelFz = _model.deepcopy()
    modelFz.parse("""
    fsig(1)
    sgen Fprm PRM.mech.F_z
    pd2 signal AS_port.p1 f2 90 fsig
    """)
    axFz = fac.Xaxis(modelFz.Fprm.f, 'log', 0.1, 10e3, 2500)

    modelz = _model.deepcopy()
    modelz.parse("""
    fsig(1)
    sgen Fprm PRM.mech.z
    pd2 signal AS_port.p1 f2 90 fsig
    """)
    axz = fac.Xaxis(modelz.Fprm.f, 'log', 0.1, 10e3, 2500)
    outs[name] = (modelFz.run(axFz,progress_bar=True), modelz.run(axz,progress_bar=True))

Plotting the Results

import numpy as np

def line_style_iterator():
    styles = ['-', '--', '-.']
    while True:
        for style in styles:
            yield style
myLines = line_style_iterator()

fig, ax = plt.subplots(nrows=2,sharex=True, ncols=2, figsize=(10,7))
for name, out in outs.items():
    outFz, outz = out
    ls = next(myLines)
    ax[0][0].semilogx(outFz.x0, np.abs(outFz['signal']), ls=ls, label=name)
    ax[1][0].semilogx(outFz.x0, np.angle(outFz['signal'], deg=True),ls=ls, label=name)

    ax[0][1].semilogx(outz.x0, np.abs(outz['signal']), ls=ls, label=name)
    ax[1][1].semilogx(outz.x0, np.angle(outz['signal'], deg=True),ls=ls, label=name)

ax[0][0].set_title('TF PRM.mech.F_z -> AS55_Q')
ax[0][1].set_title('TF PRM.mech.z -> AS55_Q')
ax[0][0].set_ylabel('Magnitude')
ax[1][0].set_ylabel('Phase [deg]')
ax[1][0].set_xlabel('Freq [Hz]')
ax[1][1].set_xlabel('Freq [Hz]')
for _ax in ax.flatten():
    _ax.grid(True)
    _ax.legend()
../_images/custom_suspensions_4_0.png

First, consider the left hand column which shows the transfer function of Newton force into demodulated watts in AS55_Q. We can see a resonance, with a quality factor of 5 which corresponds to the local damping. We can see that the response does not depend on anything else, as we expect.

If we consider the second column, we can see that the transfer function corresponds to two very close poles. However, the effect is very small in comparison to the overall transfer function.