310 lines
9.7 KiB
Python
310 lines
9.7 KiB
Python
import pytest
|
|
import copy
|
|
from typing import cast
|
|
from numpy.testing import assert_equal, assert_allclose
|
|
from numpy import pi
|
|
|
|
from ..error import PatternError
|
|
from ..abstract import Abstract
|
|
from ..pattern import Pattern
|
|
from ..shapes import Polygon
|
|
from ..ref import Ref
|
|
from ..ports import Port, PortError
|
|
from ..label import Label
|
|
from ..repetition import Grid
|
|
|
|
|
|
def test_pattern_init() -> None:
|
|
pat = Pattern()
|
|
assert pat.is_empty()
|
|
assert not pat.has_shapes()
|
|
assert not pat.has_refs()
|
|
assert not pat.has_labels()
|
|
assert not pat.has_ports()
|
|
|
|
|
|
def test_pattern_with_elements() -> None:
|
|
poly = Polygon.square(10)
|
|
label = Label("test", offset=(5, 5))
|
|
ref = Ref(offset=(100, 100))
|
|
port = Port((0, 0), 0)
|
|
|
|
pat = Pattern(shapes={(1, 0): [poly]}, labels={(1, 2): [label]}, refs={"sub": [ref]}, ports={"P1": port})
|
|
|
|
assert pat.has_shapes()
|
|
assert pat.has_labels()
|
|
assert pat.has_refs()
|
|
assert pat.has_ports()
|
|
assert not pat.is_empty()
|
|
assert pat.shapes[(1, 0)] == [poly]
|
|
assert pat.labels[(1, 2)] == [label]
|
|
assert pat.refs["sub"] == [ref]
|
|
assert pat.ports["P1"] == port
|
|
|
|
|
|
def test_pattern_append() -> None:
|
|
pat1 = Pattern()
|
|
pat1.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]])
|
|
|
|
pat2 = Pattern()
|
|
pat2.polygon((2, 0), vertices=[[10, 10], [11, 10], [11, 11]])
|
|
|
|
pat1.append(pat2)
|
|
assert len(pat1.shapes[(1, 0)]) == 1
|
|
assert len(pat1.shapes[(2, 0)]) == 1
|
|
|
|
|
|
def test_pattern_translate() -> None:
|
|
pat = Pattern()
|
|
pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]])
|
|
pat.ports["P1"] = Port((5, 5), 0)
|
|
|
|
pat.translate_elements((10, 20))
|
|
|
|
# Polygon.translate adds to vertices, and offset is always (0,0)
|
|
assert_equal(cast("Polygon", pat.shapes[(1, 0)][0]).vertices[0], [10, 20])
|
|
assert_equal(pat.ports["P1"].offset, [15, 25])
|
|
|
|
|
|
def test_pattern_scale() -> None:
|
|
pat = Pattern()
|
|
# Polygon.rect sets an offset in its constructor which is immediately translated into vertices
|
|
pat.rect((1, 0), xmin=0, xmax=1, ymin=0, ymax=1)
|
|
pat.scale_by(2)
|
|
|
|
# Vertices should be scaled
|
|
assert_equal(cast("Polygon", pat.shapes[(1, 0)][0]).vertices, [[0, 0], [0, 2], [2, 2], [2, 0]])
|
|
|
|
|
|
def test_pattern_rotate() -> None:
|
|
pat = Pattern()
|
|
pat.polygon((1, 0), vertices=[[10, 0], [11, 0], [10, 1]])
|
|
# Rotate 90 degrees CCW around (0,0)
|
|
pat.rotate_around((0, 0), pi / 2)
|
|
|
|
# [10, 0] rotated 90 deg around (0,0) is [0, 10]
|
|
assert_allclose(cast("Polygon", pat.shapes[(1, 0)][0]).vertices[0], [0, 10], atol=1e-10)
|
|
|
|
|
|
def test_pattern_mirror() -> None:
|
|
pat = Pattern()
|
|
pat.polygon((1, 0), vertices=[[10, 5], [11, 5], [10, 6]])
|
|
# Mirror across X axis (y -> -y)
|
|
pat.mirror(0)
|
|
|
|
assert_equal(cast("Polygon", pat.shapes[(1, 0)][0]).vertices[0], [10, -5])
|
|
|
|
|
|
def test_pattern_get_bounds() -> None:
|
|
pat = Pattern()
|
|
pat.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10]])
|
|
pat.polygon((1, 0), vertices=[[-5, -5], [5, -5], [5, 5]])
|
|
|
|
bounds = pat.get_bounds()
|
|
assert_equal(bounds, [[-5, -5], [10, 10]])
|
|
|
|
|
|
def test_pattern_flatten_preserves_ports_only_child() -> None:
|
|
child = Pattern(ports={"P1": Port((1, 2), 0)})
|
|
|
|
parent = Pattern()
|
|
parent.ref("child", offset=(10, 10))
|
|
|
|
parent.flatten({"child": child}, flatten_ports=True)
|
|
|
|
assert set(parent.ports) == {"P1"}
|
|
assert parent.ports["P1"].rotation == 0
|
|
assert tuple(parent.ports["P1"].offset) == (11.0, 12.0)
|
|
|
|
|
|
def test_pattern_flatten_repeated_ref_with_ports_raises() -> None:
|
|
child = Pattern(ports={"P1": Port((1, 2), 0)})
|
|
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
|
|
|
parent = Pattern()
|
|
parent.ref("child", repetition=Grid(a_vector=(10, 0), a_count=2))
|
|
|
|
with pytest.raises(PatternError, match='Cannot flatten ports from repeated ref'):
|
|
parent.flatten({"child": child}, flatten_ports=True)
|
|
|
|
|
|
def test_pattern_place_requires_abstract_for_reference() -> None:
|
|
parent = Pattern()
|
|
child = Pattern()
|
|
|
|
with pytest.raises(PatternError, match='Must provide an `Abstract`'):
|
|
parent.place(child)
|
|
|
|
assert not parent.ports
|
|
|
|
|
|
def test_pattern_place_append_requires_pattern_atomically() -> None:
|
|
parent = Pattern()
|
|
child = Abstract("child", {"A": Port((1, 2), 0)})
|
|
|
|
with pytest.raises(PatternError, match='Must provide a full `Pattern`'):
|
|
parent.place(child, append=True)
|
|
|
|
assert not parent.ports
|
|
|
|
|
|
def test_pattern_place_append_annotation_conflict_is_atomic() -> None:
|
|
parent = Pattern(annotations={"k": [1]})
|
|
child = Pattern(annotations={"k": [2]}, ports={"A": Port((1, 2), 0)})
|
|
|
|
with pytest.raises(PatternError, match="Annotation keys overlap"):
|
|
parent.place(child, append=True)
|
|
|
|
assert not parent.ports
|
|
assert parent.annotations == {"k": [1]}
|
|
|
|
|
|
def test_pattern_place_skip_geometry_overwrites_colliding_ports_last_wins() -> None:
|
|
parent = Pattern(ports={
|
|
"A": Port((5, 5), 0),
|
|
"keep": Port((9, 9), 0),
|
|
})
|
|
child = Pattern(ports={
|
|
"X": Port((1, 0), 0),
|
|
"Y": Port((2, 0), pi / 2),
|
|
})
|
|
|
|
parent.place(child, port_map={"X": "A", "Y": "A"}, skip_geometry=True, append=True)
|
|
|
|
assert set(parent.ports) == {"A", "keep"}
|
|
assert_allclose(parent.ports["A"].offset, (2, 0))
|
|
assert parent.ports["A"].rotation is not None
|
|
assert_allclose(parent.ports["A"].rotation, pi / 2)
|
|
|
|
|
|
def test_pattern_interface() -> None:
|
|
source = Pattern()
|
|
source.ports["A"] = Port((10, 20), 0, ptype="test")
|
|
|
|
iface = Pattern.interface(source, in_prefix="in_", out_prefix="out_")
|
|
|
|
assert "in_A" in iface.ports
|
|
assert "out_A" in iface.ports
|
|
assert iface.ports["in_A"].rotation is not None
|
|
assert_allclose(iface.ports["in_A"].rotation, pi, atol=1e-10)
|
|
assert iface.ports["out_A"].rotation is not None
|
|
assert_allclose(iface.ports["out_A"].rotation, 0, atol=1e-10)
|
|
assert iface.ports["in_A"].ptype == "test"
|
|
assert iface.ports["out_A"].ptype == "test"
|
|
|
|
|
|
def test_pattern_interface_duplicate_port_map_targets_raise() -> None:
|
|
source = Pattern()
|
|
source.ports["A"] = Port((10, 20), 0)
|
|
source.ports["B"] = Port((30, 40), pi)
|
|
|
|
with pytest.raises(PortError, match='Duplicate targets in `port_map`'):
|
|
Pattern.interface(source, port_map={"A": "X", "B": "X"})
|
|
|
|
|
|
def test_pattern_interface_empty_port_map_copies_no_ports() -> None:
|
|
source = Pattern()
|
|
source.ports["A"] = Port((10, 20), 0)
|
|
source.ports["B"] = Port((30, 40), pi)
|
|
|
|
assert not Pattern.interface(source, port_map={}).ports
|
|
assert not Pattern.interface(source, port_map=[]).ports
|
|
|
|
|
|
def test_pattern_plug_requires_abstract_for_reference_atomically() -> None:
|
|
parent = Pattern(ports={"X": Port((0, 0), 0)})
|
|
child = Pattern(ports={"A": Port((0, 0), pi)})
|
|
|
|
with pytest.raises(PatternError, match='Must provide an `Abstract`'):
|
|
parent.plug(child, {"X": "A"})
|
|
|
|
assert set(parent.ports) == {"X"}
|
|
|
|
|
|
def test_pattern_plug_append_annotation_conflict_is_atomic() -> None:
|
|
parent = Pattern(
|
|
annotations={"k": [1]},
|
|
ports={"X": Port((0, 0), 0), "Q": Port((9, 9), 0)},
|
|
)
|
|
child = Pattern(
|
|
annotations={"k": [2]},
|
|
ports={"A": Port((0, 0), pi), "B": Port((5, 0), 0)},
|
|
)
|
|
|
|
with pytest.raises(PatternError, match="Annotation keys overlap"):
|
|
parent.plug(child, {"X": "A"}, map_out={"B": "Y"}, append=True)
|
|
|
|
assert set(parent.ports) == {"X", "Q"}
|
|
assert_allclose(parent.ports["X"].offset, (0, 0))
|
|
assert_allclose(parent.ports["Q"].offset, (9, 9))
|
|
assert parent.annotations == {"k": [1]}
|
|
|
|
|
|
def test_pattern_plug_skip_geometry_overwrites_colliding_ports_last_wins() -> None:
|
|
parent = Pattern(ports={
|
|
"A": Port((0, 0), 0, ptype="wire"),
|
|
"B": Port((99, 99), 0, ptype="wire"),
|
|
})
|
|
child = Pattern(ports={
|
|
"in": Port((0, 0), pi, ptype="wire"),
|
|
"X": Port((10, 0), 0, ptype="wire"),
|
|
"Y": Port((20, 0), 0, ptype="wire"),
|
|
})
|
|
|
|
parent.plug(child, {"A": "in"}, map_out={"X": "B", "Y": "B"}, skip_geometry=True, append=True)
|
|
|
|
assert "A" not in parent.ports
|
|
assert "B" in parent.ports
|
|
assert_allclose(parent.ports["B"].offset, (20, 0))
|
|
assert parent.ports["B"].rotation is not None
|
|
assert_allclose(parent.ports["B"].rotation, 0)
|
|
|
|
|
|
def test_pattern_append_port_conflict_is_atomic() -> None:
|
|
pat1 = Pattern()
|
|
pat1.ports["A"] = Port((0, 0), 0)
|
|
|
|
pat2 = Pattern()
|
|
pat2.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
|
pat2.ports["A"] = Port((1, 0), 0)
|
|
|
|
with pytest.raises(PatternError, match="Port names overlap"):
|
|
pat1.append(pat2)
|
|
|
|
assert not pat1.shapes
|
|
assert set(pat1.ports) == {"A"}
|
|
|
|
|
|
def test_pattern_append_annotation_conflict_is_atomic() -> None:
|
|
pat1 = Pattern(annotations={"k": [1]})
|
|
pat2 = Pattern(annotations={"k": [2]})
|
|
pat2.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
|
|
|
with pytest.raises(PatternError, match="Annotation keys overlap"):
|
|
pat1.append(pat2)
|
|
|
|
assert not pat1.shapes
|
|
assert pat1.annotations == {"k": [1]}
|
|
|
|
|
|
def test_pattern_deepcopy_does_not_share_shape_repetitions() -> None:
|
|
pat = Pattern()
|
|
pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]], repetition=Grid(a_vector=(10, 0), a_count=2))
|
|
|
|
pat2 = copy.deepcopy(pat)
|
|
pat2.scale_by(2)
|
|
|
|
assert_allclose(cast("Polygon", pat.shapes[(1, 0)][0]).repetition.a_vector, [10, 0])
|
|
assert_allclose(cast("Polygon", pat2.shapes[(1, 0)][0]).repetition.a_vector, [20, 0])
|
|
|
|
|
|
def test_pattern_flatten_does_not_mutate_child_repetitions() -> None:
|
|
child = Pattern()
|
|
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]], repetition=Grid(a_vector=(10, 0), a_count=2))
|
|
|
|
parent = Pattern()
|
|
parent.ref("child", scale=2)
|
|
|
|
parent.flatten({"child": child})
|
|
|
|
assert_allclose(cast("Polygon", child.shapes[(1, 0)][0]).repetition.a_vector, [10, 0])
|