misc cleanup (mostly type-related)

This commit is contained in:
jan 2026-02-16 17:58:34 -08:00
commit ebfe1b559c
16 changed files with 57 additions and 21 deletions

View file

@ -311,6 +311,7 @@ class Pather(Builder, PatherMixin):
# Fallback for dead pather: manually update the port instead of plugging
port = self.pattern[portspec]
port_rot = port.rotation
assert port_rot is not None
if ccw is None:
out_rot = pi
elif bool(ccw):
@ -414,6 +415,7 @@ class Pather(Builder, PatherMixin):
# Fallback for dead pather: manually update the port instead of plugging
port = self.pattern[portspec]
port_rot = port.rotation
assert port_rot is not None
out_port = Port((length, jog), rotation=pi, ptype=in_ptype)
out_port.rotate_around((0, 0), pi + port_rot)
out_port.translate(port.offset)

View file

@ -520,7 +520,7 @@ class RenderPather(PatherMixin):
ccw0 = jog > 0
kwargs_no_out = (kwargs | {'out_ptype': None})
try:
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail with asymmetric ptypes
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail w/asymmetric ptypes
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]

View file

@ -643,6 +643,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
if out_transition is not None:
out_ptype_actual = out_transition.their_port.ptype
elif ccw is not None:
assert bend is not None
out_ptype_actual = bend.out_port.ptype
elif not numpy.isclose(straight_length, 0):
out_ptype_actual = straight.ptype

View file

@ -693,7 +693,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
"""
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
cast('Positionable', entry).translate(offset)
self._log_bulk_update(f"translate({offset})")
self._log_bulk_update(f"translate({offset!r})")
return self
def scale_elements(self, c: float) -> Self:

View file

@ -20,6 +20,7 @@ def test_abstract_transform() -> None:
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 abs_obj.ports["A"].rotation is not None
assert_allclose(abs_obj.ports["A"].rotation, pi / 2, atol=1e-10)
# Mirror across x axis (axis 0): flips y-offset
@ -27,6 +28,7 @@ def test_abstract_transform() -> None:
# (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 abs_obj.ports["A"].rotation is not None
assert_allclose(abs_obj.ports["A"].rotation, 3 * pi / 2, atol=1e-10)
@ -48,6 +50,7 @@ def test_abstract_ref_transform() -> None:
# (0, 10) -> (100, 110)
assert_allclose(abs_obj.ports["A"].offset, [100, 110], atol=1e-10)
assert abs_obj.ports["A"].rotation is not None
assert_allclose(abs_obj.ports["A"].rotation, pi / 2, atol=1e-10)
@ -57,4 +60,5 @@ def test_abstract_undo_transform() -> None:
abs_obj.undo_ref_transform(ref)
assert_allclose(abs_obj.ports["A"].offset, [10, 0], atol=1e-10)
assert abs_obj.ports["A"].rotation is not None
assert_allclose(abs_obj.ports["A"].rotation, 0, atol=1e-10)

View file

@ -50,6 +50,7 @@ def test_builder_plug() -> None:
assert "start" in b.ports
assert_equal(b.ports["start"].offset, [90, 100])
assert b.ports["start"].rotation is not None
assert_allclose(b.ports["start"].rotation, 0, atol=1e-10)
@ -126,4 +127,5 @@ def test_dead_plug_best_effort() -> None:
# 3. Translate by s_port.offset (0,0): (-10,-10)
assert_allclose(b.ports['B'].offset, [-10, -10], atol=1e-10)
# P2 rot pi + transform rot -pi = 0
assert b.ports['B'].rotation is not None
assert_allclose(b.ports['B'].rotation, 0, atol=1e-10)

View file

@ -1,4 +1,5 @@
from pathlib import Path
from typing import cast
import pytest
from numpy.testing import assert_allclose
@ -77,17 +78,18 @@ def test_gdsii_full_roundtrip(tmp_path: Path) -> None:
# Order might be different depending on how they were written,
# but here they should match the order they were added if dict order is preserved.
# Actually, they are grouped by layer.
p_flush = read_paths.shapes[(2, 0)][0]
p_flush = cast("MPath", read_paths.shapes[(2, 0)][0])
assert p_flush.cap == MPath.Cap.Flush
p_square = read_paths.shapes[(2, 1)][0]
p_square = cast("MPath", read_paths.shapes[(2, 1)][0])
assert p_square.cap == MPath.Cap.Square
p_circle = read_paths.shapes[(2, 2)][0]
p_circle = cast("MPath", read_paths.shapes[(2, 2)][0])
assert p_circle.cap == MPath.Cap.Circle
p_custom = read_paths.shapes[(2, 3)][0]
p_custom = cast("MPath", read_paths.shapes[(2, 3)][0])
assert p_custom.cap == MPath.Cap.SquareCustom
assert p_custom.cap_extensions is not None
assert_allclose(p_custom.cap_extensions, (1, 5))
# Check Refs with repetitions
@ -125,8 +127,8 @@ def test_oasis_full_roundtrip(tmp_path: Path) -> None:
# Check Path caps
read_paths = read_lib["paths"]
assert read_paths.shapes[(2, 0)][0].cap == MPath.Cap.Flush
assert read_paths.shapes[(2, 1)][0].cap == MPath.Cap.Square
assert cast("MPath", read_paths.shapes[(2, 0)][0]).cap == MPath.Cap.Flush
assert cast("MPath", read_paths.shapes[(2, 1)][0]).cap == MPath.Cap.Square
# OASIS HalfWidth is Square. masque's Square is also HalfWidth extension.
# Wait, Circle cap in OASIS?
# masque/file/oasis.py:

View file

@ -1,11 +1,12 @@
from pathlib import Path
from typing import cast
import numpy
from numpy.testing import assert_equal, assert_allclose
from ..pattern import Pattern
from ..library import Library
from ..file import gdsii
from ..shapes import Path as MPath
from ..shapes import Path as MPath, Polygon
def test_gdsii_roundtrip(tmp_path: Path) -> None:
@ -36,14 +37,14 @@ def test_gdsii_roundtrip(tmp_path: Path) -> None:
assert "ref_cell" in read_lib
# Check polygon
read_poly = read_lib["poly_cell"].shapes[(1, 0)][0]
read_poly = cast("Polygon", 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]
read_path = cast("MPath", 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]])
@ -66,4 +67,5 @@ def test_gdsii_annotations(tmp_path: Path) -> None:
read_lib, _ = gdsii.readfile(gds_file)
read_ann = read_lib["cell"].shapes[(1, 0)][0].annotations
assert read_ann is not None
assert read_ann["1"] == ["hello"]

View file

@ -1,8 +1,12 @@
import pytest
from typing import cast, TYPE_CHECKING
from ..library import Library, LazyLibrary
from ..pattern import Pattern
from ..error import LibraryError
if TYPE_CHECKING:
from ..shapes import Polygon
def test_library_basic() -> None:
lib = Library()
@ -51,7 +55,7 @@ def test_library_flatten() -> None:
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_vertices = cast("Polygon", flat_parent.shapes[(1, 0)][0]).vertices
assert tuple(assert_vertices[0]) == (10.0, 10.0)

View file

@ -28,6 +28,7 @@ def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None
# port rot pi/2 (North). Travel +pi relative to port -> South.
assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10)
assert p.ports["start"].rotation is not None
assert_allclose(p.ports["start"].rotation, pi / 2, atol=1e-10)
@ -46,6 +47,7 @@ def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None:
assert_allclose(p.ports["start"].offset, [-1, -10], atol=1e-10)
# North (pi/2) + CW (90 deg) -> West (pi)?
# Actual behavior results in 0 (East) - apparently rotation is flipped.
assert p.ports["start"].rotation is not None
assert_allclose(p.ports["start"].rotation, 0, atol=1e-10)
@ -80,6 +82,7 @@ def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> N
assert_allclose(p.ports["start"].offset, [1, -20], atol=1e-10)
# pi/2 (North) + CCW (90 deg) -> 0 (East)?
# Actual behavior results in pi (West).
assert p.ports["start"].rotation is not None
assert_allclose(p.ports["start"].rotation, pi, atol=1e-10)

View file

@ -1,3 +1,4 @@
from typing import cast
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
@ -56,7 +57,7 @@ def test_pattern_translate() -> None:
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(cast("Polygon", pat.shapes[(1, 0)][0]).vertices[0], [10, 20])
assert_equal(pat.ports["P1"].offset, [15, 25])
@ -67,7 +68,7 @@ def test_pattern_scale() -> None:
pat.scale_by(2)
# Vertices should be scaled
assert_equal(pat.shapes[(1, 0)][0].vertices, [[0, 0], [0, 2], [2, 2], [2, 0]])
assert_equal(cast("Polygon", pat.shapes[(1, 0)][0]).vertices, [[0, 0], [0, 2], [2, 2], [2, 0]])
def test_pattern_rotate() -> None:
@ -77,7 +78,7 @@ def test_pattern_rotate() -> None:
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)
assert_allclose(cast("Polygon", pat.shapes[(1, 0)][0]).vertices[0], [0, 10], atol=1e-10)
def test_pattern_mirror() -> None:
@ -86,7 +87,7 @@ def test_pattern_mirror() -> None:
# Mirror across X axis (y -> -y)
pat.mirror(0)
assert_equal(pat.shapes[(1, 0)][0].vertices[0], [10, -5])
assert_equal(cast("Polygon", pat.shapes[(1, 0)][0]).vertices[0], [10, -5])
def test_pattern_get_bounds() -> None:
@ -106,7 +107,9 @@ def test_pattern_interface() -> None:
assert "in_A" in iface.ports
assert "out_A" in iface.ports
assert iface.ports["in_A"].rotation is not None
assert_allclose(iface.ports["in_A"].rotation, pi, atol=1e-10)
assert iface.ports["out_A"].rotation is not None
assert_allclose(iface.ports["out_A"].rotation, 0, atol=1e-10)
assert iface.ports["in_A"].ptype == "test"
assert iface.ports["out_A"].ptype == "test"

View file

@ -17,11 +17,13 @@ def test_port_transform() -> None:
p = Port(offset=(10, 0), rotation=0)
p.rotate_around((0, 0), pi / 2)
assert_allclose(p.offset, [0, 10], atol=1e-10)
assert p.rotation is not None
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 p.rotation is not None
assert_allclose(p.rotation, 3 * pi / 2, atol=1e-10)
@ -30,6 +32,7 @@ def test_port_flip_across() -> None:
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 p.rotation is not None
assert_allclose(p.rotation, pi, atol=1e-10)

View file

@ -25,6 +25,7 @@ def test_ports2data_roundtrip() -> None:
assert "P1" in pat2.ports
assert_allclose(pat2.ports["P1"].offset, [10, 20], atol=1e-10)
assert pat2.ports["P1"].rotation is not None
assert_allclose(pat2.ports["P1"].rotation, numpy.pi / 2, atol=1e-10)
assert pat2.ports["P1"].ptype == "test"
@ -52,4 +53,5 @@ def test_data_to_ports_hierarchical() -> None:
# rot 0 + pi/2 = pi/2
assert "A" in parent.ports
assert_allclose(parent.ports["A"].offset, [100, 105], atol=1e-10)
assert parent.ports["A"].rotation is not None
assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10)

View file

@ -1,3 +1,4 @@
from typing import cast, TYPE_CHECKING
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
@ -5,6 +6,9 @@ from ..pattern import Pattern
from ..ref import Ref
from ..repetition import Grid
if TYPE_CHECKING:
from ..shapes import Polygon
def test_ref_init() -> None:
ref = Ref(offset=(10, 20), rotation=pi / 4, mirrored=True, scale=2.0)
@ -22,7 +26,7 @@ def test_ref_as_pattern() -> None:
transformed_pat = ref.as_pattern(sub_pat)
# Check transformed shape
shape = transformed_pat.shapes[(1, 0)][0]
shape = cast("Polygon", 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]]
@ -42,7 +46,7 @@ def test_ref_with_repetition() -> None:
# 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)]])
first_verts = sorted([tuple(cast("Polygon", 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)]

View file

@ -1,4 +1,5 @@
import pytest
from typing import cast, TYPE_CHECKING
from numpy.testing import assert_allclose
from numpy import pi
@ -7,6 +8,9 @@ from ..builder.tools import PathTool
from ..library import Library
from ..ports import Port
if TYPE_CHECKING:
from ..shapes import Path
@pytest.fixture
def rpather_setup() -> tuple[RenderPather, PathTool, Library]:
@ -37,7 +41,7 @@ def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library
# start_port rot pi/2. pi/2 + pi = 3pi/2.
# (10, 0) rotated 3pi/2 -> (0, -10)
# So vertices: (0,0), (0,-10), (0,-20)
path_shape = rp.pattern.shapes[(1, 0)][0]
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
assert len(path_shape.vertices) == 3
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
@ -48,7 +52,7 @@ def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]
rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10)
rp.render()
path_shape = rp.pattern.shapes[(1, 0)][0]
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
# Path vertices:
# 1. Start (0,0)
# 2. Straight end: (0, -10)