Simulating off-axis beams: convergence and scaling

In order to properly describe off-axis beams in the paraxial approximation, a number of higher order modes will need to be taken into account. How many modes need to be included (by setting maxtem using the modes command) will depend on the optical layout and geometry of the beam such as the waist position \(z_0\) and the relative distance of the different optical components to each other. Typically, if we choose maxtem too small, we expect to miss part of the beam in simulations, resulting in missing total beam energy. In this section we will therefore study how this total beam energy - as measured using a power_detector_dc detector - behaves when changing the various parameters. In general, when increasing maxtem we expect convergence towards the total energy put into the beam by the laser.

We will use the same optical layout as used in the previous section Simulating off-axis beams: calculating the shift, it is sketched again in Fig. 14.


Fig. 14 Basic setup

Both beamsplitters are tilted over the same small angle \(\beta\), resulting in a parallel off-axis beam. In order to correctly describe the physics at increasingly larger \(\beta\), more and more higher order modes will need to be taken into account. We will start by investigating this behaviour for different values of maxtem. We will then study the effects of changing the two gaussian beam parameters waist position \(z_0\) and waist size \(w_0\). For \(z_0\) we will find an optimal position within the optical layout, providing the largest valid range of \(\beta\). Once we have found the optimal \(z_0\), we will use it to study the valid \(\beta\) range as a function of the distance between the two beamsplitters, looking in particular at the scaling under changes of the waist size \(w_0\) and linking it to the diffraction angle and Rayleigh range.

Base script for the different simulations

Each of the simulations will be using the following base script, consisting of a laser, two beamsplitter elements and a power_detector_dc. The different (default) parameters are defined as python variables directly above the Finesse KatScript. The layout is the same as that used in Aligning and translating a beam with steering mirrors.

import matplotlib.pyplot as plt
import finesse

power = 1.5  # default laser power in Watt
w0 = 10e-3   # default gaussian waist size in meter
alpha = 30   # angle of incidence for both BS in degrees
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"""
# Define laser and gaussian beam
laser l1 P={power}
gauss g1 l1.p1.o w0={w0} z={z0}

# define beamsplitters and their positions
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}

# use photodiode to measure integrated power
nothing n1
power_detector_dc pd1 node=n1.p1.i pdtype=none

basekat = finesse.Model()

Tilt angle dependency for various maxtem

We add an appropriate xaxis action to the model to measure the total energy as a function \(\beta\) for 4 different values of maxtem: 3, 5, 7 and 9 (set using the modes command)

# Set of maxtem values (and their plot styles)
maxtem_vals   = [3, 5, 7, 9]
maxtem_styles = ['r', 'b', 'g', 'm']

# Define the model and set sweep parameter to xbeta
kat1 = finesse.Model()
kat1.parse("xaxis(bs1.xbeta, lin, 0, 4e-5, 40)")

out1a = []
for maxtem_val in maxtem_vals:

In order to verify that the convergence is independent of the position of the photodiode, i.e. to verify that the (diverging) gaussian beam emerging from the second beamsplitter remains stable over larger distances, we repeat the simulation at fixed maxtem = 7 at two different values of \(s_3\): 600 and 1000 meter:

# Set of s3 values (and their plot styles)
s3_vals   = [600, 1000]
s3_styles = ['g', 'b.']

# Reset maxtem to its default

out1b = []
for s3_val in s3_vals:
    kat1.spaces.s3.L = s3_val

We plot both results below:

f,ax = plt.subplots(ncols=2, figsize=(12, 5))

ax[0].set_title(f"Effect maxtem on total power (distance {s3})")
for sim in range(len(out1a)):
    ax[0].plot(out1a[sim].x[0], out1a[sim]['pd1'],
               maxtem_styles[sim], label=f"maxtem {maxtem_vals[sim]}")

ax[1].set_title(f"Effect distance on total power (maxtem {maxtem})")
for sim in range(len(out1b)):
    ax[1].plot(out1b[sim].x[0], out1b[sim]['pd1'],
               s3_styles[sim], label=f"{s3_vals[sim]} meter")

for i in 0,1:
    ax[i].set_xlabel("xbeta (radian)")
    ax[i].set_ylabel("power (Watt)")
    ax[i].legend(loc="lower left")

From the left figure, it is clear that we can simulate larger and larger \(\beta\) if we include more and more higher order modes in the simulation, i.e. by increasing the maxtem value. The valid range of \(\beta\) scales roughly linearly with maxtem, meaning the number of modes to be includes roughly scales quadratically (note that for maxtem = \(N\) there are \(\frac{1}{2}(N+1)(N+2)\) modes).


do we expect convergence to break down at very high beta?

From the right figure we see that the convergence is not dependent on the distance behind the second beamsplitter at which we measure the total power in the bundle indicating a stable beam.

Waist position dependency

Above we showed that the total power in the beam can give a clear indication for the range of validity of the tilt angle \(\beta\) (for given maxtem). We will now use this dependency to find the optimal position of the waist of the Gaussian beam \(z_0\).

We run simulations for two different ranges in \(z_0\): one for a larger range that starts at the laser and ends at the detector, and a second for a smaller range and closer to the two beamsplitters, where \(z_0\) varies from 600 till 1600 meter to the right of the laser (i.e from 400 meter left of the first beamsplitter till 200 meter right of the second beamsplitter).

Each simulation is run using an x2axis action varying both \(\beta\) and \(z_0\). Note that as sweep parameter in the x2axis action we have to specify either the x- or the y-coordinate of \(z_0\), so we need to make sure that they change simultaneously. We can easily enforce this by making one a reference to the other (see Expressions and references).

# Define the model
kat2 = finesse.Model()

# Make sure zy equals zx
kat2.g1.zy = kat2.g1.zx.ref

# vary beta and z0
    x2axis(bs1.xbeta, lin, 1e-5, 4e-5, 40, g1.zx, lin, 0, -2000, 60, name="full"),
    x2axis(bs1.xbeta, lin, 1e-5, 4e-5, 40, g1.zx, lin, -600, -1600, 60, name="zoom")
out2 =

# Plot the full and zoom results
f,ax = plt.subplots(ncols=2, figsize=(11.8, 5))
for (i, name) in ([0, 'full'], [1, 'zoom']):
    pxy_extent = (out2[name].x[0].min(), out2[name].x[0].max(),
                  out2[name].x[1].max(), out2[name].x[1].min())
    p = ax[i].imshow(out2[name]['pd1'].T, aspect='auto', extent = pxy_extent)
    plt.colorbar(p, ax = ax[i])
    ax[i].contour(out2[name]['pd1'].T, colors='white', extent = pxy_extent)
    ax[i].set_xlabel("xbeta (radian)")
    ax[i].set_ylabel("z0 (w.r.t. laser)")
    ax[i].set_title("total power (W)")
    ax[i].ticklabel_format(style='sci', scilimits=(0,0))

The left plot shows the full range from the laser till the detector, the right plot the close-up range around the beamsplitters. We see that the optimal location for the waist is around 1 km right of the laser, i.e. at the location of the first beamsplitter.

Effect of waist size

Now that we know the optimal location for the Gaussian waist, we will fix it that position and then study the effect of changing the distance between the two beamsplitters \(s_2\) and the scaling behaviour with respect to the waist size \(w_0\) of the beam.

We do two simulations, one at the default waist size \(w_0\) = 10 mm and one at a slightly larger waist size \(w_0\) = 12 mm. We will vary \(s_2\) between 0 and 1 km for the first simulation and between 0 and about 1.4 km for the second (we will discuss below why we choose this range). We do both simulations in a single series, using the change action to adapt the waist size. We again use a reference to ensure that the x- and y-components of both \(w_0\) and \(z_0\) are changing simultaneusly (see Expressions and references).

# w0, beta-range and L for 10mm run
w0a = 10
beta_a = (1e-5, 4e-5)
La = 1000

# w0, beta-range and L for 12mm run
w0b = 12
beta_b = (1e-5/1.2, 4e-5/1.2)
Lb = 1000*1.2**2

# Define model
kat3 = finesse.Model()

# could set w0x and w0y separately, easier to use ref for y
kat3.g1.w0y = kat3.g1.w0x.ref

# Position gauss at optimal z0: i.e. position bs1
kat3.g1.zx = -1000
kat3.g1.zy = kat3.g1.zx.ref

# run both subsimulations using series()
    x2axis(bs1.xbeta, lin, {beta_a[0]}, {beta_a[1]}, 40, s2.L, lin, 0, {La}, 60, name="a"),
    x2axis(bs1.xbeta, lin, {beta_b[0]}, {beta_b[1]}, 40, s2.L, lin, 0, {Lb}, 60, name="b")
out3 =

And we plot the results

# Plot results for both waist sizes
f,ax = plt.subplots(ncols=2, figsize=(11.8, 5))
for (i, name, w0val) in ([0, 'a', w0a], [1, 'b', w0b]):
    pxy_extent = (out3[name].x[0].min(), out3[name].x[0].max(),
                  out3[name].x[1].min(), out3[name].x[1].max())
    p = ax[i].imshow(out3[name]['pd1'].T, aspect='auto', extent = pxy_extent)
    plt.colorbar(p, ax = ax[i])
    ax[i].contour(out3[name]['pd1'].T, colors='white', extent = pxy_extent)
    ax[i].set_xlabel("xbeta (radian)")
    ax[i].set_ylabel("length s2 (meter)")
    ax[i].set_title(f"w0={w0val}mm, total power (W)")
    ax[i].ticklabel_format(axis='y', style='plain', scilimits=(0,0))
# Also add rescaled contours from 'a' to 'b'
pxy_extent_scaled = (out3['a'].x[0].min()/1.2,   out3['a'].x[0].max()/1.2,
ax[1].contour(out3['a']['pd1'].T, colors='green', linestyles='dotted',
              extent = pxy_extent_scaled);

Looking first at the left picture, we see that the usable range of \(\beta\) roughly remains constant till a distance of about 200-300 meter and then quickly decreases for larger distances. This distance of 200-300 meter corresponds roughly to the Rayleigh range \(z_R\)

(19)\[z_R = \frac{\pi w_0^2}{\lambda} \]

which for the parameters in the left plot (\(w_0 = 10\textrm{mm}, \lambda = 1.064 \mu\textrm{m}\)) is 295 meter. It would seem that our default value of 400 meter is not ideal, but we will discuss this further below when looking at the maximum attainable shift \(\Delta\).

Although in general the largest usable \(\beta\) increases as a function of maxtem we expect that it will be proportional to the diffraction angle \(\Theta\) given by (see Eq. (9.19) in [12])

(20)\[\Theta = \arctan \left(\frac{w_0}{z_R}\right) \approx \frac{w_0}{z_R} = \frac{\lambda}{\pi w_0} \]

which - again for the parameters used in the left plot - equals \(3.4 \cdot 10^{-5}\) radians (which happens to be very close to the maximum \(\beta\) at the smaller distances for the maxtem = 7 used in this simulation).

We can now discuss the scaling behaviour as a function of the waist size \(w_0\). From the diffraction angle \(\Theta\) Eq. (20) we expect the useable range of \(\beta\) to scale as \(1/w_0\), while from the Rayleigh range \(z_R\) Eq. (19) we expect the vertical range to scale as \(w_0^2\). To verify these assumptions, we use - for the right plot with the results of \(w_0\) = 12 mm - a 1.2 times smaller horizontal range and a 1.44 times larger vertical range. We also plot in the same plot the contours (dotted green) from the left plot, scaled with the same factors and see two identical pictures, confirming that \(z_R\) and \(\Theta\) set the relevant scales.

From Eq. (10) in Simulating off-axis beams: calculating the shift it now follows that for small angles \(\beta\) the largest attainable shift \(\Delta\) scales (roughly) linearly with \(w_0\), i.e. the dimensionless shift in units of \(w_0\) is actually independent of that waist size \(w_0\). It is therefore interesting to also look at the beam energy as a function of \(\Delta/w_0\) instead of as a function of \(\beta\). This will allow us to find the optimal distance between the two beamsplitters, leading to the largest range of validity for \(\Delta/w_0\). We could refactor the results of the above simulation, but it is easier to do a simulation using \(\Delta/w_0\) directly in the x2axis.

import numpy as np

# define model
kat4 = finesse.Model()

# Position gaussian waist at the optimal location
kat4.g1.zx = -1000
kat4.g1.zy = kat3.g1.zx.ref

# Introduce variable delta
kat4.parse("var Delta 1.0")
# xbeta follows from Delta/w0 = s2*sin(2*beta)
kat4.bs1.xbeta = 0.5*np.arcsin(w0*kat4.Delta.ref/kat4.spaces.s2.L.ref)

# Run actual simulation
x2axis(Delta, lin, 0.0, 4.0, 40, s2.L, lin, 0, 1500, 60)
out4 =

and plot the result

f,ax = plt.subplots()
pxy_extent = (out4.x[0].min(), out4.x[0].max(),
              out4.x[1].min(), out4.x[1].max())
p = ax.imshow(out4['pd1'].T, aspect='auto', extent = pxy_extent)
plt.colorbar(p, ax = ax)
ax.contour(out4['pd1'].T, colors='white', extent = pxy_extent)
ax.set_ylabel("length s2 (meter)")
ax.set_title(f"w0={w0a}mm, total power (W)")
ax.ticklabel_format(axis='y', style='plain', scilimits=(0,0))

We see that for larger distances between the two beamsplitters, the range of validity of \(\Delta/w_0\) becomes constant, it is then only dependent on the number of higher order modes included.

To summarize, we have seen that the best location for the first beamsplitter is at the waist of the Gaussian beam. Furthermore, the optimal position for the second beamsplitter is just beyond the Rayleigh range. Going to larger distances decreases the maximum tilt, going to smaller distances decreases the maximum (dimensionless) beamshift. The maximum value of this beamshift is not dependent on the waist size and for larger inter-beamsplitter distances depends only on the number of higher order modes included in the simulation.