[Tests] cleanup
This commit is contained in:
parent
d9adb4e1b9
commit
1cce6c1f70
23 changed files with 540 additions and 467 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,60 @@
|
||||||
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)
|
||||||
# (10, 0) rot 0 -> (0, 10) rot 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"].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
|
# Mirror across x axis (axis 0): flips y-offset
|
||||||
abs_obj.mirror(0)
|
abs_obj.mirror(0)
|
||||||
# (0, 10) mirrored(0) -> (0, -10)
|
# (0, 10) mirrored(0) -> (0, -10)
|
||||||
# rotation pi/2 mirrored(0) -> -pi/2 == 3pi/2
|
# 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"].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)
|
||||||
|
|
||||||
# Apply ref transform
|
# Apply ref transform
|
||||||
abs_obj.apply_ref_transform(ref)
|
abs_obj.apply_ref_transform(ref)
|
||||||
# Ref order: mirror, rotate, scale, translate
|
# Ref order: mirror, rotate, scale, translate
|
||||||
|
|
||||||
# 1. mirror (across x: y -> -y)
|
# 1. mirror (across x: y -> -y)
|
||||||
# (10, 0) rot 0 -> (10, 0) rot 0
|
# (10, 0) rot 0 -> (10, 0) rot 0
|
||||||
|
|
||||||
# 2. rotate pi/2 around (0,0)
|
# 2. rotate pi/2 around (0,0)
|
||||||
# (10, 0) rot 0 -> (0, 10) rot pi/2
|
# (10, 0) rot 0 -> (0, 10) rot pi/2
|
||||||
|
|
||||||
# 3. translate (100, 100)
|
# 3. translate (100, 100)
|
||||||
# (0, 10) -> (100, 110)
|
# (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)
|
|
||||||
|
|
||||||
def test_abstract_undo_transform():
|
assert_allclose(abs_obj.ports["A"].offset, [100, 110], atol=1e-10)
|
||||||
abs_obj = Abstract("test", {"A": Port((100, 110), pi/2)})
|
assert_allclose(abs_obj.ports["A"].rotation, pi / 2, atol=1e-10)
|
||||||
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)
|
abs_obj.undo_ref_transform(ref)
|
||||||
assert_allclose(abs_obj.ports["A"].offset, [10, 0], atol=1e-10)
|
assert_allclose(abs_obj.ports["A"].offset, [10, 0], atol=1e-10)
|
||||||
assert_allclose(abs_obj.ports["A"].rotation, 0, atol=1e-10)
|
assert_allclose(abs_obj.ports["A"].rotation, 0, atol=1e-10)
|
||||||
|
|
|
||||||
|
|
@ -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,30 +7,33 @@ 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)
|
||||||
# Forward (+pi relative to port) is West (-x).
|
# Forward (+pi relative to port) is West (-x).
|
||||||
# Put destination at (-20, 0) pointing East (pi).
|
# Put destination at (-20, 0) pointing East (pi).
|
||||||
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
|
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
|
||||||
|
|
||||||
p.path_into("src", "dst")
|
p.path_into("src", "dst")
|
||||||
|
|
||||||
assert "src" not in p.ports
|
assert "src" not in p.ports
|
||||||
assert "dst" not in p.ports
|
assert "dst" not in p.ports
|
||||||
# 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")
|
||||||
|
|
@ -39,44 +41,47 @@ def test_path_into_bend(advanced_pather):
|
||||||
# Wait, src forward is -x. dst is at -20, -20.
|
# 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).
|
# 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).
|
# 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")
|
p.path_into("src", "dst")
|
||||||
|
|
||||||
assert "src" not in p.ports
|
assert "src" not in p.ports
|
||||||
assert "dst" not in p.ports
|
assert "dst" not in p.ports
|
||||||
# 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)
|
||||||
p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi)
|
p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi)
|
||||||
|
|
||||||
p.path_into("src", "dst")
|
p.path_into("src", "dst")
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
p.at("dst").path_from("src")
|
p.at("dst").path_from("src")
|
||||||
|
|
||||||
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")
|
||||||
p.ports["other"] = Port((10, 10), 0)
|
p.ports["other"] = Port((10, 10), 0)
|
||||||
|
|
||||||
p.path_into("src", "dst", thru="other")
|
p.path_into("src", "dst", thru="other")
|
||||||
|
|
||||||
assert "src" in p.ports
|
assert "src" in p.ports
|
||||||
assert_equal(p.ports["src"].offset, [10, 10])
|
assert_equal(p.ports["src"].offset, [10, 10])
|
||||||
assert "other" not in p.ports
|
assert "other" not in p.ports
|
||||||
|
|
|
||||||
|
|
@ -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,75 +7,75 @@ 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
|
||||||
bend_pat = Pattern()
|
bend_pat = Pattern()
|
||||||
# 2x2 bend from (0,0) rot 0 to (2, -2) rot pi/2 (Clockwise)
|
# 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["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()
|
||||||
via_pat.ports["m1"] = Port((0, 0), 0, ptype="wire_m1")
|
via_pat.ports["m1"] = Port((0, 0), 0, ptype="wire_m1")
|
||||||
via_pat.ports["m2"] = Port((1, 0), pi, ptype="wire_m2")
|
via_pat.ports["m2"] = Port((1, 0), pi, ptype="wire_m2")
|
||||||
lib["via"] = via_pat
|
lib["via"] = via_pat
|
||||||
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)
|
||||||
# Start with an m2 port
|
# Start with an m2 port
|
||||||
p.ports["start"] = Port((0, 0), pi, ptype="wire_m2")
|
p.ports["start"] = Port((0, 0), pi, ptype="wire_m2")
|
||||||
|
|
||||||
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.
|
||||||
# length 10. Via length is 1. So straight m1 should be 9.
|
# length 10. Via length is 1. So straight m1 should be 9.
|
||||||
p.path("start", ccw=None, length=10)
|
p.path("start", ccw=None, length=10)
|
||||||
|
|
||||||
# Start at (0,0) rot pi (facing West).
|
# Start at (0,0) rot pi (facing West).
|
||||||
# Forward (+pi relative to port) is East (+x).
|
# Forward (+pi relative to port) is East (+x).
|
||||||
# Via: m2(1,0)pi -> m1(0,0)0.
|
# Via: m2(1,0)pi -> m1(0,0)0.
|
||||||
# Plug via m2 into start(0,0)pi: transformation rot=mod(pi-pi-pi, 2pi)=pi.
|
# Plug via m2 into start(0,0)pi: transformation rot=mod(pi-pi-pi, 2pi)=pi.
|
||||||
# rotate via by pi: m2 at (0,0), m1 at (-1, 0) rot pi.
|
# rotate via by pi: m2 at (0,0), m1 at (-1, 0) rot pi.
|
||||||
# Then straight m1 of length 9 from (-1, 0) rot pi -> ends at (8, 0) rot pi.
|
# Then straight m1 of length 9 from (-1, 0) rot pi -> ends at (8, 0) rot pi.
|
||||||
# Wait, (length, 0) relative to (-1, 0) rot pi:
|
# Wait, (length, 0) relative to (-1, 0) rot pi:
|
||||||
# transform (9, 0) by pi: (-9, 0).
|
# transform (9, 0) by pi: (-9, 0).
|
||||||
# (-1, 0) + (-9, 0) = (-10, 0)? No.
|
# (-1, 0) + (-9, 0) = (-10, 0)? No.
|
||||||
# Let's re-calculate.
|
# Let's re-calculate.
|
||||||
# start (0,0) rot pi. Direction East.
|
# start (0,0) rot pi. Direction East.
|
||||||
# via m2 is at (0,0), m1 is at (1,0).
|
# via m2 is at (0,0), m1 is at (1,0).
|
||||||
# When via is plugged into start: m2 goes to (0,0).
|
# When via is plugged into start: m2 goes to (0,0).
|
||||||
# since start is pi and m2 is pi, rotation is 0.
|
# since start is pi and m2 is pi, rotation is 0.
|
||||||
# so via m1 is at (1,0) rot 0.
|
# so via m1 is at (1,0) rot 0.
|
||||||
# then straight m1 length 9 from (1,0) rot 0: ends at (10, 0) rot 0.
|
# then straight m1 length 9 from (1,0) rot 0: ends at (10, 0) rot 0.
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,36 +6,39 @@ 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)
|
||||||
lib["child"] = child
|
lib["child"] = child
|
||||||
|
|
||||||
b = Builder(lib)
|
b = Builder(lib)
|
||||||
b.place("child", offset=(10, 20), port_map={"A": "child_A"})
|
b.place("child", offset=(10, 20), port_map={"A": "child_A"})
|
||||||
|
|
||||||
assert "child_A" in b.ports
|
assert "child_A" in b.ports
|
||||||
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()
|
||||||
wire.ports["in"] = Port((0, 0), 0)
|
wire.ports["in"] = Port((0, 0), 0)
|
||||||
wire.ports["out"] = Port((10, 0), pi)
|
wire.ports["out"] = Port((10, 0), pi)
|
||||||
lib["wire"] = wire
|
lib["wire"] = wire
|
||||||
|
|
||||||
b = Builder(lib)
|
b = Builder(lib)
|
||||||
b.ports["start"] = Port((100, 100), 0)
|
b.ports["start"] = Port((100, 100), 0)
|
||||||
|
|
||||||
# Plug wire's "in" port into builder's "start" port
|
# Plug wire's "in" port into builder's "start" port
|
||||||
# Wire's "out" port should be renamed to "start" because thru=True (default) and wire has 2 ports
|
# Wire's "out" port should be renamed to "start" because thru=True (default) and wire has 2 ports
|
||||||
# builder start: (100, 100) rotation 0
|
# builder start: (100, 100) rotation 0
|
||||||
|
|
@ -46,28 +47,29 @@ def test_builder_plug():
|
||||||
# Plugging wire in (rot 0) to builder start (rot 0) means wire is rotated by pi (180 deg)
|
# Plugging wire in (rot 0) to builder start (rot 0) means wire is rotated by pi (180 deg)
|
||||||
# so wire in is at (100, 100), wire out is at (100 - 10, 100) = (90, 100)
|
# so wire in is at (100, 100), wire out is at (100 - 10, 100) = (90, 100)
|
||||||
b.plug("wire", map_in={"start": "in"})
|
b.plug("wire", map_in={"start": "in"})
|
||||||
|
|
||||||
assert "start" in b.ports
|
assert "start" in b.ports
|
||||||
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)
|
||||||
lib["source"] = source
|
lib["source"] = source
|
||||||
|
|
||||||
b = Builder.interface("source", library=lib, name="iface")
|
b = Builder.interface("source", library=lib, name="iface")
|
||||||
assert "in_P1" in b.ports
|
assert "in_P1" in b.ports
|
||||||
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)
|
||||||
b.set_dead()
|
b.set_dead()
|
||||||
|
|
||||||
b.place("sub")
|
b.place("sub")
|
||||||
assert not b.pattern.has_refs()
|
assert not b.pattern.has_refs()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,22 @@
|
||||||
|
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import NDArray
|
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
|
from .. import Pattern, Arc, Circle
|
||||||
|
|
||||||
|
|
||||||
def test_circle_mirror():
|
def test_circle_mirror():
|
||||||
cc = Circle(radius=4, offset=(10, 20))
|
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[0] == 10
|
||||||
assert cc.offset[1] == -20
|
assert cc.offset[1] == -20
|
||||||
assert cc.radius == 4
|
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[0] == -10
|
||||||
assert cc.offset[1] == -20
|
assert cc.offset[1] == -20
|
||||||
assert cc.radius == 4
|
assert cc.radius == 4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,69 @@
|
||||||
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
|
||||||
pat1 = Pattern()
|
pat1 = Pattern()
|
||||||
pat1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]])
|
pat1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]])
|
||||||
lib["poly_cell"] = pat1
|
lib["poly_cell"] = pat1
|
||||||
|
|
||||||
# Path cell
|
# Path cell
|
||||||
pat2 = Pattern()
|
pat2 = Pattern()
|
||||||
pat2.path((2, 5), vertices=[[0, 0], [100, 0]], width=10)
|
pat2.path((2, 5), vertices=[[0, 0], [100, 0]], width=10)
|
||||||
lib["path_cell"] = pat2
|
lib["path_cell"] = pat2
|
||||||
|
|
||||||
# Cell with Ref
|
# Cell with Ref
|
||||||
pat3 = Pattern()
|
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
|
lib["ref_cell"] = pat3
|
||||||
|
|
||||||
gds_file = tmp_path / "test.gds"
|
gds_file = tmp_path / "test.gds"
|
||||||
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
||||||
|
|
||||||
read_lib, info = gdsii.readfile(gds_file)
|
read_lib, info = gdsii.readfile(gds_file)
|
||||||
|
|
||||||
assert "poly_cell" in read_lib
|
assert "poly_cell" in read_lib
|
||||||
assert "path_cell" in read_lib
|
assert "path_cell" in read_lib
|
||||||
assert "ref_cell" in read_lib
|
assert "ref_cell" in read_lib
|
||||||
|
|
||||||
# Check polygon
|
# Check polygon
|
||||||
read_poly = read_lib["poly_cell"].shapes[(1, 0)][0]
|
read_poly = read_lib["poly_cell"].shapes[(1, 0)][0]
|
||||||
# GDSII closes polygons, so it might have an extra vertex or different order
|
# GDSII closes polygons, so it might have an extra vertex or different order
|
||||||
assert len(read_poly.vertices) >= 4
|
assert len(read_poly.vertices) >= 4
|
||||||
# Check bounds as a proxy for geometry correctness
|
# Check bounds as a proxy for geometry correctness
|
||||||
assert_equal(read_lib["poly_cell"].get_bounds(), [[0, 0], [10, 10]])
|
assert_equal(read_lib["poly_cell"].get_bounds(), [[0, 0], [10, 10]])
|
||||||
|
|
||||||
# Check path
|
# Check path
|
||||||
read_path = read_lib["path_cell"].shapes[(2, 5)][0]
|
read_path = read_lib["path_cell"].shapes[(2, 5)][0]
|
||||||
assert isinstance(read_path, MPath)
|
assert isinstance(read_path, MPath)
|
||||||
assert read_path.width == 10
|
assert read_path.width == 10
|
||||||
assert_equal(read_path.vertices, [[0, 0], [100, 0]])
|
assert_equal(read_path.vertices, [[0, 0], [100, 0]])
|
||||||
|
|
||||||
# Check Ref
|
# Check Ref
|
||||||
read_ref = read_lib["ref_cell"].refs["poly_cell"][0]
|
read_ref = read_lib["ref_cell"].refs["poly_cell"][0]
|
||||||
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
|
||||||
pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]], annotations={"1": ["hello"]})
|
pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]], annotations={"1": ["hello"]})
|
||||||
lib["cell"] = pat
|
lib["cell"] = pat
|
||||||
|
|
||||||
gds_file = tmp_path / "test_ann.gds"
|
gds_file = tmp_path / "test_ann.gds"
|
||||||
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
gdsii.writefile(lib, gds_file, meters_per_unit=1e-9)
|
||||||
|
|
||||||
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"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,50 @@
|
||||||
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
|
|
||||||
l.translate((5, 5))
|
|
||||||
assert_allclose(l.offset, [5, 15], atol=1e-10)
|
|
||||||
|
|
||||||
def test_label_repetition():
|
# Translate
|
||||||
|
lbl.translate((5, 5))
|
||||||
|
assert_allclose(lbl.offset, [5, 15], atol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
print(f"l1: string={l1.string}, offset={l1.offset}, repetition={l1.repetition}, annotations={l1.annotations}")
|
print(f"l1: string={l1.string}, offset={l1.offset}, repetition={l1.repetition}, annotations={l1.annotations}")
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,116 @@
|
||||||
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
|
||||||
|
|
||||||
assert "cell1" in lib
|
assert "cell1" in lib
|
||||||
assert lib["cell1"] is pat
|
assert lib["cell1"] is pat
|
||||||
assert len(lib) == 1
|
assert len(lib) == 1
|
||||||
|
|
||||||
with pytest.raises(LibraryError):
|
|
||||||
lib["cell1"] = Pattern() # Overwriting not allowed
|
|
||||||
|
|
||||||
def test_library_tops():
|
with pytest.raises(LibraryError):
|
||||||
|
lib["cell1"] = Pattern() # Overwriting not allowed
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_tops() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["child"] = Pattern()
|
lib["child"] = Pattern()
|
||||||
lib["parent"] = Pattern()
|
lib["parent"] = Pattern()
|
||||||
lib["parent"].ref("child")
|
lib["parent"].ref("child")
|
||||||
|
|
||||||
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]])
|
||||||
lib["child"] = child
|
lib["child"] = child
|
||||||
|
|
||||||
parent = Pattern()
|
parent = Pattern()
|
||||||
parent.ref("child", offset=(10, 10))
|
parent.ref("child", offset=(10, 10))
|
||||||
lib["parent"] = parent
|
lib["parent"] = parent
|
||||||
|
|
||||||
flat_lib = lib.flatten("parent")
|
flat_lib = lib.flatten("parent")
|
||||||
flat_parent = flat_lib["parent"]
|
flat_parent = flat_lib["parent"]
|
||||||
|
|
||||||
assert not flat_parent.has_refs()
|
assert not flat_parent.has_refs()
|
||||||
assert len(flat_parent.shapes[(1, 0)]) == 1
|
assert len(flat_parent.shapes[(1, 0)]) == 1
|
||||||
# Transformations are baked into vertices for Polygon
|
# Transformations are baked into vertices for Polygon
|
||||||
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()
|
||||||
|
|
||||||
lib["lazy"] = make_pat
|
lib["lazy"] = make_pat
|
||||||
assert called == 0
|
assert called == 0
|
||||||
|
|
||||||
pat = lib["lazy"]
|
pat = lib["lazy"]
|
||||||
assert called == 1
|
assert called == 1
|
||||||
assert isinstance(pat, Pattern)
|
assert isinstance(pat, Pattern)
|
||||||
|
|
||||||
# Second access should be cached
|
# Second access should be cached
|
||||||
pat2 = lib["lazy"]
|
pat2 = lib["lazy"]
|
||||||
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()
|
||||||
lib["parent"].ref("old")
|
lib["parent"].ref("old")
|
||||||
|
|
||||||
lib.rename("old", "new", move_references=True)
|
lib.rename("old", "new", move_references=True)
|
||||||
|
|
||||||
assert "old" not in lib
|
assert "old" not in lib
|
||||||
assert "new" in lib
|
assert "new" in lib
|
||||||
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()
|
||||||
lib["c"] = Pattern()
|
lib["c"] = Pattern()
|
||||||
lib["a"].ref("b")
|
lib["a"].ref("b")
|
||||||
|
|
||||||
sub = lib.subtree("a")
|
sub = lib.subtree("a")
|
||||||
assert "a" in sub
|
assert "a" in sub
|
||||||
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()
|
||||||
|
|
||||||
name1 = lib.get_name("cell")
|
name1 = lib.get_name("cell")
|
||||||
assert name1 != "cell"
|
assert name1 != "cell"
|
||||||
assert name1.startswith("cell")
|
assert name1.startswith("cell")
|
||||||
|
|
||||||
name2 = lib.get_name("other")
|
name2 = lib.get_name("other")
|
||||||
assert name2 == "other"
|
assert name2 == "other"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,27 @@
|
||||||
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")
|
||||||
|
|
||||||
lib = Library()
|
lib = Library()
|
||||||
pat1 = Pattern()
|
pat1 = Pattern()
|
||||||
pat1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]])
|
pat1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]])
|
||||||
lib["cell1"] = pat1
|
lib["cell1"] = pat1
|
||||||
|
|
||||||
oas_file = tmp_path / "test.oas"
|
oas_file = tmp_path / "test.oas"
|
||||||
# OASIS needs units_per_micron
|
# OASIS needs units_per_micron
|
||||||
oasis.writefile(lib, oas_file, units_per_micron=1000)
|
oasis.writefile(lib, oas_file, units_per_micron=1000)
|
||||||
|
|
||||||
read_lib, info = oasis.readfile(oas_file)
|
read_lib, info = oasis.readfile(oas_file)
|
||||||
assert "cell1" in read_lib
|
assert "cell1" in read_lib
|
||||||
|
|
||||||
# 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]])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,51 @@
|
||||||
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]]
|
||||||
|
|
||||||
locs, rejects = maxrects_bssf(rects, containers)
|
locs, rejects = maxrects_bssf(rects, containers)
|
||||||
|
|
||||||
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]]
|
||||||
|
|
||||||
locs, rejects = maxrects_bssf(rects, containers, allow_rejects=True)
|
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
|
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]])
|
||||||
lib["p1"] = p1
|
lib["p1"] = p1
|
||||||
|
|
||||||
p2 = Pattern()
|
p2 = Pattern()
|
||||||
p2.polygon((1, 0), vertices=[[0, 0], [5, 0], [5, 5], [0, 5]])
|
p2.polygon((1, 0), vertices=[[0, 0], [5, 0], [5, 5], [0, 5]])
|
||||||
lib["p2"] = p2
|
lib["p2"] = p2
|
||||||
|
|
||||||
# Containers: one 20x20
|
# Containers: one 20x20
|
||||||
containers = [[0, 0, 20, 20]]
|
containers = [[0, 0, 20, 20]]
|
||||||
# 2um spacing
|
# 2um spacing
|
||||||
pat, rejects = pack_patterns(lib, ["p1", "p2"], containers, spacing=(2, 2))
|
pat, rejects = pack_patterns(lib, ["p1", "p2"], containers, spacing=(2, 2))
|
||||||
|
|
||||||
assert not rejects
|
assert not rejects
|
||||||
assert len(pat.refs) == 2
|
assert len(pat.refs) == 2
|
||||||
assert "p1" in pat.refs
|
assert "p1" in pat.refs
|
||||||
assert "p2" in pat.refs
|
assert "p2" in pat.refs
|
||||||
|
|
||||||
# Check that they don't overlap (simple check via bounds)
|
# Check that they don't overlap (simple check via bounds)
|
||||||
# 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,18 +28,20 @@ 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
|
||||||
assert len(polys) >= 3
|
assert len(polys) >= 3
|
||||||
|
|
||||||
# Combined bounds should be from (-1, -1) to (11, 1)
|
# Combined bounds should be from (-1, -1) to (11, 1)
|
||||||
# But wait, Path.get_bounds_single() handles this more directly
|
# But wait, Path.get_bounds_single() handles this more directly
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -18,53 +17,58 @@ def pather_setup():
|
||||||
# Add an initial port facing North (pi/2)
|
# Add an initial port facing North (pi/2)
|
||||||
# Port rotation points INTO device. So "North" rotation means device is North of port.
|
# Port rotation points INTO device. So "North" rotation means device is North of port.
|
||||||
# Pathing "forward" moves South.
|
# 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
|
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)
|
||||||
|
|
||||||
# port rot pi/2 (North). Travel +pi relative to port -> South.
|
# 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"].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).
|
||||||
# Facing South, turn Right -> West.
|
# Facing South, turn Right -> West.
|
||||||
p.path("start", ccw=False, length=10)
|
p.path("start", ccw=False, length=10)
|
||||||
|
|
||||||
# PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0.
|
# PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0.
|
||||||
# Transformed by port rot pi/2 (North) + pi (to move "forward" away from device):
|
# Transformed by port rot pi/2 (North) + pi (to move "forward" away from device):
|
||||||
# Transformation rot = pi/2 + pi = 3pi/2.
|
# Transformation rot = pi/2 + pi = 3pi/2.
|
||||||
# (10, -1) rotated 3pi/2: (x,y) -> (y, -x) -> (-1, -10).
|
# (10, -1) rotated 3pi/2: (x,y) -> (y, -x) -> (-1, -10).
|
||||||
|
|
||||||
assert_allclose(p.ports["start"].offset, [-1, -10], atol=1e-10)
|
assert_allclose(p.ports["start"].offset, [-1, -10], atol=1e-10)
|
||||||
# North (pi/2) + CW (90 deg) -> West (pi)?
|
# North (pi/2) + CW (90 deg) -> West (pi)?
|
||||||
# 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")
|
||||||
|
|
||||||
# Path both "forward" (South) to y=-20
|
# Path both "forward" (South) to y=-20
|
||||||
p.mpath(["A", "B"], ccw=None, ymin=-20)
|
p.mpath(["A", "B"], ccw=None, ymin=-20)
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,19 +16,15 @@ 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()
|
||||||
assert pat.has_refs()
|
assert pat.has_refs()
|
||||||
|
|
@ -40,72 +35,78 @@ 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]])
|
||||||
|
|
||||||
pat2 = Pattern()
|
pat2 = Pattern()
|
||||||
pat2.polygon((2, 0), vertices=[[10, 10], [11, 10], [11, 11]])
|
pat2.polygon((2, 0), vertices=[[10, 10], [11, 10], [11, 11]])
|
||||||
|
|
||||||
pat1.append(pat2)
|
pat1.append(pat2)
|
||||||
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)
|
||||||
|
|
||||||
pat.translate_elements((10, 20))
|
pat.translate_elements((10, 20))
|
||||||
|
|
||||||
# Polygon.translate adds to vertices, and offset is always (0,0)
|
# Polygon.translate adds to vertices, and offset is always (0,0)
|
||||||
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)
|
||||||
pat.scale_by(2)
|
pat.scale_by(2)
|
||||||
|
|
||||||
# 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)
|
||||||
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]
|
# [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)
|
||||||
pat.mirror(0)
|
pat.mirror(0)
|
||||||
|
|
||||||
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]])
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
iface = Pattern.interface(source, in_prefix="in_", out_prefix="out_")
|
iface = Pattern.interface(source, in_prefix="in_", out_prefix="out_")
|
||||||
|
|
||||||
assert "in_A" in iface.ports
|
assert "in_A" in iface.ports
|
||||||
assert "out_A" in iface.ports
|
assert "out_A" in iface.ports
|
||||||
assert_allclose(iface.ports["in_A"].rotation, pi, atol=1e-10)
|
assert_allclose(iface.ports["in_A"].rotation, pi, atol=1e-10)
|
||||||
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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]])
|
||||||
|
|
@ -64,47 +71,55 @@ def test_rect() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_octagon() -> 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))
|
assert_equal(octagon.vertices.shape, (8, 2))
|
||||||
diff = octagon.vertices - numpy.roll(octagon.vertices, -1, axis=0)
|
diff = octagon.vertices - numpy.roll(octagon.vertices, -1, axis=0)
|
||||||
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]])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,101 @@
|
||||||
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():
|
|
||||||
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_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)
|
||||||
assert_allclose(p.rotation, pi/2, 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
|
p.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset
|
||||||
assert_allclose(p.offset, [0, 10], atol=1e-10)
|
assert_allclose(p.offset, [0, 10], atol=1e-10)
|
||||||
# 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
|
||||||
|
|
||||||
(travel, jog), rotation = p1.measure_travel(p2)
|
(travel, jog), rotation = p1.measure_travel(p2)
|
||||||
assert travel == 10
|
assert travel == 10
|
||||||
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"})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.testing import assert_allclose
|
from numpy.testing import assert_allclose
|
||||||
|
|
||||||
|
|
@ -7,43 +6,45 @@ 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")
|
||||||
|
|
||||||
layer = (10, 0)
|
layer = (10, 0)
|
||||||
ports_to_data(pat, layer)
|
ports_to_data(pat, layer)
|
||||||
|
|
||||||
assert len(pat.labels[layer]) == 1
|
assert len(pat.labels[layer]) == 1
|
||||||
assert pat.labels[layer][0].string == "P1:test 90"
|
assert pat.labels[layer][0].string == "P1:test 90"
|
||||||
assert tuple(pat.labels[layer][0].offset) == (10.0, 20.0)
|
assert tuple(pat.labels[layer][0].offset) == (10.0, 20.0)
|
||||||
|
|
||||||
# New pattern, read ports back
|
# New pattern, read ports back
|
||||||
pat2 = Pattern()
|
pat2 = Pattern()
|
||||||
pat2.labels[layer] = pat.labels[layer]
|
pat2.labels[layer] = pat.labels[layer]
|
||||||
data_to_ports([layer], {}, pat2)
|
data_to_ports([layer], {}, pat2)
|
||||||
|
|
||||||
assert "P1" in pat2.ports
|
assert "P1" in pat2.ports
|
||||||
assert_allclose(pat2.ports["P1"].offset, [10, 20], atol=1e-10)
|
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"
|
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
|
||||||
child = Pattern()
|
child = Pattern()
|
||||||
layer = (10, 0)
|
layer = (10, 0)
|
||||||
child.label(layer=layer, string="A:type1 0", offset=(5, 0))
|
child.label(layer=layer, string="A:type1 0", offset=(5, 0))
|
||||||
lib["child"] = child
|
lib["child"] = child
|
||||||
|
|
||||||
# Parent references child
|
# Parent references child
|
||||||
parent = Pattern()
|
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)
|
# Read ports hierarchically (max_depth > 0)
|
||||||
data_to_ports([layer], lib, parent, max_depth=1)
|
data_to_ports([layer], lib, parent, max_depth=1)
|
||||||
|
|
||||||
# child port A (5,0) rot 0
|
# child port A (5,0) rot 0
|
||||||
# transformed by parent ref: rot pi/2, trans (100, 100)
|
# transformed by parent ref: rot pi/2, trans (100, 100)
|
||||||
# (5,0) rot pi/2 -> (0, 5)
|
# (5,0) rot pi/2 -> (0, 5)
|
||||||
|
|
@ -51,5 +52,4 @@ def test_data_to_ports_hierarchical():
|
||||||
# rot 0 + pi/2 = pi/2
|
# rot 0 + pi/2 = pi/2
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,47 +5,51 @@ from ..pattern import Pattern
|
||||||
from ..ref import Ref
|
from ..ref import Ref
|
||||||
from ..repetition import Grid
|
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_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]])
|
||||||
|
|
||||||
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)
|
transformed_pat = ref.as_pattern(sub_pat)
|
||||||
|
|
||||||
# Check transformed shape
|
# Check transformed shape
|
||||||
shape = transformed_pat.shapes[(1, 0)][0]
|
shape = transformed_pat.shapes[(1, 0)][0]
|
||||||
# ref.as_pattern deepcopies sub_pat then applies transformations:
|
# ref.as_pattern deepcopies sub_pat then applies transformations:
|
||||||
# 1. pattern.scale_by(2) -> vertices [[0,0], [2,0], [0,2]]
|
# 1. pattern.scale_by(2) -> vertices [[0,0], [2,0], [0,2]]
|
||||||
# 2. pattern.rotate_around((0,0), pi/2) -> vertices [[0,0], [0,2], [-2,0]]
|
# 2. pattern.rotate_around((0,0), pi/2) -> vertices [[0,0], [0,2], [-2,0]]
|
||||||
# 3. pattern.translate_elements((10,10)) -> vertices [[10,10], [10,12], [8,10]]
|
# 3. pattern.translate_elements((10,10)) -> vertices [[10,10], [10,12], [8,10]]
|
||||||
|
|
||||||
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]])
|
||||||
|
|
||||||
rep = Grid(a_vector=(10, 0), b_vector=(0, 10), a_count=2, b_count=2)
|
rep = Grid(a_vector=(10, 0), b_vector=(0, 10), a_count=2, b_count=2)
|
||||||
ref = Ref(repetition=rep)
|
ref = Ref(repetition=rep)
|
||||||
|
|
||||||
repeated_pat = ref.as_pattern(sub_pat)
|
repeated_pat = ref.as_pattern(sub_pat)
|
||||||
# Should have 4 shapes
|
# Should have 4 shapes
|
||||||
assert len(repeated_pat.shapes[(1, 0)]) == 4
|
assert len(repeated_pat.shapes[(1, 0)]) == 4
|
||||||
|
|
||||||
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]])
|
||||||
|
|
||||||
ref = Ref(offset=(10, 10), scale=2.0)
|
ref = Ref(offset=(10, 10), scale=2.0)
|
||||||
bounds = ref.get_bounds_single(sub_pat)
|
bounds = ref.get_bounds_single(sub_pat)
|
||||||
# sub_pat bounds [[0,0], [5,5]]
|
# sub_pat bounds [[0,0], [5,5]]
|
||||||
|
|
@ -55,12 +57,12 @@ 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
|
||||||
assert ref1 is not ref2
|
assert ref1 is not ref2
|
||||||
|
|
||||||
ref2.offset[0] = 100
|
ref2.offset[0] = 100
|
||||||
assert ref1.offset[0] == 1
|
assert ref1.offset[0] == 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,28 +7,30 @@ 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)
|
||||||
|
|
||||||
# Before rendering, no shapes in pattern
|
# Before rendering, no shapes in pattern
|
||||||
assert not rp.pattern.has_shapes()
|
assert not rp.pattern.has_shapes()
|
||||||
assert len(rp.paths["start"]) == 2
|
assert len(rp.paths["start"]) == 2
|
||||||
|
|
||||||
# Render
|
# Render
|
||||||
rp.render()
|
rp.render()
|
||||||
assert rp.pattern.has_shapes()
|
assert rp.pattern.has_shapes()
|
||||||
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
||||||
|
|
||||||
# Path vertices should be (0,0), (0,-10), (0,-20)
|
# Path vertices should be (0,0), (0,-10), (0,-20)
|
||||||
# transformed by start port (rot pi/2 -> 270 deg transform)
|
# transformed by start port (rot pi/2 -> 270 deg transform)
|
||||||
# wait, PathTool.render for opcode L uses rotation_matrix_2d(port_rot + pi)
|
# wait, PathTool.render for opcode L uses rotation_matrix_2d(port_rot + pi)
|
||||||
|
|
@ -40,14 +41,15 @@ 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)
|
||||||
|
|
||||||
rp.render()
|
rp.render()
|
||||||
path_shape = rp.pattern.shapes[(1, 0)][0]
|
path_shape = rp.pattern.shapes[(1, 0)][0]
|
||||||
# Path vertices:
|
# Path vertices:
|
||||||
# 1. Start (0,0)
|
# 1. Start (0,0)
|
||||||
# 2. Straight end: (0, -10)
|
# 2. Straight end: (0, -10)
|
||||||
# 3. Bend end: (-1, -20)
|
# 3. Bend end: (-1, -20)
|
||||||
|
|
@ -58,16 +60,16 @@ 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")
|
||||||
|
|
||||||
rp.at("start").path(ccw=None, length=10)
|
rp.at("start").path(ccw=None, length=10)
|
||||||
rp.retool(tool2, keys=["start"])
|
rp.retool(tool2, keys=["start"])
|
||||||
rp.at("start").path(ccw=None, length=10)
|
rp.at("start").path(ccw=None, length=10)
|
||||||
|
|
||||||
rp.render()
|
rp.render()
|
||||||
# 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,13 +39,13 @@ 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)
|
||||||
|
|
||||||
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
|
# 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,39 +40,43 @@ 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
|
||||||
assert not (c2 < c1)
|
assert not (c2 < c1)
|
||||||
|
|
||||||
p1 = Polygon([[0, 0], [10, 0], [10, 10]])
|
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
|
assert p1 < p2
|
||||||
|
|
||||||
# Different types
|
# Different types
|
||||||
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,17 +102,23 @@ 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.
|
||||||
# vertex_slices uses zip(offsets, chain(offsets[1:], [len(verts)]))
|
# vertex_slices uses zip(offsets, chain(offsets[1:], [len(verts)]))
|
||||||
# if offsets = [0, 3, 3], slices are [0:3], [3:3], [3:7]
|
# 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):
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,28 +39,32 @@ 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():
|
|
||||||
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.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():
|
|
||||||
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.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)
|
||||||
assert len(polys) == 1
|
assert len(polys) == 1
|
||||||
# Outer radius 11, inner radius 9
|
# 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.
|
# 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():
|
|
||||||
e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi/4)
|
def test_shape_mirror() -> None:
|
||||||
e.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset
|
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])
|
assert_equal(e.offset, [10, 20])
|
||||||
# rotation was pi/4, mirrored(0) -> -pi/4 == 3pi/4 (mod pi)
|
# 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)
|
a.mirror(0)
|
||||||
assert_equal(a.offset, [10, 20])
|
assert_equal(a.offset, [10, 20])
|
||||||
# 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():
|
|
||||||
e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi/4)
|
def test_shape_flip_across() -> None:
|
||||||
e.flip_across(axis=0) # Mirror across y=0: flips y-offset
|
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])
|
assert_equal(e.offset, [10, -20])
|
||||||
# rotation also flips: -pi/4 == 3pi/4 (mod pi)
|
# 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
|
# Mirror across specific y
|
||||||
e = Ellipse(radii=(10, 5), offset=(10, 20))
|
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
|
# 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,21 +115,22 @@ 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
|
||||||
e = Ellipse(radii=(10, 5))
|
e = Ellipse(radii=(10, 5))
|
||||||
# Approximate perimeter is ~48.4
|
# Approximate perimeter is ~48.4
|
||||||
# With max_arclen=5, should have > 10 segments
|
# With max_arclen=5, should have > 10 segments
|
||||||
polys = e.to_polygons(max_arclen=5)
|
polys = e.to_polygons(max_arclen=5)
|
||||||
v = polys[0].vertices
|
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 numpy.all(dist <= 5.000001)
|
||||||
assert len(v) > 10
|
assert len(v) > 10
|
||||||
|
|
||||||
# Arc
|
# 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
|
# Outer perimeter is 11 * pi/2 ~ 17.27
|
||||||
# Inner perimeter is 9 * pi/2 ~ 14.14
|
# Inner perimeter is 9 * pi/2 ~ 14.14
|
||||||
# With max_arclen=2, should have > 8 segments on outer edge
|
# With max_arclen=2, should have > 8 segments on outer edge
|
||||||
|
|
@ -127,6 +138,5 @@ def test_shape_arclen():
|
||||||
v = polys[0].vertices
|
v = polys[0].vertices
|
||||||
# Arc polygons are closed, but contain both inner and outer edges and caps
|
# Arc polygons are closed, but contain both inner and outer edges and caps
|
||||||
# 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,23 @@
|
||||||
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)
|
||||||
# The last [0,0] is a duplicate of the first [0,0] if closed_path=True
|
# The last [0,0] is a duplicate of the first [0,0] if closed_path=True
|
||||||
assert_equal(v_clean, [[0, 0], [1, 1], [2, 2]])
|
assert_equal(v_clean, [[0, 0], [1, 1], [2, 2]])
|
||||||
|
|
||||||
# Open path
|
# Open path
|
||||||
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,54 +25,59 @@ 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)
|
||||||
# Open path should keep ends. [10,0] is between [0,0] and [0,0]?
|
# Open path should keep ends. [10,0] is between [0,0] and [0,0]?
|
||||||
# Yes, they are all on the same line.
|
# Yes, they are all on the same line.
|
||||||
assert len(v_clean) == 2
|
assert len(v_clean) == 2
|
||||||
|
|
||||||
# 180 degree U-turn in closed path
|
# 180 degree U-turn in closed path
|
||||||
v = [[0, 0], [10, 0], [5, 0]]
|
v = [[0, 0], [10, 0], [5, 0]]
|
||||||
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():
|
|
||||||
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)
|
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)
|
||||||
outer = [0, 0, pi/2, 1]
|
outer = [0, 0, pi / 2, 1]
|
||||||
|
|
||||||
# Inner: (10, 0, 0, 0)
|
# Inner: (10, 0, 0, 0)
|
||||||
inner = [10, 0, 0, 0]
|
inner = [10, 0, 0, 0]
|
||||||
|
|
||||||
combined = apply_transforms(outer, inner)
|
combined = apply_transforms(outer, inner)
|
||||||
# 1. mirror inner y if outer mirrored: (10, 0) -> (10, 0)
|
# 1. mirror inner y if outer mirrored: (10, 0) -> (10, 0)
|
||||||
# 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)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue