[builder] major Pather/Planner/Tool rework
This commit is contained in:
parent
0f39ce8816
commit
22e645e527
28 changed files with 6036 additions and 2467 deletions
|
|
@ -1,26 +1,131 @@
|
|||
from typing import Any
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, Never
|
||||
|
||||
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
|
||||
from masque import Pather, Library, Port
|
||||
from masque.builder.planner import RoutePortContext, RoutingPlanner
|
||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool
|
||||
from masque.error import BuildError
|
||||
from masque.library import ILibrary
|
||||
|
||||
|
||||
def test_pather_jog_failed_fallback_is_atomic() -> None:
|
||||
class PlanningOnlyTool(Tool):
|
||||
def primitive_offers(
|
||||
self,
|
||||
kind: Literal['straight', 'bend', 's', 'u'],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[Any, ...]:
|
||||
_ = kind, in_ptype, out_ptype, kwargs
|
||||
return ()
|
||||
|
||||
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
||||
tree, pat = Library.mktree('planning_only_tool')
|
||||
pat.add_port_pair(names=port_names, ptype=batch[0].start_port.ptype if batch else 'unk')
|
||||
return tree
|
||||
|
||||
|
||||
class FirstPortOnlyTraceTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.render_calls = 0
|
||||
|
||||
def primitive_offers(
|
||||
self,
|
||||
kind: Literal['straight', 'bend', 's', 'u'],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[StraightOffer | BendOffer, ...]:
|
||||
_ = out_ptype
|
||||
if in_ptype != 'wire':
|
||||
return ()
|
||||
|
||||
if kind == 'straight':
|
||||
def endpoint(length: float) -> Port:
|
||||
return Port((length, 0), rotation=pi, ptype='wire')
|
||||
|
||||
def commit(length: float) -> dict[str, float | str]:
|
||||
return {'kind': 'straight', 'length': length}
|
||||
|
||||
return (StraightOffer(
|
||||
in_ptype='wire',
|
||||
out_ptype='wire',
|
||||
endpoint_planner=endpoint,
|
||||
commit_planner=commit,
|
||||
),)
|
||||
|
||||
if kind == 'bend':
|
||||
ccw = bool(kwargs['ccw'])
|
||||
|
||||
def endpoint(length: float) -> Port:
|
||||
return Port(
|
||||
(length, 1 if ccw else -1),
|
||||
rotation=-pi / 2 if ccw else pi / 2,
|
||||
ptype='wire',
|
||||
)
|
||||
|
||||
def commit(length: float) -> dict[str, float | str]:
|
||||
return {'kind': 'bend', 'length': length}
|
||||
|
||||
return (BendOffer(
|
||||
in_ptype='wire',
|
||||
out_ptype='wire',
|
||||
ccw=ccw,
|
||||
length_domain=(1, numpy.inf),
|
||||
endpoint_planner=endpoint,
|
||||
commit_planner=commit,
|
||||
),)
|
||||
|
||||
return ()
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs: Any,
|
||||
) -> Library:
|
||||
_ = batch, port_names, kwargs
|
||||
self.render_calls += 1
|
||||
tree, pat = Library.mktree('trace')
|
||||
pat.add_port_pair(names=port_names, ptype='wire')
|
||||
return tree
|
||||
|
||||
|
||||
class CountingPathTool(PathTool):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.render_calls = 0
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs: Any,
|
||||
) -> ILibrary:
|
||||
self.render_calls += 1
|
||||
return super().render(batch, port_names=port_names, **kwargs)
|
||||
|
||||
|
||||
def test_pather_jog_failed_two_bend_route_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'):
|
||||
with pytest.raises(BuildError, match='S-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
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None:
|
||||
lib = Library()
|
||||
|
|
@ -32,7 +137,19 @@ def test_pather_jog_accepts_sub_width_offset_when_length_is_sufficient() -> None
|
|||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-5, -1.5))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p.paths['A']) == 0
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
def test_pather_auto_render_batches_multi_step_selected_route_once() -> None:
|
||||
lib = Library()
|
||||
tool = CountingPathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=True)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.jog('A', 4, length=10)
|
||||
|
||||
assert tool.render_calls == 1
|
||||
assert len(p._paths['A']) == 0
|
||||
assert p.pattern.has_shapes()
|
||||
|
||||
def test_pather_jog_length_solved_from_single_position_bound() -> None:
|
||||
lib = Library()
|
||||
|
|
@ -50,18 +167,59 @@ def test_pather_jog_length_solved_from_single_position_bound() -> None:
|
|||
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:
|
||||
|
||||
def test_pather_positional_bound_requires_port_rotation() -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'), auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=None, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='Ports must have rotation'):
|
||||
p.trace_to('A', None, x=-5)
|
||||
|
||||
|
||||
def test_pather_jog_omitted_length_uses_minimum_length_route() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(lib, tools=tool)
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='requires either length'):
|
||||
p.jog('A', 2)
|
||||
p.jog('A', 2)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-1, -2))
|
||||
assert p.pattern.ports['A'].rotation is not None
|
||||
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
||||
assert [step.opcode for step in p._paths['A']] == ['L', 'L', 'L']
|
||||
|
||||
with pytest.raises(BuildError, match='exactly one positional bound'):
|
||||
p.jog('A', 2, x=-6, p=-6)
|
||||
|
||||
def test_pather_trace_omitted_length_uses_minimum_offer() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.trace('A', None)
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert p.pattern.ports['A'].rotation is not None
|
||||
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
||||
|
||||
p.trace('A', True)
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-1, -1))
|
||||
assert p.pattern.ports['A'].rotation is not None
|
||||
assert numpy.isclose(p.pattern.ports['A'].rotation, pi / 2)
|
||||
|
||||
def test_pather_trace_to_without_bound_uses_single_port_trace_minimum() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=2, ptype='wire')
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
p.trace_to('A', False)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-1, 1))
|
||||
assert p.pattern.ports['A'].rotation is not None
|
||||
assert numpy.isclose(p.pattern.ports['A'].rotation, 3 * pi / 2)
|
||||
|
||||
def test_pather_trace_to_rejects_conflicting_position_bounds() -> None:
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
|
||||
|
|
@ -83,7 +241,7 @@ def test_pather_trace_rejects_length_with_bundle_bound() -> None:
|
|||
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}))
|
||||
@pytest.mark.parametrize('kwargs', [{'xmin': -10, 'xmax': -20}, {'xmax': -20, 'xmin': -10}]) # noqa: PT007
|
||||
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')
|
||||
|
|
@ -92,6 +250,116 @@ def test_pather_trace_rejects_multiple_bundle_bounds(kwargs: dict[str, int]) ->
|
|||
with pytest.raises(BuildError, match='exactly one bundle bound'):
|
||||
p.trace(['A', 'B'], None, **kwargs)
|
||||
|
||||
|
||||
def test_planner_constrained_bend_requires_jog() -> None:
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
context = RoutePortContext('A', Port((0, 0), rotation=0, ptype='wire'), tool)
|
||||
|
||||
with pytest.raises(BuildError, match='trace route requires a jog constraint'):
|
||||
RoutingPlanner().plan_leg('bend', context, length=5, constrain_jog=True)
|
||||
|
||||
|
||||
def test_pather_trace_each_plans_all_ports_before_mutation() -> None:
|
||||
tool = FirstPortOnlyTraceTool()
|
||||
p = Pather(Library(), tools=tool, auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((-2, 5), rotation=0, ptype='blocked')
|
||||
|
||||
with pytest.raises(BuildError, match='No legal primitive offer for trace'):
|
||||
p.trace(['A', 'B'], None, each=5)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert numpy.allclose(p.pattern.ports['B'].offset, (-2, 5))
|
||||
assert p.pattern.ports['A'].ptype == 'wire'
|
||||
assert p.pattern.ports['B'].ptype == 'blocked'
|
||||
assert len(p._paths['A']) == 0
|
||||
assert len(p._paths['B']) == 0
|
||||
|
||||
def test_pather_bundle_trace_plans_all_ports_before_mutation_or_render() -> None:
|
||||
tool = FirstPortOnlyTraceTool()
|
||||
p = Pather(Library(), tools=tool, auto_render=True)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((0, 4), rotation=0, ptype='blocked')
|
||||
|
||||
with pytest.raises(BuildError, match='No legal primitive offer for trace'):
|
||||
p.trace(['A', 'B'], True, xmin=-10, spacing=2)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert numpy.allclose(p.pattern.ports['B'].offset, (0, 4))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert p.pattern.ports['B'].rotation == 0
|
||||
assert len(p._paths['A']) == 0
|
||||
assert len(p._paths['B']) == 0
|
||||
assert tool.render_calls == 0
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
|
||||
def test_pather_route_commit_failure_is_atomic_for_multi_port_trace() -> None:
|
||||
class CommitFailureTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.committed: list[str | None] = []
|
||||
self.render_calls = 0
|
||||
|
||||
def primitive_offers(
|
||||
self,
|
||||
kind: Literal['straight', 'bend', 's', 'u'],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[StraightOffer, ...]:
|
||||
_ = out_ptype, kwargs
|
||||
if kind != 'straight':
|
||||
return ()
|
||||
|
||||
def endpoint(length: float) -> Port:
|
||||
return Port((length, 0), rotation=pi, ptype=in_ptype)
|
||||
|
||||
def commit(length: float) -> dict[str, float | str | None]:
|
||||
_ = length
|
||||
self.committed.append(in_ptype)
|
||||
if in_ptype == 'bad':
|
||||
raise BuildError('selected commit failed')
|
||||
return {'ptype': in_ptype, 'length': length}
|
||||
|
||||
return (StraightOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=in_ptype,
|
||||
endpoint_planner=endpoint,
|
||||
commit_planner=commit,
|
||||
),)
|
||||
|
||||
def render(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs: Any,
|
||||
) -> Library:
|
||||
_ = batch, port_names, kwargs
|
||||
self.render_calls += 1
|
||||
tree, pat = Library.mktree('commit_failure_tool')
|
||||
pat.add_port_pair(names=port_names)
|
||||
return tree
|
||||
|
||||
tool = CommitFailureTool()
|
||||
p = Pather(Library(), tools=tool, auto_render=True)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((0, 4), rotation=0, ptype='bad')
|
||||
|
||||
with pytest.raises(BuildError, match='selected commit failed'):
|
||||
p.trace(['A', 'B'], None, each=5)
|
||||
|
||||
assert tool.committed == ['wire', 'bad']
|
||||
assert tool.render_calls == 0
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert numpy.allclose(p.pattern.ports['B'].offset, (0, 4))
|
||||
assert p.pattern.ports['A'].ptype == 'wire'
|
||||
assert p.pattern.ports['B'].ptype == 'bad'
|
||||
assert len(p._paths['A']) == 0
|
||||
assert len(p._paths['B']) == 0
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
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')
|
||||
|
|
@ -99,7 +367,7 @@ def test_pather_jog_rejects_length_with_position_bound() -> None:
|
|||
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}))
|
||||
@pytest.mark.parametrize('kwargs', [{'x': -999}, {'xmin': -10}]) # noqa: PT007
|
||||
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')
|
||||
|
|
@ -107,7 +375,7 @@ def test_pather_uturn_rejects_routing_bounds(kwargs: dict[str, int]) -> None:
|
|||
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:
|
||||
def test_pather_uturn_omitted_length_uses_minimum_length_route() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(lib, tools=tool)
|
||||
|
|
@ -119,9 +387,82 @@ def test_pather_uturn_none_length_defaults_to_zero() -> None:
|
|||
assert p.pattern.ports['A'].rotation is not None
|
||||
assert numpy.isclose(p.pattern.ports['A'].rotation, pi)
|
||||
|
||||
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):
|
||||
def test_pather_uturn_explicit_zero_length_preserves_old_shape() -> 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, length=0)
|
||||
|
||||
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_uturn_does_not_use_direct_planl_fallback() -> None:
|
||||
class PlanLOnlyTool(PlanningOnlyTool):
|
||||
def primitive_offers(
|
||||
self,
|
||||
kind: Literal['straight', 'bend', 's', 'u'],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Never:
|
||||
del kind, in_ptype, out_ptype, kwargs
|
||||
raise NotImplementedError
|
||||
|
||||
def legacy_planL(
|
||||
self,
|
||||
ccw: object,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**_kwargs: Any,
|
||||
) -> tuple[Port, dict[str, object]]:
|
||||
del out_ptype
|
||||
if ccw is None:
|
||||
rotation = pi
|
||||
jog = 0
|
||||
elif bool(ccw):
|
||||
rotation = -pi / 2
|
||||
jog = 1
|
||||
else:
|
||||
rotation = pi / 2
|
||||
jog = -1
|
||||
return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='No legal primitive offer for omitted-length U-turn'):
|
||||
p.uturn('A', 5)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 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
|
||||
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
p.uturn('A', 5, length=10)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 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
|
||||
|
||||
def test_pather_su_topology_rejects_out_ptype_sensitive_planl_jog() -> None:
|
||||
class OutPtypeSensitiveTool(PlanningOnlyTool):
|
||||
def legacy_planL(
|
||||
self,
|
||||
ccw: object,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**_kwargs: Any,
|
||||
) -> tuple[Port, dict[str, object]]:
|
||||
radius = 1 if out_ptype is None else 2
|
||||
if ccw is None:
|
||||
rotation = pi
|
||||
|
|
@ -135,19 +476,94 @@ def test_pather_two_l_fallback_validation_rejects_out_ptype_sensitive_jog() -> N
|
|||
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 = Pather(Library(), tools=OutPtypeSensitiveTool(), auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='fallback via two planL'):
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
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
|
||||
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):
|
||||
def test_pather_two_l_planl_only_uturn_is_not_supported() -> None:
|
||||
class PlanLOnlyTool(PlanningOnlyTool):
|
||||
def legacy_planL(
|
||||
self,
|
||||
ccw: object,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**_kwargs: Any,
|
||||
) -> tuple[Port, dict[str, object]]:
|
||||
del out_ptype
|
||||
if ccw is None:
|
||||
rotation = pi
|
||||
jog = 0
|
||||
elif bool(ccw):
|
||||
rotation = -pi / 2
|
||||
jog = 1
|
||||
else:
|
||||
rotation = pi / 2
|
||||
jog = -1
|
||||
return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
p.uturn('A', 5, length=10)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 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
|
||||
|
||||
def test_pather_two_l_planl_only_jog_is_not_supported() -> None:
|
||||
class PlanLOnlyTool(PlanningOnlyTool):
|
||||
def legacy_planL(
|
||||
self,
|
||||
ccw: object,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**_kwargs: Any,
|
||||
) -> tuple[Port, dict[str, object]]:
|
||||
del out_ptype
|
||||
if ccw is None:
|
||||
rotation = pi
|
||||
jog = 0
|
||||
elif bool(ccw):
|
||||
rotation = -pi / 2
|
||||
jog = 1
|
||||
else:
|
||||
rotation = pi / 2
|
||||
jog = -1
|
||||
return Port((length, jog), rotation=rotation, ptype=in_ptype or 'wire'), {'ccw': ccw, 'length': length}
|
||||
|
||||
p = Pather(Library(), tools=PlanLOnlyTool(), auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
p.jog('A', 5, length=10, out_ptype='unk')
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert p.pattern.ports['A'].ptype == 'wire'
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
def test_pather_su_topology_rejects_out_ptype_sensitive_planl_uturn() -> None:
|
||||
class OutPtypeSensitiveTool(PlanningOnlyTool):
|
||||
def legacy_planL(
|
||||
self,
|
||||
ccw: object,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**_kwargs: Any,
|
||||
) -> tuple[Port, dict[str, object]]:
|
||||
radius = 1 if out_ptype is None else 2
|
||||
if ccw is None:
|
||||
rotation = pi
|
||||
|
|
@ -164,50 +580,22 @@ def test_pather_two_l_fallback_validation_rejects_out_ptype_sensitive_uturn() ->
|
|||
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'):
|
||||
with pytest.raises((BuildError, NotImplementedError)):
|
||||
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
|
||||
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:
|
||||
def test_pather_uturn_failed_two_bend_route_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'):
|
||||
with pytest.raises(BuildError, match='U-turn'):
|
||||
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
|
||||
assert len(p._paths['A']) == 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue