[Pather/Planner] Improve error reporting
This commit is contained in:
parent
09b131026f
commit
3a3cd854a5
9 changed files with 777 additions and 58 deletions
|
|
@ -14,11 +14,17 @@ from ..ports import Port
|
|||
def test_builder_public_imports() -> None:
|
||||
from masque import PortPather as TopPortPather
|
||||
from masque import RenderStep as TopRenderStep
|
||||
from masque import RouteError as TopRouteError
|
||||
from masque import RouteFailureDetails as TopRouteFailureDetails
|
||||
from masque.builder import PortPather as BuilderPortPather
|
||||
from masque.builder import RenderStep as BuilderRenderStep
|
||||
from masque.builder import RouteError as BuilderRouteError
|
||||
from masque.builder import RouteFailureDetails as BuilderRouteFailureDetails
|
||||
|
||||
assert TopPortPather is BuilderPortPather
|
||||
assert TopRenderStep is BuilderRenderStep
|
||||
assert TopRouteError is BuilderRouteError
|
||||
assert TopRouteFailureDetails is BuilderRouteFailureDetails
|
||||
|
||||
|
||||
def test_builder_init() -> None:
|
||||
|
|
@ -171,3 +177,41 @@ def test_ell_handles_array_spacing_when_ccw_none() -> None:
|
|||
|
||||
with pytest.raises(BuildError, match='Spacing must be 0 or None'):
|
||||
ell(ports, None, 'min_extension', 5, spacing=numpy.array([1, 0]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bound_type', ['emin', 'emax', 'min_past_furthest'])
|
||||
@pytest.mark.parametrize(
|
||||
('rotation', 'expected'),
|
||||
[
|
||||
(0, 5),
|
||||
(pi / 2, 7),
|
||||
(pi / 6, 5),
|
||||
(pi / 3, 7),
|
||||
(pi / 4, 5),
|
||||
],
|
||||
)
|
||||
def test_ell_extension_vector_selects_dominant_route_axis(
|
||||
bound_type: str,
|
||||
rotation: float,
|
||||
expected: float,
|
||||
) -> None:
|
||||
result = ell({'A': Port((0, 0), rotation)}, None, bound_type, (5, 7), spacing=0)
|
||||
|
||||
assert_allclose(result['A'], expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bound', [(-1, 2), (1, -2), (numpy.nan, 2), (1, numpy.inf), (1, 2, 3)])
|
||||
def test_ell_rejects_invalid_extension_vector(bound: tuple[float, ...]) -> None:
|
||||
with pytest.raises(BuildError, match='bound|negative'):
|
||||
ell({'A': Port((0, 0), 0)}, None, 'emin', bound, spacing=0)
|
||||
|
||||
|
||||
def test_ell_position_vector_still_projects_onto_route_direction() -> None:
|
||||
result = ell({'A': Port((0, 0), pi)}, None, 'pmax', (5, 7), spacing=0)
|
||||
|
||||
assert_allclose(result['A'], 5)
|
||||
|
||||
|
||||
def test_ell_rejects_invalid_bound_type() -> None:
|
||||
with pytest.raises(BuildError, match='Invalid bound type'):
|
||||
ell({'A': Port((0, 0), 0)}, None, 'nearest', 5, spacing=0)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import pytest
|
|||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import Pather, Library, Port
|
||||
from masque import Pather, Library, Port, RouteError
|
||||
from masque.builder.planner import RoutePortContext, RoutingPlanner
|
||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool
|
||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool, UOffer
|
||||
from masque.error import BuildError
|
||||
from masque.library import ILibrary
|
||||
|
||||
|
|
@ -114,15 +114,306 @@ class CountingPathTool(PathTool):
|
|||
return super().render(batch, port_names=port_names, **kwargs)
|
||||
|
||||
|
||||
class RequestCountingTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.offer_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[Any, ...]:
|
||||
self.offer_calls += 1
|
||||
return super().primitive_offers(
|
||||
kind,
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class PreferredMinimumTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.commit_calls = 0
|
||||
self.diagnostic_context_kwargs: list[Any] = []
|
||||
|
||||
def _commit(self, parameter: float) -> dict[str, float]:
|
||||
self.commit_calls += 1
|
||||
return {'parameter': parameter}
|
||||
|
||||
def primitive_offers(
|
||||
self,
|
||||
kind: Literal['straight', 'bend', 's', 'u'],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[Any, ...]:
|
||||
_ = out_ptype
|
||||
self.diagnostic_context_kwargs.append(kwargs.get('diagnostic_context'))
|
||||
if kind == 'bend':
|
||||
ccw = bool(kwargs['ccw'])
|
||||
rotation = -pi / 2 if ccw else pi / 2
|
||||
jog = 1 if ccw else -1
|
||||
return (
|
||||
BendOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype='wide',
|
||||
priority_bias=100,
|
||||
ccw=ccw,
|
||||
length_domain=(2, 2),
|
||||
endpoint_planner=lambda _length: Port((2, jog), rotation, ptype='wide'),
|
||||
commit_planner=self._commit,
|
||||
),
|
||||
BendOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype='wide',
|
||||
ccw=ccw,
|
||||
length_domain=(5, 5),
|
||||
endpoint_planner=lambda _length: Port((5, jog), rotation, ptype='wide'),
|
||||
commit_planner=self._commit,
|
||||
),
|
||||
)
|
||||
if kind == 'u':
|
||||
return (UOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype='wide',
|
||||
jog_domain=(4, 4),
|
||||
endpoint_planner=lambda _jog: Port((5, 4), 0, ptype='wide'),
|
||||
commit_planner=self._commit,
|
||||
),)
|
||||
return ()
|
||||
|
||||
|
||||
def test_route_error_reports_preferred_minimum_and_request_details() -> None:
|
||||
tool = PreferredMinimumTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.ccw('A', 1, out_ptype='wide', diagnostic_context='tool-value')
|
||||
|
||||
details = exc_info.value.details
|
||||
assert isinstance(exc_info.value, BuildError)
|
||||
assert details.operation == 'trace_to'
|
||||
assert details.portspec == 'A'
|
||||
assert details.in_ptype == 'wire'
|
||||
assert details.out_ptype == 'wide'
|
||||
assert details.request == {
|
||||
'ccw': True,
|
||||
'length': 1,
|
||||
'out_ptype': 'wide',
|
||||
'diagnostic_context': 'tool-value',
|
||||
}
|
||||
assert details.resolved_length == 1
|
||||
assert details.resolved_jog is None
|
||||
# The length-2 route exists, but normal cost ranking prefers length 5.
|
||||
assert details.minimum_length == 5
|
||||
assert details.minimum_attempted
|
||||
assert not details.minimum_exhausted
|
||||
assert details.minimum_cause is None
|
||||
assert 'preferred_minimum_length: 5' in str(exc_info.value)
|
||||
assert tool.commit_calls == 0
|
||||
assert tool.diagnostic_context_kwargs
|
||||
assert set(tool.diagnostic_context_kwargs) == {'tool-value'}
|
||||
assert not p._paths
|
||||
with pytest.raises(TypeError):
|
||||
details.request['new'] = 'value' # type: ignore[index]
|
||||
|
||||
|
||||
def test_route_error_reports_uturn_preferred_minimum() -> None:
|
||||
tool = PreferredMinimumTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.uturn('A', 4, length=1, out_ptype='wide')
|
||||
|
||||
details = exc_info.value.details
|
||||
assert details.operation == 'uturn'
|
||||
assert details.resolved_length == 1
|
||||
assert details.resolved_jog == 4
|
||||
assert details.minimum_length == 5
|
||||
assert tool.commit_calls == 0
|
||||
|
||||
|
||||
def test_route_error_reports_no_route_at_any_length() -> None:
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=PathTool(layer='M1', width=2, ptype='wire'),
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.ccw('A', 10, out_ptype='optical')
|
||||
|
||||
details = exc_info.value.details
|
||||
assert details.minimum_attempted
|
||||
assert details.minimum_length is None
|
||||
assert details.minimum_exhausted
|
||||
assert details.minimum_cause is not None
|
||||
assert 'no legal route exists at any length' in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('length', [-1, numpy.nan, numpy.inf])
|
||||
def test_invalid_route_length_fails_before_offer_query_or_dead_fallback(length: float) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
).set_dead()
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.ccw('A', length)
|
||||
|
||||
details = exc_info.value.details
|
||||
assert not details.minimum_attempted
|
||||
assert details.minimum_length is None
|
||||
assert tool.offer_calls == 0
|
||||
assert numpy.allclose(p.ports['A'].offset, (0, 0))
|
||||
|
||||
|
||||
def test_negative_position_length_reports_bound_without_offer_query() -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.ccw('A', x=1)
|
||||
|
||||
details = exc_info.value.details
|
||||
assert details.request == {'ccw': True, 'x': 1}
|
||||
assert details.resolved_length == -1
|
||||
assert not details.minimum_attempted
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
def test_negative_bundle_length_reports_failing_port_and_bound() -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={
|
||||
'A': Port((0, 0), rotation=0, ptype='wire'),
|
||||
'B': Port((0, 4), rotation=0, ptype='wire'),
|
||||
},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.trace(['A', 'B'], True, emax=0, spacing=2)
|
||||
|
||||
details = exc_info.value.details
|
||||
assert details.portspec == 'A'
|
||||
assert details.request == {'ccw': True, 'emax': 0, 'spacing': 2}
|
||||
assert details.resolved_length == -2
|
||||
assert not details.minimum_attempted
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
def test_negative_each_length_reports_failing_port_without_offer_query() -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={
|
||||
'A': Port((0, 0), rotation=0, ptype='wire'),
|
||||
'B': Port((0, 4), rotation=0, ptype='wire'),
|
||||
},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.trace(['A', 'B'], None, each=-2)
|
||||
|
||||
details = exc_info.value.details
|
||||
assert details.portspec == 'A'
|
||||
assert details.request == {'ccw': None, 'each': -2}
|
||||
assert details.resolved_length == -2
|
||||
assert not details.minimum_attempted
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'operation',
|
||||
[
|
||||
lambda p: p.trace([], None, length=1),
|
||||
lambda p: p.trace_to([], None, length=1),
|
||||
lambda p: p.jog([], 2, length=3),
|
||||
lambda p: p.uturn([], 2, length=3),
|
||||
],
|
||||
ids=['trace', 'trace_to', 'jog', 'uturn'],
|
||||
)
|
||||
def test_pather_rejects_empty_route_selection_before_planning(operation: Any) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
|
||||
with pytest.raises(BuildError, match='at least one port'):
|
||||
operation(p)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
assert not p.ports
|
||||
assert not p._paths
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'operation',
|
||||
[
|
||||
lambda p: p.trace(['A', 'A'], None, each=1),
|
||||
lambda p: p.trace_to(['A', 'A'], None, xmin=-10),
|
||||
lambda p: p.jog(['A', 'A'], 2, length=3, spacing=1),
|
||||
lambda p: p.uturn(['A', 'A'], 2, length=3, spacing=1),
|
||||
lambda p: p.at(['A', 'A']).straight(1),
|
||||
],
|
||||
ids=['trace', 'trace_to', 'jog', 'uturn', 'port_pather'],
|
||||
)
|
||||
def test_pather_rejects_duplicate_route_selection_before_planning(operation: Any) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(BuildError, match=r"duplicates: \['A'\]"):
|
||||
operation(p)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
assert numpy.allclose(p.ports['A'].offset, (0, 0))
|
||||
assert not p._paths
|
||||
|
||||
|
||||
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, render='immediate')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='S-bend'):
|
||||
with pytest.raises(RouteError, match='S-bend') as exc_info:
|
||||
p.jog('A', 1.5, length=1.5)
|
||||
|
||||
assert exc_info.value.details.minimum_length == 2
|
||||
assert exc_info.value.details.resolved_jog == 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
|
||||
|
|
@ -593,9 +884,12 @@ def test_pather_uturn_failed_two_bend_route_is_atomic() -> None:
|
|||
p = Pather(lib, tools=tool)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='U-turn'):
|
||||
with pytest.raises(RouteError, match='U-turn') as exc_info:
|
||||
p.uturn('A', 1.5, length=0)
|
||||
|
||||
assert exc_info.value.details.minimum_attempted
|
||||
assert exc_info.value.details.minimum_length is None
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p._paths['A']) == 0
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import numpy
|
|||
from numpy import pi
|
||||
from numpy.testing import assert_allclose, assert_equal
|
||||
|
||||
from masque import Pather, Library, Pattern, Port
|
||||
from masque import Pather, Library, Pattern, Port, RouteError
|
||||
from masque.builder import PathTool, PrimitiveOffer, StraightOffer
|
||||
from masque.builder.planner import RoutingPlanner
|
||||
from masque.error import BuildError, PortError
|
||||
|
|
@ -136,12 +136,13 @@ def test_pather_dead_ports() -> None:
|
|||
p = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool)
|
||||
p.set_dead()
|
||||
|
||||
p.straight("in", -10)
|
||||
with pytest.raises(RouteError, match='finite and nonnegative'):
|
||||
p.straight("in", -10)
|
||||
|
||||
assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10)
|
||||
assert_allclose(p.ports["in"].offset, [0, 0], atol=1e-10)
|
||||
|
||||
p.straight("in", 20)
|
||||
assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10)
|
||||
assert_allclose(p.ports["in"].offset, [-20, 0], atol=1e-10)
|
||||
|
||||
assert not p.pattern.has_shapes()
|
||||
|
||||
|
|
@ -394,9 +395,9 @@ def test_pather_dead_fallback_preserves_out_ptype() -> None:
|
|||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.set_dead()
|
||||
|
||||
p.straight('A', -1000, out_ptype='other')
|
||||
p.straight('A', 1000, out_ptype='other')
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (1000, 0))
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-1000, 0))
|
||||
assert p.pattern.ports['A'].ptype == 'other'
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import numpy
|
|||
from numpy import pi
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from ..builder import Pather
|
||||
from ..builder import Pather, RouteError
|
||||
from ..builder.tools import PathTool, RenderStep, StraightOffer, Tool
|
||||
from ..error import BuildError
|
||||
from ..library import Library
|
||||
|
|
@ -122,9 +122,10 @@ def test_deferred_render_dead_ports() -> None:
|
|||
rp = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool, render='deferred')
|
||||
rp.set_dead()
|
||||
|
||||
rp.straight("in", -10)
|
||||
with pytest.raises(RouteError, match='finite and nonnegative'):
|
||||
rp.straight("in", -10)
|
||||
|
||||
assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10)
|
||||
assert_allclose(rp.ports["in"].offset, [0, 0], atol=1e-10)
|
||||
|
||||
assert len(rp._paths["in"]) == 0
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue