# No pollution of the parametric namespace
import itertools as _it
import typing as _typ
import warnings as _warn
from collections.abc import Sequence as _Sequence
from dataclasses import dataclass as _dataclass
import numpy as _np
import photonforge as _pf
@_dataclass(frozen=True)
class _Metadata:
type: _typ.Optional[str] = None
title: _typ.Optional[str] = None
description: _typ.Optional[str] = None
units: _typ.Optional[str] = None
required: bool = False
minimum: _typ.Optional[_typ.Union[float, int]] = None
maximum: _typ.Optional[_typ.Union[float, int]] = None
exclusive_minimum: _typ.Optional[_typ.Union[float, int]] = None
exclusive_maximum: _typ.Optional[_typ.Union[float, int]] = None
min_items: _typ.Optional[int] = None
max_items: _typ.Optional[int] = None
_Dimension = _typ.Annotated[float, _Metadata(minimum=0, units="μm")]
_OptDimension = _typ.Annotated[float, _Metadata(required=False, minimum=0, units="μm")]
_NonZeroDimension = _typ.Annotated[float, _Metadata(exclusive_minimum=0, units="μm")]
_OptNonZeroDimension = _typ.Annotated[
float, _Metadata(required=False, exclusive_minimum=0, units="μm")
]
_OptSize = _typ.Annotated[
_Sequence[_Dimension], _Metadata(required=False, min_items=2, max_items=2)
]
_Coord = _typ.Annotated[float, _Metadata(units="μm")]
_OptCoord = _typ.Annotated[float, _Metadata(required=False, units="μm")]
_Coord2D = _typ.Annotated[_Sequence[_Coord], _Metadata(min_items=2, max_items=2)]
_OptCoords = _typ.Annotated[_Sequence[_Coord2D], _Metadata(required=False)]
_OptOffsets = _typ.Annotated[_typ.Union[_Coord, _Coord2D], _Metadata(required=False)]
_OptAngle = _typ.Annotated[float, _Metadata(required=False, minimum=-360, maximum=360, units="°")]
_Fraction = _typ.Annotated[float, _Metadata(minimum=0, maximum=1)]
_OptFraction = _typ.Annotated[float, _Metadata(required=False, minimum=0, maximum=1)]
_OptFractions = _typ.Annotated[
_typ.Union[
_Fraction, _typ.Annotated[_Sequence[_Fraction], _Metadata(min_items=2, max_items=2)]
],
_Metadata(required=False),
]
_OptIntTurns = _typ.Annotated[int, _Metadata(required=False, minimum=2)]
_OptJoin = _typ.Annotated[_typ.Union[_typ.Literal["round"], float], _Metadata(required=False)]
_OptBool = _typ.Annotated[bool, _Metadata(required=False)]
_OptAxis = _typ.Annotated[_typ.Literal["", "x", "y"], _Metadata(required=False)]
_OptLayer = _typ.Annotated[
_typ.Union[str, tuple[int, int]], _Metadata(type="Layer", required=False)
]
_OptVariableFraction = _typ.Annotated[
_typ.Union[str, _pf.Expression],
_Metadata(
type="Expression",
required=False,
description="String expression parametrized by the independent variable 'u', ranging from "
"0 to 1.",
),
]
_OptVariableOffset = _typ.Annotated[
_typ.Union[float, str, _pf.Expression], _Metadata(type="Expression", required=False, units="μm")
]
_PortSpec = _typ.Annotated[_typ.Union[str, _pf.PortSpec], _Metadata(type="PortSpec")]
_PortSpec1 = _typ.Annotated[
_typ.Union[str, _pf.PortSpec], _Metadata(type="PortSpec", title="Port Spec 1")
]
_PortSpec2 = _typ.Annotated[
_typ.Union[str, _pf.PortSpec], _Metadata(type="PortSpec", title="Port Spec 2")
]
_PortSpec_x2 = _typ.Annotated[_Sequence[_PortSpec], _Metadata(min_items=2, max_items=2)]
_PortSpecPair = _typ.Union[_PortSpec, _PortSpec_x2]
_Port = _typ.Annotated[
_typ.Union[_pf.Port, tuple[_pf.Reference, str], tuple[_pf.Reference, str, int]],
_Metadata(type="Port"),
]
_Terminal = _typ.Annotated[
_typ.Union[_pf.Terminal, tuple[_pf.Reference, str], tuple[_pf.Reference, str, int]],
_Metadata(type="Terminal"),
]
_Model = _typ.Annotated[_typ.Literal["Tidy3D", "Waveguide"], _Metadata(required=False)]
_Technology = _typ.Annotated[_pf.Technology, _Metadata(required=False)]
_Name = _typ.Annotated[str, _Metadata(required=False)]
_Kwargs = _typ.Annotated[dict[str, _typ.Any], _Metadata("required", False)]
def _get_default(function, kwarg, value, default=None):
if value is not None:
return value
func_kwargs = _pf.config.default_kwargs.get(function)
if isinstance(func_kwargs, dict):
value = func_kwargs.get(kwarg)
if value is not None:
return value
value = _pf.config.default_kwargs.get(kwarg)
if value is not None:
return value
if default is not None:
return default
raise TypeError(f"{function}() missing 1 required keyword-only argument: '{kwarg}'")
[docs]
@_pf.parametric_component
def straight(
*,
port_spec: _typ.Optional[_PortSpec] = None,
length: _typ.Optional[_NonZeroDimension] = None,
bulge_width: _typ.Optional[_OptCoord] = None,
bulge_taper_length: _typ.Optional[_OptDimension] = None,
bulge_margin: _typ.Optional[_OptDimension] = None,
active_model: _typ.Optional[_Model] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
waveguide_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Straight waveguide section.
Args:
port_spec: Port specification describing waveguide cross-section.
length: Section length.
bulge_width: Width added to the waveguide cross-section in the
central region when ``length`` if enough to fit in 2 tapering
sections plus margins. If ``None``, defaults to 0.
bulge_taper_length: Length of each tapering region for bulging the
central region of the waveguide. If ``None``, defaults to 0.
bulge_margin: Length of the waveguide that must be kept without
bulging at both ends. If ``None``, defaults to 0.
active_model: Name of the model to be used by default; must be
either ``"Tidy3D"`` or ``"Waveguide"`` (default).
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
waveguide_model_kwargs: Dictionary of keyword arguments passed to
the component's :class:`photonforge.WaveguideModel`.
Returns:
Component with the straight section, ports and model.
"""
function = "straight"
port_spec = _get_default(function, "port_spec", port_spec)
length = _get_default(function, "length", length)
bulge_width = _get_default(function, "bulge_width", bulge_width, 0)
bulge_taper_length = _get_default(function, "bulge_taper_length", bulge_taper_length, 0)
bulge_margin = _get_default(function, "bulge_margin", bulge_margin, 0)
active_model = _get_default(function, "active_model", active_model, "Waveguide")
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
waveguide_model_kwargs = _get_default(
function, "waveguide_model_kwargs", waveguide_model_kwargs, {}
)
if technology is None:
technology = _pf.config.default_technology
if isinstance(port_spec, str):
port_spec = technology.ports[port_spec]
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "wg"
bulge_region = (bulge_margin + bulge_taper_length, length - bulge_margin - bulge_taper_length)
if (
bulge_width != 0
and bulge_taper_length > 0
and bulge_margin >= 0
and bulge_region[1] >= bulge_region[0]
):
for width, offset, layer in port_spec.path_profiles_list():
path = _pf.Path((0, 0), width, offset)
if bulge_margin > 0:
path.segment((bulge_margin, 0))
path.segment((bulge_region[0], 0), width + bulge_width)
if bulge_region[1] > bulge_region[0]:
path.segment((bulge_region[1], 0))
path.segment((length - bulge_margin, 0), width)
if bulge_margin > 0:
path.segment((length, 0))
c.add(layer, path)
else:
for layer, path in port_spec.get_paths((0, 0)):
c.add(layer, path.segment((length, 0)))
p0 = c.add_port(_pf.Port((0, 0), 0, port_spec))
p1 = c.add_port(_pf.Port((length, 0), 180, port_spec, inverted=True))
c.add_model(_pf.WaveguideModel(**dict(waveguide_model_kwargs)), "Waveguide", False)
model_kwargs = {"port_symmetries": [(p1, p0)]}
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D", False)
c.activate_model(active_model)
return c
[docs]
@_pf.parametric_component
def transition(
*,
port_spec1: _typ.Optional[_PortSpec1] = None,
port_spec2: _typ.Optional[_PortSpec2] = None,
length: _typ.Optional[_NonZeroDimension] = None,
constant_length: _typ.Optional[_OptDimension] = None,
profile: _typ.Optional[_OptVariableFraction] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Straight waveguide that works as a transition between port profiles.
Args:
port_spec1: Port specification describing the first cross-section.
port_spec2: Port specification describing the second cross-section.
length: Transition length.
constant_length: Constant cross-section length added to both ends.
If ``None``, defaults to 0.
profile: String expression describing the transition shape
parametrized by the independent variable ``"u"``, ranging from 0
to 1 along the transition. The expression must evaluate to a float
between 0 and 1 representing the weight of the second profile with
respect to the first at that position. Alternatively, an
:class:`photonforge.Expression` with 1 parameter can be used. If
``None``, a linear transition is used.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Component with the transition geometry, ports and model.
"""
function = "transition"
port_spec1 = _get_default(function, "port_spec1", port_spec1)
port_spec2 = _get_default(function, "port_spec2", port_spec2)
length = _get_default(function, "length", length)
constant_length = _get_default(function, "constant_length", constant_length, 0)
profile = _get_default(function, "profile", profile, "u")
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
if length <= 0 and constant_length <= 0:
raise ValueError("Transition length cannot be 0.")
if isinstance(profile, _pf.Expression):
parameter = profile.parameters
if len(parameter) != 1:
raise TypeError("Profile expression must contain 1 parameter only.")
expressions = profile.expressions
if len(expressions) == 0:
raise TypeError("Profile expression must contain at least 1 expression.")
elif isinstance(profile, str):
parameter = ["u"]
expressions = [("p", profile)]
value_name = expressions[-1][0]
def interp(a, b):
return _pf.Expression(
parameter,
[*expressions, f"{a} + {value_name} * {b - a}", f"{b - a}"],
)
if technology is None:
technology = _pf.config.default_technology
if isinstance(port_spec1, str):
port_spec1 = technology.ports[port_spec1]
if isinstance(port_spec2, str):
port_spec2 = technology.ports[port_spec2]
path_profiles1 = port_spec1.path_profiles_list()
path_profiles2 = port_spec2.path_profiles_list()
only1 = {layer for _, _, layer in path_profiles1}
only2 = {layer for _, _, layer in path_profiles2}
both = only1.intersection(only2)
only1 -= both
only2 -= both
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "transition"
start_point = (constant_length, 0)
mid_point = (constant_length + length, 0)
end_point = (2 * constant_length + length, 0)
for layer in only1:
for w1, g1, l1 in path_profiles1:
if l1 != layer:
continue
path = _pf.Path((0, 0), w1, g1)
if constant_length > 0:
path.segment(start_point, (w1, "constant"), (g1, "constant"))
if length > 0:
path.segment(mid_point, width=interp(w1, 0))
c.add(layer, path)
for layer in only2:
for w2, g2, l2 in path_profiles2:
if l2 != layer:
continue
path = _pf.Path(start_point, 0, g2)
if length > 0:
path.segment(mid_point, width=interp(0, w2))
if constant_length > 0:
path.segment(end_point, (w2, "constant"), (g2, "constant"))
c.add(layer, path)
for layer in both:
prof1 = sorted((g, w) for w, g, l1 in path_profiles1 if l1 == layer)
prof2 = sorted((g, w) for w, g, l2 in path_profiles2 if l2 == layer)
combinations = zip(prof1, prof2) if len(prof1) == len(prof2) else _it.product(prof1, prof2)
for (g1, w1), (g2, w2) in combinations:
path = _pf.Path((0, 0), w1, g1)
if constant_length > 0:
path.segment(start_point, (w1, "constant"), (g1, "constant"))
if length > 0:
path.segment(mid_point, width=interp(w1, w2), offset=interp(g1, g2))
else:
c.add(layer, path)
path = _pf.Path(mid_point, w2, g2)
if constant_length > 0:
path.segment(end_point, (w2, "constant"), (g2, "constant"))
c.add(layer, path)
c.add_port(_pf.Port((0, 0), 0, port_spec1))
c.add_port(_pf.Port(end_point, 180, port_spec2, inverted=True))
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def bend(
*,
port_spec: _typ.Optional[_PortSpec] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
angle: _typ.Optional[_OptAngle] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
port_bends: _typ.Optional[_OptBool] = None,
active_model: _typ.Optional[_Model] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
waveguide_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Waveguide bend section.
Args:
port_spec: Port specification describing waveguide cross-section.
radius: Central arc radius.
angle: Arc coverage angle. If ``None``, defaults to 90.
euler_fraction: Fraction of the bend that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
port_bends: Flag controllig whether to set a bend radius for the
ports. Not used when ``euler_factor > 0``. If ``None``, defaults
to ``False``.
active_model: Name of the model to be used by default; must be
either ``"Tidy3D"`` or ``"Waveguide" (default)``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
waveguide_model_kwargs: Dictionary of keyword arguments passed to
the component's :class:`photonforge.WaveguideModel`.
Returns:
Component with the circular bend section, ports and model.
"""
if technology is None:
technology = _pf.config.default_technology
function = "bend"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = technology.ports[port_spec]
radius = _get_default(
function,
"radius",
radius,
port_spec.default_radius if port_spec.default_radius > 0 else None,
)
angle = _get_default(function, "angle", angle, 90)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
port_bends = _get_default(function, "port_bends", port_bends, False)
active_model = _get_default(function, "active_model", active_model, "Waveguide")
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
waveguide_model_kwargs = _get_default(
function, "waveguide_model_kwargs", waveguide_model_kwargs, {}
)
if angle % 90 != 0:
_warn.warn(
"Using bends with angles not multiples of 90° might lead to disconnected waveguides. "
"Consider building a continuous path with grid-aligned ports instead of connecting "
"sections with non grid-aligned ports.",
RuntimeWarning,
3,
)
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "bend"
p0 = c.add_port(_pf.Port((0, 0), 0, port_spec))
path_length = None
if angle > 0:
radians = (angle - 90) / 180.0 * _np.pi
endpoint = (radius * _np.cos(radians), radius * (1 + _np.sin(radians)))
for layer, path in port_spec.get_paths((0, 0)):
path.arc(-90, angle - 90, radius, euler_fraction=euler_fraction, endpoint=endpoint)
c.add(layer, path)
if path_length is None:
path_length = path.length()
p1 = c.add_port(_pf.Port(endpoint, angle - 180, port_spec, inverted=True))
if port_bends and euler_fraction == 0:
c[p0].bend_radius = radius
c[p1].bend_radius = -radius
else:
radians = (90 + angle) / 180.0 * _np.pi
endpoint = (radius * _np.cos(radians), radius * (-1 + _np.sin(radians)))
for layer, path in port_spec.get_paths((0, 0)):
path.arc(90, 90 + angle, radius, euler_fraction=euler_fraction, endpoint=endpoint)
c.add(layer, path)
if path_length is None:
path_length = path.length()
p1 = c.add_port(_pf.Port(endpoint, angle + 180, port_spec, inverted=True))
if port_bends and euler_fraction == 0:
c[p0].bend_radius = -radius
c[p1].bend_radius = radius
model_kwargs = {"length": path_length}
model_kwargs.update(waveguide_model_kwargs)
c.add_model(_pf.WaveguideModel(**model_kwargs), "Waveguide", False)
model_kwargs = {"port_symmetries": [(p1, p0)]}
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D", False)
c.activate_model(active_model)
return c
[docs]
@_pf.parametric_component
def s_bend(
*,
port_spec: _typ.Optional[_PortSpec] = None,
length: _typ.Optional[_OptNonZeroDimension] = None,
offset: _typ.Optional[_Coord] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
active_model: _typ.Optional[_Model] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
waveguide_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""S bend waveguide section.
Args:
port_spec: Port specification describing waveguide cross-section.
length: Length of the S bend in the main propagation direction. If
``None``, a default is calculated based on the default bend
radius, if possible.
offset: Side offset of the S bend.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
active_model: Name of the model to be used by default; must be
either ``"Tidy3D"`` or ``"Waveguide"`` (default).
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
waveguide_model_kwargs: Dictionary of keyword arguments passed to
the component's :class:`photonforge.WaveguideModel`.
Returns:
Component with the S bend section, ports and model.
"""
if technology is None:
technology = _pf.config.default_technology
function = "s_bend"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = technology.ports[port_spec]
offset = _get_default(function, "offset", offset)
default_length = None
if length is None:
abs_offset = abs(offset)
radius = _get_default("bend", "radius", None, port_spec.default_radius)
if 4 * radius > abs_offset:
default_length = _pf.s_bend_length(abs_offset, radius)
length = _get_default(function, "length", length, default_length)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
active_model = _get_default(function, "active_model", active_model, "Waveguide")
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
waveguide_model_kwargs = _get_default(
function, "waveguide_model_kwargs", waveguide_model_kwargs, {}
)
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "s-bend"
path_length = None
for layer, path in port_spec.get_paths((0, 0)):
c.add(layer, path.s_bend((length, offset), euler_fraction))
if path_length is None:
path_length = path.length()
p0 = c.add_port(_pf.Port((0, 0), 0, port_spec))
p1 = c.add_port(_pf.Port((length, offset), 180, port_spec, inverted=True))
model_kwargs = {"length": path_length}
model_kwargs.update(waveguide_model_kwargs)
c.add_model(_pf.WaveguideModel(**model_kwargs), "Waveguide", False)
model_kwargs = {"port_symmetries": [(p1, p0)]}
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D", False)
c.activate_model(active_model)
return c
[docs]
@_pf.parametric_component
def crossing(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
arm_length: _typ.Optional[_NonZeroDimension] = None,
added_width: _typ.Optional[_OptVariableOffset] = None,
extra_length: _typ.Optional[_OptDimension] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Waveguide crossing.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each waveguide.
arm_length: Length of a single crossing arm.
added_width: Width added to the arm linearly up to the center. An
expression or string (with independent variable ``"u"``) can also
be used. If ``None``, defaults to 0.
extra_length: Additional length for a straight section at the ports.
If ``None``, defaults to 0.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Component with the crossing, ports and model.
"""
if technology is None:
technology = _pf.config.default_technology
function = "crossing"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
arm_length = _get_default(function, "arm_length", arm_length)
added_width = _get_default(function, "added_width", added_width, 0)
extra_length = _get_default(function, "extra_length", extra_length, 0)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
if isinstance(added_width, _pf.Expression):
p = added_width.parameters
if len(p) != 1:
raise TypeError("Profile expression must contain 1 parameter only.")
p = p[0]
expressions = added_width.expressions
if len(expressions) == 0:
raise TypeError("Profile expression must contain at least 1 expression.")
elif isinstance(added_width, str):
p = "u"
expressions = [("p", added_width)]
else:
p = "u"
expressions = [("p", f"{added_width}*u")]
value_name = expressions[-1][0]
names = [p] + [k for k, v in expressions]
i = 0
while f"{p}_{i}" in names:
i += 1
parameter = f"{p}_{i}"
expressions.insert(0, (p, f"1 - abs(1 - 2 * {parameter})"))
length = arm_length + extra_length
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "crossing"
for i in range(2):
v = 1 - i + 1j * i
for width, offset, layer in port_spec[i].path_profiles_list():
width_expr = _pf.Expression(
parameter, [*expressions, f"{width} + {value_name}", ("derivative", 0)]
)
arm = _pf.Path(-length * v, width, offset)
if length > arm_length:
arm.segment(-arm_length * v)
arm.segment(arm_length * v, width=width_expr)
if length > arm_length:
arm.segment(length * v)
c.add(layer, arm)
c.add_port(_pf.Port((-length, 0), 0, port_spec[0]))
c.add_port(_pf.Port((0, -length), 90, port_spec[1]))
c.add_port(_pf.Port((length, 0), 180, port_spec[0], inverted=True))
c.add_port(_pf.Port((0, length), -90, port_spec[1], inverted=True))
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def crossing45(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
arm_length: _typ.Optional[_NonZeroDimension] = None,
added_width: _typ.Optional[_OptVariableOffset] = None,
extra_length: _typ.Optional[_OptDimension] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""45° waveguide crossing.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each waveguide.
arm_length: Length of a single crossing arm.
added_width: Width added to the arm linearly up to the center. An
expression or string (with independent variable ``"u"``) can also
be used. If ``None``, defaults to 0.
extra_length: Additional length for a straight section at the ports.
If ``None``, defaults to 0.
technology: Component technology. If ``None``, the default
technology is used.
radius: Radius used for arm bends.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Component with the crossing, ports and model.
"""
if technology is None:
technology = _pf.config.default_technology
function = "crossing45"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
radius = [
_get_default(
function,
"radius",
radius,
port_spec[i].default_radius if port_spec[i].default_radius > 0 else None,
)
for i in range(2)
]
arm_length = _get_default(function, "arm_length", arm_length)
added_width = _get_default(function, "added_width", added_width, 0)
extra_length = _get_default(function, "extra_length", extra_length, 0)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
if isinstance(added_width, _pf.Expression):
p = added_width.parameters
if len(p) != 1:
raise TypeError("Profile expression must contain 1 parameter only.")
p = p[0]
expressions = added_width.expressions
if len(expressions) == 0:
raise TypeError("Profile expression must contain at least 1 expression.")
elif isinstance(added_width, str):
p = "u"
expressions = [("p", added_width)]
else:
p = "u"
expressions = [("p", f"{added_width}*u")]
value_name = expressions[-1][0]
names = [p] + [k for k, v in expressions]
i = 0
while f"{p}_{i}" in names:
i += 1
parameter = f"{p}_{i}"
expressions.insert(0, (p, f"1 - abs(1 - 2 * {parameter})"))
projected_arm_length = arm_length * 2**-0.5
projected_extra_length = extra_length * 2**-0.5
projected_length = projected_arm_length + projected_extra_length
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "crossing"
xp = [None, None]
yp = [None, None]
for i in range(2):
sign = 1 - 2 * i
arc_x = radius[i] * 2**-0.5
arc_y = radius[i] - arc_x
old_grid = _pf.config.grid
_pf.config.grid = _pf.config.tolerance
xp[i] = _pf.snap_to_grid(projected_arm_length + projected_extra_length + arc_x)
yp[i] = _pf.snap_to_grid(projected_arm_length + projected_extra_length + arc_y)
_pf.config.grid = old_grid
for width, offset, layer in port_spec[i].path_profiles_list():
width_expr = _pf.Expression(
parameter, [*expressions, f"{width} + {value_name}", ("derivative", 0)]
)
arm = _pf.Path((-xp[i], -sign * yp[i]), width, offset)
arm.arc(
-sign * 90,
-sign * 45,
radius[i],
euler_fraction=euler_fraction,
endpoint=(-projected_length, -sign * projected_length),
)
if projected_length > projected_arm_length:
arm.segment((-projected_arm_length, -sign * projected_arm_length))
arm.segment((projected_arm_length, sign * projected_arm_length), width=width_expr)
if projected_length > projected_arm_length:
arm.segment((projected_length, sign * projected_length))
arm.arc(
sign * 135,
sign * 90,
radius[i],
euler_fraction=euler_fraction,
endpoint=(xp[i], sign * yp[i]),
)
c.add(layer, arm)
c.add_port(_pf.Port((-xp[0], -yp[0]), 0, port_spec[0]))
c.add_port(_pf.Port((-xp[1], yp[1]), 0, port_spec[1]))
c.add_port(_pf.Port((xp[1], -yp[1]), 180, port_spec[1], inverted=True))
c.add_port(_pf.Port((xp[0], yp[0]), 180, port_spec[0], inverted=True))
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def ring_coupler(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
coupling_distance: _typ.Optional[_Coord] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
bus_length: _typ.Optional[_OptDimension] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
coupling_length: _typ.Optional[_OptDimension] = None,
port_bends: _typ.Optional[_OptBool] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Ring/straight coupling region.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each coupler side.
coupling_distance: Distance between bus and ring waveguide centers.
radius: Central ring radius.
bus_length: Length of the bus waveguide added to each side of the
straight coupling section. If both ``bus_length`` and
``coupling_length`` are 0, the bus waveguide is not included. If
``None``, defaults to radius.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
coupling_length: Length of straight coupling region. If ``None``,
defaults to 0.
port_bends: Flag controllig whether to set a bend radius for the
ports. Not used when ``euler_factor > 0``. If ``None``, defaults
to ``False``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Coupling component.
"""
if technology is None:
technology = _pf.config.default_technology
function = "ring_coupler"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
coupling_distance = _get_default(function, "coupling_distance", coupling_distance)
radius = _get_default(
function,
"radius",
radius,
port_spec[1].default_radius if port_spec[1].default_radius > 0 else None,
)
bus_length = _get_default(function, "bus_length", bus_length, radius)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
coupling_length = _get_default(function, "coupling_length", coupling_length, 0)
port_bends = _get_default(function, "port_bends", port_bends, False)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
xp = bus_length + 0.5 * coupling_length
yp = -radius - coupling_distance
xr = radius + 0.5 * coupling_length
if xp > 0:
for layer, path in port_spec[0].get_paths((-xp, yp)):
c.add(layer, path.segment((xp, yp)))
for layer, path in port_spec[1].get_paths((xr, 0)):
path.arc(0, -90, radius, euler_fraction=euler_fraction)
if coupling_length > 0:
path.segment((-0.5 * coupling_length, -radius))
path.arc(-90, -180, radius, endpoint=(-xr, 0), euler_fraction=euler_fraction)
c.add(layer, path)
if xp > 0:
c.add_port(_pf.Port((-xp, yp), 0, port_spec[0]))
p0 = c.add_port(_pf.Port((-xr, 0), -90, port_spec[1], inverted=True))
if xp > 0:
c.add_port(_pf.Port((xp, yp), 180, port_spec[0], inverted=True))
p1 = c.add_port(_pf.Port((xr, 0), -90, port_spec[1]))
if port_bends and euler_fraction == 0:
c[p0].bend_radius = radius
c[p1].bend_radius = -radius
model_kwargs = {}
if xp == 0:
model_kwargs["port_symmetries"] = [(p1, p0)]
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def s_bend_ring_coupler(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
coupling_distance: _typ.Optional[_Coord] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
s_bend_length: _typ.Optional[_OptNonZeroDimension] = None,
s_bend_offset: _typ.Optional[_Coord] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
coupling_length: _typ.Optional[_OptDimension] = None,
port_bends: _typ.Optional[_OptBool] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Ring coupling through an S bend curve.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each coupler side.
coupling_distance: Distance between bus and ring waveguide centers.
radius: Central ring radius.
s_bend_length: Length of the S bends. If ``None``, a default is
calculated based on the default bend radius, if possible.
s_bend_offset: Offset of the S bends.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
coupling_length: Length of straight coupling region. If ``None``,
defaults to 0.
port_bends: Flag controllig whether to set a bend radius for the
ports. Not used when ``euler_factor > 0``. If ``None``, defaults
to ``False``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Coupling component.
"""
if technology is None:
technology = _pf.config.default_technology
function = "s_bend_ring_coupler"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
coupling_distance = _get_default(function, "coupling_distance", coupling_distance)
radius = _get_default(
function,
"radius",
radius,
port_spec[1].default_radius if port_spec[1].default_radius > 0 else None,
)
s_bend_offset = _get_default(function, "s_bend_offset", s_bend_offset)
default_length = None
if s_bend_length is None:
abs_offset = abs(s_bend_offset)
s_radius = _get_default("bend", "radius", None, port_spec[0].default_radius)
if 4 * s_radius > abs_offset:
default_length = _pf.s_bend_length(abs_offset, s_radius)
s_bend_length = _get_default(function, "s_bend_length", s_bend_length, default_length)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
coupling_length = _get_default(function, "coupling_length", coupling_length, 0)
port_bends = _get_default(function, "port_bends", port_bends, False)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
xs = s_bend_length + 0.5 * coupling_length
ys = -radius - coupling_distance - s_bend_offset
y_mid = -radius - coupling_distance
for layer, path in port_spec[0].get_paths((-xs, ys)):
path.s_bend((-0.5 * coupling_length, y_mid), euler_fraction)
if coupling_length > 0:
path.segment((0.5 * coupling_length, y_mid))
path.s_bend((xs, ys), euler_fraction)
c.add(layer, path)
xr = radius + 0.5 * coupling_length
for layer, path in port_spec[1].get_paths((xr, 0)):
path.arc(0, -90, radius, euler_fraction=euler_fraction)
if coupling_length > 0:
path.segment((-0.5 * coupling_length, -radius))
path.arc(-90, -180, radius, endpoint=(-xr, 0), euler_fraction=euler_fraction)
c.add(layer, path)
c.add_port(_pf.Port((-xs, ys), 0, port_spec[0]))
p0 = c.add_port(_pf.Port((-xr, 0), -90, port_spec[1], inverted=True))
c.add_port(_pf.Port((xs, ys), 180, port_spec[0], inverted=True))
p1 = c.add_port(_pf.Port((xr, 0), -90, port_spec[1]))
if port_bends and euler_fraction == 0:
c[p0].bend_radius = radius
c[p1].bend_radius = -radius
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def dual_ring_coupler(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
coupling_distance: _typ.Optional[_Coord] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
coupling_length: _typ.Optional[_OptDimension] = None,
port_bends: _typ.Optional[_OptBool] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Dual ring coupling region.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each coupler side.
coupling_distance: Distance between bus and ring waveguide centers.
radius: Central ring radius. A tuple with 2 values can be used, one
for each coupler side.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
coupling_length: Length of straight coupling region. If ``None``,
defaults to 0.
port_bends: Flag controlling whether to set a bend radius for the
ports. Not used when ``euler_factor > 0``. If ``None``, defaults
to ``False``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Coupling component.
"""
if technology is None:
technology = _pf.config.default_technology
function = "dual_ring_coupler"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
coupling_distance = _get_default(function, "coupling_distance", coupling_distance)
if radius is None or hasattr(radius, "__float__"):
radius = [radius, radius]
radius = [
_get_default(
function,
"radius",
radius[i],
port_spec[i].default_radius if port_spec[i].default_radius > 0 else None,
)
for i in range(2)
]
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
coupling_length = _get_default(function, "coupling_length", coupling_length, 0)
port_bends = _get_default(function, "port_bends", port_bends, False)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
xr0 = radius[0] + 0.5 * coupling_length
yr = radius[0] + radius[1] + coupling_distance
for layer, path in port_spec[0].get_paths((-xr0, -yr)):
path.arc(180, 90, radius[0], euler_fraction=euler_fraction)
if coupling_length > 0:
path.segment((0.5 * coupling_length, -radius[1] - coupling_distance))
path.arc(90, 0, radius[0], endpoint=(xr0, -yr), euler_fraction=euler_fraction)
c.add(layer, path)
xr1 = radius[1] + 0.5 * coupling_length
for layer, path in port_spec[1].get_paths((xr1, 0)):
path.arc(0, -90, radius[1], euler_fraction=euler_fraction)
if coupling_length > 0:
path.segment((-0.5 * coupling_length, -radius[0]))
path.arc(-90, -180, radius[1], endpoint=(-xr1, 0), euler_fraction=euler_fraction)
c.add(layer, path)
p0 = c.add_port(_pf.Port((-xr0, -yr), 90, port_spec[0]))
p1 = c.add_port(_pf.Port((-xr1, 0), -90, port_spec[1], inverted=True))
p2 = c.add_port(_pf.Port((xr0, -yr), 90, port_spec[0], inverted=True))
p3 = c.add_port(_pf.Port((xr1, 0), -90, port_spec[1]))
if port_bends and euler_fraction == 0:
c[p0].bend_radius = -radius[0]
c[p1].bend_radius = radius[1]
c[p2].bend_radius = radius[0]
c[p3].bend_radius = -radius[1]
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def s_bend_coupler(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
coupling_distance: _typ.Optional[_Coord] = None,
s_bend_length: _typ.Optional[_OptNonZeroDimension] = None,
s_bend_offset: _typ.Optional[_Coord] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
coupling_length: _typ.Optional[_OptDimension] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""S bend coupling region.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each coupler side.
coupling_distance: Distance between waveguide centers.
s_bend_length: Length of the S bends. A tuple with 2 values can be
used, one for each coupler side.
s_bend_offset: Offset of the S bends. A tuple with 2 values can be
used, one for each coupler side.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
coupling_length: Length of straight coupling region. If ``None``,
defaults to 0.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Coupling component.
"""
if technology is None:
technology = _pf.config.default_technology
function = "s_bend_coupler"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
coupling_distance = _get_default(function, "coupling_distance", coupling_distance)
if s_bend_offset is None or hasattr(s_bend_offset, "__float__"):
s_bend_offset = [s_bend_offset, s_bend_offset]
s_bend_offset = [_get_default(function, "s_bend_offset", s_bend_offset[i]) for i in range(2)]
if s_bend_length is None or hasattr(s_bend_length, "__float__"):
s_bend_length = [s_bend_length, s_bend_length]
for i in range(2):
default_length = None
if s_bend_length[i] is None:
abs_offset = abs(s_bend_offset[i])
radius = _get_default("bend", "radius", None, port_spec[i].default_radius)
if 4 * radius > abs_offset:
default_length = _pf.s_bend_length(abs_offset, radius)
s_bend_length[i] = _get_default(function, "s_bend_length", s_bend_length[i], default_length)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
coupling_length = _get_default(function, "coupling_length", coupling_length, 0)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
x_out0 = 2 * s_bend_length[0] + coupling_length
x_out1 = s_bend_length[0] + s_bend_length[1] + coupling_length
x_in1 = s_bend_length[0] - s_bend_length[1]
y_out1 = s_bend_offset[0] + s_bend_offset[1] + coupling_distance
x_mid = s_bend_length[0] + coupling_length
y_mid = s_bend_offset[0] + coupling_distance
for layer, path in port_spec[0].get_paths((0, 0)):
path.s_bend((s_bend_length[0], s_bend_offset[0]), euler_fraction)
if coupling_length > 0:
path.segment((x_mid, s_bend_offset[0]))
path.s_bend((x_out0, 0), euler_fraction)
c.add(layer, path)
for layer, path in port_spec[1].get_paths((x_out1, y_out1)):
path.s_bend((x_mid, y_mid), euler_fraction, direction=(-1, 0))
if coupling_length > 0:
path.segment((s_bend_length[0], y_mid))
path.s_bend((x_in1, y_out1), euler_fraction)
c.add(layer, path)
c.add_port(_pf.Port((0, 0), 0, port_spec[0]))
c.add_port(_pf.Port((x_in1, y_out1), 0, port_spec[1], inverted=True))
c.add_port(_pf.Port((x_out0, 0), -180, port_spec[0], inverted=True))
c.add_port(_pf.Port((x_out1, y_out1), 180, port_spec[1]))
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
[docs]
@_pf.parametric_component
def s_bend_straight_coupler(
*,
port_spec: _typ.Optional[_PortSpecPair] = None,
coupling_distance: _typ.Optional[_Coord] = None,
s_bend_length: _typ.Optional[_OptNonZeroDimension] = None,
s_bend_offset: _typ.Optional[_Coord] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
coupling_length: _typ.Optional[_OptDimension] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""S bend/straight coupling region.
Args:
port_spec: Port specification describing waveguide cross-section.
A tuple with 2 values can be used, one for each coupler side.
coupling_distance: Distance between waveguide centers.
s_bend_length: Length of the S bends.
s_bend_offset: Offset of the S bends.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
coupling_length: Length of straight coupling region. If ``None``,
defaults to 0.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
Returns:
Coupling component.
"""
if technology is None:
technology = _pf.config.default_technology
function = "s_bend_straight_coupler"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = (technology.ports[port_spec], technology.ports[port_spec])
elif isinstance(port_spec, _pf.PortSpec):
port_spec = (port_spec, port_spec)
else:
port_spec = list(port_spec)
for i in range(2):
if isinstance(port_spec[i], str):
port_spec[i] = technology.ports[port_spec[i]]
coupling_distance = _get_default(function, "coupling_distance", coupling_distance)
s_bend_offset = _get_default(function, "s_bend_offset", s_bend_offset)
default_length = None
if s_bend_length is None:
abs_offset = abs(s_bend_offset)
radius = _get_default("bend", "radius", None, port_spec[1].default_radius)
if 4 * radius > abs_offset:
default_length = _pf.s_bend_length(abs_offset, radius)
s_bend_length = _get_default(function, "s_bend_length", s_bend_length, default_length)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
coupling_length = _get_default(function, "coupling_length", coupling_length, 0)
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
xs = 2 * s_bend_length + coupling_length
for layer, path in port_spec[0].get_paths((0, 0)):
c.add(layer, path.segment((xs, 0)))
x_mid = s_bend_length + coupling_length
ys = s_bend_offset + coupling_distance
for layer, path in port_spec[1].get_paths((xs, ys)):
path.s_bend((x_mid, coupling_distance), euler_fraction, direction=(-1, 0))
if coupling_length > 0:
path.segment((s_bend_length, coupling_distance))
path.s_bend((0, ys), euler_fraction)
c.add(layer, path)
c.add_port(_pf.Port((0, 0), 0, port_spec[0]))
c.add_port(_pf.Port((0, ys), 0, port_spec[1], inverted=True))
c.add_port(_pf.Port((xs, 0), -180, port_spec[0], inverted=True))
c.add_port(_pf.Port((xs, ys), 180, port_spec[1]))
c.add_model(_pf.Tidy3DModel(**dict(tidy3d_model_kwargs)), "Tidy3D")
return c
def _rectangular_spiral_geometry(
turns,
radius,
separation,
size,
align_ports,
technology,
name,
straight_kwds,
bend0,
bend1,
):
if align_ports == "x":
inner_size = [size[0] - 2 * separation, size[1] - separation]
elif align_ports == "y":
inner_size = [size[0] - 2 * separation - radius, size[1]]
else:
inner_size = [size[0] - 2 * separation, size[1]]
if turns % 2 == 0:
inner_size = [inner_size[1], inner_size[0]]
inner_size[0] -= 4 * radius + ((turns - 2) // 2) * 2 * separation
inner_size[1] -= 2 * radius + ((turns - 1) // 2) * 2 * separation
for i in range(2):
if inner_size[i] < 0:
j = (1 - i) if turns % 2 == 0 else i
if size[j] > 0:
raise ValueError(
f"Dimension {size[j]} is too small for the spiral in the {'xy'[j]} axis."
)
inner_size[i] = 0
straight = _pf.parametric.straight(length=inner_size[1], **straight_kwds)
p0, p1 = sorted(straight.ports)
c = _pf.Component(name, technology=technology)
start = c.add_reference(straight)
if turns % 4 == 1:
start.rotate(90)
elif turns % 4 == 2:
start.rotate(180)
elif turns % 4 == 3:
start.rotate(-90)
arm0 = start
arm1 = start
lengths = [inner_size[0] / 2, inner_size[1] + separation]
for steps in range(turns):
arm0 = c.add_reference(bend0).connect(p0, arm0[p1])
arm1 = c.add_reference(bend1).connect(p1, arm1[p0])
i = steps % 2
if steps < turns - 1 and lengths[i] > 0:
straight = _pf.parametric.straight(length=lengths[i], **straight_kwds)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
arm1 = c.add_reference(straight).connect(p1, arm1[p0])
if steps == 0:
lengths[0] += inner_size[0] / 2 + separation + 2 * radius
else:
lengths[i] += 2 * separation
straight = _pf.parametric.straight(
length=lengths[(turns + 1) % 2] - 2 * separation + radius, **straight_kwds
)
arm1 = c.add_reference(straight).connect(p1, arm1[p0])
if align_ports == "x":
straight = _pf.parametric.straight(
length=lengths[(turns + 1) % 2] - 2 * separation, **straight_kwds
)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
arm0 = c.add_reference(bend0).connect(p0, arm0[p1])
straight = _pf.parametric.straight(length=lengths[turns % 2], **straight_kwds)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
arm0 = c.add_reference(bend0).connect(p0, arm0[p1])
straight = _pf.parametric.straight(
length=lengths[(turns + 1) % 2] - separation + radius, **straight_kwds
)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
elif align_ports == "y":
straight = _pf.parametric.straight(
length=lengths[(turns + 1) % 2] - 2 * separation, **straight_kwds
)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
arm0 = c.add_reference(bend0).connect(p0, arm0[p1])
straight = _pf.parametric.straight(length=lengths[turns % 2] - separation, **straight_kwds)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
arm0 = c.add_reference(bend1).connect(p0, arm0[p1])
else:
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
if inner_size[1] == 0:
c.remove(start)
dx = -arm1[p0].center
for ref in c.references:
ref.translate(dx)
p0 = c.add_port(arm1[p0])
p1 = c.add_port(arm0[p1])
return c, p0, p1
[docs]
@_pf.parametric_component
def rectangular_spiral(
*,
port_spec: _typ.Optional[_PortSpec] = None,
turns: _typ.Optional[_OptIntTurns] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
separation: _typ.Optional[_OptDimension] = None,
size: _typ.Optional[_OptSize] = None,
full_length: _OptNonZeroDimension = None,
align_ports: _typ.Optional[_OptAxis] = None,
active_model: _typ.Optional[_Model] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
bend_kwargs: _typ.Optional[_Kwargs] = None,
straight_kwargs: _typ.Optional[_Kwargs] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
circuit_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Rectangular spiral.
Args:
port_spec: Port specification describing waveguide cross-section.
turns: Number of turns in each of the 2 spiral arms.
radius: Bend radius for the spiral turns.
separation: Distance between waveguide centers in parallel sections.
If ``None``, defaults to the port width.
size: Spiral dimensions measured from the waveguide centers. If
``None``, defaults to ``(0, 0)``.
full_length: Desired spiral length. If set to a positive value,
'turns' and 'size[1]' are calculated automatically.
align_ports: Optionally align ports to have centers with same
``"x"`` or ``"y"`` coordinates.
active_model: Name of the model to be used by default; must be
either ``"Tidy3D"`` or ``"Circuit"`` (default).
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
bend_kwargs: Dictionary of keyword arguments for :func:`bend`.
straight_kwargs: Dictionary of keyword arguments for
:func:`straight`.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
circuit_model_kwargs: Dictionary of keyword arguments passed to
the component's :class:`photonforge.CircuitModel`.
Returns:
Component with path sections, ports and model.
Note:
The full length of the spiral can be computed with the
:func:`photonforge.route_length` function.
"""
if technology is None:
technology = _pf.config.default_technology
function = "rectangular_spiral"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = technology.ports[port_spec]
radius = _get_default(
function,
"radius",
radius,
port_spec.default_radius if port_spec.default_radius > 0 else None,
)
turns = _get_default(function, "turns", turns, 0)
separation = _get_default(function, "separation", separation, 0)
size = _get_default(function, "size", size, (0, 0))
full_length = _get_default(function, "full_length", full_length, 0)
align_ports = _get_default(function, "align_ports", align_ports, "")
active_model = _get_default(function, "active_model", active_model, "Circuit")
name = _get_default(function, "name", name, "")
bend_kwargs = _get_default(function, "bend_kwargs", bend_kwargs, {})
straight_kwargs = _get_default(function, "straight_kwargs", straight_kwargs, {})
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
circuit_model_kwargs = _get_default(function, "circuit_model_kwargs", circuit_model_kwargs, {})
if full_length <= 0 and turns < 2:
raise ValueError("Argument 'turns' must be at least 2.")
if separation <= 0:
separation = port_spec.width
if align_ports == "none":
align_ports = ""
if align_ports not in ("x", "y", ""):
raise ValueError("Argument 'align_ports' must be one of 'x', 'y', 'none', or ''.")
straight_kwds = dict(straight_kwargs)
straight_kwds["technology"] = technology
straight_kwds["port_spec"] = port_spec
bend_kwds = dict(bend_kwargs)
bend_kwds["technology"] = technology
bend_kwds["port_spec"] = port_spec
bend0 = _pf.parametric.bend(radius=radius, angle=-90, **bend_kwds)
bend1 = _pf.parametric.bend(radius=radius, angle=90, **bend_kwds)
args = [radius, separation, size, align_ports, technology, name, straight_kwds, bend0, bend1]
if full_length > 0:
if turns != 0:
_warn.warn(
"When 'full_length' is specified, argument 'turns' has no effect.",
RuntimeWarning,
3,
)
# Calculate turns and size[1]
t0 = 2
c0, p0, _ = _rectangular_spiral_geometry(t0, *args)
l0 = _pf.route_length(c0)
if l0 > full_length:
raise RuntimeError(f"Length {full_length} μm is too short for the current bend radius.")
t1 = 3
c1, *_ = _rectangular_spiral_geometry(t1, *args)
l1 = _pf.route_length(c1)
while l1 <= full_length:
t0 = t1
c0 = c1
l0 = l1
t1 *= 2
c1, *_ = _rectangular_spiral_geometry(t1, *args)
l1 = _pf.route_length(c1)
x = (full_length - l0) / (l1 - l0)
turns = min(t1 - 1, max(t0 + 1, int(0.5 + t0 * (1.0 - x) + t1 * x)))
while t1 - t0 > 1:
c, *_ = _rectangular_spiral_geometry(turns, *args)
new_len = _pf.route_length(c)
if new_len <= full_length:
l0 = new_len
t0 = turns
c0 = c
else:
l1 = new_len
t1 = turns
x = (full_length - l0) / (l1 - l0)
turns = min(t1 - 1, max(t0 + 1, int(0.5 + t0 * (1.0 - x) + t1 * x)))
turns = t0
arms = (1 + turns) // 2 * 2
if align_ports == "":
arms -= 1
ymax = ymin = c0[p0].center[1]
for reference in c0.references:
for port_list in reference.get_ports().values():
for port in port_list:
y = port.center[1]
ymin = min(ymin, y)
ymax = max(ymax, y)
err = full_length - l0
args[2] = (size[0], (ymax - ymin) + err / arms)
c, p0, p1 = _rectangular_spiral_geometry(turns, *args)
c.properties.__thumbnail__ = "wg"
c.add_model(_pf.CircuitModel(**dict(circuit_model_kwargs)), "Circuit", False)
model_kwargs = {"port_symmetries": [(p1, p0)]}
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D", False)
c.activate_model(active_model)
return c
def _spiral_expression(turns, r_min, delta_r, phi0, inwards):
phi0 *= _np.pi / 180
if inwards:
r0 = r_min + turns * delta_r
else:
r0 = r_min
turns = -turns
dr = -turns * delta_r
dphi = 2 * _np.pi * turns
return _pf.Expression(
"u",
[
("phi", f"{phi0} + u * {dphi}"),
("r", f"{r0} + u * {dr}"),
("x0", r0 * _np.cos(phi0)),
("y0", r0 * _np.sin(phi0)),
("x", "r * cos(phi) - x0"), # make sure the path starts at (0, 0)
("y", "r * sin(phi) - y0"),
("dx_du", f"{dr} * cos(phi) - r * sin(phi) * {dphi}"),
("dy_du", f"{dr} * sin(phi) + r * cos(phi) * {dphi}"),
],
)
def _circular_spiral_geometry(turns, port_spec, radius, separation, align_ports, name, technology):
c = _pf.Component(name, technology=technology)
delta_r = 2 * separation
straight_length = radius + turns * delta_r + separation
center = (
straight_length,
2 * (radius + ((turns + 0.5) if align_ports else turns) * separation),
)
path_end = (
(0, separation) if align_ports else (2 * straight_length, 4 * (radius + turns * separation))
)
max_evals = max(10000, int(1000 * radius * turns))
path_length = 0
for layer, path in port_spec.get_paths((0, 0)):
# It is important to have an "well-behaved" path section before and after the parametric
# section because the gradient vector of the spiral is not perfectly aligned to the y axis
# neither at the beginning nor at the end of the spiral, which can lead to discontinuities
# in the GDSII when joining another path section.
if straight_length > 0:
path.segment((straight_length, 0))
if align_ports:
path.parametric(
_spiral_expression(turns + 0.5, 2 * radius, delta_r, -90, True), max_evals=max_evals
)
angle = (-90 + 360 * (turns + 0.5)) % 360
elif turns > 0:
path.parametric(
_spiral_expression(turns, 2 * radius, delta_r, -90, True), max_evals=max_evals
)
angle = (-90 + 360 * turns) % 360
else:
angle = -90
path.arc(angle, angle + 180, radius, euler_fraction=0.0, endpoint=center)
path.arc(angle, angle - 180, radius, euler_fraction=0.0)
if turns > 0:
path.parametric(
_spiral_expression(turns, 2 * radius, delta_r, angle + 180, False),
max_evals=max_evals,
)
if straight_length > 0:
path.segment(path_end)
c.add(layer, path)
if path_length == 0:
path_length = path.length()
return c, path_length, path_end
[docs]
@_pf.parametric_component
def circular_spiral(
*,
port_spec: _typ.Optional[_PortSpec] = None,
turns: _typ.Optional[_OptDimension] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
separation: _typ.Optional[_OptDimension] = None,
full_length: _OptNonZeroDimension = None,
align_ports: _typ.Optional[_OptBool] = None,
active_model: _typ.Optional[_Model] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
waveguide_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Circular spiral.
Args:
port_spec: Port specification describing waveguide cross-section.
turns: Number of turns in each of the 2 spiral arms. Does not need
to be an integer.
radius: Bend radius for the internal spiral turns.
separation: Distance between waveguide centers in parallel sections.
If ``None``, defaults to the port width.
full_length: Desired spiral length. If set to a positive value,
'turns' is calculated automatically.
align_ports: Optionally align ports on the same side of the spiral.
If ``None``, defaults to ``False``.
active_model: Name of the model to be used by default; must be
either ``"Tidy3D"`` or ``"Waveguide"`` (default).
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
waveguide_model_kwargs: Dictionary of keyword arguments passed to
the component's :class:`photonforge.WaveguideModel`.
Returns:
Component with the spiral section, ports and model.
Note:
The full length of the spiral can be computed with the
:func:`photonforge.route_length` function.
"""
if technology is None:
technology = _pf.config.default_technology
function = "circular_spiral"
port_spec = _get_default(function, "port_spec", port_spec)
if isinstance(port_spec, str):
port_spec = technology.ports[port_spec]
radius = _get_default(
function,
"radius",
radius,
port_spec.default_radius if port_spec.default_radius > 0 else None,
)
turns = _get_default(function, "turns", turns, 0)
separation = _get_default(function, "separation", separation, 0)
full_length = _get_default(function, "full_length", full_length, 0)
align_ports = _get_default(function, "align_ports", align_ports, False)
active_model = _get_default(function, "active_model", active_model, "Waveguide")
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
waveguide_model_kwargs = _get_default(
function, "waveguide_model_kwargs", waveguide_model_kwargs, {}
)
if full_length <= 0 and turns <= 0:
raise ValueError("Argument 'turns' must be positive.")
if separation <= 0:
separation = port_spec.width
args = (port_spec, radius, separation, align_ports, name, technology)
if full_length > 0:
if turns > 0:
_warn.warn(
"When 'full_length' is specified, argument 'turns' has no effect.",
RuntimeWarning,
3,
)
t0 = 0
_, l0, _ = _circular_spiral_geometry(t0, *args)
if l0 > full_length:
raise RuntimeError(f"Length {full_length} μm is too short for the current bend radius.")
t1 = 1
_, l1, _ = _circular_spiral_geometry(t1, *args)
while l1 < full_length:
t0 = t1
l0 = l1
t1 *= 2
_, l1, _ = _circular_spiral_geometry(t1, *args)
x = (full_length - l0) / (l1 - l0)
turns = t0 * (1.0 - x) + t1 * x
c, path_length, path_end = _circular_spiral_geometry(turns, *args)
while abs(full_length - path_length) > _pf.config.tolerance * 0.5:
if path_length < full_length:
l0 = path_length
t0 = turns
else:
l1 = path_length
t1 = turns
x = (full_length - l0) / (l1 - l0)
turns = t0 * (1.0 - x) + t1 * x
c, path_length, path_end = _circular_spiral_geometry(turns, *args)
else:
c, path_length, path_end = _circular_spiral_geometry(turns, *args)
c.properties.__thumbnail__ = "wg"
p0 = c.add_port(_pf.Port((0, 0), 0, port_spec))
p1 = c.add_port(_pf.Port(path_end, 0 if align_ports else 180, port_spec))
model_kwargs = {"length": path_length}
model_kwargs.update(waveguide_model_kwargs)
c.add_model(_pf.WaveguideModel(**model_kwargs), "Waveguide", False)
model_kwargs = {"port_symmetries": [(p1, p0)]}
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D", False)
c.activate_model(active_model)
return c
def _get_port_or_terminal(
arg: _typ.Union[
_pf.Port, _pf.Terminal, tuple[_pf.Reference, str], tuple[_pf.Reference, str, int]
],
get_ports: bool,
) -> _pf.Port:
n = "port" if get_ports else "terminal"
error = TypeError(
f"Argument '{n}*' must be a {n.capitalize()} instance or a tuple with a Reference, the "
f"{n} name, and, optionally, the reference index in case of a reference array."
)
if isinstance(arg, _pf.Port):
if not get_ports:
raise error
return arg
if isinstance(arg, _pf.Terminal):
if get_ports:
raise error
return arg
len_arg = len(arg)
if (
len_arg < 2
or len_arg > 3
or not isinstance(arg[0], _pf.Reference)
or not isinstance(arg[1], str)
or (len_arg == 3 and not isinstance(arg[2], int))
):
raise error
if get_ports:
return arg[0].get_ports(arg[1])[0 if len_arg == 2 else arg[2]]
return arg[0].get_terminals(arg[1])[0 if len_arg == 2 else arg[2]]
[docs]
@_pf.parametric_component
def route(
*,
port1: _typ.Optional[_Port] = None,
port2: _typ.Optional[_Port] = None,
radius: _typ.Optional[_NonZeroDimension] = None,
waypoints: _typ.Optional[_OptCoords] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
straight_kwargs: _typ.Optional[_Kwargs] = None,
bend_kwargs: _typ.Optional[_Kwargs] = None,
s_bend_kwargs: _typ.Optional[_Kwargs] = None,
circuit_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Route the connection between 2 compatible ports.
The route is built heuristically from :func:`straight`, :func:`bend`,
and :func:`s_bend` sections, favoring Manhattan geometry.
Args:
port1: First port to be connected. The port can be specfied as a
:class:`photonforge.Port` or as a tuple including a
:class:`photonforge.Reference`, the port name, and the repetition
index (optional, only for array references).
port2: Second port to be connected.
radius: Radius used for bends.
waypoints: 2D coordinates used to guide the route (see note).
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
straight_kwargs: Dictionary of keyword arguments passed to the
:func:`straight` function.
bend_kwargs: Dictionary of keyword arguments passed to the
:func:`bend` function.
s_bend_kwargs: Dictionary of keyword arguments passed to the
:func:`s_bend` function.
circuit_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.CircuitModel`.
Returns:
Component with the route, including ports and model.
Note:
Each waypoint can also include the route direction at that point by
including the angle (in degrees). Angles must be a multiple of 90°.
"""
function = "route"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
radius = _get_default(function, "radius", radius, ())
waypoints = _get_default(function, "waypoints", waypoints, ())
name = _get_default(function, "name", name, "")
straight_kwargs = _get_default(function, "straight_kwargs", straight_kwargs, {})
bend_kwargs = _get_default(function, "bend_kwargs", bend_kwargs, {})
s_bend_kwargs = _get_default(function, "s_bend_kwargs", s_bend_kwargs, {})
circuit_model_kwargs = _get_default(function, "circuit_model_kwargs", circuit_model_kwargs, {})
port1 = _get_port_or_terminal(port1, True)
port2 = _get_port_or_terminal(port2, True)
if not port1.can_connect_to(port2):
raise RuntimeError("Ports have incompatible specifications and cannot be connected.")
if technology is None:
technology = _pf.config.default_technology
port_spec = port1.spec if port1.inverted else port1.spec.inverted()
straight_kwargs = dict(straight_kwargs)
straight_kwargs["technology"] = technology
straight_kwargs["port_spec"] = port_spec
bend_kwargs = dict(bend_kwargs)
bend_kwargs["technology"] = technology
bend_kwargs["port_spec"] = port_spec
if radius != ():
bend_kwargs["radius"] = radius
s_bend_kwargs = dict(s_bend_kwargs)
s_bend_kwargs["technology"] = technology
s_bend_kwargs["port_spec"] = port_spec
wp = _np.empty((len(waypoints), 3))
for i, p in enumerate(waypoints):
wp[i, 0] = p[0]
wp[i, 1] = p[1]
wp[i, 2] = p[2] % 360 if len(p) > 2 else -1
component = _pf.Component(name, technology=technology)
component.properties.__thumbnail__ = "wg"
dir0 = (port1.input_direction + 180) % 360
p0 = _pf.Port(port1.center, dir0, port1.spec, inverted=not port1.inverted)
dir1 = (port2.input_direction + 180) % 360
p1 = _pf.Port(port2.center, dir1, port2.spec, inverted=not port2.inverted)
component.add_port([p0, p1])
component.add_model(_pf.CircuitModel(**dict(circuit_model_kwargs)), "Circuit")
return _pf.extension._route(
component,
radius,
wp,
straight,
straight_kwargs,
bend,
bend_kwargs,
s_bend,
s_bend_kwargs,
)
[docs]
@_pf.parametric_component
def route_s_bend(
*,
port1: _typ.Optional[_Port] = None,
port2: _typ.Optional[_Port] = None,
euler_fraction: _typ.Optional[_OptFraction] = None,
active_model: _typ.Optional[_Model] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
tidy3d_model_kwargs: _typ.Optional[_Kwargs] = None,
waveguide_model_kwargs: _typ.Optional[_Kwargs] = None,
) -> _pf.Component:
"""Create an S bend connecting 2 compatible ports.
Args:
port1: First port to be connected. The port can be specfied as a
:class:`photonforge.Port` or as a tuple including a
:class:`photonforge.Reference`, the port name, and the repetition
index (optional, only for array references).
port2: Second port to be connected.
euler_fraction: Fraction of the bends that is created using an Euler
spiral (see :func:`photonforge.Path.arc`). If ``None``, defaults
to 0.
active_model: Name of the model to be used by default; must be
either ``"Tidy3D"`` or ``"Waveguide"`` (default).
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
tidy3d_model_kwargs: Dictionary of keyword arguments passed to the
component's :class:`photonforge.Tidy3DModel`.
waveguide_model_kwargs: Dictionary of keyword arguments passed to
the component's :class:`photonforge.WaveguideModel`.
Returns:
Component with the route, including ports and model.
"""
function = "route_s_bend"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
active_model = _get_default(function, "active_model", active_model, "Waveguide")
name = _get_default(function, "name", name, "")
tidy3d_model_kwargs = _get_default(function, "tidy3d_model_kwargs", tidy3d_model_kwargs, {})
waveguide_model_kwargs = _get_default(
function, "waveguide_model_kwargs", waveguide_model_kwargs, {}
)
port1 = _get_port_or_terminal(port1, True)
port2 = _get_port_or_terminal(port2, True)
if not port1.can_connect_to(port2):
raise RuntimeError("Ports have incompatible specifications and cannot be connected.")
if abs((port1.input_direction - port2.input_direction) % 360 - 180) >= 1e-12:
raise RuntimeError("Ports must have opposite directions.")
if technology is None:
technology = _pf.config.default_technology
port_spec = port1.spec if port1.inverted else port1.spec.inverted()
angle = (port1.input_direction - 180) / 180 * _np.pi
direction = _np.array((_np.cos(angle), _np.sin(angle)))
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "wg"
path_length = None
for layer, path in port_spec.get_paths(port1.center):
c.add(layer, path.s_bend(port2.center, euler_fraction, direction))
if path_length is None:
path_length = path.length()
p0 = c.add_port(_pf.Port(port1.center, port1.input_direction - 180, port_spec))
p1 = c.add_port(_pf.Port(port2.center, port2.input_direction - 180, port_spec, inverted=True))
model_kwargs = {"length": path_length}
model_kwargs.update(waveguide_model_kwargs)
c.add_model(_pf.WaveguideModel(**model_kwargs), "Waveguide", False)
model_kwargs = {"port_symmetries": [(p1, p0)]}
model_kwargs.update(tidy3d_model_kwargs)
c.add_model(_pf.Tidy3DModel(**model_kwargs), "Tidy3D", False)
c.activate_model(active_model)
return c
[docs]
@_pf.parametric_component
def route_taper(
*,
terminal1: _typ.Optional[_Terminal] = None,
terminal2: _typ.Optional[_Terminal] = None,
layer: _typ.Optional[_OptLayer] = None,
offset_distance: _typ.Optional[_OptOffsets] = None,
use_box: _typ.Optional[_OptBool] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
):
"""Create a taper connecting 2 terminals.
Args:
terminal1: First terminal to be connected. The terminal can be
specified as a :class:`photonforge.Terminal` or as a tuple
including a :class:`photonforge.Reference`, the terminal name, and
the repetition index (optional, only for array references).
terminal2: Second terminal to be connected.
layer: Layer used for the connection. If ``None``, the routing layer
of the first terminal is used.
offset_distance: Offset applied to the terminal structure before
creating the envelope taper. If ``None``, defaults to 0.
use_box: Flag indicating whether to use the bounding box of the
terminal structures or the structures themselves. If ``None``,
defaults to ``True``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
Returns:
Component with the route.
"""
function = "route_taper"
terminal1 = _get_default(function, "terminal1", terminal1)
terminal2 = _get_default(function, "terminal2", terminal2)
layer = _get_default(function, "layer", layer, ())
offset_distance = _get_default(function, "offset_distance", offset_distance, 0)
use_box = _get_default(function, "use_box", use_box, True)
name = _get_default(function, "name", name, "")
terminal1 = _get_port_or_terminal(terminal1, False)
terminal2 = _get_port_or_terminal(terminal2, False)
if layer == ():
layer = terminal1.routing_layer
if terminal1.routing_layer != terminal2.routing_layer:
_warn.warn(
f"Terminals have different routing layers. Using {layer}.", RuntimeWarning, 3
)
if hasattr(offset_distance, "__float__"):
offset_distance = (offset_distance, offset_distance)
structure1 = terminal1.structure
if use_box:
structure1 = _pf.Rectangle(*structure1.bounds())
a, b = structure1.size
structure1.size = (max(0, a + 2 * offset_distance[0]), max(0, b + 2 * offset_distance[0]))
else:
if offset_distance[0] < 0:
structure1 = _pf.offset(structure1, offset_distance[0])
if offset_distance[0] != 0:
structure1 = _pf.envelope(structure1, max(0, offset_distance[0]))
structure2 = terminal2.structure
if use_box:
structure2 = _pf.Rectangle(*structure2.bounds())
a, b = structure2.size
structure2.size = (max(0, a + 2 * offset_distance[1]), max(0, b + 2 * offset_distance[1]))
else:
if offset_distance[1] < 0:
structure2 = _pf.offset(structure2, offset_distance[1])
if offset_distance[1] != 0:
structure2 = _pf.envelope(structure2, max(0, offset_distance[1]))
min1, max1 = structure1.bounds()
min2, max2 = structure2.bounds()
size1 = max1 - min1
size2 = max2 - min2
prefer_x = (size1[0] < _pf.config.grid) or (size2[0] < _pf.config.grid)
prefer_y = (size1[1] < _pf.config.grid) or (size2[1] < _pf.config.grid)
if prefer_x == prefer_y:
distance = (max2 + min2) - (max1 + min1)
prefer_x = abs(distance[0]) > abs(distance[1])
# prefer_y = not prefer_x (unused)
overlap_x = not (max1[0] < min2[0] or max2[0] < min1[0])
overlap_y = not (max1[1] < min2[1] or max2[1] < min1[1])
if (overlap_x and overlap_y) or not use_box:
taper = _pf.envelope([structure1, structure2])
elif overlap_y or (not overlap_x and prefer_x):
if max2[0] < min1[0]:
structure1, structure2 = structure2, structure1
min1, min2 = min2, min1
max1, max2 = max2, max1
taper = _pf.Polygon(
(
max1,
(min1[0], max1[1]),
min1,
(max1[0], min1[1]),
min2,
(max2[0], min2[1]),
max2,
(min2[0], max2[1]),
)
)
else:
if max2[1] < min1[1]:
structure1, structure2 = structure2, structure1
min1, min2 = min2, min1
max1, max2 = max2, max1
taper = _pf.Polygon(
(
(min1[0], max1[1]),
min1,
(max1[0], min1[1]),
max1,
(max2[0], min2[1]),
max2,
(min2[0], max2[1]),
min2,
)
)
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "connection"
c.add(layer, taper)
return c
[docs]
@_pf.parametric_component
def route_manhattan(
*,
terminal1: _typ.Optional[_Terminal] = None,
terminal2: _typ.Optional[_Terminal] = None,
direction1: _typ.Optional[_OptAxis] = None,
direction2: _typ.Optional[_OptAxis] = None,
layer: _typ.Optional[_OptLayer] = None,
width: _typ.Optional[_OptNonZeroDimension] = None,
overlap_fraction: _typ.Optional[_OptFractions] = None,
join_limit: _typ.Optional[_OptJoin] = None,
waypoints: _typ.Optional[_OptCoords] = None,
technology: _typ.Optional[_Technology] = None,
name: _typ.Optional[_Name] = None,
):
"""Create a Manhattan path connecting 2 terminals.
Args:
terminal1: First terminal to be connected. The terminal can be
specified as a :class:`photonforge.Terminal` or as a tuple
including a :class:`photonforge.Reference`, the terminal name, and
the repetition index (optional, only for array references).
terminal2: Second terminal to be connected.
direction1: Direction (`""`, `"x"`, or `"y"`) of the route at the
first terminal.
direction2: Direction (`""`, `"x"`, or `"y"`) of the route at the
second terminal.
layer: Layer used for the connection. If ``None``, the routing layer
of the first terminal is used.
width: Width of the routing path. If ``None``, the width is derived
from the bounding box of the first terminal.
overlap_fraction: Fraction of the terminal bounding box that the
route overlaps. If ``None``, defaults to 1.
join_limit: Join limit used by :func:`photonforge.Path.segment`. If
``None`` defaults to -1.
waypoints: Sequence of coordinates the route should go through.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
Returns:
Component with the route.
"""
function = "route_manhattan"
terminal1 = _get_default(function, "terminal1", terminal1)
terminal2 = _get_default(function, "terminal2", terminal2)
direction1 = _get_default(function, "direction1", direction1, "")
direction2 = _get_default(function, "direction2", direction2, "")
layer = _get_default(function, "layer", layer, ())
width = _get_default(function, "width", width, -1)
overlap_fraction = _get_default(function, "overlap_fraction", overlap_fraction, 1)
join_limit = _get_default(function, "join_limit", join_limit, -1)
waypoints = _get_default(function, "waypoints", waypoints, ())
name = _get_default(function, "name", name, "")
terminal1 = _get_port_or_terminal(terminal1, False)
terminal2 = _get_port_or_terminal(terminal2, False)
if layer == ():
layer = terminal1.routing_layer
if terminal1.routing_layer != terminal2.routing_layer:
_warn.warn(
f"Terminals have different routing layers. Using {layer}.", RuntimeWarning, 3
)
if hasattr(overlap_fraction, "__float__"):
overlap_fraction = (overlap_fraction, overlap_fraction)
centers = [None, None]
sizes = [None, None]
directions = [-1, -1]
for i, (terminal, direction) in enumerate(((terminal1, direction1), (terminal2, direction2))):
min_, max_ = terminal.structure.bounds()
sizes[i] = max_ - min_
centers[i] = 0.5 * (min_ + max_)
if direction == "x":
directions[i] = 0
elif direction == "y":
directions[i] = 1
elif sizes[i][0] < _pf.config.grid and sizes[i][1] >= _pf.config.grid:
directions[i] = 0
elif sizes[i][1] < _pf.config.grid and sizes[i][0] >= _pf.config.grid:
directions[i] = 1
endpoints = _pf.extension._manhatan_path(
centers[0], centers[1], directions[0], directions[1], waypoints
)
direction = 1 if endpoints[0][0] == endpoints[1][0] else 0
delta = sizes[0][direction] * (0.5 - overlap_fraction[0])
endpoints[0][direction] += (
delta if endpoints[0][direction] < endpoints[1][direction] else -delta
)
if width < 0:
width = sizes[0][1 - direction]
direction = 1 if endpoints[-1][0] == endpoints[-2][0] else 0
delta = sizes[1][direction] * (0.5 - overlap_fraction[1])
endpoints[-1][direction] += (
delta if endpoints[-1][direction] < endpoints[-2][direction] else -delta
)
c = _pf.Component(name, technology=technology)
c.properties.__thumbnail__ = "connection"
c.add(layer, _pf.Path(endpoints[0], width).segment(endpoints[1:], join_limit=join_limit))
return c