Trace verbosity¶
The trace
command in Finesse 2 provided options for printing various beam tracing
and coupling coefficient information associated with a file. On this page we will detail
the replacements (and improvements) for these options.
For a brief summary of the Finesse 2 trace
options and how these translate into
the new features, see the table below. The descriptions of each integer, n, in terms of
the Finesse 2 behaviour can be found in the old syntax reference.
Old |
New Finesse equivalent |
---|---|
1 |
The |
2 |
Print the |
4 |
|
8 |
Display the |
16 |
|
32 |
Use |
64 |
Use |
128 |
Draw the |
We will now go into a bit more detail on each of these options, in terms of the recommended ways to use them. For all of the following sections we will employ the example of a Fabry-Perot Michelson interferometer with aLIGO-like geometrical parameters for the arm cavities:
import finesse
finesse.configure(plotting=True)
model = finesse.Model()
model.parse("""
# Radii of curvature of arm cavity mirrors
var XROC1 -1934
var XROC2 2245
var YROC1 -1934
var YROC2 2245
l L0 P=1
link(L0, BS)
bs BS
s sy BS.p2 ITMY.p1 L=10
m ITMY R=0.99 T=0.01 Rc=[YROC1, YROC1]
s LY ITMY.p2 ETMY.p1 L=4k
m ETMY R=0.99 T=0.01 Rc=[YROC2, YROC2]
s sx BS.p3 ITMX.p1 L=10
m ITMX R=0.99 T=0.01 Rc=[XROC1, XROC1]
s LX ITMX.p2 ETMX.p1 L=4k
m ETMX R=0.99 T=0.01 Rc=[XROC2, XROC2]
cav cavXARM ITMX.p2
cav cavYARM ITMY.p2
""")
Note that the use of variables here for the radii of curvature of the mirrors allows us to ensure that these mirrors remain non-astigmatic; as the references will update for both planes whenever these variables change. We also separate these into X-arm and Y-arm variables such that updating one set doesn’t affect the other arm cavity. If it was useful to do so, we could simply have a single pair of RoC values instead — thereby ensuring both arm cavities remain equivalent.
Each section heading below describes the task with the old ‘trace’ command equivalent in parentheses.
Listing TEM modes used (‘trace 1’)¶
One can simply query the modes included in a model with the Model.modes()
function. In the case of our example above, we have:
print(model.modes())
[[0 0]]
Note that the existence of the Cavity
objects here automatically switches on
the modal basis with only the fundamental (pure Gaussian) mode modelled. The modal mode
can be explicitly turned off also using Model.modes()
:
print(model.modes("off"))
None
If we select additional modes via Model.modes()
and print the attribute
again then we will see this reflected in the output:
model.modes(maxtem=2)
print(model.modes())
[[0 0]
[1 0]
[0 1]
[2 0]
[1 1]
[0 2]]
See also
Selecting the modes to model for a comprehensive guide on mode selection.
Accessing cavity parameters (‘trace 2’)¶
There are several ways to obtain properties of Cavity
objects. To display a
verbose table of all the parameters (in their current state) associated with a cavity:
print(model.cavXARM.info_parameter_table())
┌───────────────────────┬─────────────────────────────────┐
│ Description │ Value │
╞═══════════════════════╪═════════════════════════════════╡
│ FSR │ 37474.05725 │
├───────────────────────┼─────────────────────────────────┤
│ Loss │ 0.01990000000000003 │
├───────────────────────┼─────────────────────────────────┤
│ Finesse │ 312.5845222828291 │
├───────────────────────┼─────────────────────────────────┤
│ FWHM │ 119.88455786717795 │
├───────────────────────┼─────────────────────────────────┤
│ Storage time │ 0.002655136673535981 │
├───────────────────────┼─────────────────────────────────┤
│ Pole │ 59.94227893358897 │
├───────────────────────┼─────────────────────────────────┤
│ Round trip length │ 8000.0 │
├───────────────────────┼─────────────────────────────────┤
│ Waist size │ [0.01195054 0.01195054] │
├───────────────────────┼─────────────────────────────────┤
│ Waist position │ [-1837.21538864 -1837.21538864] │
├───────────────────────┼─────────────────────────────────┤
│ Stability (m-factor) │ [0.67018515 0.67018515] │
├───────────────────────┼─────────────────────────────────┤
│ Stability (g-factor) │ [0.83509258 0.83509258] │
├───────────────────────┼─────────────────────────────────┤
│ Round trip gouy phase │ [312.08135656 312.08135656] │
├───────────────────────┼─────────────────────────────────┤
│ Mode separation │ [4988.07218818 4988.07218818] │
├───────────────────────┼─────────────────────────────────┤
│ Resolution │ [41.60729519 41.60729519] │
├───────────────────────┼─────────────────────────────────┤
│ Stable │ True │
├───────────────────────┼─────────────────────────────────┤
│ Critically stable │ False │
└───────────────────────┴─────────────────────────────────┘
In this case this displays the parameters associated with our cavXARM
cavity.
For those parameters which have two values, e.g. the “waist size”, the first value represents the quantity in the tangential plane whilst the second value is for the sagittal plane.
Accessing the actual values, rather than displaying them, is as simple as “calling” the
relevant property of the Cavity
class. For example, if we want to get the
eigenmode of cavYARM
in the tangential plane and do something with the value (e.g.
access the wavefront RoC of this mode), we can do:
qx_yarm = model.cavYARM.qx
# Do something the the BeamParam qx_yarm, e.g. get the RoC of the wavefront
print(qx_yarm.Rc)
-1934.0
Note
To obtain any of the parameters of the cavity during a simulation, use the
CavityPropertyDetector
.
Current mode mismatch parameters (‘trace 4’)¶
Printing the current mode mismatch state (see BeamParam.mismatch()
for a
description of the figure-of-merit mismatch quantity used) of a model is simple:
print(model.mismatches_table())
┌──────────╥──────────────┬──────────────┐
│ Coupling ║ Mismatch (x) │ Mismatch (y) │
└──────────╨──────────────┴──────────────┘
Of course, this gives an empty table for this example currently as both arm cavities are equivalent in terms of geometric parameters and the interferometer is symmetric. For illustration, we can alter one of these cavities temporarily to show the resulting mode mismatch that then occurs at the central beam splitter (where the trace trees intersect):
# Temporarily change the RoC of ITMY using a context manager
with model.temporary_parameters():
model.YROC1.value = -2000
print(model.mismatches_table())
┌────────────────────╥──────────────┬──────────────┐
│ Coupling ║ Mismatch (x) │ Mismatch (y) │
╞════════════════════╬══════════════╪══════════════╡
│ BS.p1.i -> BS.p2.o ║ 0.0075 │ 0.0075 │
├────────────────────╫──────────────┼──────────────┤
│ BS.p2.i -> BS.p4.o ║ 0.0075 │ 0.0075 │
├────────────────────╫──────────────┼──────────────┤
│ BS.p2.i -> BS.p1.o ║ 0.0075 │ 0.0075 │
├────────────────────╫──────────────┼──────────────┤
│ BS.p4.i -> BS.p2.o ║ 0.0075 │ 0.0075 │
└────────────────────╨──────────────┴──────────────┘
If you want to access the actual mismatch values themselves, you can use
Model.detect_mismatches()
. For example, we can access just the mismatch for the
coupling BS.p1.i -> BS.p2.o
with:
with model.temporary_parameters():
model.spaces.LX.L = 4100
mismatches = model.detect_mismatches()
print(mismatches[(model.BS.p1.i, model.BS.p2.o)])
{'x': 0.04390880807812923, 'y': 0.04390880807812923}
As you can see, this returns a dictionary of the mismatch values for this node coupling in both the tangential (x) and sagittal (y) planes.
Beam parameters for every node (‘trace 8’)¶
Displaying the values for the beam parameters at each node can be done via printing the
BeamTraceSolution
object — which is returned when performing a
Model.beam_trace()
and is also stored in the Model.last_trace
attribute.
Again, using our example:
# Execute a beam trace using current model state
trace = model.beam_trace()
# Draw the trace solution in a forest format
print(trace)
Internal trace of cavity: cavXARM (q = -1837.22 + 421.68j, g = 0.8350925761717988)
o ITMX.p2.o: [q = -1837.22 + 421.68j]
╰──o ETMX.p1.i: [q = 2162.78 + 421.68j]
╰──o ETMX.p1.o: [q = -2162.78 + 421.68j]
╰──o ITMX.p2.i: [q = 1837.22 + 421.68j]
Internal trace of cavity: cavYARM (q = -1837.22 + 421.68j, g = 0.8350925761717988)
o ITMY.p2.o: [q = -1837.22 + 421.68j]
╰──o ETMY.p1.i: [q = 2162.78 + 421.68j]
╰──o ETMY.p1.o: [q = -2162.78 + 421.68j]
╰──o ITMY.p2.i: [q = 1837.22 + 421.68j]
Dependency: cavXARM
o ETMX.p1.i: [q = 2162.78 + 421.68j]
╰──o ETMX.p2.o: [q = 2162.78 + 421.68j]
Dependency: cavXARM
o ITMX.p2.i: [q = 1837.22 + 421.68j]
╰──o ITMX.p1.o: [q = 1837.22 + 421.68j]
╰──o BS.p3.i: [q = 1847.22 + 421.68j]
├──o BS.p4.o: [q = 1847.22 + 421.68j]
╰──o BS.p1.o: [q = 1847.22 + 421.68j]
╰──o L0.p1.i: [q = 1847.22 + 421.68j]
Dependency: cavYARM
o ETMY.p1.i: [q = 2162.78 + 421.68j]
╰──o ETMY.p2.o: [q = 2162.78 + 421.68j]
Dependency: cavYARM
o ITMY.p2.i: [q = 1837.22 + 421.68j]
╰──o ITMY.p1.o: [q = 1837.22 + 421.68j]
╰──o BS.p2.i: [q = 1847.22 + 421.68j]
The order in which these solution trees are displayed corresponds exactly to the order
in which the beam traces were performed on the model. The TraceDependency
object associated with each trace tree is printed above the tree.
The BeamTraceSolution
provides a dict-like interface for accessing beam
parameters, and so obtaining such values is simple. For example, if we want the beam
parameter at the laser output from our above trace
:
# Get the beam parameters in both planes
qx, qy = trace[model.L0.p1.o]
# Equivalent but alternative syntax:
qx = trace[model.L0.p1.o].qx
qy = trace[model.L0.p1.o].qy
# As both qx and qy are BeamParam instances we can then access any
# beam parameter property of these or just print them, e.g:
print(qx)
BeamParam(w0=11.951 mm, z=-1.8472 km, w=53.697 mm, Rc=-1.9435 km)
Access Gouy phases for all spaces (‘trace 16’)¶
One can print all the Gouy phases accumulated over each individual space, using the current state of the model, via:
print(model.space_gouys_table())
┌──────────────╥────────────────┬────────────────┐
│ Space ║ Gouy (x) [deg] │ Gouy (y) [deg] │
╞══════════════╬════════════════╪════════════════╡
│ L0_p1__BS_p1 ║ 0.0000 │ 0.0000 │
├──────────────╫────────────────┼────────────────┤
│ sy ║ 0.0676 │ 0.0676 │
├──────────────╫────────────────┼────────────────┤
│ LY ║ 156.0407 │ 156.0407 │
├──────────────╫────────────────┼────────────────┤
│ sx ║ 0.0676 │ 0.0676 │
├──────────────╫────────────────┼────────────────┤
│ LX ║ 156.0407 │ 156.0407 │
└──────────────╨────────────────┴────────────────┘
This shows each space in the model with the corresponding Gouy phases in both the
tangential (x) and sagittal (y) planes. The values are in degrees by default, but
can be switched to radians as shown by the parameter list of Model.space_gouys_table()
.
If you want to access the actual Gouy phase values themselves, you can use
Model.compute_space_gouys()
. For example, we can access just the Gouy phase
accumulated over the space sy
with:
gouys = model.compute_space_gouys()
print(gouys[model.spaces.sy])
{'x': 0.0676471163098868, 'y': 0.0676471163098868}
See also
For tracking the accumulated Gouy phase over an arbitrary length sequence of spaces
during a simulation, use the Gouy
detector.
And for outside of a simulation, you can use Model.acc_gouy()
to calculate
the Gouy phase accumulated over a given path. Another recommended method for doing
this (and much more at the same time) is Model.propagate_beam()
which returns
a PropagationSolution
containing methods and attributes for obtaining Gouy
phases accumulated over paths and sub-paths; along with many more useful features.
Coupling coefficients of components (‘trace 32’)¶
The simplest way to obtain the values of coupling coefficients, for couplings at any
component, is to add corresponding KnmDetector
instances to a model.
For example, let’s misalign ITMX in our interferometer by rotating it by 0.5% of the
cavity divergence angle in the yaw degree of freedom. Then we will add a
KnmDetector
for probing the scattering matrix on reflection from this mirror
at the inner-surface:
# Add a KnmDetector for K22 matrix of ITMX
model.parse("knmd K22_itmx ITMX 22")
# Temporarily misalign ITM.xbeta by 0.5% of the cavXARM divergence angle
with model.temporary_parameters():
model.ITMX.xbeta = model.cavXARM.qx.divergence * 0.5 / 100
# Run the model with no parameter sweep
out = model.run()
Our out
object now contains an entry "K22_itmx"
which consists of the mode
scattering matrix data for the specified coupling. We can wrap this up neatly using a
KnmMatrix
and print it, for example:
from finesse.knm.matrix import KnmMatrix
# The from_buffer method needs to know what higher-order modes were used so pass this as second arg
K22_itmx = KnmMatrix.from_buffer(out["K22_itmx"], model.modes())
print(K22_itmx)
K[0][0]: 0 0 -> 0 0 = (0.9990013731832287+0j)
K[1][0]: 0 0 -> 1 0 = (-2.976989016666019e-18+0.04465716662614314j)
K[2][0]: 0 0 -> 0 1 = 0j
K[3][0]: 0 0 -> 2 0 = (-0.0014115661870374835-4.2185087357482133e-19j)
K[4][0]: 0 0 -> 1 1 = (-0+0j)
K[5][0]: 0 0 -> 0 2 = 0j
K[0][1]: 1 0 -> 0 0 = (-1.0900798791148438e-17-0.04465716662614314j)
K[1][1]: 1 0 -> 1 0 = (-0.997005117137333+2.2137960734299366e-16j)
K[2][1]: 1 0 -> 0 1 = 0j
K[3][1]: 1 0 -> 2 0 = (1.7478612144409567e-17-0.06309167114051635j)
K[4][1]: 1 0 -> 1 1 = (-0+0j)
K[5][1]: 1 0 -> 0 2 = 0j
K[0][2]: 0 1 -> 0 0 = 0j
K[1][2]: 0 1 -> 1 0 = (-0+0j)
K[2][2]: 0 1 -> 0 1 = (0.9990013731832287-2.2182286522803378e-16j)
K[3][2]: 0 1 -> 2 0 = 0j
K[4][2]: 0 1 -> 1 1 = (2.6513310074939703e-17+0.044657166626143135j)
K[5][2]: 0 1 -> 0 2 = 0j
K[0][3]: 2 0 -> 0 0 = (-0.0014115661870374838+4.455108644135821e-19j)
K[1][3]: 2 0 -> 1 0 = (1.7215857375126575e-17+0.06309167114051635j)
K[2][3]: 2 0 -> 0 1 = 0j
K[3][3]: 2 0 -> 2 0 = (0.9950108556023098+2.2093679232833226e-16j)
K[4][3]: 2 0 -> 1 1 = (-0+0j)
K[5][3]: 2 0 -> 0 2 = 0j
K[0][4]: 1 1 -> 0 0 = 0j
K[1][4]: 1 1 -> 1 0 = (-0+0j)
K[2][4]: 1 1 -> 0 1 = (-1.0900798791148438e-17-0.04465716662614314j)
K[3][4]: 1 1 -> 2 0 = 0j
K[4][4]: 1 1 -> 1 1 = (-0.997005117137333+6.64138822028981e-16j)
K[5][4]: 1 1 -> 0 2 = 0j
K[0][5]: 0 2 -> 0 0 = 0j
K[1][5]: 0 2 -> 1 0 = (-0+0j)
K[2][5]: 0 2 -> 0 1 = 0j
K[3][5]: 0 2 -> 2 0 = 0j
K[4][5]: 0 2 -> 1 1 = (-0+0j)
K[5][5]: 0 2 -> 0 2 = (0.9990013731832289+2.2182286522803383e-16j)
The displayed output here gives the coupling coefficients for the modes n m -> n' m'
as shown.
Mode mismatches during a simulation (‘trace 64’)¶
Obtaining mode mismatch values during a simulation can be achieved via the use of
ModeMismatchDetector
objects. Using our interferometer above, we can, for
example, scan over the RoC of ITMX during a simulation and detect the corresponding mode
mismatch which occurs from port 2 to port 4 of the beam splitter (due to the
intersecting trace trees from cavXARM
and cavYARM
at this component):
model.parse("""
# This will detect mode mismatch from BS.p2.i -> BS.p4.o
mmd mm_bs_x BS.p2.i BS.p4.o direction=x
mmd mm_bs_y BS.p2.i BS.p4.o direction=y
# Sweep the RoC of the ITM in the x-arm
xaxis(XROC1, lin, -1900, -2000, 100)
""")
out = model.run()
out.plot("mm_bs_x", "mm_bs_y");
The resulting plot shows the requested mode mismatch quantity as a function of the swept
parameter. One can specify the plane to detect in via the direction
argument of this
detector, as described in ModeMismatchDetector
. The mismatch quantities are the
same in both planes in this case as scanning XROC1
changes both the tangential
and sagittal planes of the RoC of the ITM in the X-arm.
Nodes found during the beam tracing (‘trace 128’)¶
This option was already essentially covered by the beam trace solution printing section
earlier, however one can also access the underlying TraceForest
structure of a
model itself via the Model.trace_forest
attribute. You can print this to get a
representation of how the beam traces are structured in the current model state:
print(model.trace_forest)
Dependency: cavXARM [Cavity]
o ITMX.p2.o
╰──o ETMX.p1.i
╰──o ETMX.p1.o
╰──o ITMX.p2.i
Dependency: cavYARM [Cavity]
o ITMY.p2.o
╰──o ETMY.p1.i
╰──o ETMY.p1.o
╰──o ITMY.p2.i
Dependency: cavXARM [Cavity]
o ETMX.p1.i
╰──o ETMX.p2.o
o ITMX.p2.i
╰──o ITMX.p1.o
╰──o BS.p3.i
├──o BS.p4.o
╰──o BS.p1.o
╰──o L0.p1.i
Dependency: cavYARM [Cavity]
o ETMY.p1.i
╰──o ETMY.p2.o
o ITMY.p2.i
╰──o ITMY.p1.o
╰──o BS.p2.i
This gives all of the trace trees (with the associated optical nodes) in the order in which beam traces will traverse them during the beam tracing algorithms.
One can easily change the priority of TraceDependency
instances of the model
and see how this is reflected in the new forest structure. For example, here we set
cavYARM
as the highest priority now such that this will be traced before
cavXARM
:
# Gives cavYARM the highest priority (one can also set
# priority manually via model.cavYARM.priority = value)
model.cavYARM.take_priority()
# We can check the tracing order via this method call
print("Trace order = ", model.trace_order_names)
# Perform a new beam trace so that trace_forest gets re-planted
model.beam_trace()
print(model.trace_forest)
Trace order = ['cavYARM', 'cavXARM']
Dependency: cavYARM [Cavity]
o ITMY.p2.o
╰──o ETMY.p1.i
╰──o ETMY.p1.o
╰──o ITMY.p2.i
Dependency: cavXARM [Cavity]
o ITMX.p2.o
╰──o ETMX.p1.i
╰──o ETMX.p1.o
╰──o ITMX.p2.i
Dependency: cavYARM [Cavity]
o ETMY.p1.i
╰──o ETMY.p2.o
o ITMY.p2.i
╰──o ITMY.p1.o
╰──o BS.p2.i
├──o BS.p1.o
│ ╰──o L0.p1.i
╰──o BS.p4.o
Dependency: cavXARM [Cavity]
o ETMX.p1.i
╰──o ETMX.p2.o
o ITMX.p2.i
╰──o ITMX.p1.o
╰──o BS.p3.i
And now we can see that the ordering has changed as expected.
Todo
Link to a separate page covering trace ordering in more details once written.