diff --git a/masque/builder/tools.py b/masque/builder/tools.py index c17a7b9..5fc6943 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -791,46 +791,55 @@ class AutoTool(Tool, metaclass=ABCMeta): Solve for a path consisting of two L-bends connected by a straight segment. Used for both U-turns (ccw1 == ccw2) and S-bends (ccw1 != ccw2). """ - is_u = bool(ccw1) == bool(ccw2) - out_rot = 0 if is_u else pi - for plan1 in self._iter_l_plans(ccw1, in_ptype, None): - rot_mid = rotation_matrix_2d(pi + plan1.bend_angle) - mid_axis = rot_mid @ numpy.array((1.0, 0.0)) - if not numpy.isclose(mid_axis[0], 0) or numpy.isclose(mid_axis[1], 0): - continue + for plan2 in self._iter_l_plans(ccw2, plan1.out_ptype, out_ptype): + # Solving for: + # X = L1_total +/- R2_actual = length + # Y = R1_actual + L2_straight + overhead_mid + overhead_b2 + L3_total = jog - for straight_mid in self.straights: - mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype) - mid_trans = self.transitions.get(mid_ptype_pair, None) - mid_trans_dxy = self._itransition2dxy(mid_trans) + # Sign for overhead_y2 depends on whether it's a U-turn or S-bend + is_u = bool(ccw1) == bool(ccw2) + # U-turn: X = L1_total - R2 = length => L1_total = length + R2 + # S-bend: X = L1_total + R2 = length => L1_total = length - R2 + l1_total = length + (abs(plan2.overhead_y) if is_u else -abs(plan2.overhead_y)) + l1_straight = l1_total - plan1.overhead_x - for plan2 in self._iter_l_plans(ccw2, straight_mid.ptype, out_ptype): - fixed_dxy = numpy.array((plan1.overhead_x, plan1.overhead_y)) - fixed_dxy += rot_mid @ ( - mid_trans_dxy - + numpy.array((plan2.overhead_x, plan2.overhead_y)) - ) - l1_straight = length - fixed_dxy[0] - l2_straight = (jog - fixed_dxy[1]) / mid_axis[1] + if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1]: + for straight_mid in self.straights: + # overhead_mid accounts for the transition from bend1 to straight_mid + mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype) + mid_trans = self.transitions.get(mid_ptype_pair, None) + mid_trans_dxy = self._itransition2dxy(mid_trans) - if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1] \ - and straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]: - l3_straight = 0 - if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]: - ldata0 = self.LData( - l1_straight, plan1.straight, kwargs, ccw1, plan1.bend, - plan1.in_trans, plan1.b_trans, plan1.out_trans, - ) - ldata1 = self.LData( - l3_straight, plan2.straight, kwargs, ccw2, plan2.bend, - plan2.in_trans, plan2.b_trans, plan2.out_trans, - ) + # b_trans2 accounts for the transition from straight_mid to bend2 + b2_trans = None + if plan2.bend is not None and plan2.bend.in_port.ptype != straight_mid.ptype: + b2_trans = self.transitions.get((plan2.bend.in_port.ptype, straight_mid.ptype), None) + b2_trans_dxy = self._itransition2dxy(b2_trans) - data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans) - out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype) - return out_port, data + l2_straight = abs(jog) - abs(plan1.overhead_y) - plan2.overhead_x - mid_trans_dxy[0] - b2_trans_dxy[0] + + if straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]: + # Found a solution! + # For plan2, we assume l3_straight = 0. + # We need to verify if l3=0 is valid for plan2.straight. + l3_straight = 0 + if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]: + ldata0 = self.LData( + l1_straight, plan1.straight, kwargs, ccw1, plan1.bend, + plan1.in_trans, plan1.b_trans, plan1.out_trans, + ) + ldata1 = self.LData( + l3_straight, plan2.straight, kwargs, ccw2, plan2.bend, + b2_trans, None, plan2.out_trans, + ) + + data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans) + # out_port is at (length, jog) rot pi (for S-bend) or 0 (for U-turn) relative to input + out_rot = 0 if is_u else pi + out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype) + return out_port, data raise BuildError(f"Failed to find a valid double-L configuration for {length=}, {jog=}") straights: list[Straight] diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 00c5714..2adfe10 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -231,8 +231,6 @@ class Arc(PositionableImpl, Shape): if (num_vertices is None) and (max_arclen is None): raise PatternError('Max number of points and arclength left unspecified' + ' (default was also overridden)') - if max_arclen is not None and (numpy.isnan(max_arclen) or max_arclen <= 0): - raise PatternError('Max arclength must be positive and not NaN') r0, r1 = self.radii @@ -259,19 +257,13 @@ class Arc(PositionableImpl, Shape): return arc_lengths, tt wh = self.width / 2.0 - arclen_limits: list[float] = [] - if max_arclen is not None: - arclen_limits.append(max_arclen) if num_vertices is not None: n_pts = numpy.ceil(max(self.radii + wh) / min(self.radii) * num_vertices * 100).astype(int) perimeter_inner = get_arclens(n_pts, *a_ranges[0], dr=-wh)[0].sum() perimeter_outer = get_arclens(n_pts, *a_ranges[1], dr= wh)[0].sum() implied_arclen = (perimeter_outer + perimeter_inner + self.width * 2) / num_vertices - if not (numpy.isnan(implied_arclen) or implied_arclen <= 0): - arclen_limits.append(implied_arclen) - if not arclen_limits: - raise PatternError('Arc polygonization could not determine a valid max_arclen') - max_arclen = min(arclen_limits) + max_arclen = min(implied_arclen, max_arclen if max_arclen is not None else numpy.inf) + assert max_arclen is not None def get_thetas(inner: bool) -> NDArray[numpy.float64]: """ Figure out the parameter values at which we should place vertices to meet the arclength constraint""" diff --git a/masque/test/test_autotool_refactor.py b/masque/test/test_autotool_refactor.py index 3109447..d5f1c86 100644 --- a/masque/test/test_autotool_refactor.py +++ b/masque/test/test_autotool_refactor.py @@ -60,60 +60,6 @@ def multi_bend_tool(): ) return tool, lib - -@pytest.fixture -def asymmetric_transition_tool() -> AutoTool: - lib = Library() - - bend_pat = Pattern() - bend_pat.ports["in"] = Port((0, 0), 0, ptype="core") - bend_pat.ports["out"] = Port((2, -2), pi / 2, ptype="core") - lib["core_bend"] = bend_pat - - trans_pat = Pattern() - trans_pat.ports["CORE"] = Port((0, 0), 0, ptype="core") - trans_pat.ports["MID"] = Port((3, 1), pi, ptype="mid") - lib["core_mid"] = trans_pat - - return AutoTool( - straights=[ - AutoTool.Straight( - ptype="core", - fn=lambda length: make_straight(length, ptype="core"), - in_port_name="A", - out_port_name="B", - length_range=(0, 3), - ), - AutoTool.Straight( - ptype="mid", - fn=lambda length: make_straight(length, ptype="mid"), - in_port_name="A", - out_port_name="B", - length_range=(0, 1e8), - ), - ], - bends=[ - AutoTool.Bend(lib.abstract("core_bend"), "in", "out", clockwise=True, mirror=True), - ], - sbends=[], - transitions={ - ("mid", "core"): AutoTool.Transition(lib.abstract("core_mid"), "MID", "CORE"), - }, - default_out_ptype="core", - ).add_complementary_transitions() - - -def assert_trace_matches_plan(plan_port: Port, tree: Library, port_names: tuple[str, str] = ("A", "B")) -> None: - pat = tree.top_pattern() - out_port = pat[port_names[1]] - dxy, rot = pat[port_names[0]].measure_travel(out_port) - assert_allclose(dxy, plan_port.offset) - assert rot is not None - assert plan_port.rotation is not None - assert_allclose(rot, plan_port.rotation) - assert out_port.ptype == plan_port.ptype - - def test_autotool_planL_selection(multi_bend_tool) -> None: tool, _ = multi_bend_tool @@ -147,19 +93,6 @@ def test_autotool_planU_consistency(multi_bend_tool) -> None: assert data.ldata1.straight_length == 0 assert data.ldata1.bend.abstract.name == "b1" - -def test_autotool_traceU_matches_plan_with_asymmetric_transition(asymmetric_transition_tool: AutoTool) -> None: - tool = asymmetric_transition_tool - - plan_port, data = tool.planU(12, length=0, in_ptype="core") - - assert data.ldata1.in_transition is not None - assert data.ldata1.b_transition is not None - - tree = tool.traceU(12, length=0, in_ptype="core") - assert_trace_matches_plan(plan_port, tree) - - def test_autotool_planS_double_L(multi_bend_tool) -> None: tool, lib = multi_bend_tool @@ -176,19 +109,6 @@ def test_autotool_planS_double_L(multi_bend_tool) -> None: assert data.l2_length == 6 -def test_autotool_traceS_double_l_matches_plan_with_asymmetric_transition(asymmetric_transition_tool: AutoTool) -> None: - tool = asymmetric_transition_tool - - plan_port, data = tool.planS(4, 10, in_ptype="core") - - assert isinstance(data, AutoTool.UData) - assert data.ldata1.in_transition is not None - assert data.ldata1.b_transition is not None - - tree = tool.traceS(4, 10, in_ptype="core") - assert_trace_matches_plan(plan_port, tree) - - def test_autotool_planS_pure_sbend_with_transition_dx() -> None: lib = Library() diff --git a/masque/test/test_shape_advanced.py b/masque/test/test_shape_advanced.py index 689df2a..350e8f0 100644 --- a/masque/test/test_shape_advanced.py +++ b/masque/test/test_shape_advanced.py @@ -122,12 +122,6 @@ def test_curve_polygonizers_clamp_large_max_arclen() -> None: assert len(polys[0].vertices) >= 3 -def test_arc_polygonization_rejects_nan_implied_arclen() -> None: - arc = Arc(radii=(10, 20), angles=(0, numpy.nan), width=2) - with pytest.raises(PatternError, match='valid max_arclen'): - arc.to_polygons(num_vertices=24) - - def test_ellipse_integer_radii_scale_cleanly() -> None: ellipse = Ellipse(radii=(10, 20)) ellipse.scale_by(0.5)