[Tests] cleanup

This commit is contained in:
Jan Petykiewicz 2026-02-15 12:36:13 -08:00
commit 1cce6c1f70
23 changed files with 540 additions and 467 deletions

View file

@ -3,14 +3,11 @@
Test fixtures Test fixtures
""" """
# ruff: noqa: ARG001 # ruff: noqa: ARG001
from typing import Any from typing import Any
import numpy import numpy
from numpy.typing import NDArray
import pytest # type: ignore
FixtureRequest = Any FixtureRequest = Any
PRNG = numpy.random.RandomState(12345) PRNG = numpy.random.RandomState(12345)

View file

@ -1,20 +1,20 @@
import pytest from numpy.testing import assert_allclose
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..abstract import Abstract from ..abstract import Abstract
from ..ports import Port from ..ports import Port
from ..ref import Ref from ..ref import Ref
def test_abstract_init():
def test_abstract_init() -> None:
ports = {"A": Port((0, 0), 0), "B": Port((10, 0), pi)} ports = {"A": Port((0, 0), 0), "B": Port((10, 0), pi)}
abs_obj = Abstract("test", ports) abs_obj = Abstract("test", ports)
assert abs_obj.name == "test" assert abs_obj.name == "test"
assert len(abs_obj.ports) == 2 assert len(abs_obj.ports) == 2
assert abs_obj.ports["A"] is not ports["A"] # Should be deepcopied assert abs_obj.ports["A"] is not ports["A"] # Should be deepcopied
def test_abstract_transform():
def test_abstract_transform() -> None:
abs_obj = Abstract("test", {"A": Port((10, 0), 0)}) abs_obj = Abstract("test", {"A": Port((10, 0), 0)})
# Rotate 90 deg around (0,0) # Rotate 90 deg around (0,0)
abs_obj.rotate_around((0, 0), pi / 2) abs_obj.rotate_around((0, 0), pi / 2)
@ -29,7 +29,8 @@ def test_abstract_transform():
assert_allclose(abs_obj.ports["A"].offset, [0, -10], atol=1e-10) assert_allclose(abs_obj.ports["A"].offset, [0, -10], atol=1e-10)
assert_allclose(abs_obj.ports["A"].rotation, 3 * pi / 2, atol=1e-10) assert_allclose(abs_obj.ports["A"].rotation, 3 * pi / 2, atol=1e-10)
def test_abstract_ref_transform():
def test_abstract_ref_transform() -> None:
abs_obj = Abstract("test", {"A": Port((10, 0), 0)}) abs_obj = Abstract("test", {"A": Port((10, 0), 0)})
ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True) ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True)
@ -49,7 +50,8 @@ def test_abstract_ref_transform():
assert_allclose(abs_obj.ports["A"].offset, [100, 110], atol=1e-10) assert_allclose(abs_obj.ports["A"].offset, [100, 110], atol=1e-10)
assert_allclose(abs_obj.ports["A"].rotation, pi / 2, atol=1e-10) assert_allclose(abs_obj.ports["A"].rotation, pi / 2, atol=1e-10)
def test_abstract_undo_transform():
def test_abstract_undo_transform() -> None:
abs_obj = Abstract("test", {"A": Port((100, 110), pi / 2)}) abs_obj = Abstract("test", {"A": Port((100, 110), pi / 2)})
ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True) ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True)

View file

@ -1,6 +1,5 @@
import pytest import pytest
import numpy from numpy.testing import assert_equal
from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..builder import Pather from ..builder import Pather
@ -8,15 +7,17 @@ from ..builder.tools import PathTool
from ..library import Library from ..library import Library
from ..ports import Port from ..ports import Port
@pytest.fixture @pytest.fixture
def advanced_pather(): def advanced_pather() -> tuple[Pather, PathTool, Library]:
lib = Library() lib = Library()
# Simple PathTool: 2um width on layer (1,0) # Simple PathTool: 2um width on layer (1,0)
tool = PathTool(layer=(1, 0), width=2, ptype="wire") tool = PathTool(layer=(1, 0), width=2, ptype="wire")
p = Pather(lib, tools=tool) p = Pather(lib, tools=tool)
return p, tool, lib return p, tool, lib
def test_path_into_straight(advanced_pather):
def test_path_into_straight(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = advanced_pather p, tool, lib = advanced_pather
# Facing ports # Facing ports
p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device) p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device)
@ -31,7 +32,8 @@ def test_path_into_straight(advanced_pather):
# Pather.path adds a Reference to the generated pattern # Pather.path adds a Reference to the generated pattern
assert len(p.pattern.refs) == 1 assert len(p.pattern.refs) == 1
def test_path_into_bend(advanced_pather):
def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = advanced_pather p, tool, lib = advanced_pather
# Source at (0,0) rot 0 (facing East). Forward is West (-x). # Source at (0,0) rot 0 (facing East). Forward is West (-x).
p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["src"] = Port((0, 0), 0, ptype="wire")
@ -48,7 +50,8 @@ def test_path_into_bend(advanced_pather):
# Single bend should result in 2 segments (one for x move, one for y move) # Single bend should result in 2 segments (one for x move, one for y move)
assert len(p.pattern.refs) == 2 assert len(p.pattern.refs) == 2
def test_path_into_sbend(advanced_pather):
def test_path_into_sbend(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = advanced_pather p, tool, lib = advanced_pather
# Facing but offset ports # Facing but offset ports
p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x) p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x)
@ -59,7 +62,8 @@ def test_path_into_sbend(advanced_pather):
assert "src" not in p.ports assert "src" not in p.ports
assert "dst" not in p.ports assert "dst" not in p.ports
def test_path_from(advanced_pather):
def test_path_from(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = advanced_pather p, tool, lib = advanced_pather
p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["src"] = Port((0, 0), 0, ptype="wire")
p.ports["dst"] = Port((-20, 0), pi, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
@ -69,7 +73,8 @@ def test_path_from(advanced_pather):
assert "src" not in p.ports assert "src" not in p.ports
assert "dst" not in p.ports assert "dst" not in p.ports
def test_path_into_thru(advanced_pather):
def test_path_into_thru(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = advanced_pather p, tool, lib = advanced_pather
p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["src"] = Port((0, 0), 0, ptype="wire")
p.ports["dst"] = Port((-20, 0), pi, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire")

View file

@ -1,6 +1,5 @@
import pytest import pytest
import numpy from numpy.testing import assert_allclose
from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..builder import Pather from ..builder import Pather
@ -8,17 +7,18 @@ from ..builder.tools import AutoTool
from ..library import Library from ..library import Library
from ..pattern import Pattern from ..pattern import Pattern
from ..ports import Port from ..ports import Port
from ..abstract import Abstract
def make_straight(length, width=2, ptype="wire"):
def make_straight(length: float, width: float = 2, ptype: str = "wire") -> Pattern:
pat = Pattern() pat = Pattern()
pat.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width) pat.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width)
pat.ports["in"] = Port((0, 0), 0, ptype=ptype) pat.ports["in"] = Port((0, 0), 0, ptype=ptype)
pat.ports["out"] = Port((length, 0), pi, ptype=ptype) pat.ports["out"] = Port((length, 0), pi, ptype=ptype)
return pat return pat
@pytest.fixture @pytest.fixture
def autotool_setup(): def autotool_setup() -> tuple[Pather, AutoTool, Library]:
lib = Library() lib = Library()
# Define a simple bend # Define a simple bend
@ -27,7 +27,7 @@ def autotool_setup():
bend_pat.ports["in"] = Port((0, 0), 0, ptype="wire") bend_pat.ports["in"] = Port((0, 0), 0, ptype="wire")
bend_pat.ports["out"] = Port((2, -2), pi / 2, ptype="wire") bend_pat.ports["out"] = Port((2, -2), pi / 2, ptype="wire")
lib["bend"] = bend_pat lib["bend"] = bend_pat
bend_abs = lib.abstract("bend") lib.abstract("bend")
# Define a transition (e.g., via) # Define a transition (e.g., via)
via_pat = Pattern() via_pat = Pattern()
@ -37,14 +37,13 @@ def autotool_setup():
via_abs = lib.abstract("via") via_abs = lib.abstract("via")
tool_m1 = AutoTool( tool_m1 = AutoTool(
straights=[AutoTool.Straight(ptype="wire_m1", fn=lambda l: make_straight(l, ptype="wire_m1"), straights=[
in_port_name="in", out_port_name="out")], AutoTool.Straight(ptype="wire_m1", fn=lambda length: make_straight(length, ptype="wire_m1"), in_port_name="in", out_port_name="out")
],
bends=[], bends=[],
sbends=[], sbends=[],
transitions={ transitions={("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1")},
("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1") default_out_ptype="wire_m1",
},
default_out_ptype="wire_m1"
) )
p = Pather(lib, tools=tool_m1) p = Pather(lib, tools=tool_m1)
@ -53,7 +52,8 @@ def autotool_setup():
return p, tool_m1, lib return p, tool_m1, lib
def test_autotool_transition(autotool_setup):
def test_autotool_transition(autotool_setup: tuple[Pather, AutoTool, Library]) -> None:
p, tool, lib = autotool_setup p, tool, lib = autotool_setup
# Route m1 from an m2 port. Should trigger via. # Route m1 from an m2 port. Should trigger via.
@ -79,4 +79,3 @@ def test_autotool_transition(autotool_setup):
assert_allclose(p.ports["start"].offset, [10, 0], atol=1e-10) assert_allclose(p.ports["start"].offset, [10, 0], atol=1e-10)
assert p.ports["start"].ptype == "wire_m1" assert p.ports["start"].ptype == "wire_m1"

View file

@ -1,5 +1,3 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
@ -8,13 +6,15 @@ from ..library import Library
from ..pattern import Pattern from ..pattern import Pattern
from ..ports import Port from ..ports import Port
def test_builder_init():
def test_builder_init() -> None:
lib = Library() lib = Library()
b = Builder(lib, name="mypat") b = Builder(lib, name="mypat")
assert b.pattern is lib["mypat"] assert b.pattern is lib["mypat"]
assert b.library is lib assert b.library is lib
def test_builder_place():
def test_builder_place() -> None:
lib = Library() lib = Library()
child = Pattern() child = Pattern()
child.ports["A"] = Port((0, 0), 0) child.ports["A"] = Port((0, 0), 0)
@ -27,7 +27,8 @@ def test_builder_place():
assert_equal(b.ports["child_A"].offset, [10, 20]) assert_equal(b.ports["child_A"].offset, [10, 20])
assert "child" in b.pattern.refs assert "child" in b.pattern.refs
def test_builder_plug():
def test_builder_plug() -> None:
lib = Library() lib = Library()
wire = Pattern() wire = Pattern()
@ -51,7 +52,8 @@ def test_builder_plug():
assert_equal(b.ports["start"].offset, [90, 100]) assert_equal(b.ports["start"].offset, [90, 100])
assert_allclose(b.ports["start"].rotation, 0, atol=1e-10) assert_allclose(b.ports["start"].rotation, 0, atol=1e-10)
def test_builder_interface():
def test_builder_interface() -> None:
lib = Library() lib = Library()
source = Pattern() source = Pattern()
source.ports["P1"] = Port((0, 0), 0) source.ports["P1"] = Port((0, 0), 0)
@ -62,7 +64,8 @@ def test_builder_interface():
assert "P1" in b.ports assert "P1" in b.ports
assert b.pattern is lib["iface"] assert b.pattern is lib["iface"]
def test_builder_set_dead():
def test_builder_set_dead() -> None:
lib = Library() lib = Library()
lib["sub"] = Pattern() lib["sub"] = Pattern()
b = Builder(lib) b = Builder(lib)
@ -70,4 +73,3 @@ def test_builder_set_dead():
b.place("sub") b.place("sub")
assert not b.pattern.has_refs() assert not b.pattern.has_refs()

View file

@ -22,4 +22,3 @@ def test_circle_mirror():
assert cc.offset[0] == -10 assert cc.offset[0] == -10
assert cc.offset[1] == -20 assert cc.offset[1] == -20
assert cc.radius == 4 assert cc.radius == 4

View file

@ -1,15 +1,14 @@
import pytest from pathlib import Path
import os
import numpy import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from pathlib import Path
from ..pattern import Pattern from ..pattern import Pattern
from ..library import Library from ..library import Library
from ..file import gdsii from ..file import gdsii
from ..shapes import Polygon, Path as MPath from ..shapes import Path as MPath
def test_gdsii_roundtrip(tmp_path):
def test_gdsii_roundtrip(tmp_path: Path) -> None:
lib = Library() lib = Library()
# Simple polygon cell # Simple polygon cell
@ -54,7 +53,8 @@ def test_gdsii_roundtrip(tmp_path):
assert_equal(read_ref.offset, [50, 50]) assert_equal(read_ref.offset, [50, 50])
assert_allclose(read_ref.rotation, numpy.pi / 2, atol=1e-5) assert_allclose(read_ref.rotation, numpy.pi / 2, atol=1e-5)
def test_gdsii_annotations(tmp_path):
def test_gdsii_annotations(tmp_path: Path) -> None:
lib = Library() lib = Library()
pat = Pattern() pat = Pattern()
# GDS only supports integer keys in range [1, 126] for properties # GDS only supports integer keys in range [1, 126] for properties
@ -67,4 +67,3 @@ def test_gdsii_annotations(tmp_path):
read_lib, _ = gdsii.readfile(gds_file) read_lib, _ = gdsii.readfile(gds_file)
read_ann = read_lib["cell"].shapes[(1, 0)][0].annotations read_ann = read_lib["cell"].shapes[(1, 0)][0].annotations
assert read_ann["1"] == ["hello"] assert read_ann["1"] == ["hello"]

View file

@ -1,36 +1,39 @@
import pytest import copy
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..label import Label from ..label import Label
from ..repetition import Grid from ..repetition import Grid
def test_label_init():
l = Label("test", offset=(10, 20))
assert l.string == "test"
assert_equal(l.offset, [10, 20])
def test_label_transform(): def test_label_init() -> None:
l = Label("test", offset=(10, 0)) lbl = Label("test", offset=(10, 20))
assert lbl.string == "test"
assert_equal(lbl.offset, [10, 20])
def test_label_transform() -> None:
lbl = Label("test", offset=(10, 0))
# Rotate 90 deg CCW around (0,0) # Rotate 90 deg CCW around (0,0)
l.rotate_around((0, 0), pi/2) lbl.rotate_around((0, 0), pi / 2)
assert_allclose(l.offset, [0, 10], atol=1e-10) assert_allclose(lbl.offset, [0, 10], atol=1e-10)
# Translate # Translate
l.translate((5, 5)) lbl.translate((5, 5))
assert_allclose(l.offset, [5, 15], atol=1e-10) assert_allclose(lbl.offset, [5, 15], atol=1e-10)
def test_label_repetition():
def test_label_repetition() -> None:
rep = Grid(a_vector=(10, 0), a_count=3) rep = Grid(a_vector=(10, 0), a_count=3)
l = Label("rep", offset=(0, 0), repetition=rep) lbl = Label("rep", offset=(0, 0), repetition=rep)
assert l.repetition is rep assert lbl.repetition is rep
assert_equal(l.get_bounds_single(), [[0, 0], [0, 0]]) assert_equal(lbl.get_bounds_single(), [[0, 0], [0, 0]])
# Note: Bounded.get_bounds_nonempty() for labels with repetition doesn't # Note: Bounded.get_bounds_nonempty() for labels with repetition doesn't
# seem to automatically include repetition bounds in label.py itself, # seem to automatically include repetition bounds in label.py itself,
# it's handled during pattern bounding. # it's handled during pattern bounding.
def test_label_copy():
def test_label_copy() -> None:
l1 = Label("test", offset=(1, 2), annotations={"a": [1]}) l1 = Label("test", offset=(1, 2), annotations={"a": [1]})
l2 = copy.deepcopy(l1) l2 = copy.deepcopy(l1)
@ -38,11 +41,10 @@ def test_label_copy():
print(f"l2: string={l2.string}, offset={l2.offset}, repetition={l2.repetition}, annotations={l2.annotations}") print(f"l2: string={l2.string}, offset={l2.offset}, repetition={l2.repetition}, annotations={l2.annotations}")
from ..utils import annotations_eq from ..utils import annotations_eq
print(f"annotations_eq: {annotations_eq(l1.annotations, l2.annotations)}") print(f"annotations_eq: {annotations_eq(l1.annotations, l2.annotations)}")
assert l1 == l2 assert l1 == l2
assert l1 is not l2 assert l1 is not l2
l2.offset[0] = 100 l2.offset[0] = 100
assert l1.offset[0] == 1 assert l1.offset[0] == 1
import copy

View file

@ -1,10 +1,10 @@
import pytest import pytest
from ..library import Library, LazyLibrary, LibraryView from ..library import Library, LazyLibrary
from ..pattern import Pattern from ..pattern import Pattern
from ..ref import Ref
from ..error import LibraryError from ..error import LibraryError
def test_library_basic():
def test_library_basic() -> None:
lib = Library() lib = Library()
pat = Pattern() pat = Pattern()
lib["cell1"] = pat lib["cell1"] = pat
@ -16,7 +16,8 @@ def test_library_basic():
with pytest.raises(LibraryError): with pytest.raises(LibraryError):
lib["cell1"] = Pattern() # Overwriting not allowed lib["cell1"] = Pattern() # Overwriting not allowed
def test_library_tops():
def test_library_tops() -> None:
lib = Library() lib = Library()
lib["child"] = Pattern() lib["child"] = Pattern()
lib["parent"] = Pattern() lib["parent"] = Pattern()
@ -25,14 +26,16 @@ def test_library_tops():
assert set(lib.tops()) == {"parent"} assert set(lib.tops()) == {"parent"}
assert lib.top() == "parent" assert lib.top() == "parent"
def test_library_dangling():
def test_library_dangling() -> None:
lib = Library() lib = Library()
lib["parent"] = Pattern() lib["parent"] = Pattern()
lib["parent"].ref("missing") lib["parent"].ref("missing")
assert lib.dangling_refs() == {"missing"} assert lib.dangling_refs() == {"missing"}
def test_library_flatten():
def test_library_flatten() -> None:
lib = Library() lib = Library()
child = Pattern() child = Pattern()
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
@ -51,10 +54,12 @@ def test_library_flatten():
assert_vertices = flat_parent.shapes[(1, 0)][0].vertices assert_vertices = flat_parent.shapes[(1, 0)][0].vertices
assert tuple(assert_vertices[0]) == (10.0, 10.0) assert tuple(assert_vertices[0]) == (10.0, 10.0)
def test_lazy_library():
def test_lazy_library() -> None:
lib = LazyLibrary() lib = LazyLibrary()
called = 0 called = 0
def make_pat():
def make_pat() -> Pattern:
nonlocal called nonlocal called
called += 1 called += 1
return Pattern() return Pattern()
@ -71,7 +76,8 @@ def test_lazy_library():
assert called == 1 assert called == 1
assert pat is pat2 assert pat is pat2
def test_library_rename():
def test_library_rename() -> None:
lib = Library() lib = Library()
lib["old"] = Pattern() lib["old"] = Pattern()
lib["parent"] = Pattern() lib["parent"] = Pattern()
@ -84,7 +90,8 @@ def test_library_rename():
assert "new" in lib["parent"].refs assert "new" in lib["parent"].refs
assert "old" not in lib["parent"].refs assert "old" not in lib["parent"].refs
def test_library_subtree():
def test_library_subtree() -> None:
lib = Library() lib = Library()
lib["a"] = Pattern() lib["a"] = Pattern()
lib["b"] = Pattern() lib["b"] = Pattern()
@ -96,7 +103,8 @@ def test_library_subtree():
assert "b" in sub assert "b" in sub
assert "c" not in sub assert "c" not in sub
def test_library_get_name():
def test_library_get_name() -> None:
lib = Library() lib = Library()
lib["cell"] = Pattern() lib["cell"] = Pattern()
@ -106,4 +114,3 @@ def test_library_get_name():
name2 = lib.get_name("other") name2 = lib.get_name("other")
assert name2 == "other" assert name2 == "other"

View file

@ -1,13 +1,13 @@
import pytest
import numpy
from numpy.testing import assert_equal
from pathlib import Path from pathlib import Path
import pytest
from numpy.testing import assert_equal
from ..pattern import Pattern from ..pattern import Pattern
from ..library import Library from ..library import Library
from ..file import oasis from ..file import oasis
def test_oasis_roundtrip(tmp_path):
def test_oasis_roundtrip(tmp_path: Path) -> None:
# Skip if fatamorgana is not installed # Skip if fatamorgana is not installed
pytest.importorskip("fatamorgana") pytest.importorskip("fatamorgana")
@ -25,4 +25,3 @@ def test_oasis_roundtrip(tmp_path):
# Check bounds # Check bounds
assert_equal(read_lib["cell1"].get_bounds(), [[0, 0], [10, 10]]) assert_equal(read_lib["cell1"].get_bounds(), [[0, 0], [10, 10]])

View file

@ -1,12 +1,9 @@
import pytest
import numpy
from numpy.testing import assert_equal
from ..utils.pack2d import maxrects_bssf, pack_patterns from ..utils.pack2d import maxrects_bssf, pack_patterns
from ..library import Library from ..library import Library
from ..pattern import Pattern from ..pattern import Pattern
def test_maxrects_bssf_simple():
def test_maxrects_bssf_simple() -> None:
# Pack two 10x10 squares into one 20x10 container # Pack two 10x10 squares into one 20x10 container
rects = [[10, 10], [10, 10]] rects = [[10, 10], [10, 10]]
containers = [[0, 0, 20, 10]] containers = [[0, 0, 20, 10]]
@ -15,9 +12,10 @@ def test_maxrects_bssf_simple():
assert not rejects assert not rejects
# They should be at (0,0) and (10,0) # They should be at (0,0) and (10,0)
assert set([tuple(l) for l in locs]) == {(0.0, 0.0), (10.0, 0.0)} assert {tuple(loc) for loc in locs} == {(0.0, 0.0), (10.0, 0.0)}
def test_maxrects_bssf_reject():
def test_maxrects_bssf_reject() -> None:
# Try to pack a too-large rectangle # Try to pack a too-large rectangle
rects = [[10, 10], [30, 30]] rects = [[10, 10], [30, 30]]
containers = [[0, 0, 20, 20]] containers = [[0, 0, 20, 20]]
@ -26,7 +24,8 @@ def test_maxrects_bssf_reject():
assert 1 in rejects # Second rect rejected assert 1 in rejects # Second rect rejected
assert 0 not in rejects assert 0 not in rejects
def test_pack_patterns():
def test_pack_patterns() -> None:
lib = Library() lib = Library()
p1 = Pattern() p1 = Pattern()
p1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]]) p1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]])
@ -50,4 +49,3 @@ def test_pack_patterns():
# 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

View file

@ -1,17 +1,16 @@
import pytest from numpy.testing import assert_equal
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from ..shapes import Path from ..shapes import Path
def test_path_init():
def test_path_init() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Flush) p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Flush)
assert_equal(p.vertices, [[0, 0], [10, 0]]) assert_equal(p.vertices, [[0, 0], [10, 0]])
assert p.width == 2 assert p.width == 2
assert p.cap == Path.Cap.Flush assert p.cap == Path.Cap.Flush
def test_path_to_polygons_flush():
def test_path_to_polygons_flush() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Flush) p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Flush)
polys = p.to_polygons() polys = p.to_polygons()
assert len(polys) == 1 assert len(polys) == 1
@ -19,7 +18,8 @@ def test_path_to_polygons_flush():
bounds = polys[0].get_bounds_single() bounds = polys[0].get_bounds_single()
assert_equal(bounds, [[0, -1], [10, 1]]) assert_equal(bounds, [[0, -1], [10, 1]])
def test_path_to_polygons_square():
def test_path_to_polygons_square() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Square) p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Square)
polys = p.to_polygons() polys = p.to_polygons()
assert len(polys) == 1 assert len(polys) == 1
@ -28,7 +28,8 @@ def test_path_to_polygons_square():
bounds = polys[0].get_bounds_single() bounds = polys[0].get_bounds_single()
assert_equal(bounds, [[-1, -1], [11, 1]]) assert_equal(bounds, [[-1, -1], [11, 1]])
def test_path_to_polygons_circle():
def test_path_to_polygons_circle() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Circle) p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Circle)
polys = p.to_polygons(num_vertices=32) polys = p.to_polygons(num_vertices=32)
# Path.to_polygons for Circle cap returns 1 polygon for the path + polygons for the caps # Path.to_polygons for Circle cap returns 1 polygon for the path + polygons for the caps
@ -39,7 +40,8 @@ def test_path_to_polygons_circle():
bounds = p.get_bounds_single() bounds = p.get_bounds_single()
assert_equal(bounds, [[-1, -1], [11, 1]]) assert_equal(bounds, [[-1, -1], [11, 1]])
def test_path_custom_cap():
def test_path_custom_cap() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(5, 10)) p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(5, 10))
polys = p.to_polygons() polys = p.to_polygons()
assert len(polys) == 1 assert len(polys) == 1
@ -48,7 +50,8 @@ def test_path_custom_cap():
bounds = polys[0].get_bounds_single() bounds = polys[0].get_bounds_single()
assert_equal(bounds, [[-5, -1], [20, 1]]) assert_equal(bounds, [[-5, -1], [20, 1]])
def test_path_bend():
def test_path_bend() -> None:
# L-shaped path # L-shaped path
p = Path(vertices=[[0, 0], [10, 0], [10, 10]], width=2) p = Path(vertices=[[0, 0], [10, 0], [10, 10]], width=2)
polys = p.to_polygons() polys = p.to_polygons()
@ -64,14 +67,15 @@ def test_path_bend():
# So bounds should be x: [0, 11], y: [-1, 10] # So bounds should be x: [0, 11], y: [-1, 10]
assert_equal(bounds, [[0, -1], [11, 10]]) assert_equal(bounds, [[0, -1], [11, 10]])
def test_path_mirror():
def test_path_mirror() -> None:
p = Path(vertices=[[10, 5], [20, 10]], width=2) p = Path(vertices=[[10, 5], [20, 10]], width=2)
p.mirror(0) # Mirror across x axis (y -> -y) p.mirror(0) # Mirror across x axis (y -> -y)
assert_equal(p.vertices, [[10, -5], [20, -10]]) assert_equal(p.vertices, [[10, -5], [20, -10]])
def test_path_scale():
def test_path_scale() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2) p = Path(vertices=[[0, 0], [10, 0]], width=2)
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

View file

@ -1,16 +1,15 @@
import pytest import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..builder import Pather from ..builder import Pather
from ..builder.tools import PathTool from ..builder.tools import PathTool
from ..library import Library from ..library import Library
from ..pattern import Pattern
from ..ports import Port from ..ports import Port
@pytest.fixture @pytest.fixture
def pather_setup(): def pather_setup() -> tuple[Pather, PathTool, Library]:
lib = Library() lib = Library()
# Simple PathTool: 2um width on layer (1,0) # Simple PathTool: 2um width on layer (1,0)
tool = PathTool(layer=(1, 0), width=2, ptype="wire") tool = PathTool(layer=(1, 0), width=2, ptype="wire")
@ -21,7 +20,8 @@ def pather_setup():
p.ports["start"] = Port((0, 0), pi / 2, ptype="wire") p.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
return p, tool, lib return p, tool, lib
def test_pather_straight(pather_setup):
def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Route 10um "forward" # Route 10um "forward"
p.path("start", ccw=None, length=10) p.path("start", ccw=None, length=10)
@ -30,7 +30,8 @@ def test_pather_straight(pather_setup):
assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10)
assert_allclose(p.ports["start"].rotation, pi / 2, atol=1e-10) assert_allclose(p.ports["start"].rotation, pi / 2, atol=1e-10)
def test_pather_bend(pather_setup):
def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Start (0,0) rot pi/2 (North). # Start (0,0) rot pi/2 (North).
# Path 10um "forward" (South), then turn Clockwise (ccw=False). # Path 10um "forward" (South), then turn Clockwise (ccw=False).
@ -47,14 +48,16 @@ def test_pather_bend(pather_setup):
# Actual behavior results in 0 (East) - apparently rotation is flipped. # Actual behavior results in 0 (East) - apparently rotation is flipped.
assert_allclose(p.ports["start"].rotation, 0, atol=1e-10) assert_allclose(p.ports["start"].rotation, 0, atol=1e-10)
def test_pather_path_to(pather_setup):
def test_pather_path_to(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# start at (0,0) rot pi/2 (North) # start at (0,0) rot pi/2 (North)
# path "forward" (South) to y=-50 # path "forward" (South) to y=-50
p.path_to("start", ccw=None, y=-50) p.path_to("start", ccw=None, y=-50)
assert_equal(p.ports["start"].offset, [0, -50]) assert_equal(p.ports["start"].offset, [0, -50])
def test_pather_mpath(pather_setup):
def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
p.ports["A"] = Port((0, 0), pi / 2, ptype="wire") p.ports["A"] = Port((0, 0), pi / 2, ptype="wire")
p.ports["B"] = Port((10, 0), pi / 2, ptype="wire") p.ports["B"] = Port((10, 0), pi / 2, ptype="wire")
@ -64,7 +67,8 @@ def test_pather_mpath(pather_setup):
assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["A"].offset, [0, -20])
assert_equal(p.ports["B"].offset, [10, -20]) assert_equal(p.ports["B"].offset, [10, -20])
def test_pather_at_chaining(pather_setup):
def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Fluent API test # Fluent API test
p.at("start").path(ccw=None, length=10).path(ccw=True, length=10) p.at("start").path(ccw=None, length=10).path(ccw=True, length=10)
@ -77,4 +81,3 @@ def test_pather_at_chaining(pather_setup):
# pi/2 (North) + CCW (90 deg) -> 0 (East)? # pi/2 (North) + CCW (90 deg) -> 0 (East)?
# Actual behavior results in pi (West). # Actual behavior results in pi (West).
assert_allclose(p.ports["start"].rotation, pi, atol=1e-10) assert_allclose(p.ports["start"].rotation, pi, atol=1e-10)

View file

@ -1,15 +1,14 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..pattern import Pattern from ..pattern import Pattern
from ..shapes import Polygon, Circle from ..shapes import Polygon
from ..ref import Ref from ..ref import Ref
from ..ports import Port from ..ports import Port
from ..label import Label from ..label import Label
def test_pattern_init():
def test_pattern_init() -> None:
pat = Pattern() pat = Pattern()
assert pat.is_empty() assert pat.is_empty()
assert not pat.has_shapes() assert not pat.has_shapes()
@ -17,18 +16,14 @@ def test_pattern_init():
assert not pat.has_labels() assert not pat.has_labels()
assert not pat.has_ports() assert not pat.has_ports()
def test_pattern_with_elements():
def test_pattern_with_elements() -> None:
poly = Polygon.square(10) poly = Polygon.square(10)
label = Label("test", offset=(5, 5)) label = Label("test", offset=(5, 5))
ref = Ref(offset=(100, 100)) ref = Ref(offset=(100, 100))
port = Port((0, 0), 0) port = Port((0, 0), 0)
pat = Pattern( pat = Pattern(shapes={(1, 0): [poly]}, labels={(1, 2): [label]}, refs={"sub": [ref]}, ports={"P1": port})
shapes={(1, 0): [poly]},
labels={(1, 2): [label]},
refs={"sub": [ref]},
ports={"P1": port}
)
assert pat.has_shapes() assert pat.has_shapes()
assert pat.has_labels() assert pat.has_labels()
@ -40,7 +35,8 @@ def test_pattern_with_elements():
assert pat.refs["sub"] == [ref] assert pat.refs["sub"] == [ref]
assert pat.ports["P1"] == port assert pat.ports["P1"] == port
def test_pattern_append():
def test_pattern_append() -> None:
pat1 = Pattern() pat1 = Pattern()
pat1.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]]) pat1.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]])
@ -51,7 +47,8 @@ def test_pattern_append():
assert len(pat1.shapes[(1, 0)]) == 1 assert len(pat1.shapes[(1, 0)]) == 1
assert len(pat1.shapes[(2, 0)]) == 1 assert len(pat1.shapes[(2, 0)]) == 1
def test_pattern_translate():
def test_pattern_translate() -> None:
pat = Pattern() pat = Pattern()
pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]]) pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]])
pat.ports["P1"] = Port((5, 5), 0) pat.ports["P1"] = Port((5, 5), 0)
@ -62,7 +59,8 @@ def test_pattern_translate():
assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, 20]) assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, 20])
assert_equal(pat.ports["P1"].offset, [15, 25]) assert_equal(pat.ports["P1"].offset, [15, 25])
def test_pattern_scale():
def test_pattern_scale() -> None:
pat = Pattern() pat = Pattern()
# Polygon.rect sets an offset in its constructor which is immediately translated into vertices # 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.rect((1, 0), xmin=0, xmax=1, ymin=0, ymax=1)
@ -71,7 +69,8 @@ def test_pattern_scale():
# Vertices should be scaled # Vertices should be scaled
assert_equal(pat.shapes[(1, 0)][0].vertices, [[0, 0], [0, 2], [2, 2], [2, 0]]) assert_equal(pat.shapes[(1, 0)][0].vertices, [[0, 0], [0, 2], [2, 2], [2, 0]])
def test_pattern_rotate():
def test_pattern_rotate() -> None:
pat = Pattern() pat = Pattern()
pat.polygon((1, 0), vertices=[[10, 0], [11, 0], [10, 1]]) pat.polygon((1, 0), vertices=[[10, 0], [11, 0], [10, 1]])
# Rotate 90 degrees CCW around (0,0) # Rotate 90 degrees CCW around (0,0)
@ -80,7 +79,8 @@ def test_pattern_rotate():
# [10, 0] rotated 90 deg around (0,0) is [0, 10] # [10, 0] rotated 90 deg around (0,0) is [0, 10]
assert_allclose(pat.shapes[(1, 0)][0].vertices[0], [0, 10], atol=1e-10) assert_allclose(pat.shapes[(1, 0)][0].vertices[0], [0, 10], atol=1e-10)
def test_pattern_mirror():
def test_pattern_mirror() -> None:
pat = Pattern() pat = Pattern()
pat.polygon((1, 0), vertices=[[10, 5], [11, 5], [10, 6]]) pat.polygon((1, 0), vertices=[[10, 5], [11, 5], [10, 6]])
# Mirror across X axis (y -> -y) # Mirror across X axis (y -> -y)
@ -88,7 +88,8 @@ def test_pattern_mirror():
assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, -5]) assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, -5])
def test_pattern_get_bounds():
def test_pattern_get_bounds() -> None:
pat = Pattern() pat = Pattern()
pat.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10]]) pat.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10]])
pat.polygon((1, 0), vertices=[[-5, -5], [5, -5], [5, 5]]) pat.polygon((1, 0), vertices=[[-5, -5], [5, -5], [5, 5]])
@ -96,7 +97,8 @@ def test_pattern_get_bounds():
bounds = pat.get_bounds() bounds = pat.get_bounds()
assert_equal(bounds, [[-5, -5], [10, 10]]) assert_equal(bounds, [[-5, -5], [10, 10]])
def test_pattern_interface():
def test_pattern_interface() -> None:
source = Pattern() source = Pattern()
source.ports["A"] = Port((10, 20), 0, ptype="test") source.ports["A"] = Port((10, 20), 0, ptype="test")
@ -108,4 +110,3 @@ def test_pattern_interface():
assert_allclose(iface.ports["out_A"].rotation, 0, atol=1e-10) assert_allclose(iface.ports["out_A"].rotation, 0, atol=1e-10)
assert iface.ports["in_A"].ptype == "test" assert iface.ports["in_A"].ptype == "test"
assert iface.ports["out_A"].ptype == "test" assert iface.ports["out_A"].ptype == "test"

View file

@ -1,6 +1,6 @@
import pytest import pytest
import numpy import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal
from ..shapes import Polygon from ..shapes import Polygon
@ -9,29 +9,36 @@ from ..error import PatternError
@pytest.fixture @pytest.fixture
def polygon(): def polygon() -> Polygon:
return Polygon([[0, 0], [1, 0], [1, 1], [0, 1]]) return Polygon([[0, 0], [1, 0], [1, 1], [0, 1]])
def test_vertices(polygon) -> None:
def test_vertices(polygon: Polygon) -> None:
assert_equal(polygon.vertices, [[0, 0], [1, 0], [1, 1], [0, 1]]) assert_equal(polygon.vertices, [[0, 0], [1, 0], [1, 1], [0, 1]])
def test_xs(polygon) -> None:
def test_xs(polygon: Polygon) -> None:
assert_equal(polygon.xs, [0, 1, 1, 0]) assert_equal(polygon.xs, [0, 1, 1, 0])
def test_ys(polygon) -> None:
def test_ys(polygon: Polygon) -> None:
assert_equal(polygon.ys, [0, 0, 1, 1]) assert_equal(polygon.ys, [0, 0, 1, 1])
def test_offset(polygon) -> None:
def test_offset(polygon: Polygon) -> None:
assert_equal(polygon.offset, [0, 0]) assert_equal(polygon.offset, [0, 0])
def test_square() -> None: def test_square() -> None:
square = Polygon.square(1) square = Polygon.square(1)
assert_equal(square.vertices, [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]]) assert_equal(square.vertices, [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]])
def test_rectangle() -> None: def test_rectangle() -> None:
rectangle = Polygon.rectangle(1, 2) rectangle = Polygon.rectangle(1, 2)
assert_equal(rectangle.vertices, [[-0.5, -1], [-0.5, 1], [0.5, 1], [0.5, -1]]) assert_equal(rectangle.vertices, [[-0.5, -1], [-0.5, 1], [0.5, 1], [0.5, -1]])
def test_rect() -> None: def test_rect() -> None:
rect1 = Polygon.rect(xmin=0, xmax=1, ymin=-1, ymax=1) rect1 = Polygon.rect(xmin=0, xmax=1, ymin=-1, ymax=1)
assert_equal(rect1.vertices, [[0, -1], [0, 1], [1, 1], [1, -1]]) assert_equal(rect1.vertices, [[0, -1], [0, 1], [1, 1], [1, -1]])
@ -70,41 +77,49 @@ def test_octagon() -> None:
side_len = numpy.sqrt((diff * diff).sum(axis=1)) side_len = numpy.sqrt((diff * diff).sum(axis=1))
assert numpy.allclose(side_len, 1) assert numpy.allclose(side_len, 1)
def test_to_polygons(polygon) -> None:
def test_to_polygons(polygon: Polygon) -> None:
assert polygon.to_polygons() == [polygon] assert polygon.to_polygons() == [polygon]
def test_get_bounds_single(polygon) -> None:
def test_get_bounds_single(polygon: Polygon) -> None:
assert_equal(polygon.get_bounds_single(), [[0, 0], [1, 1]]) assert_equal(polygon.get_bounds_single(), [[0, 0], [1, 1]])
def test_rotate(polygon) -> None:
def test_rotate(polygon: Polygon) -> None:
rotated_polygon = polygon.rotate(R90) rotated_polygon = polygon.rotate(R90)
assert_equal(rotated_polygon.vertices, [[0, 0], [0, 1], [-1, 1], [-1, 0]]) assert_equal(rotated_polygon.vertices, [[0, 0], [0, 1], [-1, 1], [-1, 0]])
def test_mirror(polygon) -> None:
def test_mirror(polygon: Polygon) -> None:
mirrored_by_y = polygon.deepcopy().mirror(1) mirrored_by_y = polygon.deepcopy().mirror(1)
assert_equal(mirrored_by_y.vertices, [[0, 0], [-1, 0], [-1, 1], [0, 1]]) assert_equal(mirrored_by_y.vertices, [[0, 0], [-1, 0], [-1, 1], [0, 1]])
print(polygon.vertices) print(polygon.vertices)
mirrored_by_x = polygon.deepcopy().mirror(0) mirrored_by_x = polygon.deepcopy().mirror(0)
assert_equal(mirrored_by_x.vertices, [[0, 0], [1, 0], [1, -1], [0, -1]]) assert_equal(mirrored_by_x.vertices, [[0, 0], [1, 0], [1, -1], [0, -1]])
def test_scale_by(polygon) -> None:
def test_scale_by(polygon: Polygon) -> None:
scaled_polygon = polygon.scale_by(2) scaled_polygon = polygon.scale_by(2)
assert_equal(scaled_polygon.vertices, [[0, 0], [2, 0], [2, 2], [0, 2]]) assert_equal(scaled_polygon.vertices, [[0, 0], [2, 0], [2, 2], [0, 2]])
def test_clean_vertices(polygon) -> None:
def test_clean_vertices(polygon: Polygon) -> None:
polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, -4], [2, 0], [0, 0]]).clean_vertices() polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, -4], [2, 0], [0, 0]]).clean_vertices()
assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]]) assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]])
def test_remove_duplicate_vertices() -> None: def test_remove_duplicate_vertices() -> None:
polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, 0], [0, 0]]).remove_duplicate_vertices() polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, 0], [0, 0]]).remove_duplicate_vertices()
assert_equal(polygon.vertices, [[0, 0], [1, 1], [2, 2], [2, 0]]) assert_equal(polygon.vertices, [[0, 0], [1, 1], [2, 2], [2, 0]])
def test_remove_colinear_vertices() -> None: def test_remove_colinear_vertices() -> None:
polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, 0], [0, 0]]).remove_colinear_vertices() polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, 0], [0, 0]]).remove_colinear_vertices()
assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]]) assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]])
def test_vertices_dtype():
def test_vertices_dtype() -> None:
polygon = Polygon(numpy.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], dtype=numpy.int32)) polygon = Polygon(numpy.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], dtype=numpy.int32))
polygon.scale_by(0.5) polygon.scale_by(0.5)
assert_equal(polygon.vertices, [[0, 0], [0.5, 0], [0.5, 0.5], [0, 0.5], [0, 0]]) assert_equal(polygon.vertices, [[0, 0], [0.5, 0], [0.5, 0.5], [0, 0.5], [0, 0]])

View file

@ -1,18 +1,19 @@
import pytest import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..ports import Port, PortList from ..ports import Port, PortList
from ..error import PortError from ..error import PortError
def test_port_init():
def test_port_init() -> None:
p = Port(offset=(10, 20), rotation=pi / 2, ptype="test") p = Port(offset=(10, 20), rotation=pi / 2, ptype="test")
assert_equal(p.offset, [10, 20]) assert_equal(p.offset, [10, 20])
assert p.rotation == pi / 2 assert p.rotation == pi / 2
assert p.ptype == "test" assert p.ptype == "test"
def test_port_transform():
def test_port_transform() -> None:
p = Port(offset=(10, 0), rotation=0) p = Port(offset=(10, 0), rotation=0)
p.rotate_around((0, 0), pi / 2) p.rotate_around((0, 0), pi / 2)
assert_allclose(p.offset, [0, 10], atol=1e-10) assert_allclose(p.offset, [0, 10], atol=1e-10)
@ -23,14 +24,16 @@ def test_port_transform():
# rotation was pi/2 (90 deg), mirror across x (0 deg) -> -pi/2 == 3pi/2 # rotation was pi/2 (90 deg), mirror across x (0 deg) -> -pi/2 == 3pi/2
assert_allclose(p.rotation, 3 * pi / 2, atol=1e-10) assert_allclose(p.rotation, 3 * pi / 2, atol=1e-10)
def test_port_flip_across():
def test_port_flip_across() -> None:
p = Port(offset=(10, 0), rotation=0) p = Port(offset=(10, 0), rotation=0)
p.flip_across(axis=1) # Mirror across x=0: flips x-offset p.flip_across(axis=1) # Mirror across x=0: flips x-offset
assert_equal(p.offset, [-10, 0]) assert_equal(p.offset, [-10, 0])
# rotation was 0, mirrored(1) -> pi # rotation was 0, mirrored(1) -> pi
assert_allclose(p.rotation, pi, atol=1e-10) assert_allclose(p.rotation, pi, atol=1e-10)
def test_port_measure_travel():
def test_port_measure_travel() -> None:
p1 = Port((0, 0), 0) p1 = Port((0, 0), 0)
p2 = Port((10, 5), pi) # Facing each other p2 = Port((10, 5), pi) # Facing each other
@ -39,49 +42,60 @@ def test_port_measure_travel():
assert jog == 5 assert jog == 5
assert rotation == pi assert rotation == pi
def test_port_list_rename():
def test_port_list_rename() -> None:
class MyPorts(PortList): class MyPorts(PortList):
def __init__(self): def __init__(self) -> None:
self._ports = {"A": Port((0, 0), 0)} self._ports = {"A": Port((0, 0), 0)}
@property @property
def ports(self): return self._ports def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter @ports.setter
def ports(self, val): self._ports = val def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts() pl = MyPorts()
pl.rename_ports({"A": "B"}) pl.rename_ports({"A": "B"})
assert "A" not in pl.ports assert "A" not in pl.ports
assert "B" in pl.ports assert "B" in pl.ports
def test_port_list_plugged():
def test_port_list_plugged() -> None:
class MyPorts(PortList): class MyPorts(PortList):
def __init__(self): def __init__(self) -> None:
self._ports = { self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)}
"A": Port((10, 10), 0),
"B": Port((10, 10), pi)
}
@property @property
def ports(self): return self._ports def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter @ports.setter
def ports(self, val): self._ports = val def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts() pl = MyPorts()
pl.plugged({"A": "B"}) pl.plugged({"A": "B"})
assert not pl.ports # Both should be removed assert not pl.ports # Both should be removed
def test_port_list_plugged_mismatch():
def test_port_list_plugged_mismatch() -> None:
class MyPorts(PortList): class MyPorts(PortList):
def __init__(self): def __init__(self) -> None:
self._ports = { self._ports = {
"A": Port((10, 10), 0), "A": Port((10, 10), 0),
"B": Port((11, 10), pi) # Offset mismatch "B": Port((11, 10), pi), # Offset mismatch
} }
@property @property
def ports(self): return self._ports def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter @ports.setter
def ports(self, val): self._ports = val def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts() pl = MyPorts()
with pytest.raises(PortError): with pytest.raises(PortError):
pl.plugged({"A": "B"}) pl.plugged({"A": "B"})

View file

@ -1,4 +1,3 @@
import pytest
import numpy import numpy
from numpy.testing import assert_allclose from numpy.testing import assert_allclose
@ -7,7 +6,8 @@ from ..pattern import Pattern
from ..ports import Port from ..ports import Port
from ..library import Library from ..library import Library
def test_ports2data_roundtrip():
def test_ports2data_roundtrip() -> None:
pat = Pattern() pat = Pattern()
pat.ports["P1"] = Port((10, 20), numpy.pi / 2, ptype="test") pat.ports["P1"] = Port((10, 20), numpy.pi / 2, ptype="test")
@ -28,7 +28,8 @@ def test_ports2data_roundtrip():
assert_allclose(pat2.ports["P1"].rotation, numpy.pi / 2, atol=1e-10) assert_allclose(pat2.ports["P1"].rotation, numpy.pi / 2, atol=1e-10)
assert pat2.ports["P1"].ptype == "test" assert pat2.ports["P1"].ptype == "test"
def test_data_to_ports_hierarchical():
def test_data_to_ports_hierarchical() -> None:
lib = Library() lib = Library()
# Child has port data in labels # Child has port data in labels
@ -52,4 +53,3 @@ def test_data_to_ports_hierarchical():
assert "A" in parent.ports assert "A" in parent.ports
assert_allclose(parent.ports["A"].offset, [100, 105], atol=1e-10) assert_allclose(parent.ports["A"].offset, [100, 105], atol=1e-10)
assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10) assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10)

View file

@ -1,5 +1,3 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
@ -7,14 +5,16 @@ from ..pattern import Pattern
from ..ref import Ref from ..ref import Ref
from ..repetition import Grid from ..repetition import Grid
def test_ref_init():
def test_ref_init() -> None:
ref = Ref(offset=(10, 20), rotation=pi / 4, mirrored=True, scale=2.0) ref = Ref(offset=(10, 20), rotation=pi / 4, mirrored=True, scale=2.0)
assert_equal(ref.offset, [10, 20]) assert_equal(ref.offset, [10, 20])
assert ref.rotation == pi / 4 assert ref.rotation == pi / 4
assert ref.mirrored is True assert ref.mirrored is True
assert ref.scale == 2.0 assert ref.scale == 2.0
def test_ref_as_pattern():
def test_ref_as_pattern() -> None:
sub_pat = Pattern() sub_pat = Pattern()
sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
@ -30,7 +30,8 @@ def test_ref_as_pattern():
assert_allclose(shape.vertices, [[10, 10], [10, 12], [8, 10]], atol=1e-10) assert_allclose(shape.vertices, [[10, 10], [10, 12], [8, 10]], atol=1e-10)
def test_ref_with_repetition():
def test_ref_with_repetition() -> None:
sub_pat = Pattern() sub_pat = Pattern()
sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
@ -44,7 +45,8 @@ def test_ref_with_repetition():
first_verts = sorted([tuple(s.vertices[0]) for s in repeated_pat.shapes[(1, 0)]]) first_verts = sorted([tuple(s.vertices[0]) for s in repeated_pat.shapes[(1, 0)]])
assert first_verts == [(0.0, 0.0), (0.0, 10.0), (10.0, 0.0), (10.0, 10.0)] assert first_verts == [(0.0, 0.0), (0.0, 10.0), (10.0, 0.0), (10.0, 10.0)]
def test_ref_get_bounds():
def test_ref_get_bounds() -> None:
sub_pat = Pattern() sub_pat = Pattern()
sub_pat.polygon((1, 0), vertices=[[0, 0], [5, 0], [0, 5]]) sub_pat.polygon((1, 0), vertices=[[0, 0], [5, 0], [0, 5]])
@ -55,7 +57,8 @@ def test_ref_get_bounds():
# translated [[10,10], [20,20]] # translated [[10,10], [20,20]]
assert_equal(bounds, [[10, 10], [20, 20]]) assert_equal(bounds, [[10, 10], [20, 20]])
def test_ref_copy():
def test_ref_copy() -> None:
ref1 = Ref(offset=(1, 2), rotation=0.5, annotations={"a": [1]}) ref1 = Ref(offset=(1, 2), rotation=0.5, annotations={"a": [1]})
ref2 = ref1.copy() ref2 = ref1.copy()
assert ref1 == ref2 assert ref1 == ref2
@ -63,4 +66,3 @@ def test_ref_copy():
ref2.offset[0] = 100 ref2.offset[0] = 100
assert ref1.offset[0] == 1 assert ref1.offset[0] == 1

View file

@ -1,6 +1,5 @@
import pytest import pytest
import numpy from numpy.testing import assert_allclose
from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..builder import RenderPather from ..builder import RenderPather
@ -8,15 +7,17 @@ from ..builder.tools import PathTool
from ..library import Library from ..library import Library
from ..ports import Port from ..ports import Port
@pytest.fixture @pytest.fixture
def rpather_setup(): def rpather_setup() -> tuple[RenderPather, PathTool, Library]:
lib = Library() lib = Library()
tool = PathTool(layer=(1, 0), width=2, ptype="wire") tool = PathTool(layer=(1, 0), width=2, ptype="wire")
rp = RenderPather(lib, tools=tool) rp = RenderPather(lib, tools=tool)
rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire") rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
return rp, tool, lib return rp, tool, lib
def test_renderpather_basic(rpather_setup):
def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup rp, tool, lib = rpather_setup
# Plan two segments # Plan two segments
rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10) rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10)
@ -40,7 +41,8 @@ def test_renderpather_basic(rpather_setup):
assert len(path_shape.vertices) == 3 assert len(path_shape.vertices) == 3
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)
def test_renderpather_bend(rpather_setup):
def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup rp, tool, lib = rpather_setup
# Plan straight then bend # Plan straight then bend
rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10) rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10)
@ -58,7 +60,8 @@ def test_renderpather_bend(rpather_setup):
assert len(path_shape.vertices) == 4 assert len(path_shape.vertices) == 4
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20], [-1, -20]], atol=1e-10) assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20], [-1, -20]], atol=1e-10)
def test_renderpather_retool(rpather_setup):
def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool1, lib = rpather_setup rp, tool1, lib = rpather_setup
tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") tool2 = PathTool(layer=(2, 0), width=4, ptype="wire")
@ -70,4 +73,3 @@ def test_renderpather_retool(rpather_setup):
# Different tools should cause different batches/shapes # Different tools should cause different batches/shapes
assert len(rp.pattern.shapes[(1, 0)]) == 1 assert len(rp.pattern.shapes[(1, 0)]) == 1
assert len(rp.pattern.shapes[(2, 0)]) == 1 assert len(rp.pattern.shapes[(2, 0)]) == 1

View file

@ -1,32 +1,35 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..repetition import Grid, Arbitrary from ..repetition import Grid, Arbitrary
def test_grid_displacements():
def test_grid_displacements() -> None:
# 2x2 grid # 2x2 grid
grid = Grid(a_vector=(10, 0), b_vector=(0, 5), a_count=2, b_count=2) grid = Grid(a_vector=(10, 0), b_vector=(0, 5), a_count=2, b_count=2)
disps = sorted([tuple(d) for d in grid.displacements]) disps = sorted([tuple(d) for d in grid.displacements])
assert disps == [(0.0, 0.0), (0.0, 5.0), (10.0, 0.0), (10.0, 5.0)] assert disps == [(0.0, 0.0), (0.0, 5.0), (10.0, 0.0), (10.0, 5.0)]
def test_grid_1d():
def test_grid_1d() -> None:
grid = Grid(a_vector=(10, 0), a_count=3) grid = Grid(a_vector=(10, 0), a_count=3)
disps = sorted([tuple(d) for d in grid.displacements]) disps = sorted([tuple(d) for d in grid.displacements])
assert disps == [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)] assert disps == [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)]
def test_grid_rotate():
def test_grid_rotate() -> None:
grid = Grid(a_vector=(10, 0), a_count=2) grid = Grid(a_vector=(10, 0), a_count=2)
grid.rotate(pi / 2) grid.rotate(pi / 2)
assert_allclose(grid.a_vector, [0, 10], atol=1e-10) assert_allclose(grid.a_vector, [0, 10], atol=1e-10)
def test_grid_get_bounds():
def test_grid_get_bounds() -> None:
grid = Grid(a_vector=(10, 0), b_vector=(0, 5), a_count=2, b_count=2) grid = Grid(a_vector=(10, 0), b_vector=(0, 5), a_count=2, b_count=2)
bounds = grid.get_bounds() bounds = grid.get_bounds()
assert_equal(bounds, [[0, 0], [10, 5]]) assert_equal(bounds, [[0, 0], [10, 5]])
def test_arbitrary_displacements():
def test_arbitrary_displacements() -> None:
pts = [[0, 0], [10, 20], [-5, 30]] pts = [[0, 0], [10, 20], [-5, 30]]
arb = Arbitrary(pts) arb = Arbitrary(pts)
# They should be sorted by displacements.setter # They should be sorted by displacements.setter
@ -36,7 +39,8 @@ def test_arbitrary_displacements():
assert any((disps == [10, 20]).all(axis=1)) assert any((disps == [10, 20]).all(axis=1))
assert any((disps == [-5, 30]).all(axis=1)) assert any((disps == [-5, 30]).all(axis=1))
def test_arbitrary_transform():
def test_arbitrary_transform() -> None:
arb = Arbitrary([[10, 0]]) arb = Arbitrary([[10, 0]])
arb.rotate(pi / 2) arb.rotate(pi / 2)
assert_allclose(arb.displacements, [[0, 10]], atol=1e-10) assert_allclose(arb.displacements, [[0, 10]], atol=1e-10)
@ -45,4 +49,3 @@ def test_arbitrary_transform():
# self.displacements[:, 1 - axis] *= -1 # self.displacements[:, 1 - axis] *= -1
# if axis=0, 1-axis=1, so y *= -1 # if axis=0, 1-axis=1, so y *= -1
assert_allclose(arb.displacements, [[0, -10]], atol=1e-10) assert_allclose(arb.displacements, [[0, -10]], atol=1e-10)

View file

@ -1,16 +1,17 @@
from pathlib import Path
import pytest import pytest
import numpy import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
import os
from ..shapes import Arc, Ellipse, Circle, Polygon, Path, Text, PolyCollection from ..shapes import Arc, Ellipse, Circle, Polygon, Path as MPath, Text, PolyCollection
from ..error import PatternError from ..error import PatternError
# 1. Text shape tests # 1. Text shape tests
def test_text_to_polygons(): def test_text_to_polygons() -> None:
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf" font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf"
if not os.path.exists(font_path): if not Path(font_path).exists():
pytest.skip("Font file not found") pytest.skip("Font file not found")
t = Text("Hi", height=10, font_path=font_path) t = Text("Hi", height=10, font_path=font_path)
@ -24,8 +25,9 @@ def test_text_to_polygons():
char_x_means = [p.vertices[:, 0].mean() for p in polys] char_x_means = [p.vertices[:, 0].mean() for p in polys]
assert len(set(char_x_means)) >= 2 assert len(set(char_x_means)) >= 2
# 2. Manhattanization tests # 2. Manhattanization tests
def test_manhattanize(): def test_manhattanize() -> None:
# Diamond shape # Diamond shape
poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]]) poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]])
grid = numpy.arange(0, 11, 1) grid = numpy.arange(0, 11, 1)
@ -38,8 +40,9 @@ def test_manhattanize():
# For each segment, either dx or dy must be zero # For each segment, either dx or dy must be zero
assert numpy.all((dv[:, 0] == 0) | (dv[:, 1] == 0)) assert numpy.all((dv[:, 0] == 0) | (dv[:, 1] == 0))
# 3. Comparison and Sorting tests # 3. Comparison and Sorting tests
def test_shape_comparisons(): def test_shape_comparisons() -> None:
c1 = Circle(radius=10) c1 = Circle(radius=10)
c2 = Circle(radius=20) c2 = Circle(radius=20)
assert c1 < c2 assert c1 < c2
@ -53,24 +56,27 @@ def test_shape_comparisons():
assert c1 < p1 or p1 < c1 assert c1 < p1 or p1 < c1
assert (c1 < p1) != (p1 < c1) assert (c1 < p1) != (p1 < c1)
# 4. Arc/Path Edge Cases # 4. Arc/Path Edge Cases
def test_arc_edge_cases(): def test_arc_edge_cases() -> None:
# Wrapped arc (> 360 deg) # Wrapped arc (> 360 deg)
a = Arc(radii=(10, 10), angles=(0, 3 * pi), width=2) a = Arc(radii=(10, 10), angles=(0, 3 * pi), width=2)
polys = a.to_polygons(num_vertices=64) a.to_polygons(num_vertices=64)
# Should basically be a ring # Should basically be a ring
bounds = a.get_bounds_single() bounds = a.get_bounds_single()
assert_allclose(bounds, [[-11, -11], [11, 11]], atol=1e-10) assert_allclose(bounds, [[-11, -11], [11, 11]], atol=1e-10)
def test_path_edge_cases():
def test_path_edge_cases() -> None:
# Zero-length segments # Zero-length segments
p = Path(vertices=[[0, 0], [0, 0], [10, 0]], width=2) p = MPath(vertices=[[0, 0], [0, 0], [10, 0]], width=2)
polys = p.to_polygons() polys = p.to_polygons()
assert len(polys) == 1 assert len(polys) == 1
assert_equal(polys[0].get_bounds_single(), [[0, -1], [10, 1]]) assert_equal(polys[0].get_bounds_single(), [[0, -1], [10, 1]])
# 5. PolyCollection with holes # 5. PolyCollection with holes
def test_poly_collection_holes(): def test_poly_collection_holes() -> None:
# Outer square, inner square hole # Outer square, inner square hole
# PolyCollection doesn't explicitly support holes, but its constituents (Polygons) do? # PolyCollection doesn't explicitly support holes, but its constituents (Polygons) do?
# wait, Polygon in masque is just a boundary. Holes are usually handled by having multiple # wait, Polygon in masque is just a boundary. Holes are usually handled by having multiple
@ -80,8 +86,14 @@ def test_poly_collection_holes():
# Let's test PolyCollection with multiple polygons # Let's test PolyCollection with multiple polygons
verts = [ verts = [
[0, 0], [10, 0], [10, 10], [0, 10], # Poly 1 [0, 0],
[2, 2], [2, 8], [8, 8], [8, 2] # Poly 2 [10, 0],
[10, 10],
[0, 10], # Poly 1
[2, 2],
[2, 8],
[8, 8],
[8, 2], # Poly 2
] ]
offsets = [0, 4] offsets = [0, 4]
pc = PolyCollection(verts, offsets) pc = PolyCollection(verts, offsets)
@ -90,15 +102,21 @@ def test_poly_collection_holes():
assert_equal(polys[0].vertices, [[0, 0], [10, 0], [10, 10], [0, 10]]) assert_equal(polys[0].vertices, [[0, 0], [10, 0], [10, 10], [0, 10]])
assert_equal(polys[1].vertices, [[2, 2], [2, 8], [8, 8], [8, 2]]) assert_equal(polys[1].vertices, [[2, 2], [2, 8], [8, 8], [8, 2]])
def test_poly_collection_constituent_empty():
def test_poly_collection_constituent_empty() -> None:
# One real triangle, one "empty" polygon (0 vertices), one real square # One real triangle, one "empty" polygon (0 vertices), one real square
# Note: Polygon requires 3 vertices, so "empty" here might mean just some junk # Note: Polygon requires 3 vertices, so "empty" here might mean just some junk
# that to_polygons should handle. # that to_polygons should handle.
# Actually PolyCollection doesn't check vertex count per polygon. # Actually PolyCollection doesn't check vertex count per polygon.
verts = [ verts = [
[0, 0], [1, 0], [0, 1], # Tri [0, 0],
[1, 0],
[0, 1], # Tri
# Empty space # Empty space
[10, 10], [11, 10], [11, 11], [10, 11] # Square [10, 10],
[11, 10],
[11, 11],
[10, 11], # Square
] ]
offsets = [0, 3, 3] # Index 3 is start of "empty", Index 3 is also start of Square? offsets = [0, 3, 3] # Index 3 is start of "empty", Index 3 is also start of Square?
# No, offsets should be strictly increasing or handle 0-length slices. # No, offsets should be strictly increasing or handle 0-length slices.
@ -113,22 +131,14 @@ def test_poly_collection_constituent_empty():
with pytest.raises(PatternError): with pytest.raises(PatternError):
pc.to_polygons() pc.to_polygons()
def test_poly_collection_valid():
verts = [ def test_poly_collection_valid() -> None:
[0, 0], [1, 0], [0, 1], verts = [[0, 0], [1, 0], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]]
[10, 10], [11, 10], [11, 11], [10, 11]
]
offsets = [0, 3] offsets = [0, 3]
pc = PolyCollection(verts, offsets) pc = PolyCollection(verts, offsets)
assert len(pc.to_polygons()) == 2 assert len(pc.to_polygons()) == 2
shapes = [ shapes = [Circle(radius=20), Circle(radius=10), Polygon([[0, 0], [10, 0], [10, 10]]), Ellipse(radii=(5, 5))]
Circle(radius=20),
Circle(radius=10),
Polygon([[0, 0], [10, 0], [10, 10]]),
Ellipse(radii=(5, 5))
]
sorted_shapes = sorted(shapes) sorted_shapes = sorted(shapes)
assert len(sorted_shapes) == 4 assert len(sorted_shapes) == 4
# Just verify it doesn't crash and is stable # Just verify it doesn't crash and is stable
assert sorted(sorted_shapes) == sorted_shapes assert sorted(sorted_shapes) == sorted_shapes

View file

@ -1,11 +1,11 @@
import pytest
import numpy import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..shapes import Arc, Ellipse, Circle, Polygon, PolyCollection from ..shapes import Arc, Ellipse, Circle, Polygon, PolyCollection
def test_poly_collection_init():
def test_poly_collection_init() -> None:
# Two squares: [[0,0], [1,0], [1,1], [0,1]] and [[10,10], [11,10], [11,11], [10,11]] # Two squares: [[0,0], [1,0], [1,1], [0,1]] and [[10,10], [11,10], [11,11], [10,11]]
verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]] verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]]
offsets = [0, 4] offsets = [0, 4]
@ -13,7 +13,8 @@ def test_poly_collection_init():
assert len(list(pc.polygon_vertices)) == 2 assert len(list(pc.polygon_vertices)) == 2
assert_equal(pc.get_bounds_single(), [[0, 0], [11, 11]]) assert_equal(pc.get_bounds_single(), [[0, 0], [11, 11]])
def test_poly_collection_to_polygons():
def test_poly_collection_to_polygons() -> None:
verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]] verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]]
offsets = [0, 4] offsets = [0, 4]
pc = PolyCollection(vertex_lists=verts, vertex_offsets=offsets) pc = PolyCollection(vertex_lists=verts, vertex_offsets=offsets)
@ -22,12 +23,14 @@ def test_poly_collection_to_polygons():
assert_equal(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]]) assert_equal(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]])
assert_equal(polys[1].vertices, [[10, 10], [11, 10], [11, 11], [10, 11]]) assert_equal(polys[1].vertices, [[10, 10], [11, 10], [11, 11], [10, 11]])
def test_circle_init():
def test_circle_init() -> None:
c = Circle(radius=10, offset=(5, 5)) c = Circle(radius=10, offset=(5, 5))
assert c.radius == 10 assert c.radius == 10
assert_equal(c.offset, [5, 5]) assert_equal(c.offset, [5, 5])
def test_circle_to_polygons():
def test_circle_to_polygons() -> None:
c = Circle(radius=10) c = Circle(radius=10)
polys = c.to_polygons(num_vertices=32) polys = c.to_polygons(num_vertices=32)
assert len(polys) == 1 assert len(polys) == 1
@ -36,26 +39,30 @@ def test_circle_to_polygons():
bounds = polys[0].get_bounds_single() bounds = polys[0].get_bounds_single()
assert_allclose(bounds, [[-10, -10], [10, 10]], atol=1e-10) assert_allclose(bounds, [[-10, -10], [10, 10]], atol=1e-10)
def test_ellipse_init():
def test_ellipse_init() -> None:
e = Ellipse(radii=(10, 5), offset=(1, 2), rotation=pi / 4) e = Ellipse(radii=(10, 5), offset=(1, 2), rotation=pi / 4)
assert_equal(e.radii, [10, 5]) assert_equal(e.radii, [10, 5])
assert_equal(e.offset, [1, 2]) assert_equal(e.offset, [1, 2])
assert e.rotation == pi / 4 assert e.rotation == pi / 4
def test_ellipse_to_polygons():
def test_ellipse_to_polygons() -> None:
e = Ellipse(radii=(10, 5)) e = Ellipse(radii=(10, 5))
polys = e.to_polygons(num_vertices=64) polys = e.to_polygons(num_vertices=64)
assert len(polys) == 1 assert len(polys) == 1
bounds = polys[0].get_bounds_single() bounds = polys[0].get_bounds_single()
assert_allclose(bounds, [[-10, -5], [10, 5]], atol=1e-10) assert_allclose(bounds, [[-10, -5], [10, 5]], atol=1e-10)
def test_arc_init():
def test_arc_init() -> None:
a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2, offset=(0, 0)) a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2, offset=(0, 0))
assert_equal(a.radii, [10, 10]) assert_equal(a.radii, [10, 10])
assert_equal(a.angles, [0, pi / 2]) assert_equal(a.angles, [0, pi / 2])
assert a.width == 2 assert a.width == 2
def test_arc_to_polygons():
def test_arc_to_polygons() -> None:
# Quarter circle arc # Quarter circle arc
a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2) a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2)
polys = a.to_polygons(num_vertices=32) polys = a.to_polygons(num_vertices=32)
@ -70,7 +77,8 @@ def test_arc_to_polygons():
# So x ranges from 0 to 11, y ranges from 0 to 11. # So x ranges from 0 to 11, y ranges from 0 to 11.
assert_allclose(bounds, [[0, 0], [11, 11]], atol=1e-10) assert_allclose(bounds, [[0, 0], [11, 11]], atol=1e-10)
def test_shape_mirror():
def test_shape_mirror() -> None:
e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4) e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4)
e.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset e.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset
assert_equal(e.offset, [10, 20]) assert_equal(e.offset, [10, 20])
@ -83,7 +91,8 @@ def test_shape_mirror():
# For Arc, mirror(0) negates rotation and angles # For Arc, mirror(0) negates rotation and angles
assert_allclose(a.angles, [0, -pi / 4], atol=1e-10) assert_allclose(a.angles, [0, -pi / 4], atol=1e-10)
def test_shape_flip_across():
def test_shape_flip_across() -> None:
e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4) e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4)
e.flip_across(axis=0) # Mirror across y=0: flips y-offset e.flip_across(axis=0) # Mirror across y=0: flips y-offset
assert_equal(e.offset, [10, -20]) assert_equal(e.offset, [10, -20])
@ -95,7 +104,8 @@ def test_shape_flip_across():
# y=20 mirrored across y=10 -> y=0 # y=20 mirrored across y=10 -> y=0
assert_equal(e.offset, [10, 0]) assert_equal(e.offset, [10, 0])
def test_shape_scale():
def test_shape_scale() -> None:
e = Ellipse(radii=(10, 5)) e = Ellipse(radii=(10, 5))
e.scale_by(2) e.scale_by(2)
assert_equal(e.radii, [20, 10]) assert_equal(e.radii, [20, 10])
@ -105,7 +115,8 @@ def test_shape_scale():
assert_equal(a.radii, [5, 2.5]) assert_equal(a.radii, [5, 2.5])
assert a.width == 1 assert a.width == 1
def test_shape_arclen():
def test_shape_arclen() -> None:
# Test that max_arclen correctly limits segment lengths # Test that max_arclen correctly limits segment lengths
# Ellipse # Ellipse
@ -129,4 +140,3 @@ def test_shape_arclen():
# Let's just check that all segment lengths are within limit # Let's just check that all segment lengths are within limit
dist = numpy.sqrt(numpy.sum(numpy.diff(v, axis=0, append=v[:1]) ** 2, axis=1)) dist = numpy.sqrt(numpy.sum(numpy.diff(v, axis=0, append=v[:1]) ** 2, axis=1))
assert numpy.all(dist <= 2.000001) assert numpy.all(dist <= 2.000001)

View file

@ -1,17 +1,11 @@
import pytest
import numpy import numpy
from numpy.testing import assert_equal, assert_allclose from numpy.testing import assert_equal, assert_allclose
from numpy import pi from numpy import pi
from ..utils import ( from ..utils import remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points, rotation_matrix_2d, apply_transforms
remove_duplicate_vertices,
remove_colinear_vertices,
poly_contains_points,
rotation_matrix_2d,
apply_transforms
)
def test_remove_duplicate_vertices():
def test_remove_duplicate_vertices() -> None:
# Closed path (default) # Closed path (default)
v = [[0, 0], [1, 1], [1, 1], [2, 2], [0, 0]] v = [[0, 0], [1, 1], [1, 1], [2, 2], [0, 0]]
v_clean = remove_duplicate_vertices(v, closed_path=True) v_clean = remove_duplicate_vertices(v, closed_path=True)
@ -22,7 +16,8 @@ def test_remove_duplicate_vertices():
v_clean_open = remove_duplicate_vertices(v, closed_path=False) v_clean_open = remove_duplicate_vertices(v, closed_path=False)
assert_equal(v_clean_open, [[0, 0], [1, 1], [2, 2], [0, 0]]) assert_equal(v_clean_open, [[0, 0], [1, 1], [2, 2], [0, 0]])
def test_remove_colinear_vertices():
def test_remove_colinear_vertices() -> None:
v = [[0, 0], [1, 0], [2, 0], [2, 1], [2, 2], [1, 1], [0, 0]] v = [[0, 0], [1, 0], [2, 0], [2, 1], [2, 2], [1, 1], [0, 0]]
v_clean = remove_colinear_vertices(v, closed_path=True) v_clean = remove_colinear_vertices(v, closed_path=True)
# [1, 0] is between [0, 0] and [2, 0] # [1, 0] is between [0, 0] and [2, 0]
@ -30,7 +25,8 @@ def test_remove_colinear_vertices():
# [1, 1] is between [2, 2] and [0, 0] # [1, 1] is between [2, 2] and [0, 0]
assert_equal(v_clean, [[0, 0], [2, 0], [2, 2]]) assert_equal(v_clean, [[0, 0], [2, 0], [2, 2]])
def test_remove_colinear_vertices_exhaustive():
def test_remove_colinear_vertices_exhaustive() -> None:
# U-turn # U-turn
v = [[0, 0], [10, 0], [0, 0]] v = [[0, 0], [10, 0], [0, 0]]
v_clean = remove_colinear_vertices(v, closed_path=False) v_clean = remove_colinear_vertices(v, closed_path=False)
@ -43,30 +39,35 @@ def test_remove_colinear_vertices_exhaustive():
v_clean = remove_colinear_vertices(v, closed_path=True) v_clean = remove_colinear_vertices(v, closed_path=True)
assert len(v_clean) == 2 assert len(v_clean) == 2
def test_poly_contains_points():
def test_poly_contains_points() -> None:
v = [[0, 0], [10, 0], [10, 10], [0, 10]] v = [[0, 0], [10, 0], [10, 10], [0, 10]]
pts = [[5, 5], [-1, -1], [10, 10], [11, 5]] pts = [[5, 5], [-1, -1], [10, 10], [11, 5]]
inside = poly_contains_points(v, pts) inside = poly_contains_points(v, pts)
assert_equal(inside, [True, False, True, False]) assert_equal(inside, [True, False, True, False])
def test_rotation_matrix_2d():
def test_rotation_matrix_2d() -> None:
m = rotation_matrix_2d(pi / 2) m = rotation_matrix_2d(pi / 2)
assert_allclose(m, [[0, -1], [1, 0]], atol=1e-10) assert_allclose(m, [[0, -1], [1, 0]], atol=1e-10)
def test_rotation_matrix_non_manhattan():
def test_rotation_matrix_non_manhattan() -> None:
# 45 degrees # 45 degrees
m = rotation_matrix_2d(pi / 4) m = rotation_matrix_2d(pi / 4)
s = numpy.sqrt(2) / 2 s = numpy.sqrt(2) / 2
assert_allclose(m, [[s, -s], [s, s]], atol=1e-10) assert_allclose(m, [[s, -s], [s, s]], atol=1e-10)
def test_apply_transforms():
def test_apply_transforms() -> None:
# cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] # cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
t1 = [10, 20, 0, 0] t1 = [10, 20, 0, 0]
t2 = [[5, 0, 0, 0], [0, 5, 0, 0]] t2 = [[5, 0, 0, 0], [0, 5, 0, 0]]
combined = apply_transforms(t1, t2) combined = apply_transforms(t1, t2)
assert_equal(combined, [[15, 20, 0, 0], [10, 25, 0, 0]]) assert_equal(combined, [[15, 20, 0, 0], [10, 25, 0, 0]])
def test_apply_transforms_advanced():
def test_apply_transforms_advanced() -> None:
# Ox4: (x, y, rot, mir) # Ox4: (x, y, rot, mir)
# Outer: mirror x (axis 0), then rotate 90 deg CCW # Outer: mirror x (axis 0), then rotate 90 deg CCW
# apply_transforms logic for mirror uses y *= -1 (which is axis 0 mirror) # apply_transforms logic for mirror uses y *= -1 (which is axis 0 mirror)
@ -80,4 +81,3 @@ def test_apply_transforms_advanced():
# 2. rotate by outer rotation (pi/2): (10, 0) -> (0, 10) # 2. rotate by outer rotation (pi/2): (10, 0) -> (0, 10)
# 3. add outer offset (0, 0) -> (0, 10) # 3. add outer offset (0, 0) -> (0, 10)
assert_allclose(combined[0], [0, 10, pi / 2, 1], atol=1e-10) assert_allclose(combined[0], [0, 10, pi / 2, 1], atol=1e-10)