import itertools as _it
import json as _json
import typing as _typ
import warnings as _warn
from collections import namedtuple as _namedtuple
from collections.abc import Sequence as _Sequence
import numpy as _np
from . import extension as _ext
from . import typing as _pft
from .models.circuit import CircuitModel as _CircuitModel
from .models.circuit import DirectionalCouplerCircuitModel as _DirectionalCouplerCircuitModel
from .models.tidy3d import Tidy3DModel as _Tidy3DModel
from .models.waveguide import WaveguideModel as _WaveguideModel
from .parametric_utils import _gdsii_safe
from .parametric_utils import parametric_component as _parametric_component
from .utils import _angles_equal, _is_multiple_of_90
from .utils import route_length as _route_length
_Axis = _typ.Literal["", "x", "y"]
_VariableOffset = _pft.annotate(float | _pft.expression(1, 1), units="μm")
_PortSpecOrName = _pft.annotate(str | _ext.PortSpec)
_PortSpec_x2 = _pft.annotate(_Sequence[_PortSpecOrName], minItems=2, maxItems=2)
_PortSpecPair = _PortSpecOrName | _PortSpec_x2
_ReferencePort = tuple[_ext.Reference, str] | tuple[_ext.Reference, str, int]
_Port = _ext.Port | _ReferencePort
_Terminal = _ext.Terminal | tuple[_ext.Reference, str] | tuple[_ext.Reference, str, int]
_RouteObstacle = (
_ext.Rectangle | _ext.Circle | _ext.Polygon | _ext.Path | _ext.Component | _ext.Reference
)
def _get_default(function: object, kwarg: object, value: object, default: object = None) -> object:
if value is not None:
return value
func_kwargs = _ext.config.default_kwargs.get(function)
if isinstance(func_kwargs, dict):
value = func_kwargs.get(kwarg)
if value is not None:
return value
value = _ext.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]
@_parametric_component
def straight(
*,
port_spec: _PortSpecOrName | None = None,
length: _pft.PositiveDimension | None = None,
endpoint: _pft.Coordinate2D | None = None,
bulge_width: _pft.Coordinate | None = None,
bulge_taper_length: _pft.Dimension | None = None,
bulge_margin: _pft.Dimension | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.Component:
"""Straight waveguide section.
Args:
port_spec: Port specification describing waveguide cross-section.
length: Section length. Mutually exclusive with ``endpoint``.
endpoint: Section endpoint relative to the start, building a
waveguide at an arbitrary angle with ports placed exactly at
both ends. Mutually exclusive with ``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.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.WaveguideModel` is used.
Returns:
Component with the straight section, ports and model.
"""
function = "straight"
port_spec = _get_default(function, "port_spec", port_spec)
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)
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _WaveguideModel())
if technology is None:
technology = _ext.config.default_technology
if isinstance(port_spec, str):
port_spec = technology.ports[port_spec]
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "wg"
c.properties.__labels__ = ["routing"]
c.add_model(model)
if endpoint is not None:
if length is not None:
raise ValueError("Arguments 'length' and 'endpoint' are mutually exclusive.")
endpoint = _np.array(endpoint)
length = (endpoint[0] ** 2 + endpoint[1] ** 2) ** 0.5
angle = _np.degrees(_np.arctan2(endpoint[1], endpoint[0]))
else:
length = _get_default(function, "length", length)
length = _ext.snap_to_grid(length)
if length < 0:
raise ValueError("Argument 'length' may not negative.")
endpoint = _np.array((length, 0))
angle = 0
bulge_region = (bulge_margin + bulge_taper_length, length - bulge_margin - bulge_taper_length)
if (
length > 0
and bulge_width != 0
and bulge_taper_length > 0
and bulge_margin >= 0
and bulge_region[1] >= bulge_region[0]
):
u = endpoint / length
for width, offset, layer in port_spec.path_profiles_list():
path = _ext.Path((0, 0), width, offset)
if bulge_margin > 0:
path.segment(bulge_margin * u)
path.segment(bulge_region[0] * u, width + bulge_width)
if bulge_region[1] > bulge_region[0]:
path.segment(bulge_region[1] * u)
if bulge_margin > 0:
path.segment((length - bulge_margin) * u, width)
path.segment(endpoint, width)
c.add(layer, path)
else:
for layer, path in port_spec.get_paths((0, 0)):
c.add(layer, path.segment(endpoint))
c.add_port(_ext.Port((0, 0), angle, port_spec))
c.add_port(_ext.Port(endpoint, angle + 180, port_spec, inverted=True))
return c
[docs]
@_parametric_component
def transition(
*,
port_spec1: _pft.annotate(_PortSpecOrName, label="Port Spec 1") | None = None,
port_spec2: _pft.annotate(_PortSpecOrName, label="Port Spec 2") | None = None,
length: _pft.PositiveDimension | None = None,
constant_length: _pft.Dimension | None = None,
profile: _pft.expression(1, 1) | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.Tidy3DModel` is used.
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, "")
model = _get_default(function, "model", model, _Tidy3DModel())
if length <= 0 and constant_length <= 0:
raise ValueError("Transition length cannot be 0.")
if isinstance(profile, _ext.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: float, b: float) -> _ext.Expression:
return _ext.Expression(
parameter,
[*expressions, f"{a} + {value_name} * {b - a}", f"{b - a}"],
)
if technology is None:
technology = _ext.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 = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "transition"
c.add_model(model)
start = _ext.snap_to_grid(constant_length)
mid = _ext.snap_to_grid(constant_length + length)
end = _ext.snap_to_grid(2 * constant_length + length)
for layer in only1:
for w1, g1, l1 in path_profiles1:
if l1 != layer:
continue
path = _ext.Path((0, 0), w1, g1)
if start > 0:
path.segment((start, 0), (w1, "constant"), (g1, "constant"))
if mid > start:
path.segment((mid, 0), width=interp(w1, 0))
c.add(layer, path)
for layer in only2:
for w2, g2, l2 in path_profiles2:
if l2 != layer:
continue
path = _ext.Path((start, 0), 0, g2)
if mid > start:
path.segment((mid, 0), width=interp(0, w2))
if end > mid:
path.segment((end, 0), (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, strict=False)
if len(prof1) == len(prof2)
else _it.product(prof1, prof2)
)
for (g1, w1), (g2, w2) in combinations:
path = _ext.Path((0, 0), w1, g1)
if start > 0:
path.segment((start, 0), (w1, "constant"), (g1, "constant"))
if mid > start:
path.segment((mid, 0), width=interp(w1, w2), offset=interp(g1, g2))
else:
c.add(layer, path)
path = _ext.Path((mid, 0), w2, g2)
if end > mid:
path.segment((end, 0), (w2, "constant"), (g2, "constant"))
c.add(layer, path)
c.add_port(_ext.Port((0, 0), 0, port_spec1))
c.add_port(_ext.Port((end, 0), 180, port_spec2, inverted=True))
return c
[docs]
@_parametric_component
def bend(
*,
port_spec: _PortSpecOrName | None = None,
radius: _pft.PositiveDimension | None = None,
angle: _pft.annotate(_pft.Angle, minimum=-180, maximum=180) | None = None,
euler_fraction: _pft.Fraction | None = None,
port_bends: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.WaveguideModel` is used.
Returns:
Component with the circular bend section, ports and model.
"""
if technology is None:
technology = _ext.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)
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _WaveguideModel())
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 = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "bend"
c.properties.__labels__ = ["routing"]
c.add_model(model)
p0 = c.add_port(_ext.Port((0, 0), 0, port_spec))
if angle > 0:
radians = (angle - 90) / 180.0 * _np.pi
endpoint = _ext.snap_to_grid((radius * _np.cos(radians), radius * (1 + _np.sin(radians))))
port = _ext.Port(endpoint, angle - 180, port_spec, inverted=True)
for layer, path in port_spec.get_paths((0, 0)):
path.arc(-90, angle - 90, radius, euler_fraction=euler_fraction, endpoint=port.center)
c.add(layer, path)
p1 = c.add_port(port)
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 = _ext.snap_to_grid((radius * _np.cos(radians), radius * (-1 + _np.sin(radians))))
port = _ext.Port(endpoint, angle + 180, port_spec, inverted=True)
for layer, path in port_spec.get_paths((0, 0)):
path.arc(90, 90 + angle, radius, euler_fraction=euler_fraction, endpoint=port.center)
c.add(layer, path)
p1 = c.add_port(port)
if port_bends and euler_fraction == 0:
c[p0].bend_radius = -radius
c[p1].bend_radius = radius
return c
[docs]
@_parametric_component
def s_bend(
*,
port_spec: _PortSpecOrName | None = None,
length: _pft.PositiveDimension | None = None,
offset: _pft.Coordinate | None = None,
euler_fraction: _pft.Fraction | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.WaveguideModel` is used.
Returns:
Component with the S bend section, ports and model.
"""
if technology is None:
technology = _ext.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 = _ext.s_bend_length(abs_offset, radius)
length = _get_default(function, "length", length, default_length)
euler_fraction = _get_default(function, "euler_fraction", euler_fraction, 0)
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _WaveguideModel())
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "s-bend"
c.properties.__labels__ = ["routing"]
c.add_model(model)
length = _ext.snap_to_grid(length)
offset = _ext.snap_to_grid(offset)
for layer, path in port_spec.get_paths((0, 0)):
c.add(layer, path.s_bend((length, offset), euler_fraction))
c.add_port(_ext.Port((0, 0), 0, port_spec))
c.add_port(_ext.Port((length, offset), 180, port_spec, inverted=True))
return c
[docs]
@_parametric_component
def crossing(
*,
port_spec: _PortSpecPair | None = None,
arm_length: _pft.PositiveDimension | None = None,
added_width: _VariableOffset | None = None,
extra_length: _pft.Dimension | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.Tidy3DModel` is used.
Returns:
Component with the crossing, ports and model.
"""
if technology is None:
technology = _ext.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, _ext.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, "")
model = _get_default(function, "model", model, _Tidy3DModel())
if isinstance(added_width, _ext.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})"))
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "crossing"
c.properties.__labels__ = ["routing"]
c.add_model(model)
arm_length = _ext.snap_to_grid(arm_length)
length = _ext.snap_to_grid(arm_length + extra_length)
for i in range(2):
v = 1 - i + 1j * i
for width, offset, layer in port_spec[i].path_profiles_list():
width_expr = _ext.Expression(
parameter, [*expressions, f"{width} + {value_name}", ("derivative", 0)]
)
arm = _ext.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)
p0 = c.add_port(_ext.Port((-length, 0), 0, port_spec[0]))
p1 = c.add_port(_ext.Port((0, -length), 90, port_spec[1]))
p2 = c.add_port(_ext.Port((length, 0), 180, port_spec[0], inverted=True))
p3 = c.add_port(_ext.Port((0, length), -90, port_spec[1], inverted=True))
c.properties.__internal_routes__ = [[p0, p2], [p1, p3]]
return c
[docs]
@_parametric_component
def crossing45(
*,
port_spec: _PortSpecPair | None = None,
arm_length: _pft.PositiveDimension | None = None,
added_width: _VariableOffset | None = None,
extra_length: _pft.Dimension | None = None,
radius: _pft.PositiveDimension | None = None,
euler_fraction: _pft.Fraction | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.Tidy3DModel` is used.
Returns:
Component with the crossing, ports and model.
"""
if technology is None:
technology = _ext.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, _ext.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, "")
model = _get_default(function, "model", model, _Tidy3DModel())
if isinstance(added_width, _ext.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 = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "crossing"
c.properties.__labels__ = ["routing"]
c.add_model(model)
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 = _ext.config.grid
_ext.config.grid = _ext.config.tolerance
xp[i] = _ext.snap_to_grid(projected_arm_length + projected_extra_length + arc_x)
yp[i] = _ext.snap_to_grid(projected_arm_length + projected_extra_length + arc_y)
_ext.config.grid = old_grid
for width, offset, layer in port_spec[i].path_profiles_list():
width_expr = _ext.Expression(
parameter, [*expressions, f"{width} + {value_name}", ("derivative", 0)]
)
arm = _ext.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)
p0 = c.add_port(_ext.Port((-xp[0], -yp[0]), 0, port_spec[0]))
p1 = c.add_port(_ext.Port((-xp[1], yp[1]), 0, port_spec[1]))
p2 = c.add_port(_ext.Port((xp[1], -yp[1]), 180, port_spec[1], inverted=True))
p3 = c.add_port(_ext.Port((xp[0], yp[0]), 180, port_spec[0], inverted=True))
c.properties.__internal_routes__ = [[p0, p3], [p1, p2]]
return c
[docs]
@_parametric_component
def ring_coupler(
*,
port_spec: _PortSpecPair | None = None,
coupling_distance: _pft.Coordinate | None = None,
radius: _pft.PositiveDimension | None = None,
bus_length: _pft.Dimension | None = None,
euler_fraction: _pft.Fraction | None = None,
coupling_length: _pft.Dimension | None = None,
port_bends: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.DirectionalCouplerCircuitModel` is used.
Returns:
Coupling component.
"""
if technology is None:
technology = _ext.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, _ext.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, "")
model = _get_default(function, "model", model, "default")
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
xp = _ext.snap_to_grid(bus_length + 0.5 * coupling_length)
yp = _ext.snap_to_grid(-radius - coupling_distance)
xr = _ext.snap_to_grid(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:
p0 = c.add_port(_ext.Port((-xp, yp), 0, port_spec[0]))
p1 = c.add_port(_ext.Port((-xr, 0), -90, port_spec[1], inverted=True))
if xp > 0:
p2 = c.add_port(_ext.Port((xp, yp), 180, port_spec[0], inverted=True))
p3 = c.add_port(_ext.Port((xr, 0), -90, port_spec[1]))
if port_bends and euler_fraction == 0:
c[p1].bend_radius = radius
c[p3].bend_radius = -radius
if model == "default":
if xp > 0:
model = _DirectionalCouplerCircuitModel(
arms_model={
p0: _WaveguideModel(),
p1: _Tidy3DModel(),
p2: _WaveguideModel(),
p3: _Tidy3DModel(),
}
)
else:
model = _Tidy3DModel()
c.add_model(model)
return c
[docs]
@_parametric_component
def s_bend_ring_coupler(
*,
port_spec: _PortSpecPair | None = None,
coupling_distance: _pft.Coordinate | None = None,
radius: _pft.PositiveDimension | None = None,
s_bend_length: _pft.PositiveDimension | None = None,
s_bend_offset: _pft.Coordinate | None = None,
euler_fraction: _pft.Fraction | None = None,
coupling_length: _pft.Dimension | None = None,
port_bends: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.DirectionalCouplerCircuitModel` is used.
Returns:
Coupling component.
"""
if technology is None:
technology = _ext.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, _ext.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 = _ext.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, "")
model = _get_default(function, "model", model, "default")
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
xs = _ext.snap_to_grid(s_bend_length + 0.5 * coupling_length)
ys = _ext.snap_to_grid(-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 = _ext.snap_to_grid(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)
p0 = c.add_port(_ext.Port((-xs, ys), 0, port_spec[0]))
p1 = c.add_port(_ext.Port((-xr, 0), -90, port_spec[1], inverted=True))
p2 = c.add_port(_ext.Port((xs, ys), 180, port_spec[0], inverted=True))
p3 = c.add_port(_ext.Port((xr, 0), -90, port_spec[1]))
if port_bends and euler_fraction == 0:
c[p1].bend_radius = radius
c[p3].bend_radius = -radius
if model == "default":
model = _DirectionalCouplerCircuitModel(
arms_model={
p0: _WaveguideModel(),
p1: _Tidy3DModel(),
p2: _WaveguideModel(),
p3: _Tidy3DModel(),
}
)
c.add_model(model)
return c
[docs]
@_parametric_component
def dual_ring_coupler(
*,
port_spec: _PortSpecPair | None = None,
coupling_distance: _pft.Coordinate | None = None,
radius: _pft.PositiveDimension | None = None,
euler_fraction: _pft.Fraction | None = None,
coupling_length: _pft.Dimension | None = None,
port_bends: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.DirectionalCouplerCircuitModel` is used.
Returns:
Coupling component.
"""
if technology is None:
technology = _ext.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, _ext.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, "")
model = _get_default(function, "model", model, _DirectionalCouplerCircuitModel())
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
c.add_model(model)
xr0 = _ext.snap_to_grid(radius[0] + 0.5 * coupling_length)
yr = _ext.snap_to_grid(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 = _ext.snap_to_grid(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(_ext.Port((-xr0, -yr), 90, port_spec[0]))
p1 = c.add_port(_ext.Port((-xr1, 0), -90, port_spec[1], inverted=True))
p2 = c.add_port(_ext.Port((xr0, -yr), 90, port_spec[0], inverted=True))
p3 = c.add_port(_ext.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]
return c
[docs]
@_parametric_component
def s_bend_coupler(
*,
port_spec: _PortSpecPair | None = None,
coupling_distance: _pft.Coordinate | None = None,
s_bend_length: _pft.PositiveDimension | None = None,
s_bend_offset: _pft.Coordinate | None = None,
euler_fraction: _pft.Fraction | None = None,
coupling_length: _pft.Dimension | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.DirectionalCouplerCircuitModel` is used.
Returns:
Coupling component.
"""
if technology is None:
technology = _ext.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, _ext.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 = _ext.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, "")
model = _get_default(
function, "model", model, _DirectionalCouplerCircuitModel(arms_model=_WaveguideModel())
)
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
c.add_model(model)
x_out0 = _ext.snap_to_grid(2 * s_bend_length[0] + coupling_length)
x_out1 = _ext.snap_to_grid(s_bend_length[0] + s_bend_length[1] + coupling_length)
x_in1 = _ext.snap_to_grid(s_bend_length[0] - s_bend_length[1])
y_out1 = _ext.snap_to_grid(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(_ext.Port((0, 0), 0, port_spec[0]))
c.add_port(_ext.Port((x_in1, y_out1), 0, port_spec[1], inverted=True))
c.add_port(_ext.Port((x_out0, 0), -180, port_spec[0], inverted=True))
c.add_port(_ext.Port((x_out1, y_out1), 180, port_spec[1]))
return c
[docs]
@_parametric_component
def s_bend_straight_coupler(
*,
port_spec: _PortSpecPair | None = None,
coupling_distance: _pft.Coordinate | None = None,
s_bend_length: _pft.PositiveDimension | None = None,
s_bend_offset: _pft.Coordinate | None = None,
euler_fraction: _pft.Fraction | None = None,
coupling_length: _pft.Dimension | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.DirectionalCouplerCircuitModel` is used.
Returns:
Coupling component.
"""
if technology is None:
technology = _ext.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, _ext.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 = _ext.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, "")
model = _get_default(
function, "model", model, _DirectionalCouplerCircuitModel(arms_model=_WaveguideModel())
)
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "dc"
c.add_model(model)
xs = _ext.snap_to_grid(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 = _ext.snap_to_grid(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(_ext.Port((0, 0), 0, port_spec[0]))
c.add_port(_ext.Port((0, ys), 0, port_spec[1], inverted=True))
c.add_port(_ext.Port((xs, 0), -180, port_spec[0], inverted=True))
c.add_port(_ext.Port((xs, ys), 180, port_spec[1]))
return c
def _rectangular_spiral_geometry(
turns: int,
radius: float,
separation: float,
size: _Sequence[float],
align_ports: _Axis | None,
technology: _ext.Technology,
name: str,
straight_kwds: dict[str, object],
bend0: _ext.Component,
bend1: _ext.Component,
) -> object:
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 = _straight(length=inner_size[1], **straight_kwds)
p0, p1 = sorted(straight.ports)
c = _ext.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 = _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 = _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 = _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 = _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 = _straight(length=lengths[(turns + 1) % 2] - separation + radius, **straight_kwds)
arm0 = c.add_reference(straight).connect(p0, arm0[p1])
elif align_ports == "y":
straight = _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 = _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]
@_parametric_component
def rectangular_spiral(
*,
port_spec: _PortSpecOrName | None = None,
turns: _pft.annotate(int, minimum=2) | None = None,
radius: _pft.PositiveDimension | None = None,
separation: _pft.Dimension | None = None,
size: _pft.Dimension2D | None = None,
full_length: _pft.PositiveDimension = None,
align_ports: _Axis | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
straight_kwargs: _pft.kwargs_for(straight) | None = None,
bend_kwargs: _pft.kwargs_for(bend) | None = None,
) -> _ext.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.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.CircuitModel` is used.
straight_kwargs: Dictionary of keyword arguments for
:func:`straight`.
bend_kwargs: Dictionary of keyword arguments for :func:`bend`.
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 = _ext.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, "")
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _CircuitModel())
straight_kwargs = dict(_get_default(function, "straight_kwargs", straight_kwargs, {}))
bend_kwargs = dict(_get_default(function, "bend_kwargs", bend_kwargs, {}))
straight_kwargs["technology"] = technology
straight_kwargs["port_spec"] = port_spec
straight_kwargs.pop("length", None)
bend_kwargs["technology"] = technology
bend_kwargs["port_spec"] = port_spec
bend_kwargs["radius"] = radius
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 ''.")
bend_kwargs["angle"] = -90
bend0 = _bend(**bend_kwargs)
bend_kwargs["angle"] = 90
bend1 = _bend(**bend_kwargs)
args = [radius, separation, size, align_ports, technology, name, straight_kwargs, 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 = _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 = _route_length(c1)
while l1 <= full_length:
t0 = t1
c0 = c1
l0 = l1
t1 *= 2
c1, *_ = _rectangular_spiral_geometry(t1, *args)
l1 = _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 = _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, _, _ = _rectangular_spiral_geometry(turns, *args)
c.properties.__thumbnail__ = "wg"
c.add_model(model)
return c
def _spiral_expression(
turns: float, r_min: float, delta_r: float, phi0: float, inwards: bool
) -> _ext.Expression:
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 _ext.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: float,
port_spec: _ext.PortSpec,
radius: float,
separation: float,
align_ports: bool,
name: str,
technology: _ext.Technology,
) -> object:
c = _ext.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 = _ext.snap_to_grid(
(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]
@_parametric_component
def circular_spiral(
*,
port_spec: _PortSpecOrName | None = None,
turns: _pft.Dimension | None = None,
radius: _pft.PositiveDimension | None = None,
separation: _pft.Dimension | None = None,
full_length: _pft.PositiveDimension = None,
align_ports: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.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``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.WaveguideModel` is used.
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 = _ext.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)
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _WaveguideModel())
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) > _ext.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"
c.add_model(model)
c.add_port(_ext.Port((0, 0), 0, port_spec))
c.add_port(_ext.Port(path_end, 0 if align_ports else 180, port_spec))
return c
def _get_port_or_terminal(
arg: _ext.Port | _ext.Terminal | tuple[_ext.Reference, str] | tuple[_ext.Reference, str, int],
arg_name: str,
get_ports: bool,
) -> _ext.Port:
n = "port" if get_ports else "terminal"
error = TypeError(
f"Argument '{arg_name}' must be a {n.capitalize()} instance or a tuple with a Reference, "
f"{n} name, and, optionally, the reference index in case of a reference array."
)
if isinstance(arg, _ext.Port):
if not get_ports:
raise error
return arg
if isinstance(arg, _ext.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], _ext.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]]
def _get_reference_port(arg: _ReferencePort, arg_name: str) -> tuple[_ext.Reference, str, int]:
error = TypeError(
f"Argument '{arg_name}' must be a tuple with a Reference, port name, and, optionally, "
f"the reference index in case of a reference array."
)
len_arg = len(arg)
if (
len_arg < 2
or len_arg > 3
or not isinstance(arg[0], _ext.Reference)
or not isinstance(arg[1], str)
or (len_arg == 3 and not isinstance(arg[2], int))
):
raise error
index = 0 if len_arg == 2 else arg[2]
if index < 0:
raise ValueError(f"Argument '{arg_name}' repetition index may not be negative.")
return (arg[0], arg[1], index)
def _port_from_reference_port(
endpoint: tuple[_ext.Reference, str, int], arg_name: str
) -> _ext.Port:
ports = endpoint[0].get_ports(endpoint[1])
if endpoint[2] >= len(ports):
raise IndexError(f"Argument '{arg_name}' repetition index is out of range.")
port = ports[endpoint[2]]
if not isinstance(port, _ext.Port):
raise TypeError(f"Argument '{arg_name}' must refer to a 2D optical Port.")
return port
def _route_obstacles_with_port_references(
obstacles: _Sequence[_RouteObstacle] | _ext.Component | _ext.Reference,
route_nets: _Sequence[tuple[tuple[_ext.Reference, str, int], tuple[_ext.Reference, str, int]]],
include_port_references: bool,
) -> list[_RouteObstacle]:
if isinstance(obstacles, _ext.Component | _ext.Reference):
result = [obstacles]
else:
result = list(obstacles)
if not include_port_references:
return result
existing_references = []
for obstacle in result:
if isinstance(obstacle, _ext.Reference):
if obstacle not in existing_references:
existing_references.append(obstacle)
elif isinstance(obstacle, _ext.Component):
for reference in obstacle.references:
if reference not in existing_references:
existing_references.append(reference)
for net in route_nets:
for reference, _, _ in net:
if reference not in existing_references:
existing_references.append(reference)
result.append(reference)
return result
[docs]
@_parametric_component
def route(
*,
port1: _Port | None = None,
port2: _Port | None = None,
radius: _pft.PositiveDimension | None = None,
waypoints: _Sequence[_pft.Coordinate2D] | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
straight_kwargs: _pft.kwargs_for(straight) | None = None,
bend_kwargs: _pft.kwargs_for(bend) | None = None,
s_bend_kwargs: _pft.kwargs_for(s_bend) | None = None,
) -> _ext.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. Use
:func:`route_auto` for obstacle-aware multi-net routing.
Args:
port1: First port to be connected. The port can be specified 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.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.CircuitModel` is used.
straight_kwargs: Keyword arguments for :func:`straight`.
bend_kwargs: Keyword arguments for :func:`bend`.
s_bend_kwargs: Keyword arguments for :func:`s_bend`.
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°.
"""
if technology is None:
technology = _ext.config.default_technology
function = "route"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
port1 = _get_port_or_terminal(port1, "port1", True)
port2 = _get_port_or_terminal(port2, "port2", True)
if not port1.can_connect_to(port2):
raise RuntimeError("Ports have incompatible specifications and cannot be connected.")
port_spec = port1.spec if port1.inverted else port1.spec.inverted()
radius = _get_default(function, "radius", radius, ())
waypoints = _get_default(function, "waypoints", waypoints, ())
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _CircuitModel())
straight_kwargs = dict(_get_default(function, "straight_kwargs", straight_kwargs, {}))
bend_kwargs = dict(_get_default(function, "bend_kwargs", bend_kwargs, {}))
s_bend_kwargs = dict(_get_default(function, "s_bend_kwargs", s_bend_kwargs, {}))
straight_kwargs["technology"] = technology
straight_kwargs["port_spec"] = port_spec
bend_kwargs["technology"] = technology
bend_kwargs["port_spec"] = port_spec
if radius != ():
bend_kwargs["radius"] = radius
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 = _ext.Component(name, technology=technology)
component.properties.__thumbnail__ = "wg"
component.properties.__labels__ = ["routing"]
component.add_model(model)
dir0 = (port1.input_direction + 180) % 360
p0 = _ext.Port(port1.center, dir0, port1.spec, inverted=not port1.inverted)
dir1 = (port2.input_direction + 180) % 360
p1 = _ext.Port(port2.center, dir1, port2.spec, inverted=not port2.inverted)
component.add_port([p0, p1])
return _ext._route(
component,
radius,
wp,
_straight,
straight_kwargs,
_bend,
bend_kwargs,
_s_bend,
s_bend_kwargs,
)
_BendInfo = _namedtuple("_BendInfo", ["bend", "radius", "name0", "name1"])
def _bend_info(bend: _ext.Component) -> _BendInfo:
(n0, p0), (n1, p1) = bend.ports.items()
a0 = round(p0.input_direction) % 360
a1 = round(p1.input_direction) % 360
if not (
a0 in (0, 90, 180, 270)
and a1 in (0, 90, 180, 270)
and _angles_equal(a0, p0.input_direction)
and _angles_equal(a1, p1.input_direction)
):
raise RuntimeError(
"Bends are expected to have 2 ports, aligned to the horizontal and vertical axes."
)
if (a1 - a0) % 360 == 90:
n0, n1 = n1, n0
a0, a1 = a1, a0
elif (a1 - a0) % 360 != 270:
raise RuntimeError("Expected a 90° bend.")
ref = _ext.Reference(bend, rotation=-a0)
p0 = ref[n0]
p1 = ref[n1]
v = p1.center - p0.center
if v[0] != v[1]:
raise RuntimeError("The bend radius must be the same on both axes.")
if v[0] <= 0:
raise RuntimeError("The bend radius must be positive. Unexpected port positioning.")
return _BendInfo(bend, v[0], n0, n1)
[docs]
@_parametric_component
def route_l(
*,
port1: _Port | _Sequence[_Port] | None = None,
port2: _Port | _Sequence[_Port] | None = None,
radius: _pft.PositiveDimension | None = None,
bend: _ext.Component | _Sequence[_ext.Component] | None = None,
straight_kwargs: _pft.kwargs_for(straight) | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
route_model: _ext.Model | None = None,
bundle_model: _ext.Model | None = None,
) -> _ext.Component:
"""Route the connection between orthogonal ports.
Args:
port1: First port to be connected. The port can be specified 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). A sequence of ports
can be used for bundle routing.
port2: Second port to be connected. A sequence of ports can be used
for bundle routing (with same length as ``port1``).
radius: Radius used for bends.
bend: 90° bend to be used for routing. If the bundle has different
port specifications, a sequence of bends (one for each
specification) must be provided. If ``None``, the default
parametric bend will be used.
straight_kwargs: Keyword arguments for :func:`straight`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
route_model: Model to be used with each route sub-component. If
``None`` a :class:`photonforge.CircuitModel` is used.
bundle_model: Model to be used with the top-level route component.
If ``None`` a :class:`photonforge.CircuitModel` is used.
Returns:
Component with the route, including ports and model.
"""
if technology is None:
technology = _ext.config.default_technology
function = "route_l"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
try:
ports1 = [_get_port_or_terminal(port1, "port1", True)]
except Exception:
ports1 = [_get_port_or_terminal(p, "port1[…]", True) for p in port1]
try:
ports2 = [_get_port_or_terminal(port2, "port2", True)]
except Exception:
ports2 = [_get_port_or_terminal(p, "port2[…]", True) for p in port2]
if len(ports1) != len(ports2):
raise ValueError(
f"Arguments 'port1' and 'port2' must contain the same number of ports "
f"({len(ports1)} ≠ {len(ports2)})."
)
if len(ports1) == 0:
raise ValueError("At least one port pair must be provided.")
radius = _get_default(function, "radius", radius, ())
straight_kwargs = dict(_get_default(function, "straight_kwargs", straight_kwargs, {}))
bend = _get_default(function, "bend", bend, ())
name = _get_default(function, "name", name, "")
bundle_model = _get_default(function, "bundle_model", bundle_model, _CircuitModel())
route_model = _get_default(function, "route_model", route_model, _CircuitModel())
direction1 = round(ports1[0].input_direction) % 360
direction2 = round(ports2[0].input_direction) % 360
sign1 = 1 if direction1 in (180, 270) else -1
sign2 = 1 if direction2 in (0, 90) else -1
longitudinal = 0 if direction1 in (0, 180) else 1
transverse = 1 - longitudinal
positive_bend = (direction2 - direction1) % 360 == 270
if not (_is_multiple_of_90(direction1) and _is_multiple_of_90(direction2)):
raise RuntimeError("The input direction of all ports must be horizontal or vertical.")
if direction1 % 180 == direction2 % 180:
raise RuntimeError("Ports must have orthogonal input directions.")
for i, (port1, port2) in enumerate(zip(ports1, ports2, strict=True)):
if not _angles_equal(port1.input_direction, direction1):
raise RuntimeError(
f"The input direction of port1[{i}] does not match the expected {direction1}°."
)
if not _angles_equal(port2.input_direction, direction2):
raise RuntimeError(
f"The input direction of port2[{i}] does not match the expected {direction2}°."
)
if not port1.can_connect_to(port2):
raise RuntimeError(
f"port1[{i}] and port2[{i}] have incompatible specifications and cannot be "
f"connected."
)
nets = sorted(zip(ports1, ports2, strict=True), key=lambda x: x[0].center[transverse])
z1 = nets[0][0].center[transverse]
z2 = nets[0][1].center[longitudinal]
increasing = (longitudinal == 1) is positive_bend
for port1, port2 in nets[1:]:
c1 = port1.center[transverse]
c2 = port2.center[longitudinal]
if c1 <= z1:
raise RuntimeError(
f"Port at {port1.center} expected to be after coordinate {z1} in axis {transverse}."
)
if increasing:
if c2 <= z2:
raise RuntimeError(
f"Port at {port2.center} expected to be after coordinate {z2} in axis "
f"{longitudinal}."
)
else:
if c2 >= z2:
raise RuntimeError(
f"Port at {port2.center} expected to be before coordinate {z2} in axis "
f"{longitudinal}."
)
z1, z2 = c1, c2
straight_kwargs["technology"] = technology
for kw in ("length", "endpoint"):
if kw in straight_kwargs:
del straight_kwargs[kw]
if bend == ():
bend_kwargs = {"angle": 90 if positive_bend else -90, "technology": technology}
if radius != ():
bend_kwargs["radius"] = radius
bends = []
route_specs = [p.spec if p.inverted else p.spec.inverted() for p, _ in nets]
unique_specs = []
for route_spec in route_specs:
if any(r.profile_matches(route_spec) for r in unique_specs):
continue
unique_specs.append(route_spec)
bend = _bend(port_spec=route_spec, **bend_kwargs)
bends.append(_bend_info(bend))
elif isinstance(bend, _ext.Component):
bends = [_bend_info(bend)]
else:
bends = [_bend_info(x) for x in bend]
bend_input = 2 if positive_bend else 3
bend_output = 5 - bend_input
bend_index = []
for info in bends:
p = info.bend[info[bend_input]]
bend_index.append((p.spec.inverted() if p.inverted else p.spec, info))
routes = []
for port0, port1 in nets:
route_spec = port0.spec if port0.inverted else port0.spec.inverted()
straight_kwargs["port_spec"] = route_spec
for bend_spec, info in bend_index:
if bend_spec.profile_matches(route_spec):
match = info
break
else:
raise RuntimeError(
f"No bend found matching port spec {route_spec.description!r} for port at "
f"{port0.center}."
)
bend = match[0]
radius = match[1]
n0 = match[bend_input]
n1 = match[bend_output]
c0 = port0.center
c1 = port1.center
length0 = sign1 * (c1[longitudinal] - c0[longitudinal]) - radius
length1 = sign2 * (c1[transverse] - c0[transverse]) - radius
if length0 < 0 or length1 < 0:
raise RuntimeError(
f"Not enough room to connect ports at {c0} and {c1} with bend radius of {radius}."
)
route = _ext.Component(
f"route_l__{c0[0]:g}_{c0[1]:g}__{c1[0]:g}_{c1[1]:g}".translate(_gdsii_safe), technology
)
route.properties.__thumbnail__ = "wg"
route.properties.__labels__ = ["routing"]
route.add_model(route_model)
routes.append(route)
connection = port0
if length0 > 0:
straight = _straight(length=length0, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, connection)
connection = ref[s1]
route.add_port(ref[s0])
bend_ref = route.add_reference(bend).connect(n0, connection)
if len(route.ports) == 0:
route.add_port(bend_ref[n0])
connection = bend_ref[n1]
if length1 > 0:
straight = _straight(length=length1, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, connection)
connection = ref[s1]
route.add_port(connection)
if not connection.is_connected_to(port1):
raise RuntimeError(f"Unable to close L route between {c0} and {c1}.")
if len(routes) == 1:
component = routes[0]
component.name = name
else:
component = _ext.Component(name, technology=technology)
component.properties.__thumbnail__ = "wg"
component.properties.__labels__ = ["routing"]
component.add_model(bundle_model)
component.add(*routes)
component.add_port([p for r in routes for _, p in sorted(r.ports.items())])
return component
[docs]
@_parametric_component
def route_u(
*,
port1: _Port | _Sequence[_Port] | None = None,
port2: _Port | _Sequence[_Port] | None = None,
radius: _pft.PositiveDimension | None = None,
u_offset: _pft.Coordinate | None = None,
relative: bool | None = None,
pitch: _pft.Dimension | None = None,
bend: _ext.Component | _Sequence[_ext.Component] | None = None,
straight_kwargs: _pft.kwargs_for(straight) | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
route_model: _ext.Model | None = None,
bundle_model: _ext.Model | None = None,
) -> _ext.Component:
"""Route the connection between parallel ports.
Args:
port1: First port to be connected. The port can be specified 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). A sequence of ports
can be used for bundle routing.
port2: Second port to be connected. A sequence of ports can be used
for bundle routing (with same length as ``port1``).
radius: Radius used for bends, if needed.
u_offset: Position of the base of the U shape.
relative: If ``True``, interpret ``u_offset`` relative to the
frontmost port.
pitch: Center-to-center distance between adjacent waveguides in a
bundle. If ``None``, the largest snapped port width is used.
bend: 90° bend to be used for routing. If the bundle has different
port specifications, a sequence of bends (one for each
specification) must be provided. If ``None``, the default
parametric bend will be used.
straight_kwargs: Keyword arguments for :func:`straight`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
route_model: Model to be used with each route sub-component. If
``None`` a :class:`photonforge.CircuitModel` is used.
bundle_model: Model to be used with the top-level route component.
If ``None`` a :class:`photonforge.CircuitModel` is used.
Returns:
Component with the route, including ports and model.
"""
if technology is None:
technology = _ext.config.default_technology
function = "route_u"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
try:
ports1 = [_get_port_or_terminal(port1, "port1", True)]
except Exception:
ports1 = [_get_port_or_terminal(p, "port1[…]", True) for p in port1]
try:
ports2 = [_get_port_or_terminal(port2, "port2", True)]
except Exception:
ports2 = [_get_port_or_terminal(p, "port2[…]", True) for p in port2]
if len(ports1) != len(ports2):
raise ValueError(
f"Arguments 'port1' and 'port2' must contain the same number of ports "
f"({len(ports1)} ≠ {len(ports2)})."
)
if len(ports1) == 0:
raise ValueError("At least one port pair must be provided.")
radius = _get_default(function, "radius", radius, ())
u_offset = _get_default(function, "u_offset", u_offset, object)
relative = _get_default(function, "relative", relative, False)
pitch = _get_default(
function, "pitch", pitch, _ext.grid_ceil(max(p.spec.width for p in ports1))
)
straight_kwargs = dict(_get_default(function, "straight_kwargs", straight_kwargs, {}))
bend = _get_default(function, "bend", bend, ())
name = _get_default(function, "name", name, "")
bundle_model = _get_default(function, "bundle_model", bundle_model, _CircuitModel())
route_model = _get_default(function, "route_model", route_model, _CircuitModel())
if pitch < 0:
raise ValueError("'pitch' may not be negative.")
direction = round(ports1[0].input_direction) % 360
sign = 1 if direction in (180, 270) else -1
longitudinal = 0 if direction in (0, 180) else 1
transverse = 1 - longitudinal
positive_bend = direction in (90, 180)
if not _is_multiple_of_90(direction):
raise RuntimeError("The input direction of all ports must be horizontal or vertical.")
for i, (port1, port2) in enumerate(zip(ports1, ports2, strict=True)):
if not _angles_equal(port1.input_direction, direction):
raise RuntimeError(
f"The input direction of port1[{i}] does not match the expected {direction}°."
)
if not _angles_equal(port2.input_direction, direction):
raise RuntimeError(
f"The input direction of port2[{i}] does not match the expected {direction}°."
)
if not port1.can_connect_to(port2):
raise RuntimeError(
f"port1[{i}] and port2[{i}] have incompatible specifications and cannot be "
f"connected."
)
nets = sorted(
(
(a, b) if a.center[transverse] < b.center[transverse] else (b, a)
for a, b in zip(ports1, ports2, strict=True)
),
key=lambda x: -x[0].center[transverse],
)
z1 = z2 = 0.5 * (nets[0][0].center[transverse] + nets[0][1].center[transverse])
for port1, port2 in nets:
if port1.center[transverse] >= z1:
raise RuntimeError(
f"Port at {port1.center} expected to be before coordinate {z1} in axis "
f"{transverse}."
)
z1 = port1.center[transverse]
if port2.center[transverse] <= z2:
raise RuntimeError(
f"Port at {port2.center} expected to be after coordinate {z2} in axis {transverse}."
)
z2 = port2.center[transverse]
straight_kwargs["technology"] = technology
for kw in ("length", "endpoint"):
if kw in straight_kwargs:
del straight_kwargs[kw]
if bend == ():
bend_kwargs = {"angle": 90 if positive_bend else -90, "technology": technology}
if radius != ():
bend_kwargs["radius"] = radius
bends = []
route_specs = [p.spec if p.inverted else p.spec.inverted() for p, _ in nets]
unique_specs = []
for route_spec in route_specs:
if any(r.profile_matches(route_spec) for r in unique_specs):
continue
unique_specs.append(route_spec)
bend = _bend(port_spec=route_spec, **bend_kwargs)
bends.append(_bend_info(bend))
elif isinstance(bend, _ext.Component):
bends = [_bend_info(bend)]
else:
bends = [_bend_info(x) for x in bend]
bend_input = 2 if positive_bend else 3
bend_output = 5 - bend_input
bend_index = []
for info in bends:
p = info.bend[info[bend_input]]
bend_index.append((p.spec.inverted() if p.inverted else p.spec, info))
if relative and u_offset is not object:
bases = nets[0][0].center[longitudinal], nets[0][1].center[longitudinal]
u_offset = (max(bases) if sign > 0 else min(bases)) + sign * u_offset
routes = []
for port0, port1 in nets:
route_spec = port0.spec if port0.inverted else port0.spec.inverted()
straight_kwargs["port_spec"] = route_spec
for bend_spec, info in bend_index:
if bend_spec.profile_matches(route_spec):
match = info
break
else:
raise RuntimeError(
f"No bend found matching port spec {route_spec.description!r} for port at "
f"{port0.center}."
)
bend = match[0]
radius = match[1]
n0 = match[bend_input]
n1 = match[bend_output]
c0 = port0.center
c1 = port1.center
base = (
max(c0[longitudinal], c1[longitudinal])
if sign > 0
else min(c0[longitudinal], c1[longitudinal])
)
if u_offset is object or (
(sign > 0 and u_offset < base + radius) or (sign < 0 and u_offset > base - radius)
):
if u_offset is not object:
_warn.warn(
"Value of 'u_offset' clamped based on port positions and radius.",
RuntimeWarning,
2,
)
u_offset = base + sign * radius
v0 = c0.copy()
v1 = c1.copy()
v1[longitudinal] = v0[longitudinal] = _ext.snap_to_grid(u_offset - sign * radius)
route = _ext.Component(
f"route_u__{c0[0]:g}_{c0[1]:g}__{c1[0]:g}_{c1[1]:g}".translate(_gdsii_safe), technology
)
route.properties.__thumbnail__ = "wg"
route.properties.__labels__ = ["routing"]
route.add_model(route_model)
routes.append(route)
connection = port0
length = abs(v0[longitudinal] - c0[longitudinal])
if length > 0:
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, connection)
connection = ref[s1]
route.add_port(ref[s0])
bend0 = route.add_reference(bend).connect(n0, connection)
if len(route.ports) == 0:
route.add_port(bend0[n0])
connection = port1
length = abs(v1[longitudinal] - c1[longitudinal])
if length > 0:
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s1, connection)
connection = ref[s0]
route.add_port(ref[s1])
bend1 = route.add_reference(bend).connect(n1, connection)
if len(route.ports) == 1:
route.add_port(bend1[n1])
b_port0 = bend0[n1]
b_port1 = bend1[n0]
if b_port0.center[longitudinal] != b_port1.center[longitudinal]:
raise RuntimeError(
f"Unable to connect ports at {c0} and {c1} through {b_port0.center} and "
f"{b_port1.center}. Make sure all connection and bend ports are grid-snapped."
)
length = b_port1.center[transverse] - b_port0.center[transverse]
if length < 0:
raise RuntimeError(
f"Unable to connect ports at {c0} and {c1}. Make sure that the distance between "
f"them is at least twice the radius ({2 * radius})."
)
if length > 0:
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, b_port0)
if not ref[s1].is_connected_to(b_port1):
raise RuntimeError(f"Unable to close U loop between {c0} and {c1}.")
elif not b_port0.is_connected_to(b_port1):
raise RuntimeError(f"Unable to close U loop between {c0} and {c1}.")
u_offset += sign * pitch
if len(routes) == 1:
component = routes[0]
component.name = name
else:
component = _ext.Component(name, technology=technology)
component.properties.__thumbnail__ = "wg"
component.properties.__labels__ = ["routing"]
component.add_model(bundle_model)
component.add(*routes)
component.add_port([p for r in routes for _, p in sorted(r.ports.items())])
return component
[docs]
@_parametric_component
def route_z(
*,
port1: _Port | _Sequence[_Port] | None = None,
port2: _Port | _Sequence[_Port] | None = None,
radius: _pft.PositiveDimension | None = None,
alignment: _typ.Literal["center", "port1", "port2"] | None = None,
padding: _pft.Dimension | _pft.Dimension2D | None = None,
pitch: _pft.Dimension | None = None,
bend: _ext.Component | _Sequence[_ext.Component] | None = None,
straight_kwargs: _pft.kwargs_for(straight) | None = None,
s_bend_kwargs: _pft.kwargs_for(s_bend) | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
route_model: _ext.Model | None = None,
bundle_model: _ext.Model | None = None,
) -> _ext.Component:
"""Route the connection between parallel ports.
Args:
port1: First port to be connected. The port can be specified 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). A sequence of ports
can be used for bundle routing.
port2: Second port to be connected. A sequence of ports can be used
for bundle routing (with same length as ``port1``).
radius: Radius used for bends, if needed.
alignment: Alignment of the transversal route section. One of
``"center"``, ``"port1"``, or ``"port2"``. If ``None``, defaults
to ``"center"``.
padding: Minimal straight length added before bends. Use 2 values to
set different paddings for ports 1 and 2. Affects `alignment`.
pitch: Center-to-center distance between adjacent waveguides in a
bundle. If ``None``, the largest snapped port width is used.
bend: 90° bend to be used for routing. If the bundle has different
port specifications, a sequence of bends (one for each
specification) must be provided. If ``None``, the default
parametric bend will be used.
straight_kwargs: Keyword arguments for :func:`straight`.
s_bend_kwargs: Keyword arguments for :func:`s_bend`.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
route_model: Model to be used with each route sub-component. If
``None`` a :class:`photonforge.CircuitModel` is used.
bundle_model: Model to be used with the top-level route component.
If ``None`` a :class:`photonforge.CircuitModel` is used.
Returns:
Component with the route, including ports and model.
"""
if technology is None:
technology = _ext.config.default_technology
function = "route_z"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
try:
ports1 = [_get_port_or_terminal(port1, "port1", True)]
except Exception:
ports1 = [_get_port_or_terminal(p, "port1[…]", True) for p in port1]
try:
ports2 = [_get_port_or_terminal(port2, "port2", True)]
except Exception:
ports2 = [_get_port_or_terminal(p, "port2[…]", True) for p in port2]
if len(ports1) != len(ports2):
raise ValueError(
f"Arguments 'port1' and 'port2' must contain the same number of ports "
f"({len(ports1)} ≠ {len(ports2)})."
)
if len(ports1) == 0:
raise ValueError("At least one port pair must be provided.")
radius = _get_default(function, "radius", radius, ())
alignment = _get_default(function, "alignment", alignment, "center")
padding = _get_default(function, "padding", padding, (0, 0))
pitch = _get_default(
function, "pitch", pitch, _ext.grid_ceil(max(p.spec.width for p in ports1))
)
straight_kwargs = dict(_get_default(function, "straight_kwargs", straight_kwargs, {}))
s_bend_kwargs = dict(_get_default(function, "s_bend_kwargs", s_bend_kwargs, {}))
bend = _get_default(function, "bend", bend, ())
name = _get_default(function, "name", name, "")
bundle_model = _get_default(function, "bundle_model", bundle_model, _CircuitModel())
route_model = _get_default(function, "route_model", route_model, _CircuitModel())
try:
a, b = padding
except Exception:
a = b = padding
padding = (a, b)
if a < 0 or b < 0:
raise ValueError("'padding' may not be negative.")
if pitch < 0:
raise ValueError("'pitch' may not be negative.")
direction1 = round(ports1[0].input_direction) % 360
direction2 = (direction1 + 180) % 360
sign = 1 if direction1 in (180, 270) else -1
longitudinal = 0 if direction1 in (0, 180) else 1
transverse = 1 - longitudinal
if not _is_multiple_of_90(direction1):
raise RuntimeError("The input direction of all ports must be horizontal or vertical.")
for i, (port1, port2) in enumerate(zip(ports1, ports2, strict=True)):
if not _angles_equal(port1.input_direction, direction1):
raise RuntimeError(
f"The input direction of port1[{i}] does not match the expected {direction1}°."
)
if not _angles_equal(port2.input_direction, direction2):
raise RuntimeError(
f"The input direction of port2[{i}] does not match the expected {direction2}°."
)
if not port1.can_connect_to(port2):
raise RuntimeError(
f"port1[{i}] and port2[{i}] have incompatible specifications and cannot be "
f"connected."
)
if sign * port1.center[longitudinal] >= sign * port2.center[longitudinal]:
raise RuntimeError(f"port1[{i}] and port2[{i}] are not facing towards each other.")
nets = sorted(zip(ports1, ports2, strict=True), key=lambda x: x[0].center[transverse])
groups = [[nets[0]]]
l1, l2 = nets[0][0].center[transverse], nets[0][1].center[transverse]
for port1, port2 in nets[1:]:
z1 = port1.center[transverse]
z2 = port2.center[transverse]
if z1 <= l1:
raise RuntimeError(
f"Port at {port1.center} expected to be after coordinate {l1} in axis {transverse}."
)
if z2 <= l2:
raise RuntimeError(
f"Port at {port2.center} expected to be after coordinate {l2} in axis {transverse}."
)
if (
z1 == z2
or l1 == l2
or (z1 > z2) != (l1 > l2)
or (z1 >= l2 + pitch and z2 >= l1 + pitch)
):
groups.append([(port1, port2)])
else:
groups[-1].append((port1, port2))
l1, l2 = z1, z2
straight_kwargs["technology"] = technology
for kw in ("length", "endpoint"):
if kw in straight_kwargs:
del straight_kwargs[kw]
s_bend_kwargs["technology"] = technology
for kw in ("port_spec", "length", "offset"):
if kw in s_bend_kwargs:
del s_bend_kwargs[kw]
if bend == ():
bend_kwargs = {"angle": 90, "technology": technology}
if radius != ():
bend_kwargs["radius"] = radius
bends = []
unique_specs = []
for p, _ in nets:
if any(r.profile_matches(p.spec) for r in unique_specs):
continue
route_specs = (p.spec,) if p.spec.symmetric() else (p.spec, p.spec.inverted())
unique_specs.extend(route_specs)
for route_spec in route_specs:
bend = _bend(port_spec=route_spec, **bend_kwargs)
bends.append(_bend_info(bend))
elif isinstance(bend, _ext.Component):
bends = [_bend_info(bend)]
else:
bends = [_bend_info(x) for x in bend]
max_radius = max(r for _, r, _, _ in bends)
bend_index = []
for info in bends:
p = info.bend[info[2]]
bend_index.append((p.spec.inverted() if p.inverted else p.spec, info))
routes = []
for nets in groups:
pitch = sign * (
abs(pitch)
if nets[0][1].center[transverse] < nets[0][0].center[transverse]
else -abs(pitch)
)
z1 = (p.center[longitudinal] for p, _ in nets)
z1 = (max(z1) if sign > 0 else min(z1)) + sign * padding[0]
z2 = (p.center[longitudinal] for _, p in nets)
z2 = (min(z2) if sign > 0 else max(z2)) - sign * padding[1]
group_pitch = (len(nets) - 1) * pitch
if alignment == "port1":
z_offset = z1 + sign * max_radius
if (sign > 0) != (pitch > 0):
z_offset -= group_pitch
elif alignment == "port2":
z_offset = z2 - sign * max_radius
if (sign > 0) == (pitch > 0):
z_offset -= group_pitch
else:
z_offset = 0.5 * (z1 + z2 - group_pitch)
for port0, port1 in nets:
route_spec = port0.spec if port0.inverted else port0.spec.inverted()
straight_kwargs["port_spec"] = route_spec
c0 = port0.center
c1 = port1.center
route = _ext.Component(
f"route_z__{c0[0]:g}_{c0[1]:g}__{c1[0]:g}_{c1[1]:g}".translate(_gdsii_safe),
technology,
)
route.properties.__thumbnail__ = "wg"
route.properties.__labels__ = ["routing"]
route.add_model(route_model)
routes.append(route)
if c1[transverse] == c0[transverse]:
length = abs(c1[longitudinal] - c0[longitudinal])
if length < padding[0] + padding[1]:
raise RuntimeError(
f"Ports at {c0} and {c1} are closer than the required padding."
)
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, port0)
route.add_port([ref[s0], ref[s1]])
else:
for bend_spec, info in bend_index:
if bend_spec.profile_matches(route_spec):
info_positive = info
break
else:
raise RuntimeError(
f"No positive bend found matching port spec {route_spec.description!r} for "
f"port at {port0.center}."
)
inverted = route_spec.inverted()
for bend_spec, info in bend_index:
if bend_spec.profile_matches(inverted):
info_negative = info
break
else:
raise RuntimeError(
f"No negative bend found matching port spec {route_spec.description!r} for "
f"port at {port0.center}."
)
offset = c1[transverse] - c0[transverse]
transverse_sign = 1 if offset > 0 else -1
if (direction1 in (180, 90)) == (offset > 0):
bend0, radius0, in0, out0 = info_positive
bend1, radius1, out1, in1 = info_negative
offset = abs(offset)
else:
bend0, radius0, out0, in0 = info_negative
bend1, radius1, in1, out1 = info_positive
offset = -abs(offset)
v0 = c0.copy()
v1 = c1.copy()
v0[longitudinal] = _ext.snap_to_grid(z_offset - sign * radius0)
v1[longitudinal] = _ext.snap_to_grid(z_offset + sign * radius1)
to_add0 = None
length = sign * (v0[longitudinal] - c0[longitudinal])
if length < padding[0]:
raise RuntimeError(
f"Not enough room to connect port at {c0} to required offset {z_offset} "
f"with bend radius of {radius0} and padding {padding[0]}."
)
if length > 0:
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, port0)
port0 = ref[s1]
to_add0 = ref[s0]
to_add1 = None
length = sign * (c1[longitudinal] - v1[longitudinal])
if length < padding[1]:
raise RuntimeError(
f"Not enough room to connect port at {c1} to required offset {z_offset} "
f"with bend radius of {radius1} and padding {padding[1]}."
)
if length > 0:
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s1, port1)
port1 = ref[s0]
to_add1 = ref[s1]
if abs(offset) < radius0 + radius1:
length = abs(port0.center[longitudinal] - port1.center[longitudinal])
s_bend = _s_bend(
port_spec=route_spec, length=length, offset=offset, **s_bend_kwargs
)
s0, s1 = sorted(s_bend.ports)
ref = route.add_reference(s_bend).connect(s0, port0)
if not ref[s1].is_connected_to(port1):
raise RuntimeError(
f"Unable to connect ports at {c0} and {c1} through an S-bend with "
f"length {length} and offset {offset}. Make sure all connection and "
f"bend ports are grid-snapped."
)
if to_add0 is None:
to_add0 = ref[s0]
if to_add1 is None:
to_add1 = ref[s1]
else:
ref0 = route.add_reference(bend0).connect(in0, port0)
if to_add0 is None:
to_add0 = ref0[in0]
port0 = ref0[out0]
ref1 = route.add_reference(bend1).connect(out1, port1)
if to_add1 is None:
to_add1 = ref1[out1]
port1 = ref1[in1]
length = transverse_sign * (port1.center[transverse] - port0.center[transverse])
if port0.center[longitudinal] != port1.center[longitudinal] or length < 0:
raise RuntimeError(
f"Unable to connect ports at {c0} and {c1} through {port0.center} and "
f"{port1.center}. Make sure all connection and bend ports are "
f"grid-snapped."
)
if length > 0:
straight = _straight(length=length, **straight_kwargs)
s0, s1 = sorted(straight.ports)
ref = route.add_reference(straight).connect(s0, port0)
if not ref[s1].is_connected_to(port1):
raise RuntimeError(f"Unable to close Z route between {c0} and {c1}.")
elif not port0.is_connected_to(port1):
raise RuntimeError(f"Unable to close Z route between {c0} and {c1}.")
route.add_port((to_add0, to_add1))
z_offset += pitch
if len(routes) == 1:
component = routes[0]
component.name = name
else:
component = _ext.Component(name, technology=technology)
component.properties.__thumbnail__ = "wg"
component.properties.__labels__ = ["routing"]
component.add_model(bundle_model)
component.add(*routes)
component.add_port([p for r in routes for _, p in sorted(r.ports.items())])
return component
[docs]
@_parametric_component
def route_auto(
*,
port1: _ReferencePort | _Sequence[_ReferencePort] | None = None,
port2: _ReferencePort | _Sequence[_ReferencePort] | None = None,
radius: _pft.PositiveDimension | None = None,
obstacles: _Sequence[_RouteObstacle] | _ext.Component | _ext.Reference | None = None,
include_port_references_as_obstacles: bool | None = None,
collision_layers: _Sequence[_pft.Layer] | None = None,
collision_offset: _pft.Dimension | None = None,
straight_kwargs: _pft.kwargs_for(straight) | None = None,
s_bend_kwargs: _pft.kwargs_for(s_bend) | None = None,
bend90: _ext.Component | None = None,
bend45: _ext.Component | None = None,
cross: _ext.Component | None = None,
propagation_cost: _pft.PropagationLoss | None = None,
bend90_cost: _pft.Loss | None = None,
bend45_cost: _pft.Loss | None = None,
cross_cost: _pft.Loss | None = None,
cross_space_cost: _pft.PropagationLoss | None = None,
congestion_cost: _pft.PropagationLoss | None = None,
congestion_radius: _pft.NonNegativeInt | None = None,
allow_diagonals: bool | None = None,
allow_s_bend: bool | None = None,
collapse_s_bends: bool | None = None,
max_crossings: int | None = None,
grid_size: _pft.PositiveDimension | None = None,
search_margin: _pft.Dimension | None = None,
net_reorder: bool | None = None,
allow_partial: bool | None = None,
max_iterations: int | None = None,
max_reroute_rounds: _pft.NonNegativeInt | None = None,
reroute_history_cost: _pft.NonNegativeFloat | None = None,
show_progress: bool | None = None,
diagnostics: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
route_model: _ext.Model | None = None,
bundle_model: _ext.Model | None = None,
) -> _ext.Component:
"""Automatic optical routing between reference ports.
The automatic router performs grid-based multi-net routing. Ports must be
specified as reference-port tuples so device-aware access preprocessing
can use the surrounding component geometry.
Args:
port1: First port for single-net routing. The port must be specified
as a tuple including a :class:`photonforge.Reference`, the port
name, and the repetition index (optional, only for array
references). For bundle routing, a sequence of ports can be used.
port2: Second port for single-net routing, or sequence of ports for
bundle routing (with the same length as ``port1``).
radius: Radius used to generate S-bend sections.
obstacles: Additional routing obstacles. Use a sequence of 2D
structures, or a :class:`Component` or :class:`Reference`.
include_port_references_as_obstacles: If ``True``, add the
references used by routed ports to ``obstacles``. If ``None``,
defaults to ``True``.
collision_layers: Layers used for route-section collision stamps and
for collecting obstacle polygons from a component or reference. An
empty sequence (default) uses all layers.
collision_offset: Offset applied to route collision layers.
straight_kwargs: Keyword arguments for :func:`straight`.
s_bend_kwargs: Keyword arguments for :func:`s_bend`.
bend90: 90° bend to be used for routing.
bend45: Optional 45° bend to be used for routing.
cross: Optional crossing to be used for routing.
propagation_cost: Straight section cost (per μm). Defaults to 1e-5.
bend90_cost: Cost of a 90° bend. Defaults to
``2.1 * radius * propagation_cost`` (5% penalty on the Manhattan
distance).
bend45_cost: Cost of a 45° bend. Defaults to ``0.51 * bend90_cost``.
cross_cost: Cost of a crossing. Defaults to twice the
``propagation_cost`` applied to the crossing length.
cross_space_cost: Cost multiplier for nearby crossing spacing.
Defaults to ``propagation_cost``.
congestion_cost: Cost multiplier for nearby routed occupancy.
Defaults to ``propagation_cost``.
congestion_radius: Congestion search radius in grid cells. Defaults
to 1.
allow_diagonals: Controls whether 45° bends are allowed. If
``None``, defaults to ``True`` when ``bend45`` is provided, and
``False`` otherwise.
allow_s_bend: Controls whether the router may use S bends to reach
an aligned goal directly. Port access for grid alignment use S
bends regardless of this flag. If ``None``, defaults to ``False``.
collapse_s_bends: Controls post-routing removal of unnecessary
access S bends. If ``None``, defaults to ``True``.
max_crossings: Maximum crossings per net. Use a negative value for
no explicit limit. Defaults to -1.
grid_size: Router grid size. ``grid_size + collision_offset`` should
be the center-to-center distance between parallel waveguides. If
``None``, defaults to the routed port spec width.
search_margin: Extra search margin around the routing bounds. If
``None``, uses an automatic margin based on endpoint separation
and bend size.
net_reorder: Allow net reordering before routing. If ``None``,
defaults to ``True``.
allow_partial: If ``False``, raise when any net fails. If ``None``,
defaults to ``False``.
max_iterations: Maximum search iterations per route. If ``None``,
defaults to 500000.
max_reroute_rounds: Maximum route conflict-recovery rounds. If
``None``, defaults to the number of nets clipped to [3; 16].
reroute_history_cost: History cost scale used in bundle rerouting.
If ``None``, defaults to 5.
show_progress: If ``True``, show routing progress for long-running
routes. If ``None``, defaults to ``True``.
diagnostics: If ``True``, JSON-encoded diagnostic information from
the routing algorithm is stored in
``component.properties.route_auto``. If ``None``, defaults to
``False``.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
route_model: Model to be used with each route sub-component. If
``None`` a :class:`photonforge.CircuitModel` is used.
bundle_model: Model to be used with the top-level route component.
If ``None`` a :class:`photonforge.CircuitModel` is used.
Note:
The costs of S bends are based on the computed length multiplied by
the propagation cost.
Returns:
Component with emitted route geometry.
"""
if technology is None:
technology = _ext.config.default_technology
function = "route_auto"
port1 = _get_default(function, "port1", port1)
port2 = _get_default(function, "port2", port2)
try:
ports1 = [_get_reference_port(port1, "port1")]
except Exception:
ports1 = [_get_reference_port(p, "port1[…]") for i, p in enumerate(port1)]
try:
ports2 = [_get_reference_port(port2, "port2")]
except Exception:
ports2 = [_get_reference_port(p, "port2[…]") for i, p in enumerate(port2)]
if len(ports1) != len(ports2):
raise ValueError(
f"Arguments 'port1' and 'port2' must contain the same number of ports "
f"({len(ports1)} ≠ {len(ports2)})."
)
if len(ports1) == 0:
raise ValueError("At least one port pair must be provided.")
route_nets = list(zip(ports1, ports2, strict=True))
endpoint_ports = [
(
_port_from_reference_port(net[0], f"port1[{i}][0]"),
_port_from_reference_port(net[1], f"port2[{i}][1]"),
)
for i, net in enumerate(route_nets)
]
route_spec = endpoint_ports[0][0].spec
if not route_spec.symmetric():
raise RuntimeError("'route_auto' only supports symmetric port specifications.")
for source, target in endpoint_ports:
if not route_spec.profile_matches(source.spec):
raise RuntimeError(
"All ports must use compatible path profiles. Support for heterogeneous routes "
"will be added in the future."
)
if not source.can_connect_to(target):
raise RuntimeError(
f"Ports at {source.center} and {target.center} have incompatible "
f"specifications and cannot be connected."
)
radius = _get_default(
function,
"radius",
radius,
route_spec.default_radius if route_spec.default_radius > 0 else (),
)
grid_size = _get_default(
function, "grid_size", grid_size, _ext.snap_to_grid(route_spec.width, multiple=100)
)
net_reorder = _get_default(function, "net_reorder", net_reorder, True)
obstacles = _get_default(function, "obstacles", obstacles, ())
include_port_references_as_obstacles = _get_default(
function, "include_port_references_as_obstacles", include_port_references_as_obstacles, True
)
name = _get_default(function, "name", name, "")
bundle_model = _get_default(function, "bundle_model", bundle_model, _CircuitModel())
route_model = _get_default(function, "route_model", route_model, _CircuitModel())
straight_kwargs = dict(_get_default(function, "straight_kwargs", straight_kwargs, {}))
s_bend_kwargs = dict(_get_default(function, "s_bend_kwargs", s_bend_kwargs, {}))
bend90 = _get_default(function, "bend90", bend90, ())
bend45 = _get_default(function, "bend45", bend45, ())
cross = _get_default(function, "cross", cross, ())
allow_diagonals = _get_default(function, "allow_diagonals", allow_diagonals, bend45 != ())
allow_s_bend = _get_default(function, "allow_s_bend", allow_s_bend, False)
collapse_s_bends = _get_default(function, "collapse_s_bends", collapse_s_bends, True)
allow_partial = _get_default(function, "allow_partial", allow_partial, False)
max_iterations = _get_default(function, "max_iterations", max_iterations, 500000)
search_margin = _get_default(function, "search_margin", search_margin, ())
max_reroute_rounds = _get_default(
function, "max_reroute_rounds", max_reroute_rounds, min(16, max(3, len(route_nets)))
)
reroute_history_cost = _get_default(function, "reroute_history_cost", reroute_history_cost, 5.0)
congestion_radius = _get_default(function, "congestion_radius", congestion_radius, 1)
collision_layers = _get_default(function, "collision_layers", collision_layers, ())
collision_offset = _get_default(function, "collision_offset", collision_offset, 0.0)
show_progress = _get_default(function, "show_progress", show_progress, True)
diagnostics = _get_default(function, "diagnostics", diagnostics, False)
if grid_size <= 0:
raise ValueError("'grid_size' must be positive.")
if search_margin == ():
route_search_margin = -1.0
else:
if search_margin < 0:
raise ValueError("'search_margin' may not be negative.")
route_search_margin = search_margin
if max_reroute_rounds < 0 or max_reroute_rounds > 2147483647:
raise ValueError("'max_reroute_rounds' must be between 0 and 2147483647.")
if reroute_history_cost < 0.0:
raise ValueError("'reroute_history_cost' may not be negative.")
if cross == ():
max_crossings = 0
cross = None
else:
max_crossings = _get_default(function, "max_crossings", max_crossings, -1)
if not all(route_spec.profile_matches(p.spec) for p in cross.ports.values()):
raise RuntimeError(
"All 'cross' ports must be compatible with the route port specification."
)
if bend90 == ():
bend_kwargs = {"angle": 90, "technology": technology, "port_spec": route_spec}
if radius != ():
bend_kwargs["radius"] = radius
bend90 = _bend(**bend_kwargs)
elif not all(route_spec.profile_matches(p.spec) for p in bend90.ports.values()):
raise RuntimeError(
"All 'bend90' ports must be compatible with the route port specification."
)
if radius == ():
radius = bend90.parametric_kwargs.get("radius")
if radius is None:
p0, p1 = bend90.ports.values()
radius = abs(p1.center - p0.center).max()
if grid_size >= radius:
_warn.warn(
f"Routing grid size ({grid_size}) should be smaller than the bend radius ({radius}) "
f"for better results.",
RuntimeWarning,
2,
)
if allow_diagonals:
if bend45 == ():
with _warn.catch_warnings():
_warn.simplefilter("ignore", RuntimeWarning)
bend45 = _bend(angle=45, radius=radius, technology=technology, port_spec=route_spec)
elif not all(route_spec.profile_matches(p.spec) for p in bend45.ports.values()):
raise RuntimeError(
"All 'bend45' ports must be compatible with the route port specification."
)
else:
bend45 = None
propagation_cost = _get_default(function, "propagation_cost", propagation_cost, 1e-5)
if propagation_cost <= 0.0:
raise ValueError("'propagation_cost' must be positive.")
straight_cost = propagation_cost * grid_size
bend90_cost = _get_default(
function, "bend90_cost", bend90_cost, 2.1 * radius * propagation_cost
)
if bend90_cost <= 0.0:
raise ValueError("'bend90_cost' must be positive.")
if bend45 is None:
bend45_cost = 0.0
else:
bend45_cost = _get_default(function, "bend45_cost", bend45_cost, 0.51 * bend90_cost)
if bend45_cost <= 0.0:
raise ValueError("'bend45_cost' must be positive.")
if cross is None:
cross_cost = 0.0
else:
cross_cost = _get_default(
function, "cross_cost", cross_cost, 2.0 * cross.size().max() * propagation_cost
)
if cross_cost <= 0.0:
raise ValueError("'cross_cost' must be positive.")
congestion_cost = _get_default(function, "congestion_cost", congestion_cost, propagation_cost)
if congestion_cost < 0.0:
raise ValueError("'congestion_cost' may not be negative.")
congestion_cost *= grid_size
cross_space_cost = _get_default(
function, "cross_space_cost", cross_space_cost, propagation_cost
)
if cross_space_cost < 0.0:
raise ValueError("'cross_space_cost' may not be negative.")
cross_space_cost *= grid_size
if congestion_radius < 0:
raise ValueError("'congestion_radius' may not be negative.")
obstacles = _route_obstacles_with_port_references(
obstacles, route_nets, include_port_references_as_obstacles
)
straight_kwargs["technology"] = technology
straight_kwargs["port_spec"] = route_spec
s_bend_kwargs["technology"] = technology
s_bend_kwargs["port_spec"] = route_spec
component = _ext.Component(name, technology=technology)
component.properties.__thumbnail__ = "wg"
component.properties.__labels__ = ["routing"]
component.add_model(bundle_model)
route_diagnostics = _ext._route_auto(
component,
route_nets,
bend90,
bend45,
cross,
_straight,
straight_kwargs,
_s_bend,
s_bend_kwargs,
route_spec,
obstacles,
max_crossings,
net_reorder,
radius,
grid_size,
max_iterations,
straight_cost,
bend90_cost,
bend45_cost,
cross_cost,
congestion_cost,
congestion_radius,
cross_space_cost,
collision_layers,
collision_offset,
route_model,
route_search_margin,
max_reroute_rounds,
reroute_history_cost,
allow_s_bend,
collapse_s_bends,
show_progress,
)
if route_diagnostics["failed_nets"] > 0:
failures = [r for r in route_diagnostics["routes"] if not r["success"]]
message = f"Automatic routing failed for {len(failures)} net(s)."
details = [
f"Net {r['net_key']}: {r['failure_message']}"
for r in failures
if r.get("failure_message")
]
if details:
message += " " + " ".join(details)
if allow_partial:
_warn.warn(message, RuntimeWarning, 3)
else:
raise RuntimeError(message)
if len(ports1) == 1 and len(component.references) == 1:
component = component.references[0].component
component.name = name
component.properties.__thumbnail__ = "wg"
component.properties.__labels__ = ["routing"]
else:
for ref in component.references:
route = ref.component
route.properties.__thumbnail__ = "wg"
route.properties.__labels__ = ["routing"]
if not route.name.startswith("route_auto_"):
continue
(_, p0), (_, p1) = sorted(route.ports.items())
c0 = p0.center
c1 = p1.center
route.name += f"__{c0[0]:g}_{c0[1]:g}__{c1[0]:g}_{c1[1]:g}".translate(_gdsii_safe)
if diagnostics:
component.properties.route_auto = _json.dumps(route_diagnostics, separators=(",", ":"))
return component
[docs]
@_parametric_component
def route_s_bend(
*,
port1: _Port | None = None,
port2: _Port | None = None,
euler_fraction: _pft.Fraction | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
model: _ext.Model | None = None,
) -> _ext.Component:
"""Create an S bend connecting 2 compatible ports.
Args:
port1: First port to be connected. The port can be specified 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.
technology: Component technology. If ``None``, the default
technology is used.
name: Component name.
model: Model to be used with this component. If ``None`` a
:class:`photonforge.WaveguideModel` is used.
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)
name = _get_default(function, "name", name, "")
model = _get_default(function, "model", model, _WaveguideModel())
port1 = _get_port_or_terminal(port1, "port1", True)
port2 = _get_port_or_terminal(port2, "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 = _ext.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 = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "wg"
c.properties.__labels__ = ["routing"]
c.add_model(model)
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()
c.add_port(_ext.Port(port1.center, port1.input_direction - 180, port_spec))
c.add_port(_ext.Port(port2.center, port2.input_direction - 180, port_spec, inverted=True))
return c
[docs]
@_parametric_component
def route_taper(
*,
terminal1: _Terminal | None = None,
terminal2: _Terminal | None = None,
layer: _pft.Layer | None = None,
offset_distance: _pft.Coordinate
| _pft.annotate(_Sequence[_pft.Coordinate], minItems=2, maxItems=2)
| None = None,
use_box: bool | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
) -> _ext.Component:
"""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, "terminal1", False)
terminal2 = _get_port_or_terminal(terminal2, "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 = _ext.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 = _ext.offset(structure1, offset_distance[0])
if offset_distance[0] != 0:
structure1 = _ext.envelope(structure1, max(0, offset_distance[0]))
structure2 = terminal2.structure
if use_box:
structure2 = _ext.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 = _ext.offset(structure2, offset_distance[1])
if offset_distance[1] != 0:
structure2 = _ext.envelope(structure2, max(0, offset_distance[1]))
min1, max1 = structure1.bounds()
min2, max2 = structure2.bounds()
size1 = max1 - min1
size2 = max2 - min2
ortho_1d = ((size1[0] < _ext.config.grid) and (size2[1] < _ext.config.grid)) or (
(size1[1] < _ext.config.grid) and (size2[0] < _ext.config.grid)
)
prefer_x = (size1[0] < _ext.config.grid) or (size2[0] < _ext.config.grid)
prefer_y = (size1[1] < _ext.config.grid) or (size2[1] < _ext.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 ortho_1d or (overlap_x and overlap_y) or not use_box:
taper = _ext.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 = _ext.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 = _ext.Polygon(
(
(min1[0], max1[1]),
min1,
(max1[0], min1[1]),
max1,
(max2[0], min2[1]),
max2,
(min2[0], max2[1]),
min2,
)
)
c = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "connection"
c.properties.__labels__ = ["routing"]
c.add(layer, taper)
return c
[docs]
@_parametric_component
def route_manhattan(
*,
terminal1: _Terminal | None = None,
terminal2: _Terminal | None = None,
direction1: _Axis | None = None,
direction2: _Axis | None = None,
layer: _pft.Layer | None = None,
width: _pft.PositiveDimension | None = None,
overlap_fraction: _pft.Fraction
| _pft.annotate(_typ.Sequence[_pft.Fraction], minItems=2, maxItems=2)
| None = None,
join_limit: _typ.Literal["round"] | float | None = None,
waypoints: _Sequence[_pft.Coordinate2D] | None = None,
technology: _ext.Technology | None = None,
name: str | None = None,
) -> _ext.Component:
"""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, "terminal1", False)
terminal2 = _get_port_or_terminal(terminal2, "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] < _ext.config.grid and sizes[i][1] >= _ext.config.grid:
directions[i] = 0
elif sizes[i][1] < _ext.config.grid and sizes[i][0] >= _ext.config.grid:
directions[i] = 1
endpoints = _ext._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 = _ext.Component(name, technology=technology)
c.properties.__thumbnail__ = "connection"
c.properties.__labels__ = ["routing"]
c.add(layer, _ext.Path(endpoints[0], width).segment(endpoints[1:], join_limit=join_limit))
return c
_straight = straight
_bend = bend
_s_bend = s_bend