"""Optical components representing physical beamsplitters."""
import logging
import types
import numpy as np
import finesse
from finesse.exceptions import TotalReflectionError
from finesse.parameter import float_parameter, enum_parameter, bool_parameter
from finesse.utilities import refractive_index
from finesse.symbols import Constant, Variable, Matrix
from finesse.components.general import InteractionType, LocalDegreeOfFreedom, NoiseType
from finesse.components.surface import Surface
from finesse.components.node import NodeDirection, NodeType, Node
from finesse.tracing import abcd
from finesse.enums import PlaneOfIncidence
LOGGER = logging.getLogger(__name__)
[docs]@float_parameter("R", "Reflectivity", validate="_check_R", setter="set_RTL")
@float_parameter("T", "Transmission", validate="_check_T", setter="set_RTL")
@float_parameter("L", "Loss", validate="_check_L", setter="set_RTL")
@float_parameter("phi", "Phase", units="degrees")
@float_parameter(
"alpha",
"Angle of incidence (-90 <= alpha <= 90)",
units="degrees",
is_geometric=True,
changeable_during_simulation=False,
)
@float_parameter(
"Rcx",
"Radius of curvature (x)",
units="m",
validate="_check_Rc",
is_geometric=True,
)
@float_parameter(
"Rcy",
"Radius of curvature (y)",
units="m",
validate="_check_Rc",
is_geometric=True,
)
@float_parameter("xbeta", "Yaw misalignment", units="radians")
@float_parameter("ybeta", "Pitch misalignment", units="radians")
@enum_parameter(
"plane",
"Plane of incidence",
PlaneOfIncidence,
changeable_during_simulation=False,
validate="_check_plane_of_incidence",
)
@bool_parameter("misaligned", "Misaligns beamsplitter reflection (R=0 when True)")
# IMPORTANT: renaming this class impacts the katscript spec and should be avoided!
class Beamsplitter(Surface):
"""The beamsplitter component represents a thin dielectric surface with associated
properties such as reflectivity, tuning, and radius of curvature. It has four
optical ports p1, p2, p3, and p4 which describe the four beams incident on either
side of this surface. p1 and p2 are on side 1 and p3 and p4 are on side 2. A 100%
transmissive beamsplitter will transmit all of the light incident at p1 to p3.
It also has a mechanical port `mech` which has nodes for longitudinal, yaw, and pitch motions.
These mechanical nodes are purely for exciting small signal oscillations of the mirror.
Static offsets in longitudinal displacements are set by the `phi` parameter (in units of degrees),
misalignments in yaw by the `xbeta` parameter, and pitch the `ybeta` parameter. Macroscopic angle
of incidence of the beamsplitter is set by the ``alpha`` parameter.
Beamsplitters physically operate the same as mirror components, except for the non-normal angle of incidence
option.
Parameters
----------
name : str
Name of newly created beamsplitter.
R : float, optional
Reflectivity of the beamsplitter.
T : float, optional
Transmissivity of the beamsplitter.
L : float, optional
Loss of the beamsplitter.
phi : float, optional
Microscopic tuning of the beamsplitter (in degrees).
alpha : float, optional
Angle of incidence (in degrees)
Rc : float, optional
Radius of curvature (in metres); defaults to ``numpy.inf``
to indicate a planar surface.
xbeta, ybeta : float, optional
Angle of misalignment in the yaw plane (xbeta) and pitch
(ybeta), respectively (in radians); defaults to `0`.
plane : str, optional
Plane of incidence, either 'xz' or 'yz'. Defaults to 'xz'.
misaligned : bool, optional
When True the beamsplitter will be significantly misaligned and
assumes any reflected beam is dumped. Transmissions will
still occur.
Attributes
----------
Attributes are set via the Python API and not available via KatScript.
surface_map : :class:`finesse.knm.Map`
Decsribes the surface distortion of this beamsplitter component.
Coordinate system to the map is right-handed with the postive-z
direction as the surface normal on the port 1 side of the beamsplitter.
"""
[docs] def __init__(
self,
name,
R=None,
T=None,
L=None,
phi=0,
alpha=0,
Rc=np.inf,
xbeta=0,
ybeta=0,
plane=PlaneOfIncidence.xz,
misaligned=False,
):
super().__init__(name, R, T, L, phi, Rc, xbeta, ybeta)
self.plane = plane
self.misaligned = misaligned
self._add_port("p1", NodeType.OPTICAL)
self.p1._add_node("i", NodeDirection.INPUT)
self.p1._add_node("o", NodeDirection.OUTPUT)
self._add_port("p2", NodeType.OPTICAL)
self.p2._add_node("i", NodeDirection.INPUT)
self.p2._add_node("o", NodeDirection.OUTPUT)
self._add_port("p3", NodeType.OPTICAL)
self.p3._add_node("i", NodeDirection.INPUT)
self.p3._add_node("o", NodeDirection.OUTPUT)
self._add_port("p4", NodeType.OPTICAL)
self.p4._add_node("i", NodeDirection.INPUT)
self.p4._add_node("o", NodeDirection.OUTPUT)
# optic to optic couplings => reflections
self._register_node_coupling(
"P1i_P2o", self.p1.i, self.p2.o, interaction_type=InteractionType.REFLECTION
)
self._register_node_coupling(
"P2i_P1o", self.p2.i, self.p1.o, interaction_type=InteractionType.REFLECTION
)
self._register_node_coupling(
"P3i_P4o", self.p3.i, self.p4.o, interaction_type=InteractionType.REFLECTION
)
self._register_node_coupling(
"P4i_P3o", self.p4.i, self.p3.o, interaction_type=InteractionType.REFLECTION
)
# optic to optic couplings => transmissions
self._register_node_coupling(
"P1i_P3o",
self.p1.i,
self.p3.o,
interaction_type=InteractionType.TRANSMISSION,
)
self._register_node_coupling(
"P2i_P4o",
self.p2.i,
self.p4.o,
interaction_type=InteractionType.TRANSMISSION,
)
self._register_node_coupling(
"P3i_P1o",
self.p3.i,
self.p1.o,
interaction_type=InteractionType.TRANSMISSION,
)
self._register_node_coupling(
"P4i_P2o",
self.p4.i,
self.p2.o,
interaction_type=InteractionType.TRANSMISSION,
)
# mirror motion couplings
self._add_port("mech", NodeType.MECHANICAL)
self.mech._add_node("z", NodeDirection.BIDIRECTIONAL)
self.mech._add_node("yaw", NodeDirection.BIDIRECTIONAL)
self.mech._add_node("pitch", NodeDirection.BIDIRECTIONAL)
self.mech._add_node("F_z", NodeDirection.BIDIRECTIONAL)
self.mech._add_node("F_yaw", NodeDirection.BIDIRECTIONAL)
self.mech._add_node("F_pitch", NodeDirection.BIDIRECTIONAL)
# optic to motion couplings
self._register_node_coupling("P1i_Fz", self.p1.i, self.mech.F_z)
self._register_node_coupling("P2i_Fz", self.p2.i, self.mech.F_z)
self._register_node_coupling("P3i_Fz", self.p3.i, self.mech.F_z)
self._register_node_coupling("P4i_Fz", self.p4.i, self.mech.F_z)
self._register_node_coupling("P1o_Fz", self.p1.o, self.mech.F_z)
self._register_node_coupling("P2o_Fz", self.p2.o, self.mech.F_z)
self._register_node_coupling("P3o_Fz", self.p3.o, self.mech.F_z)
self._register_node_coupling("P4o_Fz", self.p4.o, self.mech.F_z)
self._register_node_coupling("P1i_Fyaw", self.p1.i, self.mech.F_yaw)
self._register_node_coupling("P2i_Fyaw", self.p2.i, self.mech.F_yaw)
self._register_node_coupling("P3i_Fyaw", self.p3.i, self.mech.F_yaw)
self._register_node_coupling("P4i_Fyaw", self.p4.i, self.mech.F_yaw)
self._register_node_coupling("P1o_Fyaw", self.p1.o, self.mech.F_yaw)
self._register_node_coupling("P2o_Fyaw", self.p2.o, self.mech.F_yaw)
self._register_node_coupling("P3o_Fyaw", self.p3.o, self.mech.F_yaw)
self._register_node_coupling("P4o_Fyaw", self.p4.o, self.mech.F_yaw)
self._register_node_coupling("P1i_Fpitch", self.p1.i, self.mech.F_pitch)
self._register_node_coupling("P2i_Fpitch", self.p2.i, self.mech.F_pitch)
self._register_node_coupling("P3i_Fpitch", self.p3.i, self.mech.F_pitch)
self._register_node_coupling("P4i_Fpitch", self.p4.i, self.mech.F_pitch)
self._register_node_coupling("P1o_Fpitch", self.p1.o, self.mech.F_pitch)
self._register_node_coupling("P2o_Fpitch", self.p2.o, self.mech.F_pitch)
self._register_node_coupling("P3o_Fpitch", self.p3.o, self.mech.F_pitch)
self._register_node_coupling("P4o_Fpitch", self.p4.o, self.mech.F_pitch)
# motion to optic couplings: phase coupling on reflection
self._register_node_coupling("Z_P1o", self.mech.z, self.p1.o)
self._register_node_coupling("Z_P2o", self.mech.z, self.p2.o)
self._register_node_coupling("Z_P3o", self.mech.z, self.p3.o)
self._register_node_coupling("Z_P4o", self.mech.z, self.p4.o)
self._register_node_coupling("yaw_P1o", self.mech.yaw, self.p1.o)
self._register_node_coupling("yaw_P2o", self.mech.yaw, self.p2.o)
self._register_node_coupling("yaw_P3o", self.mech.yaw, self.p3.o)
self._register_node_coupling("yaw_P4o", self.mech.yaw, self.p4.o)
self._register_node_coupling("pitch_P1o", self.mech.pitch, self.p1.o)
self._register_node_coupling("pitch_P2o", self.mech.pitch, self.p2.o)
self._register_node_coupling("pitch_P3o", self.mech.pitch, self.p3.o)
self._register_node_coupling("pitch_P4o", self.mech.pitch, self.p4.o)
# NOTE (pjj) temporarily moved after add_port, as validator depends on ports
# being present. Same fix as below.
self.alpha = alpha
self.surface_map = None
self.__changing_check = set(
(self.R, self.T, self.L, self.phi, self.alpha, self.xbeta, self.ybeta)
)
# Define typical degrees of freedom for this component
self.dofs = types.SimpleNamespace()
self.dofs.z = LocalDegreeOfFreedom(
f"{self.name}.dofs.z", self.phi, self.mech.z, 1
)
self.dofs.yaw = LocalDegreeOfFreedom(
f"{self.name}.dofs.yaw", self.xbeta, self.mech.yaw, 1
)
self.dofs.pitch = LocalDegreeOfFreedom(
f"{self.name}.dofs.pitch", self.ybeta, self.mech.pitch, 1
)
self.dofs.F_z = LocalDegreeOfFreedom(
f"{self.name}.dofs.F_z", None, self.mech.F_z, None, AC_OUT=self.mech.z
)
self.dofs.F_yaw = LocalDegreeOfFreedom(
f"{self.name}.dofs.F_yaw",
self.xbeta,
self.mech.F_yaw,
None,
AC_OUT=self.mech.yaw,
)
self.dofs.F_pitch = LocalDegreeOfFreedom(
f"{self.name}.dofs.F_pitch",
self.ybeta,
self.mech.F_pitch,
None,
AC_OUT=self.mech.pitch,
)
[docs] def optical_equations(self):
with finesse.symbols.simplification():
_f_ = Variable("_f_")
_f0_ = Variable("_f0_")
nr1 = self.ports[0].refractive_index
nr2 = self.ports[1].refractive_index
pi = finesse.symbols.CONSTANTS["pi"]
cos_alpha = np.cos(self.alpha.ref * pi / 180)
cos_alpha_2 = np.cos(
np.arcsin(nr1 / nr2 * np.sin(self.alpha.ref * pi / 180))
)
r = (self.R.ref) ** 0.5
t = (self.T.ref) ** 0.5
phi = self.phi.ref * (1 + _f_ / _f0_)
aligned = 1 - self.misaligned.ref
if self._model._settings.phase_config.v2_transmission_phase or nr1 == nr2:
# old v2 phase on transmission
# The usual i on transmission and reflections
# are opposite phase on each side, ignores refractive index
phi_r1 = 2 * phi * cos_alpha
r1 = r * np.exp(1j * phi_r1)
r2 = r * np.exp(-1j * phi_r1)
t12 = t21 = 1j * t
else:
# Uses N=-1, Eq.2.25 in Living Reviews in Relativity (2016)
# 19:3 DOI 10.1007/s41114-016-0002-8
# beamsplitter transmission phase depends on the reflectivity
# refractive indices and angle of incidence
phi_r1 = +2 * phi * cos_alpha * nr1
phi_r2 = -2 * phi * cos_alpha_2 * nr2
phi_t = np.pi / 2 + 0.5 * (phi_r1 + phi_r2)
r1 = r * np.exp(1j * phi_r1)
r2 = r * np.exp(1j * phi_r2)
t12 = t * np.exp(1j * phi_t)
t21 = t * np.exp(-1j * phi_t)
plane_wave_equations = {
f"{self.name}.P1i_P2o": r1 * aligned,
f"{self.name}.P2i_P1o": r1 * aligned,
f"{self.name}.P3i_P4o": r2 * aligned,
f"{self.name}.P4i_P3o": r2 * aligned,
f"{self.name}.P1i_P3o": t12,
f"{self.name}.P2i_P4o": t12,
f"{self.name}.P3i_P1o": t21,
f"{self.name}.P4i_P2o": t21,
}
if self._model._settings.is_modal:
# Apply scatter matrices to equations for HOMs
hom_equations = {
(key := f"{self.name}.P{a}i_P{b}o"): plane_wave_equations[key]
* Matrix(f"{self.name}.K{a}{b}")
for a, b in [
(1, 2),
(2, 1),
(3, 4),
(4, 3),
(1, 3),
(2, 4),
(3, 1),
(4, 2),
]
}
return hom_equations
else:
return plane_wave_equations
[docs] def get_adjacent_port(self, p):
"""Get the port adjacent (on the same side of the surface) as `p`."""
if isinstance(p, Node):
p = p.port
adj_dict = {
self.p1: self.p2,
self.p2: self.p1,
self.p3: self.p4,
self.p4: self.p3,
}
if p not in adj_dict:
raise ValueError(f"Port {p} does not belong to Beamsplitter {self.name}")
return adj_dict[p]
def _resymbolise_ABCDs(self):
# -> reflections
self.__symbolise_ABCD(self.p1.i, self.p2.o, "x")
self.__symbolise_ABCD(self.p1.i, self.p2.o, "y")
self.__symbolise_ABCD(self.p2.i, self.p1.o, "x")
self.__symbolise_ABCD(self.p2.i, self.p1.o, "y")
self.__symbolise_ABCD(self.p3.i, self.p4.o, "x")
self.__symbolise_ABCD(self.p3.i, self.p4.o, "y")
self.__symbolise_ABCD(self.p4.i, self.p3.o, "x")
self.__symbolise_ABCD(self.p4.i, self.p3.o, "y")
# -> transmissions
self.__symbolise_ABCD(self.p1.i, self.p3.o, "x")
self.__symbolise_ABCD(self.p1.i, self.p3.o, "y")
self.__symbolise_ABCD(self.p3.i, self.p1.o, "x")
self.__symbolise_ABCD(self.p3.i, self.p1.o, "y")
self.__symbolise_ABCD(self.p2.i, self.p4.o, "x")
self.__symbolise_ABCD(self.p2.i, self.p4.o, "y")
self.__symbolise_ABCD(self.p4.i, self.p2.o, "x")
self.__symbolise_ABCD(self.p4.i, self.p2.o, "y")
@property
def refractive_index_1(self):
"""Refractive index on size 1 (port 1 and 2)"""
if self.p1.attached_to and self.p2.attached_to:
nr_p1 = self.p1.refractive_index
nr_p2 = self.p2.refractive_index
if float(nr_p1) != float(nr_p2):
raise RuntimeError(
f"{self.name} has different refractive index at port 1 and port 2 ({nr_p1} != {nr_p2})"
)
return nr_p1
elif self.p1.attached_to and not self.p2.attached_to:
return self.p1.refractive_index
elif not self.p1.attached_to and self.p2.attached_to:
return self.p2.refractive_index
else:
return Constant(1)
@property
def refractive_index_2(self):
"""Refractive index on size 2 (port 3 and 4)"""
if self.p3.attached_to and self.p4.attached_to:
nr_p3 = self.p3.refractive_index
nr_p4 = self.p4.refractive_index
if float(nr_p3) != float(nr_p4):
raise RuntimeError(
f"{self.name} has different refractive index at port 3 and port 4 ({nr_p3} != {nr_p4})"
)
return nr_p3
elif self.p3.attached_to and not self.p4.attached_to:
return self.p3.refractive_index
elif not self.p3.attached_to and self.p4.attached_to:
return self.p4.refractive_index
else:
return Constant(1)
@property
def alpha2(self):
"""Angle of incidence on side 2 in degrees, i.e. port 3 and 4 side.
Returns
-------
alpha2 : Symbol
Symbolic form of alpha on side 2. Use `float()` to convert to a
numerical value if needed.
Raises
------
Will raise a TotalReflectionError if total internal reflection is occuring
at this beamsplitter.
"""
alpha1 = self.alpha.ref
nr1 = self.refractive_index_1
nr2 = self.refractive_index_2
alpha1_rad = np.radians(alpha1)
sin_alpha2_rad = (nr1 / nr2) * np.sin(alpha1_rad)
if abs(float(sin_alpha2_rad)) > 1:
raise TotalReflectionError(f"Total reflection in beam splitter {self.name}")
alpha2_rad = np.arcsin(sin_alpha2_rad)
return np.degrees(alpha2_rad)
def __symbolise_ABCD(self, from_node, to_node, direction):
assert direction in ["x", "y"]
if direction == "x":
Rc = self.Rcx.ref
else:
Rc = self.Rcy.ref
alpha1 = self.alpha.ref
try:
alpha2 = self.alpha2
except TotalReflectionError as tr:
self._abcd_matrices[(from_node, to_node, direction)] = None, tr
return
nri = refractive_index(from_node, symbolic=True)
nro = refractive_index(to_node, symbolic=True)
if self.interaction_type(from_node, to_node) == InteractionType.REFLECTION:
if self.plane == PlaneOfIncidence.xz:
if direction == "x":
ABCD = abcd.beamsplitter_refl_t
else:
ABCD = abcd.beamsplitter_refl_s
elif self.plane == PlaneOfIncidence.yz:
if direction == "x":
ABCD = abcd.beamsplitter_refl_s
else:
ABCD = abcd.beamsplitter_refl_t
else:
raise ValueError()
# reflection
if from_node.port is self.p3 or from_node.port is self.p4:
M_sym = ABCD(
-Rc,
alpha2,
nr=nri,
)
else:
M_sym = ABCD(
Rc,
alpha1,
nr=nri,
)
else: # transmission
if self.plane == PlaneOfIncidence.xz:
if direction == "x":
ABCD = abcd.beamsplitter_trans_t
else:
ABCD = abcd.beamsplitter_trans_s
elif self.plane == PlaneOfIncidence.yz:
if direction == "x":
ABCD = abcd.beamsplitter_trans_s
else:
ABCD = abcd.beamsplitter_trans_t
if from_node.port is self.p3 or from_node.port is self.p4:
M_sym = ABCD(
-Rc,
alpha2,
nr1=nri,
nr2=nro,
)
else:
M_sym = ABCD(
Rc,
alpha1,
nr1=nri,
nr2=nro,
)
key = (from_node, to_node, direction)
# For beamsplitters the symbol nr can change when connected up to spaces
# in a model so here we update the ABCD matrix entry
if key in self._abcd_matrices:
self._abcd_matrices[key] = M_sym, np.array(M_sym, dtype=np.float64)
# Otherwise just register the new ABCD matrix as usual
else:
self.register_abcd_matrix(M_sym, (from_node, to_node, direction))
@property
def abcd12x(self):
"""Numeric ABCD matrix from port 1 to port 2 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(1, 2, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(1, 2, "x")
@property
def abcd12y(self):
"""Numeric ABCD matrix from port 1 to port 2 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(1, 2, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(1, 2, "y")
@property
def abcd21x(self):
"""Numeric ABCD matrix from port 2 to port 1 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(2, 1, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(2, 1, "x")
@property
def abcd21y(self):
"""Numeric ABCD matrix from port 2 to port 1 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(2, 1, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(2, 1, "y")
@property
def abcd34x(self):
"""Numeric ABCD matrix from port 3 to port 4 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(3, 4, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(3, 4, "x")
@property
def abcd34y(self):
"""Numeric ABCD matrix from port 3 to port 4 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(3, 4, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(3, 4, "y")
@property
def abcd43x(self):
"""Numeric ABCD matrix from port 4 to port 3 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(4, 3, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(4, 3, "x")
@property
def abcd43y(self):
"""Numeric ABCD matrix from port 4 to port 3 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(4, 3, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(4, 3, "y")
@property
def abcd13x(self):
"""Numeric ABCD matrix from port 1 to port 3 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(1, 3, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(1, 3, "x")
@property
def abcd13y(self):
"""Numeric ABCD matrix from port 1 to port 3 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(1, 3, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(1, 3, "y")
@property
def abcd31x(self):
"""Numeric ABCD matrix from port 3 to port 1 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(3, 1, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(3, 1, "x")
@property
def abcd31y(self):
"""Numeric ABCD matrix from port 3 to port 1 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(3, 1, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(3, 1, "y")
@property
def abcd24x(self):
"""Numeric ABCD matrix from port 2 to port 4 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(2, 4, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(2, 4, "x")
@property
def abcd24y(self):
"""Numeric ABCD matrix from port 2 to port 4 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(2, 4, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(2, 4, "y")
@property
def abcd42x(self):
"""Numeric ABCD matrix from port 4 to port 2 in the tangential plane.
Equivalent to ``beamsplitter.ABCD(4, 2, "x")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(4, 2, "x")
@property
def abcd42y(self):
"""Numeric ABCD matrix from port 4 to port 2 in the sagittal plane.
Equivalent to ``beamsplitter.ABCD(4, 2, "y")``.
:`getter`: Returns a copy of the (numeric) ABCD matrix for this coupling
(read-only).
"""
return self.ABCD(4, 2, "y")
[docs] def ABCD(
self,
from_node,
to_node,
direction="x",
symbolic=False,
copy=True,
retboth=False,
allow_reverse=False,
):
r"""Returns the ABCD matrix of the beam splitter for the specified coupling.
The matrices for transmission and reflection are different for
the sagittal and tangential planes (:math:`M_s` and :math:`M_t`),
as shown below.
.. rubric:: Transmission
.. _fig_abcd_bs_transmission:
.. figure:: ../images/abcd_bst.*
:align: center
For the tangential plane (`direction = 'x'`),
.. math::
M_t = \begin{pmatrix}
\frac{\cos{\alpha_2}}{\cos{\alpha_1}} & 0 \\
\frac{\Delta n}{R_c} & \frac{\cos{\alpha_1}}{\cos{\alpha_2}}
\end{pmatrix},
and for the sagittal plane (`direction = 'y'`),
.. math::
M_s = \begin{pmatrix}
1 & 0 \\
\frac{\Delta n}{R_c} & 1
\end{pmatrix},
where :math:`\alpha_1` is the angle of incidence of the beam splitter and
:math:`\alpha_2` is given by Snell's law (:math:`n_1\sin{\alpha_1} =
n_2\sin{\alpha_2}`). The quantity :math:`\Delta n` is given by,
.. math::
\Delta_n = \frac{n_2 \cos{\alpha_2} - n_1 \cos{\alpha_1}}{
\cos{\alpha_1} \cos{\alpha_2}
}.
If the direction of propagation is reversed such that the radius of curvature
of the beam splitter is in this direction, then the elements :math:`A` and
:math:`D` of the tangential matrix (:math:`M_t`) are swapped.
.. rubric:: Reflection
.. _fig_abcd_bs_reflection:
.. figure:: ../images/abcd_bsr.*
:align: center
The reflection at the front surface of the beam splitter is given by,
.. math::
M_t = \begin{pmatrix}
1 & 0 \\
-\frac{2n_1}{R_c \cos{\alpha_1}} & 1
\end{pmatrix},
for the tangential plane, and,
.. math::
M_s = \begin{pmatrix}
1 & 0 \\
-\frac{2n_1 \cos{\alpha_2}}{R_c} & 1
\end{pmatrix},
for the sagittal plane.
At the back surface :math:`R_c \rightarrow - R_c` and
:math:`\alpha_1 \rightarrow - \alpha_2`.
See :meth:`.Connector.ABCD` for descriptions of parameters, return values and possible
exceptions.
Raises
------
tre : :class:`.TotalReflectionError`
If total reflection occurs for the specified coupling - i.e. if :math:`\sin{\alpha_2} > 1.0`.
"""
return super().ABCD(
from_node, to_node, direction, symbolic, copy, retboth, allow_reverse
)
def _get_workspace(self, sim):
from finesse.components.modal.beamsplitter import (
beamsplitter_carrier_fill,
beamsplitter_signal_fill,
beamsplitter_fill_qnoise,
BeamsplitterWorkspace,
)
_, is_changing = self._eval_parameters()
carrier_refill = (
sim.is_component_in_mismatch_couplings(self)
or self in sim.trace_forest
or sim.carrier.any_frequencies_changing
or (len(is_changing) and is_changing.issubset(self.__changing_check))
)
ws = BeamsplitterWorkspace(self, sim)
ws.carrier.add_fill_function(beamsplitter_carrier_fill, carrier_refill)
if sim.signal:
signal_refill = sim.model.fsig.f.is_changing
ws.signal.add_fill_function(
beamsplitter_signal_fill, carrier_refill or signal_refill
)
# Initialise the ABCD matrix memory-views
if sim.is_modal:
try:
ws.abcd_p1p2_x = self.ABCD(self.p1.i, self.p2.o, "x", copy=False)
ws.abcd_p1p2_y = self.ABCD(self.p1.i, self.p2.o, "y", copy=False)
except TotalReflectionError:
raise
try:
ws.abcd_p2p1_x = self.ABCD(self.p2.i, self.p1.o, "x", copy=False)
ws.abcd_p2p1_y = self.ABCD(self.p2.i, self.p1.o, "y", copy=False)
except TotalReflectionError:
raise
try:
ws.abcd_p3p4_x = self.ABCD(self.p3.i, self.p4.o, "x", copy=False)
ws.abcd_p3p4_y = self.ABCD(self.p3.i, self.p4.o, "y", copy=False)
except TotalReflectionError:
raise
try:
ws.abcd_p4p3_x = self.ABCD(self.p4.i, self.p3.o, "x", copy=False)
ws.abcd_p4p3_y = self.ABCD(self.p4.i, self.p3.o, "y", copy=False)
except TotalReflectionError:
raise
ws.abcd_p1p3_x = self.ABCD(self.p1.i, self.p3.o, "x", copy=False)
ws.abcd_p1p3_y = self.ABCD(self.p1.i, self.p3.o, "y", copy=False)
ws.abcd_p3p1_x = self.ABCD(self.p3.i, self.p1.o, "x", copy=False)
ws.abcd_p3p1_y = self.ABCD(self.p3.i, self.p1.o, "y", copy=False)
ws.abcd_p2p4_x = self.ABCD(self.p2.i, self.p4.o, "x", copy=False)
ws.abcd_p2p4_y = self.ABCD(self.p2.i, self.p4.o, "y", copy=False)
ws.abcd_p4p2_x = self.ABCD(self.p4.i, self.p2.o, "x", copy=False)
ws.abcd_p4p2_y = self.ABCD(self.p4.i, self.p2.o, "y", copy=False)
alpha2 = np.arcsin((ws.nr1 / ws.nr2) * np.sin(np.deg2rad(self.alpha.value)))
cos_alpha = np.cos(np.deg2rad(self.alpha.value))
cos_alpha2 = np.cos(alpha2)
ws.set_knm_info(
"P1i_P2o",
alpha=self.alpha,
is_transmission=False,
nr_from=ws.nr1,
nr_to=ws.nr1,
beta_x=self.xbeta,
beta_x_factor=2,
beta_y=self.ybeta,
beta_y_factor=-2 * cos_alpha,
abcd_x=ws.abcd_p1p2_x,
abcd_y=ws.abcd_p1p2_y,
apply_map=self.surface_map,
map_phase_factor=-2 * ws.nr1,
map_fliplr=False,
)
ws.set_knm_info(
"P2i_P1o",
alpha=self.alpha,
is_transmission=False,
nr_from=ws.nr1,
nr_to=ws.nr1,
beta_x=self.xbeta,
beta_x_factor=2,
beta_y=self.ybeta,
beta_y_factor=-2 * cos_alpha,
abcd_x=ws.abcd_p2p1_x,
abcd_y=ws.abcd_p2p1_y,
apply_map=self.surface_map,
map_phase_factor=-2 * ws.nr1,
map_fliplr=False,
)
ws.set_knm_info(
"P3i_P4o",
alpha=self.alpha,
is_transmission=False,
nr_from=ws.nr2,
nr_to=ws.nr2,
beta_x=self.xbeta,
beta_x_factor=2,
beta_y=self.ybeta,
beta_y_factor=2 * cos_alpha2,
abcd_x=ws.abcd_p3p4_x,
abcd_y=ws.abcd_p3p4_y,
apply_map=self.surface_map,
map_phase_factor=2 * ws.nr2,
map_fliplr=True,
)
ws.set_knm_info(
"P4i_P3o",
alpha=self.alpha,
is_transmission=False,
nr_from=ws.nr2,
nr_to=ws.nr2,
beta_x=self.xbeta,
beta_x_factor=2,
beta_y=self.ybeta,
beta_y_factor=2 * cos_alpha2,
abcd_x=ws.abcd_p4p3_x,
abcd_y=ws.abcd_p4p3_y,
apply_map=self.surface_map,
map_phase_factor=2 * ws.nr2,
map_fliplr=True,
)
ws.set_knm_info(
"P1i_P3o",
alpha=self.alpha,
is_transmission=True,
nr_from=ws.nr1,
nr_to=ws.nr2,
beta_x=self.xbeta,
beta_x_factor=-(1 - ws.nr1 / ws.nr2),
beta_y=self.ybeta,
beta_y_factor=(1 - ws.nr1 / ws.nr2),
abcd_x=ws.abcd_p1p3_x,
abcd_y=ws.abcd_p1p3_y,
apply_map=self.surface_map,
map_phase_factor=(ws.nr2 - ws.nr1),
map_fliplr=False,
)
ws.set_knm_info(
"P3i_P1o",
alpha=self.alpha,
is_transmission=True,
nr_from=ws.nr2,
nr_to=ws.nr1,
beta_x=self.xbeta,
beta_x_factor=-(1 - ws.nr2 / ws.nr1),
beta_y=self.ybeta,
beta_y_factor=-(1 - ws.nr2 / ws.nr1),
abcd_x=ws.abcd_p3p1_x,
abcd_y=ws.abcd_p3p1_y,
apply_map=self.surface_map,
map_phase_factor=-(ws.nr1 - ws.nr2),
map_fliplr=True,
)
ws.set_knm_info(
"P2i_P4o",
alpha=self.alpha,
is_transmission=True,
nr_from=ws.nr1,
nr_to=ws.nr2,
beta_x=self.xbeta,
beta_x_factor=-(1 - ws.nr1 / ws.nr2),
beta_y=self.ybeta,
beta_y_factor=(1 - ws.nr1 / ws.nr2),
abcd_x=ws.abcd_p2p4_x,
abcd_y=ws.abcd_p2p4_y,
apply_map=self.surface_map,
map_phase_factor=(ws.nr2 - ws.nr1),
map_fliplr=False,
)
ws.set_knm_info(
"P4i_P2o",
alpha=self.alpha,
is_transmission=True,
nr_from=ws.nr2,
nr_to=ws.nr1,
beta_x=self.xbeta,
beta_x_factor=-(1 - ws.nr2 / ws.nr1),
beta_y=self.ybeta,
beta_y_factor=-(1 - ws.nr2 / ws.nr1),
abcd_x=ws.abcd_p4p2_x,
abcd_y=ws.abcd_p4p2_y,
apply_map=self.surface_map,
map_phase_factor=-(ws.nr1 - ws.nr2),
map_fliplr=True,
)
if sim.signal:
ws.signal.set_fill_noise_function(
NoiseType.QUANTUM, beamsplitter_fill_qnoise
)
return ws
def _check_plane_of_incidence(self, value):
try:
value = PlaneOfIncidence(value)
except ValueError:
try:
value = PlaneOfIncidence[value]
except KeyError:
raise ValueError(
f"'{value}' is not a valid plane of incidence, options are {tuple(_ for _ in PlaneOfIncidence.__members__.keys())}"
)
return value