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