# file autogenerated by scripts/examples_to_script.py
def ex_01_simple_cav():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        # Add a Laser named L0 with a power of 1 W.
        l L0 P=1
        # Space attaching L0 <-> m1 with length of 0 m (default).
        s s0 L0.p1 m1.p1
        # Input mirror of cavity.
        m m1 R=0.99 T=0.01
        # Intra-cavity space with length of 1 m.
        s CAV m1.p2 m2.p1 L=1
        # End mirror of cavity.
        m m2 R=0.991 T=0.009
        # Power detectors on reflection, circulation and transmission.
        pd refl m1.p1.o
        pd circ m2.p1.i
        pd trns m2.p2.o
        """
    )
    # Scan over the detuning DOF of m1 from -180 deg to +180 deg with 400 points.
    out = kat.run("xaxis(m1.phi, lin, -180, 180, 400)")
    out.plot(logy=True)
    # Add some curvatures to our mirrors.
    kat.m1.Rc = -0.7
    kat.m2.Rc = 0.7
    # Define the cavity.
    kat.parse("cavity cavity1 source=m1.p2.o via=m2.p1.i priority=1")
    # Plot the beam trace, starting from cavity the eigenmode.
    tsy = finesse.tracing.tools.propagate_beam(
        to_node=kat.L0.p1.i, from_node=kat.m2.p1.o, direction="y"
    )
    tsy.plot()
    # Remove s0 so we can add in a lens.
    # FIXME kat.remove is not yet working, so we use a list
    # comprehension to remove s0 from the model and
    # reparse it into a new model. (See unparse below)
    kat2 = finesse.Model()
    kat2.parse("\n".join([l for l in kat.unparse().splitlines() if "s0" not in l]))
    # Add a silicon lens with an AR surface to the input coupler.
    kat2.parse(
        """
        space s0 L0.p1 m1_AR.p1 L=0.5
        mirror m1_AR R=100e-6 L=10e-6 Rc=-0.3
        space s_M1_sub portA=m1_AR.p2 portB=m1.p1 L=10e-3 nr=3.5
        """
    )
    # Plot beam trace, starting from the cavity eigenmode.
    tsy = finesse.tracing.tools.propagate_beam(
        to_node=kat2.L0.p1.i, from_node=kat2.m2.p1.o, direction="y"
    )
    tsy.plot()
    print(kat)
    print(kat.component_tree(kat.L0))
    print(kat.unparse())


def ex_02_pdh_lock():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse

    finesse.configure(plotting=True)
    model = finesse.Model()
    model.parse(
        """
        laser i1 P=1 f=0
        space s1 portA=i1.p1 portB=eo1.p1
        modulator eo1 15M 0.7 order=3
        space s2 portA=eo1.p2 portB=m1.p1
        mirror m1 R=0.9 T=0.1
        space s3 portA=m1.p2 portB=m2.p1 L=1
        mirror m2 R=0.9 T=0.1
        power_detector_dc refl node=m1.p1.o
        power_detector_dc trans node=m2.p2.o
        """
    )
    import numpy as np

    for phase in np.linspace(0, 90, endpoint=True, num=6):
        model.parse(f"pd1 pdh{phase:.0f} node=m1.p1.o f=eo1.f phase={phase:.0f}")
    sol = model.run("xaxis(m2.phi, lin, -100, 100, 400)")
    sol.plot()

    def remove_demod_pds(verbose=True):
        for el in model.elements:
            if el.startswith("pdh"):
                model.remove(el)
                if verbose:
                    print(f"Removed {el}")

    # Remove the old detectors.
    remove_demod_pds()
    # Create another array of phases.
    phases = np.arange(0, 90, 1)
    for phase in phases:
        model.parse(f"pd1 pdh{phase:.0f} node=m1.p1.o f=eo1.f phase={phase:.0f}")
    # Sweep the mirror tuning across 3 points: ±1° and 0.
    out = model.run("xaxis(m2.phi, lin, -1, 1, 2)")
    # Calculate the gradient of the error signal.
    grad = []
    for phase in phases:
        var = out[f"pdh{phase}"]
        grad.append((var[0] - var[-1]) / 2)
    # Find the maximum gradient.
    grad = np.array(grad)
    idxmax = np.argmax(np.abs(grad))
    print(f"Maximum error signal gradient occurs at {phases[idxmax]} degrees")
    # Plot.
    from matplotlib import pyplot as plt

    plt.plot(phases, grad)
    plt.xlabel("Demodulation Phase (°)")
    plt.ylabel("Error Signal Gradient (arb)")
    remove_demod_pds(verbose=False)
    model.parse(
        """
    power_detector_demod_1 pdh node=m1.p1.o f=eo1.f phase=9
    lock lock_length pdh m1.phi -10 1e-8
    """
    )
    sol = model.run("xaxis(m2.phi, lin, 0, 100, 400, pre_step=run_locks())")
    sol.plot()
    import numpy as np

    import finesse

    finesse.init_plotting()
    model = finesse.Model()
    model.parse(
        """
        laser i1 P=1 f=0
        space s1 portA=i1.p1 portB=eo1.p1
        modulator eo1 15M 0.7 order=3
        space s2 portA=eo1.p2 portB=m1.p1
        mirror m1 R=0.9 T=0.1
        space s3 portA=m1.p2 portB=m2.p1 L=1
        mirror m2 R=0.9 T=0.1
        pd1 pdh_complex m1.p1.o f=eo1.f
        """
    )
    eps = 1e-9  # finite difference step size for gradient calculation
    sol = model.run(f"xaxis(m1.phi, lin, -{eps / 2}, {eps / 2}, 1, relative=True)")
    # compute the real and imaginary gradient with respect to the
    # cavity length change, then compute the complex argument (angle)
    # to get the optimial demodulation phase
    opt_demod = np.angle(
        np.gradient(sol["pdh_complex"], eps).mean(),  # take mean as we need
        deg=True,  # make sure we compute angle in degrees
    )
    print(opt_demod)
    model.parse(
        f"""
    pd1 pdh_I m1.p1.o f=eo1.f phase={opt_demod}
    pd1 pdh_Q m1.p1.o f=eo1.f phase={opt_demod + 90}
    """
    )
    sol = model.run("xaxis(m1.phi, lin, -50, 50, 200)")
    sol.plot("pdh_I", "pdh_Q")


def ex_03_near_unstable():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")


def ex_04_geometrical_params():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import numpy as np

    # Cavity parameters
    L = 0.1  # Cavity Length
    RcM2 = 0.5  # Radius of curvature of M2 mirror = 0.5 m
    g1 = 1  # g1 = 1 - L/RcM1 = 1 since RcM1 = infinity
    g2 = 1 - L / RcM2  # g2 for M2 mirror
    print("g1g2 =", g1 * g2)  # Stability check
    # Mirror Tilt:
    npoints = 200
    theta_low = -1e-6
    theta_high = 1e-6

    def geometric(mirror, theta_low, theta_high, npoints):
        if mirror == 1:
            # M1 tilt
            theta_1 = np.linspace(theta_low, theta_high, npoints + 1, endpoint=True)
            # M2 tilt
            theta_2 = 0
        elif mirror == 2:
            # M1 tilt
            theta_1 = 0
            # M2 tilt
            theta_2 = np.linspace(theta_low, theta_high, npoints + 1, endpoint=True)
        else:
            raise NotImplementedError("Mirror should be 1 or 2 and an integer")
        # Resultant translation
        ## Displacement on M1
        delta_x1 = (g2 / (1 - g1 * g2)) * L * theta_1 - (
            1 / (1 - g1 * g2)
        ) * L * theta_2
        ## Displacement on M2
        delta_x2 = (1 / (1 - g1 * g2)) * L * theta_1 - (
            g1 / (1 - g1 * g2)
        ) * L * theta_2
        ## Angular displacement of optical axis
        delta_theta = (delta_x2 - delta_x1) / L
        print("Results for analytical solutions:")
        print("∆x1: ", max(delta_x1), min(delta_x1), " m")
        print("∆x2: ", max(delta_x2), min(delta_x2), " m")
        print("∆θ: ", max(delta_theta), min(delta_theta), " m")
        return delta_x1, delta_x2, delta_theta

    import matplotlib.pyplot as plt

    import finesse
    from finesse import init_plotting

    init_plotting()
    T = 1e-6
    # Build Finesse model
    kat = finesse.Model()
    kat.parse(
        f"""
        laser laser P=1.0 f=0 phase=0
        # Mirrors
        mirror M1 R=1-M1.T T={T} L=0.0 phi=0 Rc=[inf, inf] xbeta=0 ybeta=0
        mirror M2 R=1-M1.T T=M1.T L=0.0 phi=0 Rc=[{RcM2}, {RcM2}] xbeta=0 ybeta=0
        # Spaces
        space s0 portA=laser.p1 portB=M1.p1 L=1 nr=1
        space s_cav portA=M1.p2 portB=M2.p1 L=0.1 nr=1
        # Amplitude Detectors
        amplitude_detector det00_1 node=M1.p2.i f=0 n=0 m=0
        amplitude_detector det00_2 node=M2.p1.o f=0 n=0 m=0
        amplitude_detector det10_1 node=M1.p2.i f=0 n=1 m=0
        amplitude_detector det10_2 node=M2.p1.o f=0 n=1 m=0
        # Beam Property Detectors
        beam_property_detector bp_n2 prop=0 node=M1.p2.i direction=x q_as_bp=false
        beam_property_detector bp_n3 prop=0 node=M2.p1.o direction=x q_as_bp=false
        beam_property_detector cavwaist prop=1 node=M1.p2.i direction=x q_as_bp=false
        # Photodiodes
        power_detector_dc pcirc node=M1.p2.o pdtype=none
        # Config
        cavity cavity1 source=M1.p2.o via=M2.p1.i priority=1
        lambda(1.064e-06)
        modes(maxtem=3)
        """
    )
    # Print beam tracing information
    tsy = finesse.tracing.tools.propagate_beam(
        to_node=kat.M2.p1.o, from_node=kat.M1.p2.o, direction="y"
    )
    print(tsy.table())

    def run_simulation(mirror, theta_low, theta_high, npoints):
        return process_sim(
            kat.run(
                f"""
                xaxis(
                    parameter=M{mirror:.0f}.xbeta,
                    mode=lin,
                    start={theta_low},
                    stop={theta_high},
                    steps={npoints},
                    pre_step=none,
                    post_step=none
                )
                """
            )
        )

    def process_sim(out):
        # Comparison
        acirc = np.sqrt(out["pcirc"].real)  # circulating amplitude
        # intermodal phase HG10 -> HG00
        # at mirror 1
        intermodal_phase_1 = np.angle(out["det10_1"]) - np.angle(out["det00_1"])
        # at mirror 2
        intermodal_phase_2 = np.angle(out["det10_2"]) - np.angle(out["det00_2"])
        # Real part of \power normalised HG10 mode ampltiude
        # at M1
        a_shift_1 = (np.abs(out["det10_1"]) / acirc) * np.cos(intermodal_phase_1)
        # at M2
        a_shift_2 = (np.abs(out["det10_2"]) / acirc) * np.cos(intermodal_phase_2)
        # Imaginary part of power normalised HG10 mode ampltiude
        # at M1
        a_tilt_1 = (np.abs(out["det10_1"]) / acirc) * np.sin(intermodal_phase_1)
        # at M2
        a_tilt_2 = (np.abs(out["det10_2"]) / acirc) * np.sin(intermodal_phase_2)
        # Spot position shift
        # at M1
        dx_finesse_1 = out["bp_n2"].real * a_shift_1
        # at M2
        dx_finesse_2 = out["bp_n3"].real * a_shift_2
        # Wavefront tilt at
        # M1
        dtheta_1 = a_tilt_1 * kat.lambda0 / (np.pi * out["cavwaist"].real)
        # M2
        dtheta_2 = a_tilt_2 * kat.lambda0 / (np.pi * out["cavwaist"].real)
        print("Results for finesse solutions:")
        print(
            "Intermodal_1: ",
            max(intermodal_phase_1 / np.pi),
            min(dx_finesse_1 / np.pi),
            " pi",
        )
        print(
            "Intermodal_2: ",
            max(intermodal_phase_2 / np.pi),
            min(dx_finesse_2 / np.pi),
            " pi",
        )
        print("∆x1: ", max(dx_finesse_1), min(dx_finesse_1), " m")
        print("∆x2: ", max(dx_finesse_2), min(dx_finesse_2), " m")
        print("∆θ1: ", max(dtheta_1), min(dtheta_1), " m")
        print("∆θ2: ", max(dtheta_2), min(dtheta_2), " m")
        return (
            intermodal_phase_1,
            intermodal_phase_2,
            dx_finesse_1,
            dx_finesse_2,
            dtheta_1,
            dtheta_2,
            out.x[0],
        )

    def comparison_plot(mirror, theta_low, theta_high, npoints, ax=None):
        delta_x1, delta_x2, delta_theta = geometric(
            mirror, theta_low, theta_high, npoints
        )
        (
            intermodal_phase_1,
            intermodal_phase_2,
            dx_finesse_1,
            dx_finesse_2,
            dtheta_1,
            dtheta_2,
            xaxis,
        ) = run_simulation(mirror, theta_low, theta_high, npoints)
        if ax is None:
            fig, ax = plt.subplots(nrows=3, sharex=True, figsize=(4, 6 / 3 * 4))
        xaxis = 1e6 * xaxis
        ax2col = "darkgreen"
        # Plot dx1
        (l3,) = ax[0].plot(xaxis, 1e6 * delta_x1, "-", label="Geometric")
        (l4,) = ax[0].plot(xaxis, 1e6 * dx_finesse_1, "--", label="Modal")
        ax[0].plot([], [], "--", color=ax2col, label="Difference")
        ax[0].set_ylabel(r"$\Delta \mathrm{x}_1$ [µm]")
        # Plot dx1 err
        ax1_2 = ax[0].twinx()
        (l5,) = ax1_2.plot(xaxis, 1e12 * (delta_x1 - dx_finesse_1), "--", color=ax2col)
        ax1_2.tick_params(axis="y", colors=ax2col)
        ax1_2.set_ylabel("Diff. [pm]", color=ax2col)
        # plot dx2
        ax[1].plot(xaxis, 1e6 * delta_x2, "-", label="Geometric")
        ax[1].plot(xaxis, 1e6 * dx_finesse_2, "--", label="Modal")
        ax[1].plot([], [], "--", color=ax2col, label="Difference")
        ax[1].set_ylabel(r"$\Delta \mathrm{x}_2$ [µm]")
        # plot dx2 err
        ax2_2 = ax[1].twinx()
        ax2_2.plot(xaxis, 1e12 * (delta_x2 - dx_finesse_2), "--", color=ax2col)
        ax2_2.tick_params(axis="y", colors=ax2col)
        ax2_2.set_ylabel("Diff. [pm]", color=ax2col)
        # plot dtheta1
        ax[2].plot(xaxis, 1e6 * delta_theta, "-", label="Geometric")
        ax[2].plot(xaxis, 1e6 * dtheta_1, "--", label="Modal")
        ax[2].plot([], [], "--", color=ax2col, label="Difference")
        ax[2].set_ylabel(r"$\theta^\mathrm{x}_0$ at M1 [µrad]")
        # plot dtheta1 err
        ax3_2 = ax[2].twinx()
        ax3_2.plot(xaxis, 1e12 * (dtheta_1 - delta_theta), "--", color=ax2col)
        ax3_2.tick_params(axis="y", colors=ax2col)
        ax3_2.set_ylabel("Diff. [prad]", color=ax2col)
        ax[0].legend()
        ax[2].set_xlabel(f"M{mirror} misalignment [µrad]")

    fig, ax = plt.subplots(nrows=3, ncols=2, sharex=True, figsize=(8, 6 / 3 * 4))
    comparison_plot(1, theta_low, theta_high, npoints, ax=ax[:, 0])
    comparison_plot(2, theta_low, theta_high, npoints, ax=ax[:, 1])
    ax[2, 1].set_ylim([-1.1, +1.1])
    fig.tight_layout()
    kat.M2.xbeta = theta_high
    kat.M1.xbeta = 0
    (
        intermodal_phase_1,
        intermodal_phase_2,
        dx_finesse_1,
        dx_finesse_2,
        dtheta_1,
        dtheta_2,
        xaxis,
    ) = process_sim(
        kat.run(
            f"""
            xaxis(
                parameter=M1.T,
                mode=log,
                start=1e-7,
                stop=1,
                steps={npoints},
                pre_step=none,
                post_step=none)
            """
        )
    )
    delta_x1, delta_x2, delta_theta = geometric(2, 0, kat.M2.xbeta, npoints)
    fig, ax = plt.subplots(figsize=(8, 8 / 3))
    ## Plot phases
    ax.loglog(
        xaxis, 1e6 * np.abs(delta_x1[-1] - dx_finesse_1), "-", label="Plane mirror (M1)"
    )
    ax.loglog(
        xaxis,
        1e6 * np.abs(delta_x2[-1] - dx_finesse_2),
        "--",
        label="Hemispherical mirror (M2)",
    )
    ax.set_ylabel("Difference in predicted\n spot position [µm]")
    ax.set_xlabel("Mirror transmissivity")
    ax.set_title(f"Mirror M2 tuning {1e6 * kat.M2.xbeta:.0f}urad")
    ax.legend()


def ex_05_modulation():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        # Laser with power 1W
        laser L0 P=1
        s s0 L0.p1 eom1.p1
        # Phase modulator at 40kHz, with a modulation index of 0.05
        mod eom1 f=40k midx=.05 order=5 mod_type=pm
        # Three amplitude detectors, looking at the first three positive modulation
        # sidebands
        ad bessel1 eom1.p2.o f=40k
        ad bessel2 eom1.p2.o f=80k
        ad bessel3 eom1.p2.o f=120k
        """
    )
    # Sweep the modulation index of the modulator from 0 to 10
    out = kat.run("xaxis(eom1.midx, lin, 0, 10, 300)")
    out.plot()


def ex_06_radiation_pressure():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        # Optical setup: laser, space and mirror:
        l l1 P=1
        s s1 l1.p1 m1.p1
        m m1 R=1 T=0
        # Define a pendulum for our mirror, with a z-motion resonance at 10Hz
        # and Q factor of 1000
        pendulum sus1 m1.mech mass=1 fz=10 Qz=1000
        # Measure the mirror's longitudinal motion
        xd m1_z m1.mech.z
        # Set a signal frequency to activate the signal simulation
        # (needed for radiation pressure effects to be observed)
        fsig(1)
        # Generate amplitude modulation at the laser
        sgen sig l1.amp
        """
    )
    # Sweep the frequency of the amplitude modulation
    out = kat.run("xaxis(fsig.f, log, 1, 100, 400)")
    out.plot(log=True)


def ex_07_homodyne():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import numpy as np

    import finesse

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        l l1 P=1-bs1.T
        s s1 l1.p1 bs1.p4 L=1
        sq sq1 db=10
        s s2 sq1.p1 bs1.p1
        bs bs1 R=0.999 L=0 alpha=45
        # Set a signal frequency to activate the signal simulation
        # (needed for squeezing effects to be observed)
        fsig(1)
        # Output the pure shot noise along with the qnoised detector
        # measuring the effects of the squeezing
        qnoised sqzd_noise bs1.p2.o
        qshot shot_noise bs1.p2.o
    """
    )
    out = kat.run("xaxis(l1.phase, lin, -90, 90, 360)")
    out.plot(separate=False)

    def to_db(quantity, reference, isAmplitude=False):
        if isAmplitude:
            factor = 20
        else:
            factor = 10
        return factor * np.log10(quantity / reference)

    sqz_db = to_db(out["sqzd_noise"], out["shot_noise"], isAmplitude=True)
    fig, ax = plt.subplots()
    ax.plot(out.x0, sqz_db)
    ax.set_ylabel("SQZ / ASQZ [dB]")
    ax.set_xlabel("LO Phase [deg]")


def ex_08_optical_spring():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        l l1 P=3
        s s1 l1.p1 Min.p1 L=1
        # The cavity
        m Min R=0.9937 L=0
        s scav Min.p2 Mend.p1 L=1
        m Mend R=1 L=0 phi=-0.048  # Roughly 40kHz detuning
        # Suspend mirrors on pendula
        pendulum sus1 Min.mech mass=0.25 fz=1 Qz=1000
        pendulum sus2 Mend.mech mass=0.25 fz=1 Qz=1000
        # Apply a `force signal' to the end mirror
        fsig(1)
        sgen force Mend.mech.F_z
        # we want to measure the longitudinal motion signal of both mirrors
        xd Mend_z Mend.mech.z
        xd Min_z Min.mech.z
        """
    )
    out = kat.run("xaxis(fsig.f, log, 0.1, 1k, 300)")
    out.plot(log=True)


def ex_09_aligo_sensitivity():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        ###########################################################################
        ###   Variables
        ###########################################################################
        var Larm 3995
        var Mtm  40
        var itmT 0.014
        var lmichx 4.5
        var lmichy 4.45
        ###########################################################################
        ###   Input optics
        ###########################################################################
        l L0 125
        s l_in L0.p1 prm.p1
        # Power recycling mirror
        m prm T=0.03 L=37.5u phi=90
        s prc prm.p2 bs.p1 L=53
        # Central beamsplitter
        bs bs T=0.5 L=0 alpha=45
        ###########################################################################
        ###   X arm
        ###########################################################################
        s lx bs.p3 itmx.p1 L=lmichx
        m itmx T=itmT L=37.5u phi=90
        s LX itmx.p2 etmx.p1 L=Larm
        m etmx T=5u L=37.5u phi=89.999875
        pendulum itmx_sus itmx.mech mass=Mtm fz=1 Qz=1M
        pendulum etmx_sus etmx.mech mass=Mtm fz=1 Qz=1M
        ###########################################################################
        ###   Y arm
        ###########################################################################
        s ly bs.p2 itmy.p1 L=lmichy
        m itmy T=itmT L=37.5u phi=0
        s LY itmy.p2 etmy.p1 L=Larm
        m etmy T=5u L=37.5u phi=0.000125
        pendulum itmy_sus itmy.mech mass=Mtm fz=1 Qz=1M
        pendulum etmy_sus etmy.mech mass=Mtm fz=1 Qz=1M
        ###########################################################################
        ###   Output and squeezing
        ###########################################################################
        s src bs.p4 srm.p1 L=50.525
        m srm T=0.2 L=37.5u phi=-90
        # A squeezed source could be injected into the dark port
        sq sq1 db=0 angle=90
        s lsqz sq1.p1 srm.p2
        # Differentially modulate the arm lengths
        fsig(1)
        sgen darmx LX.h
        sgen darmy LY.h phase=180
        # Output the full quantum noise limited sensitivity
        qnoised NSR_with_RP srm.p2.o nsr=True
        # Output just the shot noise limited sensitivity
        qshot NSR_without_RP srm.p2.o nsr=True
        # We could also display the quantum noise and the signal
        # separately by uncommenting these two lines.
        # qnoised noise srm.p2.o
        # pd1 signal srm.p2.o f=fsig
        """
    )
    out = kat.run("xaxis(darmx.f, log, 5, 5k, 100)")
    out.plot(log=True, separate=False)


def ex_10_shifted_beam():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import numpy as np

    import finesse

    finesse.configure(plotting=True)
    power = 1.5  # default laser power in Watt
    w0 = 10e-3  # default gaussian waist size in meter3
    alpha = 30  # angle of incidence for both BS in degrees0
    xbeta = 1e-5  # default tilt of both beam splitters in radians
    s1 = 1000  # distance till 1st beamsplitter
    s2 = 400  # distance between beamsplitters
    s3 = 600  # default distance between 2nd beamsplitter and detector
    z0 = -1200  # default waist position, 1200 meter right of laser
    maxtem = 7  # default maxtem
    basescript = f"""
    laser l1 P={power}
    gauss g1 l1.p1.o w0={w0} z={z0}
    modes(maxtem={maxtem})
    space s1 l1.p1 bs1.p1 L={s1}
    beamsplitter bs1 R=1 T=0 alpha={alpha} xbeta={xbeta}
    space s2 bs1.p2 bs2.p1 L={s2}
    beamsplitter bs2 R=1 T=0 alpha=bs1.alpha xbeta=bs1.xbeta
    space s3 bs2.p2 n1.p1 L={s3}
    # Add a 'nothing' component at the location of the detector
    nothing n1
    """
    basekat = finesse.Model()
    basekat.parse(basescript)
    # Simulations: measure beam 600 and 1000 meter after second BS
    s3a = 600
    s3b = 1000
    kat1 = finesse.Model()
    kat1.parse(basescript)
    kat1.parse(
        """
    # Set a 1-D CCD at n1, measure around peak at 0.8 at 600 and 1000m
    ccdline ccd1 node=n1.p1.i xlim=[0.5,1.0] npts=200
    # Also put a full 2-D CCD at the same position
    ccd ccd2 node=n1.p1.i xlim=[-3,3] ylim=[-3,3] npts=200
    """
    )
    out1 = kat1.run(
        f"""
        series(
            noxaxis(name="S600"),
            change(s3.L={s3b}),
            noxaxis(name="S1000")
        )
    """
    )
    # Plot the results
    f, ax = plt.subplots(ncols=2, figsize=(12, 5))
    for i, name, s3val in ([0, "S600", s3a], [1, "S1000", s3b]):
        ax[i].plot(kat1.ccd1.xdata, out1[name]["ccd1"], "r")
        ax[i].set_title(f"{s3val:.0f} meter")
        ax[i].set_xlabel("x/w0")
        ax[i].set_ylabel("intensity")
    f, ax = plt.subplots(ncols=1, figsize=(5, 5))
    pxy_extent = (
        kat1.ccd2.xdata.min(),
        kat1.ccd2.xdata.max(),
        kat1.ccd2.ydata.min(),
        kat1.ccd2.ydata.max(),
    )
    ax.imshow(out1["S600"]["ccd2"].T, aspect="auto", extent=pxy_extent)
    ax.set_xlabel("x/w0")
    ax.set_ylabel("y/w0")
    # Plotting range for x: [peak-dx...peak+dx]
    dx = 0.8
    # Calculate expected Delta and delta phi
    Delta = s2 * np.sin(2 * xbeta) / w0
    dphi = 2 * s2 * np.sin(xbeta) ** 2 / basekat.lambda0 * 360
    kat2 = finesse.Model()
    kat2.parse(basescript)
    kat2.parse(
        f"""
    fline fl1 node=n1.p1.i xlim=[{Delta}-{dx},{Delta}+{dx}] npts=200
    fline fl2 node=n1.p1.i xlim=[-{dx},{dx}] npts=40
    """
    )
    out2 = kat2.run(
        """
        series(
            noxaxis(name='full'),
            change(bs1.xbeta=0),
            noxaxis(name='ref')
        )
    """
    )
    f, ax = plt.subplots(ncols=1, figsize=(8, 5))
    ax.plot(
        kat2.fl1.xdata, np.angle(out2["full"]["fl1"], deg=True), "r", label="measured"
    )
    ax.plot(
        kat2.fl2.xdata + Delta,
        np.angle(out2["ref"]["fl2"], deg=True) + dphi,
        "bp",
        label="prediction",
    )
    ax.set_title("xbeta=1e-5 radian, with predicted peak")
    ax.set_xlabel("x/w0")
    ax.set_ylabel("phase (°)")
    ax.legend(loc="lower center")
    kat3 = finesse.Model()
    kat3.parse(basescript)
    # Note: important to specify n,m
    kat3.parse("amplitude_detector ad1 node=n1.p1.i f=0 n=0 m=0")
    out3 = kat3.run("xaxis(bs1.xbeta, lin, -1.5e-5, 1.5e-5, 40)")
    f, ax = plt.subplots(ncols=1, figsize=(8, 5))
    ax.plot(out3.x[0], np.angle(out3["ad1"], deg=True), "r", label="measured")
    ax.plot(
        out3.x[0],
        2 * s2 * np.sin(out3.x[0]) ** 2 / basekat.lambda0 * 360,
        "bp",
        label="2s·sin²(β)/λ·360°",
    )
    ax.set_title("measured versus calculated")
    ax.set_xlabel("xbeta (radian)")
    ax.set_ylabel("phase (°)")
    ax.legend(loc="upper center")


def ex_angular_radiation_pressure():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import cmath

    import numpy as np

    import finesse
    import finesse.components as fc
    import finesse.detectors as det
    from finesse.analysis.actions import FrequencyResponse, Noxaxis

    finesse.init_plotting()
    L = 3994.5  # [m]
    I = 0.757  # [kg.m^2]
    c = 299792458  # [m.s^-1]
    f_sus = 0.6  # [Hz]
    model = finesse.Model()
    model.fsig.f = 1  # set some initial signal frequency
    model.modes(maxtem=1)  # first order modes for modelling alignment signals
    LASER = model.add(fc.Laser("LASER", P=1000))
    # Add two mirrors for the cavity and attach a pendulum mechanics
    M1 = model.add(fc.Mirror("M1", R=0.986, T=0.014, Rc=1934))
    model.add(
        fc.mechanical.Pendulum(
            "M1_sus", model.M1.mech, mass=np.inf, I_yaw=np.inf, I_pitch=I, fpitch=f_sus
        )
    )
    M2 = model.add(fc.Mirror("M2", R=1, T=0, Rc=2245))
    model.add(
        fc.mechanical.Pendulum(
            "M2_sus", model.M2.mech, mass=np.inf, I_yaw=np.inf, I_pitch=I, fpitch=f_sus
        )
    )
    model.connect(M1.p1, M2.p1, L=L)
    model.connect(LASER.p1, M1.p2)
    model.add(fc.Cavity("cavARM", M2.p1.o))
    model.add(det.PowerDetector("P", M1.p1.o))
    # cavity power
    # Now we compute the decomposition of HARD and SOFT modes into motions of M1 and M2
    g_1 = 1 - float(L / np.abs(M1.Rcx.value))  # input mirror g-factor
    g_2 = 1 - float(L / np.abs(M2.Rcx.value))  # end mirror g-factor
    r = model.add_parameter(
        "r",
        2 / ((g_1 - g_2) + np.sqrt((g_2 - g_1) ** 2 + 4)),
        description="r term in eq.3.5 in T0900511",
    )
    HARD = model.add(
        fc.DegreeOfFreedom("HARD", M1.dofs.F_pitch, -1, M2.dofs.F_pitch, +r.ref)
    )
    SOFT = model.add(
        fc.DegreeOfFreedom("SOFT", M1.dofs.F_pitch, +r.ref, M2.dofs.F_pitch, +1)
    )
    print(HARD.amplitudes)
    print(SOFT.amplitudes)
    print("model.r = ", model.r.eval())
    print("Current HARD amplutudes:", HARD.amplitudes.astype(float))
    print(repr(model.M1.dofs.F_pitch.AC_IN))
    print(repr(model.M1.dofs.F_pitch.AC_OUT))
    print(repr(model.M1.dofs.F_pitch.DC))
    LASER.P = 1410 * 3 / 2  # get to roughly 600kW
    freq_resp = FrequencyResponse(
        np.geomspace(0.1, 10, 2000),
        [HARD.AC.i, SOFT.AC.i],
        [HARD.AC.o, SOFT.AC.o],
    )
    sol = model.run(freq_resp)
    # Alsp compute a single data point of the detectors to compute the power
    # circulating in the cavity
    out = model.run(Noxaxis())
    print(sol.out.shape)
    omega_0 = 2 * np.pi * f_sus
    P = out["P"]
    # Eq 2 from https://opg.optica.org/ao/fulltext.cfm?uri=ao-49-18-3474
    omega_plus = np.sqrt(
        omega_0**2
        + P
        * L
        / (I * c)
        * (-(g_1 + g_2) + cmath.sqrt(4 + (g_1 - g_2) ** 2))
        / (1 - g_1 * g_2)
    )
    omega_minus = np.sqrt(
        omega_0**2
        + P
        * L
        / (I * c)
        * (-(g_1 + g_2) - cmath.sqrt(4 + (g_1 - g_2) ** 2))
        / (1 - g_1 * g_2)
    )
    axs = finesse.plotting.bode(sol.f, sol["HARD.AC.o", "HARD.AC.i"], label="HARD")
    axs = finesse.plotting.bode(
        sol.f, sol["SOFT.AC.o", "SOFT.AC.i"], label="SOFT", axs=axs
    )
    axs[0].vlines(
        omega_0 / 2 / np.pi,
        -70,
        60,
        ls="--",
        label=f"$\\omega_{{0}} = {omega_0.real / 2 / np.pi:0.2f}$Hz",
        color="k",
        zorder=-100,
    )
    axs[0].vlines(
        omega_plus.real / 2 / np.pi,
        -70,
        60,
        ls="--",
        label=f"$\\omega_{{+}} = {omega_plus.real / 2 / np.pi:0.2f}$Hz",
        zorder=-100,
    )
    axs[0].vlines(
        omega_minus.real / 2 / np.pi,
        -70,
        60,
        color="red",
        ls="--",
        label=f"$\\omega_{{-}} = {omega_minus.real / 2 / np.pi:0.2f}$Hz",
        zorder=-100,
    )
    axs[0].margins(0)
    axs[0].set_title(
        f"E.Hirose, Appl. Opt. 49, 3474-3484 (2010)\nEq.2 vs FINESSE3, P={P / 1e3:0.0f}kW"
    )
    axs[0].legend()
    axs[0].set_ylabel("Magnitude [dB - rad/Nm]")
    LASER.P = 100
    sol = model.run(freq_resp)
    axs = finesse.plotting.bode(sol.f, sol["HARD.AC.o", "HARD.AC.i"], label="HARD")
    finesse.plotting.bode(sol.f, sol["SOFT.AC.o", "SOFT.AC.i"], label="SOFT", axs=axs)
    LASER.P = 1410 * 3 / 2  # get to roughly 600kW
    model.r = 1
    # Or alternatively you can replace the amplitudes directly with something like
    # HARD.amplitudes[:] = [-1, +1]
    # SOFT.amplitudes[:] = [+1, +1]
    sol = model.run(freq_resp)
    axs = finesse.plotting.bode(sol.f, sol["HARD.AC.o", "HARD.AC.i"], label="HARD")
    finesse.plotting.bode(sol.f, sol["SOFT.AC.o", "SOFT.AC.i"], label="SOFT", axs=axs)


def ex_cavity_eigenmodes():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import numpy as np

    import finesse
    from finesse.knm import Map
    from finesse.utilities.maps import circular_aperture

    finesse.init_plotting()
    model = finesse.Model()
    model.parse(
        """
        l l1
        mod mod1 f=9.1M midx=0.1
        m m1 R=0.984 T=0.014 Rc=-1940
        m m2 R=1 T=0 Rc=2245
        link(l1, mod1, m1, 3994, m2)
        cav cavity m2.p1.o
        modes(maxtem=2)
        fd E_arm m2.p1.i l1.f  # Field detector
        """
    )
    sol = model.run(
        """
        series(
            eigenmodes(cavity, -mod1.f, name="l9"),
            eigenmodes(cavity, 0,       name="c0"),
            eigenmodes(cavity, mod1.f,  name="u9"),
            xaxis(m2.phi, lin, -10, 190, 500, name="scan")
        )
        """
    )
    print(sol)
    sol["c0"]
    plt.semilogy(abs(sol["scan"]["E_arm"]) ** 2)
    sol["c0"].eigvectors
    print(abs(sol["c0"].eigvalues))
    print(np.angle(sol["c0"].eigvalues))
    from finesse.plotting import plot_field

    plot_field(model.homs, sol["c0"].eigvectors[:, 0], model.cavity.source.q)
    sol["c0"].plot_field(mode_idx=1)
    x = y = np.linspace(-0.17, 0.17, 100)
    ligo_ap = circular_aperture(x, y, 0.17)
    ligo_ap[:, abs(y) >= 0.163] = 0
    model.m1.surface_map = Map(x, y, amplitude=ligo_ap)
    model.m2.surface_map = Map(x, y, amplitude=ligo_ap)
    # Plot the aperture we are applying.
    plt.pcolormesh(x, y, ligo_ap, rasterized=True, shading="auto")
    plt.gca().set_aspect("equal")
    plt.colorbar()
    sol_aperture = model.run("eigenmodes(cavity, 0, name='c0')")
    for i in range(len(model.homs)):
        plot_field(model.homs, sol_aperture.eigvectors[:, i], model.cavity.source.q)


def ex_coupled_cavity_commands():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import numpy as np

    import finesse

    finesse.init_plotting()
    model = finesse.script.parse(
        """
    ###############################################################################
    ###   length definitions
    ###############################################################################
    variable Larm 3994.47
    variable LPR23 16.164  # distance between PR2 and PR3
    variable LPR3BS 19.538 # distance between PR3 and BS
    variable lmich 5.342   # average length of MICH
    variable lschnupp 0.08
    variable lPRC (3+0.5)*c0/(2*f1) # T1000298 Eq2.1, N=3
    variable f1 9099471
    variable f2 5*f1
    variable nsilica 1.45
    variable Mloss 0
    laser L0 P=1
    mod mod1 f=f1 midx=0.2
    link(L0, mod1, PRM)
    m PRM T=0.03 L=0 Rc=11.009 phi=0
    s lp1 PRM.p2 PR2.p1 L=lPRC-LPR3BS-LPR23-lmich
    bs PR2 T=250u L=Mloss alpha=-0.79 Rc=-4.545
    s lp2 PR2.p2 PR3.p1 L=LPR23
    bs PR3 T=0 L=Mloss alpha=0.615 Rc=36.027
    s lp3 PR3.p2 BS.p1 L=LPR3BS
    ###############################################################################
    ###   BS
    ###############################################################################
    bs BS R=1 L=0 alpha=45
    ###############################################################################
    ###   Yarm
    ###############################################################################
    # Distance from beam splitter to Y arm input mirror
    s ly1 BS.p2 ITMYlens.p1 L=lmich-lschnupp/2-ITMYsub.L*ITMYsub.nr
    lens ITMYlens f=34000
    s ly2 ITMYlens.p2 ITMYAR.p1
    m ITMYAR R=0 L=20u
    s ITMYsub ITMYAR.p2 ITMY.p1 L=0.2 nr=nsilica
    m ITMY T=0.014 L=Mloss Rc=-1934
    s LY ITMY.p2 ETMY.p1 L=Larm
    m ETMY T=5u L=Mloss Rc=2245
    pd Parm ETMY.p1.i
    pd Prc PRM.p2.o
    modes(even, maxtem=12)
    ad pu9 PRM.p2.i f=+f1
    ad pl9 PRM.p2.i f=-f1
    mathd prc_cost log10(Parm/280)+log10(abs(pu9))+log10(abs(pl9))
    """
    )
    # make a copy so that we can add different cav commands
    _model = model.deepcopy()
    _model.parse("cav cavYARM ETMY.p1.o")
    # nodes that will be defined by the ITM and ETM geometry
    print(_model.cavYARM.path.nodes)
    _model = model.deepcopy()
    _model.parse("cav cavYARM ETMY.p1.o")
    # Maximize the arm power with respect to the PRM curvature and ITMY lens
    # There are multiple optimisations that could be done here, depends on what
    # parameters you want to vary. This model is naturally astigmatic though due
    # to the off-axis telescope.
    opt_sol = _model.run(
        "maximize(Parm, [ITMYlens.f, PRM.Rcx, PRM.Rcy, PRM.phi, ETMY.phi])"
    )
    print("HOMs Parm =       ", _model.run("noxaxis()")["Parm"])
    _model.modes("off")
    print("Plane-wave Parm = ", _model.run("noxaxis()")["Parm"])
    # the plane-wave and HOM powers should be nearly identical, a sign that there
    # is no mode-mismatch. If so, we then update the original model to use these
    # new values
    model.ITMYlens.f = opt_sol.x[0]
    model.PRM.Rcx = opt_sol.x[1]
    model.PRM.Rcy = opt_sol.x[2]
    # Given the model is now in a perfectly matched state we can also take the
    # beam parameter at the input of the PRM and set the original model to use that
    model.PRM.p1.i.q = _model.PRM.p1.i.q
    model.gausses
    model.ITMYlens.f = np.inf  # opt_sol.x[0] # use opt_sol for perfect matching
    model.mismatches_table()

    def run(model, orders):
        model.modes("off")
        plane_wave = model.run()
        homs = []
        for n in orders:
            model.modes("even", maxtem=n)
            with model.temporary_parameters():
                homs.append(
                    model.run(
                        """
                    series(
                        maximize(Parm, [PRM.phi, ETMY.phi]),
                        noxaxis() # get detector outputs
                    )
                """
                    )["noxaxis"]
                )
        return plane_wave, homs

    orders = range(0, 10, 2)
    _model = model.deepcopy()
    _model.parse("cav cavYARM ETMY.p1.o")
    plane_wave, homs = run(_model, orders)
    _model = model.deepcopy()
    _model.parse("cav cavPRC PRM.p2.o via=ITMY.p1.i")
    plane_wave2, homs2 = run(_model, orders)
    _model = model.deepcopy()
    _model.parse("cav cavYARM ETMY.p1.o")
    _model.parse("cav cavPRC PRM.p2.o via=ITMY.p1.i")
    plane_wave3, homs3 = run(_model, orders)
    _model = model.deepcopy()
    _model.parse("cav cavYARM ETMY.p1.o via=PRM.p2.i")
    plane_wave4, homs4 = run(_model, orders)
    plt.plot(orders, tuple(_["Parm"] for _ in homs), label="cav ARM")
    plt.plot(orders, tuple(_["Parm"] for _ in homs2), label="cav PRC")
    plt.plot(orders, tuple(_["Parm"] for _ in homs3), label="cav ARM+PRC")
    plt.plot(orders, tuple(_["Parm"] for _ in homs4), label="long cav", ls="--")
    plt.hlines(plane_wave["Parm"], 0, 8, ls=":", color="m", label="plane-wave")
    plt.legend()
    plt.xlabel("maxtem")
    plt.ylabel("P arm")
    plt.title(repr(model.ITMYlens.f))


def ex_frequency_dependant_squeezing():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import numpy as np

    import finesse

    finesse.init_plotting()
    base = finesse.Model()
    base.parse(
        """
    variable ITM_T 0.014
    variable L_ARM 40k
    variable FC_detune 7.3 # Hz
    l l1 P=165
    link(l1, PRM)
    # ------------------------------------------------------------------------------
    # Power recycling cavity
    # ------------------------------------------------------------------------------
    m PRM T=0.03 L=40u phi=0
    s PRC PRM.p2 BS.p1 L=53.0
    bs BS R=0.5 T=0.5
    # ------------------------------------------------------------------------------
    # X-arm
    # ------------------------------------------------------------------------------
    s lx BS.p3 ITMX.p1 L=0
    m ITMX T=ITM_T L=0u phi=0
    s LX ITMX.p2 ETMX.p1 L=L_ARM
    m ETMX T=5u L=40u phi=0
    # ------------------------------------------------------------------------------
    # Y-arm
    # ------------------------------------------------------------------------------
    s ly BS.p2 ITMY.p1 L=0
    m ITMY T=ITM_T L=0u phi=90
    s LY ITMY.p2 ETMY.p1 L=L_ARM
    m ETMY T=5u L=40u phi=90
    # Signal recycling cavity
    s SRC BS.p4 SRM.p1 L=20
    m SRM T=0.02 L=0 phi=0
    # ------------------------------------------------------------------------------
    # Output optics
    # ------------------------------------------------------------------------------
    dbs OFI
    link(SRM.p2, OFI.p1)
    readout_dc AS OFI.p3.o
    # ------------------------------------------------------------------------------
    # suspensions
    # ------------------------------------------------------------------------------
    free_mass ITMX_sus ITMX mass=320
    free_mass ETMX_sus ETMX mass=320
    free_mass ITMY_sus ITMY mass=320
    free_mass ETMY_sus ETMY mass=320
    # ------------------------------------------------------------------------------
    # Degrees of Freedom
    # ------------------------------------------------------------------------------
    dof STRAIN LX.dofs.h +1  LY.dofs.h -1
    # signal generator
    sgen sig STRAIN
    qnoised NSR AS.p1.i nsr=True
    fsig(1)
    """
    )
    model = base.deepcopy()
    model.parse(
        """
    sq sqz db=10
    link(sqz, OFI.p2)
    """
    )
    with model.temporary_parameters():
        # No squeezing
        model.sqz.db = 0
        model.sqz.angle = 0
        sol_no_sqz = model.run("xaxis(fsig, log, 1, 5k, 100)")
        # Squeezing
        model.sqz.db = 10
        model.sqz.angle = 90
        sol_shot_sqz = model.run("xaxis(fsig, log, 1, 5k, 100)")
        # Squeezing
        model.sqz.db = 10
        model.sqz.angle = 0
        sol_rp_sqz = model.run("xaxis(fsig, log, 1, 5k, 100)")
    plt.loglog(sol_no_sqz.x1, abs(sol_no_sqz["NSR"]), label="No squeezing")
    plt.loglog(sol_shot_sqz.x1, abs(sol_shot_sqz["NSR"]), label="Shot-noise squeezing")
    plt.loglog(sol_rp_sqz.x1, abs(sol_rp_sqz["NSR"]), label="Rad. pressure squeezing")
    plt.legend()
    plt.xlabel("Frequency [Hz]")
    plt.ylabel("Sensitivity [h/sqrt{Hz}]")
    with model.temporary_parameters():
        model.FC_detune.value = 7.2
        model.sqz.db = 10
        # Give the variable references shorter names
        fsig = model.fsig.f.ref
        delta_f = model.FC_detune.ref
        # set the squeezing angle to be a symbolic function
        model.sqz.angle = 45 + 180 / np.pi * np.arctan(
            (fsig**2 - delta_f**2) / (fsig**2 + delta_f**2)
        )
        print(model.sqz.angle)  # print to see how it turned out
        sol_fc_sqz = model.run("xaxis(fsig, log, 1, 5k, 100)")
    plt.loglog(sol_no_sqz.x1, abs(sol_no_sqz["NSR"]), label="No squeezing")
    plt.loglog(sol_shot_sqz.x1, abs(sol_shot_sqz["NSR"]), label="Shot-noise squeezing")
    plt.loglog(sol_rp_sqz.x1, abs(sol_rp_sqz["NSR"]), label="Rad. pressure squeezing")
    plt.loglog(sol_fc_sqz.x1, abs(sol_fc_sqz["NSR"]), label="Filter cavity")
    plt.legend()
    plt.xlabel("Frequency [Hz]")
    plt.ylabel("Sensitivity [h/sqrt{Hz}]")
    # Run an optimiser to find ideal rotation
    sol_opt_sqz = model.run(
        "xaxis(fsig, log, 1, 5k, 100, pre_step=minimize(NSR, sqz.angle))"
    )
    plt.loglog(sol_no_sqz.x1, abs(sol_no_sqz["NSR"]), label="No squeezing")
    plt.loglog(sol_fc_sqz.x1, abs(sol_fc_sqz["NSR"]), label="Filter cavity (symbolic)")
    plt.loglog(
        sol_opt_sqz.x1,
        abs(sol_opt_sqz["NSR"]),
        label="Filter cavity (optimized)",
        ls="--",
    )
    plt.legend()
    plt.xlabel("Frequency [Hz]")
    plt.ylabel("Sensitivity [h/sqrt{Hz}]")
    # Extract every optimisisation solution (`x`) from all the pre-step minimize actions
    opt_angle = sol_opt_sqz["pre_step", "minimize"].x
    plt.semilogx(sol_opt_sqz.x1, opt_angle)
    plt.xlabel("Frequency [Hz]")
    plt.ylabel("Optimal squeezing angle [deg]")
    with model.temporary_parameters():
        model.FC_detune.value = 7.2
        model.SRM.phi += 0.1  # Detune slightly
        model.sqz.db = 10
        # Give the variable references shorter names
        fsig = model.fsig.f.ref
        delta_f = model.FC_detune.ref
        # set the squeezing angle to be a symbolic function
        model.sqz.angle = 45 + 180 / np.pi * np.arctan(
            (fsig**2 - delta_f**2) / (fsig**2 + delta_f**2)
        )
        sol_fc_sqz = model.run("xaxis(fsig, log, 1, 5k, 100)")
        # Run an optimiser to find ideal rotation
        model.sqz.angle = 0
        sol_opt_sqz = model.run(
            "xaxis(fsig, log, 1, 5k, 100, pre_step=minimize(NSR, sqz.angle))"
        )
        plt.loglog(sol_no_sqz.x1, abs(sol_no_sqz["NSR"]), label="No squeezing")
        plt.loglog(
            sol_fc_sqz.x1, abs(sol_fc_sqz["NSR"]), label="Filter cavity (symbolic)"
        )
        plt.loglog(
            sol_opt_sqz.x1, abs(sol_opt_sqz["NSR"]), label="Filter cavity (optimized)"
        )
        plt.legend()
        plt.xlabel("Frequency [Hz]")
        plt.ylabel("Sensitivity [h/sqrt{Hz}]")
        plt.title("Detuned SRC")
        plt.figure()
        # Extract every optimisisation solution (`x`) from all the pre-step minimize actions
        opt_angle = sol_opt_sqz["pre_step", "minimize"].x
        plt.semilogx(sol_opt_sqz.x1, opt_angle)
        plt.xlabel("Frequency [Hz]")
        plt.ylabel("Squeezing angle [deg]")
        plt.title("Optimised squeezing angle")
    model = base.deepcopy()  # make a copy so we can add extra features
    model.parse(
        """
    sq sqz db=10
    dbs FI
    m FC1 L=0 T=0.0015
    s sFC FC1.p2 FC2.p1 L=4e3
    m FC2 L=0 T=0 phi=-0.022
    link(sqz, FI.p1)
    link(FI.p3, FC1)
    link(FI.p4, OFI.p2)
    """
    )
    model.sqz.db = 10
    model.sqz.angle = 90
    model.FC1.T = 0.0015
    model.FC1.R = (
        1 - model.FC1.T.ref
    )  # Make sure we update the R correctly when we change T
    model.FC2.phi = -0.022
    # Optimise the NSR at the corner frequency
    model.fsig.f = 7.2
    # Run the optimisation a few times to reach a better level
    for _ in range(3):
        model.run("minimize(NSR, [FC1.T, FC2.phi, sFC.L])")
    # finally compute the NSR
    sol_fc2_sqz = model.run("xaxis(fsig, log, 1, 5k, 100)")
    plt.loglog(sol_no_sqz.x1, abs(sol_no_sqz["NSR"]), label="No squeezing")
    plt.loglog(sol_fc2_sqz.x1, abs(sol_fc2_sqz["NSR"]), label="Filter cavity model")
    plt.loglog(
        sol_opt_sqz.x1, abs(sol_opt_sqz["NSR"]), label="Filter cavity (ideal)", ls="--"
    )
    plt.legend()
    plt.xlabel("Frequency [Hz]")
    plt.ylabel("Sensitivity [h/sqrt{Hz}]")
    print("FC1.T =", model.FC1.T)
    print("FC2.phi =", model.FC2.phi)
    print("FC L =", model.sFC.L)


def ex_lock_actions():
    import matplotlib

    # Suppress plot showing
    matplotlib.use("Agg")
    import finesse
    from finesse.analysis.actions import (
        CheckLinearity,
        DragLocks,
        GetErrorSignals,
        OptimiseRFReadoutPhaseDC,
        RunLocks,
        SensingMatrixDC,
    )
    from finesse.analysis.actions.locks import SetLockGains

    finesse.configure(plotting=True)
    kat = finesse.Model()
    kat.parse(
        """
        # Adding a laser and modulator for use in
        # Pound-Drever-Hall-like signals.
        l L0 P=1
        s l_mod1 L0.p1 eo1.p1
        mod eo1 10M 0.1
        s s0 eo1.p2 BS.p1
        bs BS R=0.5 T=0.5
        # North Arm
        s s1 BS.p2 NI.p1
        m NI R=0.99 T=0.01 Rc=1429 phi=90
        s CAV NI.p2 NE.p1 L=10
        m NE R=0.991 T=0.009 Rc=1430 phi=90
        #East Arm
        s s2 BS.p3 EI.p1
        m EI R=0.99 T=0.01 Rc=1429 phi=0
        s CAV2 EI.p2 EE.p1 L=10
        m EE R=0.991 T=0.009 Rc=1430 phi=0
        dof NEz NE.dofs.z +1
        dof EEz EE.dofs.z +1
        dof NIz NI.dofs.z +1
        readout_rf rd_pdh1 NI.p1.o f=10M
        readout_rf rd_pdh2 EI.p1.o f=10M
        readout_rf rd_DF BS.p4.o f=10M
        # Two locks to put the cavities in resonance, and
        # one to move to the dark fringe.
        lock cav1_lock rd_pdh1.outputs.I NEz.DC 1 1e-9
        lock cav2_lock rd_pdh2.outputs.I EEz.DC 1 1e-9
        lock DF_lock rd_DF.outputs.I NIz.DC 1 1e-9
        cav cav1 NI.p2.o
        cav cav2 EI.p2.o
        modes(maxtem=5)
        """
    )
    kat.NEz.DC += 0.01
    kat.EEz.DC += 0.001
    sol_errs = kat.run(GetErrorSignals("cav1_lock", "DF_lock"))
    print("Initial Error Signals:", sol_errs.results, end="\n\n")
    sol_phases = kat.run(OptimiseRFReadoutPhaseDC("NEz", "rd_pdh1", "EEz", "rd_pdh2"))
    print("Optimal Readout Phases:", sol_phases.phases, end="\n\n")
    kat.run(SetLockGains(optimize_phase=True))
    sol_matrix = kat.run(
        SensingMatrixDC(["NEz", "EEz", "NIz"], ["rd_pdh1", "rd_pdh2", "rd_DF"])
    )
    print("Sensing Matrix:", sol_matrix, end="\n\n", sep="\n")
    kat.run(CheckLinearity(plot_results=True))
    kat.run(
        RunLocks(
            method="newton",
            display_progress=False,
            max_iterations=100,
            optimize_phase=True,
            scale_factor=1,
        )
    )
    print("Results of dragging locks:")
    kat.run(
        DragLocks(
            method="newton",
            parameters=["NE.Rcx"],
            stop_points=[1],
            relative=True,
            max_iterations=100,
        )
    )
