[library] More library rework
This commit is contained in:
parent
67afee7704
commit
0edd735a11
25 changed files with 2357 additions and 870 deletions
|
|
@ -842,7 +842,8 @@ def test_autotool_s_offer_uses_absolute_jog_range_for_both_signs() -> None:
|
|||
assert isinstance(pather._paths["A"][0].data, AutoTool.GeneratedData)
|
||||
|
||||
|
||||
def test_autotool_sbend_registration_order_sets_priority() -> None:
|
||||
@pytest.mark.parametrize('reverse', [False, True])
|
||||
def test_autotool_sbend_cost_is_independent_of_registration_order(reverse: bool) -> None:
|
||||
def first_sbend(jog: float) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
|
|
@ -855,19 +856,116 @@ def test_autotool_sbend_registration_order_sets_priority() -> None:
|
|||
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
||||
return pat
|
||||
|
||||
tool = AutoTool()
|
||||
generators = (second_sbend, first_sbend) if reverse else (first_sbend, second_sbend)
|
||||
for generator in generators:
|
||||
tool.add_sbend(generator, "core", "A", "B", jog_range=(0, 1e8))
|
||||
|
||||
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||
|
||||
assert isinstance(data, AutoTool.GeneratedData)
|
||||
assert data.fn is second_sbend
|
||||
assert_allclose(out_port.offset, [5, 4])
|
||||
|
||||
|
||||
def test_autotool_sbend_explicit_cost_can_override_geometric_cost() -> None:
|
||||
def long_sbend(jog: float) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pat.ports["B"] = Port((20, jog), pi, ptype="core")
|
||||
return pat
|
||||
|
||||
def short_sbend(jog: float) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
||||
return pat
|
||||
|
||||
tool = (
|
||||
AutoTool()
|
||||
.add_sbend(first_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
||||
.add_sbend(second_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
||||
.add_sbend(short_sbend, "core", "A", "B", jog_range=(0, 1e8), cost=10)
|
||||
.add_sbend(long_sbend, "core", "A", "B", jog_range=(0, 1e8), cost=1)
|
||||
)
|
||||
|
||||
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||
|
||||
assert isinstance(data, AutoTool.GeneratedData)
|
||||
assert data.fn is first_sbend
|
||||
assert data.fn is long_sbend
|
||||
assert_allclose(out_port.offset, [20, 4])
|
||||
|
||||
|
||||
def test_autotool_add_methods_propagate_callable_cost_to_all_created_offers() -> None:
|
||||
def cost(parameter: float, endpoint: Port) -> float:
|
||||
return abs(parameter) + abs(endpoint.x) + abs(endpoint.y)
|
||||
|
||||
def make_straight(length: float) -> Pattern:
|
||||
return _make_transition_straight(length, ptype="core")
|
||||
|
||||
def make_sbend(jog: float) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
||||
return pat
|
||||
|
||||
lib = Library()
|
||||
|
||||
bend = Pattern()
|
||||
bend.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
bend.ports["B"] = Port((2, -2), pi / 2, ptype="core")
|
||||
lib["bend"] = bend
|
||||
|
||||
uturn = Pattern()
|
||||
uturn.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||
uturn.ports["B"] = Port((3, 4), 0, ptype="core")
|
||||
lib["uturn"] = uturn
|
||||
|
||||
transition = Pattern()
|
||||
transition.ports["EXT"] = Port((0, 0), 0, ptype="external")
|
||||
transition.ports["CORE"] = Port((1, 0), pi, ptype="core")
|
||||
lib["transition"] = transition
|
||||
|
||||
tool = (
|
||||
AutoTool()
|
||||
.add_straight(make_straight, "core", "in", cost=cost)
|
||||
.add_bend(lib.abstract("bend"), "A", "B", clockwise=True, cost=cost)
|
||||
.add_sbend(
|
||||
make_sbend,
|
||||
"core",
|
||||
"A",
|
||||
"B",
|
||||
endpoint=lambda jog: Port((5, jog), pi, ptype="core"),
|
||||
cost=cost,
|
||||
)
|
||||
.add_uturn(lib.abstract("uturn"), "A", "B", cost=cost)
|
||||
.add_transition(lib.abstract("transition"), "EXT", "CORE", cost=cost)
|
||||
)
|
||||
|
||||
offers = [
|
||||
*(offer for offer in tool.primitive_offers("straight", in_ptype="core")
|
||||
if offer.in_ptype == offer.out_ptype == "core"),
|
||||
*tool.primitive_offers("bend", in_ptype="core", ccw=False),
|
||||
*tool.primitive_offers("bend", in_ptype="core", ccw=True),
|
||||
*(offer for offer in tool.primitive_offers("s", in_ptype="core")
|
||||
if offer.in_ptype == offer.out_ptype == "core"),
|
||||
*tool.primitive_offers("u", in_ptype="core"),
|
||||
*(offer for offer in tool.primitive_offers("straight", in_ptype="external")
|
||||
if offer.in_ptype == "external" and offer.out_ptype == "core"),
|
||||
*(offer for offer in tool.primitive_offers("straight", in_ptype="core")
|
||||
if offer.in_ptype == "core" and offer.out_ptype == "external"),
|
||||
]
|
||||
|
||||
assert len(offers) == 9
|
||||
assert all(offer.cost is cost for offer in offers)
|
||||
|
||||
|
||||
def test_autotool_validates_cost_before_registering_any_offers() -> None:
|
||||
def unused_sbend(_jog: float) -> Pattern:
|
||||
raise AssertionError('invalid cost should be rejected before metadata inference')
|
||||
|
||||
with pytest.raises(BuildError, match='cost factor'):
|
||||
AutoTool().add_sbend(unused_sbend, jog_range=(-1, 1), cost=-1)
|
||||
|
||||
|
||||
def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
||||
tool = make_sbend_tool((4, 4))
|
||||
offers = tool.primitive_offers('s', in_ptype="core")
|
||||
|
|
|
|||
|
|
@ -3,8 +3,20 @@ from collections.abc import Iterator
|
|||
import pytest
|
||||
|
||||
from ..builder import Pather
|
||||
from ..error import BuildError
|
||||
from ..library import BuildLibrary, BuildReport, ILibraryView, Library, cell, dangling_mode_t
|
||||
from ..error import BuildError, LibraryError
|
||||
from ..library import (
|
||||
INameView,
|
||||
ILibrary,
|
||||
IMaterializable,
|
||||
LibraryBuilder,
|
||||
BuildReport,
|
||||
CellProvenance,
|
||||
ILibraryView,
|
||||
Library,
|
||||
LibraryView,
|
||||
cell,
|
||||
dangling_mode_t,
|
||||
)
|
||||
from ..pattern import Pattern
|
||||
from ..ports import Port
|
||||
|
||||
|
|
@ -42,16 +54,73 @@ class _MetadataSource(ILibraryView):
|
|||
return self._child_graph
|
||||
|
||||
|
||||
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||
builder = BuildLibrary()
|
||||
class _MaterializableMetadataSource(_MetadataSource, IMaterializable):
|
||||
def materialize(self, name: str, *, persist: bool = True) -> Pattern: # noqa: ARG002
|
||||
return self[name]
|
||||
|
||||
def make_parent(lib: BuildLibrary) -> Pattern:
|
||||
|
||||
def test_metadata_source_base_tops_stays_lazy() -> None:
|
||||
source = _MetadataSource(
|
||||
{"child": Pattern(), "top": Pattern()},
|
||||
{"child": set(), "top": {"child"}},
|
||||
)
|
||||
|
||||
assert source.tops() == ["top"]
|
||||
assert source.loads == 0
|
||||
|
||||
|
||||
def test_metadata_source_reachability_and_subtree_stay_lazy() -> None:
|
||||
source = _MetadataSource(
|
||||
{"child": Pattern(), "top": Pattern(), "unused": Pattern()},
|
||||
{"child": set(), "top": {"child"}, "unused": set()},
|
||||
)
|
||||
|
||||
assert source.referenced_patterns("top") == {"child"}
|
||||
assert source.dangling_refs("top") == set()
|
||||
subtree = source.subtree("top")
|
||||
|
||||
assert source.loads == 0
|
||||
assert subtree.source_order() == ("child", "top")
|
||||
_ = subtree["top"]
|
||||
assert source.loads == 1
|
||||
|
||||
|
||||
def test_build_report_defensively_freezes_mappings() -> None:
|
||||
provenance = {
|
||||
"top": CellProvenance(
|
||||
requested_name="top",
|
||||
kind="declared",
|
||||
owner_declared_name="top",
|
||||
build_chain=("top",),
|
||||
),
|
||||
}
|
||||
dependencies = {"top": frozenset({"child"})}
|
||||
report = BuildReport(
|
||||
requested_roots=("top",),
|
||||
provenance=provenance,
|
||||
dependency_graph=dependencies,
|
||||
)
|
||||
provenance.clear()
|
||||
dependencies.clear()
|
||||
|
||||
assert set(report.provenance) == {"top"}
|
||||
assert report.dependency_graph == {"top": frozenset({"child"})}
|
||||
with pytest.raises(TypeError):
|
||||
report.provenance["other"] = report.provenance["top"] # type: ignore[index]
|
||||
with pytest.raises(TypeError):
|
||||
report.dependency_graph["top"] = frozenset() # type: ignore[index]
|
||||
|
||||
|
||||
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_parent(lib: ILibrary) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ref("child")
|
||||
assert lib.abstract("child").name == "child"
|
||||
return pat
|
||||
|
||||
builder.cells.parent = cell(make_parent)(builder)
|
||||
builder.cells.parent = cell(make_parent)(builder.library)
|
||||
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
||||
|
||||
built, report = builder.build()
|
||||
|
|
@ -62,10 +131,44 @@ def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
|||
assert report.provenance["parent"].kind == "declared"
|
||||
|
||||
|
||||
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
||||
builder = BuildLibrary()
|
||||
def test_build_cells_view_supports_underscore_declarations() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells._helper = Pattern()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
assert "_helper" in builder
|
||||
with pytest.raises(BuildError, match="write-only"):
|
||||
_value = builder.cells._helper
|
||||
report = builder.validate()
|
||||
assert report.provenance["_helper"].kind == "declared"
|
||||
|
||||
del builder.cells._helper
|
||||
assert "_helper" not in builder
|
||||
|
||||
|
||||
def test_build_cells_view_builds_underscore_declaration() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells._helper = Pattern()
|
||||
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert "_helper" in built
|
||||
|
||||
|
||||
def test_build_library_tree_merge_rejects_single_cell_cycle() -> None:
|
||||
tree = Library({"loop": Pattern()})
|
||||
tree["loop"].ref("loop")
|
||||
builder = LibraryBuilder()
|
||||
|
||||
with pytest.raises(LibraryError, match="exactly one topcell"):
|
||||
builder << tree
|
||||
|
||||
assert not builder
|
||||
|
||||
|
||||
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
tree = Library({"_helper": Pattern()})
|
||||
name_a = lib << tree
|
||||
name_b = lib << tree
|
||||
|
|
@ -74,7 +177,7 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
|
|||
top.ref(name_b)
|
||||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
_built, report = builder.build()
|
||||
|
||||
helpers = [
|
||||
|
|
@ -88,7 +191,7 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
|
|||
|
||||
|
||||
def test_build_library_authoring_tree_merge_renames_repeated_single_use_names() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
tree = Library({"_helper": Pattern()})
|
||||
|
||||
name_a = builder << tree
|
||||
|
|
@ -103,7 +206,7 @@ def test_build_library_authoring_tree_merge_renames_repeated_single_use_names()
|
|||
|
||||
|
||||
def test_build_library_authoring_tree_merge_remaps_internal_refs() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["_helper"] = Pattern()
|
||||
helper = Pattern()
|
||||
top = Pattern()
|
||||
|
|
@ -117,12 +220,14 @@ def test_build_library_authoring_tree_merge_remaps_internal_refs() -> None:
|
|||
assert any(name != "_helper" for name in built[top_name].refs)
|
||||
|
||||
|
||||
def test_build_library_requires_build_session_for_reads_and_freezes_after_build() -> None:
|
||||
builder = BuildLibrary()
|
||||
def test_library_builder_is_not_a_readable_library_and_freezes_after_build() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["leaf"] = Pattern()
|
||||
|
||||
with pytest.raises(BuildError, match="validate\\(\\) or build\\(\\)"):
|
||||
_ = builder["leaf"]
|
||||
assert isinstance(builder, INameView)
|
||||
assert not isinstance(builder, ILibraryView)
|
||||
with pytest.raises(TypeError, match="not subscriptable"):
|
||||
_ = builder["leaf"] # type: ignore[index]
|
||||
|
||||
with pytest.raises(BuildError, match="write-only"):
|
||||
_ = builder.cells.leaf
|
||||
|
|
@ -139,15 +244,15 @@ def test_build_library_requires_build_session_for_reads_and_freezes_after_build(
|
|||
|
||||
|
||||
def test_build_library_validate_is_retryable_after_failure() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_parent(lib: BuildLibrary) -> Pattern:
|
||||
def make_parent(lib: ILibrary) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ref("child")
|
||||
lib.abstract("child")
|
||||
return pat
|
||||
|
||||
builder.cells.parent = cell(make_parent)(builder)
|
||||
builder.cells.parent = cell(make_parent)(builder.library)
|
||||
|
||||
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
||||
builder.validate()
|
||||
|
|
@ -159,7 +264,7 @@ def test_build_library_validate_is_retryable_after_failure() -> None:
|
|||
|
||||
|
||||
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["child"] = Pattern()
|
||||
|
||||
def make_parent() -> Pattern:
|
||||
|
|
@ -174,8 +279,64 @@ def test_build_library_depends_on_supports_hidden_dependencies_for_partial_valid
|
|||
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||
|
||||
|
||||
def test_build_library_validate_accepts_single_string_and_deduplicates_roots() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["top"] = Pattern()
|
||||
|
||||
single = builder.validate(names="top")
|
||||
duplicate = builder.validate(names=("top", "top"))
|
||||
|
||||
assert single.requested_roots == ("top",)
|
||||
assert duplicate.requested_roots == ("top",)
|
||||
|
||||
|
||||
def test_build_library_validate_rejects_non_string_roots() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["top"] = Pattern()
|
||||
|
||||
with pytest.raises(TypeError, match="roots must be strings"):
|
||||
builder.validate(names=("top", 1)) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["build", "validate"])
|
||||
def test_build_library_rejects_same_builder_reentrancy(operation: str) -> None:
|
||||
builder = LibraryBuilder()
|
||||
calls = 0
|
||||
|
||||
def make_top() -> Pattern:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if operation == "build":
|
||||
builder.build()
|
||||
else:
|
||||
builder.validate(names=())
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)()
|
||||
|
||||
with pytest.raises(BuildError, match="recursively"):
|
||||
builder.build()
|
||||
assert calls == 1
|
||||
|
||||
|
||||
def test_build_library_allows_nested_build_of_different_builder() -> None:
|
||||
inner = LibraryBuilder()
|
||||
inner["leaf"] = Pattern()
|
||||
outer = LibraryBuilder()
|
||||
|
||||
def make_top() -> Pattern:
|
||||
built, _report = inner.build(output="library")
|
||||
assert "leaf" in built
|
||||
return Pattern()
|
||||
|
||||
outer.cells.top = cell(make_top)()
|
||||
built, _report = outer.build(output="library")
|
||||
|
||||
assert "top" in built
|
||||
|
||||
|
||||
def test_build_library_validate_rejects_removed_output_argument() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["leaf"] = Pattern()
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
|
|
@ -183,7 +344,7 @@ def test_build_library_validate_rejects_removed_output_argument() -> None:
|
|||
|
||||
|
||||
def test_build_library_rejects_unknown_build_output_mode() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["leaf"] = Pattern()
|
||||
|
||||
with pytest.raises(ValueError, match="Unknown build output mode"):
|
||||
|
|
@ -191,10 +352,10 @@ def test_build_library_rejects_unknown_build_output_mode() -> None:
|
|||
|
||||
|
||||
def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
helper = Pather(library=lib, ports="leaf", name="_route")
|
||||
top = Pattern()
|
||||
top.ref("_route")
|
||||
|
|
@ -202,7 +363,7 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
|
|||
top.ports.update(helper.pattern.ports)
|
||||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
_built, report = builder.build()
|
||||
|
||||
helper_prov = report.provenance["_route"]
|
||||
|
|
@ -211,11 +372,11 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
|
|||
|
||||
|
||||
def test_build_library_contains_tracks_active_session_names() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["leaf"] = Pattern()
|
||||
builder.add_source(Library({"src": Pattern()}))
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
assert "leaf" in lib
|
||||
assert "src" in lib
|
||||
assert "_helper" not in lib
|
||||
|
|
@ -223,7 +384,7 @@ def test_build_library_contains_tracks_active_session_names() -> None:
|
|||
assert "_helper" in lib
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
built, _report = builder.build()
|
||||
|
||||
assert "_helper" in built
|
||||
|
|
@ -231,7 +392,7 @@ def test_build_library_contains_tracks_active_session_names() -> None:
|
|||
|
||||
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
|
||||
source = Library({"src": Pattern()})
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder.add_source(source)
|
||||
builder.cells.top = cell(lambda: Pattern())()
|
||||
|
||||
|
|
@ -241,6 +402,18 @@ def test_build_library_preserves_source_cells_and_records_source_provenance() ->
|
|||
assert report.provenance["src"].kind == "source"
|
||||
|
||||
|
||||
def test_build_library_add_reserves_all_planned_names() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["_shape$A"] = Pattern()
|
||||
builder["_shape$B"] = Pattern()
|
||||
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||
|
||||
rename_map = builder.add(source)
|
||||
|
||||
assert len(set(rename_map.values())) == 2
|
||||
assert set(rename_map.values()) <= set(builder)
|
||||
|
||||
|
||||
def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
||||
source = Library()
|
||||
source["child"] = Pattern()
|
||||
|
|
@ -248,7 +421,7 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
|||
parent.ref("child")
|
||||
source["parent"] = parent
|
||||
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
rename_map = builder.add_source(
|
||||
source,
|
||||
rename_theirs=lambda _lib, name: f"mapped_{name}",
|
||||
|
|
@ -264,7 +437,7 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
|||
assert report.provenance["mapped_child"].requested_name == "child"
|
||||
|
||||
|
||||
def test_build_library_authoring_tree_merge_keeps_source_view_lazy() -> None:
|
||||
def test_library_builder_adds_an_ordinary_view_eagerly() -> None:
|
||||
child = Pattern()
|
||||
top = Pattern()
|
||||
top.ref("child")
|
||||
|
|
@ -273,16 +446,16 @@ def test_build_library_authoring_tree_merge_keeps_source_view_lazy() -> None:
|
|||
{"child": set(), "top": {"child"}},
|
||||
)
|
||||
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
top_name = builder << source
|
||||
built, _report = builder.build()
|
||||
|
||||
assert top_name == "top"
|
||||
assert "top" in built
|
||||
assert source.loads == 0
|
||||
assert source.loads == 2
|
||||
|
||||
|
||||
def test_build_library_authoring_source_tree_merge_returns_renamed_top() -> None:
|
||||
def test_library_builder_adds_and_renames_an_ordinary_view_eagerly() -> None:
|
||||
existing = Pattern()
|
||||
source_top = Pattern()
|
||||
source = _MetadataSource(
|
||||
|
|
@ -290,14 +463,41 @@ def test_build_library_authoring_source_tree_merge_returns_renamed_top() -> None
|
|||
{"_helper": set()},
|
||||
)
|
||||
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["_helper"] = existing
|
||||
top_name = builder << source
|
||||
built, _report = builder.build()
|
||||
|
||||
assert top_name != "_helper"
|
||||
assert top_name in built
|
||||
assert source.loads == 1
|
||||
|
||||
|
||||
def test_library_builder_add_borrows_materializable_views() -> None:
|
||||
source = _MaterializableMetadataSource(
|
||||
{"child": Pattern(), "top": Pattern()},
|
||||
{"child": set(), "top": {"child"}},
|
||||
)
|
||||
source.mapping["top"].ref("child")
|
||||
builder = LibraryBuilder()
|
||||
|
||||
builder.add(source)
|
||||
built, _report = builder.build()
|
||||
|
||||
assert source.loads == 0
|
||||
assert set(built) == {"child", "top"}
|
||||
|
||||
|
||||
def test_library_view_wrapper_intentionally_erases_materializable_marker() -> None:
|
||||
source = _MaterializableMetadataSource(
|
||||
{"top": Pattern()},
|
||||
{"top": set()},
|
||||
)
|
||||
builder = LibraryBuilder()
|
||||
|
||||
builder.add(LibraryView(source))
|
||||
|
||||
assert source.loads == 1
|
||||
|
||||
|
||||
def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_materialization() -> None:
|
||||
|
|
@ -309,7 +509,7 @@ def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_mater
|
|||
{"_helper": set(), "top": {"_helper"}},
|
||||
)
|
||||
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder["_helper"] = Pattern()
|
||||
top_name = builder << source
|
||||
built, _report = builder.build(output="library")
|
||||
|
|
@ -319,18 +519,19 @@ def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_mater
|
|||
assert source.loads == 2
|
||||
|
||||
|
||||
def test_build_library_rejects_authoring_tree_le_before_mutating() -> None:
|
||||
builder = BuildLibrary()
|
||||
def test_library_builder_does_not_expose_library_hierarchy_operations() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
with pytest.raises(BuildError, match="__le__"):
|
||||
with pytest.raises(TypeError):
|
||||
_abstract = builder <= Library({"leaf": Pattern()})
|
||||
|
||||
assert list(builder) == []
|
||||
assert not hasattr(builder, "abstract")
|
||||
assert not hasattr(builder, "resolve")
|
||||
assert not hasattr(builder, "rename")
|
||||
|
||||
|
||||
def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
||||
source = Library({"src": Pattern()})
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder.add_source(source)
|
||||
source["late"] = Pattern()
|
||||
|
||||
|
|
@ -340,7 +541,7 @@ def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
|||
|
||||
def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
||||
source = Library({"src": Pattern()})
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
builder.add_source(source)
|
||||
del source["src"]
|
||||
|
||||
|
|
@ -349,45 +550,29 @@ def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
|||
|
||||
|
||||
def test_build_library_rejects_add_source_during_build() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
lib.add_source(Library({"src": Pattern()}))
|
||||
def make_top() -> Pattern:
|
||||
builder.add_source(Library({"src": Pattern()}))
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)()
|
||||
|
||||
with pytest.raises(BuildError, match="add_source"):
|
||||
with pytest.raises(BuildError, match="Cannot modify"):
|
||||
builder.build()
|
||||
|
||||
|
||||
def test_build_library_rejects_renaming_imported_source_cells_during_authoring() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder.add_source(Library({"src": Pattern()}))
|
||||
|
||||
with pytest.raises(BuildError, match="add_source"):
|
||||
builder.rename("src", "renamed_src", move_references=True)
|
||||
|
||||
|
||||
def test_build_library_rejects_renaming_declared_cells_during_authoring() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder["declared"] = Pattern()
|
||||
|
||||
with pytest.raises(BuildError, match='Cannot rename declared build cell "declared"'):
|
||||
builder.rename("declared", "renamed_declared")
|
||||
|
||||
|
||||
def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
lib["_helper"] = Pattern()
|
||||
lib.rename("_helper", "final_helper")
|
||||
top = Pattern()
|
||||
top.ref("final_helper")
|
||||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
built, report = builder.build()
|
||||
|
||||
assert "final_helper" in built
|
||||
|
|
@ -401,14 +586,14 @@ def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
|||
|
||||
|
||||
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
lib["_helper"] = Pattern()
|
||||
del lib["_helper"]
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
built, report = builder.build()
|
||||
|
||||
assert "_helper" not in built
|
||||
|
|
@ -417,9 +602,9 @@ def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
|||
|
||||
|
||||
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
tree = Library({"_helper": Pattern()})
|
||||
_ = lib << tree
|
||||
renamed = lib << tree
|
||||
|
|
@ -429,7 +614,7 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
|
|||
top.ref("final_helper")
|
||||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
built, report = builder.build()
|
||||
|
||||
assert "final_helper" in built
|
||||
|
|
@ -438,48 +623,158 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
|
|||
|
||||
|
||||
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
|
||||
declared = BuildLibrary()
|
||||
declared = LibraryBuilder()
|
||||
declared["leaf"] = Pattern()
|
||||
|
||||
def rename_declared(lib: BuildLibrary) -> Pattern:
|
||||
def rename_declared(lib: ILibrary) -> Pattern:
|
||||
lib.rename("leaf", "renamed_leaf")
|
||||
return Pattern()
|
||||
|
||||
declared.cells.top = cell(rename_declared)(declared)
|
||||
declared.cells.top = cell(rename_declared)(declared.library)
|
||||
with pytest.raises(BuildError, match='Cannot rename declared build cell "leaf"'):
|
||||
declared.build()
|
||||
|
||||
source = BuildLibrary()
|
||||
source = LibraryBuilder()
|
||||
source.add_source(Library({"src": Pattern()}))
|
||||
|
||||
def rename_source(lib: BuildLibrary) -> Pattern:
|
||||
def rename_source(lib: ILibrary) -> Pattern:
|
||||
lib.rename("src", "renamed_src")
|
||||
return Pattern()
|
||||
|
||||
source.cells.top = cell(rename_source)(source)
|
||||
source.cells.top = cell(rename_source)(source.library)
|
||||
with pytest.raises(BuildError, match='Cannot rename imported source cell "src"'):
|
||||
source.build()
|
||||
|
||||
|
||||
def test_build_library_rejects_deleting_declared_or_source_cells_during_build() -> None:
|
||||
declared = BuildLibrary()
|
||||
declared = LibraryBuilder()
|
||||
declared["leaf"] = Pattern()
|
||||
|
||||
def delete_declared(lib: BuildLibrary) -> Pattern:
|
||||
def delete_declared(lib: ILibrary) -> Pattern:
|
||||
del lib["leaf"]
|
||||
return Pattern()
|
||||
|
||||
declared.cells.top = cell(delete_declared)(declared)
|
||||
declared.cells.top = cell(delete_declared)(declared.library)
|
||||
with pytest.raises(BuildError, match='Cannot delete declared build cell "leaf"'):
|
||||
declared.build()
|
||||
|
||||
source = BuildLibrary()
|
||||
source = LibraryBuilder()
|
||||
source.add_source(Library({"src": Pattern()}))
|
||||
|
||||
def delete_source(lib: BuildLibrary) -> Pattern:
|
||||
def delete_source(lib: ILibrary) -> Pattern:
|
||||
del lib["src"]
|
||||
return Pattern()
|
||||
|
||||
source.cells.top = cell(delete_source)(source)
|
||||
source.cells.top = cell(delete_source)(source.library)
|
||||
with pytest.raises(BuildError, match='Cannot delete imported source cell "src"'):
|
||||
source.build()
|
||||
|
||||
|
||||
def test_library_builder_replaces_its_library_placeholder_in_direct_recipe_arguments() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["leaf"] = Pattern()
|
||||
|
||||
def make_top(positional: ILibrary, *, keyword: ILibrary) -> Pattern:
|
||||
assert positional is keyword
|
||||
assert positional.abstract("leaf").name == "leaf"
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder.library, keyword=builder.library)
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert "top" in built
|
||||
|
||||
|
||||
def test_library_builder_library_placeholder_is_read_only() -> None:
|
||||
builder = LibraryBuilder()
|
||||
placeholder = builder.library
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
builder.library = object() # type: ignore[misc]
|
||||
|
||||
assert builder.library is placeholder
|
||||
builder.cells.top = cell(lambda _lib: Pattern())(builder.library)
|
||||
built, _report = builder.build(output="library")
|
||||
assert "top" in built
|
||||
|
||||
|
||||
def test_library_builder_rejects_another_builders_direct_placeholder() -> None:
|
||||
builder = LibraryBuilder()
|
||||
other = LibraryBuilder()
|
||||
|
||||
with pytest.raises(BuildError, match="another LibraryBuilder"):
|
||||
builder.cells.top = cell(lambda _lib: Pattern())(other.library)
|
||||
|
||||
|
||||
def test_library_builder_does_not_substitute_nested_placeholders() -> None:
|
||||
builder = LibraryBuilder()
|
||||
|
||||
def make_top(values: tuple[object, ...]) -> Pattern:
|
||||
assert values == (builder.library,)
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)((builder.library,))
|
||||
builder.build()
|
||||
|
||||
|
||||
def test_library_builder_keeps_context_free_recipes_and_name_queries() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder.cells.top = cell(Pattern)()
|
||||
|
||||
assert tuple(builder.keys()) == ("top",)
|
||||
assert builder.get_name("top") != "top"
|
||||
|
||||
|
||||
def test_library_builder_uses_library_name_allocation() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["cell_name"] = Pattern()
|
||||
library = Library({"cell_name": Pattern()})
|
||||
|
||||
requests = (
|
||||
("cell name", True, 32),
|
||||
("a name that needs truncation", True, 12),
|
||||
("unsanitized name", False, 32),
|
||||
("", True, 32),
|
||||
)
|
||||
for name, sanitize, max_length in requests:
|
||||
assert builder.get_name(name, sanitize=sanitize, max_length=max_length) == library.get_name(
|
||||
name,
|
||||
sanitize=sanitize,
|
||||
max_length=max_length,
|
||||
)
|
||||
|
||||
|
||||
def test_build_session_subtree_preserves_type_and_builds_deferred_dependencies() -> None:
|
||||
builder = LibraryBuilder()
|
||||
child_calls = 0
|
||||
|
||||
def make_top(lib: ILibrary) -> Pattern:
|
||||
subtree = lib.subtree("child")
|
||||
assert type(subtree) is type(lib)
|
||||
assert set(subtree) == {"child", "leaf"}
|
||||
assert subtree["child"] is lib["child"]
|
||||
with pytest.raises(KeyError):
|
||||
_ = subtree["unrelated"]
|
||||
subtree["_local"] = Pattern()
|
||||
assert "_local" not in lib
|
||||
|
||||
top = Pattern()
|
||||
top.ref("child")
|
||||
return top
|
||||
|
||||
def make_child() -> Pattern:
|
||||
nonlocal child_calls
|
||||
child_calls += 1
|
||||
child = Pattern()
|
||||
child.ref("leaf")
|
||||
return child
|
||||
|
||||
builder.cells.top = cell(make_top)(builder.library)
|
||||
builder.cells.child = cell(make_child)()
|
||||
builder.cells.leaf = Pattern()
|
||||
builder.cells.unrelated = Pattern()
|
||||
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert child_calls == 1
|
||||
assert set(built) == {"top", "child", "leaf", "unrelated"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from pathlib import Path
|
||||
import io
|
||||
|
||||
import numpy
|
||||
import pytest
|
||||
|
|
@ -6,9 +7,10 @@ from numpy.testing import assert_allclose
|
|||
|
||||
from ..file import gdsii
|
||||
from ..file.gdsii import lazy as gdsii_lazy
|
||||
from ..error import LibraryError
|
||||
from ..pattern import Pattern
|
||||
from ..ports import Port
|
||||
from ..library import Library, OverlayLibrary
|
||||
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
|
||||
|
||||
|
||||
def _make_lazy_port_library() -> Library:
|
||||
|
|
@ -29,6 +31,14 @@ def _make_lazy_port_library() -> Library:
|
|||
return lib
|
||||
|
||||
|
||||
def test_gdsii_lazy_write_ignores_non_mapping_library_info() -> None:
|
||||
lib = Library({'top': Pattern()})
|
||||
lib.library_info = None # type: ignore[attr-defined]
|
||||
|
||||
with pytest.raises(LibraryError, match='required for non-GDS-backed lazy writes'):
|
||||
gdsii_lazy.write(lib, io.BytesIO())
|
||||
|
||||
|
||||
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_source.gds'
|
||||
src = _make_lazy_port_library()
|
||||
|
|
@ -43,13 +53,74 @@ def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_pat
|
|||
'child': {'leaf'},
|
||||
'top': {'child'},
|
||||
}
|
||||
assert lib.parent_graph() == {
|
||||
'leaf': {'child'},
|
||||
'child': {'top'},
|
||||
'top': set(),
|
||||
}
|
||||
assert lib.tops() == ['top']
|
||||
global_refs = lib.find_refs_global('leaf')
|
||||
assert_allclose(global_refs[('top', 'child', 'leaf')], [[110, 220, numpy.pi / 2, 0, 1]])
|
||||
assert not lib._cache
|
||||
|
||||
with pytest.raises(ValueError, match='dangling-reference mode'):
|
||||
lib.child_graph(dangling='typo')
|
||||
with pytest.raises(ValueError, match='dangling-reference mode'):
|
||||
lib.find_refs_local('leaf', dangling='typo')
|
||||
|
||||
child = lib['child']
|
||||
assert list(child.refs.keys()) == ['leaf']
|
||||
assert set(lib._cache) == {'child'}
|
||||
|
||||
|
||||
def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_subtree_source.gds'
|
||||
src = _make_lazy_port_library()
|
||||
src['unused'] = Pattern()
|
||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-subtree')
|
||||
|
||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||
subtree = raw.subtree('top')
|
||||
|
||||
assert isinstance(raw, IMaterializable)
|
||||
assert not isinstance(raw, IBorrowing)
|
||||
assert isinstance(subtree, IMaterializable)
|
||||
assert isinstance(subtree, IBorrowing)
|
||||
assert subtree.source_order() == ('leaf', 'child', 'top')
|
||||
assert not raw._cache
|
||||
|
||||
out_file = tmp_path / 'lazy_subtree_out.gds'
|
||||
gdsii_lazy.writefile(subtree, out_file)
|
||||
assert not raw._cache
|
||||
|
||||
roundtrip, info = gdsii.readfile(out_file)
|
||||
assert info['name'] == 'classic-subtree'
|
||||
assert set(roundtrip) == {'leaf', 'child', 'top'}
|
||||
|
||||
|
||||
def test_gdsii_lazy_overlay_subtree_preserves_source_laziness(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_overlay_subtree_source.gds'
|
||||
src = _make_lazy_port_library()
|
||||
src['unused'] = Pattern()
|
||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='overlay-subtree')
|
||||
|
||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(raw)
|
||||
subtree = overlay.subtree('top')
|
||||
|
||||
assert isinstance(subtree, OverlayLibrary)
|
||||
assert subtree.borrowed_sources() == (raw,)
|
||||
|
||||
out_file = tmp_path / 'lazy_overlay_subtree_out.gds'
|
||||
gdsii_lazy.writefile(subtree, out_file)
|
||||
|
||||
assert not raw._cache
|
||||
roundtrip, info = gdsii.readfile(out_file)
|
||||
assert info['name'] == 'overlay-subtree'
|
||||
assert set(roundtrip) == {'leaf', 'child', 'top'}
|
||||
|
||||
|
||||
def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_ports.gds'
|
||||
src = _make_lazy_port_library()
|
||||
|
|
@ -67,6 +138,28 @@ def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> No
|
|||
assert not raw_top.ports
|
||||
|
||||
|
||||
def test_gdsii_lazy_ports_view_detaches_previously_cached_source(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_cached_ports.gds'
|
||||
src = _make_lazy_port_library()
|
||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-cached-ports')
|
||||
|
||||
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||
raw_top = raw['top']
|
||||
processed = raw.with_port_overrides({
|
||||
'top': {
|
||||
'P': Port((1, 2), rotation=0, ptype='wire'),
|
||||
},
|
||||
})
|
||||
|
||||
processed_top = processed['top']
|
||||
|
||||
assert processed_top is not raw_top
|
||||
assert not raw_top.ports
|
||||
assert set(processed_top.ports) == {'P'}
|
||||
assert raw['top'] is raw_top
|
||||
assert not hasattr(processed, 'close')
|
||||
|
||||
|
||||
def test_gdsii_lazy_port_overrides_without_data_stay_lazy(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_port_overrides.gds'
|
||||
src = _make_lazy_port_library()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import pytest
|
|||
pytest.importorskip('pyarrow')
|
||||
|
||||
from .. import PatternError
|
||||
from ..library import Library, OverlayLibrary
|
||||
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
|
||||
from ..pattern import Pattern
|
||||
from ..repetition import Grid
|
||||
from ..file import gdsii
|
||||
|
|
@ -223,6 +223,72 @@ def test_gdsii_lazy_arrow_untouched_write_is_copy_through(tmp_path: Path) -> Non
|
|||
assert out_file.read_bytes() == gds_file.read_bytes()
|
||||
|
||||
|
||||
def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'processed_edit_source.gds'
|
||||
src = _make_small_library()
|
||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='processed-edit')
|
||||
|
||||
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||
processed = raw.with_port_overrides({})
|
||||
processed['top'].polygon((7, 0), vertices=[[0, 0], [4, 0], [0, 4]])
|
||||
|
||||
out_file = tmp_path / 'processed_edit_out.gds'
|
||||
gdsii_lazy_arrow.writefile(processed, out_file)
|
||||
|
||||
roundtrip, _ = gdsii.readfile(out_file)
|
||||
assert len(roundtrip['top'].shapes[(7, 0)]) == 1
|
||||
|
||||
|
||||
def test_gdsii_lazy_arrow_subtree_preserves_raw_copy_and_ref_queries(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'subtree_copy_source.gds'
|
||||
src = _make_small_library()
|
||||
src['unused'] = Pattern()
|
||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='subtree-copy')
|
||||
|
||||
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||
subtree = raw.subtree('top')
|
||||
|
||||
assert isinstance(raw, IMaterializable)
|
||||
assert not isinstance(raw, IBorrowing)
|
||||
assert isinstance(subtree, IMaterializable)
|
||||
assert isinstance(subtree, IBorrowing)
|
||||
assert subtree.source_order() == ('leaf', 'mid', 'top')
|
||||
assert _global_refs_key(subtree.find_refs_global('leaf')) == _global_refs_key(raw.find_refs_global('leaf'))
|
||||
assert not raw._cache
|
||||
|
||||
out_file = tmp_path / 'subtree_copy_out.gds'
|
||||
gdsii_lazy_arrow.writefile(subtree, out_file)
|
||||
|
||||
assert not raw._cache
|
||||
roundtrip, info = gdsii.readfile(out_file)
|
||||
assert info['name'] == 'subtree-copy'
|
||||
assert set(roundtrip) == {'leaf', 'mid', 'top'}
|
||||
|
||||
|
||||
def test_gdsii_lazy_arrow_overlay_subtree_preserves_raw_copy(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'overlay_subtree_source.gds'
|
||||
src = _make_small_library()
|
||||
src['unused'] = Pattern()
|
||||
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='overlay-subtree-copy')
|
||||
|
||||
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(raw)
|
||||
subtree = overlay.subtree('top')
|
||||
|
||||
assert isinstance(subtree, OverlayLibrary)
|
||||
assert subtree.borrowed_sources() == (raw,)
|
||||
assert not raw._cache
|
||||
|
||||
out_file = tmp_path / 'overlay_subtree_out.gds'
|
||||
gdsii_lazy_arrow.writefile(subtree, out_file)
|
||||
|
||||
assert not raw._cache
|
||||
roundtrip, info = gdsii.readfile(out_file)
|
||||
assert info['name'] == 'overlay-subtree-copy'
|
||||
assert set(roundtrip) == {'leaf', 'mid', 'top'}
|
||||
|
||||
|
||||
def test_gdsii_lazy_arrow_gzipped_copy_through(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'copy_source.gds.gz'
|
||||
src = _make_small_library()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import pytest
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from typing import cast, TYPE_CHECKING
|
||||
from numpy.testing import assert_allclose
|
||||
from ..library import Library, LazyLibrary
|
||||
from ..library import IBorrowing, INameView, IMaterializable, ILibraryView, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView
|
||||
from ..pattern import Pattern
|
||||
from ..error import LibraryError, PatternError
|
||||
from ..ports import Port
|
||||
|
|
@ -18,6 +19,7 @@ def test_library_basic() -> None:
|
|||
pat = Pattern()
|
||||
lib["cell1"] = pat
|
||||
|
||||
assert isinstance(lib, INameView)
|
||||
assert "cell1" in lib
|
||||
assert lib["cell1"] is pat
|
||||
assert len(lib) == 1
|
||||
|
|
@ -26,6 +28,22 @@ def test_library_basic() -> None:
|
|||
lib["cell1"] = Pattern() # Overwriting not allowed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary, OverlayLibrary])
|
||||
def test_writable_libraries_are_restricted_mappings(
|
||||
library_cls: type[Library] | type[LazyLibrary] | type[OverlayLibrary],
|
||||
) -> None:
|
||||
lib = library_cls()
|
||||
|
||||
assert isinstance(lib, Mapping)
|
||||
assert not isinstance(lib, MutableMapping)
|
||||
for method in ("update", "setdefault", "pop", "popitem", "clear"):
|
||||
assert not hasattr(lib, method)
|
||||
|
||||
lib["top"] = Pattern()
|
||||
del lib["top"]
|
||||
assert not lib
|
||||
|
||||
|
||||
def test_library_tops() -> None:
|
||||
lib = Library()
|
||||
lib["child"] = Pattern()
|
||||
|
|
@ -36,6 +54,23 @@ def test_library_tops() -> None:
|
|||
assert lib.top() == "parent"
|
||||
|
||||
|
||||
def test_empty_ref_buckets_do_not_create_hierarchy_edges() -> None:
|
||||
parent = Pattern()
|
||||
parent.refs["ghost"]
|
||||
parent.refs["parent"]
|
||||
lib = Library({"parent": parent, "ghost": Pattern()})
|
||||
|
||||
assert not parent.has_refs()
|
||||
assert parent.referenced_patterns() == set()
|
||||
assert set(lib.tops()) == {"parent", "ghost"}
|
||||
assert lib.child_graph() == {"parent": set(), "ghost": set()}
|
||||
assert lib.parent_graph() == {"parent": set(), "ghost": set()}
|
||||
|
||||
lib.dfs(parent, hierarchy=("parent",))
|
||||
flat = lib.flatten("parent")["parent"]
|
||||
assert not flat.refs
|
||||
|
||||
|
||||
def test_library_dangling() -> None:
|
||||
lib = Library()
|
||||
lib["parent"] = Pattern()
|
||||
|
|
@ -44,6 +79,16 @@ def test_library_dangling() -> None:
|
|||
assert lib.dangling_refs() == {"missing"}
|
||||
|
||||
|
||||
def test_library_reachability_ignores_unnamed_refs() -> None:
|
||||
pattern = Pattern()
|
||||
pattern.ref(None)
|
||||
lib = Library({"top": pattern})
|
||||
|
||||
assert pattern.referenced_patterns() == {None}
|
||||
assert lib.referenced_patterns() == set()
|
||||
assert lib.dangling_refs() == set()
|
||||
|
||||
|
||||
def test_library_dangling_graph_modes() -> None:
|
||||
lib = Library()
|
||||
lib["parent"] = Pattern()
|
||||
|
|
@ -65,6 +110,24 @@ def test_library_dangling_graph_modes() -> None:
|
|||
assert lib.child_order(dangling="include") == ["missing", "parent"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "args"),
|
||||
[
|
||||
("child_graph", ()),
|
||||
("parent_graph", ()),
|
||||
("child_order", ()),
|
||||
("find_refs_local", ("top",)),
|
||||
("find_refs_global", ("top",)),
|
||||
("prune_empty", ()),
|
||||
],
|
||||
)
|
||||
def test_library_rejects_unknown_dangling_mode(method: str, args: tuple[object, ...]) -> None:
|
||||
lib = Library({"top": Pattern()})
|
||||
|
||||
with pytest.raises(ValueError, match="dangling-reference mode"):
|
||||
getattr(lib, method)(*args, dangling="typo")
|
||||
|
||||
|
||||
def test_find_refs_with_dangling_modes() -> None:
|
||||
lib = Library()
|
||||
lib["target"] = Pattern()
|
||||
|
|
@ -97,6 +160,16 @@ def test_find_refs_with_dangling_modes() -> None:
|
|||
assert_allclose(global_target[("top", "mid", "target")], [[7, 0, 0, 0, 1]])
|
||||
|
||||
|
||||
def test_find_refs_global_includes_composed_scale_column() -> None:
|
||||
lib = Library({"leaf": Pattern(), "child": Pattern(), "top": Pattern()})
|
||||
lib["child"].ref("leaf", scale=2)
|
||||
lib["top"].ref("child", scale=3)
|
||||
|
||||
transforms = lib.find_refs_global("leaf")
|
||||
|
||||
assert_allclose(transforms[("top", "child", "leaf")], [[0, 0, 0, 0, 6]])
|
||||
|
||||
|
||||
def test_preflight_prune_empty_preserves_dangling_policy(caplog: pytest.LogCaptureFixture) -> None:
|
||||
def make_lib() -> Library:
|
||||
lib = Library()
|
||||
|
|
@ -137,6 +210,42 @@ def test_library_flatten() -> None:
|
|||
assert tuple(assert_vertices[0]) == (10.0, 10.0)
|
||||
|
||||
|
||||
def test_pattern_polygon_traversal_ignores_empty_dangling_bucket() -> None:
|
||||
child = Pattern()
|
||||
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
||||
parent = Pattern()
|
||||
parent.ref("child")
|
||||
parent.refs["missing"]
|
||||
lib = Library({"child": child, "parent": parent})
|
||||
|
||||
polygons = parent.layer_as_polygons((1, 0), flatten=True, library=lib)
|
||||
|
||||
assert len(polygons) == 1
|
||||
|
||||
|
||||
def test_recursive_geometry_rejects_reference_cycles() -> None:
|
||||
lib = Library({"a": Pattern(), "b": Pattern()})
|
||||
lib["a"].ref("b")
|
||||
lib["b"].ref("a")
|
||||
|
||||
with pytest.raises(PatternError, match=r"calculating bounds: .* -> .* ->"):
|
||||
lib["a"].get_bounds(library=lib)
|
||||
with pytest.raises(PatternError, match=r"collecting layer polygons: .* -> .* ->"):
|
||||
lib["a"].layer_as_polygons((1, 0), library=lib)
|
||||
with pytest.raises(PatternError, match=r"visualizing pattern hierarchy: .* -> .* ->"):
|
||||
lib["a"].visualize(library=lib)
|
||||
with pytest.raises(PatternError, match="Circular reference"):
|
||||
lib["a"].deepcopy().flatten(library=lib)
|
||||
|
||||
|
||||
def test_nonrecursive_geometry_allows_reference_cycles() -> None:
|
||||
lib = Library({"loop": Pattern()})
|
||||
lib["loop"].ref("loop")
|
||||
|
||||
assert lib["loop"].get_bounds(library=lib, recurse=False) is None
|
||||
assert lib["loop"].layer_as_polygons((1, 0), flatten=False, library=lib) == []
|
||||
|
||||
|
||||
def test_library_flatten_preserves_ports_only_child() -> None:
|
||||
lib = Library()
|
||||
child = Pattern(ports={"P1": Port((1, 2), 0)})
|
||||
|
|
@ -207,6 +316,42 @@ def test_lazy_library() -> None:
|
|||
assert pat is pat2
|
||||
|
||||
|
||||
def test_lazy_library_reachability_loads_only_reachable_cells() -> None:
|
||||
lib = LazyLibrary()
|
||||
calls = {"top": 0, "child": 0, "unused": 0}
|
||||
|
||||
def make(name: str, target: str | None = None) -> Pattern:
|
||||
calls[name] += 1
|
||||
pattern = Pattern()
|
||||
if target is not None:
|
||||
pattern.ref(target)
|
||||
return pattern
|
||||
|
||||
lib["top"] = lambda: make("top", "child")
|
||||
lib["child"] = lambda: make("child")
|
||||
lib["unused"] = lambda: make("unused")
|
||||
|
||||
assert lib.referenced_patterns("top") == {"child"}
|
||||
assert calls == {"top": 1, "child": 1, "unused": 0}
|
||||
|
||||
|
||||
def test_abstract_view_membership_does_not_materialize_lazy_cells() -> None:
|
||||
lib = LazyLibrary()
|
||||
calls = 0
|
||||
|
||||
def make_pat() -> Pattern:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return Pattern()
|
||||
|
||||
lib["lazy"] = make_pat
|
||||
abstracts = lib.abstract_view()
|
||||
|
||||
assert "lazy" in abstracts
|
||||
assert "missing" not in abstracts
|
||||
assert calls == 0
|
||||
|
||||
|
||||
def test_library_rename() -> None:
|
||||
lib = Library()
|
||||
lib["old"] = Pattern()
|
||||
|
|
@ -221,7 +366,7 @@ def test_library_rename() -> None:
|
|||
assert "old" not in lib["parent"].refs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
||||
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||
def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||
lib = library_cls()
|
||||
lib["top"] = Pattern()
|
||||
|
|
@ -235,7 +380,7 @@ def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibra
|
|||
assert len(lib["parent"].refs["top"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
||||
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||
def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||
lib = library_cls()
|
||||
lib["top"] = Pattern()
|
||||
|
|
@ -245,7 +390,7 @@ def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyL
|
|||
assert list(lib.keys()) == ["top"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
||||
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||
def test_library_rename_missing_raises_library_error(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||
lib = library_cls()
|
||||
lib["top"] = Pattern()
|
||||
|
|
@ -254,7 +399,7 @@ def test_library_rename_missing_raises_library_error(library_cls: type[Library]
|
|||
lib.rename("missing", "new")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
||||
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||
def test_library_move_references_same_target_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||
lib = library_cls()
|
||||
lib["top"] = Pattern()
|
||||
|
|
@ -338,6 +483,80 @@ def test_library_add_returns_only_renamed_entries() -> None:
|
|||
assert "keep" not in rename_map
|
||||
|
||||
|
||||
def test_library_add_name_failure_is_atomic() -> None:
|
||||
destination = Library({"x": Pattern(), "y": Pattern()})
|
||||
source_parent = Pattern()
|
||||
source_parent.ref("x")
|
||||
source = Library({"x": Pattern(), "y": source_parent})
|
||||
|
||||
with pytest.raises(LibraryError, match="Unresolved duplicate"):
|
||||
destination.add(source, rename_theirs=lambda _lib, _name: "z", mutate_other=True)
|
||||
|
||||
assert set(destination) == {"x", "y"}
|
||||
assert set(source) == {"x", "y"}
|
||||
assert set(source["y"].refs) == {"x"}
|
||||
|
||||
|
||||
def test_library_add_callback_sees_earlier_name_reservations() -> None:
|
||||
destination = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||
callback_views: list[set[str]] = []
|
||||
|
||||
def rename(view: INameView, _name: str) -> str:
|
||||
assert isinstance(view, INameView)
|
||||
assert not isinstance(view, Mapping)
|
||||
assert not hasattr(view, "__getitem__")
|
||||
callback_views.append(set(view))
|
||||
return view.get_name("_shape")
|
||||
|
||||
rename_map = destination.add(source, rename_theirs=rename)
|
||||
|
||||
assert len(set(rename_map.values())) == 2
|
||||
assert rename_map["_shape$A"] in callback_views[1]
|
||||
assert set(rename_map.values()) <= set(destination)
|
||||
|
||||
|
||||
def test_library_can_add_itself_with_mutate_other() -> None:
|
||||
lib = Library({"_helper": Pattern()})
|
||||
|
||||
rename_map = lib.add(lib, mutate_other=True)
|
||||
|
||||
assert rename_map["_helper"] in lib
|
||||
assert len(lib) == 2
|
||||
|
||||
|
||||
def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None:
|
||||
overlay = OverlayLibrary()
|
||||
overlay["_shape$A"] = Pattern()
|
||||
overlay["_shape$B"] = Pattern()
|
||||
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||
|
||||
rename_map = overlay.add_source(
|
||||
source,
|
||||
rename_theirs=lambda view, _name: view.get_name("_shape"),
|
||||
)
|
||||
|
||||
assert len(set(rename_map.values())) == 2
|
||||
assert set(rename_map.values()) <= set(overlay)
|
||||
|
||||
|
||||
def test_ports_view_detaches_already_materialized_overlay_pattern() -> None:
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(Library({"top": Pattern()}))
|
||||
raw = overlay["top"]
|
||||
processed = PortsLibraryView(
|
||||
overlay,
|
||||
ports={"top": {"P": Port((1, 2), 0)}},
|
||||
)
|
||||
|
||||
processed_top = processed["top"]
|
||||
|
||||
assert processed_top is not raw
|
||||
assert not raw.ports
|
||||
assert set(processed_top.ports) == {"P"}
|
||||
assert not hasattr(processed, "close")
|
||||
|
||||
|
||||
def test_library_subtree() -> None:
|
||||
lib = Library()
|
||||
lib["a"] = Pattern()
|
||||
|
|
@ -346,9 +565,230 @@ def test_library_subtree() -> None:
|
|||
lib["a"].ref("b")
|
||||
|
||||
sub = lib.subtree("a")
|
||||
assert isinstance(sub, Library)
|
||||
assert "a" in sub
|
||||
assert "b" in sub
|
||||
assert "c" not in sub
|
||||
assert sub["a"] is lib["a"]
|
||||
|
||||
del sub["b"]
|
||||
assert "b" in lib
|
||||
|
||||
|
||||
def test_lazy_library_subtree_preserves_type_and_shares_materialized_patterns() -> None:
|
||||
lib = LazyLibrary()
|
||||
child = Pattern()
|
||||
top = Pattern()
|
||||
top.ref("child")
|
||||
lib["child"] = lambda: child
|
||||
lib["top"] = lambda: top
|
||||
lib["unused"] = Pattern()
|
||||
|
||||
subtree = lib.subtree("top")
|
||||
|
||||
assert isinstance(subtree, LazyLibrary)
|
||||
assert set(subtree) == {"child", "top"}
|
||||
assert subtree["top"] is lib["top"]
|
||||
del subtree["child"]
|
||||
assert "child" in lib
|
||||
|
||||
|
||||
def test_overlay_subtree_preserves_type_sources_and_independent_promotion() -> None:
|
||||
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
|
||||
source["top"].ref("child")
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(source)
|
||||
|
||||
subtree = overlay.subtree("top")
|
||||
|
||||
assert isinstance(subtree, OverlayLibrary)
|
||||
assert set(subtree) == {"child", "top"}
|
||||
assert subtree.borrowed_sources() == overlay.borrowed_sources()
|
||||
subtree_top = subtree["top"]
|
||||
overlay_top = overlay["top"]
|
||||
assert subtree_top is not overlay_top
|
||||
|
||||
shared_subtree = overlay.subtree("top")
|
||||
assert shared_subtree["top"] is overlay_top
|
||||
del shared_subtree["child"]
|
||||
assert "child" in overlay
|
||||
|
||||
|
||||
def test_overlay_subtree_preserves_reference_remaps() -> None:
|
||||
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||
source["parent"].ref("leaf")
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(source)
|
||||
overlay.rename("leaf", "renamed", move_references=True)
|
||||
|
||||
subtree = overlay.subtree("parent")
|
||||
|
||||
assert isinstance(subtree, OverlayLibrary)
|
||||
assert set(subtree) == {"parent", "renamed"}
|
||||
assert set(subtree["parent"].refs) == {"renamed"}
|
||||
|
||||
|
||||
def test_read_only_subtree_is_a_borrowed_subtree_view() -> None:
|
||||
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
|
||||
source["top"].ref("child")
|
||||
view = LibraryView(source)
|
||||
|
||||
subtree = view.subtree("top")
|
||||
|
||||
assert subtree.source_order() == ("child", "top")
|
||||
assert subtree["top"] is source["top"]
|
||||
assert "unused" not in subtree
|
||||
with pytest.raises(KeyError):
|
||||
_ = subtree["unused"]
|
||||
|
||||
|
||||
def test_library_materialization_and_borrowing_capabilities() -> None:
|
||||
lazy = LazyLibrary()
|
||||
lazy["top"] = lambda: Pattern()
|
||||
|
||||
assert isinstance(lazy, IMaterializable)
|
||||
assert not isinstance(lazy, IBorrowing)
|
||||
assert not hasattr(lazy, "borrowed_sources")
|
||||
transient = lazy.materialize("top", persist=False)
|
||||
assert "top" not in lazy.cache
|
||||
assert transient is not lazy["top"]
|
||||
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(lazy)
|
||||
ports = PortsLibraryView(overlay)
|
||||
subtree = ports.subtree("top")
|
||||
|
||||
assert isinstance(overlay, IMaterializable)
|
||||
assert isinstance(overlay, IBorrowing)
|
||||
assert isinstance(ports, IMaterializable)
|
||||
assert isinstance(ports, IBorrowing)
|
||||
assert isinstance(subtree, IMaterializable)
|
||||
assert isinstance(subtree, IBorrowing)
|
||||
assert overlay.borrowed_sources() == (lazy,)
|
||||
assert ports.borrowed_sources() == (overlay,)
|
||||
assert subtree.borrowed_sources() == (ports,)
|
||||
eager = Library({"top": Pattern()})
|
||||
plain_view = LibraryView(lazy)
|
||||
assert not isinstance(eager, IMaterializable | IBorrowing)
|
||||
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
||||
|
||||
|
||||
class _RawCopyView(ILibraryView):
|
||||
def __init__(self) -> None:
|
||||
self.mapping = {"top": Pattern()}
|
||||
|
||||
def __getitem__(self, key: str) -> Pattern:
|
||||
return self.mapping[key]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.mapping)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.mapping)
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
return key in self.mapping
|
||||
|
||||
def raw_struct_bytes(self, name: str) -> bytes:
|
||||
return name.encode()
|
||||
|
||||
def can_copy_raw_struct(self, name: str) -> bool:
|
||||
return name in self.mapping
|
||||
|
||||
|
||||
def test_ports_view_raw_copy_eligibility_tracks_persistent_materialization() -> None:
|
||||
processed = PortsLibraryView(_RawCopyView())
|
||||
|
||||
assert processed.can_copy_raw_struct("top")
|
||||
_ = processed.materialize_many(("top",), persist=False)
|
||||
assert processed.can_copy_raw_struct("top")
|
||||
|
||||
subtree = processed.subtree("top")
|
||||
assert subtree.can_copy_raw_struct("top")
|
||||
_ = subtree["top"]
|
||||
assert not processed.can_copy_raw_struct("top")
|
||||
assert not subtree.can_copy_raw_struct("top")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("materialize_parent", [False, True])
|
||||
@pytest.mark.parametrize("move_references", [False, True])
|
||||
def test_overlay_rename_is_independent_of_materialization(
|
||||
materialize_parent: bool,
|
||||
move_references: bool,
|
||||
) -> None:
|
||||
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||
source["parent"].ref("leaf")
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(source)
|
||||
|
||||
if materialize_parent:
|
||||
_ = overlay["parent"]
|
||||
|
||||
overlay.rename("leaf", "renamed", move_references=move_references)
|
||||
expected_target = "renamed" if move_references else "leaf"
|
||||
|
||||
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
|
||||
assert set(overlay["parent"].refs) == {expected_target}
|
||||
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
|
||||
|
||||
|
||||
def test_overlay_rejects_unknown_dangling_mode() -> None:
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(Library({"top": Pattern()}))
|
||||
|
||||
with pytest.raises(ValueError, match="dangling-reference mode"):
|
||||
overlay.child_graph(dangling="typo")
|
||||
with pytest.raises(ValueError, match="dangling-reference mode"):
|
||||
overlay.find_refs_local("top", dangling="typo")
|
||||
|
||||
|
||||
def test_overlay_rename_preserves_initial_import_target_without_moving_refs() -> None:
|
||||
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||
source["parent"].ref("leaf")
|
||||
overlay = OverlayLibrary()
|
||||
overlay["leaf"] = Pattern()
|
||||
rename_map = overlay.add_source(source, rename_theirs=lambda lib, name: lib.get_name(name))
|
||||
imported_leaf = rename_map["leaf"]
|
||||
|
||||
overlay.rename(imported_leaf, "renamed", move_references=False)
|
||||
|
||||
assert set(overlay["parent"].refs) == {imported_leaf}
|
||||
|
||||
|
||||
def test_overlay_chained_rename_moves_unmaterialized_references() -> None:
|
||||
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||
source["parent"].ref("leaf")
|
||||
overlay = OverlayLibrary()
|
||||
overlay.add_source(source)
|
||||
|
||||
overlay.rename("leaf", "middle", move_references=True)
|
||||
overlay.rename("middle", "final", move_references=True)
|
||||
|
||||
assert set(overlay["parent"].refs) == {"final"}
|
||||
|
||||
|
||||
def _assert_tree_merge_rejected(tree: Library) -> None:
|
||||
destination = Library({"existing": Pattern()})
|
||||
|
||||
with pytest.raises(LibraryError, match="exactly one topcell"):
|
||||
destination << tree
|
||||
|
||||
assert set(destination) == {"existing"}
|
||||
|
||||
|
||||
def test_library_tree_merge_rejects_empty_tree() -> None:
|
||||
_assert_tree_merge_rejected(Library())
|
||||
|
||||
|
||||
def test_library_tree_merge_rejects_cycle_without_top() -> None:
|
||||
tree = Library({"a": Pattern(), "b": Pattern()})
|
||||
tree["a"].ref("b")
|
||||
tree["b"].ref("a")
|
||||
_assert_tree_merge_rejected(tree)
|
||||
|
||||
|
||||
def test_library_tree_merge_rejects_multiple_tops() -> None:
|
||||
_assert_tree_merge_rejected(Library({"a": Pattern(), "b": Pattern()}))
|
||||
|
||||
|
||||
def test_library_child_order_cycle_raises_library_error() -> None:
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ class PreferredMinimumTool(PlanningOnlyTool):
|
|||
BendOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype='wide',
|
||||
priority_bias=100,
|
||||
cost=100,
|
||||
ccw=ccw,
|
||||
length_domain=(2, 2),
|
||||
endpoint_planner=lambda _length: Port((2, jog), rotation, ptype='wide'),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy
|
||||
|
|
@ -146,14 +147,15 @@ def test_straight_offer_generated_factory_uses_default_endpoint_and_data_bbox()
|
|||
'wire',
|
||||
data_at,
|
||||
length_domain=(2, 8),
|
||||
priority_bias=3,
|
||||
cost=3,
|
||||
bbox_for_data=lambda data: numpy.array([[0, 0], [data['length'], 1]]),
|
||||
)
|
||||
|
||||
endpoint = offer.endpoint_at(4)
|
||||
|
||||
assert offer.length_domain == (2, 8)
|
||||
assert offer.priority_bias == 3
|
||||
assert offer.cost == 3
|
||||
assert offer.cost_at(4) == 12
|
||||
assert numpy.allclose(endpoint.offset, [4, 0])
|
||||
assert endpoint.rotation == pi
|
||||
assert endpoint.ptype == 'wire'
|
||||
|
|
@ -256,9 +258,43 @@ def test_prebuilt_offer_factories_return_fresh_endpoint_copies() -> None:
|
|||
assert numpy.allclose(offer.bbox_at(parameter), [[-1, -2], [6, 3]])
|
||||
|
||||
|
||||
def test_offer_rejects_negative_priority_bias() -> None:
|
||||
with pytest.raises(BuildError, match='priority_bias must be nonnegative'):
|
||||
StraightOffer(in_ptype='wire', out_ptype='wire', priority_bias=-1)
|
||||
@pytest.mark.parametrize('cost', [-1, numpy.inf, numpy.nan, 'expensive'])
|
||||
def test_offer_rejects_invalid_cost_factor(cost: Any) -> None:
|
||||
with pytest.raises(BuildError, match='cost factor'):
|
||||
StraightOffer(in_ptype='wire', out_ptype='wire', cost=cost)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('result', [-1, numpy.inf, numpy.nan, 'expensive'])
|
||||
def test_offer_rejects_invalid_callable_cost_result(result: Any) -> None:
|
||||
offer = StraightOffer(
|
||||
in_ptype='wire',
|
||||
out_ptype='wire',
|
||||
cost=lambda _parameter, _endpoint: result,
|
||||
**offer_callbacks(lambda length: (Port((length, 0), rotation=pi, ptype='wire'), None)),
|
||||
)
|
||||
|
||||
with pytest.raises(BuildError, match='Primitive cost'):
|
||||
offer.cost_at(5)
|
||||
|
||||
|
||||
def test_offer_callable_cost_replaces_default_cost() -> None:
|
||||
seen: list[tuple[float, Port]] = []
|
||||
|
||||
def cost(parameter: float, endpoint: Port) -> float:
|
||||
seen.append((parameter, endpoint))
|
||||
return parameter + endpoint.y
|
||||
|
||||
offer = SOffer(
|
||||
in_ptype='wire',
|
||||
out_ptype='wire',
|
||||
cost=cost,
|
||||
jog_domain=(2, 2),
|
||||
**offer_callbacks(lambda jog: (Port((10, jog), rotation=pi, ptype='wire'), None)),
|
||||
)
|
||||
|
||||
assert offer.cost_at(2 + 1e-13) == 4
|
||||
assert seen[0][0] == 2
|
||||
assert numpy.allclose(seen[0][1].offset, (10, 2))
|
||||
|
||||
|
||||
def test_offer_rejects_one_sided_split_callbacks() -> None:
|
||||
|
|
@ -416,8 +452,8 @@ def test_pather_selects_lowest_cost_offer() -> None:
|
|||
return Port((length, 0), rotation=pi, ptype=out_ptype or in_ptype), {'kind': 'low'}
|
||||
|
||||
return (
|
||||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, priority_bias=10, **offer_callbacks(high)),
|
||||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, priority_bias=0, **offer_callbacks(low)),
|
||||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, cost=10, **offer_callbacks(high)),
|
||||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, cost=1, **offer_callbacks(low)),
|
||||
)
|
||||
|
||||
p = Pather(Library(), tools=MultiOfferTool(), render='deferred')
|
||||
|
|
@ -429,6 +465,51 @@ def test_pather_selects_lowest_cost_offer() -> None:
|
|||
assert numpy.allclose(p.ports['A'].offset, (-7, 0))
|
||||
|
||||
|
||||
def test_planner_deduplication_distinguishes_offer_costs() -> None:
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MarkedStraightOffer(StraightOffer):
|
||||
marker: str = ''
|
||||
|
||||
def commit(self, parameter: float) -> dict[str, str | float]:
|
||||
return {'kind': self.marker, 'length': self.canonicalize_parameter(parameter)}
|
||||
|
||||
def endpoint(length: float) -> Port:
|
||||
return Port((length, 0), rotation=pi, ptype='wire')
|
||||
|
||||
def shared_commit(length: float) -> dict[str, float]:
|
||||
return {'length': length}
|
||||
|
||||
class DuplicateGeometryTool(PlanningOnlyTool):
|
||||
def primitive_offers(
|
||||
self,
|
||||
kind: Literal['straight', 'bend', 's', 'u'],
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[PrimitiveOffer, ...]:
|
||||
_ = kwargs
|
||||
if kind != 'straight':
|
||||
return ()
|
||||
common = {
|
||||
'in_ptype': in_ptype,
|
||||
'out_ptype': out_ptype,
|
||||
'endpoint_planner': endpoint,
|
||||
'commit_planner': shared_commit,
|
||||
}
|
||||
return (
|
||||
MarkedStraightOffer(cost=100, marker='expensive', **common),
|
||||
MarkedStraightOffer(cost=1, marker='cheap', **common),
|
||||
)
|
||||
|
||||
pather = Pather(Library(), tools=DuplicateGeometryTool(), render='deferred')
|
||||
pather.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
pather.straight('A', 7)
|
||||
|
||||
assert pather._paths['A'][0].data == {'kind': 'cheap', 'length': 7}
|
||||
|
||||
|
||||
class StrategyTieTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.seen_kwargs: list[dict[str, Any]] = []
|
||||
|
|
@ -591,7 +672,7 @@ def test_pather_commits_only_selected_offer() -> None:
|
|||
if kind != 'straight':
|
||||
return ()
|
||||
|
||||
def make(label: str, priority: float) -> StraightOffer:
|
||||
def make(label: str, cost: float) -> StraightOffer:
|
||||
def endpoint(length: float) -> Port:
|
||||
return Port((length, 0), rotation=pi, ptype=out_ptype or in_ptype)
|
||||
|
||||
|
|
@ -602,12 +683,12 @@ def test_pather_commits_only_selected_offer() -> None:
|
|||
return StraightOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=out_ptype,
|
||||
priority_bias=priority,
|
||||
cost=cost,
|
||||
endpoint_planner=endpoint,
|
||||
commit_planner=commit,
|
||||
)
|
||||
|
||||
return (make('expensive', 100), make('cheap', 0))
|
||||
return (make('expensive', 100), make('cheap', 1))
|
||||
|
||||
p = Pather(Library(), tools=RecordingTool(), render='deferred')
|
||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ def test_data_to_ports_hierarchical() -> None:
|
|||
assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10)
|
||||
|
||||
|
||||
def test_data_to_ports_ignores_empty_dangling_ref_bucket() -> None:
|
||||
parent = Pattern()
|
||||
parent.refs["missing"]
|
||||
|
||||
data_to_ports([(10, 0)], Library(), parent, max_depth=1)
|
||||
|
||||
assert not parent.ports
|
||||
|
||||
|
||||
def test_data_to_ports_hierarchical_scaled_ref() -> None:
|
||||
lib = Library()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue