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, electrical, or mechanical state. Each state has a directionality associated with it: 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 have bidirectional nodes, but you cannot directly connect multiple mechanical components together as of Finesse 3 alpha. Additional mechanical features may be added in a later version.

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 @ 0x7f9e6026a850>
<Port M1.p2 Type=NodeType.OPTICAL @ 0x7f9e6026a2e0>

You can also get a read-only tuple of all the ports available:

print(M1.ports)
print(M1.optical_nodes)
print(M1.electrical_nodes)
print(M1.mechanical_nodes)
(<Port M1.p1 Type=NodeType.OPTICAL @ 0x7f9e6026a850>, <Port M1.p2 Type=NodeType.OPTICAL @ 0x7f9e6026a2e0>, <Port M1.mech Type=NodeType.MECHANICAL @ 0x7f9e602656a0>)
(<OpticalNode M1.p1.i @ 0x7f9e6026aac0>, <OpticalNode M1.p1.o @ 0x7f9e6026aa00>, <OpticalNode M1.p2.i @ 0x7f9e6026a7f0>, <OpticalNode M1.p2.o @ 0x7f9e6026a4c0>)
()
(<MechanicalNode M1.mech.z @ 0x7f9e62aeb0a0>, <MechanicalNode M1.mech.yaw @ 0x7f9dff5fcfa0>, <MechanicalNode M1.mech.pitch @ 0x7f9dff5fcca0>, <MechanicalNode M1.mech.F_z @ 0x7f9dff5fcfd0>, <MechanicalNode M1.mech.F_yaw @ 0x7f9dff4ccd90>, <MechanicalNode M1.mech.F_pitch @ 0x7f9dff608070>)

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', gouy_y=0.0, gouy_x=0.0, nr=1.0, L=10.0)
Port M1.p2 nodes = (<OpticalNode M1.p2.i @ 0x7fe1645e9c10>, <OpticalNode M1.p2.o @ 0x7fe1645e96d0>)

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 @ 0x7fe144188c10>), ('M1.p1.o', <OpticalNode M1.p1.o @ 0x7fe144188be0>), ('M1.p2.i', <OpticalNode M1.p2.i @ 0x7fe1645e9c10>), ('M1.p2.o', <OpticalNode M1.p2.o @ 0x7fe1645e96d0>), ('M1.mech.z', <MechanicalNode M1.mech.z @ 0x7fe1001aa730>), ('M1.mech.yaw', <MechanicalNode M1.mech.yaw @ 0x7fe1001aad00>), ('M1.mech.pitch', <MechanicalNode M1.mech.pitch @ 0x7fe1001aaca0>), ('M1.mech.F_z', <MechanicalNode M1.mech.F_z @ 0x7fe1001aa6a0>), ('M1.mech.F_yaw', <MechanicalNode M1.mech.F_yaw @ 0x7fe1001aabb0>), ('M1.mech.F_pitch', <MechanicalNode M1.mech.F_pitch @ 0x7fe1001aaa00>)])

Or all the optical nodes:

print(f"Optical nodes of M1 = {M1.optical_nodes}")
Optical nodes of M1 = (<OpticalNode M1.p1.i @ 0x7fe144188c10>, <OpticalNode M1.p1.o @ 0x7fe144188be0>, <OpticalNode M1.p2.i @ 0x7fe1645e9c10>, <OpticalNode M1.p2.o @ 0x7fe1645e96d0>)

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 @ 0x7fe144188c10>
Output node of port M1.n1 = <OpticalNode M1.p1.o @ 0x7fe144188be0>

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 @ 0x7fe144188c10>, <OpticalNode M1.p1.o @ 0x7fe144188be0>, <OpticalNode M1.p2.i @ 0x7fe1645e9c10>, <OpticalNode M1.p2.o @ 0x7fe1645e96d0>, <OpticalNode M2.p1.i @ 0x7fe1645e96a0>, <OpticalNode M2.p1.o @ 0x7fe1001aa9a0>, <OpticalNode M2.p2.i @ 0x7fe1001aaa90>, <OpticalNode M2.p2.o @ 0x7fe1001aae80>]

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 0x7fe1001ceea0; to 'OpticalNode' at 0x7fe1645e96a0>, 'owner': <weakref at 0x7fe1001be040; to 'Mirror' at 0x7fe1645e94c0>, 'optical': True}
{'name': 'P1i_P1o', 'in_ref': <weakref at 0x7fe1001ceea0; to 'OpticalNode' at 0x7fe1645e96a0>, 'out_ref': <weakref at 0x7fe1001d02c0; to 'OpticalNode' at 0x7fe1001aa9a0>, 'owner': <weakref at 0x7fe1001be040; to 'Mirror' at 0x7fe1645e94c0>, 'length': 1, 'coupling_type': <CouplingType.OPTICAL_TO_OPTICAL: 0>}