diff --git a/masque/builder/planner/planner.py b/masque/builder/planner/planner.py index 5efcaa1..282a835 100644 --- a/masque/builder/planner/planner.py +++ b/masque/builder/planner/planner.py @@ -32,6 +32,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, replace from itertools import combinations +from math import cos, sin from typing import Any, Literal import numpy @@ -40,7 +41,7 @@ from numpy.typing import ArrayLike from ...error import BuildError, PortError from ...ports import Port -from ...utils import PTypeMatch, SupportsBool, ptype_match, ptypes_compatible, rotation_matrix_2d +from ...utils import PTypeMatch, SupportsBool, ptype_match, ptypes_compatible from ..tools import ( BendOffer, PrimitiveKind, @@ -578,11 +579,10 @@ class Solver: out_port = step.out_port if out_port.rotation is None: raise ToolContractError('Primitive endpoints must have rotation') - rotation = rotation_matrix_2d(angle) - out_x = float(out_port.x) - out_y = float(out_port.y) - x += rotation[0, 0] * out_x + rotation[0, 1] * out_y - y += rotation[1, 0] * out_x + rotation[1, 1] * out_y + angle_cos = cos(angle) + angle_sin = sin(angle) + x += angle_cos * float(out_port.x) - angle_sin * float(out_port.y) + y += angle_sin * float(out_port.x) + angle_cos * float(out_port.y) angle += out_port.rotation + pi ptype = out_port.ptype return Port((x, y), rotation=angle - pi, ptype=ptype) diff --git a/masque/library/build.py b/masque/library/build.py index e69fa9a..c4b7f8f 100644 --- a/masque/library/build.py +++ b/masque/library/build.py @@ -287,7 +287,7 @@ class LibraryBuilder(INameView): self, source: Mapping[str, Pattern] | ILibraryView, *, - rename_theirs: Callable[[INameView, str], str] | None = _rename_patterns, + rename_theirs: Callable[[INameView, str], str] | None = None, rename_when: Literal['conflict', 'always'] = 'conflict', ) -> dict[str, str]: """ @@ -309,8 +309,6 @@ class LibraryBuilder(INameView): rename_theirs: Function used to choose visible names for imported source cells. Its `INameView` argument contains existing and previously reserved names, but does not support pattern lookup. - By default, conflicting single-use names are made unique; - pass `None` to reject every conflict. rename_when: If `'conflict'`, only conflicting names are renamed. If `'always'`, every imported source name is passed through `rename_theirs`. diff --git a/masque/library/overlay.py b/masque/library/overlay.py index 529684b..698a457 100644 --- a/masque/library/overlay.py +++ b/masque/library/overlay.py @@ -10,14 +10,7 @@ from ..error import LibraryError from ..pattern import Pattern, map_layers, map_targets from .base import ILibrary, ILibraryView from .capabilities import IBorrowing, IMaterializable -from .utils import ( - INameView, - dangling_mode_t, - _plan_source_names, - _rename_patterns, - _source_rename_map, - _validate_dangling_mode, - ) +from .utils import INameView, dangling_mode_t, _plan_source_names, _source_rename_map, _validate_dangling_mode from .mapping import LibraryView if TYPE_CHECKING: @@ -311,7 +304,7 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing): self, source: Mapping[str, Pattern] | ILibraryView, *, - rename_theirs: Callable[[INameView, str], str] | None = _rename_patterns, + rename_theirs: Callable[[INameView, str], str] | None = None, rename_when: Literal['conflict', 'always'] = 'conflict', ) -> dict[str, str]: """ @@ -324,8 +317,6 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing): rename_theirs: Function used to choose visible names for imported source cells. Its `INameView` argument contains existing and previously reserved names, but does not support pattern lookup. - By default, conflicting single-use names are made unique; - pass `None` to reject every conflict. rename_when: If `'conflict'`, only conflicting names are renamed. If `'always'`, every imported source name is passed through `rename_theirs`. diff --git a/masque/pattern.py b/masque/pattern.py index c649543..a630552 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -1829,8 +1829,6 @@ def map_layers( new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list) for old_layer, seq in elements.items(): new_layer = map_layer(old_layer) - if new_layer is None: - raise PatternError(f'Layer mapping returned None for source layer {old_layer!r}') new_elements[new_layer].extend(seq) return new_elements diff --git a/masque/ports.py b/masque/ports.py index 552f081..bbd18b7 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -574,10 +574,10 @@ class PortList(metaclass=ABCMeta): raise PortError(msg) translations = a_offsets - b_offsets - if not numpy.allclose(a_offsets, b_offsets): + if not numpy.allclose(translations, 0): msg = 'Port translations do not match:\n' for nn, (kk, vv) in enumerate(connections.items()): - if not numpy.allclose(a_offsets[nn], b_offsets[nn]): + if not numpy.allclose(translations[nn], 0): msg += f'{kk} | {translations[nn]} | {vv}\n' raise PortError(msg) diff --git a/masque/test/test_build_library.py b/masque/test/test_build_library.py index e020912..36dd05d 100644 --- a/masque/test/test_build_library.py +++ b/masque/test/test_build_library.py @@ -437,19 +437,6 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None: assert report.provenance["mapped_child"].requested_name == "child" -def test_build_library_add_source_renames_conflicting_single_use_name_by_default() -> None: - source = Library({'_myCellName$F': Pattern(), 'source_top': Pattern()}) - source['source_top'].ref('_myCellName$F') - builder = LibraryBuilder() - builder['_myCellName$F'] = Pattern() - - rename_map = builder.add_source(source) - built, _report = builder.build() - - assert rename_map == {'_myCellName$F': '_myCellName'} - assert set(built['source_top'].refs) == {'_myCellName'} - - def test_library_builder_adds_an_ordinary_view_eagerly() -> None: child = Pattern() top = Pattern() diff --git a/masque/test/test_gdsii_lazy.py b/masque/test/test_gdsii_lazy.py index 610d743..d155d74 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -382,7 +382,7 @@ def test_gdsii_lazy_overlay_add_source_rename_when_validation() -> None: src = _make_lazy_port_library() with pytest.raises(TypeError, match='rename_theirs'): - OverlayLibrary().add_source(src, rename_theirs=None, rename_when='always') + OverlayLibrary().add_source(src, rename_when='always') with pytest.raises(ValueError, match='rename mode'): OverlayLibrary().add_source(src, rename_when='sometimes') # type: ignore[arg-type] diff --git a/masque/test/test_library.py b/masque/test/test_library.py index adc6f3c..32e7c3f 100644 --- a/masque/test/test_library.py +++ b/masque/test/test_library.py @@ -621,26 +621,6 @@ def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None: assert set(rename_map.values()) <= set(overlay) -def test_overlay_add_source_renames_conflicting_single_use_name_by_default() -> None: - overlay = OverlayLibrary() - overlay['_myCellName$F'] = Pattern() - source = Library({'_myCellName$F': Pattern(), 'source_top': Pattern()}) - source['source_top'].ref('_myCellName$F') - - rename_map = overlay.add_source(source) - - assert rename_map == {'_myCellName$F': '_myCellName'} - assert set(overlay['source_top'].refs) == {'_myCellName'} - - -def test_overlay_add_source_can_explicitly_disable_default_renaming() -> None: - overlay = OverlayLibrary() - overlay['_helper$A'] = Pattern() - - with pytest.raises(LibraryError, match='Conflicting name'): - overlay.add_source(Library({'_helper$A': Pattern()}), rename_theirs=None) - - def test_port_load_view_detaches_already_materialized_overlay_pattern() -> None: overlay = OverlayLibrary() overlay.add_source(Library({"top": Pattern()})) @@ -693,15 +673,6 @@ def test_layer_mapped_view_detaches_and_maps_shapes_and_labels() -> None: assert not hasattr(masque.library, 'PortsLibraryView') -def test_layer_mapped_view_preflight_rejects_none_layer() -> None: - pattern = Pattern() - pattern.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]]) - mapped = LayerMappedView(Library({'top': pattern}), lambda _layer: None) # type: ignore[arg-type] - - with pytest.raises(PatternError, match=r"returned None for source layer \(1, 0\)"): - preflight_source_aware(mapped) - - def test_layer_mapped_view_materialization_and_copy_through() -> None: pattern = Pattern() pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]]) diff --git a/masque/test/test_pather_trace_into.py b/masque/test/test_pather_trace_into.py index 48ab125..9e5e268 100644 --- a/masque/test/test_pather_trace_into.py +++ b/masque/test/test_pather_trace_into.py @@ -113,22 +113,6 @@ def test_pather_trace_into_shapes() -> None: assert numpy.isclose(p.pattern.ports['I'].rotation, pi / 2) -def test_pather_trace_into_large_composed_manhattan_route_plugs() -> None: - p = Pather( - Library(), - tools=PathTool(layer='M1', width=1000, ptype='wire'), - render='deferred', - ) - p.ports['src'] = Port((123.25, 456.75), rotation=0, ptype='wire') - p.ports['dst'] = Port((-99_999_876.75, 20_000_456.75), rotation=0, ptype='wire') - - p.trace_into('src', 'dst') - - assert 'src' not in p.ports - assert 'dst' not in p.ports - assert [step.kind for step in p._paths['src']] == ['straight', 'bend', 'straight', 'bend', 'plug'] - - def test_pather_trace_into_refines_output_adapter_route_before_plug() -> None: class TransitionTool(Tool): def primitive_offers( diff --git a/masque/test/test_ports.py b/masque/test/test_ports.py index a4c560a..f17921f 100644 --- a/masque/test/test_ports.py +++ b/masque/test/test_ports.py @@ -169,17 +169,6 @@ def test_port_list_plugged() -> None: assert not pl.ports # Both should be removed -def test_port_list_plugged_uses_coordinate_relative_tolerance() -> None: - pattern = Pattern(ports={ - "A": Port((10, 10), 0), - "B": Port((10 + 5e-8, 10), pi), - }) - - pattern.plugged({"A": "B"}) - - assert not pattern.ports - - def test_port_list_plugged_ptype_compatibility_warnings(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level("WARNING", logger="masque.ports") diff --git a/masque/test/test_utils.py b/masque/test/test_utils.py index fcef0de..f5e0215 100644 --- a/masque/test/test_utils.py +++ b/masque/test/test_utils.py @@ -89,23 +89,6 @@ def test_rotation_matrix_non_manhattan() -> None: assert_allclose(m, [[s, -s], [s, s]], atol=1e-10) -def test_rotation_matrix_canonicalizes_before_caching() -> None: - expected = rotation_matrix_2d(pi / 2) - - for angle in (pi / 2 + 1e-12, pi / 2 - 1e-12, -3 * pi / 2, 5 * pi / 2): - assert rotation_matrix_2d(angle) is expected - - assert not expected.flags.writeable - - -def test_rotation_matrix_does_not_snap_non_manhattan_angle() -> None: - cardinal = rotation_matrix_2d(pi / 2) - nearby = rotation_matrix_2d(pi / 2 + 2e-3) - - assert nearby is not cardinal - assert not numpy.array_equal(nearby, cardinal) - - def test_apply_transforms() -> None: # cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] t1 = [10, 20, 0, 0] diff --git a/masque/utils/transform.py b/masque/utils/transform.py index 935b0b3..7b39122 100644 --- a/masque/utils/transform.py +++ b/masque/utils/transform.py @@ -3,7 +3,6 @@ Geometric transforms """ from collections.abc import Sequence from functools import lru_cache -from math import acos import numpy from numpy.typing import NDArray, ArrayLike @@ -14,26 +13,8 @@ from numpy import pi R90 = pi / 2 R180 = pi -# Preserve the effective tolerance of the historical -# ``isclose(cos(4 * theta), 1, atol=1e-12)`` Manhattan check. Expressing it as -# an angular distance lets us canonicalize before consulting the matrix cache. -_MANHATTAN_SNAP_ATOL = acos(1 - (1e-5 + 1e-12)) / 4 -_CARDINAL_ANGLES = (0.0, R90, R180, 3 * R90) - @lru_cache -def _rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]: - """Build and cache an immutable matrix for a canonicalized angle.""" - arr = numpy.array([[numpy.cos(theta), -numpy.sin(theta)], - [numpy.sin(theta), +numpy.cos(theta)]]) - - if theta in _CARDINAL_ANGLES: - arr = numpy.round(arr) - - arr.flags.writeable = False - return arr - - def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]: """ 2D rotation matrix for rotating counterclockwise around the origin. @@ -44,11 +25,16 @@ def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]: Returns: rotation matrix """ - theta = float(theta) - quarter_turn = round(theta / R90) - if abs(theta - quarter_turn * R90) <= _MANHATTAN_SNAP_ATOL: - theta = _CARDINAL_ANGLES[quarter_turn % 4] - return _rotation_matrix_2d(theta) + arr = numpy.array([[numpy.cos(theta), -numpy.sin(theta)], + [numpy.sin(theta), +numpy.cos(theta)]]) + + # If this was a manhattan rotation, round to remove some inaccuracies in sin & cos + # cos(4*theta) is 1 for any multiple of pi/2. + if numpy.isclose(numpy.cos(4 * theta), 1, atol=1e-12): + arr = numpy.round(arr) + + arr.flags.writeable = False + return arr def normalize_mirror(mirrored: Sequence[bool]) -> tuple[bool, float]: