From 1cce6c1f707b39816f833816fee5accda3569e57 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 15 Feb 2026 12:36:13 -0800 Subject: [PATCH] [Tests] cleanup --- masque/test/conftest.py | 5 +- masque/test/test_abstract.py | 48 +++++++-------- masque/test/test_advanced_routing.py | 49 +++++++++------- masque/test/test_autotool.py | 51 ++++++++-------- masque/test/test_builder.py | 34 ++++++----- masque/test/test_fdfd.py | 9 ++- masque/test/test_gdsii.py | 39 ++++++------ masque/test/test_label.py | 54 +++++++++-------- masque/test/test_library.py | 65 +++++++++++--------- masque/test/test_oasis.py | 17 +++--- masque/test/test_pack2d.py | 32 +++++----- masque/test/test_path.py | 34 ++++++----- masque/test/test_pather.py | 39 ++++++------ masque/test/test_pattern.py | 65 ++++++++++---------- masque/test/test_polygon.py | 47 ++++++++++----- masque/test/test_ports.py | 88 ++++++++++++++++------------ masque/test/test_ports2data.py | 32 +++++----- masque/test/test_ref.py | 40 +++++++------ masque/test/test_renderpather.py | 32 +++++----- masque/test/test_repetition.py | 29 +++++---- masque/test/test_shape_advanced.py | 72 +++++++++++++---------- masque/test/test_shapes.py | 76 +++++++++++++----------- masque/test/test_utils.py | 58 +++++++++--------- 23 files changed, 544 insertions(+), 471 deletions(-) diff --git a/masque/test/conftest.py b/masque/test/conftest.py index 62db4c5..3116ee2 100644 --- a/masque/test/conftest.py +++ b/masque/test/conftest.py @@ -3,14 +3,11 @@ Test fixtures """ + # ruff: noqa: ARG001 from typing import Any import numpy -from numpy.typing import NDArray - -import pytest # type: ignore FixtureRequest = Any PRNG = numpy.random.RandomState(12345) - diff --git a/masque/test/test_abstract.py b/masque/test/test_abstract.py index c9e2926..907cedc 100644 --- a/masque/test/test_abstract.py +++ b/masque/test/test_abstract.py @@ -1,58 +1,60 @@ -import pytest -import numpy -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose from numpy import pi from ..abstract import Abstract from ..ports import Port from ..ref import Ref -def test_abstract_init(): + +def test_abstract_init() -> None: ports = {"A": Port((0, 0), 0), "B": Port((10, 0), pi)} abs_obj = Abstract("test", ports) assert abs_obj.name == "test" assert len(abs_obj.ports) == 2 - assert abs_obj.ports["A"] is not ports["A"] # Should be deepcopied + assert abs_obj.ports["A"] is not ports["A"] # Should be deepcopied -def test_abstract_transform(): + +def test_abstract_transform() -> None: abs_obj = Abstract("test", {"A": Port((10, 0), 0)}) # Rotate 90 deg around (0,0) - abs_obj.rotate_around((0, 0), pi/2) + abs_obj.rotate_around((0, 0), pi / 2) # (10, 0) rot 0 -> (0, 10) rot pi/2 assert_allclose(abs_obj.ports["A"].offset, [0, 10], atol=1e-10) - assert_allclose(abs_obj.ports["A"].rotation, pi/2, atol=1e-10) - + assert_allclose(abs_obj.ports["A"].rotation, pi / 2, atol=1e-10) + # Mirror across x axis (axis 0): flips y-offset abs_obj.mirror(0) # (0, 10) mirrored(0) -> (0, -10) # rotation pi/2 mirrored(0) -> -pi/2 == 3pi/2 assert_allclose(abs_obj.ports["A"].offset, [0, -10], atol=1e-10) - assert_allclose(abs_obj.ports["A"].rotation, 3*pi/2, atol=1e-10) + assert_allclose(abs_obj.ports["A"].rotation, 3 * pi / 2, atol=1e-10) -def test_abstract_ref_transform(): + +def test_abstract_ref_transform() -> None: abs_obj = Abstract("test", {"A": Port((10, 0), 0)}) - ref = Ref(offset=(100, 100), rotation=pi/2, mirrored=True) - + ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True) + # Apply ref transform abs_obj.apply_ref_transform(ref) # Ref order: mirror, rotate, scale, translate - + # 1. mirror (across x: y -> -y) # (10, 0) rot 0 -> (10, 0) rot 0 - + # 2. rotate pi/2 around (0,0) # (10, 0) rot 0 -> (0, 10) rot pi/2 - + # 3. translate (100, 100) # (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(): - abs_obj = Abstract("test", {"A": Port((100, 110), pi/2)}) - ref = Ref(offset=(100, 100), rotation=pi/2, mirrored=True) - + 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() -> None: + abs_obj = Abstract("test", {"A": Port((100, 110), pi / 2)}) + ref = Ref(offset=(100, 100), rotation=pi / 2, mirrored=True) + abs_obj.undo_ref_transform(ref) assert_allclose(abs_obj.ports["A"].offset, [10, 0], atol=1e-10) assert_allclose(abs_obj.ports["A"].rotation, 0, atol=1e-10) diff --git a/masque/test/test_advanced_routing.py b/masque/test/test_advanced_routing.py index 439073d..5afcc21 100644 --- a/masque/test/test_advanced_routing.py +++ b/masque/test/test_advanced_routing.py @@ -1,6 +1,5 @@ import pytest -import numpy -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_equal from numpy import pi from ..builder import Pather @@ -8,30 +7,33 @@ from ..builder.tools import PathTool from ..library import Library from ..ports import Port + @pytest.fixture -def advanced_pather(): +def advanced_pather() -> tuple[Pather, PathTool, Library]: lib = Library() # Simple PathTool: 2um width on layer (1,0) tool = PathTool(layer=(1, 0), width=2, ptype="wire") p = Pather(lib, tools=tool) return p, tool, lib -def test_path_into_straight(advanced_pather): + +def test_path_into_straight(advanced_pather: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = advanced_pather # Facing ports - p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device) + p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device) # Forward (+pi relative to port) is West (-x). # Put destination at (-20, 0) pointing East (pi). p.ports["dst"] = Port((-20, 0), pi, ptype="wire") - + p.path_into("src", "dst") - + assert "src" not in p.ports assert "dst" not in p.ports # Pather.path adds a Reference to the generated pattern assert len(p.pattern.refs) == 1 -def test_path_into_bend(advanced_pather): + +def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = advanced_pather # Source at (0,0) rot 0 (facing East). Forward is West (-x). p.ports["src"] = Port((0, 0), 0, ptype="wire") @@ -39,44 +41,47 @@ def test_path_into_bend(advanced_pather): # Wait, src forward is -x. dst is at -20, -20. # To use a single bend, dst should be at some -x, -y and its rotation should be 3pi/2 (facing South). # Forward for South is North (+y). - p.ports["dst"] = Port((-20, -20), 3*pi/2, ptype="wire") - + p.ports["dst"] = Port((-20, -20), 3 * pi / 2, ptype="wire") + p.path_into("src", "dst") - + assert "src" 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) assert len(p.pattern.refs) == 2 -def test_path_into_sbend(advanced_pather): + +def test_path_into_sbend(advanced_pather: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = advanced_pather # Facing but offset ports - p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x) - p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi) - + p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x) + p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi) + p.path_into("src", "dst") - + assert "src" not in p.ports assert "dst" not in p.ports -def test_path_from(advanced_pather): + +def test_path_from(advanced_pather: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = advanced_pather p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire") - + p.at("dst").path_from("src") - + assert "src" not in p.ports assert "dst" not in p.ports -def test_path_into_thru(advanced_pather): + +def test_path_into_thru(advanced_pather: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = advanced_pather p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire") p.ports["other"] = Port((10, 10), 0) - + p.path_into("src", "dst", thru="other") - + assert "src" in p.ports assert_equal(p.ports["src"].offset, [10, 10]) assert "other" not in p.ports diff --git a/masque/test/test_autotool.py b/masque/test/test_autotool.py index cf730ae..5686193 100644 --- a/masque/test/test_autotool.py +++ b/masque/test/test_autotool.py @@ -1,6 +1,5 @@ import pytest -import numpy -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose from numpy import pi from ..builder import Pather @@ -8,75 +7,75 @@ from ..builder.tools import AutoTool from ..library import Library from ..pattern import Pattern from ..ports import Port -from ..abstract import Abstract -def make_straight(length, width=2, ptype="wire"): + +def make_straight(length: float, width: float = 2, ptype: str = "wire") -> Pattern: pat = Pattern() pat.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width) pat.ports["in"] = Port((0, 0), 0, ptype=ptype) pat.ports["out"] = Port((length, 0), pi, ptype=ptype) return pat + @pytest.fixture -def autotool_setup(): +def autotool_setup() -> tuple[Pather, AutoTool, Library]: lib = Library() - + # Define a simple bend bend_pat = Pattern() # 2x2 bend from (0,0) rot 0 to (2, -2) rot pi/2 (Clockwise) bend_pat.ports["in"] = Port((0, 0), 0, ptype="wire") - bend_pat.ports["out"] = Port((2, -2), pi/2, ptype="wire") + bend_pat.ports["out"] = Port((2, -2), pi / 2, ptype="wire") lib["bend"] = bend_pat - bend_abs = lib.abstract("bend") - + lib.abstract("bend") + # Define a transition (e.g., via) via_pat = Pattern() via_pat.ports["m1"] = Port((0, 0), 0, ptype="wire_m1") via_pat.ports["m2"] = Port((1, 0), pi, ptype="wire_m2") lib["via"] = via_pat via_abs = lib.abstract("via") - + tool_m1 = AutoTool( - straights=[AutoTool.Straight(ptype="wire_m1", fn=lambda l: make_straight(l, ptype="wire_m1"), - in_port_name="in", out_port_name="out")], + straights=[ + AutoTool.Straight(ptype="wire_m1", fn=lambda length: make_straight(length, ptype="wire_m1"), in_port_name="in", out_port_name="out") + ], bends=[], sbends=[], - transitions={ - ("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1") - }, - default_out_ptype="wire_m1" + transitions={("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1")}, + default_out_ptype="wire_m1", ) - + p = Pather(lib, tools=tool_m1) # Start with an m2 port p.ports["start"] = Port((0, 0), pi, ptype="wire_m2") - + return p, tool_m1, lib -def test_autotool_transition(autotool_setup): + +def test_autotool_transition(autotool_setup: tuple[Pather, AutoTool, Library]) -> None: p, tool, lib = autotool_setup - + # Route m1 from an m2 port. Should trigger via. # length 10. Via length is 1. So straight m1 should be 9. p.path("start", ccw=None, length=10) - + # Start at (0,0) rot pi (facing West). # Forward (+pi relative to port) is East (+x). # 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. # 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. - # 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). # (-1, 0) + (-9, 0) = (-10, 0)? No. # Let's re-calculate. # start (0,0) rot pi. Direction East. - # via m2 is at (0,0), m1 is at (1,0). - # When via is plugged into start: m2 goes to (0,0). + # via m2 is at (0,0), m1 is at (1,0). + # When via is plugged into start: m2 goes to (0,0). # since start is pi and m2 is pi, rotation is 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. - + assert_allclose(p.ports["start"].offset, [10, 0], atol=1e-10) assert p.ports["start"].ptype == "wire_m1" - diff --git a/masque/test/test_builder.py b/masque/test/test_builder.py index 0884c3f..1b67c65 100644 --- a/masque/test/test_builder.py +++ b/masque/test/test_builder.py @@ -1,5 +1,3 @@ -import pytest -import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi @@ -8,36 +6,39 @@ from ..library import Library from ..pattern import Pattern from ..ports import Port -def test_builder_init(): + +def test_builder_init() -> None: lib = Library() b = Builder(lib, name="mypat") assert b.pattern is lib["mypat"] assert b.library is lib -def test_builder_place(): + +def test_builder_place() -> None: lib = Library() child = Pattern() child.ports["A"] = Port((0, 0), 0) lib["child"] = child - + b = Builder(lib) b.place("child", offset=(10, 20), port_map={"A": "child_A"}) - + assert "child_A" in b.ports assert_equal(b.ports["child_A"].offset, [10, 20]) assert "child" in b.pattern.refs -def test_builder_plug(): + +def test_builder_plug() -> None: lib = Library() - + wire = Pattern() wire.ports["in"] = Port((0, 0), 0) wire.ports["out"] = Port((10, 0), pi) lib["wire"] = wire - + b = Builder(lib) b.ports["start"] = Port((100, 100), 0) - + # 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 # 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) # so wire in is at (100, 100), wire out is at (100 - 10, 100) = (90, 100) b.plug("wire", map_in={"start": "in"}) - + assert "start" in b.ports assert_equal(b.ports["start"].offset, [90, 100]) assert_allclose(b.ports["start"].rotation, 0, atol=1e-10) -def test_builder_interface(): + +def test_builder_interface() -> None: lib = Library() source = Pattern() source.ports["P1"] = Port((0, 0), 0) lib["source"] = source - + b = Builder.interface("source", library=lib, name="iface") assert "in_P1" in b.ports assert "P1" in b.ports assert b.pattern is lib["iface"] -def test_builder_set_dead(): + +def test_builder_set_dead() -> None: lib = Library() lib["sub"] = Pattern() b = Builder(lib) b.set_dead() - + b.place("sub") assert not b.pattern.has_refs() - diff --git a/masque/test/test_fdfd.py b/masque/test/test_fdfd.py index 32466c1..2b4f3d3 100644 --- a/masque/test/test_fdfd.py +++ b/masque/test/test_fdfd.py @@ -3,23 +3,22 @@ import dataclasses -import pytest # type: ignore +import pytest # type: ignore import numpy from numpy import pi from numpy.typing import NDArray -#from numpy.testing import assert_allclose, assert_array_equal +# from numpy.testing import assert_allclose, assert_array_equal from .. import Pattern, Arc, Circle def test_circle_mirror(): cc = Circle(radius=4, offset=(10, 20)) - cc.flip_across(axis=0) # flip across y=0 + cc.flip_across(axis=0) # flip across y=0 assert cc.offset[0] == 10 assert cc.offset[1] == -20 assert cc.radius == 4 - cc.flip_across(axis=1) # flip across x=0 + cc.flip_across(axis=1) # flip across x=0 assert cc.offset[0] == -10 assert cc.offset[1] == -20 assert cc.radius == 4 - diff --git a/masque/test/test_gdsii.py b/masque/test/test_gdsii.py index 7a80329..86e4bbc 100644 --- a/masque/test/test_gdsii.py +++ b/masque/test/test_gdsii.py @@ -1,70 +1,69 @@ -import pytest -import os +from pathlib import Path import numpy from numpy.testing import assert_equal, assert_allclose -from pathlib import Path from ..pattern import Pattern from ..library import Library from ..file import gdsii -from ..shapes import Polygon, Path as MPath +from ..shapes import Path as MPath -def test_gdsii_roundtrip(tmp_path): + +def test_gdsii_roundtrip(tmp_path: Path) -> None: lib = Library() - + # Simple polygon cell pat1 = Pattern() pat1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]]) lib["poly_cell"] = pat1 - + # Path cell pat2 = Pattern() pat2.path((2, 5), vertices=[[0, 0], [100, 0]], width=10) lib["path_cell"] = pat2 - + # Cell with Ref pat3 = Pattern() - pat3.ref("poly_cell", offset=(50, 50), rotation=numpy.pi/2) + pat3.ref("poly_cell", offset=(50, 50), rotation=numpy.pi / 2) lib["ref_cell"] = pat3 - + gds_file = tmp_path / "test.gds" gdsii.writefile(lib, gds_file, meters_per_unit=1e-9) - + read_lib, info = gdsii.readfile(gds_file) - + assert "poly_cell" in read_lib assert "path_cell" in read_lib assert "ref_cell" in read_lib - + # Check polygon read_poly = read_lib["poly_cell"].shapes[(1, 0)][0] # GDSII closes polygons, so it might have an extra vertex or different order assert len(read_poly.vertices) >= 4 # Check bounds as a proxy for geometry correctness assert_equal(read_lib["poly_cell"].get_bounds(), [[0, 0], [10, 10]]) - + # Check path read_path = read_lib["path_cell"].shapes[(2, 5)][0] assert isinstance(read_path, MPath) assert read_path.width == 10 assert_equal(read_path.vertices, [[0, 0], [100, 0]]) - + # Check Ref read_ref = read_lib["ref_cell"].refs["poly_cell"][0] assert_equal(read_ref.offset, [50, 50]) - assert_allclose(read_ref.rotation, numpy.pi/2, atol=1e-5) + assert_allclose(read_ref.rotation, numpy.pi / 2, atol=1e-5) -def test_gdsii_annotations(tmp_path): + +def test_gdsii_annotations(tmp_path: Path) -> None: lib = Library() pat = Pattern() # GDS only supports integer keys in range [1, 126] for properties pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]], annotations={"1": ["hello"]}) lib["cell"] = pat - + gds_file = tmp_path / "test_ann.gds" gdsii.writefile(lib, gds_file, meters_per_unit=1e-9) - + read_lib, _ = gdsii.readfile(gds_file) read_ann = read_lib["cell"].shapes[(1, 0)][0].annotations assert read_ann["1"] == ["hello"] - diff --git a/masque/test/test_label.py b/masque/test/test_label.py index 8753be3..ed40614 100644 --- a/masque/test/test_label.py +++ b/masque/test/test_label.py @@ -1,48 +1,50 @@ -import pytest -import numpy +import copy from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..label import Label from ..repetition import Grid -def test_label_init(): - l = Label("test", offset=(10, 20)) - assert l.string == "test" - assert_equal(l.offset, [10, 20]) -def test_label_transform(): - l = Label("test", offset=(10, 0)) +def test_label_init() -> None: + lbl = Label("test", offset=(10, 20)) + assert lbl.string == "test" + assert_equal(lbl.offset, [10, 20]) + + +def test_label_transform() -> None: + lbl = Label("test", offset=(10, 0)) # Rotate 90 deg CCW around (0,0) - l.rotate_around((0, 0), pi/2) - assert_allclose(l.offset, [0, 10], atol=1e-10) - - # Translate - l.translate((5, 5)) - assert_allclose(l.offset, [5, 15], atol=1e-10) + lbl.rotate_around((0, 0), pi / 2) + assert_allclose(lbl.offset, [0, 10], 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) - l = Label("rep", offset=(0, 0), repetition=rep) - assert l.repetition is rep - assert_equal(l.get_bounds_single(), [[0, 0], [0, 0]]) - # Note: Bounded.get_bounds_nonempty() for labels with repetition doesn't - # seem to automatically include repetition bounds in label.py itself, + lbl = Label("rep", offset=(0, 0), repetition=rep) + assert lbl.repetition is rep + assert_equal(lbl.get_bounds_single(), [[0, 0], [0, 0]]) + # Note: Bounded.get_bounds_nonempty() for labels with repetition doesn't + # seem to automatically include repetition bounds in label.py itself, # it's handled during pattern bounding. -def test_label_copy(): + +def test_label_copy() -> None: l1 = Label("test", offset=(1, 2), annotations={"a": [1]}) l2 = copy.deepcopy(l1) - + 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}") - + from ..utils import annotations_eq + print(f"annotations_eq: {annotations_eq(l1.annotations, l2.annotations)}") - + assert l1 == l2 assert l1 is not l2 l2.offset[0] = 100 assert l1.offset[0] == 1 - -import copy diff --git a/masque/test/test_library.py b/masque/test/test_library.py index 2c411d4..0012219 100644 --- a/masque/test/test_library.py +++ b/masque/test/test_library.py @@ -1,109 +1,116 @@ import pytest -from ..library import Library, LazyLibrary, LibraryView +from ..library import Library, LazyLibrary from ..pattern import Pattern -from ..ref import Ref from ..error import LibraryError -def test_library_basic(): + +def test_library_basic() -> None: lib = Library() pat = Pattern() lib["cell1"] = pat - + assert "cell1" in lib assert lib["cell1"] is pat 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["child"] = Pattern() lib["parent"] = Pattern() lib["parent"].ref("child") - + assert set(lib.tops()) == {"parent"} assert lib.top() == "parent" -def test_library_dangling(): + +def test_library_dangling() -> None: lib = Library() lib["parent"] = Pattern() lib["parent"].ref("missing") - + assert lib.dangling_refs() == {"missing"} -def test_library_flatten(): + +def test_library_flatten() -> None: lib = Library() child = Pattern() child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) lib["child"] = child - + parent = Pattern() parent.ref("child", offset=(10, 10)) lib["parent"] = parent - + flat_lib = lib.flatten("parent") flat_parent = flat_lib["parent"] - + assert not flat_parent.has_refs() assert len(flat_parent.shapes[(1, 0)]) == 1 # Transformations are baked into vertices for Polygon assert_vertices = flat_parent.shapes[(1, 0)][0].vertices assert tuple(assert_vertices[0]) == (10.0, 10.0) -def test_lazy_library(): + +def test_lazy_library() -> None: lib = LazyLibrary() called = 0 - def make_pat(): + + def make_pat() -> Pattern: nonlocal called called += 1 return Pattern() - + lib["lazy"] = make_pat assert called == 0 - + pat = lib["lazy"] assert called == 1 assert isinstance(pat, Pattern) - + # Second access should be cached pat2 = lib["lazy"] assert called == 1 assert pat is pat2 -def test_library_rename(): + +def test_library_rename() -> None: lib = Library() lib["old"] = Pattern() lib["parent"] = Pattern() lib["parent"].ref("old") - + lib.rename("old", "new", move_references=True) - + assert "old" not in lib assert "new" in lib assert "new" in lib["parent"].refs assert "old" not in lib["parent"].refs -def test_library_subtree(): + +def test_library_subtree() -> None: lib = Library() lib["a"] = Pattern() lib["b"] = Pattern() lib["c"] = Pattern() lib["a"].ref("b") - + sub = lib.subtree("a") assert "a" in sub assert "b" in sub assert "c" not in sub -def test_library_get_name(): + +def test_library_get_name() -> None: lib = Library() lib["cell"] = Pattern() - + name1 = lib.get_name("cell") assert name1 != "cell" assert name1.startswith("cell") - + name2 = lib.get_name("other") assert name2 == "other" - diff --git a/masque/test/test_oasis.py b/masque/test/test_oasis.py index 5f60fd8..faffa58 100644 --- a/masque/test/test_oasis.py +++ b/masque/test/test_oasis.py @@ -1,28 +1,27 @@ -import pytest -import numpy -from numpy.testing import assert_equal from pathlib import Path +import pytest +from numpy.testing import assert_equal from ..pattern import Pattern from ..library import Library from ..file import oasis -def test_oasis_roundtrip(tmp_path): + +def test_oasis_roundtrip(tmp_path: Path) -> None: # Skip if fatamorgana is not installed pytest.importorskip("fatamorgana") - + lib = Library() pat1 = Pattern() pat1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]]) lib["cell1"] = pat1 - + oas_file = tmp_path / "test.oas" # OASIS needs units_per_micron oasis.writefile(lib, oas_file, units_per_micron=1000) - + read_lib, info = oasis.readfile(oas_file) assert "cell1" in read_lib - + # Check bounds assert_equal(read_lib["cell1"].get_bounds(), [[0, 0], [10, 10]]) - diff --git a/masque/test/test_pack2d.py b/masque/test/test_pack2d.py index 31d19e0..5390a4c 100644 --- a/masque/test/test_pack2d.py +++ b/masque/test/test_pack2d.py @@ -1,53 +1,51 @@ -import pytest -import numpy -from numpy.testing import assert_equal - from ..utils.pack2d import maxrects_bssf, pack_patterns from ..library import Library from ..pattern import Pattern -def test_maxrects_bssf_simple(): + +def test_maxrects_bssf_simple() -> None: # Pack two 10x10 squares into one 20x10 container rects = [[10, 10], [10, 10]] containers = [[0, 0, 20, 10]] - + locs, rejects = maxrects_bssf(rects, containers) - + assert not rejects # They should be at (0,0) and (10,0) - assert set([tuple(l) for l in locs]) == {(0.0, 0.0), (10.0, 0.0)} + assert {tuple(loc) for loc in locs} == {(0.0, 0.0), (10.0, 0.0)} -def test_maxrects_bssf_reject(): + +def test_maxrects_bssf_reject() -> None: # Try to pack a too-large rectangle rects = [[10, 10], [30, 30]] containers = [[0, 0, 20, 20]] - + locs, rejects = maxrects_bssf(rects, containers, allow_rejects=True) - assert 1 in rejects # Second rect rejected + assert 1 in rejects # Second rect rejected assert 0 not in rejects -def test_pack_patterns(): + +def test_pack_patterns() -> None: lib = Library() p1 = Pattern() p1.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10], [0, 10]]) lib["p1"] = p1 - + p2 = Pattern() p2.polygon((1, 0), vertices=[[0, 0], [5, 0], [5, 5], [0, 5]]) lib["p2"] = p2 - + # Containers: one 20x20 containers = [[0, 0, 20, 20]] # 2um spacing pat, rejects = pack_patterns(lib, ["p1", "p2"], containers, spacing=(2, 2)) - + assert not rejects assert len(pat.refs) == 2 assert "p1" in pat.refs assert "p2" in pat.refs - + # Check that they don't overlap (simple check via bounds) # p1 size 10x10, effectively 12x12 # p2 size 5x5, effectively 7x7 # Both should fit in 20x20 - diff --git a/masque/test/test_path.py b/masque/test/test_path.py index 5d63565..766798f 100644 --- a/masque/test/test_path.py +++ b/masque/test/test_path.py @@ -1,17 +1,16 @@ -import pytest -import numpy -from numpy.testing import assert_equal, assert_allclose -from numpy import pi +from numpy.testing import assert_equal from ..shapes import Path -def test_path_init(): + +def test_path_init() -> None: p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Flush) assert_equal(p.vertices, [[0, 0], [10, 0]]) assert p.width == 2 assert p.cap == Path.Cap.Flush -def test_path_to_polygons_flush(): + +def test_path_to_polygons_flush() -> None: p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Flush) polys = p.to_polygons() assert len(polys) == 1 @@ -19,7 +18,8 @@ def test_path_to_polygons_flush(): bounds = polys[0].get_bounds_single() assert_equal(bounds, [[0, -1], [10, 1]]) -def test_path_to_polygons_square(): + +def test_path_to_polygons_square() -> None: p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Square) polys = p.to_polygons() assert len(polys) == 1 @@ -28,18 +28,20 @@ def test_path_to_polygons_square(): bounds = polys[0].get_bounds_single() assert_equal(bounds, [[-1, -1], [11, 1]]) -def test_path_to_polygons_circle(): + +def test_path_to_polygons_circle() -> None: p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.Circle) polys = p.to_polygons(num_vertices=32) # Path.to_polygons for Circle cap returns 1 polygon for the path + polygons for the caps assert len(polys) >= 3 - + # Combined bounds should be from (-1, -1) to (11, 1) # But wait, Path.get_bounds_single() handles this more directly bounds = p.get_bounds_single() assert_equal(bounds, [[-1, -1], [11, 1]]) -def test_path_custom_cap(): + +def test_path_custom_cap() -> None: p = Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(5, 10)) polys = p.to_polygons() assert len(polys) == 1 @@ -48,7 +50,8 @@ def test_path_custom_cap(): bounds = polys[0].get_bounds_single() assert_equal(bounds, [[-5, -1], [20, 1]]) -def test_path_bend(): + +def test_path_bend() -> None: # L-shaped path p = Path(vertices=[[0, 0], [10, 0], [10, 10]], width=2) polys = p.to_polygons() @@ -64,14 +67,15 @@ def test_path_bend(): # So bounds should be x: [0, 11], y: [-1, 10] assert_equal(bounds, [[0, -1], [11, 10]]) -def test_path_mirror(): + +def test_path_mirror() -> None: p = Path(vertices=[[10, 5], [20, 10]], width=2) - p.mirror(0) # Mirror across x axis (y -> -y) + p.mirror(0) # Mirror across x axis (y -> -y) assert_equal(p.vertices, [[10, -5], [20, -10]]) -def test_path_scale(): + +def test_path_scale() -> None: p = Path(vertices=[[0, 0], [10, 0]], width=2) p.scale_by(2) assert_equal(p.vertices, [[0, 0], [20, 0]]) assert p.width == 4 - diff --git a/masque/test/test_pather.py b/masque/test/test_pather.py index 814c583..e1d28d8 100644 --- a/masque/test/test_pather.py +++ b/masque/test/test_pather.py @@ -1,16 +1,15 @@ import pytest -import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..builder import Pather from ..builder.tools import PathTool from ..library import Library -from ..pattern import Pattern from ..ports import Port + @pytest.fixture -def pather_setup(): +def pather_setup() -> tuple[Pather, PathTool, Library]: lib = Library() # Simple PathTool: 2um width on layer (1,0) tool = PathTool(layer=(1, 0), width=2, ptype="wire") @@ -18,53 +17,58 @@ def pather_setup(): # Add an initial port facing North (pi/2) # Port rotation points INTO device. So "North" rotation means device is North of port. # Pathing "forward" moves South. - p.ports["start"] = Port((0, 0), pi/2, ptype="wire") + p.ports["start"] = Port((0, 0), pi / 2, ptype="wire") return p, tool, lib -def test_pather_straight(pather_setup): + +def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Route 10um "forward" p.path("start", ccw=None, length=10) - + # port rot pi/2 (North). Travel +pi relative to port -> South. assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) - assert_allclose(p.ports["start"].rotation, pi/2, atol=1e-10) + assert_allclose(p.ports["start"].rotation, pi / 2, atol=1e-10) -def test_pather_bend(pather_setup): + +def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Start (0,0) rot pi/2 (North). # Path 10um "forward" (South), then turn Clockwise (ccw=False). # Facing South, turn Right -> West. 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. # Transformed by port rot pi/2 (North) + pi (to move "forward" away from device): # Transformation rot = pi/2 + pi = 3pi/2. # (10, -1) rotated 3pi/2: (x,y) -> (y, -x) -> (-1, -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. assert_allclose(p.ports["start"].rotation, 0, atol=1e-10) -def test_pather_path_to(pather_setup): + +def test_pather_path_to(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # start at (0,0) rot pi/2 (North) # path "forward" (South) to y=-50 p.path_to("start", ccw=None, y=-50) assert_equal(p.ports["start"].offset, [0, -50]) -def test_pather_mpath(pather_setup): + +def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup - p.ports["A"] = Port((0, 0), pi/2, ptype="wire") - p.ports["B"] = Port((10, 0), pi/2, ptype="wire") - + p.ports["A"] = Port((0, 0), pi / 2, ptype="wire") + p.ports["B"] = Port((10, 0), pi / 2, ptype="wire") + # Path both "forward" (South) to y=-20 p.mpath(["A", "B"], ccw=None, ymin=-20) assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["B"].offset, [10, -20]) -def test_pather_at_chaining(pather_setup): + +def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Fluent API test p.at("start").path(ccw=None, length=10).path(ccw=True, length=10) @@ -77,4 +81,3 @@ def test_pather_at_chaining(pather_setup): # pi/2 (North) + CCW (90 deg) -> 0 (East)? # Actual behavior results in pi (West). assert_allclose(p.ports["start"].rotation, pi, atol=1e-10) - diff --git a/masque/test/test_pattern.py b/masque/test/test_pattern.py index f18913c..e66e9d5 100644 --- a/masque/test/test_pattern.py +++ b/masque/test/test_pattern.py @@ -1,15 +1,14 @@ -import pytest -import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..pattern import Pattern -from ..shapes import Polygon, Circle +from ..shapes import Polygon from ..ref import Ref from ..ports import Port from ..label import Label -def test_pattern_init(): + +def test_pattern_init() -> None: pat = Pattern() assert pat.is_empty() assert not pat.has_shapes() @@ -17,19 +16,15 @@ def test_pattern_init(): assert not pat.has_labels() assert not pat.has_ports() -def test_pattern_with_elements(): + +def test_pattern_with_elements() -> None: poly = Polygon.square(10) label = Label("test", offset=(5, 5)) ref = Ref(offset=(100, 100)) port = Port((0, 0), 0) - - pat = Pattern( - shapes={(1, 0): [poly]}, - labels={(1, 2): [label]}, - refs={"sub": [ref]}, - ports={"P1": port} - ) - + + pat = Pattern(shapes={(1, 0): [poly]}, labels={(1, 2): [label]}, refs={"sub": [ref]}, ports={"P1": port}) + assert pat.has_shapes() assert pat.has_labels() assert pat.has_refs() @@ -40,72 +35,78 @@ def test_pattern_with_elements(): assert pat.refs["sub"] == [ref] assert pat.ports["P1"] == port -def test_pattern_append(): + +def test_pattern_append() -> None: pat1 = Pattern() pat1.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]]) - + pat2 = Pattern() pat2.polygon((2, 0), vertices=[[10, 10], [11, 10], [11, 11]]) - + pat1.append(pat2) assert len(pat1.shapes[(1, 0)]) == 1 assert len(pat1.shapes[(2, 0)]) == 1 -def test_pattern_translate(): + +def test_pattern_translate() -> None: pat = Pattern() pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [1, 1]]) pat.ports["P1"] = Port((5, 5), 0) - + pat.translate_elements((10, 20)) - + # Polygon.translate adds to vertices, and offset is always (0,0) assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, 20]) assert_equal(pat.ports["P1"].offset, [15, 25]) -def test_pattern_scale(): + +def test_pattern_scale() -> None: pat = Pattern() # Polygon.rect sets an offset in its constructor which is immediately translated into vertices pat.rect((1, 0), xmin=0, xmax=1, ymin=0, ymax=1) pat.scale_by(2) - + # Vertices should be scaled assert_equal(pat.shapes[(1, 0)][0].vertices, [[0, 0], [0, 2], [2, 2], [2, 0]]) -def test_pattern_rotate(): + +def test_pattern_rotate() -> None: pat = Pattern() pat.polygon((1, 0), vertices=[[10, 0], [11, 0], [10, 1]]) # Rotate 90 degrees CCW around (0,0) - pat.rotate_around((0, 0), pi/2) - + pat.rotate_around((0, 0), pi / 2) + # [10, 0] rotated 90 deg around (0,0) is [0, 10] assert_allclose(pat.shapes[(1, 0)][0].vertices[0], [0, 10], atol=1e-10) -def test_pattern_mirror(): + +def test_pattern_mirror() -> None: pat = Pattern() pat.polygon((1, 0), vertices=[[10, 5], [11, 5], [10, 6]]) # Mirror across X axis (y -> -y) pat.mirror(0) - + assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, -5]) -def test_pattern_get_bounds(): + +def test_pattern_get_bounds() -> None: pat = Pattern() pat.polygon((1, 0), vertices=[[0, 0], [10, 0], [10, 10]]) pat.polygon((1, 0), vertices=[[-5, -5], [5, -5], [5, 5]]) - + bounds = pat.get_bounds() assert_equal(bounds, [[-5, -5], [10, 10]]) -def test_pattern_interface(): + +def test_pattern_interface() -> None: source = Pattern() source.ports["A"] = Port((10, 20), 0, ptype="test") - + iface = Pattern.interface(source, in_prefix="in_", out_prefix="out_") - + assert "in_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["out_A"].rotation, 0, atol=1e-10) assert iface.ports["in_A"].ptype == "test" assert iface.ports["out_A"].ptype == "test" - diff --git a/masque/test/test_polygon.py b/masque/test/test_polygon.py index 1ae1ff1..5d98ad9 100644 --- a/masque/test/test_polygon.py +++ b/masque/test/test_polygon.py @@ -1,6 +1,6 @@ import pytest import numpy -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_equal from ..shapes import Polygon @@ -9,29 +9,36 @@ from ..error import PatternError @pytest.fixture -def polygon(): +def polygon() -> Polygon: return Polygon([[0, 0], [1, 0], [1, 1], [0, 1]]) -def test_vertices(polygon) -> None: + +def test_vertices(polygon: Polygon) -> None: assert_equal(polygon.vertices, [[0, 0], [1, 0], [1, 1], [0, 1]]) -def test_xs(polygon) -> None: + +def test_xs(polygon: Polygon) -> None: assert_equal(polygon.xs, [0, 1, 1, 0]) -def test_ys(polygon) -> None: + +def test_ys(polygon: Polygon) -> None: assert_equal(polygon.ys, [0, 0, 1, 1]) -def test_offset(polygon) -> None: + +def test_offset(polygon: Polygon) -> None: assert_equal(polygon.offset, [0, 0]) + def test_square() -> None: square = Polygon.square(1) assert_equal(square.vertices, [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]]) + def test_rectangle() -> None: rectangle = Polygon.rectangle(1, 2) assert_equal(rectangle.vertices, [[-0.5, -1], [-0.5, 1], [0.5, 1], [0.5, -1]]) + def test_rect() -> None: rect1 = Polygon.rect(xmin=0, xmax=1, ymin=-1, ymax=1) assert_equal(rect1.vertices, [[0, -1], [0, 1], [1, 1], [1, -1]]) @@ -64,47 +71,55 @@ def test_rect() -> None: def test_octagon() -> None: - octagon = Polygon.octagon(side_length=1) # regular=True + octagon = Polygon.octagon(side_length=1) # regular=True assert_equal(octagon.vertices.shape, (8, 2)) diff = octagon.vertices - numpy.roll(octagon.vertices, -1, axis=0) side_len = numpy.sqrt((diff * diff).sum(axis=1)) assert numpy.allclose(side_len, 1) -def test_to_polygons(polygon) -> None: + +def test_to_polygons(polygon: Polygon) -> None: assert polygon.to_polygons() == [polygon] -def test_get_bounds_single(polygon) -> None: + +def test_get_bounds_single(polygon: Polygon) -> None: assert_equal(polygon.get_bounds_single(), [[0, 0], [1, 1]]) -def test_rotate(polygon) -> None: + +def test_rotate(polygon: Polygon) -> None: rotated_polygon = polygon.rotate(R90) assert_equal(rotated_polygon.vertices, [[0, 0], [0, 1], [-1, 1], [-1, 0]]) -def test_mirror(polygon) -> None: + +def test_mirror(polygon: Polygon) -> None: mirrored_by_y = polygon.deepcopy().mirror(1) assert_equal(mirrored_by_y.vertices, [[0, 0], [-1, 0], [-1, 1], [0, 1]]) print(polygon.vertices) mirrored_by_x = polygon.deepcopy().mirror(0) - assert_equal(mirrored_by_x.vertices, [[0, 0], [ 1, 0], [1, -1], [0, -1]]) + assert_equal(mirrored_by_x.vertices, [[0, 0], [1, 0], [1, -1], [0, -1]]) -def test_scale_by(polygon) -> None: + +def test_scale_by(polygon: Polygon) -> None: scaled_polygon = polygon.scale_by(2) assert_equal(scaled_polygon.vertices, [[0, 0], [2, 0], [2, 2], [0, 2]]) -def test_clean_vertices(polygon) -> None: + +def test_clean_vertices(polygon: Polygon) -> None: polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, -4], [2, 0], [0, 0]]).clean_vertices() assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]]) + def test_remove_duplicate_vertices() -> None: polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, 0], [0, 0]]).remove_duplicate_vertices() assert_equal(polygon.vertices, [[0, 0], [1, 1], [2, 2], [2, 0]]) + def test_remove_colinear_vertices() -> None: polygon = Polygon([[0, 0], [1, 1], [2, 2], [2, 2], [2, 0], [0, 0]]).remove_colinear_vertices() assert_equal(polygon.vertices, [[0, 0], [2, 2], [2, 0]]) -def test_vertices_dtype(): + +def test_vertices_dtype() -> None: polygon = Polygon(numpy.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], dtype=numpy.int32)) polygon.scale_by(0.5) assert_equal(polygon.vertices, [[0, 0], [0.5, 0], [0.5, 0.5], [0, 0.5], [0, 0]]) - diff --git a/masque/test/test_ports.py b/masque/test/test_ports.py index b6031b2..4354bff 100644 --- a/masque/test/test_ports.py +++ b/masque/test/test_ports.py @@ -1,87 +1,101 @@ import pytest -import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..ports import Port, PortList from ..error import PortError -def test_port_init(): - p = Port(offset=(10, 20), rotation=pi/2, ptype="test") + +def test_port_init() -> None: + p = Port(offset=(10, 20), rotation=pi / 2, ptype="test") assert_equal(p.offset, [10, 20]) - assert p.rotation == pi/2 + assert p.rotation == pi / 2 assert p.ptype == "test" -def test_port_transform(): + +def test_port_transform() -> None: 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.rotation, pi/2, atol=1e-10) - - p.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset + assert_allclose(p.rotation, pi / 2, atol=1e-10) + + p.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset assert_allclose(p.offset, [0, 10], atol=1e-10) # rotation was pi/2 (90 deg), mirror across x (0 deg) -> -pi/2 == 3pi/2 - assert_allclose(p.rotation, 3*pi/2, atol=1e-10) + assert_allclose(p.rotation, 3 * pi / 2, atol=1e-10) -def test_port_flip_across(): + +def test_port_flip_across() -> None: p = Port(offset=(10, 0), rotation=0) - p.flip_across(axis=1) # Mirror across x=0: flips x-offset + p.flip_across(axis=1) # Mirror across x=0: flips x-offset assert_equal(p.offset, [-10, 0]) # rotation was 0, mirrored(1) -> pi assert_allclose(p.rotation, pi, atol=1e-10) -def test_port_measure_travel(): + +def test_port_measure_travel() -> None: p1 = Port((0, 0), 0) - p2 = Port((10, 5), pi) # Facing each other - + p2 = Port((10, 5), pi) # Facing each other + (travel, jog), rotation = p1.measure_travel(p2) assert travel == 10 assert jog == 5 assert rotation == pi -def test_port_list_rename(): + +def test_port_list_rename() -> None: class MyPorts(PortList): - def __init__(self): + def __init__(self) -> None: self._ports = {"A": Port((0, 0), 0)} + @property - def ports(self): return self._ports + def ports(self) -> dict[str, Port]: + return self._ports + @ports.setter - def ports(self, val): self._ports = val - + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + pl = MyPorts() pl.rename_ports({"A": "B"}) assert "A" not in pl.ports assert "B" in pl.ports -def test_port_list_plugged(): + +def test_port_list_plugged() -> None: class MyPorts(PortList): - def __init__(self): - self._ports = { - "A": Port((10, 10), 0), - "B": Port((10, 10), pi) - } + def __init__(self) -> None: + self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)} + @property - def ports(self): return self._ports + def ports(self) -> dict[str, Port]: + return self._ports + @ports.setter - def ports(self, val): self._ports = val - + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + pl = MyPorts() pl.plugged({"A": "B"}) - assert not pl.ports # Both should be removed + assert not pl.ports # Both should be removed -def test_port_list_plugged_mismatch(): + +def test_port_list_plugged_mismatch() -> None: class MyPorts(PortList): - def __init__(self): + def __init__(self) -> None: self._ports = { "A": Port((10, 10), 0), - "B": Port((11, 10), pi) # Offset mismatch + "B": Port((11, 10), pi), # Offset mismatch } + @property - def ports(self): return self._ports + def ports(self) -> dict[str, Port]: + return self._ports + @ports.setter - def ports(self, val): self._ports = val - + def ports(self, val: dict[str, Port]) -> None: + self._ports = val + pl = MyPorts() with pytest.raises(PortError): pl.plugged({"A": "B"}) - diff --git a/masque/test/test_ports2data.py b/masque/test/test_ports2data.py index bddc0d8..32bc367 100644 --- a/masque/test/test_ports2data.py +++ b/masque/test/test_ports2data.py @@ -1,4 +1,3 @@ -import pytest import numpy from numpy.testing import assert_allclose @@ -7,43 +6,45 @@ from ..pattern import Pattern from ..ports import Port from ..library import Library -def test_ports2data_roundtrip(): + +def test_ports2data_roundtrip() -> None: pat = Pattern() - pat.ports["P1"] = Port((10, 20), numpy.pi/2, ptype="test") - + pat.ports["P1"] = Port((10, 20), numpy.pi / 2, ptype="test") + layer = (10, 0) ports_to_data(pat, layer) - + assert len(pat.labels[layer]) == 1 assert pat.labels[layer][0].string == "P1:test 90" assert tuple(pat.labels[layer][0].offset) == (10.0, 20.0) - + # New pattern, read ports back pat2 = Pattern() pat2.labels[layer] = pat.labels[layer] data_to_ports([layer], {}, pat2) - + assert "P1" in pat2.ports assert_allclose(pat2.ports["P1"].offset, [10, 20], atol=1e-10) - assert_allclose(pat2.ports["P1"].rotation, numpy.pi/2, atol=1e-10) + assert_allclose(pat2.ports["P1"].rotation, numpy.pi / 2, atol=1e-10) assert pat2.ports["P1"].ptype == "test" -def test_data_to_ports_hierarchical(): + +def test_data_to_ports_hierarchical() -> None: lib = Library() - + # Child has port data in labels child = Pattern() layer = (10, 0) child.label(layer=layer, string="A:type1 0", offset=(5, 0)) lib["child"] = child - + # Parent references child parent = Pattern() - parent.ref("child", offset=(100, 100), rotation=numpy.pi/2) - + parent.ref("child", offset=(100, 100), rotation=numpy.pi / 2) + # Read ports hierarchically (max_depth > 0) data_to_ports([layer], lib, parent, max_depth=1) - + # child port A (5,0) rot 0 # transformed by parent ref: rot pi/2, trans (100, 100) # (5,0) rot pi/2 -> (0, 5) @@ -51,5 +52,4 @@ def test_data_to_ports_hierarchical(): # rot 0 + pi/2 = pi/2 assert "A" in parent.ports assert_allclose(parent.ports["A"].offset, [100, 105], atol=1e-10) - assert_allclose(parent.ports["A"].rotation, numpy.pi/2, atol=1e-10) - + assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10) diff --git a/masque/test/test_ref.py b/masque/test/test_ref.py index 4820874..8872699 100644 --- a/masque/test/test_ref.py +++ b/masque/test/test_ref.py @@ -1,5 +1,3 @@ -import pytest -import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi @@ -7,47 +5,51 @@ from ..pattern import Pattern from ..ref import Ref from ..repetition import Grid -def test_ref_init(): - ref = Ref(offset=(10, 20), rotation=pi/4, mirrored=True, scale=2.0) + +def test_ref_init() -> None: + ref = Ref(offset=(10, 20), rotation=pi / 4, mirrored=True, scale=2.0) assert_equal(ref.offset, [10, 20]) - assert ref.rotation == pi/4 + assert ref.rotation == pi / 4 assert ref.mirrored is True assert ref.scale == 2.0 -def test_ref_as_pattern(): + +def test_ref_as_pattern() -> None: sub_pat = Pattern() sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) - - ref = Ref(offset=(10, 10), rotation=pi/2, scale=2.0) + + ref = Ref(offset=(10, 10), rotation=pi / 2, scale=2.0) transformed_pat = ref.as_pattern(sub_pat) - + # Check transformed shape shape = transformed_pat.shapes[(1, 0)][0] # ref.as_pattern deepcopies sub_pat then applies transformations: # 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]] # 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) -def test_ref_with_repetition(): + +def test_ref_with_repetition() -> None: sub_pat = Pattern() sub_pat.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) - + rep = Grid(a_vector=(10, 0), b_vector=(0, 10), a_count=2, b_count=2) ref = Ref(repetition=rep) - + repeated_pat = ref.as_pattern(sub_pat) # Should have 4 shapes assert len(repeated_pat.shapes[(1, 0)]) == 4 - + first_verts = sorted([tuple(s.vertices[0]) for s in repeated_pat.shapes[(1, 0)]]) assert first_verts == [(0.0, 0.0), (0.0, 10.0), (10.0, 0.0), (10.0, 10.0)] -def test_ref_get_bounds(): + +def test_ref_get_bounds() -> None: sub_pat = Pattern() sub_pat.polygon((1, 0), vertices=[[0, 0], [5, 0], [0, 5]]) - + ref = Ref(offset=(10, 10), scale=2.0) bounds = ref.get_bounds_single(sub_pat) # sub_pat bounds [[0,0], [5,5]] @@ -55,12 +57,12 @@ def test_ref_get_bounds(): # translated [[10,10], [20,20]] assert_equal(bounds, [[10, 10], [20, 20]]) -def test_ref_copy(): + +def test_ref_copy() -> None: ref1 = Ref(offset=(1, 2), rotation=0.5, annotations={"a": [1]}) ref2 = ref1.copy() assert ref1 == ref2 assert ref1 is not ref2 - + ref2.offset[0] = 100 assert ref1.offset[0] == 1 - diff --git a/masque/test/test_renderpather.py b/masque/test/test_renderpather.py index e0479f9..b843066 100644 --- a/masque/test/test_renderpather.py +++ b/masque/test/test_renderpather.py @@ -1,6 +1,5 @@ import pytest -import numpy -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose from numpy import pi from ..builder import RenderPather @@ -8,28 +7,30 @@ from ..builder.tools import PathTool from ..library import Library from ..ports import Port + @pytest.fixture -def rpather_setup(): +def rpather_setup() -> tuple[RenderPather, PathTool, Library]: lib = Library() tool = PathTool(layer=(1, 0), width=2, ptype="wire") rp = RenderPather(lib, tools=tool) - rp.ports["start"] = Port((0, 0), pi/2, ptype="wire") + rp.ports["start"] = Port((0, 0), pi / 2, ptype="wire") return rp, tool, lib -def test_renderpather_basic(rpather_setup): + +def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan two segments rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10) - + # Before rendering, no shapes in pattern assert not rp.pattern.has_shapes() assert len(rp.paths["start"]) == 2 - + # Render rp.render() assert rp.pattern.has_shapes() assert len(rp.pattern.shapes[(1, 0)]) == 1 - + # Path vertices should be (0,0), (0,-10), (0,-20) # transformed by start port (rot pi/2 -> 270 deg transform) # 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_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10) -def test_renderpather_bend(rpather_setup): + +def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan straight then bend rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10) - + rp.render() path_shape = rp.pattern.shapes[(1, 0)][0] - # Path vertices: + # Path vertices: # 1. Start (0,0) # 2. Straight end: (0, -10) # 3. Bend end: (-1, -20) @@ -58,16 +60,16 @@ def test_renderpather_bend(rpather_setup): assert len(path_shape.vertices) == 4 assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20], [-1, -20]], atol=1e-10) -def test_renderpather_retool(rpather_setup): + +def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool1, lib = rpather_setup tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") - + rp.at("start").path(ccw=None, length=10) rp.retool(tool2, keys=["start"]) rp.at("start").path(ccw=None, length=10) - + rp.render() # Different tools should cause different batches/shapes assert len(rp.pattern.shapes[(1, 0)]) == 1 assert len(rp.pattern.shapes[(2, 0)]) == 1 - diff --git a/masque/test/test_repetition.py b/masque/test/test_repetition.py index 6b151e8..5ef2fa9 100644 --- a/masque/test/test_repetition.py +++ b/masque/test/test_repetition.py @@ -1,32 +1,35 @@ -import pytest -import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..repetition import Grid, Arbitrary -def test_grid_displacements(): + +def test_grid_displacements() -> None: # 2x2 grid grid = Grid(a_vector=(10, 0), b_vector=(0, 5), a_count=2, b_count=2) disps = sorted([tuple(d) for d in grid.displacements]) assert disps == [(0.0, 0.0), (0.0, 5.0), (10.0, 0.0), (10.0, 5.0)] -def test_grid_1d(): + +def test_grid_1d() -> None: grid = Grid(a_vector=(10, 0), a_count=3) disps = sorted([tuple(d) for d in grid.displacements]) assert disps == [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)] -def test_grid_rotate(): + +def test_grid_rotate() -> None: grid = Grid(a_vector=(10, 0), a_count=2) - grid.rotate(pi/2) + grid.rotate(pi / 2) assert_allclose(grid.a_vector, [0, 10], atol=1e-10) -def test_grid_get_bounds(): + +def test_grid_get_bounds() -> None: grid = Grid(a_vector=(10, 0), b_vector=(0, 5), a_count=2, b_count=2) bounds = grid.get_bounds() assert_equal(bounds, [[0, 0], [10, 5]]) -def test_arbitrary_displacements(): + +def test_arbitrary_displacements() -> None: pts = [[0, 0], [10, 20], [-5, 30]] arb = Arbitrary(pts) # They should be sorted by displacements.setter @@ -36,13 +39,13 @@ def test_arbitrary_displacements(): assert any((disps == [10, 20]).all(axis=1)) assert any((disps == [-5, 30]).all(axis=1)) -def test_arbitrary_transform(): + +def test_arbitrary_transform() -> None: arb = Arbitrary([[10, 0]]) - arb.rotate(pi/2) + arb.rotate(pi / 2) assert_allclose(arb.displacements, [[0, 10]], atol=1e-10) - - arb.mirror(0) # Mirror x across y axis? Wait, mirror(axis=0) in repetition.py is: + + arb.mirror(0) # Mirror x across y axis? Wait, mirror(axis=0) in repetition.py is: # self.displacements[:, 1 - axis] *= -1 # if axis=0, 1-axis=1, so y *= -1 assert_allclose(arb.displacements, [[0, -10]], atol=1e-10) - diff --git a/masque/test/test_shape_advanced.py b/masque/test/test_shape_advanced.py index ef73ea0..f6ba69d 100644 --- a/masque/test/test_shape_advanced.py +++ b/masque/test/test_shape_advanced.py @@ -1,16 +1,17 @@ +from pathlib import Path import pytest import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi -import os -from ..shapes import Arc, Ellipse, Circle, Polygon, Path, Text, PolyCollection +from ..shapes import Arc, Ellipse, Circle, Polygon, Path as MPath, Text, PolyCollection from ..error import PatternError + # 1. Text shape tests -def test_text_to_polygons(): +def test_text_to_polygons() -> None: font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf" - if not os.path.exists(font_path): + if not Path(font_path).exists(): pytest.skip("Font file not found") t = Text("Hi", height=10, font_path=font_path) @@ -24,8 +25,9 @@ def test_text_to_polygons(): char_x_means = [p.vertices[:, 0].mean() for p in polys] assert len(set(char_x_means)) >= 2 + # 2. Manhattanization tests -def test_manhattanize(): +def test_manhattanize() -> None: # Diamond shape poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]]) grid = numpy.arange(0, 11, 1) @@ -38,39 +40,43 @@ def test_manhattanize(): # For each segment, either dx or dy must be zero assert numpy.all((dv[:, 0] == 0) | (dv[:, 1] == 0)) + # 3. Comparison and Sorting tests -def test_shape_comparisons(): +def test_shape_comparisons() -> None: c1 = Circle(radius=10) c2 = Circle(radius=20) assert c1 < c2 assert not (c2 < c1) p1 = Polygon([[0, 0], [10, 0], [10, 10]]) - p2 = Polygon([[0, 0], [10, 0], [10, 11]]) # Different vertex + p2 = Polygon([[0, 0], [10, 0], [10, 11]]) # Different vertex assert p1 < p2 # Different types assert c1 < p1 or p1 < c1 assert (c1 < p1) != (p1 < c1) + # 4. Arc/Path Edge Cases -def test_arc_edge_cases(): +def test_arc_edge_cases() -> None: # Wrapped arc (> 360 deg) - a = Arc(radii=(10, 10), angles=(0, 3*pi), width=2) - polys = a.to_polygons(num_vertices=64) + a = Arc(radii=(10, 10), angles=(0, 3 * pi), width=2) + a.to_polygons(num_vertices=64) # Should basically be a ring bounds = a.get_bounds_single() assert_allclose(bounds, [[-11, -11], [11, 11]], atol=1e-10) -def test_path_edge_cases(): + +def test_path_edge_cases() -> None: # Zero-length segments - p = Path(vertices=[[0, 0], [0, 0], [10, 0]], width=2) + p = MPath(vertices=[[0, 0], [0, 0], [10, 0]], width=2) polys = p.to_polygons() assert len(polys) == 1 assert_equal(polys[0].get_bounds_single(), [[0, -1], [10, 1]]) + # 5. PolyCollection with holes -def test_poly_collection_holes(): +def test_poly_collection_holes() -> None: # Outer square, inner square hole # PolyCollection doesn't explicitly support holes, but its constituents (Polygons) do? # wait, Polygon in masque is just a boundary. Holes are usually handled by having multiple @@ -80,8 +86,14 @@ def test_poly_collection_holes(): # Let's test PolyCollection with multiple polygons verts = [ - [0, 0], [10, 0], [10, 10], [0, 10], # Poly 1 - [2, 2], [2, 8], [8, 8], [8, 2] # Poly 2 + [0, 0], + [10, 0], + [10, 10], + [0, 10], # Poly 1 + [2, 2], + [2, 8], + [8, 8], + [8, 2], # Poly 2 ] offsets = [0, 4] pc = PolyCollection(verts, offsets) @@ -90,17 +102,23 @@ def test_poly_collection_holes(): assert_equal(polys[0].vertices, [[0, 0], [10, 0], [10, 10], [0, 10]]) assert_equal(polys[1].vertices, [[2, 2], [2, 8], [8, 8], [8, 2]]) -def test_poly_collection_constituent_empty(): + +def test_poly_collection_constituent_empty() -> None: # One real triangle, one "empty" polygon (0 vertices), one real square # Note: Polygon requires 3 vertices, so "empty" here might mean just some junk # that to_polygons should handle. # Actually PolyCollection doesn't check vertex count per polygon. verts = [ - [0, 0], [1, 0], [0, 1], # Tri + [0, 0], + [1, 0], + [0, 1], # Tri # Empty space - [10, 10], [11, 10], [11, 11], [10, 11] # Square + [10, 10], + [11, 10], + [11, 11], + [10, 11], # Square ] - offsets = [0, 3, 3] # Index 3 is start of "empty", Index 3 is also start of Square? + offsets = [0, 3, 3] # Index 3 is start of "empty", Index 3 is also start of Square? # No, offsets should be strictly increasing or handle 0-length slices. # vertex_slices uses zip(offsets, chain(offsets[1:], [len(verts)])) # if offsets = [0, 3, 3], slices are [0:3], [3:3], [3:7] @@ -113,22 +131,14 @@ def test_poly_collection_constituent_empty(): with pytest.raises(PatternError): pc.to_polygons() -def test_poly_collection_valid(): - verts = [ - [0, 0], [1, 0], [0, 1], - [10, 10], [11, 10], [11, 11], [10, 11] - ] + +def test_poly_collection_valid() -> None: + verts = [[0, 0], [1, 0], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]] offsets = [0, 3] pc = PolyCollection(verts, offsets) assert len(pc.to_polygons()) == 2 - shapes = [ - Circle(radius=20), - Circle(radius=10), - Polygon([[0, 0], [10, 0], [10, 10]]), - Ellipse(radii=(5, 5)) - ] + shapes = [Circle(radius=20), Circle(radius=10), Polygon([[0, 0], [10, 0], [10, 10]]), Ellipse(radii=(5, 5))] sorted_shapes = sorted(shapes) assert len(sorted_shapes) == 4 # Just verify it doesn't crash and is stable assert sorted(sorted_shapes) == sorted_shapes - diff --git a/masque/test/test_shapes.py b/masque/test/test_shapes.py index 940c67a..b19d6bc 100644 --- a/masque/test/test_shapes.py +++ b/masque/test/test_shapes.py @@ -1,11 +1,11 @@ -import pytest import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..shapes import Arc, Ellipse, Circle, Polygon, PolyCollection -def test_poly_collection_init(): + +def test_poly_collection_init() -> None: # Two squares: [[0,0], [1,0], [1,1], [0,1]] and [[10,10], [11,10], [11,11], [10,11]] verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]] offsets = [0, 4] @@ -13,7 +13,8 @@ def test_poly_collection_init(): assert len(list(pc.polygon_vertices)) == 2 assert_equal(pc.get_bounds_single(), [[0, 0], [11, 11]]) -def test_poly_collection_to_polygons(): + +def test_poly_collection_to_polygons() -> None: verts = [[0, 0], [1, 0], [1, 1], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]] offsets = [0, 4] pc = PolyCollection(vertex_lists=verts, vertex_offsets=offsets) @@ -22,12 +23,14 @@ def test_poly_collection_to_polygons(): assert_equal(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]]) assert_equal(polys[1].vertices, [[10, 10], [11, 10], [11, 11], [10, 11]]) -def test_circle_init(): + +def test_circle_init() -> None: c = Circle(radius=10, offset=(5, 5)) assert c.radius == 10 assert_equal(c.offset, [5, 5]) -def test_circle_to_polygons(): + +def test_circle_to_polygons() -> None: c = Circle(radius=10) polys = c.to_polygons(num_vertices=32) assert len(polys) == 1 @@ -36,28 +39,32 @@ def test_circle_to_polygons(): bounds = polys[0].get_bounds_single() assert_allclose(bounds, [[-10, -10], [10, 10]], atol=1e-10) -def test_ellipse_init(): - e = Ellipse(radii=(10, 5), offset=(1, 2), rotation=pi/4) + +def test_ellipse_init() -> None: + e = Ellipse(radii=(10, 5), offset=(1, 2), rotation=pi / 4) assert_equal(e.radii, [10, 5]) assert_equal(e.offset, [1, 2]) - assert e.rotation == pi/4 + assert e.rotation == pi / 4 -def test_ellipse_to_polygons(): + +def test_ellipse_to_polygons() -> None: e = Ellipse(radii=(10, 5)) polys = e.to_polygons(num_vertices=64) assert len(polys) == 1 bounds = polys[0].get_bounds_single() assert_allclose(bounds, [[-10, -5], [10, 5]], atol=1e-10) -def test_arc_init(): - a = Arc(radii=(10, 10), angles=(0, pi/2), width=2, offset=(0, 0)) + +def test_arc_init() -> None: + a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2, offset=(0, 0)) assert_equal(a.radii, [10, 10]) - assert_equal(a.angles, [0, pi/2]) + assert_equal(a.angles, [0, pi / 2]) assert a.width == 2 -def test_arc_to_polygons(): + +def test_arc_to_polygons() -> None: # Quarter circle arc - a = Arc(radii=(10, 10), angles=(0, pi/2), width=2) + a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2) polys = a.to_polygons(num_vertices=32) assert len(polys) == 1 # Outer radius 11, inner radius 9 @@ -70,32 +77,35 @@ def test_arc_to_polygons(): # So x ranges from 0 to 11, y ranges from 0 to 11. assert_allclose(bounds, [[0, 0], [11, 11]], atol=1e-10) -def test_shape_mirror(): - e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi/4) - e.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset + +def test_shape_mirror() -> None: + e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4) + e.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset assert_equal(e.offset, [10, 20]) # rotation was pi/4, mirrored(0) -> -pi/4 == 3pi/4 (mod pi) - assert_allclose(e.rotation, 3*pi/4, atol=1e-10) - - a = Arc(radii=(10, 10), angles=(0, pi/4), width=2, offset=(10, 20)) + 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.mirror(0) assert_equal(a.offset, [10, 20]) # For Arc, mirror(0) negates rotation and angles - assert_allclose(a.angles, [0, -pi/4], atol=1e-10) + assert_allclose(a.angles, [0, -pi / 4], atol=1e-10) -def test_shape_flip_across(): - e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi/4) - e.flip_across(axis=0) # Mirror across y=0: flips y-offset + +def test_shape_flip_across() -> None: + e = Ellipse(radii=(10, 5), offset=(10, 20), rotation=pi / 4) + e.flip_across(axis=0) # Mirror across y=0: flips y-offset assert_equal(e.offset, [10, -20]) # rotation also flips: -pi/4 == 3pi/4 (mod pi) - assert_allclose(e.rotation, 3*pi/4, atol=1e-10) + assert_allclose(e.rotation, 3 * pi / 4, atol=1e-10) # Mirror across specific y e = Ellipse(radii=(10, 5), offset=(10, 20)) - e.flip_across(y=10) # Mirror across y=10 + e.flip_across(y=10) # Mirror across y=10 # y=20 mirrored across y=10 -> y=0 assert_equal(e.offset, [10, 0]) -def test_shape_scale(): + +def test_shape_scale() -> None: e = Ellipse(radii=(10, 5)) e.scale_by(2) assert_equal(e.radii, [20, 10]) @@ -105,21 +115,22 @@ def test_shape_scale(): assert_equal(a.radii, [5, 2.5]) assert a.width == 1 -def test_shape_arclen(): + +def test_shape_arclen() -> None: # Test that max_arclen correctly limits segment lengths - + # Ellipse e = Ellipse(radii=(10, 5)) # Approximate perimeter is ~48.4 # With max_arclen=5, should have > 10 segments polys = e.to_polygons(max_arclen=5) v = polys[0].vertices - dist = numpy.sqrt(numpy.sum(numpy.diff(v, axis=0, append=v[:1])**2, axis=1)) + dist = numpy.sqrt(numpy.sum(numpy.diff(v, axis=0, append=v[:1]) ** 2, axis=1)) assert numpy.all(dist <= 5.000001) assert len(v) > 10 # Arc - a = Arc(radii=(10, 10), angles=(0, pi/2), width=2) + a = Arc(radii=(10, 10), angles=(0, pi / 2), width=2) # Outer perimeter is 11 * pi/2 ~ 17.27 # Inner perimeter is 9 * pi/2 ~ 14.14 # With max_arclen=2, should have > 8 segments on outer edge @@ -127,6 +138,5 @@ def test_shape_arclen(): v = polys[0].vertices # Arc polygons are closed, but contain both inner and outer edges and caps # Let's just check that all segment lengths are within limit - dist = numpy.sqrt(numpy.sum(numpy.diff(v, axis=0, append=v[:1])**2, axis=1)) + dist = numpy.sqrt(numpy.sum(numpy.diff(v, axis=0, append=v[:1]) ** 2, axis=1)) assert numpy.all(dist <= 2.000001) - diff --git a/masque/test/test_utils.py b/masque/test/test_utils.py index 1badf64..882b5bd 100644 --- a/masque/test/test_utils.py +++ b/masque/test/test_utils.py @@ -1,28 +1,23 @@ -import pytest import numpy from numpy.testing import assert_equal, assert_allclose from numpy import pi -from ..utils import ( - remove_duplicate_vertices, - remove_colinear_vertices, - poly_contains_points, - rotation_matrix_2d, - apply_transforms -) +from ..utils import remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points, rotation_matrix_2d, apply_transforms -def test_remove_duplicate_vertices(): + +def test_remove_duplicate_vertices() -> None: # Closed path (default) v = [[0, 0], [1, 1], [1, 1], [2, 2], [0, 0]] v_clean = remove_duplicate_vertices(v, closed_path=True) # 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]]) - + # Open path v_clean_open = remove_duplicate_vertices(v, closed_path=False) assert_equal(v_clean_open, [[0, 0], [1, 1], [2, 2], [0, 0]]) -def test_remove_colinear_vertices(): + +def test_remove_colinear_vertices() -> None: v = [[0, 0], [1, 0], [2, 0], [2, 1], [2, 2], [1, 1], [0, 0]] v_clean = remove_colinear_vertices(v, closed_path=True) # [1, 0] is between [0, 0] and [2, 0] @@ -30,54 +25,59 @@ def test_remove_colinear_vertices(): # [1, 1] is between [2, 2] and [0, 0] assert_equal(v_clean, [[0, 0], [2, 0], [2, 2]]) -def test_remove_colinear_vertices_exhaustive(): + +def test_remove_colinear_vertices_exhaustive() -> None: # U-turn v = [[0, 0], [10, 0], [0, 0]] v_clean = remove_colinear_vertices(v, closed_path=False) - # 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. - assert len(v_clean) == 2 - + assert len(v_clean) == 2 + # 180 degree U-turn in closed path v = [[0, 0], [10, 0], [5, 0]] 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]] pts = [[5, 5], [-1, -1], [10, 10], [11, 5]] inside = poly_contains_points(v, pts) assert_equal(inside, [True, False, True, False]) -def test_rotation_matrix_2d(): - m = rotation_matrix_2d(pi/2) + +def test_rotation_matrix_2d() -> None: + m = rotation_matrix_2d(pi / 2) assert_allclose(m, [[0, -1], [1, 0]], atol=1e-10) -def test_rotation_matrix_non_manhattan(): + +def test_rotation_matrix_non_manhattan() -> None: # 45 degrees - m = rotation_matrix_2d(pi/4) - s = numpy.sqrt(2)/2 + m = rotation_matrix_2d(pi / 4) + s = numpy.sqrt(2) / 2 assert_allclose(m, [[s, -s], [s, s]], atol=1e-10) -def test_apply_transforms(): + +def test_apply_transforms() -> None: # cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] t1 = [10, 20, 0, 0] t2 = [[5, 0, 0, 0], [0, 5, 0, 0]] combined = apply_transforms(t1, t2) assert_equal(combined, [[15, 20, 0, 0], [10, 25, 0, 0]]) -def test_apply_transforms_advanced(): + +def test_apply_transforms_advanced() -> None: # Ox4: (x, y, rot, mir) # Outer: mirror x (axis 0), then rotate 90 deg CCW # apply_transforms logic for mirror uses y *= -1 (which is axis 0 mirror) - outer = [0, 0, pi/2, 1] - + outer = [0, 0, pi / 2, 1] + # Inner: (10, 0, 0, 0) inner = [10, 0, 0, 0] - + combined = apply_transforms(outer, inner) # 1. mirror inner y if outer mirrored: (10, 0) -> (10, 0) # 2. rotate by outer rotation (pi/2): (10, 0) -> (0, 10) # 3. add outer offset (0, 0) -> (0, 10) - assert_allclose(combined[0], [0, 10, pi/2, 1], atol=1e-10) - + assert_allclose(combined[0], [0, 10, pi / 2, 1], atol=1e-10)