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' @ 0x7ae255465bb0 (Laser)>
Adding a component automatically creates an attribute on the
finesse.model.Model
object with the name of the component.
model.l1
<'l1' @ 0x7ae255465bb0 (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' @ 0x7ae254fe31a0 (Laser)>,
<'l1' @ 0x7ae254fe9670 (Laser)>,
<'l2' @ 0x7ae254feaa80 (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 @ 0x7ae254ff04d0> for <'l1' @ 0x7ae254ff0680 (Laser)>
Selecting port <Port m1.p1 Type=NodeType.OPTICAL @ 0x7ae254fecb30> for <'m1' @ 0x7ae254ff0f80 (Mirror)>
Selecting port <Port m1.p2 Type=NodeType.OPTICAL @ 0x7ae254ff11c0> for <'m1' @ 0x7ae254ff0f80 (Mirror)>
Selecting port <Port m2.p1 Type=NodeType.OPTICAL @ 0x7ae254ff19d0> for <'m2' @ 0x7ae254ff0470 (Mirror)>
Using the link method¶
The finesse.model.Model.link()
method calls the
finesse.model.Model.connect()
method between each pair of components provided.
Connector names will be autogenerated, but it is possible to specify the length/time
delay of the connector by adding integers to the argument list. This is useful
when you have a long list of components to connect quickly.
m = Model()
l1 = Laser("l1")
m1 = Mirror("m1", R=0.1, T=0.9)
m2 = Mirror("m2", R=0.1, T=0.9)
# Connect with distances l1 - 10m -> m1 - 20m -> m2
m.link(l1, 10, m1, 20, m2, verbose=True)
Connecting l1 to m1
Selecting port <Port l1.p1 Type=NodeType.OPTICAL @ 0x7ae254ff5730> for <'l1' @ 0x7ae254ff5ac0 (Laser)>
Selecting port <Port m1.p1 Type=NodeType.OPTICAL @ 0x7ae254ff67b0> for <'m1' @ 0x7ae254ff67e0 (Mirror)>
Connecting m1 to m2
Selecting port <Port m1.p2 Type=NodeType.OPTICAL @ 0x7ae254ff6ab0> for <'m1' @ 0x7ae254ff67e0 (Mirror)>
Selecting port <Port m2.p1 Type=NodeType.OPTICAL @ 0x7ae254ff6f30> for <'m2' @ 0x7ae254ff6f60 (Mirror)>
You can also be more verbose with the ports linked m.link(l1.p1, 10,
m1.p2, m2.p1, 20, m2.p2, verbose=True)
. Note that when connecting through a
component you need to specify which port to in and which to go out, as seen with
m1
above.
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' @ 0x7ae26a81e6c0 (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)
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)
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)
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)