Course: Learn Laser Interferometry with Finesse


Finesse for LIGO: Introducing 'Site Files'

Author: Anna Green

This notebook was developed as part of a series of tutorial workshops at the University of Florida in 2019. It is currently in the process of being 'polished' to fit the Learn Interferometry course.

Goal For Today

Today the goal is to introduce the so-called 'site files': what they are, what they look like, where to find them, and how to start using them.

Purpose of the site files:

  • multi-user
  • long-term
  • multi-purpose
  • base for other case-specific work

--> set things up a little differently to how you might on your own, and need to bear in mind 'standard practices' that have been developed or are currently the norm.

Session Outline

From session 1: essentially we're covering the same things but for a large-scale, long-term, multi-user model:

  • How to approach building a model
    • --> how to approach large files bearing in mind the difference in use case
  • constructing an optical nodal network using Finesse syntax (the kat object)
    • --> getting to grips with the site files themselves :
  • using pykat to create, interact with and modify an existing kat object
    • --> the full ecosystem
  • some Finesse 'quirks' , e.g. Finesse's definitions of length and curvature

Tasks

In [1]:
# all the usual stuff we've used before

from pykat import finesse        # import the whole pykat.finesse package
from pykat.commands import *     # import all packages in pykat.commands
import numpy as np               # for basic math/sci/array functions
import matplotlib.pyplot as plt  # for plotting

# tell the notebook to automatically show plots inline below each cell
%matplotlib inline               
# use pykat's plotting style. change dpi to change plot sizes on your screen
pykat.init_pykat_plotting(dpi=90)
                                              ..-
    PyKat develop         _                  '(
                          \`.|\.__...-""""-_." )
       ..+-----.._        /  ' `            .-'
   . '            `:      7/* _/._\    \   (
  (        '::;;+;;:      `-"' =" /,`"" `) /
  L.        \`:::a:f            c_/     n_'
  ..`--...___`.  .    ,
   `^-....____:   +.      www.gwoptics.org/pykat

taking a look at the LIGO design.kat file

Accessing the file:

  1. The raw file: design.kat itself
    • gets pre-installed as part of PyKat
    • 'general public' version installed via miniconda/pip: path something like ~/miniconda3/envs/test/lib/python3.7/site-packages/pykat/ifo/aligo/design.kat
    • 'live'/editable version via the lvc git repo (also finesse)
    • 'raw' version, just directly what's in the kat file. Could load this in to a kat model using kat.load("<path>") but misses the initialisation steps we nearly always need, so instead:
  2. Raw file + initial processing to usable state: import via pykat, then use pykat tools

we'll come back to the processing and pykat tools later. for now, let's just take a look at the file itself.

In [2]:
import pykat.ifo.aligo as aligo
base = aligo.make_kat()
print(base)
% Generated by PyKat 03.09.2019 18:07:15

%%% FTblock laser
l L0 125.0 0.0 0.0 ni
bs jitter 1.0 0.0 0.0 0.0 ni n0 dump dump
s lmod1 1.0 n0 n1
mod mod1 9099471.0 0.18 1 pm 0.0 n1 n2
s lmod2 1.0 n2 n3
mod mod2 45497355.0 0.18 1 pm 0.0 n3 nLaserOut
%%% FTend laser

%%% FTblock IMC
s sIMCin 0.0 nLaserOut nMC1in
bs1 MC1 0.006 0.0 0.0 44.59 nMC1in nMC1refl nMC1trans nMC1fromMC3
s sMC1_MC2 16.24057 nMC1trans nMC2in
bs1 MC2 0.0 0.0 0.0 0.82 nMC2in nMC2refl nMC2trans dump
attr MC2 Rcx 27.24
attr MC2 Rcy 27.24
s sMC2_MC3 16.24057 nMC2refl nMC3in
bs1 MC3 0.006 0.0 0.0 44.59 nMC3in nMC3refl nMC3trans nMCreturn_refl
s sMC3substrate 0.0845 1.44963098985906 nMC3trans nMC3ARin
bs2 MC3AR 0.0 0.0 0.0 28.9661 nMC3ARin dump nIMCout dump
s sMC3_MC1 0.465 nMC3refl nMC1fromMC3
%%% FTend IMC

%%% FTblock HAM2
s sHAM2in 0.4282 nIMCout nIM11
bs1 IM1 0.0 0.0 0.0 53.0 nIM11 nIM12 dump dump
s sIM1_IM2 1.2938 nIM12 nIM21
bs1 IM2 0.0 0.0 0.0 7.0 nIM21 nIM22 dump dump
attr IM2 Rcx 12.8
attr IM2 Rcy 12.8
s sIM2_FI 0.26 nIM22 nFI1
dbs FI nFI1 nFI2 nFI3 nREFL
s sFI_IM3 0.91 nFI3 nIM31
bs1 IM3 0.0 0.0 0.0 7.1 nIM31 nIM32 dump dump
attr IM3 Rcx -6.24
attr IM3 Rcy -6.24
s sIM3_IM4 1.21 nIM32 nIM41
bs1 IM4 0.0 0.0 0.0 45.0 nIM41 nHAM2out dump dump
%%% FTend HAM2

%%% FTblock PRC
s sPRCin 0.4135 nHAM2out nPRM1
m2 PRMAR 0.0 4e-05 0.0 nPRM1 nPRMs1
s sPRMsub1 0.0737 1.44963098985906 nPRMs1 nPRMs2
m1 PRM 0.03 8.5e-06 0.0 nPRMs2 nPRM2
attr PRM Rcx 11.009
attr PRM Rcy 11.009
s lp1 16.6107 nPRM2 nPR2a
bs1 PR2 0.00025 3.75e-05 0.0 -0.79 nPR2a nPR2b nPOP nAPOP
attr PR2 Rcx -4.545
attr PR2 Rcy -4.545
s lp2 16.1647 nPR2b nPR3a
bs1 PR3 0.0 3.75e-05 0.0 0.615 nPR3a nPR3b dump dump
attr PR3 Rcx 36.027
attr PR3 Rcy 36.027
s lp3 19.5381 nPR3b nPRBS
%%% FTend PRC

%%% FTblock BS
bs1 BS 0.5 3.75e-05 0.0 45.0 nPRBS nYBS nBSi1 nBSi3
s BSsub1 0.0687 1.44963098985906 nBSi1 nBSi2
s BSsub2 0.0687 1.44963098985906 nBSi3 nBSi4
bs2 BSAR1 5e-05 0.0 0.0 -29.195 nBSi2 dump14 nXBS nPOX
bs2 BSAR2 5e-05 0.0 0.0 29.195 nBSi4 dump15 nSRBS dump16
%%% FTend BS

%%% FTblock Yarm
s ly1 5.0126 nYBS nITMY1a
lens ITMY_lens 34500.0 nITMY1a nITMY1b
s sITMY_th2 0.0 nITMY1b nITMY1
m2 ITMYAR 0.0 2e-05 0.0 nITMY1 nITMYs1
s ITMYsub 0.2 1.44963098985906 nITMYs1 nITMYs2
m1 ITMY 0.014 3.75e-05 0.0 nITMYs2 nITMY2
attr ITMY Rcx -1934.0
attr ITMY Rcy -1934.0
attr ITMY mass 40.0
s LY 3994.4692 nITMY2 nETMY1
m1 ETMY 5e-06 3.75e-05 0.0 nETMY1 nETMYs1
attr ETMY Rcx 2245.0
attr ETMY Rcy 2245.0
attr ETMY mass 40.0
s ETMYsub 0.2 1.44963098985906 nETMYs1 nETMYs2
m2 ETMYAR 0.0 0.0005 0.0 nETMYs2 nPTY
%%% FTend Yarm

%%% FTblock Xarm
s lx1 4.993 nXBS nITMX1a
lens ITMX_lens 34500.0 nITMX1a nITMX1b
s sITMX_th2 0.0 nITMX1b nITMX1
m2 ITMXAR 0.0 2e-05 0.0 nITMX1 nITMXs1
s ITMXsub 0.2 1.44963098985906 nITMXs1 nITMXs2
m1 ITMX 0.014 3.75e-05 0.0 nITMXs2 nITMX2
attr ITMX Rcx -1934.0
attr ITMX Rcy -1934.0
attr ITMX mass 40.0
s LX 3994.4692 nITMX2 nETMX1
m1 ETMX 5e-06 3.75e-05 0.0 nETMX1 nETMXs1
attr ETMX Rcx 2245.0
attr ETMX Rcy 2245.0
attr ETMX mass 40.0
s ETMXsub 0.2 1.44963098985906 nETMXs1 nETMXs2
m2 ETMXAR 0.0 0.0005 0.0 nETMXs2 nPTX
%%% FTend Xarm

%%% FTblock SRC
s ls3 19.3661 nSRBS nSR3b
bs1 SR3 0.0 3.75e-05 0.0 0.785 nSR3b nSR3a dump dump
attr SR3 Rcx 35.972841
attr SR3 Rcy 35.972841
s ls2 15.4435 nSR3a nSR2b
bs1 SR2 0.0 3.75e-05 0.0 -0.87 nSR2b nSR2a dump dump
attr SR2 Rcx -6.406
attr SR2 Rcy -6.406
s ls1 15.7586 nSR2a nSRM1
m1 SRM 0.2 8.7e-06 90.0 nSRM1 nSRMs1
attr SRM Rcx -5.6938
attr SRM Rcy -5.6938
s SRMsub 0.0749 1.44963098985906 nSRMs1 nSRMs2
m2 SRMAR 0.0 5e-08 0.0 nSRMs2 nSRM2
s sSRM_FI 0.7278 nSRM2 nFI2a
%%% FTend SRC

%%% FTblock FI
dbs FI2 nFI2a nFI2b nFI2c nFI2d
s sFI_OM1 2.9339 nFI2c nOM1a
bs1 OM1 0.0008 3.75e-05 0.0 2.251 nOM1a nOM1b nOM1c dump22
attr OM1 Rcx 4.6
attr OM1 Rcy 4.6
s sOM1_OM2 1.395 nOM1b nOM2a
bs1 OM2 1e-05 3.75e-05 0.0 4.399 nOM2a nOM2b nOM2c nOM2d
attr OM2 Rcx 1.7058
attr OM2 Rcy 1.7058
s sOM2_OM3 0.631 nOM2b nOM3a
bs1 OM3 1e-05 3.75e-05 0.0 30.037 nOM3a nOM3b nOM3c nOM3d
s sOM3_OMC 0.2034 nOM3b nOMC_ICa
%%% FTend FI

%%% FTblock OMC
bs1 OMC_IC 0.0076 1e-05 0.0 2.7609 nOMC_ICa nOMC_ICb nOMC_ICc nOMC_ICd
s lIC_OC 0.2815 nOMC_ICc nOMC_OCa
bs1 OMC_OC 0.0075 1e-05 0.0 4.004 nOMC_OCa nOMC_OCb nAS nOMC_OCd
s lOC_CM1 0.2842 nOMC_OCb nOMC_CM1a
bs1 OMC_CM1 3.6e-05 1e-05 0.0 4.004 nOMC_CM1a nOMC_CM1b nOMC_CM1c nOMC_CM1d
attr OMC_CM1 Rcx 2.57321
attr OMC_CM1 Rcy 2.57321
s lCM1_CM2 0.2815 nOMC_CM1b nOMC_CM2a
bs1 OMC_CM2 3.59e-05 1e-05 0.0 4.004 nOMC_CM2a nOMC_CM2b nOMC_CM2c nOMC_CM2d
attr OMC_CM2 Rcx 2.57369
attr OMC_CM2 Rcy 2.57369
s lCM2_IC 0.2842 nOMC_CM2b nOMC_ICd
%%% FTend OMC

%%% FTblock cavities
cav cavIMC MC2 nMC2in MC2 nMC2refl
cav cavXARM ITMX nITMX2 ETMX nETMX1
cav cavYARM ITMY nITMY2 ETMY nETMY1
cav cavSRX SRM nSRM1 ITMX nITMXs2
cav cavSRY SRM nSRM1 ITMY nITMYs2
cav cavPRX PRM nPRM2 ITMX nITMXs2
cav cavPRY PRM nPRM2 ITMY nITMYs2
cav cavOMC OMC_IC nOMC_ICc OMC_IC nOMC_ICd
%%% FTend cavities
yaxis abs

Task: discussion about the `design.kat` file

  • General comments
  • 'blocks'??
  • no xaxis, no detectors, just optics
  • 'composite optics' - multiple bs's used to define the central bs, ITMs, ETMs, etc
  • nodes with odd/special names, e.g. nREFL *

Task: identifying the key elements in the file

Use the design.kat file to sketch the LIGO core optical layout (PRM to SRM) and identify the names of the main components in the kat file

--> useful reference: my inkscape sketch: aLIGO_fullwsqz_Jan2019.svg (via LIGO git)

& if there's time

Go back and extend your sketch to include the input and output optical paths (i.e. blocks before the PRC and blocks after the SRC)

Enhanced functionality via the aligo package: make_kat() and beyond

The design.kat file itself is pretty stable - the aLIGO design is, in principal, still the same as it was in 2010. It can be used on it's own, with some caveats we'll cover later, but it's quite bare-bones. This is where accompanying scripts come in.

There are 4 files: ifo/aligo/__init__.py, ifo/aligo/plot.py, and the more general ifo/__init__.py and ifo/plot.py. Together they serve several purposes:

  1. to store scripts commonly used to manipulate the main kat object
    • e.g. to remove the blocks containing the input optics and automatically reconnect the nodes so the file still runs, and to set the file to a good initial operating point (see below)
  2. to store scripts to make common plots
    • e.g. to plot error signals for the main degrees of freedom in the IFO
  3. to store properties of the detector used to set the model up properly
  4. to store components, or set of components that are often needed but would significantly slow down the model if included by default every time
    • usually this is detectors - photodiodes and so on - that have a specific name and purpose. Adding detectors adds to the number of calculations, making it slower.

All of these scripts are a work in progress. Literally: I added new updates yesterday. So it's sometimes tricky to know what's possible. You might have noticed that you can often use the tab key to auto-complete function names in python. The same is true here! And it's especially useful for larger kat files where there's lots going on.

The main ways to find things are:

  • skim through the current kat object
  • 'tab around' kat.[...], kat.ALIGO_IFO.[...] and kat.IFO.[...] (NB: only works in cells outside the cell where the current kat is defined, even if you've already run that cell)
  • directly look in the __init__.py and plot.py files or using the inspect package, to see what your function is actually doing...

Finding functions: likely locations

location contents
ifo/aligo/__init__.py:
main functions
The main one to look at first.
Functions to create the aLIGO pykat object and prepare it for use, using info in the ALIGO_IFO class.
key functions: make_kat and setup
ifo/aligo/__init__.py:
ALIGO_IFO class
aLIGO-specific functions to edit the main kat object where those functions involve interactions between the kat and associated IFO object,
e.g. add/remove blocks, set lengths so a certain frequency is resonant, etc
ifo/aligo/plot.py common ligo-specific plotting functions, such as the QNLS curve or error signals vs DOFs
ifo/__init__.py Tools that are less detector-specific, plus definitions for classes used in all IFO objects, e.g. DOF
ifo/plot.py plotting functions that are less detector-specific.

Task: quick discussion of what `make_kat` does beyond our normal `parse` or `load` options

Didn't just do kat.load(design.kat) - we used make_kat(). Essentially that's where all the missing features are!

Run inspect on aligo.make_kat() - bit intimidating but quick scan through shows that we basically define a lot of:

  • ports of class Output
    • these are essentially key detectors, PDs or PD1s, that are used frequently
  • signals of class DOF
    • combine a detector with a type of motion to correspond to a detector signal and its error signal. E.g. kat.IFO.PRCL defines the PRCL dof, associating it with the POP_f1 port (a demodulated PD behind PR2) and the tuning of the PRM, with some scaling etc wrt other DOFs.
In [3]:
import inspect
print(inspect.getsource(aligo.make_kat))
def make_kat(name="design", katfile=None, verbose = False, debug=False, use_RF_DARM_lock=False,
             keepComments=False, preserveConstants=False):
    """
    Returns a kat object and fills in the kat.IFO property for storing
    the associated interferometer data.
    
    The `name` argument selects from default aLIGO files included in Pykat:
    
    
    keepComments: If true it will keep the original comments from the file
    preserveComments: If true it will keep the const commands in the kat
    """
    names = ['design']
    
    if debug:
        kat = finesse.kat(tempdir=".",tempname="test")
    else:
        kat = finesse.kat()
    
    kat.verbose=verbose
    
    # Create empty object to just store whatever DOFs, port, variables in
    # that will be used by processing functions
    kat.IFO = ALIGO_IFO(kat,
                        # Define which keys are used for a tuning description
                        ["maxtem", "phase"],
                        # Define which mirrors create the tuning description
                        ["PRM", "ITMX", "ETMX", "ITMY", "ETMY", "BS", "SRM"])
    
    kat.IFO._data_path=pkg_resources.resource_filename('pykat.ifo', os.path.join('aligo','files'))

    kat.IFO.rawBlocks = BlockedKatFile()
    
    if katfile:
        kat.load(katfile, keepComments=keepComments, preserveConstants=preserveConstants)
        kat.IFO.rawBlocks.read(katfile)
    else:
        if name not in names:
            pkex.printWarning("aLIGO name `{}' not recognised, options are {}, "
                              "using default 'design'".format(name, names))
        
        katkile = os.path.join(kat.IFO._data_path, name+".kat")
        
        kat.load(katkile, keepComments=keepComments, preserveConstants=preserveConstants)
        kat.IFO.rawBlocks.read(katkile)
    
    # ----------------------------------------------------------------------
    # get and derive parameters from the kat file
    
    # get main frequencies
    if "f1" in kat.constants.keys():
        kat.IFO.f1 = float(kat.constants["f1"].value)
    else:
        kat.IFO.f1 = 9099471.0
        
    if "f2" in kat.constants.keys():
        kat.IFO.f2 = float(kat.constants["f2"].value)
    else:
        kat.IFO.f2 = 5.0 * kat.IFO.f1
        
    # TODO add else here!
    # check modultion frequencies
    if (5 * kat.IFO.f1 != kat.IFO.f2):
        print(" ** Warning: modulation frequencies do not match: 5*f1!=f2")
    
    # defining a dicotionary for the main mirror positions (tunings),
    # keys should include maxtem, phase and all main optics names
    #kat.IFO.tunings = get_tunings(dict.fromkeys(["maxtem", "phase", "PRM", "ITMX", "ETMX", "ITMY", "ETMY", "BS", "SRM"]))
    kat.IFO.compute_derived_lengths()
        
    # ----------------------------------------------------------------------
    # define ports and signals 
    
    # useful ports
    kat.IFO.POP_f1  = Output(kat.IFO, "POP_f1",  "nPOP",  "f1", phase=101)
    kat.IFO.POP_f2  = Output(kat.IFO, "POP_f2",  "nPOP",  "f2", phase=13)
    kat.IFO.REFL_f1 = Output(kat.IFO, "REFL_f1", "nREFL", "f1", phase=101)
    kat.IFO.REFL_f2 = Output(kat.IFO, "REFL_f2", "nREFL", "f2", phase=14)
        
    kat.IFO.AS_f1  = Output(kat.IFO, "AS_f1",  "nSRM2",  "f1", phase=101)
    kat.IFO.AS_f2  = Output(kat.IFO, "AS_f2",  "nSRM2",  "f2", phase=14)
    kat.IFO.AS_f36 = Output(kat.IFO, "AS_f36", "nSRM2", "f36M", phase=14)

    kat.IFO.AS_DC   = Output(kat.IFO, "AS_DC", "nAS")
    kat.IFO.POW_BS  = Output(kat.IFO, "PowBS", "nPRBS*")
    kat.IFO.POW_X   = Output(kat.IFO, "PowX",  "nITMX2")
    kat.IFO.POW_Y   = Output(kat.IFO, "PowY",  "nITMY2")
    kat.IFO.TRX     = Output(kat.IFO, "TRX",   "nETMX2")
    kat.IFO.TRY     = Output(kat.IFO, "TRY",  " nETMY2")

    # pretune LSC DOF
    kat.IFO.preARMX  =  DOF(kat.IFO, "ARMX", kat.IFO.POW_X,   "", "ETMX", 1, 1.0, sigtype="z")
    kat.IFO.preARMY  =  DOF(kat.IFO, "ARMY", kat.IFO.POW_Y,   "", "ETMY", 1, 1.0, sigtype="z")
    kat.IFO.preMICH  =  DOF(kat.IFO, "AS"  , kat.IFO.AS_DC,   "", ["ITMX", "ETMX", "ITMY", "ETMY"], [1,1,-1,-1], 6.0, sigtype="z")
    kat.IFO.prePRCL  =  DOF(kat.IFO, "PRCL", kat.IFO.POW_BS,  "", "PRM",  1, 10.0, sigtype="z")
    kat.IFO.preSRCL  =  DOF(kat.IFO, "SRCL", kat.IFO.AS_DC,   "", "SRM",  1, 10.0, sigtype="z")
     
    # Used by new pretuning scripts DOFs - based on lock aquisition stuff in martynov thesis
    kat.IFO._preMICH =  DOF(kat.IFO, "preMICH", kat.IFO.AS_f2,   "Q", ["ITMX", "ETMX", "ITMY", "ETMY"], [1,1,-1,-1], 1.0, sigtype="z")
    kat.IFO._preSRCL =  DOF(kat.IFO, "preSRCL", kat.IFO.AS_f2,   "I", "SRM", 1, 1.0, sigtype="z")
    kat.IFO._prePRCL =  DOF(kat.IFO, "prePRCL", kat.IFO.REFL_f1, "I", "PRM", 1, 1.0, sigtype="z")
    
    kat.IFO._preALSX =  DOF(kat.IFO, "ALSX", kat.IFO.POW_X,   "", "ETMX", 1, 1.0, sigtype="z")
    kat.IFO._preALSY =  DOF(kat.IFO, "ALSY", kat.IFO.POW_Y,   "", "ETMY", 1, 1.0, sigtype="z")
    
    # control scheme as in [1] Table C.1. Due to Finesse
    # conventions, the overall factor for all but PRCL are multiplied by -1
    # compared to the LIGO defintion, to match the same defintion. 
    kat.IFO.PRCL =  DOF(kat.IFO, "PRCL", kat.IFO.POP_f1,  "I", "PRM", 1, 100.0, sigtype="z")
    kat.IFO.MICH =  DOF(kat.IFO, "MICH", kat.IFO.POP_f2,  "Q", ["ITMX", "ETMX", "ITMY", "ETMY"], [-0.5,-0.5,0.5,0.5], 100.0, sigtype="z")
    kat.IFO.CARM =  DOF(kat.IFO, "CARM", kat.IFO.REFL_f1, "I", ["ETMX", "ETMY"], [-1, -1], 1.5, sigtype="z")
    
    if use_RF_DARM_lock:
        kat.IFO.DARM =  DOF(kat.IFO, "DARM", kat.IFO.AS_f2, "Q", ["ETMX", "ETMY"], [-1,1], 1.0, sigtype="z")
    else:
        kat.IFO.DARM =  DOF(kat.IFO, "DARM", kat.IFO.AS_DC, "",  ["ETMX", "ETMY"], [-1,1], 1.0, sigtype="z")
                            
    kat.IFO.SRCL =  DOF(kat.IFO, "SRCL", kat.IFO.REFL_f2, "I", "SRM", -1, 1e2, sigtype="z")

    kat.IFO.DARM_h =  DOF(kat.IFO, "DARM_h", kat.IFO.AS_DC, "", ["LY", "LX"], [-1,1], 1.0, sigtype="phase")
    
    kat.IFO.LSC_DOFs = (kat.IFO.PRCL, kat.IFO.MICH, kat.IFO.CARM, kat.IFO.DARM, kat.IFO.SRCL)
    kat.IFO.CAV_POWs = (kat.IFO.POW_X, kat.IFO.POW_Y, kat.IFO.POW_BS)
    
    # Pitch DOfs
    # There is a difference in the way LIGO and Finesse define positive and negative
    # rotations of the cavity mirrors. For LIGO the rotational DOFs assume ITM + rotation
    # is clockwise and ETM + rotation is anticlockwise.
    # I'll be explict here for future reference.
    cav_mirrors = ["ETMX", "ETMXAR", "ETMY", "ETMYAR", "ITMX", "ITMXAR", "ITMY", "ITMYAR"]

    # LIGO definitions
    # Based on figure 7 in T0900511-v4
    CHARD_factors   = np.array([ 1, 1, 1, 1,-1,-1,-1,-1])
    DHARD_factors   = np.array([ 1, 1,-1,-1,-1,-1, 1, 1])
    CSOFT_factors   = np.array([-1,-1,-1,-1,-1,-1,-1,-1])
    # DSOFT_factors   = np.array([-1,-1, 1, 1, 1, 1,-1,-1])   # Wrong!
    DSOFT_factors   = np.array([-1,-1, 1, 1,-1,-1, 1, 1])
    
    # Finesse definitions
    # negative for ITM rotations
    ITMS = np.in1d(cav_mirrors, np.array(["ITMX", "ITMXAR", "ITMY", "ITMYAR"]))
    CHARD_factors[ITMS] *= -1
    DHARD_factors[ITMS] *= -1
    CSOFT_factors[ITMS] *= -1
    DSOFT_factors[ITMS] *= -1

    kat.IFO.CHARD_P = DOF(kat.IFO, "CHARD_P", None , None, cav_mirrors, CHARD_factors, 1, sigtype="pitch")
    kat.IFO.DHARD_P = DOF(kat.IFO, "DHARD_P", None , None, cav_mirrors, DHARD_factors, 1, sigtype="pitch")
    kat.IFO.CSOFT_P = DOF(kat.IFO, "CSOFT_P", None , None, cav_mirrors, CSOFT_factors, 1, sigtype="pitch")
    kat.IFO.DSOFT_P = DOF(kat.IFO, "DSOFT_P", None , None, cav_mirrors, DSOFT_factors, 1, sigtype="pitch")
    kat.IFO.PRM_P   = DOF(kat.IFO, "PRM_P"  , None , None, ["PRM", "PRMAR"], [1,1], 1, sigtype="pitch")
    kat.IFO.PRC2_P  = DOF(kat.IFO, "PRC2_P" , None , None, ["PR2"], [1], 1, sigtype="pitch")
    kat.IFO.PRC3_P  = DOF(kat.IFO, "PRC3_P" , None , None, ["PR3"], [1], 1, sigtype="pitch")
    kat.IFO.SRM_P   = DOF(kat.IFO, "SRM_P"  , None , None, ["SRM", "SRMAR"], [1,1], 1, sigtype="pitch")
    kat.IFO.SRC2_P  = DOF(kat.IFO, "SRC2_P" , None , None, ["SR2"], [1], 1, sigtype="pitch")
    kat.IFO.SRC3_P  = DOF(kat.IFO, "SRC3_P" , None , None, ["SR3"], [1], 1, sigtype="pitch")
    kat.IFO.MICH_P  = DOF(kat.IFO, "MICH_P" , None , None, ["BS", "BSAR1", "BSAR2"], [1,1,1], 1, sigtype="pitch")
    
    kat.IFO.ASC_P_DOFs = (kat.IFO.CHARD_P, kat.IFO.DHARD_P,
                          kat.IFO.CSOFT_P, kat.IFO.DSOFT_P,
                          kat.IFO.PRM_P, kat.IFO.PRC2_P,
                          kat.IFO.PRC3_P, kat.IFO.SRM_P,
                          kat.IFO.SRC2_P, kat.IFO.SRC3_P,
                          kat.IFO.MICH_P)
    
    kat.IFO.update()

    kat.IFO.lockNames = None
    
    return kat

Task: quick examples of using these features

  1. add a power detector automatically in the right place (and get the power there)
  2. plot the signal used to detect a DOF quickly
  3. list all available Outputs (detectors) and DOFs available for use this way
In [4]:
# 1. add a power detector automatically in the right place (and get the power there)

kat = base.deepcopy()
kat.IFO.POW_X.add_signal()
kat.noxaxis=True
o=kat.run()
print(o[kat.IFO.POW_X.name])
707292.367935397
In [5]:
# 2. plot an error signal for a DOF quickly
kat = base.deepcopy()
o = kat.IFO.CARM.scan()
o.plot()
In [6]:
kat.IFO.CARM.signal_name
Out[6]:
<bound method DOF.signal_name of <pykat.ifo.DOF object at 0x1819a73c88>>
In [7]:
# list all available detectors (Outputs) or DOFs available for use this way
kat.IFO.Outputs, kat.IFO.DOFs
Out[7]:
({'AS_DC': <pykat.ifo.Output at 0x1819a73898>,
  'AS_f1': <pykat.ifo.Output at 0x1819a737f0>,
  'AS_f2': <pykat.ifo.Output at 0x1819a73828>,
  'AS_f36': <pykat.ifo.Output at 0x1819a73860>,
  'POP_f1': <pykat.ifo.Output at 0x1819a73710>,
  'POP_f2': <pykat.ifo.Output at 0x1819a73748>,
  'POW_BS': <pykat.ifo.Output at 0x1819a738d0>,
  'POW_X': <pykat.ifo.Output at 0x1819a73908>,
  'POW_Y': <pykat.ifo.Output at 0x1819a73940>,
  'REFL_f1': <pykat.ifo.Output at 0x1819a73780>,
  'REFL_f2': <pykat.ifo.Output at 0x1819a737b8>,
  'TRX': <pykat.ifo.Output at 0x1819a73978>,
  'TRY': <pykat.ifo.Output at 0x1819a739b0>},
 {'CARM': <pykat.ifo.DOF at 0x1819a73c88>,
  'CHARD_P': <pykat.ifo.DOF at 0x1819a73d68>,
  'CSOFT_P': <pykat.ifo.DOF at 0x1819a73dd8>,
  'DARM': <pykat.ifo.DOF at 0x1819a73cc0>,
  'DARM_h': <pykat.ifo.DOF at 0x1819a73d30>,
  'DHARD_P': <pykat.ifo.DOF at 0x1819a73da0>,
  'DSOFT_P': <pykat.ifo.DOF at 0x1819a73e10>,
  'MICH': <pykat.ifo.DOF at 0x1819a73c50>,
  'MICH_P': <pykat.ifo.DOF at 0x1819a73f98>,
  'PRC2_P': <pykat.ifo.DOF at 0x1819a73e80>,
  'PRC3_P': <pykat.ifo.DOF at 0x1819a73eb8>,
  'PRCL': <pykat.ifo.DOF at 0x1819a73c18>,
  'PRM_P': <pykat.ifo.DOF at 0x1819a73e48>,
  'SRC2_P': <pykat.ifo.DOF at 0x1819a73f28>,
  'SRC3_P': <pykat.ifo.DOF at 0x1819a73f60>,
  'SRCL': <pykat.ifo.DOF at 0x1819a73cf8>,
  'SRM_P': <pykat.ifo.DOF at 0x1819a73ef0>,
  '_preALSX': <pykat.ifo.DOF at 0x1819a73ba8>,
  '_preALSY': <pykat.ifo.DOF at 0x1819a73be0>,
  '_preMICH': <pykat.ifo.DOF at 0x1819a73b00>,
  '_prePRCL': <pykat.ifo.DOF at 0x1819a73b70>,
  '_preSRCL': <pykat.ifo.DOF at 0x1819a73b38>,
  'preARMX': <pykat.ifo.DOF at 0x1819a739e8>,
  'preARMY': <pykat.ifo.DOF at 0x1819a73a20>,
  'preMICH': <pykat.ifo.DOF at 0x1819a73a58>,
  'prePRCL': <pykat.ifo.DOF at 0x1819a73a90>,
  'preSRCL': <pykat.ifo.DOF at 0x1819a73ac8>})

typical basic usage & first sanity check: QNLS plot

Task: plot the quantum-noise-limited sensitivity of the LIGO design.

2 methods. (1) we do it by hand, and (2) we make use of a handy function included in the ligo plotting tools.

Using the plotting tool, we just need to

  1. import the aligo plotting tools package:
    import pykat.ifo.aligo.plot as aligoplt
    
  2. use the strain_sensitivity() function from this package to directly plot the curve with some default parameter ranges.
      - take a look at the function iteself in `pykat/ifo/aligo/plot.py` to see what's actually going on.

Use either method to plot the quantum-limited sensitivity of the LIGO design. How does it compare to the design curve we usually show, such as that shown on gwplotter.com?

In [8]:
import pykat.ifo.aligo.plot as aligoplt
aligoplt.strain_sensitivity(base)
Out[8]:
<pykat.finesse.kat_7 at 0x1819c45da0>

either way, this doesn't look right. In fact, it looks rubbish!

Task: discussion

What are we missing?

Ans: we're not at an operating point!

What is an operating point?
  • operating point = condition in which ifo can be used as intended. In particular for LIGO, all optics correctly tuned such that power high & low in the correct places.
  • single cavity: operating point might be that the cavity is on resonance & can be used as a reference cav.
    • in Finesse, this is easy: phi=0 on both mirrors and automatically everything is fine.
  • LIGO: multiple cavities, thick, realistic optics.
    • Finesse with all tunings to 0 is no longer quite correct: phase accumulates going from one optic to the next, amplified by cavities, etc, so don't have 'phi=0 is resonant' for all optics any more.
  • need to find a usable tuning for each optic.
  • in cases where the file is pretty 'clean' - i.e. not many distortions that siignificantly affect accumulated phase/Gouy phase, we can use the script setup to run through the file, using realistic error signals for the main degrees of freedom, to set the tunings of the main optics to get everything resonant (or anti-resonant) as required.
  • won't go over it in detail today, maybe next time?
  • but in essence, need to run this at the start of every model.

Task: `setup` the ligo file, then plot the quantum-noise-limited sensitivity of the LIGO design again.

all we need is

base2 = aligo.setup(base)
aligoplt.strain_sensitivity(base2)
In [ ]:
base2 = aligo.setup(base)
aligoplt.strain_sensitivity(base2)

Task: Discussion of results

Option 1: masses missing

You might see this version which is flat at low frequencies - no Quantum Radiation Pressure Noise taken into account!

Actually, we spotted this a while ago, I made a corrected version that's propagating through the system now... If not, main issue is that the mirrors get fixed in place by setup() & have infinite mass. If we restore the mass values, they act as suspended free masses and we see the 'full' QNLS plot:

In [ ]:
LIGO = base2.deepcopy()
LIGO.ETMX.mass = LIGO.ETMY.mass = LIGO.ITMX.mass = LIGO.ITMY.mass = 40
aligoplt.strain_sensitivity(LIGO)
Option 2: what does the function do? why?
Short version: Let's inspect the built-in function and discuss it
In [ ]:
print(inspect.getsource(aligoplt.strain_sensitivity))
In [ ]:
print(base.IFO.DARM_h.signal())
print(base.IFO.DARM_h.transfer())
print(base.IFO.DARM_h.optics)
print(base.IFO.DARM.optics)

print(base.IFO.DARM_h.fsig())
Longer version: Let's recreate this by hand, with no IFO-sourced detectorsm to show we can, then compare the two side-by-side
In [ ]:
kat = base2.deepcopy()
kat.parse("""
fsig DARM_h LY phase 1.0 180.0 1
fsig DARM_h LX phase 1.0 0.0 1
qnoisedS NSR 1 $fs nAS
xaxis DARM_h f log 10 5000 100
yaxis log abs
""")
o = kat.run()
o.plot(['NSR'])