Compare commits
8 commits
dc828cf5fc
...
1e77863753
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e77863753 | |||
| 77699cafad | |||
| afbbb458c8 | |||
| 13d75dc079 | |||
| c410c4d381 | |||
| 58ab4f6c09 | |||
| 717c31abf4 | |||
| 1f4185107b |
10 changed files with 641 additions and 473 deletions
|
|
@ -22,7 +22,7 @@ Contents
|
|||
- [library](library.py)
|
||||
* Continue from `devices.py` by declaring a mixed library with `BuildLibrary`
|
||||
* Import source-backed GDS cells and register python-generated recipes together
|
||||
* Call `build()` to produce a normal library for downstream `Pather` usage and writing
|
||||
* Call `build()` to produce a normal library and report for downstream `Pather` usage and writing
|
||||
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
|
||||
- [pather](pather.py)
|
||||
* Use `Pather` to route individual wires and wire bundles
|
||||
|
|
|
|||
|
|
@ -81,8 +81,9 @@ def main() -> None:
|
|||
# Build the declaration set into a normal library.
|
||||
#
|
||||
|
||||
built = builder.build()
|
||||
built, report = builder.build()
|
||||
print('Built library contains:\n' + pformat(list(built.keys())))
|
||||
print('Build dependency graph:\n' + pformat(report.dependency_graph))
|
||||
|
||||
#
|
||||
# Continue designing against the built library.
|
||||
|
|
|
|||
|
|
@ -54,9 +54,10 @@ def main() -> None:
|
|||
(rpather.at(['GND', 'VCC'])
|
||||
.trace(True, xmax=-10_000, spacing=5_000) # Move West to -10k, turn South
|
||||
.retool(M1_tool) # Retools both GND and VCC
|
||||
.trace(True, emax=50_000, spacing=1_200) # Turn East, moves 50um extension
|
||||
.trace(False, emin=1_000, spacing=1_200) # U-turn back South
|
||||
.trace(False, emin=2_000, spacing=4_500) # U-turn back West
|
||||
.set_spacing(1_200) # Default bundle spacing for later bends
|
||||
.trace(True, emax=50_000) # Turn East, moves 50um extension
|
||||
.trace(False, emin=1_000) # U-turn back South
|
||||
.trace(False, emin=2_000, spacing=4_500) # U-turn back West, overriding the default spacing
|
||||
)
|
||||
|
||||
# Retool VCC back to M2 and move both to x=-28k
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ from .library import (
|
|||
ILibrary as ILibrary,
|
||||
LibraryView as LibraryView,
|
||||
Library as Library,
|
||||
BuiltLibrary as BuiltLibrary,
|
||||
BuildLibrary as BuildLibrary,
|
||||
BuildReport as BuildReport,
|
||||
CellProvenance as CellProvenance,
|
||||
|
|
|
|||
|
|
@ -1173,30 +1173,50 @@ class Pather(PortList):
|
|||
self.pattern.flatten(self.library)
|
||||
return self
|
||||
|
||||
def at(self, portspec: str | Iterable[str]) -> 'PortPather':
|
||||
return PortPather(portspec, self)
|
||||
def at(
|
||||
self,
|
||||
portspec: str | Iterable[str],
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
) -> 'PortPather':
|
||||
return PortPather(portspec, self, default_spacing=spacing)
|
||||
|
||||
|
||||
class PortPather:
|
||||
""" Port state manager for fluent pathing. """
|
||||
def __init__(self, ports: str | Iterable[str], pather: Pather) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
ports: str | Iterable[str],
|
||||
pather: Pather,
|
||||
*,
|
||||
default_spacing: float | ArrayLike | None = None,
|
||||
) -> None:
|
||||
self.ports = [ports] if isinstance(ports, str) else list(ports)
|
||||
self.pather = pather
|
||||
self.default_spacing = default_spacing
|
||||
|
||||
def retool(self, tool: Tool) -> Self:
|
||||
self.pather.retool(tool, self.ports)
|
||||
return self
|
||||
|
||||
def set_spacing(self, spacing: float | ArrayLike | None) -> Self:
|
||||
self.default_spacing = spacing
|
||||
return self
|
||||
|
||||
@contextmanager
|
||||
def toolctx(self, tool: Tool) -> Iterator[Self]:
|
||||
with self.pather.toolctx(tool, keys=self.ports):
|
||||
yield self
|
||||
|
||||
def trace(self, ccw: SupportsBool | None, length: float | None = None, **kw: Any) -> Self:
|
||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
||||
kw['spacing'] = self.default_spacing
|
||||
self.pather.trace(self.ports, ccw, length, **kw)
|
||||
return self
|
||||
|
||||
def trace_to(self, ccw: SupportsBool | None, **kw: Any) -> Self:
|
||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
||||
kw['spacing'] = self.default_spacing
|
||||
self.pather.trace_to(self.ports, ccw, **kw)
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Both the classic and Arrow-backed lazy GDS readers rely on these helpers.
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import IO, Any, cast
|
||||
from typing import IO, Any, Literal, cast
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterator, Mapping, Sequence
|
||||
import copy
|
||||
|
|
@ -32,7 +32,7 @@ from numpy.typing import NDArray
|
|||
from . import gdsii
|
||||
from .utils import tmpfile
|
||||
from ..error import LibraryError
|
||||
from ..library import ILibrary, ILibraryView, LibraryView, dangling_mode_t
|
||||
from ..library import ILibrary, ILibraryView, LibraryView, _plan_source_names, _source_rename_map, dangling_mode_t
|
||||
from ..pattern import Pattern, map_targets
|
||||
from ..utils import apply_transforms
|
||||
from ..utils.ports2data import data_to_ports
|
||||
|
|
@ -303,26 +303,30 @@ class OverlayLibrary(ILibrary):
|
|||
source: Mapping[str, Pattern] | ILibraryView,
|
||||
*,
|
||||
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
||||
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Add a source-backed library layer.
|
||||
|
||||
Args:
|
||||
rename_theirs: Function used to choose visible names for imported
|
||||
source cells.
|
||||
rename_when: If `'conflict'`, only conflicting names are renamed.
|
||||
If `'always'`, every imported source name is passed through
|
||||
`rename_theirs`.
|
||||
"""
|
||||
view = _coerce_library_view(source)
|
||||
source_order = list(view.source_order())
|
||||
child_graph = view.child_graph(dangling='include')
|
||||
|
||||
source_to_visible: dict[str, str] = {}
|
||||
visible_to_source: dict[str, str] = {}
|
||||
rename_map: dict[str, str] = {}
|
||||
|
||||
for name in source_order:
|
||||
visible = name
|
||||
if visible in self._entries or visible in visible_to_source:
|
||||
if rename_theirs is None:
|
||||
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
||||
visible = rename_theirs(self, name)
|
||||
if visible in self._entries or visible in visible_to_source:
|
||||
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
||||
rename_map[name] = visible
|
||||
source_to_visible[name] = visible
|
||||
visible_to_source[visible] = name
|
||||
source_to_visible = _plan_source_names(
|
||||
self,
|
||||
source_order,
|
||||
self._entries,
|
||||
rename_theirs = rename_theirs,
|
||||
rename_when = rename_when,
|
||||
)
|
||||
visible_to_source = {visible: source_name for source_name, visible in source_to_visible.items()}
|
||||
|
||||
layer = _SourceLayer(
|
||||
library=view,
|
||||
|
|
@ -339,7 +343,7 @@ class OverlayLibrary(ILibrary):
|
|||
if visible_name not in self._order:
|
||||
self._order.append(visible_name)
|
||||
|
||||
return rename_map
|
||||
return _source_rename_map(source_to_visible)
|
||||
|
||||
def rename(
|
||||
self,
|
||||
|
|
@ -549,20 +553,6 @@ class OverlayLibrary(ILibrary):
|
|||
return tuple(name for name in self._order if name in self._entries)
|
||||
|
||||
|
||||
class BuiltOverlayLibrary(OverlayLibrary):
|
||||
"""
|
||||
Internal overlay output returned by `BuildLibrary.build(output='overlay')`.
|
||||
|
||||
The type is intentionally not part of the public API. It exists so build
|
||||
outputs can carry a `build_report` while still behaving like an
|
||||
`OverlayLibrary`.
|
||||
"""
|
||||
|
||||
def __init__(self, *, build_report: Any | None = None) -> None:
|
||||
super().__init__()
|
||||
self.build_report = build_report
|
||||
|
||||
|
||||
def _iter_library_infos(library: Mapping[str, Pattern] | ILibraryView) -> Iterator[dict[str, Any]]:
|
||||
info = getattr(library, 'library_info', None)
|
||||
if isinstance(info, dict):
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ Classes include:
|
|||
library. Generated with `ILibraryView.abstract_view()`.
|
||||
"""
|
||||
from typing import Self, TYPE_CHECKING, Any, cast, TypeAlias, Protocol, Literal
|
||||
from collections.abc import Iterator, Mapping, MutableMapping, Sequence, Callable
|
||||
from collections.abc import Container, Iterator, Mapping, MutableMapping, Sequence, Callable
|
||||
import logging
|
||||
import re
|
||||
import copy
|
||||
from functools import wraps
|
||||
from pprint import pformat
|
||||
from collections import defaultdict
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
|
@ -69,9 +70,6 @@ Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
|||
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
||||
""" How helpers should handle refs whose targets are not present in the library. """
|
||||
|
||||
emitted_via_t: TypeAlias = Literal['declaration', 'helper_write', 'tree_merge', 'source_import']
|
||||
""" Build-provenance origin tags for emitted cells. """
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CellProvenance:
|
||||
|
|
@ -83,29 +81,18 @@ class CellProvenance:
|
|||
chosen.
|
||||
|
||||
Attributes:
|
||||
final_name: Name exposed by the completed library.
|
||||
requested_name: First name requested for this cell during the build.
|
||||
kind: Whether the cell came from a declaration, helper emission, or an
|
||||
imported source library.
|
||||
owner_declared_name: Declared cell responsible for this output cell, if
|
||||
any. Imported source cells leave this as `None`.
|
||||
emitted_via: High-level path by which the cell entered the output.
|
||||
build_chain: Declared-cell dependency chain that was active when the
|
||||
cell was emitted.
|
||||
renamed_from: Original requested name when the final name differs.
|
||||
source_name: Original on-source name for imported cells.
|
||||
source_metadata: Optional source-library metadata copied through from
|
||||
lazy GDS readers.
|
||||
"""
|
||||
final_name: str
|
||||
requested_name: str
|
||||
kind: Literal['declared', 'helper', 'source']
|
||||
owner_declared_name: str | None
|
||||
emitted_via: emitted_via_t
|
||||
build_chain: tuple[str, ...]
|
||||
renamed_from: str | None = None
|
||||
source_name: str | None = None
|
||||
source_metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -121,15 +108,11 @@ class BuildReport:
|
|||
requested_roots: Roots explicitly requested for the run. A full
|
||||
`build()` uses all declared cells.
|
||||
provenance: Mapping from final output name to provenance metadata.
|
||||
owned_cells: Mapping from declared cell name to all final output cell
|
||||
names it owns, including helper cells emitted while that declared
|
||||
cell was building.
|
||||
dependency_graph: Declared-cell dependency graph discovered through
|
||||
library-mediated reads and explicit recipe hints.
|
||||
"""
|
||||
requested_roots: tuple[str, ...]
|
||||
provenance: Mapping[str, CellProvenance]
|
||||
owned_cells: Mapping[str, tuple[str, ...]]
|
||||
dependency_graph: Mapping[str, frozenset[str]]
|
||||
|
||||
|
||||
|
|
@ -166,6 +149,47 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str:
|
|||
return lib.get_name(SINGLE_USE_PREFIX + stem)
|
||||
|
||||
|
||||
def _plan_source_names(
|
||||
target: 'ILibraryView',
|
||||
source_order: Sequence[str],
|
||||
existing_names: Container[str],
|
||||
*,
|
||||
rename_theirs: Callable[['ILibraryView', str], str] | None = None,
|
||||
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||
) -> dict[str, str]:
|
||||
if rename_when not in ('conflict', 'always'):
|
||||
raise ValueError(f'Unknown source rename mode: {rename_when!r}')
|
||||
if rename_when == 'always' and rename_theirs is None:
|
||||
raise TypeError('rename_theirs is required when rename_when="always"')
|
||||
|
||||
source_to_visible: dict[str, str] = {}
|
||||
visible_names: set[str] = set()
|
||||
|
||||
for name in source_order:
|
||||
visible = name
|
||||
if rename_when == 'always':
|
||||
assert rename_theirs is not None
|
||||
visible = rename_theirs(target, name)
|
||||
elif visible in existing_names or visible in visible_names:
|
||||
if rename_theirs is None:
|
||||
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
||||
visible = rename_theirs(target, name)
|
||||
if visible in existing_names or visible in visible_names:
|
||||
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
||||
source_to_visible[name] = visible
|
||||
visible_names.add(visible)
|
||||
|
||||
return source_to_visible
|
||||
|
||||
|
||||
def _source_rename_map(source_to_visible: Mapping[str, str]) -> dict[str, str]:
|
||||
return {
|
||||
source_name: visible_name
|
||||
for source_name, visible_name in source_to_visible.items()
|
||||
if source_name != visible_name
|
||||
}
|
||||
|
||||
|
||||
class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
"""
|
||||
Interface for a read-only library.
|
||||
|
|
@ -1467,42 +1491,6 @@ class Library(ILibrary):
|
|||
return tree, pat
|
||||
|
||||
|
||||
class BuiltLibrary(Library):
|
||||
"""
|
||||
Eager library returned by `BuildLibrary.build(output='library')`.
|
||||
|
||||
This is a normal materialized `Library` with one additional attribute,
|
||||
`build_report`, which records how the library was assembled from
|
||||
declarations, helper emissions, and imported source-backed cells.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mapping: MutableMapping[str, 'Pattern'] | None = None,
|
||||
*,
|
||||
build_report: BuildReport | None = None,
|
||||
) -> None:
|
||||
super().__init__(mapping=mapping)
|
||||
self.build_report = build_report
|
||||
|
||||
|
||||
class _CellFactory:
|
||||
"""
|
||||
Adapter that turns a plain pattern factory into a deferred recipe factory.
|
||||
|
||||
Calling the wrapper captures arguments and returns a `_BuildRecipe`
|
||||
instead of executing the function immediately.
|
||||
"""
|
||||
|
||||
def __init__(self, func: Callable[..., 'Pattern']) -> None:
|
||||
self.func = func
|
||||
self.__name__ = getattr(func, '__name__', type(self).__name__)
|
||||
self.__doc__ = getattr(func, '__doc__')
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> '_BuildRecipe':
|
||||
return _BuildRecipe(func=self.func, args=args, kwargs=kwargs)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _BuildRecipe:
|
||||
""" Captured deferred call to a pattern factory. """
|
||||
|
|
@ -1516,41 +1504,17 @@ class _BuildRecipe:
|
|||
return self
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _PatternDeclaration:
|
||||
""" Declared cell backed by an already-built `Pattern`. """
|
||||
pattern: 'Pattern'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _RecipeDeclaration:
|
||||
""" Declared cell backed by a deferred recipe. """
|
||||
recipe: _BuildRecipe
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _SourceDeclaration:
|
||||
"""
|
||||
Imported source-backed names registered with a `BuildLibrary`.
|
||||
|
||||
The declaration stores visible-name remapping plus pre-scanned graph
|
||||
metadata. Underlying source cells stay lazy until a build session
|
||||
materializes or copies them through.
|
||||
"""
|
||||
library: ILibraryView
|
||||
source_to_visible: Mapping[str, str]
|
||||
visible_to_source: Mapping[str, str]
|
||||
child_graph: Mapping[str, set[str]]
|
||||
order: tuple[str, ...]
|
||||
|
||||
|
||||
def cell(func: Callable[..., 'Pattern']) -> _CellFactory:
|
||||
def cell(func: Callable[..., 'Pattern']) -> Callable[..., _BuildRecipe]:
|
||||
"""
|
||||
Wrap a plain pattern factory so calls return deferred build recipes.
|
||||
|
||||
Use as either `cell(fn)(...)` or `@cell`.
|
||||
"""
|
||||
return _CellFactory(func)
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> _BuildRecipe:
|
||||
return _BuildRecipe(func=func, args=args, kwargs=kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class BuildCellsView:
|
||||
|
|
@ -1596,19 +1560,16 @@ class BuildLibrary(ILibrary):
|
|||
The builder itself is not a normal readable library during authoring.
|
||||
Instead, `validate()` and `build()` create a temporary build-session library
|
||||
that recipes can read from and write helper cells into while dependencies
|
||||
are resolved. `build()` then freezes the builder on success and returns a
|
||||
normal library-like object carrying a `build_report`.
|
||||
are resolved. `build()` then freezes the builder on success and returns the
|
||||
built library plus a `BuildReport`.
|
||||
"""
|
||||
|
||||
def __init__(self, *, check_on_register: bool = False) -> None:
|
||||
self.check_on_register = check_on_register
|
||||
def __init__(self) -> None:
|
||||
self.cells = BuildCellsView(self)
|
||||
self.last_build_report: BuildReport | None = None
|
||||
self._frozen = False
|
||||
self._declarations: dict[str, _PatternDeclaration | _RecipeDeclaration] = {}
|
||||
self._sources: list[_SourceDeclaration] = []
|
||||
self._names: set[str] = set()
|
||||
self._order: list[str] = []
|
||||
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
|
||||
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
||||
self._names: dict[str, None] = {}
|
||||
|
||||
def _active_session(self) -> '_BuildSessionLibrary | None':
|
||||
sessions = _ACTIVE_BUILD_SESSIONS.get()
|
||||
|
|
@ -1633,7 +1594,7 @@ class BuildLibrary(ILibrary):
|
|||
session = self._active_session()
|
||||
if session is not None:
|
||||
return iter(session)
|
||||
return iter(self._order)
|
||||
return iter(self._names)
|
||||
|
||||
def __len__(self) -> int:
|
||||
session = self._active_session()
|
||||
|
|
@ -1653,7 +1614,7 @@ class BuildLibrary(ILibrary):
|
|||
def __setitem__(
|
||||
self,
|
||||
key: str,
|
||||
value: 'Pattern | _BuildRecipe | Callable[[], Pattern]',
|
||||
value: 'Pattern | _BuildRecipe',
|
||||
) -> None:
|
||||
session = self._active_session()
|
||||
if session is not None:
|
||||
|
|
@ -1664,26 +1625,15 @@ class BuildLibrary(ILibrary):
|
|||
if key in self._names:
|
||||
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
||||
|
||||
declaration: _PatternDeclaration | _RecipeDeclaration
|
||||
if isinstance(value, _BuildRecipe):
|
||||
declaration = _RecipeDeclaration(value)
|
||||
declaration = value
|
||||
else:
|
||||
if callable(value):
|
||||
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||
declaration = _PatternDeclaration(value)
|
||||
declaration = value
|
||||
|
||||
self._declarations[key] = declaration
|
||||
self._names.add(key)
|
||||
self._order.append(key)
|
||||
|
||||
if self.check_on_register:
|
||||
try:
|
||||
self.validate(names=(key,))
|
||||
except Exception:
|
||||
del self._declarations[key]
|
||||
self._names.remove(key)
|
||||
self._order.remove(key)
|
||||
raise
|
||||
self._names[key] = None
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
session = self._active_session()
|
||||
|
|
@ -1695,8 +1645,7 @@ class BuildLibrary(ILibrary):
|
|||
if key not in self._declarations:
|
||||
raise KeyError(key)
|
||||
del self._declarations[key]
|
||||
self._names.remove(key)
|
||||
self._order.remove(key)
|
||||
del self._names[key]
|
||||
|
||||
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||
session = self._active_session()
|
||||
|
|
@ -1711,10 +1660,77 @@ class BuildLibrary(ILibrary):
|
|||
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
||||
mutate_other: bool = False,
|
||||
) -> dict[str, str]:
|
||||
from .pattern import map_targets # noqa: PLC0415
|
||||
|
||||
session = self._active_session()
|
||||
if session is not None:
|
||||
return session.add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||
return super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||
|
||||
self._assert_editable()
|
||||
|
||||
source_backed = isinstance(other, ILibraryView) and not isinstance(other, Library | LibraryView)
|
||||
if source_backed:
|
||||
if mutate_other:
|
||||
raise BuildError('BuildLibrary.add(..., mutate_other=True) is not supported for source-backed inputs.')
|
||||
return self.add_source(
|
||||
other,
|
||||
rename_theirs = rename_theirs,
|
||||
rename_when = 'conflict',
|
||||
)
|
||||
|
||||
source_order = tuple(other.keys())
|
||||
source_to_visible = _plan_source_names(
|
||||
self,
|
||||
source_order,
|
||||
self._names,
|
||||
rename_theirs = rename_theirs,
|
||||
rename_when = 'conflict',
|
||||
)
|
||||
rename_map = _source_rename_map(source_to_visible)
|
||||
|
||||
if mutate_other:
|
||||
temp = other
|
||||
else:
|
||||
temp = Library(copy.deepcopy(dict(other)))
|
||||
|
||||
for source_name in source_order:
|
||||
visible_name = source_to_visible[source_name]
|
||||
pattern = temp[source_name]
|
||||
if rename_map:
|
||||
pattern.refs = map_targets(
|
||||
pattern.refs,
|
||||
lambda target: cast('dict[str | None, str | None]', rename_map).get(target, target),
|
||||
)
|
||||
self[visible_name] = pattern
|
||||
|
||||
return rename_map
|
||||
|
||||
def __lshift__(self, other: TreeView) -> str:
|
||||
session = self._active_session()
|
||||
if session is not None:
|
||||
return session << other
|
||||
|
||||
self._assert_editable()
|
||||
if len(other) == 1:
|
||||
name = next(iter(other))
|
||||
elif isinstance(other, ILibraryView) and not isinstance(other, Library | LibraryView):
|
||||
source_order = other.source_order()
|
||||
child_graph = other.child_graph(dangling='include')
|
||||
referenced = set().union(*child_graph.values()) if child_graph else set()
|
||||
tops = [candidate for candidate in source_order if candidate not in referenced]
|
||||
if len(tops) != 1:
|
||||
raise LibraryError(f'Asked for the single topcell, but found the following: {pformat(tops)}')
|
||||
name = tops[0]
|
||||
else:
|
||||
return super().__lshift__(other)
|
||||
|
||||
rename_map = self.add(other)
|
||||
return rename_map.get(name, name)
|
||||
|
||||
def __le__(self, other: Mapping[str, 'Pattern']) -> Abstract:
|
||||
if self._active_session() is not None:
|
||||
return super().__le__(other)
|
||||
raise BuildError('BuildLibrary.__le__() is only available while validate() or build() is running.')
|
||||
|
||||
def rename(
|
||||
self,
|
||||
|
|
@ -1723,12 +1739,11 @@ class BuildLibrary(ILibrary):
|
|||
move_references: bool = False,
|
||||
) -> Self:
|
||||
"""
|
||||
Rename an imported source-backed visible name during authoring.
|
||||
Rename a helper cell during an active build session.
|
||||
|
||||
Only imported source-backed cells may be renamed on the builder itself.
|
||||
Declared/generated cells must be registered under their intended final
|
||||
names. `move_references=True` is intentionally unsupported here because
|
||||
deferred recipes and declaration internals cannot be rewritten safely.
|
||||
During authoring, declared cells must be registered under their
|
||||
intended final names and imported source cells must be renamed through
|
||||
`add_source(...)`.
|
||||
"""
|
||||
session = self._active_session()
|
||||
if session is not None:
|
||||
|
|
@ -1745,46 +1760,11 @@ class BuildLibrary(ILibrary):
|
|||
)
|
||||
if old_name not in self._names:
|
||||
raise LibraryError(f'"{old_name}" does not exist in the builder.')
|
||||
if new_name in self._names:
|
||||
raise LibraryError(f'"{new_name}" already exists in the builder.')
|
||||
if move_references:
|
||||
raise BuildError(
|
||||
'BuildLibrary.rename(..., move_references=True) is not supported for imported source cells. '
|
||||
'Builder-level renames only change the visible imported name.'
|
||||
f'Cannot rename imported source cell "{old_name}" during authoring. '
|
||||
'Choose visible source names with add_source(..., rename_theirs=..., rename_when=...).'
|
||||
)
|
||||
|
||||
source_index = next(
|
||||
(idx for idx, spec in enumerate(self._sources) if old_name in spec.visible_to_source),
|
||||
None,
|
||||
)
|
||||
if source_index is None:
|
||||
raise BuildError(
|
||||
f'Cannot rename "{old_name}" during authoring because only imported source-backed '
|
||||
'cells may be renamed on a BuildLibrary.'
|
||||
)
|
||||
|
||||
spec = self._sources[source_index]
|
||||
source_name = spec.visible_to_source[old_name]
|
||||
source_to_visible = dict(spec.source_to_visible)
|
||||
visible_to_source = dict(spec.visible_to_source)
|
||||
order = list(spec.order)
|
||||
|
||||
source_to_visible[source_name] = new_name
|
||||
del visible_to_source[old_name]
|
||||
visible_to_source[new_name] = source_name
|
||||
order[order.index(old_name)] = new_name
|
||||
|
||||
self._sources[source_index] = replace(
|
||||
spec,
|
||||
source_to_visible=source_to_visible,
|
||||
visible_to_source=visible_to_source,
|
||||
order=tuple(order),
|
||||
)
|
||||
self._names.remove(old_name)
|
||||
self._names.add(new_name)
|
||||
self._order[self._order.index(old_name)] = new_name
|
||||
return self
|
||||
|
||||
def abstract(self, name: str) -> Abstract:
|
||||
return self._require_active_session('abstract').abstract(name)
|
||||
|
||||
|
|
@ -1800,54 +1780,48 @@ class BuildLibrary(ILibrary):
|
|||
source: Mapping[str, 'Pattern'] | ILibraryView,
|
||||
*,
|
||||
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
||||
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Register an imported source-backed library with the builder.
|
||||
|
||||
The source is not materialized immediately. Instead, its names and
|
||||
child graph are scanned once and stored as an import declaration. The
|
||||
source may be renamed on entry to avoid collisions with existing
|
||||
The source is not materialized immediately. Its names are scanned once
|
||||
to reserve visible builder names, then the source is read again when a
|
||||
build session starts. The source's cell membership must not be
|
||||
structurally mutated between `add_source()` and `build()`/`validate()`.
|
||||
Source cells may be renamed on entry to avoid collisions with existing
|
||||
declarations or other imported sources.
|
||||
|
||||
Args:
|
||||
rename_theirs: Function used to choose visible names for imported
|
||||
source cells.
|
||||
rename_when: If `'conflict'`, only conflicting names are renamed.
|
||||
If `'always'`, every imported source name is passed through
|
||||
`rename_theirs`.
|
||||
|
||||
Returns:
|
||||
Mapping of `{source_name: visible_name}` for imported names that
|
||||
were renamed while being added.
|
||||
"""
|
||||
if self._active_session() is not None:
|
||||
raise BuildError('BuildLibrary.add_source() is only available while authoring, not during validate() or build().')
|
||||
self._assert_editable()
|
||||
|
||||
view = source if isinstance(source, ILibraryView) else LibraryView(source)
|
||||
source_order = tuple(view.source_order())
|
||||
child_graph = view.child_graph(dangling='include')
|
||||
source_to_visible = _plan_source_names(
|
||||
self,
|
||||
source_order,
|
||||
self._names,
|
||||
rename_theirs = rename_theirs,
|
||||
rename_when = rename_when,
|
||||
)
|
||||
|
||||
source_to_visible: dict[str, str] = {}
|
||||
visible_to_source: dict[str, str] = {}
|
||||
rename_map: dict[str, str] = {}
|
||||
new_names: list[str] = []
|
||||
|
||||
for name in source_order:
|
||||
visible = name
|
||||
if visible in self._names or visible in visible_to_source:
|
||||
if rename_theirs is None:
|
||||
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
||||
visible = rename_theirs(self, name)
|
||||
if visible in self._names or visible in visible_to_source:
|
||||
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
||||
rename_map[name] = visible
|
||||
source_to_visible[name] = visible
|
||||
visible_to_source[visible] = name
|
||||
new_names.append(visible)
|
||||
|
||||
self._sources.append(_SourceDeclaration(
|
||||
library=view,
|
||||
source_to_visible=dict(source_to_visible),
|
||||
visible_to_source=dict(visible_to_source),
|
||||
child_graph={name: set(children) for name, children in child_graph.items()},
|
||||
order=tuple(source_to_visible[name] for name in source_order),
|
||||
))
|
||||
for visible in new_names:
|
||||
self._names.add(visible)
|
||||
self._order.append(visible)
|
||||
return rename_map
|
||||
self._sources.append((view, dict(source_to_visible)))
|
||||
for source_name in source_order:
|
||||
visible = source_to_visible[source_name]
|
||||
self._names[visible] = None
|
||||
return _source_rename_map(source_to_visible)
|
||||
|
||||
def validate(
|
||||
self,
|
||||
|
|
@ -1862,8 +1836,7 @@ class BuildLibrary(ILibrary):
|
|||
execution path used by `build()`. Any generated library is discarded
|
||||
after validation completes.
|
||||
"""
|
||||
report, _output = self._run_build(names=names, output='overlay', allow_dangling=allow_dangling, persist_output=False)
|
||||
self.last_build_report = report
|
||||
_session, report = self._run_build(names=names, allow_dangling=allow_dangling)
|
||||
return report
|
||||
|
||||
def build(
|
||||
|
|
@ -1871,9 +1844,9 @@ class BuildLibrary(ILibrary):
|
|||
*,
|
||||
output: Literal['overlay', 'library'] = 'overlay',
|
||||
allow_dangling: bool = False,
|
||||
) -> 'BuiltLibrary | ILibrary':
|
||||
) -> tuple[ILibrary, BuildReport]:
|
||||
"""
|
||||
Materialize declarations and return a usable output library.
|
||||
Materialize declarations and return a usable output library plus report.
|
||||
|
||||
Args:
|
||||
output: `'overlay'` preserves imported source-backed cells where
|
||||
|
|
@ -1882,20 +1855,23 @@ class BuildLibrary(ILibrary):
|
|||
allow_dangling: If `False`, fail the build when the completed
|
||||
library still contains dangling references.
|
||||
"""
|
||||
if output not in ('overlay', 'library'):
|
||||
raise ValueError(f'Unknown build output mode: {output!r}')
|
||||
self._assert_editable()
|
||||
report, built_output = self._run_build(names=None, output=output, allow_dangling=allow_dangling, persist_output=True)
|
||||
session, report = self._run_build(names=None, allow_dangling=allow_dangling)
|
||||
if output == 'library':
|
||||
built_output = session.to_library()
|
||||
else:
|
||||
built_output = session.to_overlay()
|
||||
self._frozen = True
|
||||
self.last_build_report = report
|
||||
return built_output
|
||||
return built_output, report
|
||||
|
||||
def _run_build(
|
||||
self,
|
||||
*,
|
||||
names: Sequence[str] | None,
|
||||
output: Literal['overlay', 'library'],
|
||||
allow_dangling: bool,
|
||||
persist_output: bool,
|
||||
) -> tuple[BuildReport, BuiltLibrary | ILibrary | None]:
|
||||
) -> tuple['_BuildSessionLibrary', BuildReport]:
|
||||
roots = tuple(dict.fromkeys(names if names is not None else self._declarations.keys()))
|
||||
unknown = [name for name in roots if name not in self._names]
|
||||
if unknown:
|
||||
|
|
@ -1909,19 +1885,11 @@ class BuildLibrary(ILibrary):
|
|||
session.materialize_many(roots)
|
||||
if not allow_dangling:
|
||||
session.child_graph(dangling='error')
|
||||
if output == 'library':
|
||||
built_output = session.to_library() if persist_output else None
|
||||
elif persist_output:
|
||||
built_output = session.to_overlay()
|
||||
else:
|
||||
built_output = None
|
||||
finally:
|
||||
_ACTIVE_BUILD_SESSIONS.reset(token)
|
||||
|
||||
report = session.build_report(roots)
|
||||
if built_output is not None:
|
||||
built_output.build_report = report
|
||||
return report, built_output
|
||||
return session, report
|
||||
|
||||
|
||||
class _BuildSessionLibrary(ILibrary):
|
||||
|
|
@ -1935,71 +1903,62 @@ class _BuildSessionLibrary(ILibrary):
|
|||
"""
|
||||
|
||||
def __init__(self, builder: BuildLibrary) -> None:
|
||||
from .file.gdsii_lazy_core import BuiltOverlayLibrary, _SourceEntry, _SourceLayer # noqa: PLC0415
|
||||
from .file.gdsii_lazy_core import OverlayLibrary # noqa: PLC0415
|
||||
|
||||
self._builder = builder
|
||||
self._overlay = BuiltOverlayLibrary()
|
||||
self._source_entry_type = _SourceEntry
|
||||
self._source_layer_type = _SourceLayer
|
||||
self._states: dict[str, Literal['unbuilt', 'building', 'built']] = {
|
||||
name: 'unbuilt' for name in builder._declarations
|
||||
}
|
||||
self._overlay = OverlayLibrary()
|
||||
self._built: set[str] = set()
|
||||
self._declared_stack: list[str] = []
|
||||
self._emission_stack: list[str] = []
|
||||
self._emission_via_stack: list[emitted_via_t] = []
|
||||
self._names = set(builder._names)
|
||||
self._order = list(builder._order)
|
||||
self._names = dict(builder._names)
|
||||
self._provenance: dict[str, CellProvenance] = {}
|
||||
self._owned_cells: defaultdict[str, list[str]] = defaultdict(list)
|
||||
self._dependency_graph: defaultdict[str, set[str]] = defaultdict(set)
|
||||
self._install_sources()
|
||||
|
||||
def _install_sources(self) -> None:
|
||||
for spec in self._builder._sources:
|
||||
layer = self._source_layer_type(
|
||||
library=spec.library,
|
||||
source_to_visible=dict(spec.source_to_visible),
|
||||
visible_to_source=dict(spec.visible_to_source),
|
||||
child_graph={name: set(children) for name, children in spec.child_graph.items()},
|
||||
order=list(spec.order),
|
||||
for source_library, source_to_visible in self._builder._sources:
|
||||
source_order = source_library.source_order()
|
||||
expected_names = set(source_to_visible)
|
||||
actual_names = set(source_order)
|
||||
if actual_names != expected_names:
|
||||
added_names = sorted(actual_names - expected_names)
|
||||
removed_names = sorted(expected_names - actual_names)
|
||||
detail = []
|
||||
if added_names:
|
||||
detail.append(f'added={added_names}')
|
||||
if removed_names:
|
||||
detail.append(f'removed={removed_names}')
|
||||
raise BuildError(
|
||||
'Imported source library changed after add_source() was called '
|
||||
f'({", ".join(detail)}). '
|
||||
'Do not structurally mutate source libraries between add_source() and build()/validate().'
|
||||
)
|
||||
layer_index = len(self._overlay._layers)
|
||||
self._overlay._layers.append(layer)
|
||||
source_info = getattr(spec.library, 'library_info', None)
|
||||
source_meta = dict(source_info) if isinstance(source_info, dict) else None
|
||||
|
||||
for source_name, visible_name in spec.source_to_visible.items():
|
||||
self._overlay._entries[visible_name] = self._source_entry_type(
|
||||
layer_index=layer_index,
|
||||
source_name=source_name,
|
||||
def rename_source(_lib: ILibraryView, name: str, *, mapping: Mapping[str, str] = source_to_visible) -> str:
|
||||
return mapping[name]
|
||||
|
||||
self._overlay.add_source(
|
||||
source_library,
|
||||
rename_theirs = rename_source,
|
||||
rename_when = 'always',
|
||||
)
|
||||
if visible_name not in self._overlay._order:
|
||||
self._overlay._order.append(visible_name)
|
||||
|
||||
for source_name in source_order:
|
||||
visible_name = source_to_visible[source_name]
|
||||
self._provenance[visible_name] = CellProvenance(
|
||||
final_name=visible_name,
|
||||
requested_name = source_name,
|
||||
kind = 'source',
|
||||
owner_declared_name = None,
|
||||
emitted_via='source_import',
|
||||
build_chain = (),
|
||||
renamed_from=source_name if visible_name != source_name else None,
|
||||
source_name=source_name,
|
||||
source_metadata=source_meta,
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return (name for name in self._order if name in self._names)
|
||||
return iter(self._names)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._names)
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
return key in self._names or key in self._overlay
|
||||
|
||||
def _touch_name(self, key: str) -> None:
|
||||
if key not in self._names:
|
||||
self._names.add(key)
|
||||
self._order.append(key)
|
||||
return key in self._names
|
||||
|
||||
def _current_declared(self) -> str | None:
|
||||
if not self._declared_stack:
|
||||
|
|
@ -2020,14 +1979,6 @@ class _BuildSessionLibrary(ILibrary):
|
|||
if provenance is not None and provenance.kind == 'source':
|
||||
raise BuildError(f'Cannot {operation} imported source cell "{key}" during an active build session.')
|
||||
|
||||
def _remove_owned_cell(self, owner: str | None, name: str) -> None:
|
||||
if owner is None or owner not in self._owned_cells:
|
||||
return
|
||||
cells = self._owned_cells[owner]
|
||||
self._owned_cells[owner] = [cell for cell in cells if cell != name]
|
||||
if not self._owned_cells[owner]:
|
||||
del self._owned_cells[owner]
|
||||
|
||||
def rename(
|
||||
self,
|
||||
old_name: str,
|
||||
|
|
@ -2046,26 +1997,13 @@ class _BuildSessionLibrary(ILibrary):
|
|||
raise LibraryError(f'"{new_name}" already exists in the library.')
|
||||
|
||||
self._overlay.rename(old_name, new_name, move_references=move_references)
|
||||
self._names.discard(old_name)
|
||||
self._names.add(new_name)
|
||||
if old_name in self._order:
|
||||
idx = self._order.index(old_name)
|
||||
self._order[idx] = new_name
|
||||
self._names = {
|
||||
new_name if name == old_name else name: None
|
||||
for name in self._names
|
||||
}
|
||||
|
||||
provenance = self._provenance.pop(old_name)
|
||||
requested_name = provenance.requested_name
|
||||
self._provenance[new_name] = replace(
|
||||
provenance,
|
||||
final_name=new_name,
|
||||
renamed_from=requested_name if new_name != requested_name else None,
|
||||
)
|
||||
|
||||
owner = provenance.owner_declared_name
|
||||
if owner is not None and owner in self._owned_cells:
|
||||
self._owned_cells[owner] = [
|
||||
new_name if cell_name == old_name else cell_name
|
||||
for cell_name in self._owned_cells[owner]
|
||||
]
|
||||
self._provenance[new_name] = provenance
|
||||
return self
|
||||
|
||||
def __getitem__(self, key: str) -> 'Pattern':
|
||||
|
|
@ -2087,31 +2025,20 @@ class _BuildSessionLibrary(ILibrary):
|
|||
|
||||
pattern = value() if callable(value) else value
|
||||
self._overlay[key] = pattern
|
||||
self._touch_name(key)
|
||||
self._names.setdefault(key, None)
|
||||
|
||||
kind: Literal['declared', 'helper']
|
||||
via = self._emission_via_stack[-1] if self._emission_via_stack else 'helper_write'
|
||||
if current is not None and key == current:
|
||||
kind = 'declared'
|
||||
via = 'declaration'
|
||||
else:
|
||||
kind = 'helper'
|
||||
if not self._emission_via_stack:
|
||||
via = 'helper_write'
|
||||
|
||||
self._emission_stack.append(key)
|
||||
try:
|
||||
self._record_provenance(
|
||||
final_name=key,
|
||||
self._provenance[key] = CellProvenance(
|
||||
requested_name = key,
|
||||
kind = kind,
|
||||
owner_declared_name = current if kind == 'helper' else key,
|
||||
emitted_via=via,
|
||||
build_chain = tuple(self._declared_stack),
|
||||
renamed_from=None,
|
||||
)
|
||||
finally:
|
||||
self._emission_stack.pop()
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
if key not in self._overlay:
|
||||
|
|
@ -2120,15 +2047,10 @@ class _BuildSessionLibrary(ILibrary):
|
|||
raise KeyError(key)
|
||||
|
||||
self._guard_mutable_output_name(key, operation='delete')
|
||||
provenance = self._provenance.get(key)
|
||||
if key in self._overlay:
|
||||
del self._overlay[key]
|
||||
self._names.discard(key)
|
||||
if key in self._order:
|
||||
self._order.remove(key)
|
||||
self._names.pop(key, None)
|
||||
self._provenance.pop(key, None)
|
||||
if provenance is not None:
|
||||
self._remove_owned_cell(provenance.owner_declared_name, key)
|
||||
|
||||
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||
self[key_self] = copy.deepcopy(other[key_other])
|
||||
|
|
@ -2139,11 +2061,7 @@ class _BuildSessionLibrary(ILibrary):
|
|||
rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns,
|
||||
mutate_other: bool = False,
|
||||
) -> dict[str, str]:
|
||||
self._emission_via_stack.append('tree_merge')
|
||||
try:
|
||||
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||
finally:
|
||||
self._emission_via_stack.pop()
|
||||
|
||||
current = self._current_declared()
|
||||
for old_name, new_name in rename_map.items():
|
||||
|
|
@ -2151,40 +2069,13 @@ class _BuildSessionLibrary(ILibrary):
|
|||
self._provenance[new_name] = replace(
|
||||
self._provenance[new_name],
|
||||
requested_name = old_name,
|
||||
renamed_from=old_name,
|
||||
owner_declared_name = current if current is not None else self._provenance[new_name].owner_declared_name,
|
||||
)
|
||||
return rename_map
|
||||
|
||||
def _record_provenance(
|
||||
self,
|
||||
*,
|
||||
final_name: str,
|
||||
requested_name: str,
|
||||
kind: Literal['declared', 'helper'],
|
||||
owner_declared_name: str | None,
|
||||
emitted_via: emitted_via_t,
|
||||
build_chain: tuple[str, ...],
|
||||
renamed_from: str | None,
|
||||
) -> None:
|
||||
self._provenance[final_name] = CellProvenance(
|
||||
final_name=final_name,
|
||||
requested_name=requested_name,
|
||||
kind=kind,
|
||||
owner_declared_name=owner_declared_name,
|
||||
emitted_via=emitted_via,
|
||||
build_chain=build_chain,
|
||||
renamed_from=renamed_from,
|
||||
)
|
||||
if owner_declared_name is not None and final_name not in self._owned_cells[owner_declared_name]:
|
||||
self._owned_cells[owner_declared_name].append(final_name)
|
||||
|
||||
def _wrap_error(self, name: str, exc: Exception) -> BuildError:
|
||||
helper = self._emission_stack[-1] if self._emission_stack else None
|
||||
chain = tuple(self._declared_stack)
|
||||
msg = [f'Failed while building declared cell "{name}"']
|
||||
if helper is not None and helper != name:
|
||||
msg.append(f'while materializing helper/output "{helper}"')
|
||||
if chain:
|
||||
msg.append(f'Dependency chain: {" -> ".join(chain)}')
|
||||
msg.append(f'Cause: {exc}')
|
||||
|
|
@ -2202,25 +2093,23 @@ class _BuildSessionLibrary(ILibrary):
|
|||
def _ensure_declared(self, name: str) -> None:
|
||||
from .pattern import Pattern # noqa: PLC0415
|
||||
|
||||
state = self._states[name]
|
||||
if state == 'built':
|
||||
if name in self._built:
|
||||
return
|
||||
if state == 'building':
|
||||
if name in self._declared_stack:
|
||||
chain = ' -> '.join(self._declared_stack + [name])
|
||||
raise BuildError(f'Cycle detected while building declared cells: {chain}')
|
||||
|
||||
declaration = self._builder._declarations[name]
|
||||
self._states[name] = 'building'
|
||||
self._declared_stack.append(name)
|
||||
try:
|
||||
if isinstance(declaration, _PatternDeclaration):
|
||||
pattern = declaration.pattern.deepcopy()
|
||||
else:
|
||||
for dep in declaration.recipe.explicit_dependencies:
|
||||
if isinstance(declaration, _BuildRecipe):
|
||||
for dep in declaration.explicit_dependencies:
|
||||
self._ensure_named(dep)
|
||||
pattern = declaration.recipe.func(*declaration.recipe.args, **declaration.recipe.kwargs)
|
||||
pattern = declaration.func(*declaration.args, **declaration.kwargs)
|
||||
if not isinstance(pattern, Pattern):
|
||||
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern')
|
||||
else:
|
||||
pattern = declaration.deepcopy()
|
||||
|
||||
if name in self._overlay:
|
||||
if self._overlay[name] is not pattern:
|
||||
|
|
@ -2229,9 +2118,8 @@ class _BuildSessionLibrary(ILibrary):
|
|||
)
|
||||
else:
|
||||
self[name] = pattern
|
||||
self._states[name] = 'built'
|
||||
self._built.add(name)
|
||||
except Exception as exc:
|
||||
self._states[name] = 'unbuilt'
|
||||
raise self._wrap_error(name, exc) from exc
|
||||
finally:
|
||||
self._declared_stack.pop()
|
||||
|
|
@ -2261,23 +2149,18 @@ class _BuildSessionLibrary(ILibrary):
|
|||
for name in self._builder._declarations
|
||||
if name in self._dependency_graph or name in requested_roots
|
||||
}
|
||||
owned_cells = {
|
||||
name: tuple(cells)
|
||||
for name, cells in self._owned_cells.items()
|
||||
}
|
||||
return BuildReport(
|
||||
requested_roots = tuple(dict.fromkeys(requested_roots)),
|
||||
provenance = dict(self._provenance),
|
||||
owned_cells=owned_cells,
|
||||
dependency_graph = dependency_graph,
|
||||
)
|
||||
|
||||
def to_overlay(self) -> ILibrary:
|
||||
return self._overlay
|
||||
|
||||
def to_library(self) -> BuiltLibrary:
|
||||
def to_library(self) -> Library:
|
||||
mapping = {name: self._overlay[name] for name in self._overlay.source_order()}
|
||||
return BuiltLibrary(mapping)
|
||||
return Library(mapping)
|
||||
|
||||
|
||||
class LazyLibrary(ILibrary):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,47 @@
|
|||
from collections.abc import Iterator
|
||||
|
||||
import pytest
|
||||
|
||||
from ..builder import Pather
|
||||
from ..error import BuildError
|
||||
from ..library import BuildLibrary, BuiltLibrary, Library, cell
|
||||
from ..library import BuildLibrary, BuildReport, ILibraryView, Library, cell, dangling_mode_t
|
||||
from ..pattern import Pattern
|
||||
from ..ports import Port
|
||||
|
||||
|
||||
def _owned_by(report: BuildReport, owner: str) -> set[str]:
|
||||
return {
|
||||
name for name, prov in report.provenance.items()
|
||||
if prov.owner_declared_name == owner
|
||||
}
|
||||
|
||||
|
||||
class _MetadataSource(ILibraryView):
|
||||
def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None:
|
||||
self.mapping = mapping
|
||||
self._child_graph = child_graph
|
||||
self.loads = 0
|
||||
|
||||
def __getitem__(self, key: str) -> Pattern:
|
||||
self.loads += 1
|
||||
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 source_order(self) -> tuple[str, ...]:
|
||||
return tuple(self.mapping)
|
||||
|
||||
def child_graph(self, dangling: dangling_mode_t = 'error') -> dict[str, set[str]]: # noqa: ARG002
|
||||
return self._child_graph
|
||||
|
||||
|
||||
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||
builder = BuildLibrary()
|
||||
|
||||
|
|
@ -19,12 +54,12 @@ def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
|||
builder.cells.parent = cell(make_parent)(builder)
|
||||
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
||||
|
||||
built = builder.build()
|
||||
built, report = builder.build()
|
||||
|
||||
assert "parent" in built
|
||||
assert "child" in built
|
||||
assert built.build_report.dependency_graph["parent"] == frozenset({"child"})
|
||||
assert built.build_report.provenance["parent"].kind == "declared"
|
||||
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||
assert report.provenance["parent"].kind == "declared"
|
||||
|
||||
|
||||
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
||||
|
|
@ -40,18 +75,46 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
|
|||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
built = builder.build()
|
||||
report = built.build_report
|
||||
_built, report = builder.build()
|
||||
|
||||
helpers = [
|
||||
prov for prov in report.provenance.values()
|
||||
(name, prov) for name, prov in report.provenance.items()
|
||||
if prov.owner_declared_name == "top" and prov.kind == "helper"
|
||||
]
|
||||
|
||||
assert "top" in report.owned_cells["top"]
|
||||
assert "top" in _owned_by(report, "top")
|
||||
assert len(helpers) == 2
|
||||
assert all(prov.emitted_via == "tree_merge" for prov in helpers)
|
||||
assert any(prov.renamed_from == "_helper" for prov in helpers)
|
||||
assert any(name != prov.requested_name for name, prov in helpers)
|
||||
|
||||
|
||||
def test_build_library_authoring_tree_merge_renames_repeated_single_use_names() -> None:
|
||||
builder = BuildLibrary()
|
||||
tree = Library({"_helper": Pattern()})
|
||||
|
||||
name_a = builder << tree
|
||||
name_b = builder << tree
|
||||
built, report = builder.build()
|
||||
|
||||
assert name_a == "_helper"
|
||||
assert name_b != "_helper"
|
||||
assert name_a in built
|
||||
assert name_b in built
|
||||
assert report.provenance[name_b].requested_name == name_b
|
||||
|
||||
|
||||
def test_build_library_authoring_tree_merge_remaps_internal_refs() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder["_helper"] = Pattern()
|
||||
helper = Pattern()
|
||||
top = Pattern()
|
||||
top.ref("_helper")
|
||||
|
||||
top_name = builder << Library({"_helper": helper, "top": top})
|
||||
built, _report = builder.build()
|
||||
|
||||
assert top_name == "top"
|
||||
assert "_helper" not in built[top_name].refs
|
||||
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:
|
||||
|
|
@ -64,10 +127,10 @@ def test_build_library_requires_build_session_for_reads_and_freezes_after_build(
|
|||
with pytest.raises(BuildError, match="write-only"):
|
||||
_ = builder.cells.leaf
|
||||
|
||||
built = builder.build(output="library")
|
||||
built, report = builder.build(output="library")
|
||||
|
||||
assert isinstance(built, BuiltLibrary)
|
||||
assert built.build_report.requested_roots == ("leaf",)
|
||||
assert isinstance(built, Library)
|
||||
assert report.requested_roots == ("leaf",)
|
||||
|
||||
with pytest.raises(BuildError, match="frozen"):
|
||||
builder["later"] = Pattern()
|
||||
|
|
@ -95,21 +158,6 @@ def test_build_library_validate_is_retryable_after_failure() -> None:
|
|||
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||
|
||||
|
||||
def test_build_library_check_on_register_rolls_back_failed_declarations() -> None:
|
||||
builder = BuildLibrary(check_on_register=True)
|
||||
|
||||
def make_parent(lib: BuildLibrary) -> Pattern:
|
||||
pat = Pattern()
|
||||
pat.ref("child")
|
||||
lib.abstract("child")
|
||||
return pat
|
||||
|
||||
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
||||
builder.cells.parent = cell(make_parent)(builder)
|
||||
|
||||
assert "parent" not in builder
|
||||
|
||||
|
||||
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder["child"] = Pattern()
|
||||
|
|
@ -134,6 +182,14 @@ def test_build_library_validate_rejects_removed_output_argument() -> None:
|
|||
builder.validate(output="library") # type: ignore[call-arg]
|
||||
|
||||
|
||||
def test_build_library_rejects_unknown_build_output_mode() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder["leaf"] = Pattern()
|
||||
|
||||
with pytest.raises(ValueError, match="Unknown build output mode"):
|
||||
builder.build(output="bad") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
||||
|
|
@ -147,50 +203,169 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
|
|||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
built = builder.build()
|
||||
_built, report = builder.build()
|
||||
|
||||
helper_prov = built.build_report.provenance["_route"]
|
||||
helper_prov = report.provenance["_route"]
|
||||
assert helper_prov.kind == "helper"
|
||||
assert helper_prov.owner_declared_name == "top"
|
||||
|
||||
|
||||
def test_build_library_contains_tracks_active_session_names() -> None:
|
||||
builder = BuildLibrary()
|
||||
builder["leaf"] = Pattern()
|
||||
builder.add_source(Library({"src": Pattern()}))
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
assert "leaf" in lib
|
||||
assert "src" in lib
|
||||
assert "_helper" not in lib
|
||||
lib["_helper"] = Pattern()
|
||||
assert "_helper" in lib
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
built, _report = builder.build()
|
||||
|
||||
assert "_helper" in built
|
||||
|
||||
|
||||
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
|
||||
source = Library({"src": Pattern()})
|
||||
builder = BuildLibrary()
|
||||
builder.add_source(source)
|
||||
builder.cells.top = cell(lambda: Pattern())()
|
||||
|
||||
built = builder.build()
|
||||
built, report = builder.build()
|
||||
|
||||
assert "src" in built
|
||||
assert built.build_report.provenance["src"].kind == "source"
|
||||
assert built.build_report.provenance["src"].emitted_via == "source_import"
|
||||
assert report.provenance["src"].kind == "source"
|
||||
|
||||
|
||||
def test_build_library_can_rename_imported_source_cells_during_authoring() -> None:
|
||||
def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
||||
source = Library()
|
||||
source["child"] = Pattern()
|
||||
parent = Pattern()
|
||||
parent.ref("child")
|
||||
source["parent"] = parent
|
||||
|
||||
builder = BuildLibrary()
|
||||
rename_map = builder.add_source(
|
||||
source,
|
||||
rename_theirs=lambda _lib, name: f"mapped_{name}",
|
||||
rename_when="always",
|
||||
)
|
||||
built, report = builder.build()
|
||||
|
||||
assert rename_map == {
|
||||
"child": "mapped_child",
|
||||
"parent": "mapped_parent",
|
||||
}
|
||||
assert "mapped_child" in built["mapped_parent"].refs
|
||||
assert report.provenance["mapped_child"].requested_name == "child"
|
||||
|
||||
|
||||
def test_build_library_authoring_tree_merge_keeps_source_view_lazy() -> None:
|
||||
child = Pattern()
|
||||
top = Pattern()
|
||||
top.ref("child")
|
||||
source = _MetadataSource(
|
||||
{"child": child, "top": top},
|
||||
{"child": set(), "top": {"child"}},
|
||||
)
|
||||
|
||||
builder = BuildLibrary()
|
||||
top_name = builder << source
|
||||
built, _report = builder.build()
|
||||
|
||||
assert top_name == "top"
|
||||
assert "top" in built
|
||||
assert source.loads == 0
|
||||
|
||||
|
||||
def test_build_library_authoring_source_tree_merge_returns_renamed_top() -> None:
|
||||
existing = Pattern()
|
||||
source_top = Pattern()
|
||||
source = _MetadataSource(
|
||||
{"_helper": source_top},
|
||||
{"_helper": set()},
|
||||
)
|
||||
|
||||
builder = BuildLibrary()
|
||||
builder["_helper"] = existing
|
||||
top_name = builder << source
|
||||
built, _report = builder.build()
|
||||
|
||||
assert top_name != "_helper"
|
||||
assert top_name in built
|
||||
assert source.loads == 0
|
||||
|
||||
|
||||
def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_materialization() -> None:
|
||||
source_helper = Pattern()
|
||||
source_top = Pattern()
|
||||
source_top.ref("_helper")
|
||||
source = _MetadataSource(
|
||||
{"_helper": source_helper, "top": source_top},
|
||||
{"_helper": set(), "top": {"_helper"}},
|
||||
)
|
||||
|
||||
builder = BuildLibrary()
|
||||
builder["_helper"] = Pattern()
|
||||
top_name = builder << source
|
||||
built, _report = builder.build(output="library")
|
||||
|
||||
assert top_name == "top"
|
||||
assert "_helper" not in built[top_name].refs
|
||||
assert source.loads == 2
|
||||
|
||||
|
||||
def test_build_library_rejects_authoring_tree_le_before_mutating() -> None:
|
||||
builder = BuildLibrary()
|
||||
|
||||
with pytest.raises(BuildError, match="__le__"):
|
||||
_abstract = builder <= Library({"leaf": Pattern()})
|
||||
|
||||
assert list(builder) == []
|
||||
|
||||
|
||||
def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
||||
source = Library({"src": Pattern()})
|
||||
builder = BuildLibrary()
|
||||
builder.add_source(source)
|
||||
builder.rename("child", "renamed_child")
|
||||
source["late"] = Pattern()
|
||||
|
||||
built = builder.build()
|
||||
|
||||
assert "renamed_child" in built
|
||||
assert "child" not in built
|
||||
assert "renamed_child" in built["parent"].refs
|
||||
assert built.build_report.provenance["renamed_child"].source_name == "child"
|
||||
with pytest.raises(BuildError, match="Do not structurally mutate source libraries"):
|
||||
builder.build()
|
||||
|
||||
|
||||
def test_build_library_rejects_move_references_for_source_rename() -> None:
|
||||
def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
||||
source = Library({"src": Pattern()})
|
||||
builder = BuildLibrary()
|
||||
builder.add_source(source)
|
||||
del source["src"]
|
||||
|
||||
with pytest.raises(BuildError, match="Do not structurally mutate source libraries"):
|
||||
builder.build()
|
||||
|
||||
|
||||
def test_build_library_rejects_add_source_during_build() -> None:
|
||||
builder = BuildLibrary()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
lib.add_source(Library({"src": Pattern()}))
|
||||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
|
||||
with pytest.raises(BuildError, match="add_source"):
|
||||
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="move_references=True"):
|
||||
with pytest.raises(BuildError, match="add_source"):
|
||||
builder.rename("src", "renamed_src", move_references=True)
|
||||
|
||||
|
||||
|
|
@ -202,7 +377,7 @@ def test_build_library_rejects_renaming_declared_cells_during_authoring() -> Non
|
|||
builder.rename("declared", "renamed_declared")
|
||||
|
||||
|
||||
def test_build_library_helper_rename_updates_provenance_and_owned_cells() -> None:
|
||||
def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
||||
builder = BuildLibrary()
|
||||
|
||||
def make_top(lib: BuildLibrary) -> Pattern:
|
||||
|
|
@ -213,18 +388,16 @@ def test_build_library_helper_rename_updates_provenance_and_owned_cells() -> Non
|
|||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
built = builder.build()
|
||||
report = built.build_report
|
||||
built, report = builder.build()
|
||||
|
||||
assert "final_helper" in built
|
||||
assert "_helper" not in built
|
||||
assert "final_helper" in report.owned_cells["top"]
|
||||
assert "_helper" not in report.owned_cells["top"]
|
||||
owned = _owned_by(report, "top")
|
||||
assert "final_helper" in owned
|
||||
assert "_helper" not in owned
|
||||
prov = report.provenance["final_helper"]
|
||||
assert prov.kind == "helper"
|
||||
assert prov.requested_name == "_helper"
|
||||
assert prov.renamed_from == "_helper"
|
||||
assert prov.final_name == "final_helper"
|
||||
|
||||
|
||||
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||
|
|
@ -236,12 +409,11 @@ def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
|||
return Pattern()
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
built = builder.build()
|
||||
report = built.build_report
|
||||
built, report = builder.build()
|
||||
|
||||
assert "_helper" not in built
|
||||
assert "_helper" not in report.provenance
|
||||
assert report.owned_cells["top"] == ("top",)
|
||||
assert _owned_by(report, "top") == {"top"}
|
||||
|
||||
|
||||
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
||||
|
|
@ -258,13 +430,11 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
|
|||
return top
|
||||
|
||||
builder.cells.top = cell(make_top)(builder)
|
||||
built = builder.build()
|
||||
report = built.build_report
|
||||
built, report = builder.build()
|
||||
|
||||
assert "final_helper" in built
|
||||
prov = report.provenance["final_helper"]
|
||||
assert prov.requested_name == "_helper"
|
||||
assert prov.renamed_from == "_helper"
|
||||
|
||||
|
||||
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from pathlib import Path
|
||||
|
||||
import numpy
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from ..file import gdsii, gdsii_lazy
|
||||
|
|
@ -82,6 +83,35 @@ def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: P
|
|||
assert set(abstract.ports) == {'A'}
|
||||
|
||||
|
||||
def test_gdsii_lazy_overlay_add_source_can_rename_every_source_cell() -> None:
|
||||
src = _make_lazy_port_library()
|
||||
overlay = gdsii_lazy.OverlayLibrary()
|
||||
|
||||
rename_map = overlay.add_source(
|
||||
src,
|
||||
rename_theirs=lambda _lib, name: f'mapped_{name}',
|
||||
rename_when='always',
|
||||
)
|
||||
|
||||
assert rename_map == {
|
||||
'leaf': 'mapped_leaf',
|
||||
'child': 'mapped_child',
|
||||
'top': 'mapped_top',
|
||||
}
|
||||
assert tuple(overlay.keys()) == ('mapped_leaf', 'mapped_child', 'mapped_top')
|
||||
assert 'mapped_leaf' in overlay['mapped_child'].refs
|
||||
|
||||
|
||||
def test_gdsii_lazy_overlay_add_source_rename_when_validation() -> None:
|
||||
src = _make_lazy_port_library()
|
||||
|
||||
with pytest.raises(TypeError, match='rename_theirs'):
|
||||
gdsii_lazy.OverlayLibrary().add_source(src, rename_when='always')
|
||||
|
||||
with pytest.raises(ValueError, match='rename mode'):
|
||||
gdsii_lazy.OverlayLibrary().add_source(src, rename_when='sometimes') # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_gdsii_lazy_processed_write_roundtrips_without_explicit_units(tmp_path: Path) -> None:
|
||||
gds_file = tmp_path / 'lazy_roundtrip.gds'
|
||||
src = _make_lazy_port_library()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.testing import assert_allclose, assert_equal
|
||||
|
||||
from masque import Pather, Library, Pattern, Port
|
||||
from masque.builder.tools import PathTool, Tool
|
||||
from masque.error import BuildError, PortError, PatternError
|
||||
from masque.builder.tools import PathTool
|
||||
from masque.error import BuildError, PortError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -117,6 +115,82 @@ def test_pather_bundle_trace() -> None:
|
|||
assert numpy.isclose(p.pattern.ports['A'].offset[0], -20000)
|
||||
assert numpy.isclose(p.pattern.ports['B'].offset[0], -22000)
|
||||
|
||||
def test_portpather_default_spacing_matches_explicit_spacing() -> None:
|
||||
lib_default = Library()
|
||||
tool_default = PathTool(layer='M1', width=1000)
|
||||
p_default = Pather(lib_default, tools=tool_default, auto_render=False)
|
||||
p_default.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_default.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
lib_explicit = Library()
|
||||
tool_explicit = PathTool(layer='M1', width=1000)
|
||||
p_explicit = Pather(lib_explicit, tools=tool_explicit, auto_render=False)
|
||||
p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p_default.at(['A', 'B'], spacing=2000).ccw(xmin=-20000)
|
||||
p_explicit.at(['A', 'B']).ccw(xmin=-20000, spacing=2000)
|
||||
|
||||
assert_allclose(p_default.pattern.ports['A'].offset, p_explicit.pattern.ports['A'].offset)
|
||||
assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset)
|
||||
|
||||
def test_portpather_default_spacing_reused_and_overridden() -> None:
|
||||
p_default = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_default.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_default.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
pp_default = p_default.at(['A', 'B'], spacing=2000)
|
||||
pp_default.ccw(xmin=-20000)
|
||||
pp_default.cw(emin=1000)
|
||||
p_explicit.at(['A', 'B']).ccw(xmin=-20000, spacing=2000).cw(emin=1000, spacing=2000)
|
||||
|
||||
assert_allclose(p_default.pattern.ports['A'].offset, p_explicit.pattern.ports['A'].offset)
|
||||
assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset)
|
||||
|
||||
p_override = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_override.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_override.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p_override_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False)
|
||||
p_override_explicit.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p_override_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p_override.at(['A', 'B'], spacing=2000).ccw(xmin=-20000, spacing=3000)
|
||||
p_override_explicit.at(['A', 'B']).ccw(xmin=-20000, spacing=3000)
|
||||
|
||||
assert_allclose(p_override.pattern.ports['A'].offset, p_override_explicit.pattern.ports['A'].offset)
|
||||
assert_allclose(p_override.pattern.ports['B'].offset, p_override_explicit.pattern.ports['B'].offset)
|
||||
|
||||
def test_portpather_default_spacing_not_injected_for_straight_bundle() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1000)
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
|
||||
p.at(['A', 'B'], spacing=2000).straight(xmin=-10000)
|
||||
|
||||
assert numpy.isclose(p.pattern.ports['A'].offset[0], -10000)
|
||||
assert numpy.isclose(p.pattern.ports['B'].offset[0], -10000)
|
||||
|
||||
def test_portpather_default_spacing_vector_revalidated_after_selection_change() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1000)
|
||||
p = Pather(lib, tools=tool, auto_render=False)
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
||||
p.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
||||
p.pattern.ports['C'] = Port((0, 4000), rotation=0)
|
||||
|
||||
pp = p.at(['A', 'B', 'C'], spacing=[2000, 3000])
|
||||
pp.deselect('C')
|
||||
|
||||
with pytest.raises(BuildError, match='spacing must be scalar or have length 1'):
|
||||
pp.ccw(xmin=-20000)
|
||||
|
||||
def test_pather_each_bound() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1000)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue