Compare commits
No commits in common. "ac34108253c3029956adc34023f00d381ab26843" and "93dd3503cad3b8bb2054a6ee93a3c393da871c44" have entirely different histories.
ac34108253
...
93dd3503ca
12 changed files with 22 additions and 135 deletions
|
|
@ -32,6 +32,7 @@ from __future__ import annotations
|
||||||
from collections.abc import Iterable, Mapping, Sequence
|
from collections.abc import Iterable, Mapping, Sequence
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
|
from math import cos, sin
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -40,7 +41,7 @@ from numpy.typing import ArrayLike
|
||||||
|
|
||||||
from ...error import BuildError, PortError
|
from ...error import BuildError, PortError
|
||||||
from ...ports import Port
|
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 (
|
from ..tools import (
|
||||||
BendOffer,
|
BendOffer,
|
||||||
PrimitiveKind,
|
PrimitiveKind,
|
||||||
|
|
@ -578,11 +579,10 @@ class Solver:
|
||||||
out_port = step.out_port
|
out_port = step.out_port
|
||||||
if out_port.rotation is None:
|
if out_port.rotation is None:
|
||||||
raise ToolContractError('Primitive endpoints must have rotation')
|
raise ToolContractError('Primitive endpoints must have rotation')
|
||||||
rotation = rotation_matrix_2d(angle)
|
angle_cos = cos(angle)
|
||||||
out_x = float(out_port.x)
|
angle_sin = sin(angle)
|
||||||
out_y = float(out_port.y)
|
x += angle_cos * float(out_port.x) - angle_sin * float(out_port.y)
|
||||||
x += rotation[0, 0] * out_x + rotation[0, 1] * out_y
|
y += angle_sin * float(out_port.x) + angle_cos * float(out_port.y)
|
||||||
y += rotation[1, 0] * out_x + rotation[1, 1] * out_y
|
|
||||||
angle += out_port.rotation + pi
|
angle += out_port.rotation + pi
|
||||||
ptype = out_port.ptype
|
ptype = out_port.ptype
|
||||||
return Port((x, y), rotation=angle - pi, ptype=ptype)
|
return Port((x, y), rotation=angle - pi, ptype=ptype)
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,7 @@ class LibraryBuilder(INameView):
|
||||||
self,
|
self,
|
||||||
source: Mapping[str, Pattern] | ILibraryView,
|
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',
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -309,8 +309,6 @@ class LibraryBuilder(INameView):
|
||||||
rename_theirs: Function used to choose visible names for imported
|
rename_theirs: Function used to choose visible names for imported
|
||||||
source cells. Its `INameView` argument contains existing and
|
source cells. Its `INameView` argument contains existing and
|
||||||
previously reserved names, but does not support pattern lookup.
|
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.
|
rename_when: If `'conflict'`, only conflicting names are renamed.
|
||||||
If `'always'`, every imported source name is passed through
|
If `'always'`, every imported source name is passed through
|
||||||
`rename_theirs`.
|
`rename_theirs`.
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,7 @@ from ..error import LibraryError
|
||||||
from ..pattern import Pattern, map_layers, map_targets
|
from ..pattern import Pattern, map_layers, map_targets
|
||||||
from .base import ILibrary, ILibraryView
|
from .base import ILibrary, ILibraryView
|
||||||
from .capabilities import IBorrowing, IMaterializable
|
from .capabilities import IBorrowing, IMaterializable
|
||||||
from .utils import (
|
from .utils import INameView, dangling_mode_t, _plan_source_names, _source_rename_map, _validate_dangling_mode
|
||||||
INameView,
|
|
||||||
dangling_mode_t,
|
|
||||||
_plan_source_names,
|
|
||||||
_rename_patterns,
|
|
||||||
_source_rename_map,
|
|
||||||
_validate_dangling_mode,
|
|
||||||
)
|
|
||||||
from .mapping import LibraryView
|
from .mapping import LibraryView
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -311,7 +304,7 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
self,
|
self,
|
||||||
source: Mapping[str, Pattern] | ILibraryView,
|
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',
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -324,8 +317,6 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
rename_theirs: Function used to choose visible names for imported
|
rename_theirs: Function used to choose visible names for imported
|
||||||
source cells. Its `INameView` argument contains existing and
|
source cells. Its `INameView` argument contains existing and
|
||||||
previously reserved names, but does not support pattern lookup.
|
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.
|
rename_when: If `'conflict'`, only conflicting names are renamed.
|
||||||
If `'always'`, every imported source name is passed through
|
If `'always'`, every imported source name is passed through
|
||||||
`rename_theirs`.
|
`rename_theirs`.
|
||||||
|
|
|
||||||
|
|
@ -1829,8 +1829,6 @@ def map_layers(
|
||||||
new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list)
|
new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list)
|
||||||
for old_layer, seq in elements.items():
|
for old_layer, seq in elements.items():
|
||||||
new_layer = map_layer(old_layer)
|
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)
|
new_elements[new_layer].extend(seq)
|
||||||
return new_elements
|
return new_elements
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -574,10 +574,10 @@ class PortList(metaclass=ABCMeta):
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
translations = a_offsets - b_offsets
|
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'
|
msg = 'Port translations do not match:\n'
|
||||||
for nn, (kk, vv) in enumerate(connections.items()):
|
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'
|
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -437,19 +437,6 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
||||||
assert report.provenance["mapped_child"].requested_name == "child"
|
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:
|
def test_library_builder_adds_an_ordinary_view_eagerly() -> None:
|
||||||
child = Pattern()
|
child = Pattern()
|
||||||
top = Pattern()
|
top = Pattern()
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,7 @@ def test_gdsii_lazy_overlay_add_source_rename_when_validation() -> None:
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
||||||
with pytest.raises(TypeError, match='rename_theirs'):
|
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'):
|
with pytest.raises(ValueError, match='rename mode'):
|
||||||
OverlayLibrary().add_source(src, rename_when='sometimes') # type: ignore[arg-type]
|
OverlayLibrary().add_source(src, rename_when='sometimes') # type: ignore[arg-type]
|
||||||
|
|
|
||||||
|
|
@ -621,26 +621,6 @@ def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None:
|
||||||
assert set(rename_map.values()) <= set(overlay)
|
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:
|
def test_port_load_view_detaches_already_materialized_overlay_pattern() -> None:
|
||||||
overlay = OverlayLibrary()
|
overlay = OverlayLibrary()
|
||||||
overlay.add_source(Library({"top": Pattern()}))
|
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')
|
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:
|
def test_layer_mapped_view_materialization_and_copy_through() -> None:
|
||||||
pattern = Pattern()
|
pattern = Pattern()
|
||||||
pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
||||||
|
|
|
||||||
|
|
@ -113,22 +113,6 @@ def test_pather_trace_into_shapes() -> None:
|
||||||
assert numpy.isclose(p.pattern.ports['I'].rotation, pi / 2)
|
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:
|
def test_pather_trace_into_refines_output_adapter_route_before_plug() -> None:
|
||||||
class TransitionTool(Tool):
|
class TransitionTool(Tool):
|
||||||
def primitive_offers(
|
def primitive_offers(
|
||||||
|
|
|
||||||
|
|
@ -169,17 +169,6 @@ def test_port_list_plugged() -> None:
|
||||||
assert not pl.ports # Both should be removed
|
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:
|
def test_port_list_plugged_ptype_compatibility_warnings(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
caplog.set_level("WARNING", logger="masque.ports")
|
caplog.set_level("WARNING", logger="masque.ports")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,23 +89,6 @@ def test_rotation_matrix_non_manhattan() -> None:
|
||||||
assert_allclose(m, [[s, -s], [s, s]], atol=1e-10)
|
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:
|
def test_apply_transforms() -> None:
|
||||||
# cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
|
# cumulative [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
|
||||||
t1 = [10, 20, 0, 0]
|
t1 = [10, 20, 0, 0]
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ Geometric transforms
|
||||||
"""
|
"""
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from math import acos
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
|
@ -14,26 +13,8 @@ from numpy import pi
|
||||||
R90 = pi / 2
|
R90 = pi / 2
|
||||||
R180 = pi
|
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
|
@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]:
|
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
2D rotation matrix for rotating counterclockwise around the origin.
|
2D rotation matrix for rotating counterclockwise around the origin.
|
||||||
|
|
@ -44,11 +25,16 @@ def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||||
Returns:
|
Returns:
|
||||||
rotation matrix
|
rotation matrix
|
||||||
"""
|
"""
|
||||||
theta = float(theta)
|
arr = numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
|
||||||
quarter_turn = round(theta / R90)
|
[numpy.sin(theta), +numpy.cos(theta)]])
|
||||||
if abs(theta - quarter_turn * R90) <= _MANHATTAN_SNAP_ATOL:
|
|
||||||
theta = _CARDINAL_ANGLES[quarter_turn % 4]
|
# If this was a manhattan rotation, round to remove some inaccuracies in sin & cos
|
||||||
return _rotation_matrix_2d(theta)
|
# 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]:
|
def normalize_mirror(mirrored: Sequence[bool]) -> tuple[bool, float]:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue