Source code for zeroheliumkit.src.routing

"""
routing.py

This file contains a collection of functions for routing in a network.
Provides methods for creating fillet routing lines and Bezier curves between two anchor points.
"""

import numpy as np
from scipy.interpolate import BPoly
from numpy import deg2rad
from shapely import Point, LineString, affinity

from .utils import append_line, fmodnew
from .anchors import Anchor
from .functions import get_distance
from .errors import RouteError


[docs] def ArcLine(centerx: float, centery: float, radius: float, start_angle: float, end_angle: float, numsegments: int=10) -> LineString: """ Returns LineString representing an arc. Args: centerx (float): center.x of arcline centery (float): center.y of arcline radius (float): radius of the arcline start_angle (float): starting angle end_angle (float): end angle numsegments (int, optional): number of the segments. Defaults to 10. Returns: LineString: A LineString representing the arc. Example: >>> ArcLine(centerx=0, centery=0, radius=5, start_angle=0, end_angle=180) """ theta = np.radians(np.linspace(start_angle, end_angle, num=numsegments, endpoint=True)) x = centerx + radius * np.cos(theta) y = centery + radius * np.sin(theta) return LineString(np.column_stack([x, y]))
[docs] def get_fillet_params(anchor: Anchor, radius: float) -> tuple: """ Calculate the lengths required for fillet routing and return fillet parameters. The origin of fillet line is at (0,0). Args: anchor (Anchor): The anchor (second) point for the fillet. radius (float): The radius of the fillet curve. Returns: tuple: A tuple containing the calculated lengths and the anchor direction. """ rad = anchor.direction * np.pi/180 # calculating lengths if np.cos(rad)==1: length2 = anchor.y - radius else: length2 = (np.sign(rad) * (anchor.y - np.sign(rad) * radius * (1 - np.cos(rad))) / np.sqrt(1 - np.cos(rad)**2)) length1 = anchor.x - 1 * length2 * np.cos(rad) - np.sign(rad) * radius * np.sin(rad) return length1, length2, anchor.direction
[docs] def make_fillet_line(length1: float, length2: float, direction: float, radius: float, num_segments: int) -> LineString: """ Returns a fillet shape LineString. Args: length1 (float): length of the first section length2 (float): length of the second section placed after arcline direction (float): orientation of the endsegment radius (float): radius if the arcline num_segments (int): num of segments in the arcline Returns: LineString: A LineString in a fillet shape. """ direction = fmodnew(direction) rad = direction * np.pi/180 # creating first section pts = [(0, 0), (length1, 0)] skeletone = LineString(pts) # creating arcline section if direction > 0: x0, y0, phi0 = (0, radius, 270) else: x0, y0, phi0 = (0, -radius, 90) skeletone = append_line(skeletone, ArcLine(x0, y0, radius, phi0, phi0 + direction, num_segments)) # creating second straight line section skeletone = append_line(skeletone, LineString([(0, 0), (length2 * np.cos(rad), length2 * np.sin(rad))])) return skeletone
[docs] def normalize_anchors(anchor1: Anchor, anchor2: Anchor) -> Anchor: """ Returns normalizied anchor: anchor1 moves to (0,0) and rotates to dir=0 and anchor2 transforms accordingly. Args: anchor1 (Anchor): first anchor anchor2 (Anchor): second anchor Returns: Anchor: Normalized anchor with coordinates (0,0) and direction 0. """ direction = anchor2.direction - anchor1.direction point = affinity.rotate(Point((anchor2.x - anchor1.x, anchor2.y - anchor1.y)), angle=-anchor1.direction, origin=(0,0)) norm_anchor = Anchor(point, direction) return norm_anchor
[docs] def snap_line_to_anchor(anchor: Anchor, line: LineString) -> LineString: """ Snaps the linestring to the anchor position and direction Args: anchor (Anchor): line start point will be snapped to this anchor line (LineString): linestring to be snapped Returns: LineString: A linestring snapped to the anchor position and direction. """ line = affinity.rotate(line, anchor.direction, origin=(0,0)) line = affinity.translate(line, xoff=anchor.x, yoff=anchor.y) return line
[docs] def create_route(a1: Anchor, a2: Anchor, radius: float, num_segments: int=10, print_status: bool=False, bezier_cfg: dict={"extension_fraction": 0.1, "depth_factor": 3}) -> LineString: """ Returns a fillet routing linestring Args: a1 (Anchor): The first anchor. a2 (Anchor): The second anchor. radius (float): The radius of the fillet. num_segments (int, optional): The number of segments to use for the fillet curve. Defaults to 10. print_status (bool, optional): If True, prints the status of the route construction. Defaults to False. bezier_cfg (dict, optional): Configuration for Bezier curve construction. Defaults to {"extension_fraction": 0.1, "depth_factor": 3}. Returns: LineString: A linestring representing the route between two anchors. Raises: RouteError: If the route cannot be constructed between the two anchors. """ # normalizing anchors: a1 moves to (0,0) and rotates to dir=0 and a2 transforms accordingly anchor_norm = normalize_anchors(a1, a2) length1, length2, direction = get_fillet_params(anchor_norm, radius) rad = direction * np.pi/180 # constructing route depending on the params if length2 < 0 and anchor_norm.y == 0 and direction == 0: # straight line route_line = LineString([(0,0), (anchor_norm.x, 0)]) if print_status: print(f"route between {a1.label} and {a2.label}: straight line") return snap_line_to_anchor(a1, route_line) elif length2 < 0 and anchor_norm.y == 0 and direction == 180: raise RouteError(f"Cannot construct route between {a1.label} and {a2.label}") elif (length1 > 0 and length2 > 0) and np.sign(anchor_norm.y)==np.sign(np.sin(rad)): # valid fillet route_line = make_fillet_line(length1, length2, direction, radius, num_segments) # correction of the last point -> adjusting to anchor point route_line = LineString(list(route_line.coords[:-1]) + [anchor_norm.coords]) if print_status: print(f"route between {a1.label} and {a2.label}: fillet curve") return snap_line_to_anchor(a1, route_line) elif np.sign(anchor_norm.y)==np.sign(np.sin(rad)) and length1 > 0: # correcting the radius of the round section to make valid fillet print("using smaller radius for routing") while length2 < 0: radius = radius * 0.6 length1, length2, direction = get_fillet_params(anchor_norm, radius) route_line = make_fillet_line(length1, length2, direction, radius, num_segments) # correction of the last point -> adjusting to anchor point route_line = LineString(list(route_line.coords[:-1]) + [anchor_norm.coords]) if print_status: print(f"route between {a1.label} and {a2.label}: fillet curve, mod radius is {radius}") return snap_line_to_anchor(a1, route_line) else: # using bezier construct for invalid fillet params route_line = make_bezier_line(a1, a2, num_segments, bezier_cfg["extension_fraction"], bezier_cfg["depth_factor"]) if print_status: print(f"route between {a1.label} and {a2.label}: bezier curve") return route_line
[docs] def make_bezier_line(a1: Anchor, a2: Anchor, num_segments: int=20, extension_fraction: float=0.1, depth_factor: float=3) -> LineString: """ Returns a Bezier routing LineString. Args: a1 (Anchor): The first anchor. a2 (Anchor): The second anchor. num_segments (int, optional): The number of segments in the arcline. Defaults to 10. extension_fraction (float, optional): The fraction of the length between a1 and a2 excluded from construction of bezier curve. Defaults to 0.1. Choose betweer 0 and 1. depth_factor (float, optional): The factor to control the depth of the bezier curve. Defaults to 3. Choose between (2, 4). Returns: LineString: A Bezier curve LineString between two anchors. """ d = get_distance(a1, a2) * extension_fraction dx1, dy1 = d * np.cos(deg2rad(a1.direction)), d * np.sin(deg2rad(a1.direction)) dx2, dy2 = d * np.cos(deg2rad(a2.direction)), d * np.sin(deg2rad(a2.direction)) x = [a1.x + dx1, a1.x + depth_factor * dx1, a2.x - depth_factor * dx2, a2.x - dx2] y = [a1.y + dy1, a1.y + depth_factor * dy1, a2.y - depth_factor * dy2, a2.y - dy2] # nodes = np.asfortranarray([x, y]) # curve = bezier.Curve(nodes, degree=len(x)-1) # pts_bezier = curve.evaluate_multi(np.linspace(0.0, 1.0, num_segments)) nodes = np.asarray(list(zip(x, y))) curve = BPoly(nodes[:, np.newaxis, :], [0, 1]) pts_bezier = curve(np.linspace(0, 1, num_segments)).T line = LineString([(a1.x, a1.y)] + list(zip(*pts_bezier)) + [(a2.x, a2.y)]) return line