[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
"""
# ruff: noqa: ARG001
from typing import Any
import numpy
from numpy.typing import NDArray
import pytest # type: ignore
FixtureRequest = Any
PRNG = numpy.random.RandomState(12345)

View file

@ -1,37 +1,38 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy.testing import assert_allclose
from numpy import pi
from ..abstract import Abstract
from ..ports import Port
from ..ref import Ref
def test_abstract_init():
def test_abstract_init() -> None:
ports = {"A": Port((0, 0), 0), "B": Port((10, 0), pi)}
abs_obj = Abstract("test", ports)
assert abs_obj.name == "test"
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)})
# Rotate 90 deg around (0,0)
abs_obj.rotate_around((0, 0), pi/2)
abs_obj.rotate_around((0, 0), pi / 2)
# (10, 0) rot 0 -> (0, 10) rot pi/2
assert_allclose(abs_obj.ports["A"].offset, [0, 10], 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)
# Mirror across x axis (axis 0): flips y-offset
abs_obj.mirror(0)
# (0, 10) mirrored(0) -> (0, -10)
# rotation pi/2 mirrored(0) -> -pi/2 == 3pi/2
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)})
ref = Ref(offset=(100, 100), rotation=pi/2, mirrored=True)
ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True)
# Apply ref transform
abs_obj.apply_ref_transform(ref)
@ -47,11 +48,12 @@ def test_abstract_ref_transform():
# (0, 10) -> (100, 110)
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():
abs_obj = Abstract("test", {"A": Port((100, 110), pi/2)})
ref = Ref(offset=(100, 100), rotation=pi/2, mirrored=True)
def test_abstract_undo_transform() -> None:
abs_obj = Abstract("test", {"A": Port((100, 110), pi / 2)})
ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True)
abs_obj.undo_ref_transform(ref)
assert_allclose(abs_obj.ports["A"].offset, [10, 0], atol=1e-10)

View file

@ -1,6 +1,5 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy.testing import assert_equal
from numpy import pi
from ..builder import Pather
@ -8,18 +7,20 @@ from ..builder.tools import PathTool
from ..library import Library
from ..ports import Port
@pytest.fixture
def advanced_pather():
def advanced_pather() -> tuple[Pather, PathTool, Library]:
lib = Library()
# Simple PathTool: 2um width on layer (1,0)
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
p = Pather(lib, tools=tool)
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
# 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)
# Forward (+pi relative to port) is West (-x).
# Put destination at (-20, 0) pointing East (pi).
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
@ -31,7 +32,8 @@ def test_path_into_straight(advanced_pather):
# Pather.path adds a Reference to the generated pattern
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
# Source at (0,0) rot 0 (facing East). Forward is West (-x).
p.ports["src"] = Port((0, 0), 0, ptype="wire")
@ -39,7 +41,7 @@ def test_path_into_bend(advanced_pather):
# Wait, src forward is -x. dst is at -20, -20.
# To use a single bend, dst should be at some -x, -y and its rotation should be 3pi/2 (facing South).
# Forward for South is North (+y).
p.ports["dst"] = Port((-20, -20), 3*pi/2, ptype="wire")
p.ports["dst"] = Port((-20, -20), 3 * pi / 2, ptype="wire")
p.path_into("src", "dst")
@ -48,18 +50,20 @@ def test_path_into_bend(advanced_pather):
# Single bend should result in 2 segments (one for x move, one for y move)
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
# Facing but offset ports
p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x)
p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi)
p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x)
p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi)
p.path_into("src", "dst")
assert "src" 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.ports["src"] = Port((0, 0), 0, 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 "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.ports["src"] = Port((0, 0), 0, ptype="wire")
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")

View file

@ -1,6 +1,5 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy.testing import assert_allclose
from numpy import pi
from ..builder import Pather
@ -8,26 +7,27 @@ from ..builder.tools import AutoTool
from ..library import Library
from ..pattern import Pattern
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.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width)
pat.ports["in"] = Port((0, 0), 0, ptype=ptype)
pat.ports["out"] = Port((length, 0), pi, ptype=ptype)
return pat
@pytest.fixture
def autotool_setup():
def autotool_setup() -> tuple[Pather, AutoTool, Library]:
lib = Library()
# Define a simple bend
bend_pat = Pattern()
# 2x2 bend from (0,0) rot 0 to (2, -2) rot pi/2 (Clockwise)
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
bend_abs = lib.abstract("bend")
lib.abstract("bend")
# Define a transition (e.g., via)
via_pat = Pattern()
@ -37,14 +37,13 @@ def autotool_setup():
via_abs = lib.abstract("via")
tool_m1 = AutoTool(
straights=[AutoTool.Straight(ptype="wire_m1", fn=lambda l: make_straight(l, ptype="wire_m1"),
in_port_name="in", out_port_name="out")],
straights=[
AutoTool.Straight(ptype="wire_m1", fn=lambda length: make_straight(length, ptype="wire_m1"), in_port_name="in", out_port_name="out")
],
bends=[],
sbends=[],
transitions={
("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1")
},
default_out_ptype="wire_m1"
transitions={("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1")},
default_out_ptype="wire_m1",
)
p = Pather(lib, tools=tool_m1)
@ -53,7 +52,8 @@ def autotool_setup():
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
# 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 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 import pi
@ -8,13 +6,15 @@ from ..library import Library
from ..pattern import Pattern
from ..ports import Port
def test_builder_init():
def test_builder_init() -> None:
lib = Library()
b = Builder(lib, name="mypat")
assert b.pattern is lib["mypat"]
assert b.library is lib
def test_builder_place():
def test_builder_place() -> None:
lib = Library()
child = Pattern()
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 "child" in b.pattern.refs
def test_builder_plug():
def test_builder_plug() -> None:
lib = Library()
wire = Pattern()
@ -51,7 +52,8 @@ def test_builder_plug():
assert_equal(b.ports["start"].offset, [90, 100])
assert_allclose(b.ports["start"].rotation, 0, atol=1e-10)
def test_builder_interface():
def test_builder_interface() -> None:
lib = Library()
source = Pattern()
source.ports["P1"] = Port((0, 0), 0)
@ -62,7 +64,8 @@ def test_builder_interface():
assert "P1" in b.ports
assert b.pattern is lib["iface"]
def test_builder_set_dead():
def test_builder_set_dead() -> None:
lib = Library()
lib["sub"] = Pattern()
b = Builder(lib)
@ -70,4 +73,3 @@ def test_builder_set_dead():
b.place("sub")
assert not b.pattern.has_refs()

View file

@ -3,23 +3,22 @@
import dataclasses
import pytest # type: ignore
import pytest # type: ignore
import numpy
from numpy import pi
from numpy.typing import NDArray
#from numpy.testing import assert_allclose, assert_array_equal
# from numpy.testing import assert_allclose, assert_array_equal
from .. import Pattern, Arc, Circle
def test_circle_mirror():
cc = Circle(radius=4, offset=(10, 20))
cc.flip_across(axis=0) # flip across y=0
cc.flip_across(axis=0) # flip across y=0
assert cc.offset[0] == 10
assert cc.offset[1] == -20
assert cc.radius == 4
cc.flip_across(axis=1) # flip across x=0
cc.flip_across(axis=1) # flip across x=0
assert cc.offset[0] == -10
assert cc.offset[1] == -20
assert cc.radius == 4

View file

@ -1,15 +1,14 @@
import pytest
import os
from pathlib import Path
import numpy
from numpy.testing import assert_equal, assert_allclose
from pathlib import Path
from ..pattern import Pattern
from ..library import Library
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()
# Simple polygon cell
@ -24,7 +23,7 @@ def test_gdsii_roundtrip(tmp_path):
# Cell with Ref
pat3 = Pattern()
pat3.ref("poly_cell", offset=(50, 50), rotation=numpy.pi/2)
pat3.ref("poly_cell", offset=(50, 50), rotation=numpy.pi / 2)
lib["ref_cell"] = pat3
gds_file = tmp_path / "test.gds"
@ -52,9 +51,10 @@ def test_gdsii_roundtrip(tmp_path):
# Check Ref
read_ref = read_lib["ref_cell"].refs["poly_cell"][0]
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()
pat = Pattern()
# 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_ann = read_lib["cell"].shapes[(1, 0)][0].annotations
assert read_ann["1"] == ["hello"]

View file

@ -1,36 +1,39 @@
import pytest
import numpy
import copy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from ..label import Label
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():
l = Label("test", offset=(10, 0))
def test_label_init() -> None:
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)
l.rotate_around((0, 0), pi/2)
assert_allclose(l.offset, [0, 10], atol=1e-10)
lbl.rotate_around((0, 0), pi / 2)
assert_allclose(lbl.offset, [0, 10], atol=1e-10)
# Translate
l.translate((5, 5))
assert_allclose(l.offset, [5, 15], atol=1e-10)
lbl.translate((5, 5))
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)
l = Label("rep", offset=(0, 0), repetition=rep)
assert l.repetition is rep
assert_equal(l.get_bounds_single(), [[0, 0], [0, 0]])
lbl = Label("rep", offset=(0, 0), repetition=rep)
assert lbl.repetition is rep
assert_equal(lbl.get_bounds_single(), [[0, 0], [0, 0]])
# Note: Bounded.get_bounds_nonempty() for labels with repetition doesn't
# seem to automatically include repetition bounds in label.py itself,
# it's handled during pattern bounding.
def test_label_copy():
def test_label_copy() -> None:
l1 = Label("test", offset=(1, 2), annotations={"a": [1]})
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}")
from ..utils import annotations_eq
print(f"annotations_eq: {annotations_eq(l1.annotations, l2.annotations)}")
assert l1 == l2
assert l1 is not l2
l2.offset[0] = 100
assert l1.offset[0] == 1
import copy

View file

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

View file

@ -1,13 +1,13 @@
import pytest
import numpy
from numpy.testing import assert_equal
from pathlib import Path
import pytest
from numpy.testing import assert_equal
from ..pattern import Pattern
from ..library import Library
from ..file import oasis
def test_oasis_roundtrip(tmp_path):
def test_oasis_roundtrip(tmp_path: Path) -> None:
# Skip if fatamorgana is not installed
pytest.importorskip("fatamorgana")
@ -25,4 +25,3 @@ def test_oasis_roundtrip(tmp_path):
# Check bounds
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 ..library import Library
from ..pattern import Pattern
def test_maxrects_bssf_simple():
def test_maxrects_bssf_simple() -> None:
# Pack two 10x10 squares into one 20x10 container
rects = [[10, 10], [10, 10]]
containers = [[0, 0, 20, 10]]
@ -15,18 +12,20 @@ def test_maxrects_bssf_simple():
assert not rejects
# 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
rects = [[10, 10], [30, 30]]
containers = [[0, 0, 20, 20]]
locs, rejects = maxrects_bssf(rects, containers, allow_rejects=True)
assert 1 in rejects # Second rect rejected
assert 1 in rejects # Second rect rejected
assert 0 not in rejects
def test_pack_patterns():
def test_pack_patterns() -> None:
lib = Library()
p1 = Pattern()
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
# p2 size 5x5, effectively 7x7
# Both should fit in 20x20

View file

@ -1,17 +1,16 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from numpy.testing import assert_equal
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)
assert_equal(p.vertices, [[0, 0], [10, 0]])
assert p.width == 2
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)
polys = p.to_polygons()
assert len(polys) == 1
@ -19,7 +18,8 @@ def test_path_to_polygons_flush():
bounds = polys[0].get_bounds_single()
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)
polys = p.to_polygons()
assert len(polys) == 1
@ -28,7 +28,8 @@ def test_path_to_polygons_square():
bounds = polys[0].get_bounds_single()
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)
polys = p.to_polygons(num_vertices=32)
# 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()
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))
polys = p.to_polygons()
assert len(polys) == 1
@ -48,7 +50,8 @@ def test_path_custom_cap():
bounds = polys[0].get_bounds_single()
assert_equal(bounds, [[-5, -1], [20, 1]])
def test_path_bend():
def test_path_bend() -> None:
# L-shaped path
p = Path(vertices=[[0, 0], [10, 0], [10, 10]], width=2)
polys = p.to_polygons()
@ -64,14 +67,15 @@ def test_path_bend():
# So bounds should be x: [0, 11], y: [-1, 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.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]])
def test_path_scale():
def test_path_scale() -> None:
p = Path(vertices=[[0, 0], [10, 0]], width=2)
p.scale_by(2)
assert_equal(p.vertices, [[0, 0], [20, 0]])
assert p.width == 4

View file

@ -1,16 +1,15 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from ..builder import Pather
from ..builder.tools import PathTool
from ..library import Library
from ..pattern import Pattern
from ..ports import Port
@pytest.fixture
def pather_setup():
def pather_setup() -> tuple[Pather, PathTool, Library]:
lib = Library()
# Simple PathTool: 2um width on layer (1,0)
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
@ -18,19 +17,21 @@ def pather_setup():
# Add an initial port facing North (pi/2)
# Port rotation points INTO device. So "North" rotation means device is North of port.
# Pathing "forward" moves South.
p.ports["start"] = Port((0, 0), pi/2, ptype="wire")
p.ports["start"] = Port((0, 0), pi / 2, ptype="wire")
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
# Route 10um "forward"
p.path("start", ccw=None, length=10)
# port rot pi/2 (North). Travel +pi relative to port -> South.
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
# Start (0,0) rot pi/2 (North).
# Path 10um "forward" (South), then turn Clockwise (ccw=False).
@ -47,24 +48,27 @@ def test_pather_bend(pather_setup):
# Actual behavior results in 0 (East) - apparently rotation is flipped.
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
# start at (0,0) rot pi/2 (North)
# path "forward" (South) to y=-50
p.path_to("start", ccw=None, y=-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.ports["A"] = Port((0, 0), pi/2, ptype="wire")
p.ports["B"] = Port((10, 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")
# Path both "forward" (South) to y=-20
p.mpath(["A", "B"], ccw=None, ymin=-20)
assert_equal(p.ports["A"].offset, [0, -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
# Fluent API test
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)?
# Actual behavior results in pi (West).
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 import pi
from ..pattern import Pattern
from ..shapes import Polygon, Circle
from ..shapes import Polygon
from ..ref import Ref
from ..ports import Port
from ..label import Label
def test_pattern_init():
def test_pattern_init() -> None:
pat = Pattern()
assert pat.is_empty()
assert not pat.has_shapes()
@ -17,18 +16,14 @@ def test_pattern_init():
assert not pat.has_labels()
assert not pat.has_ports()
def test_pattern_with_elements():
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}
)
pat = Pattern(shapes={(1, 0): [poly]}, labels={(1, 2): [label]}, refs={"sub": [ref]}, ports={"P1": port})
assert pat.has_shapes()
assert pat.has_labels()
@ -40,7 +35,8 @@ def test_pattern_with_elements():
assert pat.refs["sub"] == [ref]
assert pat.ports["P1"] == port
def test_pattern_append():
def test_pattern_append() -> None:
pat1 = Pattern()
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[(2, 0)]) == 1
def test_pattern_translate():
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)
@ -62,7 +59,8 @@ def test_pattern_translate():
assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, 20])
assert_equal(pat.ports["P1"].offset, [15, 25])
def test_pattern_scale():
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)
@ -71,16 +69,18 @@ def test_pattern_scale():
# Vertices should be scaled
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.polygon((1, 0), vertices=[[10, 0], [11, 0], [10, 1]])
# Rotate 90 degrees CCW around (0,0)
pat.rotate_around((0, 0), pi/2)
pat.rotate_around((0, 0), pi / 2)
# [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)
def test_pattern_mirror():
def test_pattern_mirror() -> None:
pat = Pattern()
pat.polygon((1, 0), vertices=[[10, 5], [11, 5], [10, 6]])
# 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])
def test_pattern_get_bounds():
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]])
@ -96,7 +97,8 @@ def test_pattern_get_bounds():
bounds = pat.get_bounds()
assert_equal(bounds, [[-5, -5], [10, 10]])
def test_pattern_interface():
def test_pattern_interface() -> None:
source = Pattern()
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 iface.ports["in_A"].ptype == "test"
assert iface.ports["out_A"].ptype == "test"

View file

@ -1,6 +1,6 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy.testing import assert_equal
from ..shapes import Polygon
@ -9,29 +9,36 @@ from ..error import PatternError
@pytest.fixture
def polygon():
def polygon() -> Polygon:
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]])
def test_xs(polygon) -> None:
def test_xs(polygon: Polygon) -> None:
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])
def test_offset(polygon) -> None:
def test_offset(polygon: Polygon) -> None:
assert_equal(polygon.offset, [0, 0])
def test_square() -> None:
square = Polygon.square(1)
assert_equal(square.vertices, [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]])
def test_rectangle() -> None:
rectangle = Polygon.rectangle(1, 2)
assert_equal(rectangle.vertices, [[-0.5, -1], [-0.5, 1], [0.5, 1], [0.5, -1]])
def test_rect() -> None:
rect1 = Polygon.rect(xmin=0, xmax=1, ymin=-1, ymax=1)
assert_equal(rect1.vertices, [[0, -1], [0, 1], [1, 1], [1, -1]])
@ -64,47 +71,55 @@ def test_rect() -> None:
def test_octagon() -> None:
octagon = Polygon.octagon(side_length=1) # regular=True
octagon = Polygon.octagon(side_length=1) # regular=True
assert_equal(octagon.vertices.shape, (8, 2))
diff = octagon.vertices - numpy.roll(octagon.vertices, -1, axis=0)
side_len = numpy.sqrt((diff * diff).sum(axis=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]
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]])
def test_rotate(polygon) -> None:
def test_rotate(polygon: Polygon) -> None:
rotated_polygon = polygon.rotate(R90)
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)
assert_equal(mirrored_by_y.vertices, [[0, 0], [-1, 0], [-1, 1], [0, 1]])
print(polygon.vertices)
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)
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()
assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]])
def test_remove_duplicate_vertices() -> None:
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]])
def test_remove_colinear_vertices() -> None:
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]])
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.scale_by(0.5)
assert_equal(polygon.vertices, [[0, 0], [0.5, 0], [0.5, 0.5], [0, 0.5], [0, 0]])

View file

@ -1,87 +1,101 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from ..ports import Port, PortList
from ..error import PortError
def test_port_init():
p = Port(offset=(10, 20), rotation=pi/2, ptype="test")
def test_port_init() -> None:
p = Port(offset=(10, 20), rotation=pi / 2, ptype="test")
assert_equal(p.offset, [10, 20])
assert p.rotation == pi/2
assert p.rotation == pi / 2
assert p.ptype == "test"
def test_port_transform():
p = Port(offset=(10, 0), rotation=0)
p.rotate_around((0, 0), pi/2)
assert_allclose(p.offset, [0, 10], atol=1e-10)
assert_allclose(p.rotation, pi/2, atol=1e-10)
p.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset
def test_port_transform() -> None:
p = Port(offset=(10, 0), rotation=0)
p.rotate_around((0, 0), pi / 2)
assert_allclose(p.offset, [0, 10], atol=1e-10)
assert_allclose(p.rotation, pi / 2, atol=1e-10)
p.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset
assert_allclose(p.offset, [0, 10], atol=1e-10)
# 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.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])
# rotation was 0, mirrored(1) -> pi
assert_allclose(p.rotation, pi, atol=1e-10)
def test_port_measure_travel():
def test_port_measure_travel() -> None:
p1 = Port((0, 0), 0)
p2 = Port((10, 5), pi) # Facing each other
p2 = Port((10, 5), pi) # Facing each other
(travel, jog), rotation = p1.measure_travel(p2)
assert travel == 10
assert jog == 5
assert rotation == pi
def test_port_list_rename():
def test_port_list_rename() -> None:
class MyPorts(PortList):
def __init__(self):
def __init__(self) -> None:
self._ports = {"A": Port((0, 0), 0)}
@property
def ports(self): return self._ports
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val): self._ports = val
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
pl.rename_ports({"A": "B"})
assert "A" not in pl.ports
assert "B" in pl.ports
def test_port_list_plugged():
def test_port_list_plugged() -> None:
class MyPorts(PortList):
def __init__(self):
self._ports = {
"A": Port((10, 10), 0),
"B": Port((10, 10), pi)
}
def __init__(self) -> None:
self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)}
@property
def ports(self): return self._ports
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val): self._ports = val
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
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):
def __init__(self):
def __init__(self) -> None:
self._ports = {
"A": Port((10, 10), 0),
"B": Port((11, 10), pi) # Offset mismatch
"B": Port((11, 10), pi), # Offset mismatch
}
@property
def ports(self): return self._ports
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val): self._ports = val
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError):
pl.plugged({"A": "B"})

View file

@ -1,4 +1,3 @@
import pytest
import numpy
from numpy.testing import assert_allclose
@ -7,9 +6,10 @@ from ..pattern import Pattern
from ..ports import Port
from ..library import Library
def test_ports2data_roundtrip():
def test_ports2data_roundtrip() -> None:
pat = Pattern()
pat.ports["P1"] = Port((10, 20), numpy.pi/2, ptype="test")
pat.ports["P1"] = Port((10, 20), numpy.pi / 2, ptype="test")
layer = (10, 0)
ports_to_data(pat, layer)
@ -25,10 +25,11 @@ def test_ports2data_roundtrip():
assert "P1" in pat2.ports
assert_allclose(pat2.ports["P1"].offset, [10, 20], atol=1e-10)
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"
def test_data_to_ports_hierarchical():
def test_data_to_ports_hierarchical() -> None:
lib = Library()
# Child has port data in labels
@ -39,7 +40,7 @@ def test_data_to_ports_hierarchical():
# Parent references child
parent = Pattern()
parent.ref("child", offset=(100, 100), rotation=numpy.pi/2)
parent.ref("child", offset=(100, 100), rotation=numpy.pi / 2)
# Read ports hierarchically (max_depth > 0)
data_to_ports([layer], lib, parent, max_depth=1)
@ -51,5 +52,4 @@ def test_data_to_ports_hierarchical():
# rot 0 + pi/2 = pi/2
assert "A" in parent.ports
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 import pi
@ -7,18 +5,20 @@ from ..pattern import Pattern
from ..ref import Ref
from ..repetition import Grid
def test_ref_init():
ref = Ref(offset=(10, 20), rotation=pi/4, mirrored=True, scale=2.0)
def test_ref_init() -> None:
ref = Ref(offset=(10, 20), rotation=pi / 4, mirrored=True, scale=2.0)
assert_equal(ref.offset, [10, 20])
assert ref.rotation == pi/4
assert ref.rotation == pi / 4
assert ref.mirrored is True
assert ref.scale == 2.0
def test_ref_as_pattern():
def test_ref_as_pattern() -> None:
sub_pat = Pattern()
sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
ref = Ref(offset=(10, 10), rotation=pi/2, scale=2.0)
ref = Ref(offset=(10, 10), rotation=pi / 2, scale=2.0)
transformed_pat = ref.as_pattern(sub_pat)
# Check transformed shape
@ -30,7 +30,8 @@ def test_ref_as_pattern():
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.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)]])
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.polygon((1, 0), vertices=[[0, 0], [5, 0], [0, 5]])
@ -55,7 +57,8 @@ def test_ref_get_bounds():
# translated [[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]})
ref2 = ref1.copy()
assert ref1 == ref2
@ -63,4 +66,3 @@ def test_ref_copy():
ref2.offset[0] = 100
assert ref1.offset[0] == 1

View file

@ -1,6 +1,5 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy.testing import assert_allclose
from numpy import pi
from ..builder import RenderPather
@ -8,15 +7,17 @@ from ..builder.tools import PathTool
from ..library import Library
from ..ports import Port
@pytest.fixture
def rpather_setup():
def rpather_setup() -> tuple[RenderPather, PathTool, Library]:
lib = Library()
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
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
def test_renderpather_basic(rpather_setup):
def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup
# Plan two segments
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_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
# Plan straight then bend
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_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
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
assert len(rp.pattern.shapes[(1, 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 import pi
from ..repetition import Grid, Arbitrary
def test_grid_displacements():
def test_grid_displacements() -> None:
# 2x2 grid
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])
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)
disps = sorted([tuple(d) for d in grid.displacements])
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.rotate(pi/2)
grid.rotate(pi / 2)
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)
bounds = grid.get_bounds()
assert_equal(bounds, [[0, 0], [10, 5]])
def test_arbitrary_displacements():
def test_arbitrary_displacements() -> None:
pts = [[0, 0], [10, 20], [-5, 30]]
arb = Arbitrary(pts)
# They should be sorted by displacements.setter
@ -36,13 +39,13 @@ def test_arbitrary_displacements():
assert any((disps == [10, 20]).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.rotate(pi/2)
arb.rotate(pi / 2)
assert_allclose(arb.displacements, [[0, 10]], atol=1e-10)
arb.mirror(0) # Mirror x across y axis? Wait, mirror(axis=0) in repetition.py is:
arb.mirror(0) # Mirror x across y axis? Wait, mirror(axis=0) in repetition.py is:
# self.displacements[:, 1 - axis] *= -1
# if axis=0, 1-axis=1, so y *= -1
assert_allclose(arb.displacements, [[0, -10]], atol=1e-10)

View file

@ -1,16 +1,17 @@
from pathlib import Path
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
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
# 1. Text shape tests
def test_text_to_polygons():
def test_text_to_polygons() -> None:
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")
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]
assert len(set(char_x_means)) >= 2
# 2. Manhattanization tests
def test_manhattanize():
def test_manhattanize() -> None:
# Diamond shape
poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]])
grid = numpy.arange(0, 11, 1)
@ -38,39 +40,43 @@ def test_manhattanize():
# For each segment, either dx or dy must be zero
assert numpy.all((dv[:, 0] == 0) | (dv[:, 1] == 0))
# 3. Comparison and Sorting tests
def test_shape_comparisons():
def test_shape_comparisons() -> None:
c1 = Circle(radius=10)
c2 = Circle(radius=20)
assert c1 < c2
assert not (c2 < c1)
p1 = Polygon([[0, 0], [10, 0], [10, 10]])
p2 = Polygon([[0, 0], [10, 0], [10, 11]]) # Different vertex
p2 = Polygon([[0, 0], [10, 0], [10, 11]]) # Different vertex
assert p1 < p2
# Different types
assert c1 < p1 or p1 < c1
assert (c1 < p1) != (p1 < c1)
# 4. Arc/Path Edge Cases
def test_arc_edge_cases():
def test_arc_edge_cases() -> None:
# Wrapped arc (> 360 deg)
a = Arc(radii=(10, 10), angles=(0, 3*pi), width=2)
polys = a.to_polygons(num_vertices=64)
a = Arc(radii=(10, 10), angles=(0, 3 * pi), width=2)
a.to_polygons(num_vertices=64)
# Should basically be a ring
bounds = a.get_bounds_single()
assert_allclose(bounds, [[-11, -11], [11, 11]], atol=1e-10)
def test_path_edge_cases():
def test_path_edge_cases() -> None:
# 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()
assert len(polys) == 1
assert_equal(polys[0].get_bounds_single(), [[0, -1], [10, 1]])
# 5. PolyCollection with holes
def test_poly_collection_holes():
def test_poly_collection_holes() -> None:
# Outer square, inner square hole
# 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
@ -80,8 +86,14 @@ def test_poly_collection_holes():
# Let's test PolyCollection with multiple polygons
verts = [
[0, 0], [10, 0], [10, 10], [0, 10], # Poly 1
[2, 2], [2, 8], [8, 8], [8, 2] # Poly 2
[0, 0],
[10, 0],
[10, 10],
[0, 10], # Poly 1
[2, 2],
[2, 8],
[8, 8],
[8, 2], # Poly 2
]
offsets = [0, 4]
pc = PolyCollection(verts, offsets)
@ -90,17 +102,23 @@ def test_poly_collection_holes():
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]])
def test_poly_collection_constituent_empty():
def test_poly_collection_constituent_empty() -> None:
# One real triangle, one "empty" polygon (0 vertices), one real square
# Note: Polygon requires 3 vertices, so "empty" here might mean just some junk
# that to_polygons should handle.
# Actually PolyCollection doesn't check vertex count per polygon.
verts = [
[0, 0], [1, 0], [0, 1], # Tri
[0, 0],
[1, 0],
[0, 1], # Tri
# 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.
# vertex_slices uses zip(offsets, chain(offsets[1:], [len(verts)]))
# if offsets = [0, 3, 3], slices are [0:3], [3:3], [3:7]
@ -113,22 +131,14 @@ def test_poly_collection_constituent_empty():
with pytest.raises(PatternError):
pc.to_polygons()
def test_poly_collection_valid():
verts = [
[0, 0], [1, 0], [0, 1],
[10, 10], [11, 10], [11, 11], [10, 11]
]
def test_poly_collection_valid() -> None:
verts = [[0, 0], [1, 0], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]]
offsets = [0, 3]
pc = PolyCollection(verts, offsets)
assert len(pc.to_polygons()) == 2
shapes = [
Circle(radius=20),
Circle(radius=10),
Polygon([[0, 0], [10, 0], [10, 10]]),
Ellipse(radii=(5, 5))
]
shapes = [Circle(radius=20), Circle(radius=10), Polygon([[0, 0], [10, 0], [10, 10]]), Ellipse(radii=(5, 5))]
sorted_shapes = sorted(shapes)
assert len(sorted_shapes) == 4
# Just verify it doesn't crash and is stable
assert sorted(sorted_shapes) == sorted_shapes

View file

@ -1,11 +1,11 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
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]]
verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]]
offsets = [0, 4]
@ -13,7 +13,8 @@ def test_poly_collection_init():
assert len(list(pc.polygon_vertices)) == 2
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]]
offsets = [0, 4]
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[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))
assert c.radius == 10
assert_equal(c.offset, [5, 5])
def test_circle_to_polygons():
def test_circle_to_polygons() -> None:
c = Circle(radius=10)
polys = c.to_polygons(num_vertices=32)
assert len(polys) == 1
@ -36,28 +39,32 @@ def test_circle_to_polygons():
bounds = polys[0].get_bounds_single()
assert_allclose(bounds, [[-10, -10], [10, 10]], atol=1e-10)
def test_ellipse_init():
e = Ellipse(radii=(10, 5), offset=(1, 2), rotation=pi/4)
def test_ellipse_init() -> None:
e = Ellipse(radii=(10, 5), offset=(1, 2), rotation=pi / 4)
assert_equal(e.radii, [10, 5])
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))
polys = e.to_polygons(num_vertices=64)
assert len(polys) == 1
bounds = polys[0].get_bounds_single()
assert_allclose(bounds, [[-10, -5], [10, 5]], atol=1e-10)
def test_arc_init():
a = Arc(radii=(10, 10), angles=(0, pi/2), width=2, offset=(0, 0))
def test_arc_init() -> None:
a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2, offset=(0, 0))
assert_equal(a.radii, [10, 10])
assert_equal(a.angles, [0, pi/2])
assert_equal(a.angles, [0, pi / 2])
assert a.width == 2
def test_arc_to_polygons():
def test_arc_to_polygons() -> None:
# 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)
assert len(polys) == 1
# Outer radius 11, inner radius 9
@ -70,32 +77,35 @@ def test_arc_to_polygons():
# So x ranges from 0 to 11, y ranges from 0 to 11.
assert_allclose(bounds, [[0, 0], [11, 11]], atol=1e-10)
def test_shape_mirror():
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
def test_shape_mirror() -> None:
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
assert_equal(e.offset, [10, 20])
# rotation was pi/4, mirrored(0) -> -pi/4 == 3pi/4 (mod pi)
assert_allclose(e.rotation, 3*pi/4, atol=1e-10)
assert_allclose(e.rotation, 3 * pi / 4, atol=1e-10)
a = Arc(radii=(10, 10), angles=(0, pi/4), width=2, offset=(10, 20))
a = Arc(radii=(10, 10), angles=(0, pi / 4), width=2, offset=(10, 20))
a.mirror(0)
assert_equal(a.offset, [10, 20])
# 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():
e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi/4)
e.flip_across(axis=0) # Mirror across y=0: flips y-offset
def test_shape_flip_across() -> None:
e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4)
e.flip_across(axis=0) # Mirror across y=0: flips y-offset
assert_equal(e.offset, [10, -20])
# rotation also flips: -pi/4 == 3pi/4 (mod pi)
assert_allclose(e.rotation, 3*pi/4, atol=1e-10)
assert_allclose(e.rotation, 3 * pi / 4, atol=1e-10)
# Mirror across specific y
e = Ellipse(radii=(10, 5), offset=(10, 20))
e.flip_across(y=10) # Mirror across y=10
e.flip_across(y=10) # Mirror across y=10
# y=20 mirrored across y=10 -> y=0
assert_equal(e.offset, [10, 0])
def test_shape_scale():
def test_shape_scale() -> None:
e = Ellipse(radii=(10, 5))
e.scale_by(2)
assert_equal(e.radii, [20, 10])
@ -105,7 +115,8 @@ def test_shape_scale():
assert_equal(a.radii, [5, 2.5])
assert a.width == 1
def test_shape_arclen():
def test_shape_arclen() -> None:
# Test that max_arclen correctly limits segment lengths
# Ellipse
@ -114,12 +125,12 @@ def test_shape_arclen():
# With max_arclen=5, should have > 10 segments
polys = e.to_polygons(max_arclen=5)
v = polys[0].vertices
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 <= 5.000001)
assert len(v) > 10
# Arc
a = Arc(radii=(10, 10), angles=(0, pi/2), width=2)
a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2)
# Outer perimeter is 11 * pi/2 ~ 17.27
# Inner perimeter is 9 * pi/2 ~ 14.14
# With max_arclen=2, should have > 8 segments on outer edge
@ -127,6 +138,5 @@ def test_shape_arclen():
v = polys[0].vertices
# Arc polygons are closed, but contain both inner and outer edges and caps
# 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)

View file

@ -1,17 +1,11 @@
import pytest
import numpy
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from ..utils import (
remove_duplicate_vertices,
remove_colinear_vertices,
poly_contains_points,
rotation_matrix_2d,
apply_transforms
)
from ..utils import 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)
v = [[0, 0], [1, 1], [1, 1], [2, 2], [0, 0]]
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)
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_clean = remove_colinear_vertices(v, closed_path=True)
# [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]
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
v = [[0, 0], [10, 0], [0, 0]]
v_clean = remove_colinear_vertices(v, closed_path=False)
@ -43,34 +39,39 @@ def test_remove_colinear_vertices_exhaustive():
v_clean = remove_colinear_vertices(v, closed_path=True)
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]]
pts = [[5, 5], [-1, -1], [10, 10], [11, 5]]
inside = poly_contains_points(v, pts)
assert_equal(inside, [True, False, True, False])
def test_rotation_matrix_2d():
m = rotation_matrix_2d(pi/2)
def test_rotation_matrix_2d() -> None:
m = rotation_matrix_2d(pi / 2)
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
m = rotation_matrix_2d(pi/4)
s = numpy.sqrt(2)/2
m = rotation_matrix_2d(pi / 4)
s = numpy.sqrt(2) / 2
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)]
t1 = [10, 20, 0, 0]
t2 = [[5, 0, 0, 0], [0, 5, 0, 0]]
combined = apply_transforms(t1, t2)
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)
# Outer: mirror x (axis 0), then rotate 90 deg CCW
# apply_transforms logic for mirror uses y *= -1 (which is axis 0 mirror)
outer = [0, 0, pi/2, 1]
outer = [0, 0, pi / 2, 1]
# Inner: (10, 0, 0, 0)
inner = [10, 0, 0, 0]
@ -79,5 +80,4 @@ def test_apply_transforms_advanced():
# 1. mirror inner y if outer mirrored: (10, 0) -> (10, 0)
# 2. rotate by outer rotation (pi/2): (10, 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)