Compare commits
No commits in common. "master" and "path_improvements" have entirely different histories.
master
...
path_impro
@ -172,7 +172,6 @@ my_pattern.place(abstract, ...)
|
|||||||
|
|
||||||
# or
|
# or
|
||||||
my_pattern.place(library << make_tree(...), ...)
|
my_pattern.place(library << make_tree(...), ...)
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Quickly add geometry, labels, or refs:
|
### Quickly add geometry, labels, or refs:
|
||||||
|
@ -83,12 +83,10 @@ from .builder import (
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
ports2data as ports2data,
|
ports2data as ports2data,
|
||||||
oneshot as oneshot,
|
oneshot as oneshot,
|
||||||
R90 as R90,
|
|
||||||
R180 as R180,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
__version__ = '3.3'
|
__version__ = '3.2'
|
||||||
version = __version__ # legacy
|
version = __version__ # legacy
|
||||||
|
@ -21,7 +21,7 @@ def ell(
|
|||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
) -> dict[str, numpy.float64]:
|
) -> dict[str, float]:
|
||||||
"""
|
"""
|
||||||
Calculate extension for each port in order to build a 90-degree bend with the provided
|
Calculate extension for each port in order to build a 90-degree bend with the provided
|
||||||
channel spacing:
|
channel spacing:
|
||||||
|
@ -233,7 +233,7 @@ class Arc(Shape):
|
|||||||
r0, r1 = self.radii
|
r0, r1 = self.radii
|
||||||
|
|
||||||
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||||
a_ranges = cast(_array2x2_t, self._angles_to_parameters())
|
a_ranges = cast(tuple[tuple[float, float], tuple[float, float]], self._angles_to_parameters())
|
||||||
|
|
||||||
# Approximate perimeter via numerical integration
|
# Approximate perimeter via numerical integration
|
||||||
|
|
||||||
@ -246,13 +246,13 @@ class Arc(Shape):
|
|||||||
|
|
||||||
def get_arclens(n_pts: int, a0: float, a1: float, dr: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
def get_arclens(n_pts: int, a0: float, a1: float, dr: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||||
""" Get `n_pts` arclengths """
|
""" Get `n_pts` arclengths """
|
||||||
tt, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
t, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||||
r0sin = (r0 + dr) * numpy.sin(tt)
|
r0sin = (r0 + dr) * numpy.sin(t)
|
||||||
r1cos = (r1 + dr) * numpy.cos(tt)
|
r1cos = (r1 + dr) * numpy.cos(t)
|
||||||
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
||||||
#arc_lengths = numpy.diff(tt) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
#arc_lengths = numpy.diff(t) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||||
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
||||||
return arc_lengths, tt
|
return arc_lengths, t
|
||||||
|
|
||||||
wh = self.width / 2.0
|
wh = self.width / 2.0
|
||||||
if num_vertices is not None:
|
if num_vertices is not None:
|
||||||
@ -286,7 +286,6 @@ class Arc(Shape):
|
|||||||
thetas = thetas[::-1]
|
thetas = thetas[::-1]
|
||||||
return thetas
|
return thetas
|
||||||
|
|
||||||
thetas_inner: NDArray[numpy.float64]
|
|
||||||
if wh in (r0, r1):
|
if wh in (r0, r1):
|
||||||
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
||||||
else:
|
else:
|
||||||
@ -321,11 +320,11 @@ class Arc(Shape):
|
|||||||
|
|
||||||
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
||||||
"""
|
"""
|
||||||
a_ranges = cast(_array2x2_t, self._angles_to_parameters())
|
a_ranges = self._angles_to_parameters()
|
||||||
|
|
||||||
mins = []
|
mins = []
|
||||||
maxs = []
|
maxs = []
|
||||||
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||||
wh = sgn * self.width / 2
|
wh = sgn * self.width / 2
|
||||||
rx = self.radius_x + wh
|
rx = self.radius_x + wh
|
||||||
ry = self.radius_y + wh
|
ry = self.radius_y + wh
|
||||||
@ -336,13 +335,13 @@ class Arc(Shape):
|
|||||||
maxs.append([0, 0])
|
maxs.append([0, 0])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
a0, a1 = aa
|
a0, a1 = a
|
||||||
a0_offset = a0 - (a0 % (2 * pi))
|
a0_offset = a0 - (a0 % (2 * pi))
|
||||||
|
|
||||||
sin_r = numpy.sin(self.rotation)
|
sin_r = numpy.sin(self.rotation)
|
||||||
cos_r = numpy.cos(self.rotation)
|
cos_r = numpy.cos(self.rotation)
|
||||||
sin_a = numpy.sin(aa)
|
sin_a = numpy.sin(a)
|
||||||
cos_a = numpy.cos(aa)
|
cos_a = numpy.cos(a)
|
||||||
|
|
||||||
# Cutoff angles
|
# Cutoff angles
|
||||||
xpt = (-self.rotation) % (2 * pi) + a0_offset
|
xpt = (-self.rotation) % (2 * pi) + a0_offset
|
||||||
@ -432,19 +431,19 @@ class Arc(Shape):
|
|||||||
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
a_ranges = cast(_array2x2_t, self._angles_to_parameters())
|
a_ranges = self._angles_to_parameters()
|
||||||
|
|
||||||
mins = []
|
mins = []
|
||||||
maxs = []
|
maxs = []
|
||||||
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||||
wh = sgn * self.width / 2
|
wh = sgn * self.width / 2
|
||||||
rx = self.radius_x + wh
|
rx = self.radius_x + wh
|
||||||
ry = self.radius_y + wh
|
ry = self.radius_y + wh
|
||||||
|
|
||||||
sin_r = numpy.sin(self.rotation)
|
sin_r = numpy.sin(self.rotation)
|
||||||
cos_r = numpy.cos(self.rotation)
|
cos_r = numpy.cos(self.rotation)
|
||||||
sin_a = numpy.sin(aa)
|
sin_a = numpy.sin(a)
|
||||||
cos_a = numpy.cos(aa)
|
cos_a = numpy.cos(a)
|
||||||
|
|
||||||
# arc endpoints
|
# arc endpoints
|
||||||
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
||||||
@ -462,23 +461,21 @@ class Arc(Shape):
|
|||||||
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
||||||
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
||||||
"""
|
"""
|
||||||
aa = []
|
a = []
|
||||||
for sgn in (-1, +1):
|
for sgn in (-1, +1):
|
||||||
wh = sgn * self.width / 2.0
|
wh = sgn * self.width / 2.0
|
||||||
rx = self.radius_x + wh
|
rx = self.radius_x + wh
|
||||||
ry = self.radius_y + wh
|
ry = self.radius_y + wh
|
||||||
|
|
||||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(ai), ry * numpy.cos(ai)) for ai in self.angles)
|
a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles)
|
||||||
sign = numpy.sign(self.angles[1] - self.angles[0])
|
sign = numpy.sign(self.angles[1] - self.angles[0])
|
||||||
if sign != numpy.sign(a1 - a0):
|
if sign != numpy.sign(a1 - a0):
|
||||||
a1 += sign * 2 * pi
|
a1 += sign * 2 * pi
|
||||||
|
|
||||||
aa.append((a0, a1))
|
a.append((a0, a1))
|
||||||
return numpy.array(aa, dtype=float)
|
return numpy.array(a, dtype=float)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||||
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||||
|
|
||||||
_array2x2_t = tuple[tuple[float, float], tuple[float, float]]
|
|
||||||
|
@ -271,7 +271,7 @@ class Path(Shape):
|
|||||||
# TODO: Path.travel() needs testing
|
# TODO: Path.travel() needs testing
|
||||||
direction = numpy.array([1, 0])
|
direction = numpy.array([1, 0])
|
||||||
|
|
||||||
verts: list[NDArray[numpy.float64]] = [numpy.zeros(2)]
|
verts = [numpy.zeros(2)]
|
||||||
for angle, distance in travel_pairs:
|
for angle, distance in travel_pairs:
|
||||||
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
|
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
|
||||||
verts.append(verts[-1] + direction * distance)
|
verts.append(verts[-1] + direction * distance)
|
||||||
@ -307,8 +307,8 @@ class Path(Shape):
|
|||||||
bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
|
bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
|
||||||
ds = v[1:-1] - v[:-2] - perp[1:] + perp[:-1]
|
ds = v[1:-1] - v[:-2] - perp[1:] + perp[:-1]
|
||||||
|
|
||||||
rp = numpy.linalg.solve(As, bs[:, :, None])[:, 0]
|
rp = numpy.linalg.solve(As, bs)[:, 0, None]
|
||||||
rn = numpy.linalg.solve(As, ds[:, :, None])[:, 0]
|
rn = numpy.linalg.solve(As, ds)[:, 0, None]
|
||||||
|
|
||||||
intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1]
|
intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1]
|
||||||
intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1]
|
intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1]
|
||||||
|
@ -25,8 +25,6 @@ from .transform import (
|
|||||||
normalize_mirror as normalize_mirror,
|
normalize_mirror as normalize_mirror,
|
||||||
rotate_offsets_around as rotate_offsets_around,
|
rotate_offsets_around as rotate_offsets_around,
|
||||||
apply_transforms as apply_transforms,
|
apply_transforms as apply_transforms,
|
||||||
R90 as R90,
|
|
||||||
R180 as R180,
|
|
||||||
)
|
)
|
||||||
from .comparisons import (
|
from .comparisons import (
|
||||||
annotation2key as annotation2key,
|
annotation2key as annotation2key,
|
||||||
|
@ -2,11 +2,6 @@ import numpy
|
|||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
|
||||||
try:
|
|
||||||
from numpy import trapezoid
|
|
||||||
except ImportError:
|
|
||||||
from numpy import trapz as trapezoid
|
|
||||||
|
|
||||||
|
|
||||||
def bezier(
|
def bezier(
|
||||||
nodes: ArrayLike,
|
nodes: ArrayLike,
|
||||||
@ -27,30 +22,28 @@ def bezier(
|
|||||||
Returns:
|
Returns:
|
||||||
`[[x0, y0], [x1, y1], ...]` corresponding to `[tt0, tt1, ...]`
|
`[[x0, y0], [x1, y1], ...]` corresponding to `[tt0, tt1, ...]`
|
||||||
"""
|
"""
|
||||||
nodes = numpy.asarray(nodes)
|
|
||||||
tt = numpy.asarray(tt)
|
|
||||||
nn = nodes.shape[0]
|
nn = nodes.shape[0]
|
||||||
weights = numpy.ones(nn) if weights is None else numpy.asarray(weights)
|
if weights is None:
|
||||||
|
weights = numpy.ones(nn)
|
||||||
|
|
||||||
with numpy.errstate(divide='ignore'):
|
t_half0 = tt <= 0.5
|
||||||
umul = (tt / (1 - tt)).clip(max=1)
|
umul = tt / (1 - tt)
|
||||||
udiv = ((1 - tt) / tt).clip(max=1)
|
udiv = 1 / umul
|
||||||
|
umul[~t_half0] = 1
|
||||||
|
udiv[t_half0] = 1
|
||||||
|
|
||||||
hh = numpy.ones((tt.size,))
|
hh = numpy.ones((tt.size, 1))
|
||||||
qq = nodes[None, 0, :] * hh[:, None]
|
qq = nodes[None, 0] * hh
|
||||||
for kk in range(1, nn):
|
for kk in range(1, nn):
|
||||||
hh *= umul * (nn - kk) * weights[kk]
|
hh *= umul * (nn + 1 - kk) * weights[kk]
|
||||||
hh /= kk * udiv * weights[kk - 1] + hh
|
hh /= kk * udiv * weights[kk - 1] + hh
|
||||||
qq *= 1.0 - hh[:, None]
|
qq *= 1.0 - hh
|
||||||
qq += hh[:, None] * nodes[None, kk, :]
|
qq += hh * nodes[None, kk]
|
||||||
return qq
|
return qq
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def euler_bend(
|
def euler_bend(switchover_angle: float) -> NDArray[numpy.float64]:
|
||||||
switchover_angle: float,
|
|
||||||
num_points: int = 200,
|
|
||||||
) -> NDArray[numpy.float64]:
|
|
||||||
"""
|
"""
|
||||||
Generate a 90 degree Euler bend (AKA Clothoid bend or Cornu spiral).
|
Generate a 90 degree Euler bend (AKA Clothoid bend or Cornu spiral).
|
||||||
|
|
||||||
@ -58,44 +51,42 @@ def euler_bend(
|
|||||||
switchover_angle: After this angle, the bend will transition into a circular arc
|
switchover_angle: After this angle, the bend will transition into a circular arc
|
||||||
(and transition back to an Euler spiral on the far side). If this is set to
|
(and transition back to an Euler spiral on the far side). If this is set to
|
||||||
`>= pi / 4`, no circular arc will be added.
|
`>= pi / 4`, no circular arc will be added.
|
||||||
num_points: Number of points in the curve
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`[[x0, y0], ...]` for the curve
|
`[[x0, y0], ...]` for the curve
|
||||||
"""
|
"""
|
||||||
ll_max = numpy.sqrt(2 * switchover_angle) # total length of (one) spiral portion
|
# Switchover angle
|
||||||
ll_tot = 2 * ll_max + (pi / 2 - 2 * switchover_angle)
|
# AKA: Clothoid bend, Cornu spiral
|
||||||
num_points_spiral = numpy.floor(ll_max / ll_tot * num_points).astype(int)
|
theta_max = numpy.sqrt(2 * switchover_angle)
|
||||||
num_points_arc = num_points - 2 * num_points_spiral
|
|
||||||
|
|
||||||
def gen_spiral(ll_max: float) -> NDArray[numpy.float64]:
|
def gen_curve(theta_max: float):
|
||||||
xx = []
|
xx = []
|
||||||
yy = []
|
yy = []
|
||||||
for ll in numpy.linspace(0, ll_max, num_points_spiral):
|
for theta in numpy.linspace(0, theta_max, 100):
|
||||||
qq = numpy.linspace(0, ll, 1000) # integrate to current arclength
|
qq = numpy.linspace(0, theta, 1000)
|
||||||
xx.append(trapezoid( numpy.cos(qq * qq / 2), qq))
|
xx.append(numpy.trapz( numpy.cos(qq * qq / 2), qq))
|
||||||
yy.append(trapezoid(-numpy.sin(qq * qq / 2), qq))
|
yy.append(numpy.trapz(-numpy.sin(qq * qq / 2), qq))
|
||||||
xy_part = numpy.stack((xx, yy), axis=1)
|
xy_part = numpy.stack((xx, yy), axis=1)
|
||||||
return xy_part
|
return xy_part
|
||||||
|
|
||||||
xy_spiral = gen_spiral(ll_max)
|
xy_part = gen_curve(theta_max)
|
||||||
xy_parts = [xy_spiral]
|
xy_parts = [xy_part]
|
||||||
|
|
||||||
if switchover_angle < pi / 4:
|
if switchover_angle < pi / 4:
|
||||||
# Build a circular segment to join the two euler portions
|
# Build a circular segment to join the two euler portions
|
||||||
rmin = 1.0 / ll_max
|
rmin = 1.0 / theta_max
|
||||||
half_angle = pi / 4 - switchover_angle
|
half_angle = pi / 4 - switchover_angle
|
||||||
qq = numpy.linspace(half_angle * 2, 0, num_points_arc + 1) + switchover_angle
|
qq = numpy.linspace(half_angle * 2, 0, 10) + switchover_angle
|
||||||
xc = rmin * numpy.cos(qq)
|
xc = rmin * numpy.cos(qq)
|
||||||
yc = rmin * numpy.sin(qq) + xy_spiral[-1, 1]
|
yc = rmin * numpy.sin(qq) + xy_part[-1, 1]
|
||||||
xc += xy_spiral[-1, 0] - xc[0]
|
xc += xy_part[-1, 0] - xc[0]
|
||||||
yc += xy_spiral[-1, 1] - yc[0]
|
yc += xy_part[-1, 1] - yc[0]
|
||||||
xy_parts.append(numpy.stack((xc[1:], yc[1:]), axis=1))
|
xy_parts.append(numpy.stack((xc, yc), axis=1))
|
||||||
|
|
||||||
endpoint_xy = xy_parts[-1][-1, :]
|
endpoint_xy = xy_parts[-1][-1, :]
|
||||||
second_spiral = xy_spiral[::-1, ::-1] + endpoint_xy - xy_spiral[-1, ::-1]
|
second_curve = xy_part[::-1, ::-1] + endpoint_xy - xy_part[-1, ::-1]
|
||||||
|
|
||||||
xy_parts.append(second_spiral)
|
xy_parts.append(second_curve)
|
||||||
xy = numpy.concatenate(xy_parts)
|
xy = numpy.concatenate(xy_parts)
|
||||||
|
|
||||||
# Remove any 2x-duplicate points
|
# Remove any 2x-duplicate points
|
||||||
|
@ -9,11 +9,6 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
|
||||||
|
|
||||||
# Constants for shorthand rotations
|
|
||||||
R90 = pi / 2
|
|
||||||
R180 = pi
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user