Basic usage of ports and nodes

All components in Finesse 3 that can be connected together have various ports associated with them, which in turn contain Node objects representing some optical, or small signal (electrical, or mechanical) state. A port can only contain one type of node.

Each node has a directionality associated with it for the purposes of builing a model and desrcibing the flow of information: input, output, or bidirectional. Input nodes can only be connected to another output node, whereas bidirectional do not care. Every optical port has an input and output node, usually called i and o. Electrical ports usually just have a single input or output node. Mechanical ports are bidirectional nodes.

Ports and nodes have a standard naming and access convention: component.port.node.

Optical Ports are nearly always called something like p1, p2, …, pN. Each optical port always has one input node i and one output node o. These describe the optical field going into and out of the component. Optical ports between two different elements can only be connected to other optical ports via a Space element.

Electrical and Mechanical Ports contain Signal Nodes. These nodes represent electronic, mechanical, or other small linearised states in the model. Signal nodes can all be connected together via Wires which allow you to form feedback loops. For example, you can use a Readout to convert some optical signal into an electronic one, which is then filtered, amplified, and fed back to some mechanical state. These signal states are all small linearised oscillatory states, they do not represent a static effect in the model. These signal nodes can then be used to inject in signals and then measure the corresponding response at another signal node.

Accessing ports

Ports of a component can be accessed via the component through the names that they were assigned during construction. For example, a Mirror has two ports (p1 and p2) which can be grabbed directly:

from finesse.components import Mirror

M1 = Mirror("M1")
print(M1.p1)
print(M1.p2)
<Port M1.p1 Type=NodeType.OPTICAL @ 0x7fd8702cae50>
<Port M1.p2 Type=NodeType.OPTICAL @ 0x7fd8702caa30>

You can also get a read-only tuple of all the ports available at an element using:

print(M1.ports)
(<Port M1.p1 Type=NodeType.OPTICAL @ 0x7fd8702cae50>, <Port M1.p2 Type=NodeType.OPTICAL @ 0x7fd8702caa30>, <Port M1.mech Type=NodeType.MECHANICAL @ 0x7fd8702cad90>)

or all of the optical nodes:

print(M1.optical_nodes)
(<OpticalNode M1.p1.i @ 0x7fd8702cae80>, <OpticalNode M1.p1.o @ 0x7fd8702ca970>, <OpticalNode M1.p2.i @ 0x7fd8702cadf0>, <OpticalNode M1.p2.o @ 0x7fd8702cae20>)

or all of the signal nodes:

print(M1.signal_nodes)
(<SignalNode M1.mech.z @ 0x7fd8702cad00>, <SignalNode M1.mech.yaw @ 0x7fd839d9e5b0>, <SignalNode M1.mech.pitch @ 0x7fd839d9e550>, <SignalNode M1.mech.F_z @ 0x7fd839d9e580>, <SignalNode M1.mech.F_yaw @ 0x7fd8336d10d0>, <SignalNode M1.mech.F_pitch @ 0x7fd8336d1040>)

What does a port contain?

In the example code below we show the main properties of a Port - namely its type, the nodes that it holds and the component that it is attached to (which is always a Space between different optical connections):

from finesse import Model
from finesse.components import Mirror, Space

M1 = Mirror("M1")
M2 = Mirror("M2")

model = Model()
# connect M1 <-> M2 in a model via a Space of length 1m
model.chain(M1, Space("M1_M2", L=10), M2)

print(f"Port M1.p2 name = {M1.p2.name}")
print(f"Port M1.p2 type = {M1.p2.type}")
print(f"Port M1.p2 owning component = {M1.p2.component}")
print(f"Port M1.p2 attached component = {M1.p2.attached_to}")
print(f"Port M1.p2 nodes = {M1.p2.nodes}")
Port M1.p2 name = p2
Port M1.p2 type = NodeType.OPTICAL
Port M1.p2 owning component = Mirror('M1', misaligned=False, ybeta=0.0, xbeta=0.0, Rcy=inf, Rcx=inf, phi=0.0, L=0.0, T=0.5, R=0.5)
Port M1.p2 attached component = Space('M1_M2', user_gouy_y=None, user_gouy_x=None, nr=1.0, L=10.0)
Port M1.p2 nodes = (<OpticalNode M1.p2.i @ 0x7f891c027700>, <OpticalNode M1.p2.o @ 0x7f891c0276d0>)

Accessing nodes — via a component

Nodes can be accessed directly through components or via the Model instance that their owning component is associated with (see next section for details). To access all the nodes of a component:

print(f"All nodes of M1 = {M1.nodes}")
All nodes of M1 = OrderedDict([('M1.p1.i', <OpticalNode M1.p1.i @ 0x7f88e5b70160>), ('M1.p1.o', <OpticalNode M1.p1.o @ 0x7f88e5b70190>), ('M1.p2.i', <OpticalNode M1.p2.i @ 0x7f891c027700>), ('M1.p2.o', <OpticalNode M1.p2.o @ 0x7f891c0276d0>), ('M1.mech.z', <SignalNode M1.mech.z @ 0x7f88df48cfd0>), ('M1.mech.yaw', <SignalNode M1.mech.yaw @ 0x7f88df48cdf0>), ('M1.mech.pitch', <SignalNode M1.mech.pitch @ 0x7f88df48ce50>), ('M1.mech.F_z', <SignalNode M1.mech.F_z @ 0x7f88df48cc70>), ('M1.mech.F_yaw', <SignalNode M1.mech.F_yaw @ 0x7f88df48cd60>), ('M1.mech.F_pitch', <SignalNode M1.mech.F_pitch @ 0x7f88df53dc70>)])

Or all the optical nodes:

print(f"Optical nodes of M1 = {M1.optical_nodes}")
Optical nodes of M1 = (<OpticalNode M1.p1.i @ 0x7f88e5b70160>, <OpticalNode M1.p1.o @ 0x7f88e5b70190>, <OpticalNode M1.p2.i @ 0x7f891c027700>, <OpticalNode M1.p2.o @ 0x7f891c0276d0>)

Get a single optical node of a component by its direction:

print(f"Input node of port M1.n1 = {M1.p1.i}")
print(f"Output node of port M1.n1 = {M1.p1.o}")
Input node of port M1.n1 = <OpticalNode M1.p1.i @ 0x7f88e5b70160>
Output node of port M1.n1 = <OpticalNode M1.p1.o @ 0x7f88e5b70190>

Accessing nodes — via the model

Nodes play an important role in the Model class as the Node.full_name property forms the node_type of the underlying directed graph object (stored in Model.network). Thus, we use Node instances to report on graph data as well as perform operations on this graph - see the networkx DiGraph documentation for details on these methods and attributes.

You can also access all the nodes of a given NodeType in a Model instance with (e.g. for optical nodes):

print(f"All optical nodes in model: {model.optical_nodes}")
All optical nodes in model: [<OpticalNode M1.p1.i @ 0x7f88e5b70160>, <OpticalNode M1.p1.o @ 0x7f88e5b70190>, <OpticalNode M1.p2.i @ 0x7f891c027700>, <OpticalNode M1.p2.o @ 0x7f891c0276d0>, <OpticalNode M2.p1.i @ 0x7f891c027670>, <OpticalNode M2.p1.o @ 0x7f88df53dc40>, <OpticalNode M2.p2.i @ 0x7f88df49a160>, <OpticalNode M2.p2.o @ 0x7f88df264070>]

Node names are used as keys in the network of a Model to get data on the node itself and edges connected to the node:

print(model.network.nodes[model.M2.p1.i.full_name])
print(model.network[model.M2.p1.i.full_name][model.M2.p1.o.full_name])
{'weakref': <weakref at 0x7f88df2804a0; to 'OpticalNode' at 0x7f891c027670>, 'owner': <weakref at 0x7f88df269f40; to 'Mirror' at 0x7f891c027640>, 'optical': True}
{'name': 'P1i_P1o', 'in_ref': <weakref at 0x7f88df2804a0; to 'OpticalNode' at 0x7f891c027670>, 'out_ref': <weakref at 0x7f88df280900; to 'OpticalNode' at 0x7f88df53dc40>, 'owner': <weakref at 0x7f88df269f40; to 'Mirror' at 0x7f891c027640>, 'length': 1, 'coupling_type': <CouplingType.OPTICAL_TO_OPTICAL: 0>, 'internal': False}