Calculating general transfer functions

There are three types of nodes in Finesse that inject and probe different types of signals (see also The port and node system):

  • Optical: Optical modes.

  • Mechanical: Mechanical degrees of freedom such as lengths and angles (pitch and yaw), and forces and torques.

  • Electrical: electrical signals such as outputs from photodetectors and DegreeOfFreedom components.

Note

Whilst differentiated in descriptions, internally electrical and mechanical nodes are both SignalNode attached to an element. Such as the laser amplitude signal laser.amp, or mirror.mech.z. Signal nodes represent a small AC oscillations at the model.fsig frequency.

The previous section described how transfer functions are computed from electrical and mechanical nodes to electrical and mechanical nodes using the FrequencyResponse action. There are a total of four frequency response actions which calculate transfer functions between optical nodes as well so that optical modes can be probed directly. This is especially useful when analyzing quantum noise as described in two_photon_transfer. The four actions are

In this section we use these four actions to analyze a simple Fabry-Perot cavity with Pound-Drever-Hall [25] readout.

Setting up the model

import numpy as np
import finesse
import finesse.components as fc
import finesse.analysis.actions as fa
from finesse.plotting import bode

finesse.init_plotting()

Fmod_Hz = 9e6
model = finesse.Model()
ETM = fc.Mirror("ETM", T=0, L=0, Rc=2245)
ITM = fc.Mirror("ITM", T=0.014, L=0, Rc=1940)
Laser = fc.Laser("Laser", P=5e3)
Mod = fc.Modulator("Mod", f=Fmod_Hz, midx=0.2, order=1)
REFL = fc.ReadoutRF("REFL", ITM.p2.o, f=Fmod_Hz, output_detectors=True)

model.add((Laser, Mod, ETM, ITM, REFL))
model.link(Laser.p1, Mod, ITM.p2, ITM.p1, 4e3, ETM.p1)
model.phase_config(zero_k00=False)

If you are using higher order modes, you would also add a Cavity to use the mode of the cavity to define the modal basis. See Defining the modal basis for details. Here we manually set a mismatched beam parameter for illustrative purposes so that there will be coupling between the HOMs and the fundamental.

# Usually we would do this
# model.add(fc.Cavity("cavARM", model.ETM.p1.o))
# and not this
model.ITM.p1.i.q = finesse.BeamParam(w=55e-3, Rc=1943)
model.modes("even", maxtem=2)
print(model.mismatches_table())
┌──────────────────────╥──────────────┬──────────────┐
│       Coupling       ║ Mismatch (x) │ Mismatch (y) │
╞══════════════════════╬══════════════╪══════════════╡
│ ETM.p1.i -> ETM.p1.o ║       0.0033 │       0.0033 │
├──────────────────────╫──────────────┼──────────────┤
│ ETM.p2.i -> ETM.p2.o ║       0.0033 │       0.0033 │
├──────────────────────╫──────────────┼──────────────┤
│ ITM.p1.i -> ITM.p1.o ║       0.0001 │       0.0001 │
├──────────────────────╫──────────────┼──────────────┤
│ ITM.p2.i -> ITM.p2.o ║       0.0001 │       0.0001 │
└──────────────────────╨──────────────┴──────────────┘

This is fair bit of mismatch, so we need to add a Lock to make sure the cavity is on the correct operating point before calculating transfer functions. See Locking actions for details.

model.add(
 finesse.locks.Lock("cav_lock", model.REFL.outputs.I, model.ETM.phi, 1e-4,
 1e-6)
);

Calculating the transfer functions

The FrequencyResponseN actions are called like

FrequencyResponseN(F_Hz, [input_nodes], [output_nodes], name="action name")

For FrequencyResponse{2,3,4} which include optical nodes, input and output optical nodes need to be listed as a tuple (node, freq) where freq is the frequency the sideband is calculated at. This frequency must be a symbolic reference to the model’s fsig.f parameter.

Note

Positive frequencies correspond to upper sidebands while negative frequencies correspond to the conjugate of the lower sidebands, not the lower sidebands. Therefore, propagation of both result in a phase delay.

For example, with nodes defined as

fsig = model.fsig.f.ref
carrier_nodes = [
    (node, +fsig),
    (node, -fsig),
]
rf_sideband_nodes = [
    (node, Fmod_Hz + fsig),
    (node, Fmod_Hz - fsig),
    (node, -Fmod_Hz + fsig),
    (node, -Fmod_Hz - fsig),
]

the first node in carrier_nodes is the upper sideband of the carrier and the second node is the conjugate of the lower sideband of the carrier. The first two nodes listed in rf_sideband_nodes are the upper sideband and conjugate of the lower sideband around the RF sideband at +Fmod_Hz. The second two nodes are the upper and conjugate of the lower sideband around the RF sideband at -Fmod_Hz.

In our example we calculate the response at the reflection of the ITM to signals excited at or by the ETM for the different types of signals.

# Run the four FrequencyResponse actions
F_Hz = np.geomspace(1, 2e3, 200)
model.fsig.f = 1  # make sure to set this or you'll get a warning
fsig = model.fsig.f.ref  # Make a shorthand reference to the fsig symbol

# FrequencyResponse actions need a symbolic reference to frequency
sol = model.run(
    fa.Series(
        # First lock the cavity
        fa.RunLocks(),
        # ETM motion to PDH readout
        fa.FrequencyResponse(
            F_Hz,
            [model.ETM.mech.z],
            [model.REFL.I.o, model.REFL.Q.o],
            name="fresp",
        ),
        # ETM optical fields to PDH readout
        fa.FrequencyResponse2(
            F_Hz,
            [
                (model.ETM.p1.o, +fsig),  # upper sideband
                (model.ETM.p1.o, -fsig),  # conjugate lower sideband
            ],
            [model.REFL.I.o, model.REFL.Q.o],
            name="fresp2",
        ),
        # ETM optical fields to ITM optical fields
        fa.FrequencyResponse3(
            F_Hz,
            [
                (model.ETM.p1.o, +fsig),
                (model.ETM.p1.o, -fsig),
            ],
            [
                (model.ITM.p2.o, +fsig),
                (model.ITM.p2.o, -fsig),
            ],
            name="fresp3",
        ),
        # ETM motion to ITM optical fields
        fa.FrequencyResponse4(
            F_Hz,
            [model.ETM.mech.z],
            [
                (model.ITM.p2.o, +fsig),
                (model.ITM.p2.o, -fsig),
            ],
            name="fresp4",
        ),
    )
)

The transfer functions are returned in the out attribute of the solutions. The dimensions returned for each action are

The transfer functions computed with FrequencyResponse can also be accessed through the output and input keys like sol["fresp"]["out", "in"]. In this example each of the solutions has three HOMs where

for idx, hom in enumerate(model.homs):
    print(f"index {idx} corresponds to HG{''.join(hom.astype(str))}")
index 0 corresponds to HG00
index 1 corresponds to HG20
index 2 corresponds to HG02

Frequency response of the fundamental mode

In this example, FrequencyResponse calculates ETM mirror motion to the PDH REFL readout. There are two outputs (the PDH quadratures) and one input (ETM motion). Here we plot mirror motion to the I and Q PDH readout.

print("FrequencyResponse shape", sol["fresp"].out.shape)

axs = bode(F_Hz, sol["fresp"].out[..., 0, 0], db=False, label="REFL I")
bode(F_Hz, sol["fresp"].out[..., 1, 0], axs=axs, db=False, label="REFL Q", ls="--")
axs[0].set_ylabel("Magnitude [W / m]")
axs[0].set_title("ETM mirror motion to REFL readout");
FrequencyResponse shape (200, 2, 1)
../../_images/frequency_response_5_1.svg

As noted above, transfer functions calculated with FrequencyResponse can also be accessed by their input and output keys:

print(np.all(sol["fresp"]["REFL.I.o", "ETM.mech.z"] == sol["fresp"].out[..., 0, 0]))
print(np.all(sol["fresp"]["REFL.Q.o", "ETM.mech.z"] == sol["fresp"].out[..., 1, 0]))
True
True

In this example, FrequencyResponse2 calculates optical fields to PDH REFL readout. It has the same two outputs as FrequencyResponse and two inputs (the upper and lower sidebands of the optical fields at the ETM). Here we plot the upper and conjugate lower sidebands of the optical fields at the front of the ETM to the REFL I PDH readout.

print("FrequencyResponce2 shape", sol["fresp2"].out.shape)
axs = bode(F_Hz, sol["fresp2"].out[..., 0, 0, 0], db=False, label="upper")
bode(F_Hz, sol["fresp2"].out[..., 0, 1, 0], axs=axs, db=False, label="lower", ls="--")
axs[0].set_ylabel("Magnitude [W / $\sqrt{\mathrm{W}}$]")
axs[0].set_title("ETM optical field to REFL I readout");
FrequencyResponce2 shape (200, 2, 2, 3)
../../_images/frequency_response_7_2.svg

In this example, FrequencyResponse3 calculates optical fields at the ETM to optical fields at the ITM. There are two outputs (upper and lower sidebands of the fields at the ITM) and two inputs (upper and lower sidebands of the fields at the ETM). Here we plot the upper and conjugate lower sidebands at the front of the ETM to the sidebands at the back of the ITM.

print("FrequencyResponse3 shape", sol["fresp3"].out.shape)
axs = bode(F_Hz, sol["fresp3"].out[..., 0, 0, 0, 0], db=False, label="upper")
bode(F_Hz, sol["fresp3"].out[..., 1, 1, 0, 0], axs=axs, db=False, label="lower", ls="--")
axs[0].set_ylabel("Magnitude [$\sqrt{\mathrm{W}} / \sqrt{\mathrm{W}}$]")
axs[0].set_title("ETM optical field to reflected optical field");
FrequencyResponse3 shape (200, 2, 2, 3, 3)
../../_images/frequency_response_8_2.svg

There is no mixing between the uppper and lower sidebands in this case:

print(np.allclose(sol["fresp3"].out[..., 1, 0, :, :], 0))
print(np.allclose(sol["fresp3"].out[..., 0, 1, :, :], 0))
True
True

In this example, FrequencyResponse4 calculates ETM mirror motion to optical fields at the back of the ITM. There are two outputs (upper and lower sidebands of the fields at the ITM) and one input (ETM motion). Here we plot ETM mirror motion to the upper and conjugate lower sidebands at the back of the ITM.

print("FrequencyResponse4 shape", sol["fresp4"].out.shape)
axs = bode(F_Hz, sol["fresp4"].out[..., 0, 0, 0], db=False, label="upper")
bode(F_Hz, sol["fresp4"].out[..., 1, 0, 0], axs=axs, db=False, ls="--", label="lower")
axs[0].set_ylabel("Magnitude [$\sqrt{\mathrm{W}}$ / m]")
axs[0].set_title("Mirror motion to reflected optical field");
FrequencyResponse4 shape (200, 2, 1, 3)
../../_images/frequency_response_10_2.svg

Frequency response between HOMs

We can also look at transfer functions between HOMs. Here we plot the HG20 upper and conjugate lower sidebands at the front of the ETM to the HG00 upper and conjugate lower sidebands at the back of the ITM.

axs = bode(F_Hz, sol["fresp3"].out[..., 0, 0, 0, 1], db=False, label="upper")
bode(F_Hz, sol["fresp3"].out[..., 1, 1, 0, 1], axs=axs, db=False, label="lower", ls="--")
axs[0].set_ylabel("Magnitude [$\sqrt{\mathrm{W}} / \sqrt{\mathrm{W}}$]")
axs[0].set_title("ETM HG20 optical field to reflected HG00 optical field");
../../_images/frequency_response_11_1.svg

Here we plot mirror motion to the HG02 upper and conjugate lower sidebands at the back of the ITM.

axs = bode(F_Hz, sol["fresp4"].out[..., 0, 0, 2], db=False, label="upper")
bode(F_Hz, sol["fresp4"].out[..., 1, 0, 2], axs=axs, db=False, ls="--", label="lower")
axs[0].set_ylabel("Magnitude [$\sqrt{\mathrm{W}}$ / m]")
axs[0].set_title("Mirror motion to reflected HG02 optical field");
../../_images/frequency_response_12_1.svg

Similarly, the response of REFL.Q to excitation of the conjugate lower HG02 mode at the ETM is given by sol["fresp2"].out[..., 1, 1, 2], etc.