import warnings
from collections.abc import Callable
import numpy as np
from gdsfactory.component import Component, ComponentReference, Port
from gdsfactory.components.bend_euler import bend_euler
from gdsfactory.components.straight import straight as straight_function
from gdsfactory.config import CONF
from gdsfactory.get_netlist import difference_between_angles
from gdsfactory.path import Path, extrude
from gdsfactory.routing.auto_taper import (
_get_taper_io_port_names,
taper_to_cross_section,
)
from gdsfactory.typings import STEP_DIRECTIVES_ALL_ANGLE as STEP_DIRECTIVES
from gdsfactory.typings import (
ComponentFactory,
ComponentSpec,
CrossSectionSpec,
Route,
StepAllAngle,
)
BEND_PATH_FUNCS = {
# 'euler_bend': euler_path,
}
Connector = Callable[..., list[ComponentReference]]
def get_connector(name: str) -> Connector:
"""Gets a connector function by name.
Args:
name: the name of the connector function to retrieve.
Returns:
The specified connector function.
"""
try:
connector = CONNECTORS[name]
except KeyError as e:
raise KeyError(
f"{name} is not a valid connector type! Valid types are {list(CONNECTORS.keys())}"
) from e
return connector
def vector_intersection(
p0, a0, p1, a1, max_distance=100000, raise_error=True
) -> np.ndarray | None:
"""
Gets the intersection point between two vectors, specified by (point, angle) pairs, (p0, a0) and (p1, a1).
Args:
p0: x,y location of vector 0.
a0: angle of vector 0 [degrees].
p1: x,y location of vector 1.
a1: angle of vector 1 [degrees].
max_distance: maximum search distance for an intersection [um].
raise_error: if True, raises an error if no intersection is found. Otherwise, returns None in that case.
Returns:
The (x,y) point of intersection, if one is found. Otherwise None.
"""
import shapely.geometry as sg
a0_rad = np.deg2rad(a0)
a1_rad = np.deg2rad(a1)
dx0 = max_distance * np.cos(a0_rad)
dy0 = max_distance * np.sin(a0_rad)
p0_far = np.asarray(p0) + [dx0, dy0]
l0 = sg.LineString([p0, p0_far])
dx1 = max_distance * np.cos(a1_rad)
dy1 = max_distance * np.sin(a1_rad)
p1_far = np.asarray(p1) + [dx1, dy1]
l1 = sg.LineString([p1, p1_far])
intersect = l0.intersection(l1)
if isinstance(intersect, sg.Point):
return intersect.coords[0]
if raise_error:
raise ValueError(
f"Vectors at {tuple(p0)} and {tuple(p1)} with angles {a0} and {a1} do not intersect!"
)
else:
return None
def _line_intercept(p1, a1, p2, a2):
if (((a2 - a1) % 180) + 180) % 180 == 0:
raise ValueError("Lines are parallel!")
k1 = np.tan(np.deg2rad(a1))
k2 = np.tan(np.deg2rad(a2))
x1, y1 = p1
x2, y2 = p2
if ((a1 % 180) + 180) % 180 == 90:
return np.array((x1, k2 * (x1 - x2) + y2))
elif ((a2 % 180) + 180) % 180 == 90:
return np.array((x2, k1 * (x2 - x1) + y1))
else:
xi = (y1 - y2 - x1 * k1 + x2 * k2) / (k2 - k1)
yi = k1 * (xi - x1) + y1
return np.array((xi, yi))
def _get_bend_ports(bend):
# this is a bit of a hack, but o1 < o2, in0 < out0, hopefully there are no other wacky conventions!
sorted_port_names = sorted(bend.ports.keys())
return [bend.ports[n] for n in sorted_port_names]
LOW_LOSS_CROSS_SECTIONS = [
{"cross_section": "xs_sc", "settings": {"width": 0.9}},
"xs_sc",
]
def low_loss_connector(
port1: Port,
port2: Port,
prioritized_cross_sections: list[CrossSectionSpec] | None = None,
**kwargs,
) -> list[ComponentReference]:
"""
Routes between two ports, using the lowest-loss cross-section which will fit.
Args:
port1: the starting port.
port2: the ending port.
prioritized_cross_sections: a list of cross-sections, sorted by preference (starting with most preferred). If None, uses the global variable LOW_LOSS_CROSS_SECTIONS.
Keyword Args:
kwargs are added for API compatibility, but they are ignored.
Returns:
A list of component references comprising the connection.
"""
distance = np.sqrt(np.sum(np.square(port2.center - port1.center)))
if prioritized_cross_sections is None:
prioritized_cross_sections = LOW_LOSS_CROSS_SECTIONS
# try to route with the lowest-loss cross-section
for low_loss_cs in prioritized_cross_sections:
taper1 = taper_to_cross_section(port1, cross_section=low_loss_cs)
taper2 = taper_to_cross_section(port2, cross_section=low_loss_cs)
taper_lengths = [
taper.info["length"] for taper in (taper1, taper2) if taper is not None
]
total_taper_length = sum(taper_lengths)
if total_taper_length < distance:
refs = []
if taper1:
output_port_name = _get_taper_io_port_names(taper1)[1]
port1 = taper1.ports[output_port_name]
refs.append(taper1)
if taper2:
output_port_name = _get_taper_io_port_names(taper2)[1]
port2 = taper2.ports[output_port_name]
intermediate_connector = straight_connector(
port1, port2, cross_section=low_loss_cs
)
refs += intermediate_connector
if taper2:
refs.append(taper2)
return refs
if port1.cross_section == port2.cross_section:
# if both cross-sections are the same, keep it
return straight_connector(port1, port2, cross_section=port1.cross_section)
elif port1.layer == port2.layer:
# if the layer is the same, put a width taper, maximizing length of the fatty
if port2.width > port1.width:
taper = taper_to_cross_section(port1, port2.cross_section)
refs = [taper]
output_port_name = _get_taper_io_port_names(taper)[1]
refs += straight_connector(
taper.ports[output_port_name], port2, cross_section=port2.cross_section
)
else:
taper = taper_to_cross_section(port2, port1.cross_section)
output_port_name = _get_taper_io_port_names(taper1)[1]
refs = straight_connector(
port1, taper.ports[output_port_name], cross_section=port2.cross_section
)
refs.append(taper)
return refs
else:
# if cross-sections are different, just put the cross-section at the start
taper = taper_to_cross_section(port1, port2.cross_section)
refs = [taper]
output_port_name = _get_taper_io_port_names(taper1)[1]
refs += straight_connector(
taper.ports[output_port_name], port2, cross_section=port2.cross_section
)
return refs
def _make_error_trace(port1: Port, port2: Port, message: str):
from gdsfactory.routing.manhattan import RouteWarning
warnings.warn(message, RouteWarning)
path = Path([port1.center, port2.center])
error_component = extrude(path, layer=CONF.layer_error_path, width=1)
error_ref = ComponentReference(error_component)
return [error_ref]
def straight_connector(
port1: Port,
port2: Port,
cross_section: CrossSectionSpec = "xs_sc",
straight: ComponentFactory = straight_function,
) -> list[ComponentReference]:
"""
Connects between the two ports with a straight of the given cross-section.
Args:
port1: the starting port.
port2: the ending port.
cross_section: the cross-section to use.
straight: Component function for straights to use.
Returns:
A list of component references comprising the connection.
"""
if np.array_equal(port1.center, port2.center):
return []
path = Path([port1.center, port2.center])
# in usual cases, these angles should be the same, unless they are on opposite sides, in which they are 180 degrees separated
if abs(difference_between_angles(path.start_angle, port1.orientation)) > 1:
return _make_error_trace(
port1,
port2,
message=f"Not enough room to route between ports: {port1} and {port2}",
)
length = np.linalg.norm(port1.center - port2.center)
straight_component = straight(length=length, cross_section=cross_section)
straight_ref = ComponentReference(straight_component)
straight_ref.connect(list(straight_component.ports.keys())[0], port1)
return [straight_ref]
def auto_taper_connector(
port1: Port,
port2: Port,
cross_section: CrossSectionSpec = "xs_sc",
inner_connector: Connector = straight_connector,
) -> list[ComponentReference]:
"""
Connects the two ports with a straight in the specified cross_section, adding tapers at either end if necessary.
Args:
port1: the first port.
port2: the final port.
cross_section: the primary cross section to use for the route.
inner_connector: the connector to use after attaching tapers.
Returns:
A list of references comprising the connection.
"""
taper1 = taper_to_cross_section(port1, cross_section)
taper2 = taper_to_cross_section(port2, cross_section)
route_refs = []
if taper1:
route_refs.append(taper1)
output_port_name = _get_taper_io_port_names(taper1)[1]
port1 = taper1.ports[output_port_name]
if taper2:
route_refs.append(taper2)
output_port_name = _get_taper_io_port_names(taper2)[1]
port2 = taper2.ports[output_port_name]
conn = inner_connector(port1, port2, cross_section)
route_refs += conn
return route_refs
CONNECTORS = {
"low_loss": low_loss_connector,
"simple": straight_connector,
"auto_taper": auto_taper_connector,
None: straight_connector,
}
"""A dictionary of named connectors which can be used for all-angle routing"""
def _place_bend(bend_component: Component, position, rotation) -> ComponentReference:
"""
Places a bend by its control point at a given position and rotation. The control point of a bend is the intersection of the inverted port vectors.
Args:
bend_component: the bend component
position: the (x,y) position to place the bend
rotation: the rotation of the bend
Returns:
The resulting bend ComponentReference
"""
bend_ports = _get_bend_ports(bend_component)
bend_control_point = vector_intersection(
bend_ports[0].center,
bend_ports[0].orientation + 180,
bend_ports[1].center,
bend_ports[1].orientation + 180,
)
bend_ref = ComponentReference(bend_component)
bend_ref.rotate(
rotation + 180 - bend_ports[0].orientation, center=bend_control_point
)
bend_ref.move(origin=bend_control_point, destination=position)
return bend_ref
def _point_intersects_ray(p0, a0, p1, angle_tolerance=1e-4):
x0, y0 = p0
x1, y1 = p1
a1 = np.arctan2(y1 - y0, x1 - x0)
a1 = np.rad2deg(a1)
return abs(difference_between_angles(a1, a0)) < angle_tolerance
def _null_handler(refs):
return None
def _all_angle_connector(
port1: Port,
port2: Port,
bend_angle: float,
intersect: np.ndarray,
bend: ComponentFactory = bend_euler,
cross_section: CrossSectionSpec = "xs_sc",
connector1: Connector = straight_connector,
cross_section1: CrossSectionSpec | None = None,
connector2: Connector = straight_connector,
cross_section2: CrossSectionSpec | None = None,
report_segment_separation: Callable[[list[ComponentReference]], None] | None = None,
):
if cross_section1 is None:
cross_section1 = cross_section
if cross_section2 is None:
cross_section2 = cross_section
if report_segment_separation is None:
report_segment_separation = _null_handler
# in the case that the two ports already directly align
if bend_angle == 0 and _point_intersects_ray(
port1.center, port1.orientation, port2.center
):
straight_connection = connector2(port1, port2, cross_section=cross_section2)
report_segment_separation(straight_connection)
return straight_connection
if intersect is None:
# if difference_between_angles(port2.orientation, port1.orientation) == 180:
sample_bend = _get_bend(bend, angle=90, cross_section=cross_section)
bend_cs = _get_bend_ports(sample_bend)[0].cross_section
taper1 = taper_to_cross_section(port1, bend_cs)
taper2 = taper_to_cross_section(port2, bend_cs)
route_refs = []
if taper1:
route_refs.append(taper1)
output_port_name = _get_taper_io_port_names(taper1)[1]
port1 = taper1.ports[output_port_name]
if taper2:
route_refs.append(taper2)
output_port_name = _get_taper_io_port_names(taper2)[1]
port2 = taper2.ports[output_port_name]
# try:
bend_angles = _get_bend_angles(
port1.center, port2.center, port1.orientation, port2.orientation, bend=bend
)
bends = [
_get_bend(bend, angle=bend_angle, cross_section=cross_section)
for bend_angle in bend_angles
]
bend_refs = [ComponentReference(b) for b in bends]
bend_refs_ports = [_get_bend_ports(br) for br in bend_refs]
bend_refs[0].connect(bend_refs_ports[0][0], port1)
bend_refs[1].connect(bend_refs_ports[1][0], port2)
bend_refs_ports = [_get_bend_ports(br) for br in bend_refs]
connection = connector2(
bend_refs_ports[0][1], bend_refs_ports[1][1], cross_section=cross_section2
)
route_refs += bend_refs + connection
report_segment_separation(connection + [bend_refs[0]])
# except Exception as e:
# failure_message = f'Unable to complete route! Error message when attempting to create S bend between ports at {port1.center} and {port2.center}: {e}'
# route_refs += _make_error_trace(port1, port2, message=failure_message)
return route_refs
# return _make_error_trace(port1, port2, f'Port vectors do not intersect: {port1} and {port2}')
bend_component = _get_bend(bend, angle=bend_angle, cross_section=cross_section)
bend_ref = _place_bend(
bend_component, position=intersect, rotation=port1.orientation
)
bend_ref_ports = _get_bend_ports(bend_ref)
straight1 = connector1(port1, bend_ref_ports[0], cross_section=cross_section1)
straight2 = connector2(bend_ref_ports[1], port2, cross_section=cross_section2)
route_refs = straight1 + [bend_ref] + straight2
report_segment_separation(straight1)
return route_refs
def _get_bend(
component: ComponentSpec,
angle: float,
cross_section: CrossSectionSpec,
angle_precision: int = 9,
):
from gdsfactory.pdk import get_component
if (
isinstance(component, dict)
and "settings" in component
and "cross_section" in component["settings"]
):
return get_component(component, angle=round(angle, angle_precision))
return get_component(
component, angle=round(angle, angle_precision), cross_section=cross_section
)
def _get_bend_angles(p0, p1, a0, a1, bend):
"""get the direct line between the two points."""
import scipy.optimize
from gdsfactory.pdk import get_component
a_connect = np.arctan2(p1[1] - p0[1], p1[0] - p0[0])
a_connect_deg = np.rad2deg(a_connect)
# these are the angles which should be swept by the bends, if the bends were to take up no space
bend_angle_ideal_0 = difference_between_angles(a_connect_deg, a0)
bend_angle_ideal_1 = difference_between_angles(a_connect_deg + 180, a1)
# retrieves a function to calculate just the bend path (for efficiency) if available
bend_path_func = BEND_PATH_FUNCS.get(bend)
def optimization_func(d_angle):
# apply a delta to both bend angles
bend_angle0 = bend_angle_ideal_0 + d_angle
bend_angle1 = bend_angle_ideal_1 + d_angle
if bend_path_func is None:
bend0 = get_component(bend, angle=bend_angle0).ref()
bend1 = get_component(bend, angle=bend_angle1).ref()
bend0 = bend0.rotate(a0).move(p0)
bend1 = bend1.rotate(a1).move(p1)
bend0_output_port = _get_bend_ports(bend0)[1]
bend1_output_port = _get_bend_ports(bend1)[1]
dx, dy = bend1_output_port.center - bend0_output_port.center
else:
bend0 = Path(bend_path_func(angle=bend_angle0))
bend1 = Path(bend_path_func(angle=bend_angle1))
# get rotated coordinates of bends and check if dx/dy match up to tan(angle)
bend0 = bend0.rotate(a0).move(p0)
bend1 = bend1.rotate(a1).move(p1)
dx, dy = bend1.points[-1] - bend0.points[-1]
angle_est = np.rad2deg(np.arctan2(dy, dx))
angle_actual = a0 + bend_angle0
angle_error = abs(difference_between_angles(angle_actual, angle_est))
return angle_error
result = scipy.optimize.minimize_scalar(optimization_func, bounds=(-45, 45))
d_angle = result.x
bend_angle_0 = bend_angle_ideal_0 + d_angle
bend_angle_1 = bend_angle_ideal_1 + d_angle
return bend_angle_0, bend_angle_1
def _points_approx_equal(
point1: np.ndarray, point2: np.ndarray, tolerance: float = 5e-4
) -> bool:
return np.sqrt(np.sum(np.square(point1 - point2))) < tolerance
def _angles_approx_opposing(angle1: float, angle2: float, tolerance: float = 1e-4):
return abs(difference_between_angles(angle1 + 180, angle2)) < tolerance
[docs]
def get_bundle_all_angle(
ports1: list[Port],
ports2: list[Port],
steps: list[StepAllAngle] | None = None,
cross_section: CrossSectionSpec = "xs_sc",
bend: ComponentFactory = bend_euler,
connector: str | Callable[..., list[ComponentReference]] = "low_loss",
start_angle: float | None = None,
end_angle: float | None = None,
end_connector: str | Callable[..., list[ComponentReference]] | None = None,
end_cross_section: CrossSectionSpec | None = None,
separation: float = 3,
**kwargs,
) -> list[Route]:
"""Connects a bundle of ports, allowing steps which create waypoints at \
arbitrary, non-manhattan angles.
Args:
ports1: ports at the start of the bundle.
ports2: ports at the end of the bundle.
steps: a list of steps, which contain directives on how to proceed with the route. \
"x", "y", "dx", "dy", "ds", "exit_angle", "cross_section", "connector", "separation". \
The first route, between ports1[0] and ports2[0] will take on the role of the primary route, \
and other routes will follow, given the bundling logic. \
It is assume that both ports1 and ports2 are sorted. \
cross_section: the default cross-section of the bends. Then the specified connector may also use this information for straights in between.
bend: the default component to use for the bends.
connector: the default connector to use to connect between two ports.
start_angle: if defined and different from the angle of port1, \
will cap the starting port with a bend, as to exit with this angle.
end_angle: if defined, and different from the angle of port2, \
will cap the ending port with a bend, as to exit with this angle.
end_connector: specifies the connector to use for the final straight segment of the route.
end_cross_section: specifies the cross section to use for the final straight segment of the route.
separation: specifies the separation between adjacent routes.
kwargs: added for compatibility, but in general, kwargs will be ignored with a warning.
Returns:
List of Routes between ports1 and ports2.
.. plot::
:include-source:
import gdsfactory as gf
c = gf.Component("demo")
mmi = gf.components.mmi2x2(width_mmi=10, gap_mmi=3)
mmi1 = c << mmi
mmi2 = c << mmi
mmi2.move((100, 30))
mmi2.rotate(30)
routes = gf.routing.get_bundle_all_angle(
mmi1.get_ports_list(orientation=0),
[mmi2.ports["o2"], mmi2.ports["o1"]],
connector=None,
)
for route in routes:
c.add(route.references)
c.plot()
"""
from gdsfactory.pdk import get_cross_section
if kwargs:
warnings.warn(
f"Unrecognized arguments for all-angle route will be ignored: {kwargs}"
)
connector_func = connector if callable(connector) else get_connector(connector)
routes = []
is_primary_route = True
final_connector_func = connector_func
final_cross_section = cross_section
waypoints, angles = None, None
# by default, the second to last segment (in case of a two-step end connection) should have default
# connector and cross-section
semi_final_connector_func = connector_func
semi_final_cross_section = cross_section
# however, this can be overridden by providing a final step without any directional arguments
if steps and {"connector", "cross_section"}.issuperset(steps[-1]):
final_step = steps[-1]
steps = steps[:-1]
if "cross_section" in final_step:
semi_final_cross_section = final_step["cross_section"]
semi_final_connector_func = auto_taper_connector
if "connector" in final_step:
semi_final_connector_func = get_connector(final_step["connector"])
segment_separations = []
if end_connector or end_cross_section:
if end_cross_section:
final_cross_section = end_cross_section
final_connector_func = auto_taper_connector
if end_connector:
final_connector_func = (
end_connector
if callable(end_connector)
else get_connector(end_connector)
)
for port1, port2 in zip(ports1, ports2):
if _points_approx_equal(port1.center, port2.center) and _angles_approx_opposing(
port1.orientation, port2.orientation
):
continue
route_refs = []
if (
start_angle is not None
and difference_between_angles(start_angle, port1.orientation) != 0
):
bend_angle = difference_between_angles(start_angle, port1.orientation)
bend_component = _get_bend(
bend, angle=bend_angle, cross_section=cross_section
)
bend_ref = ComponentReference(bend_component)
bend_ref_ports = _get_bend_ports(bend_ref)
initial_taper = taper_to_cross_section(
port1, bend_ref_ports[0].cross_section
)
if initial_taper:
route_refs.append(initial_taper)
output_port_name = _get_taper_io_port_names(initial_taper)[1]
port1 = initial_taper.ports[output_port_name]
bend_ref.connect(bend_ref_ports[0], port1)
bend_ref_ports = _get_bend_ports(bend_ref)
route_refs.append(bend_ref)
port1 = bend_ref_ports[1]
if (
end_angle is not None
and difference_between_angles(end_angle, port2.orientation) != 0
):
bend_angle = difference_between_angles(end_angle, port2.orientation)
bend_component = _get_bend(
bend, angle=bend_angle, cross_section=cross_section
)
bend_ref = ComponentReference(bend_component)
bend_ref_ports = _get_bend_ports(bend_ref)
end_taper = taper_to_cross_section(port2, bend_ref_ports[0].cross_section)
if end_taper:
route_refs.append(end_taper)
output_port_name = _get_taper_io_port_names(end_taper)[1]
port2 = end_taper.ports[output_port_name]
bend_ref.connect(bend_ref_ports[0], port2)
bend_ref_ports = _get_bend_ports(bend_ref)
route_refs.append(bend_ref)
port2 = bend_ref_ports[1]
if not is_primary_route and steps:
# for non-primary routes in the bundle, reset the steps for each new route,
# based on the primary route's waypoints and angles
these_waypoints = [port1.center]
these_angles = [port1.orientation]
i_step = 0
intercept_sign = (
1
if vector_intersection(
these_waypoints[i_step],
these_angles[i_step],
waypoints[i_step + 1],
angles[i_step + 1] + 90,
raise_error=False,
)
is not None
else -1
)
for i_waypoint in range(1, len(waypoints) - 2):
# here we need the pitch for the *next* segment, after the bend
pitch = segment_separations[i_waypoint]
# the angle orthogonal from the next section's angle of propagation
offset_angle = angles[i_waypoint] + 90 * intercept_sign
# offset the next waypoint out by the desired pitch
offset_pt = waypoints[i_waypoint] + pitch * np.array(
[np.cos(np.deg2rad(offset_angle)), np.sin(np.deg2rad(offset_angle))]
)
# the next waypoint will be the intersect of the current vector and the line offset from the previous route's next segment
next_waypoint = _line_intercept(
offset_pt,
angles[i_waypoint],
these_waypoints[i_waypoint - 1],
these_angles[i_waypoint - 1],
)
these_waypoints.append(next_waypoint)
these_angles.append(angles[i_waypoint])
# final_intercept = vector_intersection(these_waypoints[-1], these_angles[-1], port2.center,
# port2.orientation, raise_error=True)
waypoints = these_waypoints
angles = these_angles
has_explicit_end_angle = True
if steps and is_primary_route:
x0, y0 = port1.center
a0 = port1.orientation
waypoints = [(x0, y0)]
angles = [port1.orientation]
a_final = None
# for each step, get the next waypoint
last_step_index = len(steps) - 1
for i_step, step in enumerate(steps):
x1, y1 = None, None
if not STEP_DIRECTIVES.issuperset(step):
invalid_step_directives = list(set(step.keys()) - STEP_DIRECTIVES)
raise ValueError(
f"Invalid step directives: {invalid_step_directives}. Valid directives are {list(STEP_DIRECTIVES)}"
)
if not {"x", "y", "dx", "dy", "ds"}.isdisjoint(step):
if "x" in step or "dx" in step:
x1 = step.get("x", x0)
x1 += step.get("dx", 0)
if "y" in step or "dy" in step:
y1 = step.get("y", y0)
y1 += step.get("dy", 0)
if x1 is not None and y1 is not None and a0 is not None:
raise ValueError(
"Route is overconstrained! x and y and incoming angle are all defined. Please remove one"
)
if "ds" in step:
if {"x", "y", "dx", "dy"}.isdisjoint(step):
if a0 is not None:
ds = step["ds"]
dx = ds * np.cos(np.deg2rad(a0))
dy = ds * np.sin(np.deg2rad(a0))
x1 = x0 + dx
y1 = y0 + dy
if i_step == last_step_index:
a_final = a0
else:
raise ValueError(
'When specifying "ds" as a step, the previous step must have an explicit exit_angle'
)
else:
raise ValueError(
f"Route is overconstrained! ds is defined as well as x/y/dx/dy: {step}"
)
if x1 is None:
if a0 is not None:
# get intercept with desired y value
x1, _ = vector_intersection(
(x0, y0), a0, (-1e6, y1), 0, max_distance=2e6
)
if i_step == last_step_index:
a_final = a0
elif i_step == last_step_index:
if "exit_angle" in step:
# if exit_angle is set, assume the segment is vertical, from the previous point to the specified y
x1 = x0
else:
# otherwise, let x be at the intercept of the specified y with the ray defined by port 2's vector
x1, _ = vector_intersection(
port2.center,
port2.orientation,
(-1e6, y1),
0,
max_distance=2e6,
)
else:
x1 = x0
elif y1 is None:
if a0 is not None:
# get intercept with desired x value
_, y1 = vector_intersection(
(x0, y0), a0, (x1, -1e6), 90, max_distance=2e6
)
if i_step == last_step_index:
a_final = a0
elif i_step == last_step_index:
if "exit_angle" in step:
y1 = y0
else:
_, y1 = vector_intersection(
port2.center,
port2.orientation,
(x1, -1e6),
90,
max_distance=2e6,
)
else:
y1 = y0
elif a0 is None and i_step == last_step_index:
a_final = np.rad2deg(np.arctan2(y1 - y0, x1 - x0))
waypoints.append((x1, y1))
a0 = step.get("exit_angle", a_final)
angles.append(a0)
x0, y0 = x1, y1
else:
raise ValueError(
f"Unable to process improperly or incompletely formed step routing command: {step}"
)
has_explicit_end_angle = angles[-1] is not None
if steps:
waypoints.append(port2.center)
bends = []
prev_port = port1
# go back over the waypoints and calculate unspecified angles and bends
for i_step, _ in enumerate(steps):
# override the current cross section and connector, if applicable
override_cs = steps[i_step].get("cross_section")
override_connector = steps[i_step].get("connector")
# override the current separation, if applicable
if override_cs:
this_cs = get_cross_section(override_cs)
this_connector = auto_taper_connector
else:
this_cs = cross_section
this_connector = connector_func
if override_connector:
this_connector = get_connector(override_connector)
# build up the sequence of references in the current segment of the route
if i_step + 1 < len(angles):
# the angle going into the current step
# angle 0 should always be explicitly defined, from the first port
angle0 = angles[i_step]
# the angle going out of the current step
angle_next = angles[i_step + 1]
# if no angle was explicitly defined, let's calculate it here
if angle_next is None:
p1 = waypoints[i_step + 2]
p0 = waypoints[i_step + 1]
angle_next = np.rad2deg(
np.arctan2(p1[1] - p0[1], p1[0] - p0[0])
)
dangle = difference_between_angles(angle_next, angle0)
if dangle == 180:
raise ValueError(
"Intermediate 180 degree bends are not currently supported!"
)
elif dangle == 0:
next_port = prev_port.flip()
next_port.center = waypoints[i_step + 1]
connection = this_connector(
prev_port, next_port, cross_section=this_cs
)
route_refs += connection
prev_port = next_port.flip()
else:
bend_component = _get_bend(
bend, angle=dangle, cross_section=cross_section
)
bend_ref = _place_bend(
bend_component,
position=waypoints[i_step + 1],
rotation=angle0,
)
bend_ref_ports = _get_bend_ports(bend_ref)
connection = this_connector(
prev_port, bend_ref_ports[0], cross_section=this_cs
)
route_refs += connection
route_refs.append(bend_ref)
bends.append(bend_ref)
prev_port = bend_ref_ports[1]
angles[i_step + 1] = angle_next
if is_primary_route:
segment_separations.append(separation)
if has_explicit_end_angle:
port1 = prev_port
else:
if _point_intersects_ray(
port2.center, port2.orientation, prev_port.center
):
final_connection = final_connector_func(
prev_port, port2, cross_section=final_cross_section
)
route_refs += final_connection
this_separation = separation
segment_separations.append(this_separation)
else:
route_refs += _make_error_trace(
prev_port,
port2,
"Cannot complete final step of route! "
"Try setting an exit_angle in your final "
"step which intersects the vector of the destination port.",
)
if not steps or has_explicit_end_angle:
intersect = vector_intersection(
port1.center,
port1.orientation,
port2.center,
port2.orientation,
raise_error=False,
)
report_segment_separation = None
def _report_separations_w_steps(refs) -> None:
segment_separations.append(separation)
if steps:
angles.insert(-1, port1.orientation)
waypoints.insert(-1, intersect)
report_segment_separation = _report_separations_w_steps
bend_angle = difference_between_angles(
port2.orientation + 180, port1.orientation
)
final_connection = _all_angle_connector(
port1,
port2,
bend_angle=bend_angle,
intersect=intersect,
bend=bend,
cross_section=cross_section,
connector1=semi_final_connector_func,
connector2=final_connector_func,
cross_section1=semi_final_cross_section,
cross_section2=final_cross_section,
report_segment_separation=report_segment_separation,
)
route_refs += final_connection
this_separation = separation
segment_separations.append(this_separation)
route_length = sum(r.info["length"] for r in route_refs)
route = Route(
references=route_refs,
ports=(port1, port2),
length=np.round(route_length, 3),
)
routes.append(route)
is_primary_route = False
return routes
if __name__ == "__main__":
import gdsfactory as gf
@gf.cell
def demo_issue() -> gf.Component:
c = gf.Component()
mmi = gf.components.mmi2x2(width_mmi=10, gap_mmi=3)
mmi1 = c << mmi
mmi2 = c << mmi
mmi2.move((100, 30))
mmi2.rotate(30)
routes = gf.routing.get_bundle_all_angle(
mmi1.get_ports_list(orientation=0),
[mmi2.ports["o2"], mmi2.ports["o1"]],
connector=None,
)
for route in routes:
c.add(route.references)
return c
# c = demo_issue(decorator=gf.decorators.flatten_offgrid_references)
c = demo_issue()
# c = c.flatten_offgrid_references()
c.show()