Models and Components

Using the python interface to construct your Finesse model can be more verbose than using KatScript, but gives you access to syntax highlighting and autocompletion, especially when using a modern IDE like VSCode or PyCharm. It also offers the most feature rich API to interact with Finesse models. All features are supported in Python API, whereas only a subset are support in KatScript.

Note

If you want to know how to map a KatScript command to a Python method or class you can inspect the finesse.script.spec.KatSpec._register_constructs() method in script/spec.py which sets up the various links between KatScript and Python API.

Adding components to a model

All the KatScript components, have a Python class associated in either the finesse.components or the finesse.detectors module. Documentation can also be accessed using the python help() method

from finesse import Model
from finesse.components import Laser, Mirror, Space, Beamsplitter
from finesse.detectors import PowerDetector

help(Laser)
Help on class Laser in module finesse.components.laser:

class Laser(finesse.components.general.Connector, finesse.components.general.FrequencyGenerator)
 |  Laser(name, P=1, f=0, phase=0, signals_only=False)
 |
 |  Represents a laser producing a beam with associated properties such as power and
 |  frequency.
 |
 |  Parameters
 |  ----------
 |  name : str
 |      Name of the newly created laser.
 |
 |  P : float, optional
 |      Power of the laser (in Watts), defaults to 1 W.
 |
 |  f : float or :class:`.Frequency`, optional
 |      Frequency-offset of the laser from the default (in Hz) or :class:`.Frequency`
 |      object. Defaults to 0 Hz offset.
 |
 |  phase : float, optional
 |      Phase-offset of the laser from the default, defaults to zero.
 |
 |  signals_only : bool, optional
 |      When True, this laser component will only inject signal sidebands. They will use
 |      the current carrier value as a scaling terms but the carrier will not be
 |      injected into the simulation. This allows a user to just inject signal sidebands
 |      into a model.
 |
 |  Attributes
 |  ----------
 |  add_gouy_phase : bool
 |      When set to True the gouy phase of the current beam parameters values at the
 |      laser will be added to the optical field outputs during the simulation. When
 |      False, it will not. This can be used with :meth:`.set_output_field` to force a
 |      particular optical field output from a laser.
 |
 |  Method resolution order:
 |      Laser
 |      finesse.components.general.Connector
 |      finesse.element.ModelElement
 |      finesse.components.general.FrequencyGenerator
 |      builtins.object
 |
 |  Methods defined here:
 |
 |  __init__(self, name, P=1, f=0, phase=0, signals_only=False)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  get_output_field(self, homs)
 |      Get optical field outputted as a HOM vector.
 |
 |      Returns the complex amplitude of the modes specified in homs. This does not
 |      respect the `add_gouy_phase`` attribute of the laser element and will always
 |      return the complex amplitude without the gouy phase. If the gouy phase is
 |      required then it is recommended to use a FieldDetector at the laser output with
 |      ``add_gouy_phase`` set to ``True``.
 |
 |      Parameters
 |      ----------
 |      homs : sequence
 |          Collection of (n, m) higher order modes to retrieve the output field for.
 |          Typically this is just the ``model.homs`` value. The output ``E`` vector
 |          will match the ordering of ``homs``.
 |
 |      Returns
 |      -------
 |      sequence
 |          The output fields for `homs`. If a given (n, m) has no defined coefficients,
 |          its field defaults to 0.
 |
 |  optical_equations(self)
 |
 |  set_output_field(self, E, homs)
 |      Set optical field outputted using HOM vector.
 |
 |      This changes the output power and mode content of the laser to match the
 |      requested ``E`` field.
 |
 |      Parameters
 |      ----------
 |      E : sequence
 |          The complex optical field amplitude for `homs`.
 |      homs : sequence
 |          Sequence of (n, m) higher order modes. Typically this is just the
 |          ``model.homs`` value. It should match the size of ``E``.
 |
 |      Notes
 |      -----
 |      If you do not want the gouy phase due to the current beam parameter values set
 |      at the laser to be added to the output, set the ``add_gouy_phase`` attribute of
 |      the laser element to ``False``.
 |
 |  source_equation(self, node, f)
 |      Returns optical carrier field to inject for a simulation.
 |
 |  tem(self, n, m, factor, phase=0.0)
 |      Distributes power into the mode HGnm.
 |
 |      Parameters
 |      ----------
 |      n, m : int
 |          Mode indices.
 |
 |      factor : float
 |          Relative power factor, modes with equal `factor` will
 |          have equivalent power distributed to them.
 |
 |      phase : float, optional; default = 0.0
 |          Phase offset for the field, in degrees.
 |
 |      Notes
 |      -----
 |      This does not change the total power of the laser, rather, it redistributes this
 |      power into / out of the specified mode.
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties defined here:
 |
 |  power_coeffs
 |      The relative power factors and phase offsets for each HGnm mode.
 |
 |      :`getter`: Returns the mode factors and phase offsets as a dict with the mode
 |                 indices as keys. Read-only.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  P
 |      P : float, optional
 |      Power of the laser (in Watts), defaults to 1 W.
 |
 |  f
 |      f : float or :class:`.Frequency`, optional
 |      Frequency-offset of the laser from the default (in Hz) or :class:`.Frequency`
 |      object. Defaults to 0 Hz offset.
 |
 |  phase
 |      phase : float, optional
 |      Phase-offset of the laser from the default, defaults to zero.
 |
 |  signals_only
 |      signals_only : bool, optional
 |      When True, this laser component will only inject signal sidebands. They will use
 |      the current carrier value as a scaling terms but the carrier will not be
 |      injected into the simulation. This allows a user to just inject signal sidebands
 |      into a model.
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  DEFAULT_POWER_COEFFS = {(0, 0): (1.0, 0.0)}
 |
 |  ----------------------------------------------------------------------
 |  Methods inherited from finesse.components.general.Connector:
 |
 |  ABCD(self, from_node, to_node, direction='x', symbolic=False, copy=True, retboth=False, allow_reverse=False)
 |      Parameters
 |      ----------
 |      from_node : :class:`.OpticalNode` or :class:`.Port` or str or int
 |          Input node. If a port, or string repr of a port, is given then
 |          the *input* optical node of that port will be used.
 |
 |      to_node : :class:`.OpticalNode` or :class:`.Port` or str or int
 |          Output node. If a port, or string repr of a port, is given then
 |          the *output* optical node of that port will be used.
 |
 |      direction : str, optional; default: 'x'
 |          Direction of ABCD matrix computation, default is 'x' for tangential plane.
 |
 |      symbolic : bool, optional; default: False
 |          Whether to return the symbolic matrix (as given by equations above). Defaults
 |          to False such that the numeric matrix is returned.
 |
 |      copy : bool, optional; default: True
 |          Whether to return a copy of ABCD matrix (or matrices if `retboth` is true). Defaults
 |          to True so that the internal matrix cannot be accidentally altered. Use caution
 |          if switching this flag off.
 |
 |      retboth : bool, optional; default: False
 |          Whether to return both the symbolic and numeric matrices as a tuple
 |          in that order.
 |
 |      allow_reverse : bool, optional
 |          When True, if the coupling does not exist at the component from_node->to_node
 |          but to_node->from_node does exist, it will return the ABCD from that.
 |          Otherwise a NoCouplingError will be raised.
 |
 |      Returns
 |      -------
 |      M : :class:`numpy.ndarray`
 |          The ABCD matrix of the specified coupling for the mirror. This is symbolic
 |          if either of `symbolic` or `retboth` flags are True.
 |
 |      M2 : :class:`numpy.ndarray`
 |          Only returned if `retboth` is True, otherwise just `M` above is returned. This
 |          will always be the numeric matrix.
 |
 |      Raises
 |      ------
 |      err : :class:`ValueError`
 |          If no coupling exists between `from_node` and `to_node`.
 |
 |  check_coupling(self, from_node, to_node)
 |      Checks that a coupling exists between `from_node` -> `to_node` and raises a
 |      ``ValueError`` if not.
 |
 |  coupling_type(self, from_node, to_node)
 |      Obtains the type of coupling (see :class:`.CouplingType`) between the two
 |      specified nodes at this component.
 |
 |      Parameters
 |      ----------
 |      from_node : :class:`.Node`
 |          Node which has a forwards coupling to `to_node`.
 |
 |      to_node : :class:`.Node`
 |          Node which has a backwards coupling from `from_node`.
 |
 |      Returns
 |      -------
 |      coupling_t : :class:`.CouplingType`
 |          The type of coupling between the specified nodes.
 |
 |  interaction_type(self, from_node, to_node)
 |      Obtains the type of interaction (see :class:`.InteractionType`) between the
 |      two specified nodes at this component.
 |
 |      Parameters
 |      ----------
 |      from_node : :class:`.Node`
 |          Node which has a forwards coupling to `to_node`.
 |
 |      to_node : :class:`.Node`
 |          Node which has a backwards coupling from `from_node`.
 |
 |      Returns
 |      -------
 |      interaction_t : :class:`.InteractionType`
 |          The type of interaction between the specified nodes.
 |
 |  is_valid_coupling(self, from_node, to_node)
 |      Flags whether the provided node coupling exists at this connector.
 |
 |  register_abcd_matrix(self, M_sym, *couplings)
 |      Register an ABCD matrix of the given symbolic form for a sequence of
 |      coupling(s).
 |
 |      Specifying several couplings for one `M_sym` means that all these
 |      node couplings will point to the same reference ABCDs --- i.e. the
 |      matrices kept in the underlying ABCD matrix store will be the same
 |      blocks of memory.
 |
 |      .. warning::
 |
 |          This should only be used in the ``_resymbolise_ABCDs`` method of Connectors,
 |          when implementing a new component.
 |
 |      Parameters
 |      ----------
 |      M_sym : :class:`numpy.ndarray`
 |          A 2x2 matrix of symbolic elements describing the analytic form of
 |          the ABCD matrix for the given coupling(s).
 |
 |      couplings : sequence of tuples
 |          Arguments of tuples giving the node couplings which are described by
 |          the given symbolic ABCD matrix `M_sym`.
 |
 |          These tuples can be of size two or three, with the first two elements
 |          always as the `from_node` -> `to_node` instances. The former case implies that
 |          both the tangential and sagittal plane ABCD matrix couplings are equal
 |          and so both directions 'x' and 'y' in the underlying matrices store will
 |          be set to the same values. Whilst the latter case, where the third element is
 |          either 'x' or 'y', sets just these direction keys to this matrix.
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from finesse.components.general.Connector:
 |
 |  all_internal_connections
 |      A dictionary of all the connections this element is making between its
 |      nodes.
 |
 |  all_internal_optical_connections
 |      A dictionary of all the optical connections this element is making between
 |      its nodes.
 |
 |  borrows_nodes
 |      Whether this element borrows node references from another.
 |
 |      When this is True the element may not create all of its own nodes and just link
 |      into one that already exists and is owned by another element.
 |
 |  nodes
 |      All the nodes of all the ports at this component. Order is likely to be the
 |      order in which the ports and nodes were created, but this is not guaranteed.
 |
 |      Returns
 |      -------
 |      nodes : tuple(:class:`.Node`)
 |          Copy of nodes dictionary
 |
 |  optical_nodes
 |      The optical nodes stored by the connector.
 |
 |      :`getter`: Returns a list of the stored optical nodes (read-only).
 |
 |  ports
 |      Retrieves the ports available at the object.
 |
 |      Returns
 |      -------
 |      tuple
 |          Read-only tuple of the ports available at this object.
 |
 |  signal_nodes
 |      The signal nodes stored by the connector.
 |
 |      :`getter`: Returns a list of the stored signal nodes (read-only).
 |
 |  ----------------------------------------------------------------------
 |  Methods inherited from finesse.element.ModelElement:
 |
 |  __deepcopy__(self, memo)
 |
 |  __repr__(self)
 |      Return repr(self).
 |
 |  __setattr__ = frozensetattr(self, name, value) from finesse.freeze.canFreeze.<locals>
 |
 |  __str__(self)
 |      Return str(self).
 |
 |  info(self, eval_refs=False)
 |      Element information.
 |
 |      Parameters
 |      ----------
 |      eval_refs : bool
 |          Whether to evaluate symbolic references to their numerical values.
 |          Defaults to False.
 |
 |
 |      Returns
 |      -------
 |      str
 |          The formatted info.
 |
 |  info_parameter_table(self)
 |      Info parameter table.
 |
 |      This provides a table with useful fields in addition to those contained in
 |      :meth:`.parameter_table`.
 |
 |      Parameters
 |      ----------
 |
 |      Returns
 |      -------
 |      str
 |          The formatted extra info table.
 |      None
 |          If there are no info parameters.
 |
 |  parameter_table(self, eval_refs=False, return_str=False)
 |      Model parameter table.
 |
 |      Parameters
 |      ----------
 |      eval_refs : bool
 |          Whether to evaluate symbolic references to their numerical values. Does not
 |          have effect when `return_str` is False. Defaults to False.
 |      return_str : bool
 |          Return str representation instead of :class:`finesse.utilities.Table`.
 |          Necessary when setting `eval_refs` to True. Defaults to False.
 |
 |
 |      Returns
 |      -------
 |      :class:`finesse.utilities.Table`
 |          The formatted parameter info table.
 |      str
 |          String representation of the table, if 'return_str' is True
 |      None
 |          If there are no parameters.
 |
 |  ----------------------------------------------------------------------
 |  Static methods inherited from finesse.element.ModelElement:
 |
 |  __new__(cls, name, *args, **kwargs)
 |      Create and return a new object.  See help(type) for accurate signature.
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties inherited from finesse.element.ModelElement:
 |
 |  default_parameter_name
 |      The default parameter to assume when the component is directly referenced.
 |
 |      This is used for example in kat script when the component is directly referenced in an
 |      expression, instead of the model parameter, e.g. &l1 instead of &l1.P.
 |
 |      Returns
 |      -------
 |      str
 |          The name of the default model parameter.
 |
 |      None
 |          If there is no default.
 |
 |  has_model
 |      Returns true if this element has been associated with a Model.
 |
 |  name
 |      Name of the element.
 |
 |      Returns
 |      -------
 |      str
 |          The name of the element.
 |
 |  parameters
 |      Returns a list of the parameters available for this element.
 |
 |  ref
 |      Reference to the default model parameter, if set.
 |
 |      Returns
 |      -------
 |      :class:`.ParameterRef`
 |          Reference to the default model parameter, if set.
 |
 |      Raises
 |      ------
 |      ValueError
 |          If there is no default model parameter set for this element.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from finesse.element.ModelElement:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object

Adding components is done using finesse.model.Model.add(), which returns the component added.

model = Model()
l1 = model.add(Laser("l1", P=10))
l1
<'l1' @ 0x7bc213a983b0 (Laser)>

Adding a component automatically creates an attribute on the finesse.model.Model object with the name of the component.

model.l1
<'l1' @ 0x7bc213a983b0 (Laser)>

Note

When writing a python script, your IDE might warn you that these attributes don”t exist and not provide autocompletion. When working in an interactive/Jupyter context, these attributes will be available for autocompletion after the add method has been executed.

Component names need to be unique:

model = Model()
model.add(Laser("l1", P=10))
model.add(Laser("l1", P=10))
FinesseException: 	(use finesse.tb() to see the full traceback)
An element with the name l1 is already present in the model (Laser('l1', signals_only=False, f=0.0, phase=0.0, P=10.0))

The finesse.model.Model.add() function also accepts an iterable of components:

model = Model()
l1, l2, l3 = model.add([Laser(f"l{i}", P=i) for i in range(3)])
model.components
(<'l0' @ 0x7bc21378e840 (Laser)>,
 <'l1' @ 0x7bc2136228a0 (Laser)>,
 <'l2' @ 0x7bc213622fc0 (Laser)>)

Connecting components

For any useful simulation, components have to be connected together. Many spaces must be made in more complicated models and often you do not need to give them a name. Therefore the python interface provides multiple ways of doing so depending on your use case.

Specifying ports on Space components

Directly adding a Space element is the most low-level option. We first create and add the components for a simple cavity:

m = Model()

l1, m1, m2 = m.add(
    [Laser("l1"),
     Mirror("m1", R=0.1, T=0.9),
     Mirror("m2", R=0.1, T=0.9)
    ]
)

And then we create finesse.components.space.Space components, connecting their ports to the ports of existing components.

s1 = m.add(Space("s1", portA=l1.p1, portB=m1.p1))
s2 = m.add(Space("s2", portA=m1.p2, portB=m2.p1))

Using the connect method

The finesse.model.Model.connect() connects the provided components or ports together and returns the created space/wire component. It tries to connect in a “smart” way, so it can be useful to use the verbose argument to see what is happening.

m = Model()

l1 = Laser("l1")
m1 = Mirror("m1", R=0.1, T=0.9)
m2 = Mirror("m2", R=0.1, T=0.9)

m.add([l1, m1, m2])

s1 = m.connect(l1, m1, L=1, name="s1", verbose=True)
s2 = m.connect(m1, m2, L=1, name="s2", verbose=True)
Selecting port <Port l1.p1 Type=NodeType.OPTICAL @ 0x7bc213629730> for <'l1' @ 0x7bc21362a690 (Laser)>
Selecting port <Port m1.p1 Type=NodeType.OPTICAL @ 0x7bc21362b260> for <'m1' @ 0x7bc21362b290 (Mirror)>
Selecting port <Port m1.p2 Type=NodeType.OPTICAL @ 0x7bc21362b530> for <'m1' @ 0x7bc21362b290 (Mirror)>
Selecting port <Port m2.p1 Type=NodeType.OPTICAL @ 0x7bc21362b950> for <'m2' @ 0x7bc21362af00 (Mirror)>

Using the chain method

This method is documented for completeness but maybe removed later as link and connect above are used far more frequently.

The finesse.model.Model.chain() method is similar to the finesse.model.Model.link() method. One difference is it accepts dictionaries that specify space components, allowing for named connectors.

m = Model()
l1, m1, m2 = m.chain(
    Laser("l1"), Mirror("m1"), {"name": "s1", "L": 1}, Mirror("m2")
)
print(m.s1)
Space('s1', user_gouy_y=None, user_gouy_x=None, nr=1.0, L=1.0)

Visualizing the model

After creating and adding your components, it can be helpful to visualize your model to ensure your components are connected like you would expect. For this example we will create a simple interferometer.

m = Model()

l1 = Laser("l1")
bs = Beamsplitter("bs", R=0.5, T=0.5)
m.link(l1, bs)

ETMx = Mirror("ETMx", R=1, T=0)
m.link(bs.p3, ETMx)

ETMy = Mirror("ETMy", R=1, T=0)
m.link(bs.p2, ETMy)

signal = PowerDetector("signal", m.bs.p4.o)
m.add(signal)
<'signal' @ 0x7bc2138e33e0 (PowerDetector)>

We can use finesse.model.Model.component_tree() to draw a tree of the model using ASCI characters.

print(m.component_tree())
○ l1
╰──○ bs
   ├──○ ETMy
   ╰──○ ETMx

For larger models, it can be clearer to only display a subsection of the model, by using the root and radius arguments to select a subset of the graph.

# Only display components that are connect directly to the end mirror
print(m.component_tree("ETMx", radius=1))
○ ETMx
╰──○ bs

We can exchange some clarity with verbosity by showing the ports components are connected to by using the show_ports argument.

print(m.component_tree(show_ports=True))
○ l1
╰──○ bs (l1.p1 ↔ bs.p1)
   ├──○ ETMy (bs.p2 ↔ ETMy.p1)
   ╰──○ ETMx (bs.p3 ↔ ETMx.p1)

We can also include the detectors in the component tree, although they are strictly speaking not components since they are not part of the optical network.

print(m.component_tree(show_detectors=True))
○ l1
╰──○ bs
   ├──○ ETMy
   ├──○ ETMx
   ╰──○ signal

Finally we can also use component_tree to visualize other networks besides the component network, using the network_type argument:

from finesse.utilities.network_filter import NetworkType

# note you can also use 'network_type="optical"'
print(m.component_tree(m.l1.p1.o, network_type=NetworkType.OPTICAL))
○ l1.p1.o
╰──○ bs.p1.i
   ├──○ bs.p2.o
   │  ╰──○ ETMy.p1.i
   │     ├──○ ETMy.p1.o
   │     │  ╰──○ bs.p2.i
   │     │     ├──○ bs.p1.o
   │     │     │  ╰──○ l1.p1.i
   │     │     ╰──○ bs.p4.o
   │     ╰──○ ETMy.p2.o
   ╰──○ bs.p3.o
      ╰──○ ETMx.p1.i
         ├──○ ETMx.p1.o
         │  ╰──○ bs.p3.i
         ╰──○ ETMx.p2.o

You can also directly visualize the The model graph by using networkx via finesse.model.Model.plot_graph().

m.plot_graph(graphviz=False)
../../_images/models_and_components_16_1.png
<Figure size 640x480 with 0 Axes>
<Figure size 640x480 with 0 Axes>

To see more information about how the optical ports are connected, you can specify the network type optical.

m.plot_graph(network_type="optical", graphviz=False)
../../_images/models_and_components_17_0.png
<Figure size 640x480 with 0 Axes>
<Figure size 640x480 with 0 Axes>

To get a prettier view, you can get use the optional pygraphviz dependency (which you should have if you used conda, otherwise see here)

m.plot_graph(network_type="optical", graphviz=True)
../../_images/models_and_components_18_1.svg

Note that the graphviz argument controls whether graphviz will be used. If not available, it will automatically revert to networkx and emit a warning.

plot_graph also supports the radius and root arguments, which should be used in tandem.

m.plot_graph(network_type="optical", graphviz=True, root="bs.p2.o", radius=2)
../../_images/models_and_components_19_0.svg