masque/masque/test/test_pattern.py

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])