936 lines
33 KiB
Python
936 lines
33 KiB
Python
from typing import Any
|
|
|
|
import pytest
|
|
import numpy
|
|
from numpy import pi
|
|
from masque import Pather, Library, Pattern, Port
|
|
from masque.builder.tools import PathTool, Tool
|
|
from masque.error import BuildError, PortError, PatternError
|
|
|
|
def test_pather_trace_basic() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
|
|
# 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 p.pattern.ports['A'].rotation is not None
|
|
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, auto_render=False)
|
|
|
|
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, auto_render=False)
|
|
|
|
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, auto_render=False)
|
|
|
|
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_mark_fork_reject_overwrite_and_duplicate_targets() -> None:
|
|
lib = Library()
|
|
|
|
p_mark = Pather(lib, pattern=Pattern(ports={
|
|
'A': Port((0, 0), rotation=0),
|
|
'C': Port((2, 0), rotation=0),
|
|
}))
|
|
with pytest.raises(PortError, match='overwrite existing ports'):
|
|
p_mark.at('A').mark('C')
|
|
assert numpy.allclose(p_mark.pattern.ports['C'].offset, (2, 0))
|
|
|
|
p_fork = Pather(lib, pattern=Pattern(ports={
|
|
'A': Port((0, 0), rotation=0),
|
|
'B': Port((1, 0), rotation=0),
|
|
}))
|
|
pp = p_fork.at(['A', 'B'])
|
|
with pytest.raises(PortError, match='targets would collide'):
|
|
pp.fork({'A': 'X', 'B': 'X'})
|
|
assert set(p_fork.pattern.ports) == {'A', 'B'}
|
|
assert pp.ports == ['A', 'B']
|
|
|
|
|
|
def test_mark_fork_dead_overwrite_and_duplicate_targets() -> None:
|
|
lib = Library()
|
|
p = Pather(lib, pattern=Pattern(ports={
|
|
'A': Port((0, 0), rotation=0),
|
|
'B': Port((1, 0), rotation=0),
|
|
'C': Port((2, 0), rotation=0),
|
|
}))
|
|
p.set_dead()
|
|
|
|
p.at('A').mark('C')
|
|
assert numpy.allclose(p.pattern.ports['C'].offset, (0, 0))
|
|
|
|
pp = p.at(['A', 'B'])
|
|
pp.fork({'A': 'X', 'B': 'X'})
|
|
assert numpy.allclose(p.pattern.ports['X'].offset, (1, 0))
|
|
assert pp.ports == ['X']
|
|
|
|
|
|
def test_mark_fork_reject_missing_sources() -> None:
|
|
lib = Library()
|
|
p = Pather(lib, pattern=Pattern(ports={
|
|
'A': Port((0, 0), rotation=0),
|
|
'B': Port((1, 0), rotation=0),
|
|
}))
|
|
|
|
with pytest.raises(PortError, match='selected ports'):
|
|
p.at(['A', 'B']).mark({'Z': 'C'})
|
|
|
|
with pytest.raises(PortError, match='selected ports'):
|
|
p.at(['A', 'B']).fork({'Z': 'C'})
|
|
|
|
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 = Pather(lib, tools=tool, auto_render=False)
|
|
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 rp.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(rp.pattern.ports['A'].rotation, pi)
|
|
|
|
def test_autotool_uturn() -> None:
|
|
from masque.builder.tools import AutoTool
|
|
lib = Library()
|
|
|
|
# Setup AutoTool with a simple straight and a bend
|
|
def make_straight(length: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.rect(layer='M1', xmin=0, xmax=length, yctr=0, ly=1000)
|
|
pat.ports['in'] = Port((0, 0), 0)
|
|
pat.ports['out'] = Port((length, 0), pi)
|
|
return pat
|
|
|
|
bend_pat = Pattern()
|
|
bend_pat.polygon(layer='M1', vertices=[(0, -500), (0, 500), (1000, -500)])
|
|
bend_pat.ports['in'] = Port((0, 0), 0)
|
|
bend_pat.ports['out'] = Port((500, -500), pi/2)
|
|
lib['bend'] = bend_pat
|
|
|
|
tool = AutoTool(
|
|
straights=[AutoTool.Straight(ptype='wire', fn=make_straight, in_port_name='in', out_port_name='out')],
|
|
bends=[AutoTool.Bend(abstract=lib.abstract('bend'), in_port_name='in', out_port_name='out', clockwise=True)],
|
|
sbends=[],
|
|
transitions={},
|
|
default_out_ptype='wire'
|
|
)
|
|
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), 0)
|
|
|
|
# CW U-turn (jog < 0)
|
|
# R = 500. jog = -2000. length = 1000.
|
|
# p0 = planL(length=1000) -> out at (1000, -500) rot pi/2
|
|
# R2 = 500.
|
|
# l2_length = abs(-2000) - abs(-500) - 500 = 1000.
|
|
p.at('A').uturn(offset=-2000, length=1000)
|
|
|
|
# Final port should be at (-1000, 2000) rot pi
|
|
# Start: (0,0) rot 0. Wire direction is rot + pi = pi (West, -x).
|
|
# Tool planU returns (length, jog) = (1000, -2000) relative to (0,0) rot 0.
|
|
# Rotation of pi transforms (1000, -2000) to (-1000, 2000).
|
|
# Final rotation: 0 + pi = pi.
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-1000, 2000))
|
|
assert p.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, pi)
|
|
|
|
def test_pather_trace_into() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
|
|
# 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))
|
|
|
|
# 4. U-bend (0 deg angle)
|
|
p.pattern.ports['G'] = Port((0, 0), rotation=0)
|
|
p.pattern.ports['H'] = Port((-10000, 2000), rotation=0)
|
|
p.at('G').trace_into('H', plug_destination=False)
|
|
assert 'H' in p.pattern.ports
|
|
assert 'G' in p.pattern.ports
|
|
# A U-bend with length=-travel=10000 and jog=-2000 from (0,0) rot 0
|
|
# ends up at (-10000, 2000) rot pi.
|
|
assert numpy.allclose(p.pattern.ports['G'].offset, (-10000, 2000))
|
|
assert p.pattern.ports['G'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['G'].rotation, pi)
|
|
|
|
# 5. Vertical straight connector
|
|
p.pattern.ports['I'] = Port((0, 0), rotation=pi / 2)
|
|
p.pattern.ports['J'] = Port((0, -10000), rotation=3 * pi / 2)
|
|
p.at('I').trace_into('J', plug_destination=False)
|
|
assert 'J' in p.pattern.ports
|
|
assert 'I' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['I'].offset, (0, -10000))
|
|
assert p.pattern.ports['I'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['I'].rotation, pi / 2)
|
|
|
|
|
|
def test_pather_trace_into_dead_updates_ports_without_geometry() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000, ptype='wire')
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = Port((-10000, 0), rotation=pi, ptype='wire')
|
|
p.set_dead()
|
|
|
|
p.trace_into('A', 'B', plug_destination=False)
|
|
|
|
assert set(p.pattern.ports) == {'A', 'B'}
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0))
|
|
assert p.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert len(p.paths['A']) == 0
|
|
assert not p.pattern.has_shapes()
|
|
assert not p.pattern.has_refs()
|
|
|
|
|
|
def test_pather_dead_fallback_preserves_out_ptype() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000, ptype='wire')
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.set_dead()
|
|
|
|
p.straight('A', -1000, out_ptype='other')
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (1000, 0))
|
|
assert p.pattern.ports['A'].ptype == 'other'
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_dead_place_overwrites_colliding_ports_last_wins() -> None:
|
|
lib = Library()
|
|
p = Pather(lib, pattern=Pattern(ports={
|
|
'A': Port((5, 5), rotation=0),
|
|
'keep': Port((9, 9), rotation=0),
|
|
}))
|
|
p.set_dead()
|
|
|
|
other = Pattern()
|
|
other.ports['X'] = Port((1, 0), rotation=0)
|
|
other.ports['Y'] = Port((2, 0), rotation=pi / 2)
|
|
|
|
p.place(other, port_map={'X': 'A', 'Y': 'A'})
|
|
|
|
assert set(p.pattern.ports) == {'A', 'keep'}
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (2, 0))
|
|
assert p.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, pi / 2)
|
|
|
|
|
|
def test_pather_dead_plug_overwrites_colliding_outputs_last_wins() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000, ptype='wire')
|
|
p = Pather(lib, tools=tool, pattern=Pattern(ports={
|
|
'A': Port((0, 0), rotation=0, ptype='wire'),
|
|
'B': Port((99, 99), rotation=0, ptype='wire'),
|
|
}))
|
|
p.set_dead()
|
|
|
|
other = Pattern()
|
|
other.ports['in'] = Port((0, 0), rotation=pi, ptype='wire')
|
|
other.ports['X'] = Port((10, 0), rotation=0, ptype='wire')
|
|
other.ports['Y'] = Port((20, 0), rotation=0, ptype='wire')
|
|
|
|
p.plug(other, map_in={'A': 'in'}, map_out={'X': 'B', 'Y': 'B'})
|
|
|
|
assert 'A' not in p.pattern.ports
|
|
assert 'B' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['B'].offset, (20, 0))
|
|
assert p.pattern.ports['B'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['B'].rotation, 0)
|
|
|
|
|
|
def test_pather_dead_rename_overwrites_colliding_ports_last_wins() -> None:
|
|
p = Pather(Library(), pattern=Pattern(ports={
|
|
'A': Port((0, 0), rotation=0),
|
|
'B': Port((1, 0), rotation=0),
|
|
'C': Port((2, 0), rotation=0),
|
|
}))
|
|
p.set_dead()
|
|
|
|
p.rename_ports({'A': 'C', 'B': 'C'})
|
|
|
|
assert set(p.pattern.ports) == {'C'}
|
|
assert numpy.allclose(p.pattern.ports['C'].offset, (1, 0))
|
|
|
|
|
|
def test_pather_jog_failed_fallback_is_atomic() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=2, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='shorter than required bend'):
|
|
p.jog('A', 1.5, length=1.5)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert p.pattern.ports['A'].rotation == 0
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=2, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.jog('A', 1.5, length=5)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-5, -1.5))
|
|
assert p.pattern.ports['A'].rotation == 0
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_jog_length_solved_from_single_position_bound() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.jog('A', 2, x=-6)
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-6, -2))
|
|
assert p.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
|
|
q = Pather(Library(), tools=tool)
|
|
q.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
q.jog('A', 2, p=-6)
|
|
assert numpy.allclose(q.pattern.ports['A'].offset, (-6, -2))
|
|
|
|
|
|
def test_pather_jog_requires_length_or_one_position_bound() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='requires either length'):
|
|
p.jog('A', 2)
|
|
|
|
with pytest.raises(BuildError, match='exactly one positional bound'):
|
|
p.jog('A', 2, x=-6, p=-6)
|
|
|
|
|
|
def test_pather_trace_to_rejects_conflicting_position_bounds() -> None:
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
|
|
for kwargs in ({'x': -5, 'y': 2}, {'y': 2, 'x': -5}, {'p': -7, 'x': -5}):
|
|
p = Pather(Library(), tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
with pytest.raises(BuildError, match='exactly one positional bound'):
|
|
p.trace_to('A', None, **kwargs)
|
|
|
|
p = Pather(Library(), tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
with pytest.raises(BuildError, match='length cannot be combined'):
|
|
p.trace_to('A', None, x=-5, length=3)
|
|
|
|
|
|
def test_pather_trace_rejects_length_with_bundle_bound() -> None:
|
|
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='length cannot be combined'):
|
|
p.trace('A', None, length=5, xmin=-100)
|
|
|
|
|
|
@pytest.mark.parametrize('kwargs', ({'xmin': -10, 'xmax': -20}, {'xmax': -20, 'xmin': -10}))
|
|
def test_pather_trace_rejects_multiple_bundle_bounds(kwargs: dict[str, int]) -> None:
|
|
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = Port((0, 5), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='exactly one bundle bound'):
|
|
p.trace(['A', 'B'], None, **kwargs)
|
|
|
|
|
|
def test_pather_jog_rejects_length_with_position_bound() -> None:
|
|
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='length cannot be combined'):
|
|
p.jog('A', 2, length=5, x=-999)
|
|
|
|
|
|
@pytest.mark.parametrize('kwargs', ({'x': -999}, {'xmin': -10}))
|
|
def test_pather_uturn_rejects_routing_bounds(kwargs: dict[str, int]) -> None:
|
|
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='Unsupported routing bounds for uturn'):
|
|
p.uturn('A', 4, **kwargs)
|
|
|
|
|
|
def test_pather_uturn_none_length_defaults_to_zero() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.uturn('A', 4)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, -4))
|
|
assert p.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, pi)
|
|
|
|
|
|
def test_pather_trace_into_failure_rolls_back_ports_and_paths() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = Port((-5, 5), rotation=pi / 2, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='does not match path ptype'):
|
|
p.trace_into('A', 'B', plug_destination=False, out_ptype='other')
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert numpy.allclose(p.pattern.ports['B'].offset, (-5, 5))
|
|
assert numpy.isclose(p.pattern.ports['B'].rotation, pi / 2)
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_trace_into_rename_failure_rolls_back_ports_and_paths() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = Port((-10, 0), rotation=pi, ptype='wire')
|
|
p.pattern.ports['other'] = Port((3, 4), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(PortError, match='overwritten'):
|
|
p.trace_into('A', 'B', plug_destination=False, thru='other')
|
|
|
|
assert set(p.pattern.ports) == {'A', 'B', 'other'}
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.allclose(p.pattern.ports['B'].offset, (-10, 0))
|
|
assert numpy.allclose(p.pattern.ports['other'].offset, (3, 4))
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('dst', 'kwargs', 'match'),
|
|
(
|
|
(Port((-5, 5), rotation=pi / 2, ptype='wire'), {'x': -99}, r'trace_to\(\) arguments: x'),
|
|
(Port((-10, 2), rotation=pi, ptype='wire'), {'length': 1}, r'jog\(\) arguments: length'),
|
|
(Port((-10, 2), rotation=0, ptype='wire'), {'length': 1}, r'uturn\(\) arguments: length'),
|
|
),
|
|
)
|
|
def test_pather_trace_into_rejects_reserved_route_kwargs(
|
|
dst: Port,
|
|
kwargs: dict[str, Any],
|
|
match: str,
|
|
) -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = dst
|
|
|
|
with pytest.raises(BuildError, match=match):
|
|
p.trace_into('A', 'B', plug_destination=False, **kwargs)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert numpy.allclose(p.pattern.ports['B'].offset, dst.offset)
|
|
assert dst.rotation is not None
|
|
assert p.pattern.ports['B'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['B'].rotation, dst.rotation)
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_two_l_fallback_validation_rejects_out_ptype_sensitive_jog() -> None:
|
|
class OutPtypeSensitiveTool(Tool):
|
|
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs):
|
|
radius = 1 if out_ptype is None else 2
|
|
if ccw is None:
|
|
rotation = pi
|
|
jog = 0
|
|
elif bool(ccw):
|
|
rotation = -pi / 2
|
|
jog = radius
|
|
else:
|
|
rotation = pi / 2
|
|
jog = -radius
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return Port((length, jog), rotation=rotation, ptype=ptype), {'ccw': ccw, 'length': length}
|
|
|
|
p = Pather(Library(), tools=OutPtypeSensitiveTool())
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='fallback via two planL'):
|
|
p.jog('A', 5, length=10, out_ptype='wide')
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_two_l_fallback_validation_rejects_out_ptype_sensitive_uturn() -> None:
|
|
class OutPtypeSensitiveTool(Tool):
|
|
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs):
|
|
radius = 1 if out_ptype is None else 2
|
|
if ccw is None:
|
|
rotation = pi
|
|
jog = 0
|
|
elif bool(ccw):
|
|
rotation = -pi / 2
|
|
jog = radius
|
|
else:
|
|
rotation = pi / 2
|
|
jog = -radius
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return Port((length, jog), rotation=rotation, ptype=ptype), {'ccw': ccw, 'length': length}
|
|
|
|
p = Pather(Library(), tools=OutPtypeSensitiveTool())
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='fallback via two planL'):
|
|
p.uturn('A', 5, length=10, out_ptype='wide')
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_tool_planL_fallback_accepts_custom_port_names() -> None:
|
|
class DummyTool(Tool):
|
|
def traceL(self, ccw, length, *, in_ptype=None, out_ptype=None, port_names=('A', 'B'), **kwargs) -> Library:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
pat.ports[port_names[0]] = Port((0, 0), 0, ptype='wire')
|
|
pat.ports[port_names[1]] = Port((length, 0), pi, ptype='wire')
|
|
lib['top'] = pat
|
|
return lib
|
|
|
|
out_port, _ = DummyTool().planL(None, 5, port_names=('X', 'Y'))
|
|
assert numpy.allclose(out_port.offset, (5, 0))
|
|
assert numpy.isclose(out_port.rotation, pi)
|
|
|
|
|
|
def test_tool_planS_fallback_accepts_custom_port_names() -> None:
|
|
class DummyTool(Tool):
|
|
def traceS(self, length, jog, *, in_ptype=None, out_ptype=None, port_names=('A', 'B'), **kwargs) -> Library:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
pat.ports[port_names[0]] = Port((0, 0), 0, ptype='wire')
|
|
pat.ports[port_names[1]] = Port((length, jog), pi, ptype='wire')
|
|
lib['top'] = pat
|
|
return lib
|
|
|
|
out_port, _ = DummyTool().planS(5, 2, port_names=('X', 'Y'))
|
|
assert numpy.allclose(out_port.offset, (5, 2))
|
|
assert numpy.isclose(out_port.rotation, pi)
|
|
|
|
|
|
def test_pather_uturn_failed_fallback_is_atomic() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=2, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='shorter than required bend'):
|
|
p.uturn('A', 1.5, length=0)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert p.pattern.ports['A'].rotation == 0
|
|
assert len(p.paths['A']) == 0
|
|
|
|
|
|
def test_pather_render_auto_renames_single_use_tool_children() -> None:
|
|
class FullTreeTool(Tool):
|
|
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return Port((length, 0), rotation=pi, ptype=ptype), {'length': length}
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((1, 0), pi, ptype='wire'),
|
|
})
|
|
child = Pattern(annotations={'batch': [len(batch)]})
|
|
top.ref('_seg')
|
|
tree['_top'] = top
|
|
tree['_seg'] = child
|
|
return tree
|
|
|
|
lib = Library()
|
|
p = Pather(lib, tools=FullTreeTool(), auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.straight('A', 10)
|
|
p.render()
|
|
p.straight('A', 10)
|
|
p.render()
|
|
|
|
assert len(lib) == 2
|
|
assert set(lib.keys()) == set(p.pattern.refs.keys())
|
|
assert len(set(p.pattern.refs.keys())) == 2
|
|
assert all(name.startswith('_seg') for name in lib)
|
|
assert p.pattern.referenced_patterns() <= set(lib.keys())
|
|
|
|
|
|
def test_tool_render_fallback_preserves_segment_subtrees() -> None:
|
|
class TraceTreeTool(Tool):
|
|
def traceL(self, ccw, length, *, in_ptype=None, out_ptype=None, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((length, 0), pi, ptype='wire'),
|
|
})
|
|
child = Pattern(annotations={'length': [length]})
|
|
top.ref('_seg')
|
|
tree['_top'] = top
|
|
tree['_seg'] = child
|
|
return tree
|
|
|
|
lib = Library()
|
|
p = Pather(lib, tools=TraceTreeTool(), auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.straight('A', 10)
|
|
p.render()
|
|
|
|
assert '_seg' in lib
|
|
assert '_seg' in p.pattern.refs
|
|
assert p.pattern.referenced_patterns() <= set(lib.keys())
|
|
|
|
|
|
def test_pather_render_rejects_missing_single_use_tool_refs() -> None:
|
|
class MissingSingleUseTool(Tool):
|
|
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return Port((length, 0), rotation=pi, ptype=ptype), {'length': length}
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((1, 0), pi, ptype='wire'),
|
|
})
|
|
top.ref('_seg')
|
|
tree['_top'] = top
|
|
return tree
|
|
|
|
lib = Library()
|
|
lib['_seg'] = Pattern(annotations={'stale': [1]})
|
|
p = Pather(lib, tools=MissingSingleUseTool(), auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.straight('A', 10)
|
|
|
|
with pytest.raises(BuildError, match='missing single-use refs'):
|
|
p.render()
|
|
|
|
assert list(lib.keys()) == ['_seg']
|
|
assert not p.pattern.refs
|
|
|
|
|
|
def test_pather_render_allows_missing_non_single_use_tool_refs() -> None:
|
|
class SharedRefTool(Tool):
|
|
def planL(self, ccw, length, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202
|
|
ptype = out_ptype or in_ptype or 'wire'
|
|
return Port((length, 0), rotation=pi, ptype=ptype), {'length': length}
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202
|
|
tree = Library()
|
|
top = Pattern(ports={
|
|
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
|
port_names[1]: Port((1, 0), pi, ptype='wire'),
|
|
})
|
|
top.ref('shared')
|
|
tree['_top'] = top
|
|
return tree
|
|
|
|
lib = Library()
|
|
lib['shared'] = Pattern(annotations={'shared': [1]})
|
|
p = Pather(lib, tools=SharedRefTool(), auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.straight('A', 10)
|
|
p.render()
|
|
|
|
assert 'shared' in p.pattern.refs
|
|
assert p.pattern.referenced_patterns() <= set(lib.keys())
|
|
|
|
|
|
def test_renderpather_rename_to_none_keeps_pending_geometry_without_port() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
rp = Pather(lib, tools=tool, auto_render=False)
|
|
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
rp.at('A').straight(5000)
|
|
rp.rename_ports({'A': None})
|
|
|
|
assert 'A' not in rp.pattern.ports
|
|
assert len(rp.paths['A']) == 1
|
|
|
|
rp.render()
|
|
assert rp.pattern.has_shapes()
|
|
assert 'A' not in rp.pattern.ports
|
|
|
|
|
|
def test_pather_place_treeview_resolves_once() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool)
|
|
|
|
tree = {'child': Pattern(ports={'B': Port((1, 0), pi)})}
|
|
|
|
p.place(tree)
|
|
|
|
assert len(lib) == 1
|
|
assert 'child' in lib
|
|
assert 'child' in p.pattern.refs
|
|
assert 'B' in p.pattern.ports
|
|
|
|
|
|
def test_pather_plug_treeview_resolves_once() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
tree = {'child': Pattern(ports={'B': Port((0, 0), pi)})}
|
|
|
|
p.plug(tree, {'A': 'B'})
|
|
|
|
assert len(lib) == 1
|
|
assert 'child' in lib
|
|
assert 'child' in p.pattern.refs
|
|
assert 'A' not in p.pattern.ports
|
|
|
|
|
|
def test_pather_failed_plug_does_not_add_break_marker() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.annotations = {'k': [1]}
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
p.at('A').trace(None, 5000)
|
|
assert [step.opcode for step in p.paths['A']] == ['L']
|
|
|
|
other = Pattern(
|
|
annotations={'k': [2]},
|
|
ports={'X': Port((0, 0), pi), 'Y': Port((5, 0), 0)},
|
|
)
|
|
|
|
with pytest.raises(PatternError, match='Annotation keys overlap'):
|
|
p.plug(other, {'A': 'X'}, map_out={'Y': 'Z'}, append=True)
|
|
|
|
assert [step.opcode for step in p.paths['A']] == ['L']
|
|
assert set(p.pattern.ports) == {'A'}
|
|
|
|
|
|
def test_pather_place_reused_deleted_name_keeps_break_marker() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
p.at('A').straight(5000)
|
|
p.rename_ports({'A': None})
|
|
|
|
other = Pattern(ports={'X': Port((-5000, 0), rotation=0)})
|
|
p.place(other, port_map={'X': 'A'}, append=True)
|
|
p.at('A').straight(2000)
|
|
|
|
assert [step.opcode for step in p.paths['A']] == ['L', 'P', 'L']
|
|
|
|
p.render()
|
|
assert p.pattern.has_shapes()
|
|
assert 'A' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-7000, 0))
|
|
|
|
|
|
def test_pather_plug_reused_deleted_name_keeps_break_marker() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
p.pattern.ports['B'] = Port((0, 0), rotation=0)
|
|
|
|
p.at('A').straight(5000)
|
|
p.rename_ports({'A': None})
|
|
|
|
other = Pattern(
|
|
ports={
|
|
'X': Port((0, 0), rotation=pi),
|
|
'Y': Port((-5000, 0), rotation=0),
|
|
},
|
|
)
|
|
p.plug(other, {'B': 'X'}, map_out={'Y': 'A'}, append=True)
|
|
p.at('A').straight(2000)
|
|
|
|
assert [step.opcode for step in p.paths['A']] == ['L', 'P', 'L']
|
|
|
|
p.render()
|
|
assert p.pattern.has_shapes()
|
|
assert 'A' in p.pattern.ports
|
|
assert 'B' not in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-7000, 0))
|
|
|
|
|
|
def test_pather_failed_plugged_does_not_add_break_marker() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1000)
|
|
p = Pather(lib, tools=tool, auto_render=False)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
|
|
p.at('A').straight(5000)
|
|
assert [step.opcode for step in p.paths['A']] == ['L']
|
|
|
|
with pytest.raises(PortError, match='Connection destination ports were not found'):
|
|
p.plugged({'A': 'missing'})
|
|
|
|
assert [step.opcode for step in p.paths['A']] == ['L']
|
|
assert set(p.paths) == {'A'}
|