Compare commits
6 commits
d03fafcaf6
...
e7f847d4c7
| Author | SHA1 | Date | |
|---|---|---|---|
| e7f847d4c7 | |||
| 3beadd2bf0 | |||
| 1bcf5901d6 | |||
| 56e401196a | |||
| 83ec64158a | |||
| aa7007881f |
10 changed files with 228 additions and 14 deletions
|
|
@ -415,8 +415,17 @@ class Pather(PortList):
|
|||
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
|
||||
return self
|
||||
|
||||
self._traceL(portspec, ccw0, L1, **(kwargs | {'out_ptype': None}))
|
||||
self._traceL(portspec, not ccw0, L2, **(kwargs | {'plug_into': plug_into}))
|
||||
try:
|
||||
out_port0, data0 = tool.planL(ccw0, L1, in_ptype=in_ptype, **(kwargs | {'out_ptype': None}))
|
||||
out_port1, data1 = tool.planL(not ccw0, L2, in_ptype=out_port0.ptype, **kwargs)
|
||||
except (BuildError, NotImplementedError):
|
||||
if not self._dead:
|
||||
raise
|
||||
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
|
||||
return self
|
||||
|
||||
self._apply_step('L', portspec, out_port0, data0, tool)
|
||||
self._apply_step('L', portspec, out_port1, data1, tool, plug_into)
|
||||
return self
|
||||
if out_port is not None:
|
||||
self._apply_step('S', portspec, out_port, data, tool, plug_into)
|
||||
|
|
@ -436,14 +445,16 @@ class Pather(PortList):
|
|||
try:
|
||||
R = self._get_tool_R(tool, ccw, in_ptype, **kwargs)
|
||||
L1, L2 = length + R, abs(jog) - R
|
||||
self._traceL(portspec, ccw, L1, **(kwargs | {'out_ptype': None}))
|
||||
self._traceL(portspec, ccw, L2, **(kwargs | {'plug_into': plug_into}))
|
||||
out_port0, data0 = tool.planL(ccw, L1, in_ptype=in_ptype, **(kwargs | {'out_ptype': None}))
|
||||
out_port1, data1 = tool.planL(ccw, L2, in_ptype=out_port0.ptype, **kwargs)
|
||||
except (BuildError, NotImplementedError):
|
||||
if not self._dead:
|
||||
raise
|
||||
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=0)
|
||||
return self
|
||||
else:
|
||||
self._apply_step('L', portspec, out_port0, data0, tool)
|
||||
self._apply_step('L', portspec, out_port1, data1, tool, plug_into)
|
||||
return self
|
||||
if out_port is not None:
|
||||
self._apply_step('U', portspec, out_port, data, tool, plug_into)
|
||||
|
|
|
|||
|
|
@ -1031,7 +1031,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
|||
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1]
|
||||
if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]:
|
||||
sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
|
||||
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[1] + otrans_dxy[1])
|
||||
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[0] + otrans_dxy[0])
|
||||
if success:
|
||||
b_transition = None
|
||||
straight_length = 0
|
||||
|
|
@ -1244,7 +1244,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> Library:
|
||||
out_port, dxy = self.planL(
|
||||
out_port, _data = self.planL(
|
||||
ccw,
|
||||
length,
|
||||
in_ptype=in_ptype,
|
||||
|
|
@ -1252,7 +1252,12 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
)
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
|
||||
vertices: list[tuple[float, float]]
|
||||
if ccw is None:
|
||||
vertices = [(0.0, 0.0), (length, 0.0)]
|
||||
else:
|
||||
vertices = [(0.0, 0.0), (length, 0.0), tuple(out_port.offset)]
|
||||
pat.path(layer=self.layer, width=self.width, vertices=vertices)
|
||||
|
||||
if ccw is None:
|
||||
out_rot = pi
|
||||
|
|
@ -1263,7 +1268,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
|
||||
pat.ports = {
|
||||
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype),
|
||||
port_names[1]: Port(dxy, rotation=out_rot, ptype=self.ptype),
|
||||
port_names[1]: Port(out_port.offset, rotation=out_rot, ptype=self.ptype),
|
||||
}
|
||||
|
||||
return tree
|
||||
|
|
|
|||
|
|
@ -453,6 +453,8 @@ class Path(Shape):
|
|||
def scale_by(self, c: float) -> 'Path':
|
||||
self.vertices *= c
|
||||
self.width *= c
|
||||
if self.cap_extensions is not None:
|
||||
self.cap_extensions *= c
|
||||
return self
|
||||
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
|
|
@ -476,13 +478,15 @@ class Path(Shape):
|
|||
reordered_vertices = rotated_vertices
|
||||
|
||||
width0 = self.width / norm_value
|
||||
cap_extensions0 = None if self.cap_extensions is None else tuple(float(v) / norm_value for v in self.cap_extensions)
|
||||
|
||||
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap),
|
||||
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, cap_extensions0),
|
||||
(offset, scale / norm_value, rotation, False),
|
||||
lambda: Path(
|
||||
reordered_vertices * norm_value,
|
||||
width=self.width * norm_value,
|
||||
width=width0 * norm_value,
|
||||
cap=self.cap,
|
||||
cap_extensions=None if cap_extensions0 is None else tuple(v * norm_value for v in cap_extensions0),
|
||||
))
|
||||
|
||||
def clean_vertices(self) -> 'Path':
|
||||
|
|
|
|||
|
|
@ -108,6 +108,62 @@ def test_autotool_planS_double_L(multi_bend_tool) -> None:
|
|||
assert data.ldata1.straight_length == 0
|
||||
assert data.l2_length == 6
|
||||
|
||||
|
||||
def test_autotool_planS_pure_sbend_with_transition_dx() -> None:
|
||||
lib = Library()
|
||||
|
||||
def make_straight(length: float) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pat.ports["B"] = Port((length, 0), pi, ptype="core")
|
||||
return pat
|
||||
|
||||
def make_sbend(jog: float) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pat.ports["B"] = Port((10, jog), pi, ptype="core")
|
||||
return pat
|
||||
|
||||
trans_pat = Pattern()
|
||||
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
||||
trans_pat.ports["CORE"] = Port((5, 0), pi, ptype="core")
|
||||
lib["xin"] = trans_pat
|
||||
|
||||
tool = AutoTool(
|
||||
straights=[
|
||||
AutoTool.Straight(
|
||||
ptype="core",
|
||||
fn=make_straight,
|
||||
in_port_name="A",
|
||||
out_port_name="B",
|
||||
length_range=(1, 1e8),
|
||||
)
|
||||
],
|
||||
bends=[],
|
||||
sbends=[
|
||||
AutoTool.SBend(
|
||||
ptype="core",
|
||||
fn=make_sbend,
|
||||
in_port_name="A",
|
||||
out_port_name="B",
|
||||
jog_range=(0, 1e8),
|
||||
)
|
||||
],
|
||||
transitions={
|
||||
("ext", "core"): AutoTool.Transition(lib.abstract("xin"), "EXT", "CORE"),
|
||||
},
|
||||
default_out_ptype="core",
|
||||
)
|
||||
|
||||
p, data = tool.planS(15, 4, in_ptype="ext")
|
||||
|
||||
assert_allclose(p.offset, [15, 4])
|
||||
assert_allclose(p.rotation, pi)
|
||||
assert data.straight_length == 0
|
||||
assert data.jog_remaining == 4
|
||||
assert data.in_transition is not None
|
||||
|
||||
|
||||
def test_renderpather_autotool_double_L(multi_bend_tool) -> None:
|
||||
tool, lib = multi_bend_tool
|
||||
rp = RenderPather(lib, tools=tool)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from ..pattern import Pattern
|
|||
from ..error import LibraryError, PatternError
|
||||
from ..ports import Port
|
||||
from ..repetition import Grid
|
||||
from ..shapes import Path
|
||||
from ..file.utils import preflight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -243,3 +244,18 @@ def test_library_get_name() -> None:
|
|||
|
||||
name2 = lib.get_name("other")
|
||||
assert name2 == "other"
|
||||
|
||||
|
||||
def test_library_dedup_shapes_does_not_merge_custom_capped_paths() -> None:
|
||||
lib = Library()
|
||||
pat = Pattern()
|
||||
pat.shapes[(1, 0)] += [
|
||||
Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(1, 2)),
|
||||
Path(vertices=[[20, 0], [30, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(3, 4)),
|
||||
]
|
||||
lib["top"] = pat
|
||||
|
||||
lib.dedup(norm_value=1, threshold=2)
|
||||
|
||||
assert not lib["top"].refs
|
||||
assert len(lib["top"].shapes[(1, 0)]) == 2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ..utils.pack2d import maxrects_bssf, pack_patterns
|
||||
from ..utils.pack2d import maxrects_bssf, guillotine_bssf_sas, pack_patterns
|
||||
from ..library import Library
|
||||
from ..pattern import Pattern
|
||||
|
||||
|
|
@ -25,6 +25,34 @@ def test_maxrects_bssf_reject() -> None:
|
|||
assert 0 not in rejects
|
||||
|
||||
|
||||
def test_maxrects_bssf_exact_fill_rejects_remaining() -> None:
|
||||
rects = [[20, 20], [1, 1]]
|
||||
containers = [[0, 0, 20, 20]]
|
||||
|
||||
locs, rejects = maxrects_bssf(rects, containers, presort=False, allow_rejects=True)
|
||||
|
||||
assert tuple(locs[0]) == (0.0, 0.0)
|
||||
assert rejects == {1}
|
||||
|
||||
|
||||
def test_maxrects_bssf_presort_reject_mapping() -> None:
|
||||
rects = [[10, 12], [19, 14], [13, 11]]
|
||||
containers = [[0, 0, 20, 20]]
|
||||
|
||||
_locs, rejects = maxrects_bssf(rects, containers, presort=True, allow_rejects=True)
|
||||
|
||||
assert rejects == {0, 2}
|
||||
|
||||
|
||||
def test_guillotine_bssf_sas_presort_reject_mapping() -> None:
|
||||
rects = [[2, 1], [17, 15], [16, 11]]
|
||||
containers = [[0, 0, 20, 20]]
|
||||
|
||||
_locs, rejects = guillotine_bssf_sas(rects, containers, presort=True, allow_rejects=True)
|
||||
|
||||
assert rejects == {2}
|
||||
|
||||
|
||||
def test_pack_patterns() -> None:
|
||||
lib = Library()
|
||||
p1 = Pattern()
|
||||
|
|
@ -49,3 +77,20 @@ def test_pack_patterns() -> None:
|
|||
# p1 size 10x10, effectively 12x12
|
||||
# p2 size 5x5, effectively 7x7
|
||||
# Both should fit in 20x20
|
||||
|
||||
|
||||
def test_pack_patterns_reject_names_match_original_patterns() -> None:
|
||||
lib = Library()
|
||||
for name, (lx, ly) in {
|
||||
"p0": (10, 12),
|
||||
"p1": (19, 14),
|
||||
"p2": (13, 11),
|
||||
}.items():
|
||||
pat = Pattern()
|
||||
pat.rect((1, 0), xmin=0, xmax=lx, ymin=0, ymax=ly)
|
||||
lib[name] = pat
|
||||
|
||||
pat, rejects = pack_patterns(lib, ["p0", "p1", "p2"], [[0, 0, 20, 20]], spacing=(0, 0))
|
||||
|
||||
assert set(rejects) == {"p0", "p2"}
|
||||
assert set(pat.refs) == {"p1"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from numpy.testing import assert_equal
|
||||
from numpy.testing import assert_equal, assert_allclose
|
||||
|
||||
from ..shapes import Path
|
||||
|
||||
|
|
@ -79,3 +79,33 @@ def test_path_scale() -> None:
|
|||
p.scale_by(2)
|
||||
assert_equal(p.vertices, [[0, 0], [20, 0]])
|
||||
assert p.width == 4
|
||||
|
||||
|
||||
def test_path_scale_custom_cap_extensions() -> None:
|
||||
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(1, 2))
|
||||
p.scale_by(3)
|
||||
|
||||
assert_equal(p.vertices, [[0, 0], [30, 0]])
|
||||
assert p.width == 6
|
||||
assert p.cap_extensions is not None
|
||||
assert_allclose(p.cap_extensions, [3, 6])
|
||||
assert_equal(p.to_polygons()[0].get_bounds_single(), [[-3, -3], [36, 3]])
|
||||
|
||||
|
||||
def test_path_normalized_form_preserves_width_and_custom_cap_extensions() -> None:
|
||||
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(1, 2))
|
||||
|
||||
intrinsic, _extrinsic, ctor = p.normalized_form(5)
|
||||
q = ctor()
|
||||
|
||||
assert intrinsic[-1] == (0.2, 0.4)
|
||||
assert q.width == 2
|
||||
assert q.cap_extensions is not None
|
||||
assert_allclose(q.cap_extensions, [1, 2])
|
||||
|
||||
|
||||
def test_path_normalized_form_distinguishes_custom_caps() -> None:
|
||||
p1 = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(1, 2))
|
||||
p2 = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(3, 4))
|
||||
|
||||
assert p1.normalized_form(1)[0] != p2.normalized_form(1)[0]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import pytest
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from masque import Pather, RenderPather, Library, Pattern, Port
|
||||
from masque.builder.tools import PathTool
|
||||
from masque.error import BuildError
|
||||
|
||||
def test_pather_trace_basic() -> None:
|
||||
lib = Library()
|
||||
|
|
@ -240,3 +242,31 @@ def test_pather_trace_into() -> None:
|
|||
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)
|
||||
|
||||
|
||||
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=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_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
|
||||
|
|
|
|||
|
|
@ -119,3 +119,14 @@ def test_renderpather_rename_port(rpather_setup: tuple[RenderPather, PathTool, L
|
|||
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
|
||||
assert "new_start" in rp.ports
|
||||
assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10)
|
||||
|
||||
|
||||
def test_pathtool_traceL_bend_geometry_matches_ports() -> None:
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||
|
||||
tree = tool.traceL(True, 10)
|
||||
pat = tree.top_pattern()
|
||||
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
||||
|
||||
assert_allclose(path_shape.vertices, [[0, 0], [10, 0], [10, 1]], atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].offset, [10, 1], atol=1e-10)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ def maxrects_bssf(
|
|||
degenerate = (min_more & max_less).any(axis=0)
|
||||
regions = regions[~degenerate]
|
||||
|
||||
if regions.shape[0] == 0:
|
||||
if allow_rejects:
|
||||
rejected_inds.add(rect_ind)
|
||||
continue
|
||||
raise MasqueError(f'Failed to find a suitable location for rectangle {rect_ind}')
|
||||
|
||||
''' Place the rect '''
|
||||
# Best short-side fit (bssf) to pick a region
|
||||
region_sizes = regions[:, 2:] - regions[:, :2]
|
||||
|
|
@ -102,7 +108,7 @@ def maxrects_bssf(
|
|||
if presort:
|
||||
unsort_order = rect_order.argsort()
|
||||
rect_locs = rect_locs[unsort_order]
|
||||
rejected_inds = set(unsort_order[list(rejected_inds)])
|
||||
rejected_inds = {int(rect_order[ii]) for ii in rejected_inds}
|
||||
|
||||
return rect_locs, rejected_inds
|
||||
|
||||
|
|
@ -187,7 +193,7 @@ def guillotine_bssf_sas(
|
|||
if presort:
|
||||
unsort_order = rect_order.argsort()
|
||||
rect_locs = rect_locs[unsort_order]
|
||||
rejected_inds = set(unsort_order[list(rejected_inds)])
|
||||
rejected_inds = {int(rect_order[ii]) for ii in rejected_inds}
|
||||
|
||||
return rect_locs, rejected_inds
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue