From 9d6fb985d8fbc131e3ccbcd7d3522cccb916deac Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 6 Mar 2026 23:09:59 -0800 Subject: [PATCH] [Pather/RenderPather/PathTool] Add updated pather tests --- masque/test/test_pather_api.py | 183 +++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 masque/test/test_pather_api.py diff --git a/masque/test/test_pather_api.py b/masque/test/test_pather_api.py new file mode 100644 index 0000000..0803b77 --- /dev/null +++ b/masque/test/test_pather_api.py @@ -0,0 +1,183 @@ +import numpy +from numpy import pi +from masque import Pather, RenderPather, Library, Port +from masque.builder.tools import PathTool + +def test_pather_trace_basic() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + + # Port rotation 0 points in +x (INTO device). + # To extend it, we move in -x direction. + p.pattern.ports['A'] = Port((0, 0), rotation=0) + + # Trace single port + p.at('A').trace(None, 5000) + assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0)) + + # Trace with bend + p.at('A').trace(True, 5000) # CCW bend + # Port was at (-5000, 0) rot 0. + # New wire starts at (-5000, 0) rot 0. + # Output port of wire before rotation: (5000, 500) rot -pi/2 + # Rotate by pi (since dev port rot is 0 and tool port rot is 0): + # (-5000, -500) rot pi - pi/2 = pi/2 + # Add to start: (-10000, -500) rot pi/2 + assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, -500)) + assert numpy.isclose(p.pattern.ports['A'].rotation, pi/2) + +def test_pather_trace_to() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + + p.pattern.ports['A'] = Port((0, 0), rotation=0) + + # Trace to x=-10000 + p.at('A').trace_to(None, x=-10000) + assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0)) + + # Trace to position=-20000 + p.at('A').trace_to(None, p=-20000) + assert numpy.allclose(p.pattern.ports['A'].offset, (-20000, 0)) + +def test_pather_bundle_trace() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + + p.pattern.ports['A'] = Port((0, 0), rotation=0) + p.pattern.ports['B'] = Port((0, 2000), rotation=0) + + # Straight bundle - all should align to same x + p.at(['A', 'B']).straight(xmin=-10000) + assert numpy.isclose(p.pattern.ports['A'].offset[0], -10000) + assert numpy.isclose(p.pattern.ports['B'].offset[0], -10000) + + # Bundle with bend + p.at(['A', 'B']).ccw(xmin=-20000, spacing=2000) + # Traveling in -x direction. CCW turn turns towards -y. + # A is at y=0, B is at y=2000. + # Rotation center is at y = -R. + # A is closer to center than B. So A is inner, B is outer. + # xmin is coordinate of innermost bend (A). + assert numpy.isclose(p.pattern.ports['A'].offset[0], -20000) + # B's bend is further out (more negative x) + assert numpy.isclose(p.pattern.ports['B'].offset[0], -22000) + +def test_pather_each_bound() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + + p.pattern.ports['A'] = Port((0, 0), rotation=0) + p.pattern.ports['B'] = Port((-1000, 2000), rotation=0) + + # Each should move by 5000 (towards -x) + p.at(['A', 'B']).trace(None, each=5000) + assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0)) + assert numpy.allclose(p.pattern.ports['B'].offset, (-6000, 2000)) + +def test_selection_management() -> None: + lib = Library() + p = Pather(lib) + p.pattern.ports['A'] = Port((0, 0), rotation=0) + p.pattern.ports['B'] = Port((0, 0), rotation=0) + + pp = p.at('A') + assert pp.ports == ['A'] + + pp.select('B') + assert pp.ports == ['A', 'B'] + + pp.deselect('A') + assert pp.ports == ['B'] + + pp.select(['A']) + assert pp.ports == ['B', 'A'] + + pp.drop() + assert 'A' not in p.pattern.ports + assert 'B' not in p.pattern.ports + assert pp.ports == [] + +def test_mark_fork() -> None: + lib = Library() + p = Pather(lib) + p.pattern.ports['A'] = Port((100, 200), rotation=1) + + pp = p.at('A') + pp.mark('B') + assert 'B' in p.pattern.ports + assert numpy.allclose(p.pattern.ports['B'].offset, (100, 200)) + assert p.pattern.ports['B'].rotation == 1 + assert pp.ports == ['A'] # mark keeps current selection + + pp.fork('C') + assert 'C' in p.pattern.ports + assert pp.ports == ['C'] # fork switches to new name + +def test_rename() -> None: + lib = Library() + p = Pather(lib) + p.pattern.ports['A'] = Port((0, 0), rotation=0) + + p.at('A').rename('B') + assert 'A' not in p.pattern.ports + assert 'B' in p.pattern.ports + + p.pattern.ports['C'] = Port((0, 0), rotation=0) + pp = p.at(['B', 'C']) + pp.rename({'B': 'D', 'C': 'E'}) + assert 'B' not in p.pattern.ports + assert 'C' not in p.pattern.ports + assert 'D' in p.pattern.ports + assert 'E' in p.pattern.ports + assert set(pp.ports) == {'D', 'E'} + +def test_renderpather_uturn_fallback() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + rp = RenderPather(lib, tools=tool) + rp.pattern.ports['A'] = Port((0, 0), rotation=0) + + # PathTool doesn't implement planU, so it should fall back to two planL calls + rp.at('A').uturn(offset=10000, length=5000) + + # Two steps should be added + assert len(rp.paths['A']) == 2 + assert rp.paths['A'][0].opcode == 'L' + assert rp.paths['A'][1].opcode == 'L' + + rp.render() + assert numpy.isclose(rp.pattern.ports['A'].rotation, pi) + +def test_pather_trace_into() -> None: + lib = Library() + tool = PathTool(layer='M1', width=1000) + p = Pather(lib, tools=tool) + + # 1. Straight connector + p.pattern.ports['A'] = Port((0, 0), rotation=0) + p.pattern.ports['B'] = Port((-10000, 0), rotation=pi) + p.at('A').trace_into('B', plug_destination=False) + assert 'B' in p.pattern.ports + assert 'A' in p.pattern.ports + assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0)) + + # 2. Single bend + p.pattern.ports['C'] = Port((0, 0), rotation=0) + p.pattern.ports['D'] = Port((-5000, 5000), rotation=pi/2) + p.at('C').trace_into('D', plug_destination=False) + assert 'D' in p.pattern.ports + assert 'C' in p.pattern.ports + assert numpy.allclose(p.pattern.ports['C'].offset, (-5000, 5000)) + + # 3. Jog (S-bend) + p.pattern.ports['E'] = Port((0, 0), rotation=0) + p.pattern.ports['F'] = Port((-10000, 2000), rotation=pi) + p.at('E').trace_into('F', plug_destination=False) + assert 'F' in p.pattern.ports + assert 'E' in p.pattern.ports + assert numpy.allclose(p.pattern.ports['E'].offset, (-10000, 2000))