"""
Space-type objects representing physical distances between components.
"""
import logging
import numpy as np
import types
from finesse.parameter import float_parameter
from finesse.components.general import (
Connector,
InteractionType,
borrows_nodes,
DOFDefinition,
)
from finesse.components.workspace import ConnectionSetting
from finesse.components.node import NodeDirection, NodeType, Port
LOGGER = logging.getLogger(__name__)
[docs]@borrows_nodes()
@float_parameter(
"L",
"Length",
validate="_check_L",
units="m",
is_geometric=True,
)
@float_parameter(
"nr",
"Refractive index",
validate="_check_nr",
is_geometric=True,
changeable_during_simulation=False,
)
@float_parameter("gouy_x", "Gouy phase (x)", units="Degrees")
@float_parameter("gouy_y", "Gouy phase (y)", units="Degrees")
class Space(Connector):
"""Represents a space between two components in the interferometer configuration, with a given \
length and index of refraction.
Parameters
----------
name : str, optional
Name of newly created space.
portA, portB : :class:`.Port`, optional
Ports to connect.
L : float, optional
Geometric length of newly created :class:`.Space` instance; defaults to 0.
nr : float, optional
Index of refraction of newly created :class:`.Space` instance; defaults to 1.0.
"""
[docs] def __init__(self, name=None, portA=None, portB=None, L=0, nr=1.0, gouy_x=0, gouy_y=0):
if (portA is None) != (portB is None):
LOGGER.warn(
"Can't construct a space with only one port connected; ignoring ports."
)
portA = None
portB = None
if portA is not None and not isinstance(portA, Port):
raise Exception("PortA is not a Port")
if portA is not None and not isinstance(portB, Port):
raise Exception("PortB is not a Port")
if portA is not None and portA.type != NodeType.OPTICAL:
raise Exception("PortA is not an optical port")
if portB is not None and portB.type != NodeType.OPTICAL:
raise Exception("PortB is not an optical port")
if portA is not None and portB is not None:
if portA.component._model is not portB.component._model:
raise Exception("Port A and B are not part of the same model")
if name is None:
if portA is not None and portB is not None:
compA = portA.component.name
compB = portB.component.name
name = f"{compA}_{portA.name}__{compB}_{portB.name}"
else:
raise ValueError(
"Cannot create an unconnected space without providing a name"
)
super().__init__(name)
self.__portA = portA
self.__portB = portB
self._add_to_model_namespace = True
self._namespace = ".spaces"
self.L = L
self.nr = nr
self.gouy_x.is_tunable = True
self.gouy_y.is_tunable = True
self.gouy_x = gouy_x
self.gouy_y = gouy_y
self._add_port("p1", NodeType.OPTICAL)
self._add_port("p2", NodeType.OPTICAL)
# Phase modulation input
self._add_port("phs", NodeType.ELECTRICAL)
self.phs._add_node("i", NodeDirection.INPUT)
# Amplitude modulation input
self._add_port("amp", NodeType.ELECTRICAL)
self.amp._add_node("i", NodeDirection.INPUT)
# strain input
self._add_port("h", NodeType.ELECTRICAL)
self.h._add_node("i", NodeDirection.INPUT)
if portA is not None and portB is not None:
self.connect(portA, portB)
self.__changing_check = set((self.L, self.nr))
# Define typical degrees of freedom for this component
self.dofs = types.SimpleNamespace()
# Strain doesn't have a DC term really in Finesse
# changing a space length won't generate a signal
self.dofs.h = DOFDefinition(None, self.h.i, 1)
@property
def portA(self):
return self.__portA
@property
def portB(self):
return self.__portB
[docs] def connect(self, portA, portB):
"""
Sets the ports of this `Space`.
Parameters
----------
portA : :class:`.Port`, optional
Port to connect
portB : :class:`.Port`, optional
Port to connect
"""
if portA.is_connected:
raise Exception(f"Port {portA} has already been connected to")
if portB.is_connected:
raise Exception(f"Port {portB} has already been connected to")
# From the Space's perspective the input and output
# nodes are swapped around for its ports
self.p1._add_node("i", None, portA.o)
self.p1._add_node("o", None, portA.i)
self.p2._add_node("i", None, portB.o)
self.p2._add_node("o", None, portB.i)
self._register_node_coupling(
"P1i_P2o",
self.p1.i,
self.p2.o,
interaction_type=InteractionType.TRANSMISSION,
)
self._register_node_coupling(
"P2i_P1o",
self.p2.i,
self.p1.o,
interaction_type=InteractionType.TRANSMISSION,
)
self._register_node_coupling("SIGPHS_P1o", self.phs.i, self.p1.o)
self._register_node_coupling("SIGPHS_P2o", self.phs.i, self.p2.o)
self._register_node_coupling("SIGAMP_P1o", self.amp.i, self.p1.o)
self._register_node_coupling("SIGAMP_P2o", self.amp.i, self.p2.o)
self._register_node_coupling("H_P1o", self.h.i, self.p1.o)
self._register_node_coupling("H_P2o", self.h.i, self.p2.o)
def _get_workspace(self, sim):
from finesse.components.modal.space import (
space_carrier_fill,
space_signal_fill,
space_set_gouy,
SpaceWorkspace,
)
_, is_changing = self._eval_parameters()
carrier_refill = sim.carrier.any_frequencies_changing and (float(self.L.value) > 0 or self.L.is_changing)
carrier_refill |= len(is_changing)
carrier_refill |= sim.trace_forest.contains_space(self)
ws = SpaceWorkspace(self, sim, False)
ws.set_gouy_function(space_set_gouy)
# Set the fill function for this simulation
ws.carrier.add_fill_function(space_carrier_fill, carrier_refill)
ws.carrier.connection_settings["P1i_P2o"] = ConnectionSetting.DIAGONAL
ws.carrier.connection_settings["P2i_P1o"] = ConnectionSetting.DIAGONAL
if sim.signal:
signal_refill = sim.signal.any_frequencies_changing and (float(self.L.value) > 0 or self.L.is_changing)
signal_refill |= len(is_changing)
signal_refill |= sim.trace_forest.contains_space(self)
ws.signal.add_fill_function(space_signal_fill, signal_refill)
ws.signal.connection_settings["P1i_P2o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["P2i_P1o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["SIGPHS_P1o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["SIGPHS_P2o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["SIGAMP_P1o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["SIGAMP_P2o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["H_P1o"] = ConnectionSetting.DIAGONAL
ws.signal.connection_settings["H_P2o"] = ConnectionSetting.DIAGONAL
# Initialise the ABCD matrix memory-views
if sim.is_modal:
ws.abcd = self.ABCD(self.p1.i, self.p2.o, "x", copy=False)
return ws
def _check_L(self, value):
if value < 0:
raise ValueError("Length of a space must not be negative.")
return value
def _check_nr(self, value):
if value < 1:
raise ValueError("Index of refraction must be >= 1")
return value
def _resymbolise_ABCDs(self):
L = self.L.ref
nr = self.nr.ref
M_sym = np.array([[1.0, L / nr], [0.0, 1.0]])
# Matrices same in both propagations and both planes so
# only need one register call for all couplings here
self.register_abcd_matrix(
M_sym, (self.p1.i, self.p2.o), (self.p2.i, self.p1.o),
)
@property
def abcd(self):
"""Numeric ABCD matrix.
Equivalent to any of ``space.ABCD(1, 2, "x")``, ``space.ABCD(2, 1, "x")``,
``space.ABCD(1, 2, "y")``, ``space.ABCD(2, 1, "y")``.
:getter: Returns a copy of the (numeric) ABCD matrix (read-only).
"""
return self.ABCD(1, 2, "x")
[docs] def ABCD(
self,
from_node,
to_node,
direction="x",
symbolic=False,
copy=True,
retboth=False,
):
r"""Returns the ABCD matrix of the space for the specified coupling.
.. _fig_abcd_space_transmission:
.. figure:: ../images/abcd_spacet.*
:align: center
This is given by,
.. math::
M = \begin{pmatrix}
1 & \frac{L}{n_r} \\
0 & 1
\end{pmatrix},
where :math:`L` is the length of the space and :math:`n_r` is
the index of refraction.
See :meth:`.Connector.ABCD` for descriptions of parameters, return values and possible
exceptions.
"""
return super().ABCD(from_node, to_node, direction, symbolic, copy, retboth)