[library / gdsii] add materialize_detached and do some more cleanup
This commit is contained in:
parent
29947c6c01
commit
93dd3503ca
11 changed files with 930 additions and 23 deletions
|
|
@ -35,6 +35,7 @@ from ...error import LibraryError
|
||||||
from ...library import (
|
from ...library import (
|
||||||
ILibraryView,
|
ILibraryView,
|
||||||
IMaterializable,
|
IMaterializable,
|
||||||
|
LibraryView,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -197,10 +198,7 @@ class GdsLibrarySource(ILibraryView, IMaterializable):
|
||||||
raise LibraryError(f'Unexpected end of GDS source while copying structure {name!r}')
|
raise LibraryError(f'Unexpected end of GDS source while copying structure {name!r}')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
def _decode_pattern(self, name: str) -> Pattern:
|
||||||
if name in self._cache:
|
|
||||||
return self._cache[name]
|
|
||||||
|
|
||||||
if name not in self._cells:
|
if name not in self._cells:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
|
|
||||||
|
|
@ -220,10 +218,32 @@ class GdsLibrarySource(ILibraryView, IMaterializable):
|
||||||
finally:
|
finally:
|
||||||
self._lookups_in_progress.pop()
|
self._lookups_in_progress.pop()
|
||||||
|
|
||||||
|
return pat
|
||||||
|
|
||||||
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
|
if name in self._cache:
|
||||||
|
return self._cache[name]
|
||||||
|
|
||||||
|
pat = self._decode_pattern(name)
|
||||||
|
|
||||||
if persist:
|
if persist:
|
||||||
self._cache[name] = pat
|
self._cache[name] = pat
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
def materialize_detached(self, name: str) -> Pattern:
|
||||||
|
if name in self._cache:
|
||||||
|
return self._cache[name].deepcopy()
|
||||||
|
return self._decode_pattern(name)
|
||||||
|
|
||||||
|
def materialize_many_detached(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
) -> LibraryView:
|
||||||
|
return LibraryView({
|
||||||
|
name: self.materialize_detached(name)
|
||||||
|
for name in dict.fromkeys(names)
|
||||||
|
})
|
||||||
|
|
||||||
def _raw_children(self, name: str) -> set[str]:
|
def _raw_children(self, name: str) -> set[str]:
|
||||||
if name in self._cache:
|
if name in self._cache:
|
||||||
return super()._raw_children(name)
|
return super()._raw_children(name)
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,14 @@ class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
*,
|
*,
|
||||||
persist: bool = True,
|
persist: bool = True,
|
||||||
) -> LibraryView:
|
) -> LibraryView:
|
||||||
mats = self._materialize_patterns(names, persist=persist)
|
mats = self._materialize_patterns(names, persist=persist, detached=False)
|
||||||
|
return LibraryView(mats)
|
||||||
|
|
||||||
|
def materialize_many_detached(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
) -> LibraryView:
|
||||||
|
mats = self._materialize_patterns(names, persist=False, detached=True)
|
||||||
return LibraryView(mats)
|
return LibraryView(mats)
|
||||||
|
|
||||||
def _materialize_patterns(
|
def _materialize_patterns(
|
||||||
|
|
@ -257,6 +264,7 @@ class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
names: Sequence[str],
|
names: Sequence[str],
|
||||||
*,
|
*,
|
||||||
persist: bool,
|
persist: bool,
|
||||||
|
detached: bool,
|
||||||
) -> dict[str, Pattern]:
|
) -> dict[str, Pattern]:
|
||||||
ordered_names = list(dict.fromkeys(names))
|
ordered_names = list(dict.fromkeys(names))
|
||||||
missing = [name for name in ordered_names if name not in self._payload.cells]
|
missing = [name for name in ordered_names if name not in self._payload.cells]
|
||||||
|
|
@ -286,12 +294,16 @@ class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
self._cache[name] = pat
|
self._cache[name] = pat
|
||||||
|
|
||||||
for name in ordered_names:
|
for name in ordered_names:
|
||||||
if name in self._cache:
|
if name not in materialized:
|
||||||
materialized[name] = self._cache[name]
|
cached = self._cache[name]
|
||||||
|
materialized[name] = cached.deepcopy() if detached else cached
|
||||||
return materialized
|
return materialized
|
||||||
|
|
||||||
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
return self._materialize_patterns((name,), persist=persist)[name]
|
return self._materialize_patterns((name,), persist=persist, detached=False)[name]
|
||||||
|
|
||||||
|
def materialize_detached(self, name: str) -> Pattern:
|
||||||
|
return self._materialize_patterns((name,), persist=False, detached=True)[name]
|
||||||
|
|
||||||
def _raw_children(self, name: str) -> set[str]:
|
def _raw_children(self, name: str) -> set[str]:
|
||||||
if name in self._cache:
|
if name in self._cache:
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@ from numpy.typing import ArrayLike
|
||||||
import svgwrite # type: ignore
|
import svgwrite # type: ignore
|
||||||
|
|
||||||
from .utils import mangle_name
|
from .utils import mangle_name
|
||||||
from .. import Pattern
|
from .. import Pattern, Ref
|
||||||
|
from ..library import IMaterializable
|
||||||
from ..utils import rotation_matrix_2d
|
from ..utils import rotation_matrix_2d
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _ref_to_svg_transform(ref) -> str:
|
def _ref_to_svg_transform(ref: Ref) -> str:
|
||||||
linear = rotation_matrix_2d(ref.rotation) * ref.scale
|
linear = rotation_matrix_2d(ref.rotation) * ref.scale
|
||||||
if ref.mirrored:
|
if ref.mirrored:
|
||||||
linear = linear @ numpy.diag((1.0, -1.0))
|
linear = linear @ numpy.diag((1.0, -1.0))
|
||||||
|
|
@ -46,6 +47,9 @@ def _make_svg_ids(names: Mapping[str, Pattern]) -> dict[str, str]:
|
||||||
|
|
||||||
|
|
||||||
def _detached_library(library: Mapping[str, Pattern]) -> dict[str, Pattern]:
|
def _detached_library(library: Mapping[str, Pattern]) -> dict[str, Pattern]:
|
||||||
|
if isinstance(library, IMaterializable):
|
||||||
|
detached = library.materialize_many_detached(tuple(library))
|
||||||
|
return dict(detached.items())
|
||||||
return {name: pat.deepcopy() for name, pat in library.items()}
|
return {name: pat.deepcopy() for name, pat in library.items()}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,23 @@ class IMaterializable(ABC):
|
||||||
for name in dict.fromkeys(names)
|
for name in dict.fromkeys(names)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def materialize_detached(self, name: str) -> Pattern:
|
||||||
|
"""Materialize a caller-owned pattern which is safe to mutate."""
|
||||||
|
return self.materialize(name, persist=False).deepcopy()
|
||||||
|
|
||||||
|
def materialize_many_detached(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
) -> LibraryView:
|
||||||
|
"""Materialize caller-owned patterns without retaining them in this library."""
|
||||||
|
from .mapping import LibraryView # noqa: PLC0415
|
||||||
|
|
||||||
|
materialized = self.materialize_many(names, persist=False)
|
||||||
|
return LibraryView({
|
||||||
|
name: materialized[name].deepcopy()
|
||||||
|
for name in dict.fromkeys(names)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class IBorrowing(ABC):
|
class IBorrowing(ABC):
|
||||||
"""Capability for library views which directly borrow other libraries."""
|
"""Capability for library views which directly borrow other libraries."""
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from .capabilities import IBorrowing, IMaterializable
|
||||||
from .utils import dangling_mode_t, _validate_dangling_mode
|
from .utils import dangling_mode_t, _validate_dangling_mode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Iterator, Mapping, MutableMapping
|
from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
@ -103,6 +103,25 @@ class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
return self._source.materialize(name, persist=persist)
|
return self._source.materialize(name, persist=persist)
|
||||||
return self._source[name]
|
return self._source[name]
|
||||||
|
|
||||||
|
def materialize_detached(self, name: str) -> Pattern:
|
||||||
|
if name not in self._names:
|
||||||
|
raise KeyError(name)
|
||||||
|
if isinstance(self._source, IMaterializable):
|
||||||
|
return self._source.materialize_detached(name)
|
||||||
|
return self._source[name].deepcopy()
|
||||||
|
|
||||||
|
def materialize_many_detached(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
) -> LibraryView:
|
||||||
|
ordered_names = tuple(dict.fromkeys(names))
|
||||||
|
missing = next((name for name in ordered_names if name not in self._names), None)
|
||||||
|
if missing is not None:
|
||||||
|
raise KeyError(missing)
|
||||||
|
if isinstance(self._source, IMaterializable):
|
||||||
|
return self._source.materialize_many_detached(ordered_names)
|
||||||
|
return LibraryView({name: self._source[name].deepcopy() for name in ordered_names})
|
||||||
|
|
||||||
def child_graph(
|
def child_graph(
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class _SourceEntry:
|
||||||
|
|
||||||
def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern:
|
def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern:
|
||||||
if isinstance(view, IMaterializable):
|
if isinstance(view, IMaterializable):
|
||||||
return view.materialize(name, persist=False).deepcopy()
|
return view.materialize_detached(name)
|
||||||
return view[name].deepcopy()
|
return view[name].deepcopy()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -74,10 +74,7 @@ class _ProcessedLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
"""Apply this view's processing to one detached source pattern."""
|
"""Apply this view's processing to one detached source pattern."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
def _materialize_uncached_detached(self, name: str) -> Pattern:
|
||||||
if name in self._cache:
|
|
||||||
return self._cache[name]
|
|
||||||
|
|
||||||
if name in self._lookups_in_progress:
|
if name in self._lookups_in_progress:
|
||||||
chain = ' -> '.join(self._lookups_in_progress + [name])
|
chain = ' -> '.join(self._lookups_in_progress + [name])
|
||||||
raise LibraryError(
|
raise LibraryError(
|
||||||
|
|
@ -93,10 +90,52 @@ class _ProcessedLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
finally:
|
finally:
|
||||||
self._lookups_in_progress.pop()
|
self._lookups_in_progress.pop()
|
||||||
|
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
|
if name in self._cache:
|
||||||
|
return self._cache[name]
|
||||||
|
|
||||||
|
pattern = self._materialize_uncached_detached(name)
|
||||||
|
|
||||||
if persist:
|
if persist:
|
||||||
self._cache[name] = pattern
|
self._cache[name] = pattern
|
||||||
return pattern
|
return pattern
|
||||||
|
|
||||||
|
def materialize_detached(self, name: str) -> Pattern:
|
||||||
|
if name in self._cache:
|
||||||
|
return self._cache[name].deepcopy()
|
||||||
|
return self._materialize_uncached_detached(name)
|
||||||
|
|
||||||
|
def materialize_many_detached(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
) -> LibraryView:
|
||||||
|
ordered_names = tuple(dict.fromkeys(names))
|
||||||
|
result: dict[str, Pattern] = {}
|
||||||
|
uncached = [name for name in ordered_names if name not in self._cache]
|
||||||
|
|
||||||
|
for name in ordered_names:
|
||||||
|
if name in self._cache:
|
||||||
|
result[name] = self._cache[name].deepcopy()
|
||||||
|
|
||||||
|
if uncached:
|
||||||
|
if isinstance(self._source, IMaterializable):
|
||||||
|
source_patterns = self._source.materialize_many_detached(uncached)
|
||||||
|
else:
|
||||||
|
source_patterns = LibraryView({name: self._source[name].deepcopy() for name in uncached})
|
||||||
|
for name in uncached:
|
||||||
|
if name in self._lookups_in_progress:
|
||||||
|
chain = ' -> '.join(self._lookups_in_progress + [name])
|
||||||
|
raise LibraryError(f'Detected circular reference or recursive lookup of "{name}".\nLookup chain: {chain}')
|
||||||
|
self._lookups_in_progress.append(name)
|
||||||
|
try:
|
||||||
|
result[name] = self._process_pattern(name, source_patterns[name])
|
||||||
|
finally:
|
||||||
|
self._lookups_in_progress.pop()
|
||||||
|
|
||||||
|
return LibraryView({name: result[name] for name in ordered_names})
|
||||||
|
|
||||||
def source_order(self) -> tuple[str, ...]:
|
def source_order(self) -> tuple[str, ...]:
|
||||||
return self._source.source_order()
|
return self._source.source_order()
|
||||||
|
|
||||||
|
|
@ -362,6 +401,14 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
visible = layer.source_target_map.get(target, target)
|
visible = layer.source_target_map.get(target, target)
|
||||||
return self._resolve_target(visible)
|
return self._resolve_target(visible)
|
||||||
|
|
||||||
|
def _remap_source_pattern(self, layer: _SourceLayer, source_pat: Pattern) -> Pattern:
|
||||||
|
def remap(target: str | None) -> str | None:
|
||||||
|
return None if target is None else self._effective_target(layer, target)
|
||||||
|
|
||||||
|
if source_pat.refs:
|
||||||
|
source_pat.refs = map_targets(source_pat.refs, remap)
|
||||||
|
return source_pat
|
||||||
|
|
||||||
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
if name not in self._entries:
|
if name not in self._entries:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
|
|
@ -371,17 +418,54 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
|
|
||||||
layer = self._layers[entry.layer_index]
|
layer = self._layers[entry.layer_index]
|
||||||
source_pat = _materialize_detached_pattern(layer.library, entry.source_name)
|
source_pat = _materialize_detached_pattern(layer.library, entry.source_name)
|
||||||
|
pat = self._remap_source_pattern(layer, source_pat)
|
||||||
def remap(target: str | None) -> str | None:
|
|
||||||
return None if target is None else self._effective_target(layer, target)
|
|
||||||
|
|
||||||
if source_pat.refs:
|
|
||||||
source_pat.refs = map_targets(source_pat.refs, remap)
|
|
||||||
pat = source_pat
|
|
||||||
if persist:
|
if persist:
|
||||||
self._entries[name] = pat
|
self._entries[name] = pat
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
def materialize_detached(self, name: str) -> Pattern:
|
||||||
|
if name not in self._entries:
|
||||||
|
raise KeyError(name)
|
||||||
|
entry = self._entries[name]
|
||||||
|
if isinstance(entry, Pattern):
|
||||||
|
return entry.deepcopy()
|
||||||
|
layer = self._layers[entry.layer_index]
|
||||||
|
source_pat = _materialize_detached_pattern(layer.library, entry.source_name)
|
||||||
|
return self._remap_source_pattern(layer, source_pat)
|
||||||
|
|
||||||
|
def materialize_many_detached(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
) -> LibraryView:
|
||||||
|
ordered_names = tuple(dict.fromkeys(names))
|
||||||
|
missing = next((name for name in ordered_names if name not in self._entries), None)
|
||||||
|
if missing is not None:
|
||||||
|
raise KeyError(missing)
|
||||||
|
|
||||||
|
result: dict[str, Pattern] = {}
|
||||||
|
grouped: dict[int, list[tuple[str, str]]] = defaultdict(list)
|
||||||
|
for name in ordered_names:
|
||||||
|
entry = self._entries[name]
|
||||||
|
if isinstance(entry, Pattern):
|
||||||
|
result[name] = entry.deepcopy()
|
||||||
|
else:
|
||||||
|
grouped[entry.layer_index].append((name, entry.source_name))
|
||||||
|
|
||||||
|
for layer_index, cells in grouped.items():
|
||||||
|
layer = self._layers[layer_index]
|
||||||
|
source_names = [source_name for _name, source_name in cells]
|
||||||
|
if isinstance(layer.library, IMaterializable):
|
||||||
|
source_patterns = layer.library.materialize_many_detached(source_names)
|
||||||
|
else:
|
||||||
|
source_patterns = LibraryView({
|
||||||
|
source_name: layer.library[source_name].deepcopy()
|
||||||
|
for source_name in source_names
|
||||||
|
})
|
||||||
|
for name, source_name in cells:
|
||||||
|
result[name] = self._remap_source_pattern(layer, source_patterns[source_name])
|
||||||
|
|
||||||
|
return LibraryView({name: result[name] for name in ordered_names})
|
||||||
|
|
||||||
def child_graph(
|
def child_graph(
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,36 @@ def test_gdsii_lazy_cached_source_cell_disables_only_its_raw_copy(
|
||||||
assert set(roundtrip['leaf'].labels) == {(10, 0), (30, 0)}
|
assert set(roundtrip['leaf'].labels) == {(10, 0), (30, 0)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_detached_processing_copies_only_cached_patterns(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
gds_file = tmp_path / 'classic_detached_source.gds'
|
||||||
|
gdsii.writefile(_make_lazy_port_library(), gds_file, meters_per_unit=1e-9)
|
||||||
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
|
mapped = LayerMappedView(raw, lambda layer: layer)
|
||||||
|
|
||||||
|
original_deepcopy = Pattern.deepcopy
|
||||||
|
copied: list[Pattern] = []
|
||||||
|
|
||||||
|
def count_deepcopy(pattern: Pattern) -> Pattern:
|
||||||
|
copied.append(pattern)
|
||||||
|
return original_deepcopy(pattern)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
|
||||||
|
fresh = mapped.materialize_detached('top')
|
||||||
|
assert not copied
|
||||||
|
assert not raw._cache
|
||||||
|
assert not mapped._cache
|
||||||
|
|
||||||
|
cached = raw['top']
|
||||||
|
copied.clear()
|
||||||
|
detached = mapped.materialize_detached('top')
|
||||||
|
assert copied == [cached]
|
||||||
|
assert detached is not cached
|
||||||
|
assert detached is not fresh
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_materialized_cell_disables_only_its_raw_copy(
|
def test_gdsii_lazy_materialized_cell_disables_only_its_raw_copy(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,32 @@ def test_gdsii_lazy_arrow_ref_queries_match_eager_reader(tmp_path: Path) -> None
|
||||||
assert _global_refs_key(lazy.find_refs_global(name)) == _global_refs_key(eager.find_refs_global(name))
|
assert _global_refs_key(lazy.find_refs_global(name)) == _global_refs_key(eager.find_refs_global(name))
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_arrow_detached_batch_preserves_native_batching(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
gds_file = tmp_path / 'lazy_arrow_detached_batch.gds'
|
||||||
|
gdsii.writefile(_make_small_library(), gds_file, meters_per_unit=1e-9)
|
||||||
|
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||||
|
mapped = LayerMappedView(raw, lambda layer: layer)
|
||||||
|
|
||||||
|
original_read = gdsii_arrow._read_selected_cells_to_arrow
|
||||||
|
call_count = 0
|
||||||
|
|
||||||
|
def count_read(*args, **kwargs) -> object:
|
||||||
|
nonlocal call_count
|
||||||
|
call_count += 1
|
||||||
|
return original_read(*args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(gdsii_arrow, '_read_selected_cells_to_arrow', count_read)
|
||||||
|
detached = mapped.materialize_many_detached(('leaf', 'mid', 'leaf'))
|
||||||
|
|
||||||
|
assert tuple(detached) == ('leaf', 'mid')
|
||||||
|
assert call_count == 1
|
||||||
|
assert not raw._cache
|
||||||
|
assert not mapped._cache
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_arrow_invalid_input_raises_klamath_error(tmp_path: Path) -> None:
|
def test_gdsii_lazy_arrow_invalid_input_raises_klamath_error(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'invalid.gds'
|
gds_file = tmp_path / 'invalid.gds'
|
||||||
gds_file.write_bytes(b'not-a-gds')
|
gds_file.write_bytes(b'not-a-gds')
|
||||||
|
|
|
||||||
|
|
@ -825,6 +825,74 @@ def test_library_materialization_and_borrowing_capabilities() -> None:
|
||||||
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
||||||
|
|
||||||
|
|
||||||
|
def test_detached_materialization_default_is_owned_and_deduplicated() -> None:
|
||||||
|
shared_a = Pattern()
|
||||||
|
shared_b = Pattern()
|
||||||
|
lazy = LazyLibrary()
|
||||||
|
lazy['a'] = lambda: shared_a
|
||||||
|
lazy['b'] = lambda: shared_b
|
||||||
|
|
||||||
|
detached = lazy.materialize_many_detached(('b', 'a', 'b'))
|
||||||
|
|
||||||
|
assert tuple(detached) == ('b', 'a')
|
||||||
|
assert detached['a'] is not shared_a
|
||||||
|
assert detached['b'] is not shared_b
|
||||||
|
detached['a'].polygon('L', vertices=[[0, 0], [1, 0], [0, 1]])
|
||||||
|
assert not shared_a.shapes
|
||||||
|
assert not lazy.cache
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_detached_views_copy_plain_source_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
source_pattern = Pattern()
|
||||||
|
source_pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
||||||
|
source = Library({'top': source_pattern})
|
||||||
|
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
|
||||||
|
mapped = LayerMappedView(ports, lambda _layer: 'B')
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(mapped)
|
||||||
|
|
||||||
|
original_deepcopy = Pattern.deepcopy
|
||||||
|
copied: list[Pattern] = []
|
||||||
|
|
||||||
|
def count_deepcopy(pattern: Pattern) -> Pattern:
|
||||||
|
copied.append(pattern)
|
||||||
|
return original_deepcopy(pattern)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
|
||||||
|
detached = overlay.materialize_detached('top')
|
||||||
|
|
||||||
|
assert copied == [source_pattern]
|
||||||
|
assert set(detached.shapes) == {'B'}
|
||||||
|
assert set(detached.ports) == {'P'}
|
||||||
|
assert not source_pattern.ports
|
||||||
|
assert not ports._cache
|
||||||
|
assert not mapped._cache
|
||||||
|
assert overlay.source_cell('top') == (mapped, 'top')
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_detached_views_copy_cached_pattern_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
source_pattern = Pattern()
|
||||||
|
source = Library({'top': source_pattern})
|
||||||
|
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
|
||||||
|
cached = ports['top']
|
||||||
|
mapped = LayerMappedView(ports, lambda layer: layer)
|
||||||
|
|
||||||
|
original_deepcopy = Pattern.deepcopy
|
||||||
|
copied: list[Pattern] = []
|
||||||
|
|
||||||
|
def count_deepcopy(pattern: Pattern) -> Pattern:
|
||||||
|
copied.append(pattern)
|
||||||
|
return original_deepcopy(pattern)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
|
||||||
|
detached = mapped.materialize_detached('top')
|
||||||
|
|
||||||
|
assert copied == [cached]
|
||||||
|
assert detached is not cached
|
||||||
|
detached.ports.clear()
|
||||||
|
assert set(cached.ports) == {'P'}
|
||||||
|
|
||||||
|
|
||||||
def test_borrowed_source_cell_tracks_persistent_materialization() -> None:
|
def test_borrowed_source_cell_tracks_persistent_materialization() -> None:
|
||||||
source = Library({"top": Pattern()})
|
source = Library({"top": Pattern()})
|
||||||
source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined]
|
source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined]
|
||||||
|
|
|
||||||
1
tools/__init__.py
Normal file
1
tools/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Repository development tools."""
|
||||||
626
tools/generate_gds_perf.py
Normal file
626
tools/generate_gds_perf.py
Normal file
|
|
@ -0,0 +1,626 @@
|
||||||
|
"""
|
||||||
|
Synthetic GDS fixture generation for reader/writer performance testing.
|
||||||
|
|
||||||
|
The presets here are intentionally hierarchical and deterministic. They aim to
|
||||||
|
approximate a pair of real-world layout families discussed during GDS reader and
|
||||||
|
writer work:
|
||||||
|
|
||||||
|
* `many_cells`: tens of thousands of cells, moderate reference count, very heavy
|
||||||
|
box usage after flattening, and moderate polygon density.
|
||||||
|
* `many_instances`: a much smaller cell library with very high reference count,
|
||||||
|
similar box density, and far fewer polygons.
|
||||||
|
|
||||||
|
Fixtures are written by streaming structures through `klamath` directly so large
|
||||||
|
benchmark files can be produced without first materializing an equally large
|
||||||
|
`masque.Library` in Python.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import klamath
|
||||||
|
from klamath import elements
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_PROPERTIES: dict[int, bytes] = {}
|
||||||
|
METERS_PER_DB_UNIT = 1e-9
|
||||||
|
USER_UNITS_PER_DB_UNIT = 1e-3
|
||||||
|
TOTAL_LAYERS = 200
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FixturePreset:
|
||||||
|
name: str
|
||||||
|
total_layers: int
|
||||||
|
box_layers: int
|
||||||
|
heavy_box_layers: int
|
||||||
|
polygon_layers: int
|
||||||
|
box_cells: int
|
||||||
|
poly_cells: int
|
||||||
|
box_wrappers: int
|
||||||
|
poly_wrappers: int
|
||||||
|
box_clusters: int
|
||||||
|
poly_clusters: int
|
||||||
|
box_cluster_refs: int
|
||||||
|
poly_cluster_refs: int
|
||||||
|
top_direct_box_refs: int
|
||||||
|
top_direct_poly_refs: int
|
||||||
|
heavy_boxes_per_cell: int
|
||||||
|
regular_boxes_per_cell: int
|
||||||
|
polygons_per_cell: int
|
||||||
|
path_stride: int
|
||||||
|
text_stride: int
|
||||||
|
box_cluster_array: tuple[int, int]
|
||||||
|
top_box_array: tuple[int, int]
|
||||||
|
poly_cluster_array: tuple[int, int]
|
||||||
|
top_poly_array: tuple[int, int]
|
||||||
|
rare_annotation_stride: int
|
||||||
|
|
||||||
|
|
||||||
|
PRESETS: dict[str, FixturePreset] = {
|
||||||
|
'many_cells': FixturePreset(
|
||||||
|
name='many_cells',
|
||||||
|
total_layers=TOTAL_LAYERS,
|
||||||
|
box_layers=20,
|
||||||
|
heavy_box_layers=3,
|
||||||
|
polygon_layers=20,
|
||||||
|
box_cells=17_000,
|
||||||
|
poly_cells=6_000,
|
||||||
|
box_wrappers=18_000,
|
||||||
|
poly_wrappers=6_000,
|
||||||
|
box_clusters=2_000,
|
||||||
|
poly_clusters=999,
|
||||||
|
box_cluster_refs=24,
|
||||||
|
poly_cluster_refs=16,
|
||||||
|
top_direct_box_refs=21_000,
|
||||||
|
top_direct_poly_refs=7_000,
|
||||||
|
heavy_boxes_per_cell=6,
|
||||||
|
regular_boxes_per_cell=2,
|
||||||
|
polygons_per_cell=50,
|
||||||
|
path_stride=2,
|
||||||
|
text_stride=3,
|
||||||
|
box_cluster_array=(24, 16),
|
||||||
|
top_box_array=(8, 8),
|
||||||
|
poly_cluster_array=(4, 2),
|
||||||
|
top_poly_array=(3, 2),
|
||||||
|
rare_annotation_stride=1_250,
|
||||||
|
),
|
||||||
|
'many_instances': FixturePreset(
|
||||||
|
name='many_instances',
|
||||||
|
total_layers=TOTAL_LAYERS,
|
||||||
|
box_layers=25,
|
||||||
|
heavy_box_layers=3,
|
||||||
|
polygon_layers=10,
|
||||||
|
box_cells=2_500,
|
||||||
|
poly_cells=500,
|
||||||
|
box_wrappers=1_000,
|
||||||
|
poly_wrappers=500,
|
||||||
|
box_clusters=1_000,
|
||||||
|
poly_clusters=499,
|
||||||
|
box_cluster_refs=1_200,
|
||||||
|
poly_cluster_refs=400,
|
||||||
|
top_direct_box_refs=102_001,
|
||||||
|
top_direct_poly_refs=0,
|
||||||
|
heavy_boxes_per_cell=40,
|
||||||
|
regular_boxes_per_cell=16,
|
||||||
|
polygons_per_cell=60,
|
||||||
|
path_stride=1,
|
||||||
|
text_stride=2,
|
||||||
|
box_cluster_array=(1, 1),
|
||||||
|
top_box_array=(1, 1),
|
||||||
|
poly_cluster_array=(1, 1),
|
||||||
|
top_poly_array=(1, 1),
|
||||||
|
rare_annotation_stride=250,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FixtureManifest:
|
||||||
|
preset: str
|
||||||
|
scale: float
|
||||||
|
gds_path: str
|
||||||
|
library_name: str
|
||||||
|
cells: int
|
||||||
|
refs: int
|
||||||
|
layers: int
|
||||||
|
box_layers: int
|
||||||
|
heavy_box_layers: list[list[int]]
|
||||||
|
polygon_layers: list[list[int]]
|
||||||
|
hierarchical_boxes_per_heavy_layer: int
|
||||||
|
hierarchical_boxes_per_regular_layer: int
|
||||||
|
hierarchical_polygons_total: int
|
||||||
|
hierarchical_paths_total: int
|
||||||
|
hierarchical_texts_total: int
|
||||||
|
flattened_box_placements: int
|
||||||
|
flattened_poly_placements: int
|
||||||
|
estimated_flat_boxes_per_heavy_layer: int
|
||||||
|
estimated_flat_polygons_per_active_polygon_layer: int
|
||||||
|
|
||||||
|
|
||||||
|
def _scaled_count(value: int, scale: float, minimum: int = 0) -> int:
|
||||||
|
if value == 0:
|
||||||
|
return 0
|
||||||
|
scaled = int(math.ceil(value * scale))
|
||||||
|
return max(minimum, scaled)
|
||||||
|
|
||||||
|
|
||||||
|
def _scaled_preset(preset: FixturePreset, scale: float) -> FixturePreset:
|
||||||
|
if scale <= 0:
|
||||||
|
raise ValueError(f'scale must be positive, got {scale!r}')
|
||||||
|
|
||||||
|
return FixturePreset(
|
||||||
|
name=preset.name,
|
||||||
|
total_layers=preset.total_layers,
|
||||||
|
box_layers=min(preset.box_layers, preset.total_layers),
|
||||||
|
heavy_box_layers=min(preset.heavy_box_layers, preset.box_layers),
|
||||||
|
polygon_layers=min(preset.polygon_layers, preset.total_layers),
|
||||||
|
box_cells=_scaled_count(preset.box_cells, scale, minimum=1),
|
||||||
|
poly_cells=_scaled_count(preset.poly_cells, scale, minimum=1),
|
||||||
|
box_wrappers=_scaled_count(preset.box_wrappers, scale),
|
||||||
|
poly_wrappers=_scaled_count(preset.poly_wrappers, scale),
|
||||||
|
box_clusters=_scaled_count(preset.box_clusters, scale, minimum=1),
|
||||||
|
poly_clusters=_scaled_count(preset.poly_clusters, scale, minimum=1),
|
||||||
|
box_cluster_refs=_scaled_count(preset.box_cluster_refs, scale, minimum=1),
|
||||||
|
poly_cluster_refs=_scaled_count(preset.poly_cluster_refs, scale, minimum=1),
|
||||||
|
top_direct_box_refs=_scaled_count(preset.top_direct_box_refs, scale),
|
||||||
|
top_direct_poly_refs=_scaled_count(preset.top_direct_poly_refs, scale),
|
||||||
|
heavy_boxes_per_cell=max(1, preset.heavy_boxes_per_cell),
|
||||||
|
regular_boxes_per_cell=max(1, preset.regular_boxes_per_cell),
|
||||||
|
polygons_per_cell=max(1, preset.polygons_per_cell),
|
||||||
|
path_stride=max(1, preset.path_stride),
|
||||||
|
text_stride=max(1, preset.text_stride),
|
||||||
|
box_cluster_array=preset.box_cluster_array,
|
||||||
|
top_box_array=preset.top_box_array,
|
||||||
|
poly_cluster_array=preset.poly_cluster_array,
|
||||||
|
top_poly_array=preset.top_poly_array,
|
||||||
|
rare_annotation_stride=max(1, _scaled_count(preset.rare_annotation_stride, scale, minimum=1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rect_xy(xmin: int, ymin: int, xmax: int, ymax: int) -> numpy.ndarray[Any, numpy.dtype[numpy.int32]]:
|
||||||
|
return numpy.array(
|
||||||
|
[[xmin, ymin], [xmin, ymax], [xmax, ymax], [xmax, ymin], [xmin, ymin]],
|
||||||
|
dtype=numpy.int32,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _poly_xy(points: list[tuple[int, int]]) -> numpy.ndarray[Any, numpy.dtype[numpy.int32]]:
|
||||||
|
closed = points + [points[0]]
|
||||||
|
return numpy.array(closed, dtype=numpy.int32)
|
||||||
|
|
||||||
|
|
||||||
|
def _sref(
|
||||||
|
target: str,
|
||||||
|
xy: tuple[int, int],
|
||||||
|
properties: dict[int, bytes] | None = None,
|
||||||
|
) -> elements.Reference:
|
||||||
|
return klamath.library.Reference(
|
||||||
|
struct_name=target.encode('ASCII'),
|
||||||
|
invert_y=False,
|
||||||
|
mag=1.0,
|
||||||
|
angle_deg=0.0,
|
||||||
|
xy=numpy.array([xy], dtype=numpy.int32),
|
||||||
|
colrow=None,
|
||||||
|
properties=EMPTY_PROPERTIES if properties is None else properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _aref(
|
||||||
|
target: str,
|
||||||
|
origin: tuple[int, int],
|
||||||
|
counts: tuple[int, int],
|
||||||
|
step: tuple[int, int],
|
||||||
|
properties: dict[int, bytes] | None = None,
|
||||||
|
) -> elements.Reference:
|
||||||
|
cols, rows = counts
|
||||||
|
dx, dy = step
|
||||||
|
xy = numpy.array(
|
||||||
|
[
|
||||||
|
origin,
|
||||||
|
(origin[0] + cols * dx, origin[1]),
|
||||||
|
(origin[0], origin[1] + rows * dy),
|
||||||
|
],
|
||||||
|
dtype=numpy.int32,
|
||||||
|
)
|
||||||
|
return klamath.library.Reference(
|
||||||
|
struct_name=target.encode('ASCII'),
|
||||||
|
invert_y=False,
|
||||||
|
mag=1.0,
|
||||||
|
angle_deg=0.0,
|
||||||
|
xy=xy,
|
||||||
|
colrow=(cols, rows),
|
||||||
|
properties=EMPTY_PROPERTIES if properties is None else properties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _annotation(index: int) -> dict[int, bytes]:
|
||||||
|
return {1: f'perf-{index}'.encode('ASCII')}
|
||||||
|
|
||||||
|
|
||||||
|
def _make_box_cell(index: int, cfg: FixturePreset) -> list[elements.Element]:
|
||||||
|
cell_elements: list[elements.Element] = []
|
||||||
|
xbase = (index % 17) * 600
|
||||||
|
ybase = (index // 17) * 180
|
||||||
|
|
||||||
|
for layer in range(cfg.heavy_box_layers):
|
||||||
|
for box_idx in range(cfg.heavy_boxes_per_cell):
|
||||||
|
x0 = xbase + box_idx * 22
|
||||||
|
y0 = ybase + layer * 40
|
||||||
|
width = 10 + ((index + box_idx + layer) % 7) * 6
|
||||||
|
height = 10 + ((index * 3 + box_idx + layer) % 5) * 8
|
||||||
|
properties = _annotation(index) if index % cfg.rare_annotation_stride == 0 and box_idx == 0 and layer == 0 else EMPTY_PROPERTIES
|
||||||
|
cell_elements.append(elements.Boundary(
|
||||||
|
layer=(layer, 0),
|
||||||
|
xy=_rect_xy(x0, y0, x0 + width, y0 + height),
|
||||||
|
properties=properties,
|
||||||
|
))
|
||||||
|
|
||||||
|
for layer in range(cfg.heavy_box_layers, cfg.box_layers):
|
||||||
|
for box_idx in range(cfg.regular_boxes_per_cell):
|
||||||
|
x0 = xbase + box_idx * 38
|
||||||
|
y0 = ybase + (layer - cfg.heavy_box_layers) * 28 + 400
|
||||||
|
width = 18 + ((index + layer + box_idx) % 9) * 4
|
||||||
|
height = 12 + ((index + 2 * layer + box_idx) % 6) * 5
|
||||||
|
cell_elements.append(elements.Boundary(
|
||||||
|
layer=(layer, 0),
|
||||||
|
xy=_rect_xy(x0, y0, x0 + width, y0 + height),
|
||||||
|
properties=EMPTY_PROPERTIES,
|
||||||
|
))
|
||||||
|
|
||||||
|
return cell_elements
|
||||||
|
|
||||||
|
|
||||||
|
def _make_poly_cell(index: int, cfg: FixturePreset) -> list[elements.Element]:
|
||||||
|
cell_elements: list[elements.Element] = []
|
||||||
|
xbase = (index % 19) * 900
|
||||||
|
ybase = (index // 19) * 260
|
||||||
|
|
||||||
|
for poly_idx in range(cfg.polygons_per_cell):
|
||||||
|
layer = poly_idx % cfg.polygon_layers
|
||||||
|
dx = xbase + (poly_idx % 5) * 120
|
||||||
|
dy = ybase + (poly_idx // 5) * 80
|
||||||
|
size = 18 + ((index + poly_idx + layer) % 11) * 7
|
||||||
|
points = [
|
||||||
|
(dx, dy),
|
||||||
|
(dx + size, dy + size // 5),
|
||||||
|
(dx + size + size // 3, dy + size),
|
||||||
|
(dx + size // 2, dy + size + size // 2),
|
||||||
|
(dx - size // 4, dy + size // 2),
|
||||||
|
]
|
||||||
|
properties = _annotation(index) if poly_idx == 0 and index % cfg.rare_annotation_stride == 0 else EMPTY_PROPERTIES
|
||||||
|
cell_elements.append(elements.Boundary(
|
||||||
|
layer=(layer, 0),
|
||||||
|
xy=_poly_xy(points),
|
||||||
|
properties=properties,
|
||||||
|
))
|
||||||
|
|
||||||
|
if index % cfg.path_stride == 0:
|
||||||
|
layer = index % cfg.polygon_layers
|
||||||
|
cell_elements.append(elements.Path(
|
||||||
|
layer=(layer, 1),
|
||||||
|
path_type=2,
|
||||||
|
width=12 + (index % 5) * 4,
|
||||||
|
extension=(0, 0),
|
||||||
|
xy=numpy.array(
|
||||||
|
[
|
||||||
|
[xbase, ybase + 900],
|
||||||
|
[xbase + 240, ybase + 930],
|
||||||
|
[xbase + 420, ybase + 960],
|
||||||
|
],
|
||||||
|
dtype=numpy.int32,
|
||||||
|
),
|
||||||
|
properties=EMPTY_PROPERTIES,
|
||||||
|
))
|
||||||
|
|
||||||
|
if index % cfg.text_stride == 0:
|
||||||
|
layer = index % cfg.polygon_layers
|
||||||
|
properties = _annotation(index) if index % cfg.rare_annotation_stride == 0 else EMPTY_PROPERTIES
|
||||||
|
cell_elements.append(elements.Text(
|
||||||
|
layer=(layer, 2),
|
||||||
|
presentation=0,
|
||||||
|
path_type=0,
|
||||||
|
width=0,
|
||||||
|
invert_y=False,
|
||||||
|
mag=1.0,
|
||||||
|
angle_deg=0.0,
|
||||||
|
xy=numpy.array([[xbase + 64, ybase + 1536]], dtype=numpy.int32),
|
||||||
|
string=f'T{index:05d}'.encode('ASCII'),
|
||||||
|
properties=properties,
|
||||||
|
))
|
||||||
|
|
||||||
|
return cell_elements
|
||||||
|
|
||||||
|
|
||||||
|
def _write_struct(stream: Any, name: str, cell_elements: list[elements.Element]) -> None:
|
||||||
|
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=cell_elements)
|
||||||
|
|
||||||
|
|
||||||
|
def _box_name(index: int) -> str:
|
||||||
|
return f'box_{index:05d}'
|
||||||
|
|
||||||
|
|
||||||
|
def _poly_name(index: int) -> str:
|
||||||
|
return f'poly_{index:05d}'
|
||||||
|
|
||||||
|
|
||||||
|
def _box_wrapper_name(index: int) -> str:
|
||||||
|
return f'box_wrap_{index:05d}'
|
||||||
|
|
||||||
|
|
||||||
|
def _poly_wrapper_name(index: int) -> str:
|
||||||
|
return f'poly_wrap_{index:05d}'
|
||||||
|
|
||||||
|
|
||||||
|
def _box_cluster_name(index: int) -> str:
|
||||||
|
return f'box_cluster_{index:05d}'
|
||||||
|
|
||||||
|
|
||||||
|
def _poly_cluster_name(index: int) -> str:
|
||||||
|
return f'poly_cluster_{index:05d}'
|
||||||
|
|
||||||
|
|
||||||
|
def _write_box_cells(stream: Any, cfg: FixturePreset) -> None:
|
||||||
|
for idx in range(cfg.box_cells):
|
||||||
|
_write_struct(stream, _box_name(idx), _make_box_cell(idx, cfg))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_poly_cells(stream: Any, cfg: FixturePreset) -> None:
|
||||||
|
for idx in range(cfg.poly_cells):
|
||||||
|
_write_struct(stream, _poly_name(idx), _make_poly_cell(idx, cfg))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_wrappers(stream: Any, cfg: FixturePreset) -> None:
|
||||||
|
for idx in range(cfg.box_wrappers):
|
||||||
|
target = _box_name(idx % cfg.box_cells)
|
||||||
|
origin = ((idx % 97) * 2_000, (idx // 97) * 2_000)
|
||||||
|
_write_struct(stream, _box_wrapper_name(idx), [_sref(target, origin)])
|
||||||
|
|
||||||
|
for idx in range(cfg.poly_wrappers):
|
||||||
|
target = _poly_name(idx % cfg.poly_cells)
|
||||||
|
origin = ((idx % 61) * 3_200, (idx // 61) * 3_200)
|
||||||
|
_write_struct(stream, _poly_wrapper_name(idx), [_sref(target, origin)])
|
||||||
|
|
||||||
|
|
||||||
|
def _write_box_clusters(stream: Any, cfg: FixturePreset) -> None:
|
||||||
|
array_refs = min(cfg.box_cluster_refs, max(1, (3 * cfg.box_cluster_refs) // 4))
|
||||||
|
for idx in range(cfg.box_clusters):
|
||||||
|
cell_elements: list[elements.Element] = []
|
||||||
|
for ref_idx in range(cfg.box_cluster_refs):
|
||||||
|
target = _box_name((idx * cfg.box_cluster_refs + ref_idx) % cfg.box_cells)
|
||||||
|
origin = (
|
||||||
|
(ref_idx % 6) * 48_000,
|
||||||
|
(ref_idx // 6) * 48_000,
|
||||||
|
)
|
||||||
|
if ref_idx < array_refs:
|
||||||
|
cell_elements.append(_aref(target, origin, cfg.box_cluster_array, (720, 900)))
|
||||||
|
else:
|
||||||
|
cell_elements.append(_sref(target, origin))
|
||||||
|
_write_struct(stream, _box_cluster_name(idx), cell_elements)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_poly_clusters(stream: Any, cfg: FixturePreset) -> None:
|
||||||
|
array_refs = min(cfg.poly_cluster_refs, cfg.poly_cluster_refs // 2)
|
||||||
|
for idx in range(cfg.poly_clusters):
|
||||||
|
cell_elements: list[elements.Element] = []
|
||||||
|
for ref_idx in range(cfg.poly_cluster_refs):
|
||||||
|
target = _poly_name((idx * cfg.poly_cluster_refs + ref_idx) % cfg.poly_cells)
|
||||||
|
origin = (
|
||||||
|
(ref_idx % 10) * 96_000,
|
||||||
|
(ref_idx // 10) * 96_000,
|
||||||
|
)
|
||||||
|
if ref_idx < array_refs:
|
||||||
|
cell_elements.append(_aref(target, origin, cfg.poly_cluster_array, (12_000, 8_500)))
|
||||||
|
else:
|
||||||
|
cell_elements.append(_sref(target, origin))
|
||||||
|
_write_struct(stream, _poly_cluster_name(idx), cell_elements)
|
||||||
|
|
||||||
|
|
||||||
|
def _top_box_refs(cfg: FixturePreset) -> list[elements.Reference]:
|
||||||
|
refs: list[elements.Reference] = []
|
||||||
|
|
||||||
|
for idx in range(cfg.box_wrappers):
|
||||||
|
refs.append(_sref(
|
||||||
|
_box_wrapper_name(idx),
|
||||||
|
((idx % 240) * 240_000, (idx // 240) * 240_000),
|
||||||
|
))
|
||||||
|
|
||||||
|
for idx in range(cfg.box_clusters):
|
||||||
|
refs.append(_sref(
|
||||||
|
_box_cluster_name(idx),
|
||||||
|
((idx % 100) * 800_000, (idx // 100) * 800_000 + 14_000_000),
|
||||||
|
))
|
||||||
|
|
||||||
|
for idx in range(cfg.top_direct_box_refs):
|
||||||
|
target = _box_name(idx % cfg.box_cells)
|
||||||
|
origin = (
|
||||||
|
(idx % 150) * 160_000,
|
||||||
|
(idx // 150) * 160_000 + 26_000_000,
|
||||||
|
)
|
||||||
|
if cfg.top_box_array == (1, 1):
|
||||||
|
refs.append(_sref(target, origin))
|
||||||
|
else:
|
||||||
|
refs.append(_aref(target, origin, cfg.top_box_array, (1_100, 1_350)))
|
||||||
|
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def _top_poly_refs(cfg: FixturePreset) -> list[elements.Reference]:
|
||||||
|
refs: list[elements.Reference] = []
|
||||||
|
|
||||||
|
for idx in range(cfg.poly_wrappers):
|
||||||
|
refs.append(_sref(
|
||||||
|
_poly_wrapper_name(idx),
|
||||||
|
((idx % 180) * 360_000, (idx // 180) * 360_000 + 44_000_000),
|
||||||
|
))
|
||||||
|
|
||||||
|
for idx in range(cfg.poly_clusters):
|
||||||
|
refs.append(_sref(
|
||||||
|
_poly_cluster_name(idx),
|
||||||
|
((idx % 70) * 1_100_000, (idx // 70) * 1_100_000 + 58_000_000),
|
||||||
|
))
|
||||||
|
|
||||||
|
for idx in range(cfg.top_direct_poly_refs):
|
||||||
|
target = _poly_name(idx % cfg.poly_cells)
|
||||||
|
origin = (
|
||||||
|
(idx % 110) * 420_000,
|
||||||
|
(idx // 110) * 420_000 + 72_000_000,
|
||||||
|
)
|
||||||
|
if cfg.top_poly_array == (1, 1):
|
||||||
|
refs.append(_sref(target, origin))
|
||||||
|
else:
|
||||||
|
refs.append(_aref(target, origin, cfg.top_poly_array, (16_000, 14_000)))
|
||||||
|
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def _write_top(stream: Any, cfg: FixturePreset) -> None:
|
||||||
|
cell_elements: list[elements.Element] = []
|
||||||
|
cell_elements.extend(_top_box_refs(cfg))
|
||||||
|
cell_elements.extend(_top_poly_refs(cfg))
|
||||||
|
_write_struct(stream, 'TOP', cell_elements)
|
||||||
|
|
||||||
|
|
||||||
|
def fixture_manifest(path: str | Path, preset: str, scale: float = 1.0) -> FixtureManifest:
|
||||||
|
base = PRESETS[preset]
|
||||||
|
cfg = _scaled_preset(base, scale)
|
||||||
|
|
||||||
|
box_cluster_array_refs = min(cfg.box_cluster_refs, max(1, (3 * cfg.box_cluster_refs) // 4))
|
||||||
|
box_cluster_array_mult = cfg.box_cluster_array[0] * cfg.box_cluster_array[1]
|
||||||
|
box_cluster_ref_instances = (
|
||||||
|
box_cluster_array_refs * box_cluster_array_mult
|
||||||
|
+ (cfg.box_cluster_refs - box_cluster_array_refs)
|
||||||
|
)
|
||||||
|
poly_cluster_array_refs = min(cfg.poly_cluster_refs, cfg.poly_cluster_refs // 2)
|
||||||
|
poly_cluster_array_mult = cfg.poly_cluster_array[0] * cfg.poly_cluster_array[1]
|
||||||
|
poly_cluster_ref_instances = (
|
||||||
|
poly_cluster_array_refs * poly_cluster_array_mult
|
||||||
|
+ (cfg.poly_cluster_refs - poly_cluster_array_refs)
|
||||||
|
)
|
||||||
|
|
||||||
|
flattened_box_placements = (
|
||||||
|
cfg.box_wrappers
|
||||||
|
+ cfg.box_clusters * box_cluster_ref_instances
|
||||||
|
+ cfg.top_direct_box_refs * cfg.top_box_array[0] * cfg.top_box_array[1]
|
||||||
|
)
|
||||||
|
flattened_poly_placements = (
|
||||||
|
cfg.poly_wrappers
|
||||||
|
+ cfg.poly_clusters * poly_cluster_ref_instances
|
||||||
|
+ cfg.top_direct_poly_refs * cfg.top_poly_array[0] * cfg.top_poly_array[1]
|
||||||
|
)
|
||||||
|
polygon_layers = max(1, cfg.polygon_layers)
|
||||||
|
polys_per_layer = (cfg.poly_cells * cfg.polygons_per_cell) // polygon_layers
|
||||||
|
|
||||||
|
return FixtureManifest(
|
||||||
|
preset=cfg.name,
|
||||||
|
scale=scale,
|
||||||
|
gds_path=str(Path(path)),
|
||||||
|
library_name=f'masque-perf-{cfg.name}',
|
||||||
|
cells=cfg.box_cells + cfg.poly_cells + cfg.box_wrappers + cfg.poly_wrappers + cfg.box_clusters + cfg.poly_clusters + 1,
|
||||||
|
refs=(
|
||||||
|
cfg.box_wrappers
|
||||||
|
+ cfg.poly_wrappers
|
||||||
|
+ cfg.box_clusters * cfg.box_cluster_refs
|
||||||
|
+ cfg.poly_clusters * cfg.poly_cluster_refs
|
||||||
|
+ cfg.box_wrappers + cfg.poly_wrappers + cfg.box_clusters + cfg.poly_clusters
|
||||||
|
+ cfg.top_direct_box_refs + cfg.top_direct_poly_refs
|
||||||
|
),
|
||||||
|
layers=cfg.total_layers,
|
||||||
|
box_layers=cfg.box_layers,
|
||||||
|
heavy_box_layers=[[layer, 0] for layer in range(cfg.heavy_box_layers)],
|
||||||
|
polygon_layers=[[layer, 0] for layer in range(cfg.polygon_layers)],
|
||||||
|
hierarchical_boxes_per_heavy_layer=cfg.box_cells * cfg.heavy_boxes_per_cell,
|
||||||
|
hierarchical_boxes_per_regular_layer=cfg.box_cells * cfg.regular_boxes_per_cell,
|
||||||
|
hierarchical_polygons_total=cfg.poly_cells * cfg.polygons_per_cell,
|
||||||
|
hierarchical_paths_total=(cfg.poly_cells - 1) // cfg.path_stride + 1,
|
||||||
|
hierarchical_texts_total=(cfg.poly_cells - 1) // cfg.text_stride + 1,
|
||||||
|
flattened_box_placements=flattened_box_placements,
|
||||||
|
flattened_poly_placements=flattened_poly_placements,
|
||||||
|
estimated_flat_boxes_per_heavy_layer=flattened_box_placements * cfg.heavy_boxes_per_cell,
|
||||||
|
estimated_flat_polygons_per_active_polygon_layer=flattened_poly_placements * polys_per_layer // cfg.poly_cells if cfg.poly_cells else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_fixture(
|
||||||
|
path: str | Path,
|
||||||
|
*,
|
||||||
|
preset: str,
|
||||||
|
scale: float = 1.0,
|
||||||
|
write_manifest: bool = True,
|
||||||
|
) -> FixtureManifest:
|
||||||
|
if preset not in PRESETS:
|
||||||
|
known = ', '.join(sorted(PRESETS))
|
||||||
|
raise KeyError(f'unknown preset {preset!r}; expected one of: {known}')
|
||||||
|
|
||||||
|
manifest = fixture_manifest(path, preset, scale)
|
||||||
|
cfg = _scaled_preset(PRESETS[preset], scale)
|
||||||
|
output = Path(path)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with output.open('wb') as stream:
|
||||||
|
header = klamath.library.FileHeader(
|
||||||
|
name=manifest.library_name.encode('ASCII'),
|
||||||
|
user_units_per_db_unit=USER_UNITS_PER_DB_UNIT,
|
||||||
|
meters_per_db_unit=METERS_PER_DB_UNIT,
|
||||||
|
)
|
||||||
|
header.write(stream)
|
||||||
|
_write_box_cells(stream, cfg)
|
||||||
|
_write_poly_cells(stream, cfg)
|
||||||
|
_write_wrappers(stream, cfg)
|
||||||
|
_write_box_clusters(stream, cfg)
|
||||||
|
_write_poly_clusters(stream, cfg)
|
||||||
|
_write_top(stream, cfg)
|
||||||
|
klamath.records.ENDLIB.write(stream, None)
|
||||||
|
|
||||||
|
if write_manifest:
|
||||||
|
manifest_path = output.with_suffix(output.suffix + '.json')
|
||||||
|
manifest_path.write_text(json.dumps(asdict(manifest), indent=2, sort_keys=True) + '\n')
|
||||||
|
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def build_arg_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description='Generate synthetic GDS fixtures for GDS reader/writer performance work.')
|
||||||
|
parser.add_argument(
|
||||||
|
'preset',
|
||||||
|
nargs='?',
|
||||||
|
default='many_cells',
|
||||||
|
choices=sorted(PRESETS),
|
||||||
|
help='Fixture family to generate.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'output',
|
||||||
|
nargs='?',
|
||||||
|
help='Output .gds path. Defaults to build/gds_perf/<preset>.gds',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--scale',
|
||||||
|
type=float,
|
||||||
|
default=1.0,
|
||||||
|
help='Scale the preset counts down or up while keeping the same shape mix. Default: 1.0',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-manifest',
|
||||||
|
action='store_true',
|
||||||
|
help='Do not write the sidecar JSON manifest.',
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = build_arg_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
output = Path(args.output) if args.output is not None else Path('build/gds_perf') / f'{args.preset}.gds'
|
||||||
|
manifest = write_fixture(output, preset=args.preset, scale=args.scale, write_manifest=not args.no_manifest)
|
||||||
|
print(json.dumps(asdict(manifest), indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue