[library] More library rework

This commit is contained in:
Jan Petykiewicz 2026-07-13 12:24:33 -07:00
commit 0edd735a11
25 changed files with 2357 additions and 870 deletions

View file

@ -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")

View file

@ -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"}

View file

@ -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()

View file

@ -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()

View file

@ -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:

View file

@ -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'),

View file

@ -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')

View file

@ -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()