Source code for photonforge.parametric

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