From 0e34242ba5946a9f478c5b6dd2b37017177f75d7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 3 Mar 2025 00:51:45 -0800 Subject: [PATCH 1/4] misc type hint fixes --- masque/builder/utils.py | 2 +- masque/shapes/arc.py | 9 ++++++--- masque/shapes/path.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/masque/builder/utils.py b/masque/builder/utils.py index c466c71..6e3334d 100644 --- a/masque/builder/utils.py +++ b/masque/builder/utils.py @@ -21,7 +21,7 @@ def ell( *, spacing: float | ArrayLike | None = None, set_rotation: float | None = None, - ) -> dict[str, float]: + ) -> dict[str, numpy.float64]: """ Calculate extension for each port in order to build a 90-degree bend with the provided channel spacing: diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 67d932d..4b69dbb 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -233,7 +233,7 @@ class Arc(Shape): r0, r1 = self.radii # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) - a_ranges = cast(tuple[tuple[float, float], tuple[float, float]], self._angles_to_parameters()) + a_ranges = cast(_array2x2_t, self._angles_to_parameters()) # Approximate perimeter via numerical integration @@ -286,6 +286,7 @@ class Arc(Shape): thetas = thetas[::-1] return thetas + thetas_inner: NDArray[numpy.float64] if wh in (r0, r1): thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin else: @@ -320,7 +321,7 @@ class Arc(Shape): If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. """ - a_ranges = self._angles_to_parameters() + a_ranges = cast(_array2x2_t, self._angles_to_parameters()) mins = [] maxs = [] @@ -431,7 +432,7 @@ class Arc(Shape): [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse. ``` """ - a_ranges = self._angles_to_parameters() + a_ranges = cast(_array2x2_t, self._angles_to_parameters()) mins = [] maxs = [] @@ -479,3 +480,5 @@ class Arc(Shape): angles = f' a°{numpy.rad2deg(self.angles)}' rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' return f'' + +_array2x2_t = tuple[tuple[float, float], tuple[float, float]] diff --git a/masque/shapes/path.py b/masque/shapes/path.py index a53b5c7..717e59f 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -271,7 +271,7 @@ class Path(Shape): # TODO: Path.travel() needs testing direction = numpy.array([1, 0]) - verts = [numpy.zeros(2)] + verts: list[NDArray[numpy.float64]] = [numpy.zeros(2)] for angle, distance in travel_pairs: direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T verts.append(verts[-1] + direction * distance) From c74573e7ddc71b4b5dd1aed66477db74da1f90d3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 3 Mar 2025 00:52:24 -0800 Subject: [PATCH 2/4] [Arc] improve some variable names --- masque/shapes/arc.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 4b69dbb..b3a9b7d 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -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]]: """ Get `n_pts` arclengths """ - t, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points - r0sin = (r0 + dr) * numpy.sin(t) - r1cos = (r1 + dr) * numpy.cos(t) + tt, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points + r0sin = (r0 + dr) * numpy.sin(tt) + r1cos = (r1 + dr) * numpy.cos(tt) arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos) - #arc_lengths = numpy.diff(t) * (arc_dl[1:] + arc_dl[:-1]) / 2 + #arc_lengths = numpy.diff(tt) * (arc_dl[1:] + arc_dl[:-1]) / 2 arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2 - return arc_lengths, t + return arc_lengths, tt wh = self.width / 2.0 if num_vertices is not None: @@ -325,7 +325,7 @@ class Arc(Shape): mins = [] maxs = [] - for a, sgn in zip(a_ranges, (-1, +1), strict=True): + for aa, sgn in zip(a_ranges, (-1, +1), strict=True): wh = sgn * self.width / 2 rx = self.radius_x + wh ry = self.radius_y + wh @@ -336,13 +336,13 @@ class Arc(Shape): maxs.append([0, 0]) continue - a0, a1 = a + a0, a1 = aa a0_offset = a0 - (a0 % (2 * pi)) sin_r = numpy.sin(self.rotation) cos_r = numpy.cos(self.rotation) - sin_a = numpy.sin(a) - cos_a = numpy.cos(a) + sin_a = numpy.sin(aa) + cos_a = numpy.cos(aa) # Cutoff angles xpt = (-self.rotation) % (2 * pi) + a0_offset @@ -436,15 +436,15 @@ class Arc(Shape): mins = [] maxs = [] - for a, sgn in zip(a_ranges, (-1, +1), strict=True): + for aa, sgn in zip(a_ranges, (-1, +1), strict=True): wh = sgn * self.width / 2 rx = self.radius_x + wh ry = self.radius_y + wh sin_r = numpy.sin(self.rotation) cos_r = numpy.cos(self.rotation) - sin_a = numpy.sin(a) - cos_a = numpy.cos(a) + sin_a = numpy.sin(aa) + cos_a = numpy.cos(aa) # arc endpoints xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) @@ -462,19 +462,19 @@ class Arc(Shape): "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 = [] + aa = [] for sgn in (-1, +1): wh = sgn * self.width / 2.0 rx = self.radius_x + wh ry = self.radius_y + wh - a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles) + a0, a1 = (numpy.arctan2(rx * numpy.sin(ai), ry * numpy.cos(ai)) for ai in self.angles) sign = numpy.sign(self.angles[1] - self.angles[0]) if sign != numpy.sign(a1 - a0): a1 += sign * 2 * pi - a.append((a0, a1)) - return numpy.array(a, dtype=float) + aa.append((a0, a1)) + return numpy.array(aa, dtype=float) def __repr__(self) -> str: angles = f' a°{numpy.rad2deg(self.angles)}' From b27b1d93d85fb6047375d718cfa6b518e72fa378 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 3 Mar 2025 00:52:51 -0800 Subject: [PATCH 3/4] [utils.curves.bezier] improve handling of non-ndarray inputs --- masque/utils/curves.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/masque/utils/curves.py b/masque/utils/curves.py index 276c0ed..871b58e 100644 --- a/masque/utils/curves.py +++ b/masque/utils/curves.py @@ -22,9 +22,10 @@ def bezier( Returns: `[[x0, y0], [x1, y1], ...]` corresponding to `[tt0, tt1, ...]` """ + nodes = numpy.asarray(nodes) + tt = numpy.asarray(tt) nn = nodes.shape[0] - if weights is None: - weights = numpy.ones(nn) + weights = numpy.ones(nn) if weights is None else numpy.asarray(weights) t_half0 = tt <= 0.5 umul = tt / (1 - tt) From 858ef4a1146a2a2c72fd165f6f36cf40f2d31bba Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 3 Mar 2025 00:53:34 -0800 Subject: [PATCH 4/4] [utils.curves.euler_bend] add num_point arg and improve naming --- masque/utils/curves.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/masque/utils/curves.py b/masque/utils/curves.py index 871b58e..41f82ad 100644 --- a/masque/utils/curves.py +++ b/masque/utils/curves.py @@ -44,7 +44,10 @@ def bezier( -def euler_bend(switchover_angle: float) -> NDArray[numpy.float64]: +def euler_bend( + switchover_angle: float, + num_points: int = 200, + ) -> NDArray[numpy.float64]: """ Generate a 90 degree Euler bend (AKA Clothoid bend or Cornu spiral). @@ -52,42 +55,44 @@ def euler_bend(switchover_angle: float) -> NDArray[numpy.float64]: 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 `>= pi / 4`, no circular arc will be added. + num_points: Number of points in the curve Returns: `[[x0, y0], ...]` for the curve """ - # Switchover angle - # AKA: Clothoid bend, Cornu spiral - theta_max = numpy.sqrt(2 * switchover_angle) + ll_max = numpy.sqrt(2 * switchover_angle) # total length of (one) spiral portion + ll_tot = 2 * ll_max + (pi / 2 - 2 * switchover_angle) + num_points_spiral = numpy.floor(ll_max / ll_tot * num_points).astype(int) + num_points_arc = num_points - 2 * num_points_spiral - def gen_curve(theta_max: float): + def gen_spiral(ll_max: float): xx = [] yy = [] - for theta in numpy.linspace(0, theta_max, 100): - qq = numpy.linspace(0, theta, 1000) + for ll in numpy.linspace(0, ll_max, num_points_spiral): + qq = numpy.linspace(0, ll, 1000) # integrate to current arclength xx.append(numpy.trapz( numpy.cos(qq * qq / 2), qq)) yy.append(numpy.trapz(-numpy.sin(qq * qq / 2), qq)) xy_part = numpy.stack((xx, yy), axis=1) return xy_part - xy_part = gen_curve(theta_max) - xy_parts = [xy_part] + xy_spiral = gen_spiral(ll_max) + xy_parts = [xy_spiral] if switchover_angle < pi / 4: # Build a circular segment to join the two euler portions - rmin = 1.0 / theta_max + rmin = 1.0 / ll_max half_angle = pi / 4 - switchover_angle - qq = numpy.linspace(half_angle * 2, 0, 10) + switchover_angle + qq = numpy.linspace(half_angle * 2, 0, num_points_arc + 1) + switchover_angle xc = rmin * numpy.cos(qq) - yc = rmin * numpy.sin(qq) + xy_part[-1, 1] - xc += xy_part[-1, 0] - xc[0] - yc += xy_part[-1, 1] - yc[0] - xy_parts.append(numpy.stack((xc, yc), axis=1)) + yc = rmin * numpy.sin(qq) + xy_spiral[-1, 1] + xc += xy_spiral[-1, 0] - xc[0] + yc += xy_spiral[-1, 1] - yc[0] + xy_parts.append(numpy.stack((xc[1:], yc[1:]), axis=1)) endpoint_xy = xy_parts[-1][-1, :] - second_curve = xy_part[::-1, ::-1] + endpoint_xy - xy_part[-1, ::-1] + second_spiral = xy_spiral[::-1, ::-1] + endpoint_xy - xy_spiral[-1, ::-1] - xy_parts.append(second_curve) + xy_parts.append(second_spiral) xy = numpy.concatenate(xy_parts) # Remove any 2x-duplicate points