import copy as libcopy
import io
import json
import struct
import warnings
import zlib
from collections.abc import Sequence
from typing import Any, Literal
import numpy
from . import typing as pft
from .cache import cache_s_matrix
from .extension import (
Component,
Interpolator,
Model,
Path,
Port,
PortSpec,
Reference,
SMatrix,
Technology,
_from_bytes,
boolean,
config,
frequency_classification,
register_model_class,
text,
)
from .tidy3d_model import _ModeSolverRunner
from .utils import C_0, route_length
_ComplexCoeff = pft.array(complex, 0, 2)
_ComplexCoeff1D = pft.array(complex, 0, 1)
_FloatCoeff = pft.array(float, 0, 2)
_bb_layer = (0, 32767)
def _add_bb_text(component, width):
temp = Component(technology=component.technology)
temp.add(_bb_layer, *text("BB", width, typeface=1))
ref = Reference(temp)
ref_size = ref.size()
size = component.size()
if ref_size[0] > 0.8 * size[0] or ref_size[1] > 0.8 * size[1]:
ref.scale(0.8 * min(size[0] / ref_size[0], size[1] / ref_size[1]))
elif ref_size[0] < 0.2 * size[0] and ref_size[1] < 0.2 * size[1]:
ref.scale(0.2 * min(size[0] / ref_size[0], size[1] / ref_size[1]))
ref.translate(0.5 * (sum(component.bounds()) - sum(ref.bounds())))
component.add(_bb_layer, *ref.get_structures(_bb_layer))
def _ensure_correct_shape(x: Any, ndims=2) -> numpy.ndarray | Interpolator:
if isinstance(x, Interpolator):
if ndims == 1 and isinstance(x.y, list):
x = Interpolator(x.x, x.y[0], x.method, x.coords)
elif ndims == 2 and not isinstance(x.y, list):
x = Interpolator(x.x, [x.y], x.method, x.coords)
return x
y = numpy.array(x)
if y.ndim < ndims:
shape = (-1,) + (1,) * (ndims - 1)
y = y.reshape(shape)
return y
def _sample(
component_name: str,
name: str,
value: numpy.ndarray | Interpolator,
frequencies: Sequence[float],
num_modes: int,
) -> numpy.ndarray:
if isinstance(value, Interpolator):
value = value(frequencies)
if value.shape[0] == 1 and num_modes > 1:
value = numpy.array(numpy.broadcast_to(value, (num_modes, len(frequencies))))
else:
shape = (max(num_modes, value.shape[0]), len(frequencies))
value = numpy.array(numpy.broadcast_to(value, shape))
if value.shape[0] < num_modes:
raise RuntimeError(
f"The first dimension of {name!r} in the model for {component_name!r} must be "
f"{num_modes} to account for all modes in the component's ports."
)
return value[:num_modes]
class ModelResult:
"""DEPRECATED - Model.start may return an SMatrix directly. This class is no longer required."""
def __init__(self, s_matrix: SMatrix, status: dict[str, Any] | None = None) -> None:
warnings.warn(
"ModelResult class is deprecated. The method Model.start may return an SMatrix "
"instance directly. This class is no longer needed and it will be removed in the "
"future.",
FutureWarning,
stacklevel=2,
)
self.status = {"progress": 100, "message": "success"} if status is None else status
self.s_matrix = s_matrix
[docs]
class TerminationModel(Model):
r"""Data model for a 1-port device.
Args:
r: Reflection coefficient for the first port. For multimode ports, a
sequence of coefficients must be provided.
Notes:
For multimode ports, mixed-mode coefficients are 0. Dispersion can
be included in the model by setting the coefficient to an
:class:`Interpolator` (with multiple values for multimode ports), or
a 2D array with shape (M, N), in which M is the number of modes, and
N the length of the frequency sequence used in the S matrix
computation.
"""
def __init__(self, r: _ComplexCoeff | Interpolator = 0) -> None:
super().__init__(r=_ensure_correct_shape(r))
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "termination"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = width * 8
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
component.add(layer, Path((0, 0), w, g).segment((length, 0), 0))
_add_bb_text(component, width)
component.add_port(Port((0, 0), 0, port_spec))
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 1:
raise RuntimeError(
f"TerminationModel can only be used on components with 1 port. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
name, port = next(iter(component_ports.items()))
r = _sample(component.name, "r", self.parametric_kwargs["r"], frequencies, port.num_modes)
elements = {(f"{name}@{mode}", f"{name}@{mode}"): r[mode] for mode in range(port.num_modes)}
return SMatrix(frequencies, elements, {name: port})
# Deprecated: kept for backwards compatibility with old phf files
[docs]
@classmethod
def from_bytes(cls, byte_repr: bytes) -> "TerminationModel":
"""De-serialize this model."""
size = struct.calcsize("<BQ")
version, length = struct.unpack("<BQ", byte_repr[:size])
if version != 0:
raise RuntimeError("Unsuported TerminationModel version.")
if len(byte_repr) != size + length:
raise ValueError("Unexpected byte representation for TerminationModel")
mem_io = io.BytesIO()
mem_io.write(byte_repr[size:])
mem_io.seek(0)
coeff = numpy.load(mem_io)
return cls(coeff)
[docs]
class TwoPortModel(Model):
r"""Data model for a 2-port component.
.. math:: S = \begin{bmatrix}
r_0 e^{j \phi} & t e^{j \phi} \\
t e^{j \phi} & r_1 e^{j \phi} \\
\end{bmatrix}
with dispersion modeled by:
.. math:: \phi = \frac{2 \pi l_p}{c_0}
[n_\text{eff} f_0 + n_\text{group} (f - f_0)]
Args:
t: Transmission coefficient.
r0: Reflection coefficient for the first port.
r1: Reflection coefficient for the second port.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
propagation_length: Propagation length :math:`l_p` for dispersion
modeling.
n_eff: Effective refractive index for dispersion modeling.
n_group: Group index. If ``None``, the value of ``n_eff`` is used.
reference_frequency: Reference frequency :math:`f_0` for dispersion
calculation. If ``None``, the central frequency is used.
Notes:
For multimode ports, a sequence of coefficients must be used, and
mixed-mode coefficients are 0. Dispersion can be included in the
model by setting the coefficients to an :class:`Interpolator` (with
multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
t: _ComplexCoeff | Interpolator = 1,
r0: _ComplexCoeff | Interpolator = 0,
r1: _ComplexCoeff | Interpolator = 0,
ports: pft.annotate(Sequence[str], minItems=2, maxItems=2) | None = None,
*,
propagation_length: pft.annotate(float, units="μm") = 0.0,
n_eff: _ComplexCoeff | Interpolator = 0.0,
n_group: _FloatCoeff | None = None,
reference_frequency: pft.Frequency | None = None,
) -> None:
super().__init__(
t=_ensure_correct_shape(t),
r0=_ensure_correct_shape(r0),
r1=_ensure_correct_shape(r1),
ports=ports,
propagation_length=propagation_length,
n_eff=_ensure_correct_shape(n_eff),
n_group=n_group,
reference_frequency=reference_frequency,
)
if ports is not None and len(ports) != 2:
raise TypeError(
f"TwoPortModel can only be used on components with 2 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "wg"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = abs(self.parametric_kwargs["propagation_length"])
if length == 0.0:
length = width * 8
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
component.add(layer, Path((0, 0), w, g).segment((length, 0)))
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 2
component.add_port(Port((0, 0), 0, port_spec), port_names[0])
component.add_port(Port((length, 0), 180, port_spec), port_names[1])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 2:
raise RuntimeError(
f"TwoPortModel can only be used on components with 2 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in TwoPortModel match the {classification} port "
f"names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"TwoPortModel requires that all component ports have the same number of "
f"modes. Ports from '{component.name}' support different numbers of modes."
)
lp = _ensure_correct_shape(p["propagation_length"])
f0 = p["reference_frequency"]
if f0 is None:
f0 = 0.5 * (frequencies.min() + frequencies.max())
n_eff = _sample(component.name, "n_eff", p["n_eff"], frequencies, num_modes)
n_group = p["n_group"]
if n_group is None:
n_group = n_eff
else:
n_group = _ensure_correct_shape(n_group)
phase = numpy.exp(
2j * numpy.pi * lp * ((n_eff - n_group) * f0 + n_group * frequencies) / C_0
)
t = _sample(component.name, "t", p["t"], frequencies, num_modes) * phase
r0 = _sample(component.name, "r0", p["r0"], frequencies, num_modes) * phase
r1 = _sample(component.name, "r1", p["r1"], frequencies, num_modes) * phase
s = (
(r0, t),
(t, r1),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
# Deprecated: kept for backwards compatibility with old phf files
[docs]
@classmethod
def from_bytes(cls, byte_repr: bytes) -> "TwoPortModel":
"""De-serialize this model."""
size = struct.calcsize("<B5Q")
version, *lengths = struct.unpack("<B5Q", byte_repr[:size])
if version != 0:
raise RuntimeError("Unsuported TwoPortModel version.")
coeffs = []
for length in lengths[:3]:
mem_io = io.BytesIO()
mem_io.write(byte_repr[size : size + length])
mem_io.seek(0)
coeffs.append(numpy.load(mem_io))
size += length
if all(length == 0 for length in lengths[3:]):
ports = None
else:
ports = []
for length in lengths[3:]:
ports.append(byte_repr[size : size + length].decode("utf8"))
size += length
return cls(*coeffs, ports)
[docs]
class PowerSplitterModel(Model):
r"""Data model for a 3-port power splitter.
.. math:: S = \begin{bmatrix}
r_0 & t & t \\
t & r_1 & i \\
t & i & r_1 \\
\end{bmatrix}
Args:
t: Transmission coefficient.
i: Leakage (isolation) coefficient.
r0: Reflection coefficient for the first port.
r1: Reflection coefficient for the remaining ports.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
Notes:
For multimode ports, a sequence of coefficients must be used, and
mixed-mode coefficients are 0. Dispersion can be included in the
model by setting the coefficients to an :class:`Interpolator` (with
multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
t: _ComplexCoeff | Interpolator = 2**-0.5,
i: _ComplexCoeff | Interpolator = 0,
r0: _ComplexCoeff | Interpolator = 0,
r1: _ComplexCoeff | Interpolator = 0,
ports: pft.annotate(Sequence[str], minItems=3, maxItems=3) | None = None,
) -> None:
super().__init__(
t=_ensure_correct_shape(t),
i=_ensure_correct_shape(i),
r0=_ensure_correct_shape(r0),
r1=_ensure_correct_shape(r1),
ports=ports,
)
if ports is not None and len(ports) != 3:
raise TypeError(
f"PowerSplitterModel can only be used on components with 3 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "y-splitter"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = width * 8
p1 = [(0, 0), (0.25 * length, 0), (0.75 * length, 0.75 * width), (length, 0.75 * width)]
p2 = [(x, -y) for x, y in p1]
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
polygons = boolean(
Path(p1[0], w, g).segment(p1[1:]), Path(p2[0], w, g).segment(p2[1:]), "+"
)
component.add(layer, *polygons)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 3
component.add_port(Port(p1[0], 0, port_spec), port_names[0])
component.add_port(Port(p2[-1], 180, port_spec), port_names[1])
component.add_port(Port(p1[-1], 180, port_spec), port_names[2])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 3:
raise RuntimeError(
f"PowerSplitterModel can only be used on components with 3 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in PowerSplitterModel match the {classification} "
f"port names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"PowerSplitterModel requires that all component ports have the same number of "
f"modes. Ports from '{component.name}' support different numbers of modes."
)
t = _sample(component.name, "t", p["t"], frequencies, num_modes)
i = _sample(component.name, "i", p["i"], frequencies, num_modes)
r0 = _sample(component.name, "r0", p["r0"], frequencies, num_modes)
r1 = _sample(component.name, "r1", p["r1"], frequencies, num_modes)
s = (
(r0, t, t),
(t, r1, i),
(t, i, r1),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
# Deprecated: kept for backwards compatibility with old phf files
[docs]
@classmethod
def from_bytes(cls, byte_repr: bytes) -> "PowerSplitterModel":
"""De-serialize this model."""
size = struct.calcsize("<B7Q")
version, *lengths = struct.unpack("<B7Q", byte_repr[:size])
if version != 0:
raise RuntimeError("Unsuported PowerSplitterModel version.")
coeffs = []
for length in lengths[:4]:
mem_io = io.BytesIO()
mem_io.write(byte_repr[size : size + length])
mem_io.seek(0)
coeffs.append(numpy.load(mem_io))
size += length
if all(length == 0 for length in lengths[4:]):
ports = None
else:
ports = []
for length in lengths[4:]:
ports.append(byte_repr[size : size + length].decode("utf8"))
size += length
return cls(*coeffs, ports)
[docs]
class PolarizationBeamSplitterModel(Model):
r"""Data model for a 3-port polarization beam splitter.
The S matrix, considering no mode mixing, is represented by:
.. math:: S = \begin{bmatrix}
r_0 & t_1 & t_2 \\
t_1 & r_1 & i \\
t_2 & i & r_2 \\
\end{bmatrix}
The defaults assume that the 3 ports support up to 2 modes. More modes
can be supported by extending the coefficients (see note below).
Args:
t1: Transmission coefficient to the first output port.
t2: Transmission coefficient to the second output port.
i: Leakage (isolation) coefficient between outputs.
r0: Reflection coefficient for the input port.
r1: Reflection coefficient for the first output port.
r2: Reflection coefficient for the second output port.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
Notes:
For multimode ports, a sequence of coefficients must be used, and
mixed-mode coefficients are 0. Dispersion can be included in the
model by setting the coefficients to an :class:`Interpolator` (with
multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
*,
t1: _ComplexCoeff = (1, 0),
t2: _ComplexCoeff = (0, 1),
i: _ComplexCoeff = (0, 0),
r0: _ComplexCoeff = (0, 0),
r1: _ComplexCoeff = (0, 0),
r2: _ComplexCoeff = (0, 0),
ports: pft.annotate(Sequence[str], minItems=3, maxItems=3) | None = None,
) -> None:
super().__init__(
t1=_ensure_correct_shape(t1),
t2=_ensure_correct_shape(t2),
i=_ensure_correct_shape(i),
r0=_ensure_correct_shape(r0),
r1=_ensure_correct_shape(r1),
r2=_ensure_correct_shape(r2),
ports=ports,
)
if ports is not None and len(ports) != 3:
raise TypeError(
f"PolarizationBeamSplitterModel can only be used on components with 3 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "pbs"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = width * 8
p1 = [(0, 0), (0.25 * length, 0), (0.75 * length, 1.5 * width), (length, 1.5 * width)]
p2 = [(0, 0), (length, 0)]
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
polygons = boolean(
Path(p1[0], w, g).segment(p1[1:]), Path(p2[0], w, g).segment(p2[1:]), "+"
)
component.add(layer, *polygons)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 3
component.add_port(Port(p1[0], 0, port_spec), port_names[0])
component.add_port(Port(p2[-1], 180, port_spec), port_names[1])
component.add_port(Port(p1[-1], 180, port_spec), port_names[2])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 3:
raise RuntimeError(
f"PolarizationBeamSplitterModel can only be used on components with 3 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in PolarizationBeamSplitterModel match the "
f"{classification} port names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"PolarizationBeamSplitterModel requires that all component ports have the same "
f"number of modes. Ports from '{component.name}' support different numbers of "
f"modes."
)
t1 = _sample(component.name, "t1", p["t1"], frequencies, num_modes)
t2 = _sample(component.name, "t2", p["t2"], frequencies, num_modes)
i = _sample(component.name, "i", p["i"], frequencies, num_modes)
r0 = _sample(component.name, "r0", p["r0"], frequencies, num_modes)
r1 = _sample(component.name, "r1", p["r1"], frequencies, num_modes)
r2 = _sample(component.name, "r2", p["r2"], frequencies, num_modes)
s = (
(r0, t1, t2),
(t1, r1, i),
(t2, i, r2),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
[docs]
class PolarizationSplitterRotatorModel(Model):
r"""Data model for a polarization splitter rotator.
Two types of PSR models are supported: a 4-port model that assumes a
2-mode input port and 2 single-mode outputs; and a 6-port model that
assumes all 3 ports support 2 modes.
The full 6-port S matrix is represented by the usual coefficients:
.. math:: S_{6p} = \begin{bmatrix}
s_{00} & s_{01} & \cdots & s_{05} \\
s_{01} & s_{11} & \cdots & s_{15} \\
\vdots & \vdots & \ddots & \vdots \\
s_{05} & s_{15} & \cdots & s_{55} \\
\end{bmatrix}
The 4-port version drops the coefficients related to the unused mode in
the output ports. Using ``output_mode = 0`` (default):
.. math:: S_{4p} = \begin{bmatrix}
s_{00} & s_{01} & s_{02} & s_{04} \\
s_{01} & s_{11} & s_{12} & s_{14} \\
s_{02} & s_{12} & s_{22} & s_{24} \\
s_{04} & s_{14} & s_{24} & s_{44} \\
\end{bmatrix}
and using ``output_mode = 1``:
.. math:: S'_{4p} = \begin{bmatrix}
s_{00} & s_{01} & s_{03} & s_{05} \\
s_{01} & s_{11} & s_{13} & s_{15} \\
s_{03} & s_{13} & s_{33} & s_{35} \\
s_{05} & s_{15} & s_{35} & s_{55} \\
\end{bmatrix}
Args:
s00: Reflection for first mode on input port.
s01: Inter-mode reflection for input port.
s02: Transmission for first mode on input port to first mode on
first output port.
s03: Transmission for first mode on input port to second mode on
first output port.
s04: Transmission for first mode on input port to first mode on
second output port.
s05: Transmission for first mode on input port to second mode on
second output port.
s11: Reflection for second mode on input port.
s12: Transmission for second mode on input port to first mode on
first output port.
s13: Transmission for second mode on input port to second mode on
first output port.
s14: Transmission for second mode on input port to first mode on
second output port.
s15: Transmission for second mode on input port to second mode on
second output port.
s22: Reflection for first mode on first output port.
s23: Inter-mode reflection for the first output port.
s24: Leakage (isolation) between the first mode on the first output
port and the first mode on the second output port.
s25: Leakage (isolation) between the first mode on the first output
port and the second mode on the second output port.
s33: Reflection for second mode on first output port.
s34: Leakage (isolation) between the second mode on the first output
port and the first mode on the second output port.
s35: Leakage (isolation) between the second mode on the first output
port and the second mode on the second output port.
s44: Reflection for first mode on second output port.
s45: Inter-mode reflection for the second output port.
s55: Reflection for second mode on second output port.
output_mode: Mode number used in output ports in the 4-port version.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
Notes:
Dispersion can be included in the model by setting the coefficients
to :class:`Interpolator` objects or to 1D arrays with the length of
the frequencies vector to be used in the computation.
"""
def __init__(
self,
*,
s00: _ComplexCoeff1D | Interpolator = 0,
s01: _ComplexCoeff1D | Interpolator = 0,
s02: _ComplexCoeff1D | Interpolator = 1,
s03: _ComplexCoeff1D | Interpolator = 0,
s04: _ComplexCoeff1D | Interpolator = 0,
s05: _ComplexCoeff1D | Interpolator = 0,
s11: _ComplexCoeff1D | Interpolator = 0,
s12: _ComplexCoeff1D | Interpolator = 0,
s13: _ComplexCoeff1D | Interpolator = 0,
s14: _ComplexCoeff1D | Interpolator = 1,
s15: _ComplexCoeff1D | Interpolator = 0,
s22: _ComplexCoeff1D | Interpolator = 0,
s23: _ComplexCoeff1D | Interpolator = 0,
s24: _ComplexCoeff1D | Interpolator = 0,
s25: _ComplexCoeff1D | Interpolator = 0,
s33: _ComplexCoeff1D | Interpolator = 0,
s34: _ComplexCoeff1D | Interpolator = 0,
s35: _ComplexCoeff1D | Interpolator = 0,
s44: _ComplexCoeff1D | Interpolator = 0,
s45: _ComplexCoeff1D | Interpolator = 0,
s55: _ComplexCoeff1D | Interpolator = 0,
output_mode: Literal[0, 1] = 0,
ports: pft.annotate(Sequence[str], minItems=3, maxItems=3) | None = None,
) -> None:
super().__init__(
s00=_ensure_correct_shape(s00, ndims=1),
s01=_ensure_correct_shape(s01, ndims=1),
s02=_ensure_correct_shape(s02, ndims=1),
s03=_ensure_correct_shape(s03, ndims=1),
s04=_ensure_correct_shape(s04, ndims=1),
s05=_ensure_correct_shape(s05, ndims=1),
s11=_ensure_correct_shape(s11, ndims=1),
s12=_ensure_correct_shape(s12, ndims=1),
s13=_ensure_correct_shape(s13, ndims=1),
s14=_ensure_correct_shape(s14, ndims=1),
s15=_ensure_correct_shape(s15, ndims=1),
s22=_ensure_correct_shape(s22, ndims=1),
s23=_ensure_correct_shape(s23, ndims=1),
s24=_ensure_correct_shape(s24, ndims=1),
s25=_ensure_correct_shape(s25, ndims=1),
s33=_ensure_correct_shape(s33, ndims=1),
s34=_ensure_correct_shape(s34, ndims=1),
s35=_ensure_correct_shape(s35, ndims=1),
s44=_ensure_correct_shape(s44, ndims=1),
s45=_ensure_correct_shape(s45, ndims=1),
s55=_ensure_correct_shape(s55, ndims=1),
output_mode=int(output_mode),
ports=ports,
)
if ports is not None and len(ports) != 3:
raise TypeError(
f"PolarizationSplitterRotatorModel can only be used on components with 3 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
output_port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
output_port_spec: Port specification used for the output ports in
the component. If ``None``, use the same as ``port_spec``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
# TODO: Add PSR icon
component.properties.__thumbnail__ = "y-splitter"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
if isinstance(output_port_spec, str):
name = output_port_spec
output_port_spec = component.technology.ports.get(output_port_spec)
if output_port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
elif output_port_spec is None:
output_port_spec = port_spec
width = max(port_spec.width, output_port_spec.width)
length = width * 8
p0 = [(0, 0), (0.5 * length, 0)]
p1 = [(0.5 * length, 0), (length, 0)]
p2 = [(0.25 * length, 1.5 * width), (0.75 * length, 1.5 * width), (length, 1.5 * width)]
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
component.add(layer, Path(p0[0], w, g).segment(p0[1:]))
profiles = output_port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
component.add(
layer,
Path(p1[0], w, g).segment(p1[1:]),
Path(p2[0], 0, g - 0.5 * width).segment(p2[1], w, g).segment(p2[2]),
)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 3
component.add_port(Port(p0[0], 0, port_spec), port_names[0])
component.add_port(Port(p1[-1], 180, output_port_spec), port_names[1])
component.add_port(Port(p2[-1], 180, output_port_spec), port_names[2])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 3:
raise RuntimeError(
f"PolarizationSplitterRotatorModel can only be used on components with 3 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
p = self.parametric_kwargs
names = p["ports"]
output_mode = p["output_mode"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in PolarizationSplitterRotatorModel match the "
f"{classification} port names in component '{component.name}'."
)
p0, p1, p2 = names
input_modes = component_ports[p0].num_modes
output_modes = component_ports[p1].num_modes
if not (
input_modes == 2
and output_modes in (1, 2)
and output_modes == component_ports[p2].num_modes
):
raise RuntimeError(
f"PolarizationSplitterRotatorModel requires 2 modes in the first port '{p0}', and "
f"the same number of modes (1 or 2) for ports '{p1}' and '{p2}'."
)
shape = (len(frequencies),)
s00 = p["s00"]
s00 = (
s00(frequencies)
if isinstance(s00, Interpolator)
else numpy.array(numpy.broadcast_to(s00, shape))
)
s01 = p["s01"]
s01 = (
s01(frequencies)
if isinstance(s01, Interpolator)
else numpy.array(numpy.broadcast_to(s01, shape))
)
s02 = p["s02"]
s02 = (
s02(frequencies)
if isinstance(s02, Interpolator)
else numpy.array(numpy.broadcast_to(s02, shape))
)
s03 = p["s03"]
s03 = (
s03(frequencies)
if isinstance(s03, Interpolator)
else numpy.array(numpy.broadcast_to(s03, shape))
)
s04 = p["s04"]
s04 = (
s04(frequencies)
if isinstance(s04, Interpolator)
else numpy.array(numpy.broadcast_to(s04, shape))
)
s05 = p["s05"]
s05 = (
s05(frequencies)
if isinstance(s05, Interpolator)
else numpy.array(numpy.broadcast_to(s05, shape))
)
s11 = p["s11"]
s11 = (
s11(frequencies)
if isinstance(s11, Interpolator)
else numpy.array(numpy.broadcast_to(s11, shape))
)
s12 = p["s12"]
s12 = (
s12(frequencies)
if isinstance(s12, Interpolator)
else numpy.array(numpy.broadcast_to(s12, shape))
)
s13 = p["s13"]
s13 = (
s13(frequencies)
if isinstance(s13, Interpolator)
else numpy.array(numpy.broadcast_to(s13, shape))
)
s14 = p["s14"]
s14 = (
s14(frequencies)
if isinstance(s14, Interpolator)
else numpy.array(numpy.broadcast_to(s14, shape))
)
s15 = p["s15"]
s15 = (
s15(frequencies)
if isinstance(s15, Interpolator)
else numpy.array(numpy.broadcast_to(s15, shape))
)
s22 = p["s22"]
s22 = (
s22(frequencies)
if isinstance(s22, Interpolator)
else numpy.array(numpy.broadcast_to(s22, shape))
)
s23 = p["s23"]
s23 = (
s23(frequencies)
if isinstance(s23, Interpolator)
else numpy.array(numpy.broadcast_to(s23, shape))
)
s24 = p["s24"]
s24 = (
s24(frequencies)
if isinstance(s24, Interpolator)
else numpy.array(numpy.broadcast_to(s24, shape))
)
s25 = p["s25"]
s25 = (
s25(frequencies)
if isinstance(s25, Interpolator)
else numpy.array(numpy.broadcast_to(s25, shape))
)
s33 = p["s33"]
s33 = (
s33(frequencies)
if isinstance(s33, Interpolator)
else numpy.array(numpy.broadcast_to(s33, shape))
)
s34 = p["s34"]
s34 = (
s34(frequencies)
if isinstance(s34, Interpolator)
else numpy.array(numpy.broadcast_to(s34, shape))
)
s35 = p["s35"]
s35 = (
s35(frequencies)
if isinstance(s35, Interpolator)
else numpy.array(numpy.broadcast_to(s35, shape))
)
s44 = p["s44"]
s44 = (
s44(frequencies)
if isinstance(s44, Interpolator)
else numpy.array(numpy.broadcast_to(s44, shape))
)
s45 = p["s45"]
s45 = (
s45(frequencies)
if isinstance(s45, Interpolator)
else numpy.array(numpy.broadcast_to(s45, shape))
)
s55 = p["s55"]
s55 = (
s55(frequencies)
if isinstance(s55, Interpolator)
else numpy.array(numpy.broadcast_to(s55, shape))
)
if output_modes == 2:
s = (
(s00, s01, s02, s03, s04, s05),
(s01, s11, s12, s13, s14, s15),
(s02, s12, s22, s23, s24, s25),
(s03, s13, s23, s33, s34, s35),
(s04, s14, s24, s34, s44, s45),
(s05, s15, s25, s35, s45, s55),
)
names = [f"{p0}@0", f"{p0}@1", f"{p1}@0", f"{p1}@1", f"{p2}@0", f"{p2}@1"]
else:
if output_mode == 0:
s = (
(s00, s01, s02, s04),
(s01, s11, s12, s14),
(s02, s12, s22, s24),
(s04, s14, s24, s44),
)
else:
s = (
(s00, s01, s03, s05),
(s01, s11, s13, s15),
(s03, s13, s33, s35),
(s05, s15, s35, s55),
)
names = [f"{p0}@0", f"{p0}@1", f"{p1}@{output_mode}", f"{p2}@{output_mode}"]
elements = {
(port_in, port_out): s[j][i]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
}
return SMatrix(frequencies, elements, component_ports)
[docs]
class DirectionalCouplerModel(Model):
r"""Data model for a 4-port directional coupler.
.. math:: S = \begin{bmatrix}
r & i & t' & c' \\
i & r & c' & t' \\
t' & c' & r & i \\
c' & t' & i & r \\
\end{bmatrix}
with coefficients:
.. math::
t' &= t e^{j \phi}
c' &= c e^{j \phi}
\phi &= \frac{2 \pi l_p}{c_0}
[n_\text{eff} f_0 + n_\text{group} (f - f_0)]
Args:
t: Transmission coefficient. If ``None``, it is calculated based on
the magnitude of the other coefficients and the phase of ``c``
plus 90°.
c: Coupling coefficient.
i: Leakage (isolation) coefficient.
r: Reflection coefficient.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
propagation_length: Propagation length :math:`l_p` for dispersion
modeling.
n_eff: Effective refractive index for dispersion modeling.
n_group: Group index. If ``None``, the value of ``n_eff`` is used.
reference_frequency: Reference frequency :math:`f_0` for dispersion
calculation. If ``None``, the central frequency is used.
Notes:
For multimode ports, a sequence of coefficients must be used, and
mixed-mode coefficients are 0. Dispersion can be included in the
model by setting the coefficients to an :class:`Interpolator` (with
multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
t: _ComplexCoeff | Interpolator | None = None,
c: _ComplexCoeff | Interpolator = -1j * 2**-0.5,
i: _ComplexCoeff | Interpolator = 0,
r: _ComplexCoeff | Interpolator = 0,
ports: pft.annotate(Sequence[str], minItems=4, maxItems=4) | None = None,
*,
propagation_length: pft.annotate(float, units="μm") = 0.0,
n_eff: _ComplexCoeff | Interpolator = 0.0,
n_group: _FloatCoeff | None = None,
reference_frequency: pft.Frequency | None = None,
) -> None:
super().__init__(
t=t,
c=_ensure_correct_shape(c),
i=_ensure_correct_shape(i),
r=_ensure_correct_shape(r),
ports=ports,
propagation_length=propagation_length,
n_eff=_ensure_correct_shape(n_eff),
n_group=n_group,
reference_frequency=reference_frequency,
)
if ports is not None and len(ports) != 4:
raise TypeError(
f"DirectionalCouplerModel can only be used on components with 4 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "dc"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = abs(self.parametric_kwargs["propagation_length"])
if length == 0.0:
length = width * 8
p1 = [
(0, -0.75 * width),
(0.25 * length, -0.75 * width),
(0.75 * length, 0.75 * width),
(length, 0.75 * width),
]
p2 = [(x, -y) for x, y in p1]
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
polygons = boolean(
Path(p1[0], w, g).segment(p1[1:]), Path(p2[0], w, g).segment(p2[1:]), "+"
)
component.add(layer, *polygons)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 4
component.add_port(Port(p1[0], 0, port_spec), port_names[0])
component.add_port(Port(p2[0], 0, port_spec), port_names[1])
component.add_port(Port(p2[-1], 180, port_spec), port_names[2])
component.add_port(Port(p1[-1], 180, port_spec), port_names[3])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 4:
raise RuntimeError(
f"DirectionalCouplerModel can only be used on components with 4 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in DirectionalCouplerModel match the "
f"{classification} port names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"DirectionalCouplerModel requires that all component ports have the same number "
f"of modes. Ports from '{component.name}' support different numbers of modes."
)
lp = _ensure_correct_shape(p["propagation_length"])
f0 = p["reference_frequency"]
if f0 is None:
f0 = 0.5 * (frequencies.min() + frequencies.max())
n_eff = _sample(component.name, "n_eff", p["n_eff"], frequencies, num_modes)
n_group = p["n_group"]
if n_group is None:
n_group = n_eff
else:
n_group = _ensure_correct_shape(n_group)
n_f = (n_eff - n_group) * f0 + n_group * frequencies
phase = numpy.exp(2j * numpy.pi * lp * n_f / C_0)
c = _sample(component.name, "c", p["c"], frequencies, num_modes) * phase
i = _sample(component.name, "i", p["i"], frequencies, num_modes)
r = _sample(component.name, "r", p["r"], frequencies, num_modes)
t = p["t"]
if t is None:
t_mag = numpy.sqrt(1 - numpy.abs(c) ** 2 - numpy.abs(i) ** 2 - numpy.abs(r) ** 2)
t = 1j * numpy.exp(1j * numpy.angle(c)) * t_mag
else:
t = (
_sample(component.name, "t", _ensure_correct_shape(t), frequencies, num_modes)
* phase
)
s = (
(r, i, t, c),
(i, r, c, t),
(t, c, r, i),
(c, t, i, r),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
# Deprecated: kept for backwards compatibility with old phf files
[docs]
@classmethod
def from_bytes(cls, byte_repr: bytes) -> "DirectionalCouplerModel":
"""De-serialize this model."""
size = struct.calcsize("<B8Q")
version, *lengths = struct.unpack("<B8Q", byte_repr[:size])
if version != 0:
raise RuntimeError("Unsuported DirectionalCouplerModel version.")
coeffs = []
for length in lengths[:4]:
mem_io = io.BytesIO()
mem_io.write(byte_repr[size : size + length])
mem_io.seek(0)
coeffs.append(numpy.load(mem_io))
size += length
if all(length == 0 for length in lengths[4:]):
ports = None
else:
ports = []
for length in lengths[4:]:
ports.append(byte_repr[size : size + length].decode("utf8"))
size += length
return cls(*coeffs, ports)
[docs]
class CrossingModel(Model):
r"""Data model for a 4-port waveguide crossing.
.. math:: S = \begin{bmatrix}
r & x & t & x \\
x & r & x & t \\
t & x & r & x \\
x & t & x & r \\
\end{bmatrix} e^{j \phi}
with dispersion modeled by:
.. math:: \phi = \frac{2 \pi l_p}{c_0}
[n_\text{eff} f_0 + n_\text{group} (f - f_0)]
Args:
t: Transmission coefficient.
x: Cross-coupling coefficient.
r: Reflection coefficient.
propagation_length: Propagation length :math:`l_p` for dispersion
modeling.
n_eff: Effective refractive index for dispersion modeling.
n_group: Group index. If ``None``, the value of ``n_eff`` is used.
reference_frequency: Reference frequency :math:`f_0` for dispersion
calculation. If ``None``, the central frequency is used.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
Notes:
For multimode ports, a sequence of coefficients must be used, and
mixed-mode coefficients are 0. Dispersion can be included in the
model by setting the coefficients to an :class:`Interpolator` (with
multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
*,
t: _ComplexCoeff | Interpolator = 1.0,
x: _ComplexCoeff | Interpolator = 0.0,
r: _ComplexCoeff | Interpolator = 0.0,
propagation_length: pft.annotate(float, units="μm") = 0.0,
n_eff: _ComplexCoeff | Interpolator = 0.0,
n_group: _FloatCoeff | None = None,
reference_frequency: pft.Frequency | None = None,
ports: pft.annotate(Sequence[str], minItems=4, maxItems=4) | None = None,
) -> None:
super().__init__(
t=_ensure_correct_shape(t),
x=_ensure_correct_shape(x),
r=_ensure_correct_shape(r),
ports=ports,
propagation_length=propagation_length,
n_eff=_ensure_correct_shape(n_eff),
n_group=n_group,
reference_frequency=reference_frequency,
)
if ports is not None and len(ports) != 4:
raise TypeError(
f"CrossingModel can only be used on components with 4 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "crossing"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = abs(self.parametric_kwargs["propagation_length"])
if length == 0.0:
length = width * 8
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
polygons = boolean(
Path((-length / 2, 0), w, g).segment((length / 2, 0)),
Path((0, -length / 2), w, g).segment((0, length / 2)),
"+",
)
component.add(layer, *polygons)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 4
component.add_port(Port((-length / 2, 0), 0, port_spec), port_names[0])
component.add_port(Port((0, -length / 2), 90, port_spec), port_names[1])
component.add_port(Port((length / 2, 0), 180, port_spec), port_names[2])
component.add_port(Port((0, length / 2), 270, port_spec), port_names[3])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 4:
raise RuntimeError(
f"DirectionalCouplerModel can only be used on components with 4 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports."
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in DirectionalCouplerModel match the "
f"{classification} port names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"DirectionalCouplerModel requires that all component ports have the same number "
f"of modes. Ports from '{component.name}' support different numbers of modes."
)
lp = _ensure_correct_shape(p["propagation_length"])
f0 = p["reference_frequency"]
if f0 is None:
f0 = 0.5 * (frequencies.min() + frequencies.max())
n_eff = _sample(component.name, "n_eff", p["n_eff"], frequencies, num_modes)
n_group = p["n_group"]
if n_group is None:
n_group = n_eff
else:
n_group = _ensure_correct_shape(n_group)
n_f = (n_eff - n_group) * f0 + n_group * frequencies
phase = numpy.exp(2j * numpy.pi * lp * n_f / C_0)
t = _sample(component.name, "t", p["t"], frequencies, num_modes) * phase
x = _sample(component.name, "x", p["x"], frequencies, num_modes) * phase
r = _sample(component.name, "r", p["r"], frequencies, num_modes) * phase
s = (
(r, x, t, x),
(x, r, x, t),
(t, x, r, x),
(x, t, x, r),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
def _port_with_x_section(port_name, component):
port = component.ports[port_name].copy(True)
direction = int((port.input_direction + 45) // 90) % 4
angle = port.input_direction - 90 * direction
axis = "x" if direction % 2 == 0 else "y"
x_length = port.spec.width + 2 * config.tolerance
x_center = port.center.copy()
x_center[direction % 2] += config.grid * 2 * (1 - 2 * (direction // 2))
inner = Component(technology=component.technology).add(Reference(component, -port.center))
x_comp = Component(technology=component.technology).add(Reference(inner, port.center, -angle))
port.spec.path_profiles = x_comp.slice_profile(axis, x_center, x_length)
return port
class _WaveguideModelRunner:
def __init__(self, runner, free_space_phase, frequencies, ports) -> None:
self.runner = runner
self.free_space_phase = free_space_phase
self.frequencies = frequencies
self.ports = ports
self._s_matrix = None
@property
def status(self):
return self.runner.status
@property
def s_matrix(self):
if self._s_matrix is None:
data = self.runner.data
num_modes = next(iter(self.ports.values())).num_modes
n_complex = data.n_complex.values.T
s = numpy.exp(1j * self.free_space_phase * n_complex)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[mode]
for port_in in self.ports
for port_out in self.ports
for mode in range(num_modes)
if port_in != port_out
}
self._s_matrix = SMatrix(self.frequencies, elements, self.ports)
return self._s_matrix
[docs]
class WaveguideModel(Model):
r"""Data model for straight waveguides.
The component is expected to have 2 ports with identical profiles. The S
matrix is zero for all reflection or mixed-mode coefficients. Same-mode
transmission coefficients are modeled by:
.. math:: S_{jk} = \exp(i 2 \pi f n_c L / c₀)
with :math:`n_c` the complex effective index for the port profile modes,
and :math:`L` the waveguide length.
Args:
n_complex: Waveguide complex effective index. For multimode models,
a sequence of indices must be provided, one for each mode. If set
to ``None``, automatic computation is performed by mode-solving
the first component port. If desired, the port specification of
the component port can be overridden by setting ``n_complex`` to
``"cross-section"`` (uses :func:`Component.slice_profile`) or to
a :class:`PortSpec` object.
length: Physical length of the waveguide. If not provided, the
length is measured by :func:`route_length` or ports distance.
mesh_refinement: Minimal number of mesh elements per wavelength used
for mode solving.
verbose: Flag setting the verbosity of mode solver runs.
Note:
Dispersion can be included in the model by setting ``n_complex`` to
an :class:`Interpolator` (with multiple values for multimode ports),
or a 2D array with shape (M, N), in which M is the number of modes
in the waveguide, and N the length of the frequency sequence used in
the S matrix computation.
See also:
`Mach-Zehnder Interferometer
<../examples/MZI.ipynb#Semi-Analytic-Design-Exploration>`__
"""
def __init__(
self,
n_complex: _ComplexCoeff | Interpolator | PortSpec | Literal["cross-section"] | None = None,
length: pft.Coordinate | None = None,
mesh_refinement: pft.PositiveFloat | None = None,
verbose: bool = True,
) -> None:
super().__init__(
n_complex=n_complex,
length=length,
mesh_refinement=mesh_refinement,
verbose=verbose,
)
self.n_complex = (
_ensure_correct_shape(n_complex)
if self._classify_n_complex(n_complex) in (numpy.ndarray, Interpolator)
else n_complex
)
self.length = length
self.mesh_refinement = mesh_refinement
self.verbose = verbose
@classmethod
def _classify_n_complex(cls, n_complex):
if n_complex is None:
return None
elif isinstance(n_complex, str):
if n_complex == "cross-section":
return str
raise ValueError(
f"'n_complex' must be a scalar, array, PortSpec object, or the string "
f"'cross-section'. The string {n_complex!r} is not valid."
)
elif isinstance(n_complex, PortSpec):
return PortSpec
elif isinstance(n_complex, Interpolator):
return Interpolator
else:
return numpy.ndarray
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "wg"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = self.length if self.length and self.length > 0 else width * 8
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
component.add(layer, Path((0, 0), w, g).segment((length, 0)))
_add_bb_text(component, width)
component.add_port((Port((0, 0), 0, port_spec), Port((length, 0), 180, port_spec)))
component.add_model(self, model_name)
return component
[docs]
@cache_s_matrix
def start(
self,
component: Component,
frequencies: Sequence[float],
verbose: bool | None = None,
cost_estimation: bool = False,
**kwargs: Any,
) -> SMatrix | _WaveguideModelRunner:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
verbose: If set, overrides the model's `verbose` attribute.
cost_estimation: If set, simulations are uploaded, but not
executed. S matrix may *not* be computed.
**kwargs: Unused.
Returns:
Result object with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 2:
raise RuntimeError(
f"WaveguideModel can only be used on components with 2 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports.",
)
port_names = sorted(component_ports)
port0 = component_ports[port_names[0]]
port1 = component_ports[port_names[1]]
if not isinstance(port0, Port) or not isinstance(port1, Port):
raise RuntimeError(
"WaveguideModel can only be used on components with planar ports (Port instances)."
)
if not port0.can_connect_to(port1):
raise RuntimeError(
"WaveguideModel can only be used on components with 2 ports with matching path "
"profiles."
)
length = self.length
if length is None:
length = 0
for _, _, layer in port0.spec.path_profiles_list():
length = max(length, route_length(component, layer))
if length <= 0:
length = numpy.sqrt(numpy.sum((port0.center - port1.center) ** 2))
frequencies = numpy.array(frequencies, dtype=float, ndmin=1)
if verbose is None:
verbose = self.verbose
n_type = self._classify_n_complex(self.n_complex)
if n_type in (numpy.ndarray, Interpolator):
num_modes = port0.num_modes
coeff = (2.0j * numpy.pi / C_0) * _sample(
component.name, "n_complex", self.n_complex, frequencies, num_modes
)
t = numpy.exp(coeff * length * frequencies)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): t[mode]
for port_in, port_out in [port_names, (port_names[1], port_names[0])]
for mode in range(num_modes)
}
return SMatrix(frequencies, elements, component_ports)
free_space_phase = 2.0 * numpy.pi / C_0 * length * frequencies
for port in component_ports.values():
if port.spec.polarization != "":
port.spec = port.spec.copy()
port.spec.polarization = ""
port.spec.num_modes += port.spec.added_solver_modes
port.spec.added_solver_modes = 0
if n_type is str:
ms_port = _port_with_x_section(port_names[0], component)
else:
ms_port = port0.copy(True)
if n_type is PortSpec:
ms_port.spec = libcopy.deepcopy(self.n_complex)
runner = _ModeSolverRunner(
ms_port,
frequencies,
self.mesh_refinement,
component.technology,
cost_estimation=cost_estimation,
verbose=verbose,
)
return _WaveguideModelRunner(runner, free_space_phase, frequencies, component_ports)
# Deprecated: kept for backwards compatibility with old phf files
[docs]
@classmethod
def from_bytes(cls, byte_repr: bytes) -> "WaveguideModel":
"""De-serialize this model."""
version = byte_repr[0]
if version == 2:
return cls(**dict(_from_bytes(byte_repr[1:])))
elif version == 1:
head_len = struct.calcsize("<BB2d")
flags, length, mesh_refinement = struct.unpack("<B2d", byte_repr[1:head_len])
verbose = (flags & 0x01) > 0
lenght_is_none = (flags & 0x02) > 0
n_type = {0x00: None, 0x04: str, 0x08: PortSpec, 0x0C: numpy.ndarray}.get(flags & 0x0C)
elif version == 0:
head_len = struct.calcsize("<B3?2d")
n_complex_is_none, lenght_is_none, verbose, length, mesh_refinement = struct.unpack(
"<3?2d", byte_repr[1:head_len]
)
n_type = None if n_complex_is_none else numpy.ndarray
else:
raise RuntimeError(
"This WaveguideModel seems to have been created by a more recent version of "
"PhotonForge and it is not supported by the this version."
)
if mesh_refinement <= 0:
mesh_refinement = None
if lenght_is_none:
length = None
n_complex = None
if n_type is PortSpec:
kwds = json.loads(zlib.decompress(byte_repr[head_len:]).decode("utf-8"))
profiles = kwds["path_profiles"]
if isinstance(profiles, dict):
kwds["path_profiles"] = {
k: (v["width"], v["offset"], v["layer"]) for k, v in profiles.items()
}
else:
kwds["path_profiles"] = [(v["width"], v["offset"], v["layer"]) for v in profiles]
if "electrical_spec" in kwds:
# The 1e-5 factor is to fix a bug that existed in the json conversion
kwds.update(
{k: numpy.array(v) * 1e-5 for k, v in kwds.pop("electrical_spec").items()}
)
n_complex = PortSpec(**kwds)
elif n_type is str:
n_complex = "cross-section"
elif n_type is numpy.ndarray:
mem_io = io.BytesIO()
mem_io.write(byte_repr[head_len:])
mem_io.seek(0)
n_complex = numpy.load(mem_io)
if version == 0:
# Version 0 stored the transformed coefficient
n_complex *= -0.5j / numpy.pi * C_0
return cls(n_complex, length, mesh_refinement, verbose)
def _waveguide_transmission(
frequencies,
length,
n_eff,
n_group,
dispersion,
dispersion_slope,
reference_frequency,
propagation_loss,
extra_loss,
dn_dT,
dL_dT,
temperature,
reference_temperature,
voltage,
v_piL,
dloss_dv,
dloss_dv2,
):
if reference_frequency is None:
reference_frequency = 0.5 * (frequencies.min() + frequencies.max())
if n_group is None:
n_group = n_eff
else:
n_group = _ensure_correct_shape(n_group)
dT = temperature - reference_temperature
n_eff = n_eff + dn_dT * dT
propagation_loss = propagation_loss + dL_dT * dT
total_loss = extra_loss + length * (
propagation_loss + voltage * (dloss_dv + voltage * dloss_dv2)
)
w0 = 2 * numpy.pi * reference_frequency
lda0 = C_0 / reference_frequency
lda0_w0 = lda0 / w0
beta0 = w0 * n_eff / C_0
beta1 = n_group / C_0
beta2 = -lda0_w0 * dispersion
beta3 = lda0_w0**2 * (dispersion_slope + 2 * dispersion / lda0)
dw = 2 * numpy.pi * (frequencies - reference_frequency)
beta = beta0 + dw * (beta1 + dw * (beta2 / 2 + dw * beta3 / 6))
if v_piL is None:
beta_eo = 0.0
else:
beta_eo = numpy.pi * voltage / v_piL
t = 10 ** (total_loss / -20) * numpy.exp(1j * (beta + beta_eo) * length)
return t
[docs]
class AnalyticWaveguideModel(Model):
r"""Analytic model for waveguides, bends, and EO phase-shifters.
This model for 2-port components includes dispersion and temperature
sensitivity for single- and multi-mode waveguides. For each mode, the
transmission between ports at frequency :math:`f` is:
.. math::
S_{12} &= S_{21} = 10^{-\frac{L}{20}} e^{j (\beta \ell + \phi_{eo})}
L &= L_0 + \ell \left(L_p + \frac{{\rm d}L_p}{{\rm d}V} V
+ \frac{{\rm d}^2L_p}{{\rm d}V^2} V^2 \right)
\beta &= \beta_0 + \beta_1 \Delta\omega
+ \frac{\beta_2}{2} \Delta\omega^2
+ \frac{\beta_3}{6} \Delta\omega^3
\beta_0 &= \frac{\omega_0}{c_0} n_\text{eff}
\beta_1 &= \frac{n_\text{group}}{c_0}
\beta_2 &= -\frac{\lambda_0}{\omega_0} D
\beta_3 &= \left(\frac{\lambda_0}{\omega_0}\right)^2
\left(S + \frac{2}{\lambda_0} D\right)
\phi_{eo} &= \frac{\pi V \ell}{V_{\pi L}}
in which :math:`\lambda_0 = c_0 f_0^{-1}`, :math:`\omega_0 = 2 \pi f_0`,
:math:`\Delta\omega = 2 \pi (f - f_0)`, and the temperature dependence
is taken into account through:
.. math::
n_\text{eff}(T) &= n_\text{eff}(T_0)
+ \frac{{\rm d}n_\text{eff}}{{\rm d}T} (T - T_0)
L_p(T) &= L_p(T_0) + \frac{{\rm d}L_p}{{\rm d}T} (T - T_0)
Args:
n_eff: Effective refractive index (loss can be included here by
using complex values).
length: Length :math:`\ell` of the waveguide. If not provided, the
length is measured by :func:`route_length` or ports distance.
propagation_loss: Propagation loss :math:`L_p`.
extra_loss: Length-independent loss :math:`L_0`. This can be used,
for example, to model bending losses.
n_group: Group index. If ``None``, the value of ``n_eff`` is used.
dispersion: Chromatic dispersion coefficient :math:`D`.
dispersion_slope: Chromatic dispersion slope :math:`S`.
reference_frequency: Reference frequency :math:`f_0` for dispersion
coefficients. If ``None``, the central frequency is used.
dn_dT: Temperature sensitivity for ``n_eff``.
dL_dT: Temperature sensitivity for ``propagation_loss``.
temperature: Operating temperature :math:`T`.
reference_temperature: Reference temperature :math:`T_0`.
voltage: Operating voltage :math:`V`.
v_piL: Electro-optic phase coefficient :math:`V_{\pi L}`.
dloss_dv: Linear voltage-dependent propagation loss coefficient.
dloss_dv2: Quadratic voltage-dependent propagation loss coefficient.
"""
def __init__(
self,
*,
n_eff: complex | Sequence[complex] | Interpolator,
length: pft.Coordinate | None = None,
propagation_loss: pft.PropagationLoss | Sequence[pft.PropagationLoss] = 0.0,
extra_loss: pft.Loss | Sequence[pft.Loss] = 0.0,
n_group: float | Sequence[float] | None = None,
dispersion: pft.Dispersion | Sequence[pft.Dispersion] = 0.0,
dispersion_slope: pft.DispersionSlope | Sequence[pft.DispersionSlope] = 0.0,
reference_frequency: pft.Frequency | None = None,
dn_dT: pft.annotate(complex | Sequence[complex], label="dn/dT", units="1/K") = 0.0,
dL_dT: pft.annotate(float | Sequence[float], label="dL/dT", units="dB/μm/K") = 0.0,
temperature: pft.Temperature = 293.0,
reference_temperature: pft.Temperature = 293.0,
voltage: pft.Voltage = 0.0,
v_piL: pft.annotate(float, label="VπL", units="V·μm") | None = None,
dloss_dv: pft.annotate(float, label="dL/dV", units="dB/μm/V") = 0,
dloss_dv2: pft.annotate(float, label="d²L/dV²", units="dB/μm/V²") = 0,
):
super().__init__(
length=length,
n_eff=_ensure_correct_shape(n_eff),
propagation_loss=_ensure_correct_shape(propagation_loss),
extra_loss=_ensure_correct_shape(extra_loss),
n_group=n_group,
dispersion=_ensure_correct_shape(dispersion),
dispersion_slope=_ensure_correct_shape(dispersion_slope),
reference_frequency=reference_frequency,
dn_dT=_ensure_correct_shape(dn_dT),
dL_dT=_ensure_correct_shape(dL_dT),
temperature=float(temperature),
reference_temperature=float(reference_temperature),
voltage=float(voltage),
v_piL=v_piL,
dloss_dv=dloss_dv,
dloss_dv2=dloss_dv2,
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
p = self.parametric_kwargs
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "wg" if p["v_piL"] is None else "eo-ps"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = p["length"]
if length is None or length <= 0:
length = width * 8
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
component.add(layer, Path((0, 0), w, g).segment((length, 0)))
_add_bb_text(component, width)
component.add_port((Port((0, 0), 0, port_spec), Port((length, 0), 180, port_spec)))
component.add_model(self, model_name)
return component
[docs]
def start(
self,
component: Component,
frequencies: Sequence[pft.Frequency],
temperature: pft.Temperature | None = None,
voltage: pft.Voltage | None = None,
**kwargs,
) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
temperature: Operating temperature override.
voltage: Operating voltage override.
**kwargs: Unused.
Returns:
Result object with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 2:
raise RuntimeError(
f"AnalyticWaveguideModel can only be used on components with 2 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports.",
)
port_names = sorted(component_ports)
port0 = component_ports[port_names[0]]
port1 = component_ports[port_names[1]]
if port0.num_modes != port1.num_modes:
raise RuntimeError(
f"AnalyticWaveguideModel requires that all component ports have the same number of "
f"modes. Ports from '{component.name}' support different numbers of modes."
)
frequencies = numpy.asarray(frequencies)
p = self.parametric_kwargs
length = p["length"]
if length is None:
length = 0
for _, _, layer in port0.spec.path_profiles_list():
length = max(length, route_length(component, layer))
if length <= 0:
length = numpy.sqrt(numpy.sum((port0.center - port1.center) ** 2))
if temperature is None:
temperature = p["temperature"]
if voltage is None:
voltage = p["voltage"]
n_eff = _sample(component.name, "n_eff", p["n_eff"], frequencies, port0.num_modes)
t = _waveguide_transmission(
frequencies,
length,
n_eff,
p["n_group"],
p["dispersion"],
p["dispersion_slope"],
p["reference_frequency"],
p["propagation_loss"],
p["extra_loss"],
p["dn_dT"],
p["dL_dT"],
temperature,
p["reference_temperature"],
voltage,
p["v_piL"],
p["dloss_dv"],
p["dloss_dv2"],
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): t[mode]
for port_in, port_out in [port_names, (port_names[1], port_names[0])]
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
[docs]
class AnalyticDirectionalCouplerModel(Model):
r"""Analytic model for a 4-port directional coupler.
The S matrix for the directional coupler for each mode is given by:
.. math:: S = \begin{bmatrix}
r & i & t & c \\
i & r & c & t \\
t & c & r & i \\
c & t & i & r \\
\end{bmatrix}
with coefficients:
.. math::
t &= \sqrt{1 - c_r} A e^{j \phi}
c &= \sqrt{c_r} A e^{j (\phi + \Delta\phi)}
c_r &= \sin^2\left(\frac{\pi l_i}{2 l_c}\right)
A &= 10^{-\frac{L_\text{dB}}{20}}
\phi &= \frac{2 \pi l_p}{c_0}
[n_\text{eff} f_0 + n_\text{group} (f - f_0)]
Args:
interaction_length: Interaction length :math:`l_i`.
coupling_length: Beat length :math:`l_c`.
propagation_length: Propagation length :math:`l_p`. If ``None``, the
value of ``interaction_length`` is used.
cross_phase: Cross-port phase shift :math:`\Delta\phi`.
insertion_loss: Insertion loss :math:`L_\text{dB}`.
isolation: Leakage (isolation) coefficient :math:`i`.
reflection: Reflection coefficient :math:`r`.
n_eff: Effective refractive index.
n_group: Group index. If ``None``, the value of ``n_eff`` is used.
reference_frequency: Reference frequency :math:`f_0` for dispersion
calculation. If ``None``, the frequency average is used.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
Notes:
For multimode ports, a sequence of coefficients must be used, and
mixed-mode coefficients are 0. Dispersion can be included in the
model by setting the coefficients to an :class:`Interpolator` (with
multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
*,
interaction_length: float,
coupling_length: pft.annotate(_FloatCoeff, units="μm"),
propagation_length: pft.annotate(_FloatCoeff, units="μm") | None = None,
cross_phase: pft.annotate(_FloatCoeff, units="°") = -90,
insertion_loss: pft.annotate(_ComplexCoeff, units="dB") = 0.0,
isolation: _ComplexCoeff | Interpolator = 0.0,
reflection: _ComplexCoeff | Interpolator = 0.0,
n_eff: _ComplexCoeff | Interpolator = 0.0,
n_group: _FloatCoeff | None = None,
reference_frequency: pft.Frequency | None = None,
ports: pft.annotate(Sequence[str], minItems=4, maxItems=4) | None = None,
) -> None:
super().__init__(
interaction_length=float(interaction_length),
coupling_length=_ensure_correct_shape(coupling_length),
propagation_length=propagation_length,
cross_phase=_ensure_correct_shape(cross_phase),
insertion_loss=_ensure_correct_shape(insertion_loss),
isolation=_ensure_correct_shape(isolation),
reflection=_ensure_correct_shape(reflection),
n_eff=_ensure_correct_shape(n_eff),
n_group=n_group,
reference_frequency=reference_frequency,
ports=ports,
)
if ports is not None and len(ports) != 4:
raise TypeError(
f"AnalyticDirectionalCouplerModel can only be used on components with 4 ports. "
f"Argument 'ports' has length {len(ports)}."
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "dc"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
length = self.parametric_kwargs["interaction_length"]
if length <= 0:
length = width * 8
p1 = [
(0, -0.75 * width),
(0.25 * length, -0.75 * width),
(0.75 * length, 0.75 * width),
(length, 0.75 * width),
]
p2 = [(x, -y) for x, y in p1]
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
polygons = boolean(
Path(p1[0], w, g).segment(p1[1:]), Path(p2[0], w, g).segment(p2[1:]), "+"
)
component.add(layer, *polygons)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 4
component.add_port(Port(p1[0], 0, port_spec), port_names[0])
component.add_port(Port(p2[0], 0, port_spec), port_names[1])
component.add_port(Port(p2[-1], 180, port_spec), port_names[2])
component.add_port(Port(p1[-1], 180, port_spec), port_names[3])
component.add_model(self, model_name)
return component
[docs]
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
**kwargs: Unused.
Returns:
Model result with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 4:
raise RuntimeError(
f"AnalyticDirectionalCouplerModel can only be used on components with 4 ports. "
f"'{component.name}' has {len(component_ports)} {classification} ports.",
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in AnalyticDirectionalCouplerModel match the "
f"{classification} port names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"AnalyticDirectionalCouplerModel requires that all ports have the same number of "
f"modes. Ports from '{component.name}' support different numbers of modes."
)
frequencies = numpy.asarray(frequencies)
li = p["interaction_length"]
lc = p["coupling_length"]
lp = p["propagation_length"]
if lp is None:
lp = li
else:
lp = _ensure_correct_shape(lp)
f0 = p["reference_frequency"]
if f0 is None:
f0 = frequencies.mean()
n_eff = _sample(component.name, "n_eff", p["n_eff"], frequencies, num_modes)
n_group = p["n_group"]
if n_group is None:
n_group = n_eff
else:
n_group = _ensure_correct_shape(n_group)
phi = 2 * numpy.pi * lp * ((n_eff - n_group) * f0 + n_group * frequencies) / C_0
cr = numpy.sin((numpy.pi * li) / (2 * lc)) ** 2
a = 10 ** (-p["insertion_loss"] / 20)
t = a * numpy.sqrt(1 - cr) * numpy.exp(1j * phi)
c = a * numpy.sqrt(cr) * numpy.exp(1j * (phi + p["cross_phase"] / 180 * numpy.pi))
i = _sample(component.name, "isolation", p["isolation"], frequencies, num_modes)
r = _sample(component.name, "reflection", p["reflection"], frequencies, num_modes)
s = (
(r, i, t, c),
(i, r, c, t),
(t, c, r, i),
(c, t, i, r),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
}
return SMatrix(frequencies, elements, component_ports)
[docs]
class AnalyticMZIModel(Model):
r"""Analytic model for a 4-port Mach-Zehnder interferometer.
The S matrix for the MZI for each mode is given by:
.. math::
S_{31} &= \tau_1 t_1 \tau_2 + \kappa_1 t_2 \kappa_2
S_{41} &= \tau_1 t_1 \kappa_2 + \kappa_1 t_2 \tau_2
S_{32} &= \kappa_1 t_1 \tau_2 + \tau_1 t_2 \kappa_2
S_{42} &= \kappa_1 t_1 \kappa_2 + \tau_1 t_2 \tau_2
with remaining coefficients zero and transmissions $t_1$ and $t_2$
calculated through an :class:`AnalyticWaveguideModel`.
Args:
n_eff1: Effective refractive index for the first arm (loss can be
included here by using complex values).
n_eff2: Effective refractive index for the second arm. If ``None``,
defaults to ``n_eff1``.
length1: Length of the first arm.
length2: Length of the second arm. If ``None``, defaults to
``length1``.
tau1: Transmission coefficient for the first coupler. If ``None``,
it is calculated based on the magnitude of ``kappa1`` and its
phase plus 90°.
tau2: Transmission coefficient for the second coupler. If ``None``,
it is calculated based on the magnitude of ``kappa2`` and its
phase plus 90°.
kappa1: Coupling coefficient for the first coupler.
kappa2: Coupling coefficient for the second coupler.
propagation_loss1: Propagation loss for the first arm.
propagation_loss2: Propagation loss for the second arm.
extra_loss1: Length-independent loss for the first arm.
extra_loss2: Length-independent loss for the second arm.
n_group1: Group index for the first arm.
n_group2: Group index for the second arm.
dispersion1: Chromatic dispersion coefficient for the first arm.
dispersion2: Chromatic dispersion coefficient for the second arm.
dispersion_slope1: Chromatic dispersion slope for the first arm.
dispersion_slope2: Chromatic dispersion slope for the second arm.
reference_frequency: Reference frequency for the dispersion
coefficients. If ``None``, the central frequency is used.
dn1_dT: Temperature sensitivity for ``n_eff1``.
dn2_dT: Temperature sensitivity for ``n_eff2``.
dL1_dT: Temperature sensitivity for ``propagation_loss1``.
dL2_dT: Temperature sensitivity for ``propagation_loss2``.
temperature1: Operating temperature for the first arm.
temperature2: Operating temperature for the second arm.
reference_temperature: Reference temperature.
voltage1: Operating voltage for the first arm.
voltage2: Operating voltage for the second arm.
v_piL1: Electro-optic phase coefficient for the first arm.
v_piL2: Electro-optic phase coefficient for the second arm.
dloss_dv_1: Linear voltage-dependent propagation loss coefficient
for the first arm.
dloss_dv_2: Linear voltage-dependent propagation loss coefficient
for the second arm.
dloss_dv2_1: Quadratic voltage-dependent propagation loss
coefficient for the first arm.
dloss_dv2_2: Quadratic voltage-dependent propagation loss
coefficient for the second arm.
ports: List of port names. If not set, the *sorted* list of port
names from the component is used.
Notes:
For multimode ports, mixed-mode coefficients are 0. Parameters can
be specified per mode by using sequences of values. Dispersion for
``tau1``, ``tau2``, ``kappa1``, and ``kappa2`` can also be manually
included by setting the coefficients to an :class:`Interpolator`
(with multiple values for multimode ports), or a 2D array with shape
(M, N), in which M is the number of modes, and N the length of the
frequency sequence used in the S matrix computation.
"""
def __init__(
self,
*,
n_eff1: _ComplexCoeff | Interpolator,
n_eff2: _ComplexCoeff | Interpolator | None = None,
length1: pft.Coordinate,
length2: pft.Coordinate | None = None,
tau1: _ComplexCoeff | Interpolator | None = None,
tau2: _ComplexCoeff | Interpolator | None = None,
kappa1: _ComplexCoeff | Interpolator = -1j * 2**-0.5,
kappa2: _ComplexCoeff | Interpolator = -1j * 2**-0.5,
propagation_loss1: pft.PropagationLoss | Sequence[pft.PropagationLoss] = 0.0,
propagation_loss2: pft.PropagationLoss | Sequence[pft.PropagationLoss] = 0.0,
extra_loss1: pft.Loss | Sequence[pft.Loss] = 0.0,
extra_loss2: pft.Loss | Sequence[pft.Loss] = 0.0,
n_group1: float | Sequence[float] | None = None,
n_group2: float | Sequence[float] | None = None,
dispersion1: pft.Dispersion | Sequence[pft.Dispersion] = 0.0,
dispersion2: pft.Dispersion | Sequence[pft.Dispersion] = 0.0,
dispersion_slope1: pft.DispersionSlope | Sequence[pft.DispersionSlope] = 0.0,
dispersion_slope2: pft.DispersionSlope | Sequence[pft.DispersionSlope] = 0.0,
reference_frequency: pft.Frequency | None = None,
dn1_dT: pft.annotate(complex | Sequence[complex], label="dn1/dT", units="1/K") = 0.0,
dn2_dT: pft.annotate(complex | Sequence[complex], label="dn2/dT", units="1/K") = 0.0,
dL1_dT: pft.annotate(float | Sequence[float], label="dL1/dT", units="dB/μm/K") = 0.0,
dL2_dT: pft.annotate(float | Sequence[float], label="dL2/dT", units="dB/μm/K") = 0.0,
temperature1: pft.Temperature = 293.0,
temperature2: pft.Temperature = 293.0,
reference_temperature: pft.Temperature = 293.0,
voltage1: pft.Voltage = 0.0,
voltage2: pft.Voltage = 0.0,
v_piL1: pft.annotate(float, label="VπL1", units="V·μm") | None = None,
v_piL2: pft.annotate(float, label="VπL2", units="V·μm") | None = None,
dloss_dv_1: pft.annotate(float, label="dL/dV", units="dB/μm/V") = 0,
dloss_dv_2: pft.annotate(float, label="dL/dV", units="dB/μm/V") = 0,
dloss_dv2_1: pft.annotate(float, label="d²L/dV²", units="dB/μm/V²") = 0,
dloss_dv2_2: pft.annotate(float, label="d²L/dV²", units="dB/μm/V²") = 0,
ports: Sequence[str] | None = None,
) -> None:
super().__init__(
n_eff1=_ensure_correct_shape(n_eff1),
n_eff2=n_eff2,
length1=float(length1),
length2=length2,
tau1=tau1,
tau2=tau2,
kappa1=_ensure_correct_shape(kappa1),
kappa2=_ensure_correct_shape(kappa2),
propagation_loss1=_ensure_correct_shape(propagation_loss1),
propagation_loss2=_ensure_correct_shape(propagation_loss2),
extra_loss1=_ensure_correct_shape(extra_loss1),
extra_loss2=_ensure_correct_shape(extra_loss2),
n_group1=n_group1,
n_group2=n_group2,
dispersion1=_ensure_correct_shape(dispersion1),
dispersion2=_ensure_correct_shape(dispersion2),
dispersion_slope1=_ensure_correct_shape(dispersion_slope1),
dispersion_slope2=_ensure_correct_shape(dispersion_slope2),
reference_frequency=reference_frequency,
dn1_dT=_ensure_correct_shape(dn1_dT),
dn2_dT=_ensure_correct_shape(dn2_dT),
dL1_dT=_ensure_correct_shape(dL1_dT),
dL2_dT=_ensure_correct_shape(dL2_dT),
temperature1=float(temperature1),
temperature2=float(temperature2),
reference_temperature=float(reference_temperature),
voltage1=float(voltage1),
voltage2=float(voltage2),
v_piL1=v_piL1,
v_piL2=v_piL2,
dloss_dv_1=dloss_dv_1,
dloss_dv_2=dloss_dv_2,
dloss_dv2_1=dloss_dv2_1,
dloss_dv2_2=dloss_dv2_2,
ports=ports,
)
[docs]
def black_box_component(
self,
port_spec: str | PortSpec | None = None,
technology: Technology | None = None,
name: str | None = None,
) -> Component:
"""Create a black-box component using this model for testing.
Args:
port_spec: Port specification used in the component. If ``None``,
look for ``"port_spec"`` in :attr:`config.default_kwargs`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name. If ``None`` a default is used.
Returns:
Component with ports and model.
"""
p = self.parametric_kwargs
model_name = self.__class__.__name__[:-5]
component = Component(f"BB{model_name}" if name is None else name, technology=technology)
component.properties.__thumbnail__ = "mzm"
if port_spec is None:
port_spec = config.default_kwargs.get("port_spec")
if port_spec is None:
raise RuntimeError("Missing argument 'port_spec'.")
if isinstance(port_spec, str):
name = port_spec
port_spec = component.technology.ports.get(port_spec)
if port_spec is None:
raise RuntimeError(f"Port spec '{name}' not found in component's technology.")
width = port_spec.width
dc_length = width * 8
length1 = p["length1"]
if length1 <= 0:
length1 = dc_length
length2 = p["length2"] or length1
if length2 <= 0:
length2 = dc_length
if max(length1, length2) > 3 * dc_length:
length1 *= 3 * dc_length / max(length1, length2)
length2 *= 3 * dc_length / max(length1, length2)
p1 = [
(0, -0.75 * width),
(0.25 * dc_length, -0.75 * width),
(0.75 * dc_length, 0.75 * width),
(dc_length, 0.75 * width),
(dc_length, 0.75 * width + 0.5 * length2),
(2 * width + dc_length, 0.75 * width + 0.5 * length2),
(2 * width + dc_length, 0.75 * width),
(2 * width + 1.25 * dc_length, 0.75 * width),
(2 * width + 1.75 * dc_length, -0.75 * width),
(2 * width + 2 * dc_length, -0.75 * width),
]
p2 = [
(0, 0.75 * width),
(0.25 * dc_length, 0.75 * width),
(0.75 * dc_length, -0.75 * width),
(dc_length, -0.75 * width),
(dc_length, -0.75 * width - 0.5 * length1),
(2 * width + dc_length, -0.75 * width - 0.5 * length1),
(2 * width + dc_length, -0.75 * width),
(2 * width + 1.25 * dc_length, -0.75 * width),
(2 * width + 1.75 * dc_length, 0.75 * width),
(2 * width + 2 * dc_length, 0.75 * width),
]
profiles = port_spec.path_profiles_list()
if len(profiles) == 0:
profiles = [(width, 0, _bb_layer)]
for w, g, layer in profiles:
polygons = boolean(
Path(p1[0], w, g).segment(p1[1:]), Path(p2[0], w, g).segment(p2[1:]), "+"
)
component.add(layer, *polygons)
_add_bb_text(component, width)
port_names = self.parametric_kwargs["ports"] or [None] * 4
component.add_port(Port(p1[0], 0, port_spec), port_names[0])
component.add_port(Port(p2[0], 0, port_spec), port_names[1])
component.add_port(Port(p2[-1], 180, port_spec), port_names[2])
component.add_port(Port(p1[-1], 180, port_spec), port_names[3])
component.add_model(self, model_name)
return component
[docs]
def start(
self,
component: Component,
frequencies: Sequence[pft.Frequency],
temperature1: pft.Temperature | None = None,
voltage1: pft.Voltage | None = None,
temperature2: pft.Temperature | None = None,
voltage2: pft.Voltage | None = None,
**kwargs,
) -> SMatrix:
"""Start computing the S matrix response from a component.
Args:
component: Component from which to compute the S matrix.
frequencies: Sequence of frequencies at which to perform the
computation.
temperature1: Operating temperature override for first arm.
temperature2: Operating temperature override for second arm.
voltage1: Operating voltage override for first arm.
voltage2: Operating voltage override for second arm.
**kwargs: Unused.
Returns:
Result object with attributes ``status`` and ``s_matrix``.
"""
classification = frequency_classification(frequencies)
component_ports = {
name: port.copy(True) for name, port in component.select_ports(classification).items()
}
if len(component_ports) != 4:
raise RuntimeError(
f"AnalyticMZIModel can only be used on components with 4 ports. '{component.name}' "
f"has {len(component_ports)} {classification} ports.",
)
p = self.parametric_kwargs
names = p["ports"]
if names is None:
names = sorted(component_ports)
elif not all(name in component_ports for name in names):
raise RuntimeError(
f"Not all port names defined in AnalyticMZIModel match the {classification} port "
f"names in component '{component.name}'."
)
num_modes = component_ports[names[0]].num_modes
if not all(port.num_modes == num_modes for port in component_ports.values()):
raise RuntimeError(
f"AnalyticMZIModel requires that all ports have the same number of modes. Ports "
f"from '{component.name}' support different numbers of modes."
)
frequencies = numpy.asarray(frequencies)
if temperature1 is None:
temperature1 = p["temperature1"]
if temperature2 is None:
temperature2 = p["temperature2"]
if voltage1 is None:
voltage1 = p["voltage1"]
if voltage2 is None:
voltage2 = p["voltage2"]
n_eff1 = _sample(component.name, "n_eff1", p["n_eff1"], frequencies, num_modes)
n_eff2 = p["n_eff2"]
if n_eff2 is None:
n_eff2 = n_eff1
else:
n_eff2 = _sample(
component.name, "n_eff2", _ensure_correct_shape(n_eff2), frequencies, num_modes
)
t1 = _ensure_correct_shape(
_waveguide_transmission(
frequencies,
p["length1"],
n_eff1,
p["n_group1"],
p["dispersion1"],
p["dispersion_slope1"],
p["reference_frequency"],
p["propagation_loss1"],
p["extra_loss1"],
p["dn1_dT"],
p["dL1_dT"],
temperature1,
p["reference_temperature"],
voltage1,
p["v_piL1"],
p["dloss_dv_1"],
p["dloss_dv2_1"],
)
)
t2 = _ensure_correct_shape(
_waveguide_transmission(
frequencies,
p["length2"] or p["length1"],
n_eff2,
p["n_group2"],
p["dispersion2"],
p["dispersion_slope2"],
p["reference_frequency"],
p["propagation_loss2"],
p["extra_loss2"],
p["dn2_dT"],
p["dL2_dT"],
temperature2,
p["reference_temperature"],
voltage2,
p["v_piL2"],
p["dloss_dv_2"],
p["dloss_dv2_2"],
)
)
kappa1 = _sample(component.name, "kappa1", p["kappa1"], frequencies, num_modes)
kappa2 = _sample(component.name, "kappa2", p["kappa2"], frequencies, num_modes)
tau1 = p["tau1"]
if tau1 is None:
tau1 = 1j * numpy.exp(1j * numpy.angle(kappa1)) * numpy.sqrt(1 - numpy.abs(kappa1) ** 2)
else:
tau1 = _sample(
component.name, "tau1", _ensure_correct_shape(tau1), frequencies, num_modes
)
tau2 = p["tau2"]
if tau2 is None:
tau2 = 1j * numpy.exp(1j * numpy.angle(kappa2)) * numpy.sqrt(1 - numpy.abs(kappa2) ** 2)
else:
tau2 = _sample(
component.name, "tau2", _ensure_correct_shape(tau2), frequencies, num_modes
)
s20 = tau1 * t1 * tau2 + kappa1 * t2 * kappa2
s30 = tau1 * t1 * kappa2 + kappa1 * t2 * tau2
s21 = kappa1 * t1 * tau2 + tau1 * t2 * kappa2
s31 = kappa1 * t1 * kappa2 + tau1 * t2 * tau2
s = (
(None, None, s20, s30),
(None, None, s21, s31),
(s20, s21, None, None),
(s30, s31, None, None),
)
elements = {
(f"{port_in}@{mode}", f"{port_out}@{mode}"): s[j][i][mode]
for i, port_in in enumerate(names)
for j, port_out in enumerate(names)
for mode in range(component_ports[port_in].num_modes)
if s[j][i] is not None
}
return SMatrix(frequencies, elements, component_ports)
register_model_class(TerminationModel)
register_model_class(TwoPortModel)
register_model_class(PowerSplitterModel)
register_model_class(PolarizationBeamSplitterModel)
register_model_class(PolarizationSplitterRotatorModel)
register_model_class(DirectionalCouplerModel)
register_model_class(CrossingModel)
register_model_class(WaveguideModel)
register_model_class(AnalyticWaveguideModel)
register_model_class(AnalyticDirectionalCouplerModel)
register_model_class(AnalyticMZIModel)