Compare commits

..

6 commits

10 changed files with 228 additions and 14 deletions

View file

@ -415,8 +415,17 @@ class Pather(PortList):
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi) self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=pi)
return self return self
self._traceL(portspec, ccw0, L1, **(kwargs | {'out_ptype': None})) try:
self._traceL(portspec, not ccw0, L2, **(kwargs | {'plug_into': plug_into})) 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 return self
if out_port is not None: if out_port is not None:
self._apply_step('S', portspec, out_port, data, tool, plug_into) self._apply_step('S', portspec, out_port, data, tool, plug_into)
@ -436,14 +445,16 @@ class Pather(PortList):
try: try:
R = self._get_tool_R(tool, ccw, in_ptype, **kwargs) R = self._get_tool_R(tool, ccw, in_ptype, **kwargs)
L1, L2 = length + R, abs(jog) - R L1, L2 = length + R, abs(jog) - R
self._traceL(portspec, ccw, L1, **(kwargs | {'out_ptype': None})) out_port0, data0 = tool.planL(ccw, L1, in_ptype=in_ptype, **(kwargs | {'out_ptype': None}))
self._traceL(portspec, ccw, L2, **(kwargs | {'plug_into': plug_into})) out_port1, data1 = tool.planL(ccw, L2, in_ptype=out_port0.ptype, **kwargs)
except (BuildError, NotImplementedError): except (BuildError, NotImplementedError):
if not self._dead: if not self._dead:
raise raise
self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=0) self._apply_dead_fallback(portspec, length, jog, None, in_ptype, plug_into, out_rot=0)
return self return self
else: else:
self._apply_step('L', portspec, out_port0, data0, tool)
self._apply_step('L', portspec, out_port1, data1, tool, plug_into)
return self return self
if out_port is not None: if out_port is not None:
self._apply_step('U', portspec, out_port, data, tool, plug_into) self._apply_step('U', portspec, out_port, data, tool, plug_into)

View file

@ -1031,7 +1031,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1] jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1]
if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]: if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]:
sbend_dxy = self._sbend2dxy(sbend, jog_remaining) 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: if success:
b_transition = None b_transition = None
straight_length = 0 straight_length = 0
@ -1244,7 +1244,7 @@ class PathTool(Tool, metaclass=ABCMeta):
port_names: tuple[str, str] = ('A', 'B'), port_names: tuple[str, str] = ('A', 'B'),
**kwargs, # noqa: ARG002 (unused) **kwargs, # noqa: ARG002 (unused)
) -> Library: ) -> Library:
out_port, dxy = self.planL( out_port, _data = self.planL(
ccw, ccw,
length, length,
in_ptype=in_ptype, in_ptype=in_ptype,
@ -1252,7 +1252,12 @@ class PathTool(Tool, metaclass=ABCMeta):
) )
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') 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: if ccw is None:
out_rot = pi out_rot = pi
@ -1263,7 +1268,7 @@ class PathTool(Tool, metaclass=ABCMeta):
pat.ports = { pat.ports = {
port_names[0]: Port((0, 0), rotation=0, ptype=self.ptype), 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 return tree

View file

@ -453,6 +453,8 @@ class Path(Shape):
def scale_by(self, c: float) -> 'Path': def scale_by(self, c: float) -> 'Path':
self.vertices *= c self.vertices *= c
self.width *= c self.width *= c
if self.cap_extensions is not None:
self.cap_extensions *= c
return self return self
def normalized_form(self, norm_value: float) -> normalized_shape_tuple: def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
@ -476,13 +478,15 @@ class Path(Shape):
reordered_vertices = rotated_vertices reordered_vertices = rotated_vertices
width0 = self.width / norm_value 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), (offset, scale / norm_value, rotation, False),
lambda: Path( lambda: Path(
reordered_vertices * norm_value, reordered_vertices * norm_value,
width=self.width * norm_value, width=width0 * norm_value,
cap=self.cap, 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': def clean_vertices(self) -> 'Path':

View file

@ -108,6 +108,62 @@ def test_autotool_planS_double_L(multi_bend_tool) -> None:
assert data.ldata1.straight_length == 0 assert data.ldata1.straight_length == 0
assert data.l2_length == 6 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: def test_renderpather_autotool_double_L(multi_bend_tool) -> None:
tool, lib = multi_bend_tool tool, lib = multi_bend_tool
rp = RenderPather(lib, tools=tool) rp = RenderPather(lib, tools=tool)

View file

@ -6,6 +6,7 @@ from ..pattern import Pattern
from ..error import LibraryError, PatternError from ..error import LibraryError, PatternError
from ..ports import Port from ..ports import Port
from ..repetition import Grid from ..repetition import Grid
from ..shapes import Path
from ..file.utils import preflight from ..file.utils import preflight
if TYPE_CHECKING: if TYPE_CHECKING:
@ -243,3 +244,18 @@ def test_library_get_name() -> None:
name2 = lib.get_name("other") name2 = lib.get_name("other")
assert name2 == "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

View file

@ -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 ..library import Library
from ..pattern import Pattern from ..pattern import Pattern
@ -25,6 +25,34 @@ def test_maxrects_bssf_reject() -> None:
assert 0 not in rejects 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: def test_pack_patterns() -> None:
lib = Library() lib = Library()
p1 = Pattern() p1 = Pattern()
@ -49,3 +77,20 @@ def test_pack_patterns() -> None:
# p1 size 10x10, effectively 12x12 # p1 size 10x10, effectively 12x12
# p2 size 5x5, effectively 7x7 # p2 size 5x5, effectively 7x7
# Both should fit in 20x20 # 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"}

View file

@ -1,4 +1,4 @@
from numpy.testing import assert_equal from numpy.testing import assert_equal, assert_allclose
from ..shapes import Path from ..shapes import Path
@ -79,3 +79,33 @@ def test_path_scale() -> None:
p.scale_by(2) p.scale_by(2)
assert_equal(p.vertices, [[0, 0], [20, 0]]) assert_equal(p.vertices, [[0, 0], [20, 0]])
assert p.width == 4 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]

View file

@ -1,7 +1,9 @@
import pytest
import numpy import numpy
from numpy import pi from numpy import pi
from masque import Pather, RenderPather, Library, Pattern, Port from masque import Pather, RenderPather, Library, Pattern, Port
from masque.builder.tools import PathTool from masque.builder.tools import PathTool
from masque.error import BuildError
def test_pather_trace_basic() -> None: def test_pather_trace_basic() -> None:
lib = Library() lib = Library()
@ -240,3 +242,31 @@ def test_pather_trace_into() -> None:
assert numpy.allclose(p.pattern.ports['G'].offset, (-10000, 2000)) assert numpy.allclose(p.pattern.ports['G'].offset, (-10000, 2000))
assert p.pattern.ports['G'].rotation is not None assert p.pattern.ports['G'].rotation is not None
assert numpy.isclose(p.pattern.ports['G'].rotation, pi) 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

View file

@ -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_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
assert "new_start" in rp.ports assert "new_start" in rp.ports
assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10) 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)

View file

@ -60,6 +60,12 @@ def maxrects_bssf(
degenerate = (min_more & max_less).any(axis=0) degenerate = (min_more & max_less).any(axis=0)
regions = regions[~degenerate] 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 ''' ''' Place the rect '''
# Best short-side fit (bssf) to pick a region # Best short-side fit (bssf) to pick a region
region_sizes = regions[:, 2:] - regions[:, :2] region_sizes = regions[:, 2:] - regions[:, :2]
@ -102,7 +108,7 @@ def maxrects_bssf(
if presort: if presort:
unsort_order = rect_order.argsort() unsort_order = rect_order.argsort()
rect_locs = rect_locs[unsort_order] 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 return rect_locs, rejected_inds
@ -187,7 +193,7 @@ def guillotine_bssf_sas(
if presort: if presort:
unsort_order = rect_order.argsort() unsort_order = rect_order.argsort()
rect_locs = rect_locs[unsort_order] 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 return rect_locs, rejected_inds