[library] More library rework
This commit is contained in:
parent
67afee7704
commit
0edd735a11
25 changed files with 2357 additions and 870 deletions
334
MIGRATION.md
334
MIGRATION.md
|
|
@ -1,7 +1,8 @@
|
||||||
# Migration Guide
|
# Migration Guide
|
||||||
|
|
||||||
This guide covers changes between the git tag `release` and the current tree.
|
This guide covers changes between the `master` branch and the current tree.
|
||||||
At `release`, `masque.__version__` was `3.3`; the current tree reports `3.4`.
|
Both `master` and the current tree report `masque.__version__ == '3.4'`; the
|
||||||
|
version string has not yet been bumped for these changes.
|
||||||
|
|
||||||
Most downstream changes are in `masque/builder/*`, but there are a few other
|
Most downstream changes are in `masque/builder/*`, but there are a few other
|
||||||
API changes that may require code updates.
|
API changes that may require code updates.
|
||||||
|
|
@ -126,28 +127,40 @@ builder = Pather(...)
|
||||||
deferred = Pather(..., render='deferred')
|
deferred = Pather(..., render='deferred')
|
||||||
```
|
```
|
||||||
|
|
||||||
Top-level imports from `masque` also continue to work.
|
The new `Pather` remains importable from both `masque` and `masque.builder`.
|
||||||
|
The removed `Builder` and `RenderPather` names are no longer exported from
|
||||||
|
either location.
|
||||||
|
|
||||||
`Pather` now defaults to `render='auto'`, so plain construction replaces the
|
`Pather` now defaults to `render='auto'`, so plain construction replaces the
|
||||||
old `Builder` behavior. Use `Pather(..., render='deferred')` where you
|
old `Builder` behavior. Use `Pather(..., render='deferred')` where you
|
||||||
previously used `RenderPather`.
|
previously used `RenderPather`.
|
||||||
|
|
||||||
## `BasicTool` was replaced
|
## `SimpleTool` was removed and `AutoTool` registration changed
|
||||||
|
|
||||||
`BasicTool` is no longer exported. Use `AutoTool` for reusable straight, bend,
|
`SimpleTool` is no longer exported. `AutoTool` remains, but its old public
|
||||||
transition, and S-bend primitives.
|
descriptor classes and constructor-oriented configuration were replaced by
|
||||||
|
registration methods. Use it for generated straights and S-bends and reusable
|
||||||
|
bends, U-turns, and transitions.
|
||||||
|
|
||||||
### Old `BasicTool`
|
### Old `AutoTool`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from masque.builder.tools import BasicTool
|
from masque.builder import AutoTool
|
||||||
|
|
||||||
tool = BasicTool(
|
tool = AutoTool(
|
||||||
straight=(make_straight, 'input', 'output'),
|
straights=[
|
||||||
bend=(lib.abstract('bend'), 'input', 'output'),
|
AutoTool.Straight('m1wire', make_straight, 'input', 'output'),
|
||||||
|
],
|
||||||
|
bends=[
|
||||||
|
AutoTool.Bend(lib.abstract('bend'), 'input', 'output'),
|
||||||
|
],
|
||||||
|
sbends=[],
|
||||||
transitions={
|
transitions={
|
||||||
'm2wire': (lib.abstract('via'), 'top', 'bottom'),
|
('m2wire', 'm1wire'): AutoTool.Transition(
|
||||||
|
lib.abstract('via'), 'top', 'bottom'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
default_out_ptype='m1wire',
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -166,12 +179,44 @@ tool = (
|
||||||
|
|
||||||
The key differences are:
|
The key differences are:
|
||||||
|
|
||||||
- `BasicTool` -> `AutoTool`
|
- `SimpleTool` was removed; use `AutoTool` or implement the new `Tool`
|
||||||
- `straight=(fn, in_name, out_name)` -> `add_straight(fn, ptype, in_name)`
|
primitive-offer interface
|
||||||
- `bend=(abstract, in_name, out_name)` -> `add_bend(abstract, in_name, out_name)`
|
- `AutoTool.Straight(...)` -> `add_straight(fn, ptype, in_name)`
|
||||||
|
- `AutoTool.Bend(...)` -> `add_bend(abstract, in_name, out_name)`
|
||||||
|
- `AutoTool.SBend(...)` -> `add_sbend(fn, ptype, in_name, out_name)`
|
||||||
|
- reusable native U-turns can be registered with `add_uturn(...)`
|
||||||
- transitions are registered with `add_transition(abstract, external_port, internal_port)`
|
- transitions are registered with `add_transition(abstract, external_port, internal_port)`
|
||||||
- transitions are bidirectional by default; pass `one_way=True` to inhibit the reverse adapter
|
- transitions are bidirectional by default; pass `one_way=True` to inhibit the reverse adapter
|
||||||
|
|
||||||
|
Primitive costs are now explicit and independent of `AutoTool` registration
|
||||||
|
order. Every `add_*()` method accepts `cost=`, as do the concrete offer
|
||||||
|
factories. A numeric value scales the default geometric cost; for example,
|
||||||
|
`cost=2` makes a primitive twice as expensive. A callable receives the
|
||||||
|
canonical primitive parameter and local endpoint and returns the complete
|
||||||
|
cost:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tool.add_straight(make_straight, 'm1wire', 'input', cost=1.5)
|
||||||
|
tool.add_sbend(
|
||||||
|
make_sbend,
|
||||||
|
'm1wire',
|
||||||
|
'input',
|
||||||
|
'output',
|
||||||
|
cost=lambda jog, endpoint: abs(jog) + 2 * abs(endpoint.x),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`PrimitiveOffer.priority_bias` was removed; custom offers should use `cost`
|
||||||
|
instead. Exact equal-cost candidates still use deterministic discovery order
|
||||||
|
as the final tie-break, but registration order no longer changes their
|
||||||
|
reported cost.
|
||||||
|
|
||||||
|
For two-port primitives, `AutoTool` can infer omitted port names and, for
|
||||||
|
generated straight/S-bend primitives, omitted ptype metadata by sampling an
|
||||||
|
in-domain example. Supply those values explicitly when generation requires
|
||||||
|
route-specific keyword arguments, because metadata inference does not receive
|
||||||
|
`tool_options`.
|
||||||
|
|
||||||
## Custom `Tool` subclasses
|
## Custom `Tool` subclasses
|
||||||
|
|
||||||
If you maintain your own `Tool` subclass, the interface changed:
|
If you maintain your own `Tool` subclass, the interface changed:
|
||||||
|
|
@ -412,25 +457,268 @@ Check code that calls:
|
||||||
- `Ref.rotate(...)`
|
- `Ref.rotate(...)`
|
||||||
- `Ref.mirror(...)`
|
- `Ref.mirror(...)`
|
||||||
- `Label.rotate_around(...)` / `Label.mirror(...)`
|
- `Label.rotate_around(...)` / `Label.mirror(...)`
|
||||||
|
- `Abstract.mirror_port_offsets(...)` / `Abstract.mirror_ports(...)`
|
||||||
|
|
||||||
If that code expected offsets or repetition grids to move automatically, it
|
If that code expected offsets or repetition grids to move automatically, it
|
||||||
needs updating. For whole-pattern transforms, prefer calling `Pattern.mirror()`
|
needs updating. For whole-pattern transforms, prefer calling `Pattern.mirror()`
|
||||||
or `Pattern.rotate_around(...)` at the container level.
|
or `Pattern.rotate_around(...)` at the container level.
|
||||||
|
|
||||||
|
`Abstract.mirror_port_offsets()` and `Abstract.mirror_ports()` were removed.
|
||||||
|
Use `Abstract.mirror(axis)` to mirror both port locations and orientations. If
|
||||||
|
you intentionally need only one half of that operation, update the individual
|
||||||
|
ports explicitly with `Port.flip_across(...)` or `Port.mirror(...)`.
|
||||||
|
|
||||||
|
## Library hierarchy and graph behavior
|
||||||
|
|
||||||
|
`masque/library.py` was split into the `masque.library` package. Imports from
|
||||||
|
the public module remain stable:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from masque import Library, LazyLibrary
|
||||||
|
# or
|
||||||
|
from masque.library import Library, LazyLibrary
|
||||||
|
```
|
||||||
|
|
||||||
|
Code importing the implementation file itself must move to the public package;
|
||||||
|
do not depend on the new internal `base`, `mapping`, or `lazy` module paths.
|
||||||
|
|
||||||
|
Hierarchy helpers now handle dangling references explicitly. The following
|
||||||
|
methods accept `dangling='error' | 'ignore' | 'include'` and default to
|
||||||
|
`'error'`:
|
||||||
|
|
||||||
|
- `child_graph()`
|
||||||
|
- `parent_graph()`
|
||||||
|
- `child_order()`
|
||||||
|
- `find_refs_local()`
|
||||||
|
- `find_refs_global()`
|
||||||
|
- `prune_empty()`
|
||||||
|
|
||||||
|
On `master`, graph construction could expose missing targets implicitly or
|
||||||
|
fail later with a `KeyError`. If dangling refs are intentional, choose the
|
||||||
|
behavior explicitly, for example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
graph = library.child_graph(dangling='include')
|
||||||
|
order = library.child_order(dangling='ignore')
|
||||||
|
```
|
||||||
|
|
||||||
|
Graph cycles and invalid hierarchy states are now reported as `LibraryError`
|
||||||
|
with context. Audit code that caught `KeyError` or `graphlib.CycleError` from
|
||||||
|
these helpers.
|
||||||
|
|
||||||
|
Invalid `dangling=` strings now raise `ValueError`; they no longer fall through
|
||||||
|
to the `include` behavior. Empty lists stored in `Pattern.refs` are consistently
|
||||||
|
treated as absent references by hierarchy and geometry traversal. Code that
|
||||||
|
uses a `defaultdict` lookup such as `pattern.refs[name]` without appending a
|
||||||
|
`Ref` will therefore not create an edge or force that target to be loaded.
|
||||||
|
|
||||||
|
`Library.add()` now resolves the full name plan and remaps references before it
|
||||||
|
starts inserting cells. Name-resolution and preparation failures no longer
|
||||||
|
leave partially-added cells behind. A `rename_theirs` callback now receives an
|
||||||
|
`INameView` containing both existing names and names reserved earlier in the
|
||||||
|
same addition. Use membership, iteration, `len()`, or `get_name()`; the callback
|
||||||
|
argument is not the destination object and does not support pattern lookup or
|
||||||
|
mapping helpers such as `keys()` and `items()`. Update callback annotations from
|
||||||
|
`ILibraryView` to `INameView`. Custom `_merge()` implementations remain
|
||||||
|
responsible for their own rollback if they fail during the final commit.
|
||||||
|
|
||||||
|
Reference transforms returned by `find_refs_local()` and
|
||||||
|
`find_refs_global()` are now Nx5 arrays. The fifth column is the cumulative
|
||||||
|
scale, so rows have the form `(x, y, rotation, mirrored, scale)` rather than the
|
||||||
|
old Nx4 form.
|
||||||
|
|
||||||
|
`BuildReport` mapping fields are now defensively copied and read-only. Copy a
|
||||||
|
field to a new `dict` before adding or removing report entries.
|
||||||
|
|
||||||
|
`PortsLibraryView`, `OverlayLibrary`, and source-backed outputs returned by
|
||||||
|
`LibraryBuilder.build()` borrow their sources. They do not close source resources, and
|
||||||
|
`PortsLibraryView` is not a context manager. Keep each lazy source open and
|
||||||
|
unchanged until every borrowing view or overlay is finished, then close the
|
||||||
|
owning source explicitly. An eager `build(output='library')` result is detached
|
||||||
|
and does not need its sources afterward.
|
||||||
|
|
||||||
|
Read-only `subtree()` results are now borrowed lazy views rather than eager
|
||||||
|
`LibraryView` snapshots. Creating one no longer loads its reachable patterns,
|
||||||
|
and the view preserves source ordering, hierarchy metadata, and lazy GDS
|
||||||
|
copy-through. Mutable `Library`, `LazyLibrary`, and `OverlayLibrary` subtrees
|
||||||
|
return the same writable type as their source. Overlay subtrees retain their
|
||||||
|
source layers and lazy GDS capabilities. Mutable subtree containers are
|
||||||
|
structurally independent, but already-materialized patterns remain shared.
|
||||||
|
Keep borrowed sources open and structurally unchanged for the subtree's
|
||||||
|
lifetime.
|
||||||
|
|
||||||
|
`ILibrary` no longer inherits `collections.abc.MutableMapping`. It remains a
|
||||||
|
readable `Mapping` with explicit insert-only item assignment and deletion, but
|
||||||
|
generic mutation helpers such as `update()`, `setdefault()`, `pop()`,
|
||||||
|
`popitem()`, and `clear()` are no longer supplied. Use `add()`, `rename()`,
|
||||||
|
`delete()`, item insertion, and item deletion so library name and reference
|
||||||
|
invariants remain explicit.
|
||||||
|
|
||||||
|
Library-level `referenced_patterns()` and `dangling_refs()` now report only
|
||||||
|
named cell targets and return `set[str]`. Populated refs whose target is `None`
|
||||||
|
are ignored, matching `child_graph()` and recursive geometry behavior;
|
||||||
|
`Pattern.referenced_patterns()` continues to report `None` locally.
|
||||||
|
|
||||||
|
Port-importing views now always process detached patterns, including when the
|
||||||
|
raw source cell was already cached. Code can safely retain and compare raw and
|
||||||
|
processed views without port overrides leaking back into the raw pattern.
|
||||||
|
Once a processed cell is persistently materialized, lazy GDS writers no longer
|
||||||
|
copy the raw source structure for that cell, so later mutations to the returned
|
||||||
|
`Pattern` are serialized. Non-persistent materialization does not mark the cell
|
||||||
|
as changed.
|
||||||
|
|
||||||
|
Underscore-prefixed declarations work through the attribute authoring surface:
|
||||||
|
`builder.cells._helper = pattern` now declares `_helper`. Only the view's exact
|
||||||
|
internal `_library` attribute is reserved.
|
||||||
|
|
||||||
|
Recursive geometry operations now reject cyclic reference hierarchies with a
|
||||||
|
contextual `PatternError` instead of eventually leaking `RecursionError`. This
|
||||||
|
applies to bounds calculation, flattened layer polygon extraction, and
|
||||||
|
visualization as well as the existing flattening checks.
|
||||||
|
|
||||||
|
`LibraryBuilder.validate(names=...)` now accepts either one string or a sequence
|
||||||
|
of strings. Non-string roots raise `TypeError`, and duplicate roots are reduced
|
||||||
|
to their first occurrence. A recipe may build a different `LibraryBuilder`, but
|
||||||
|
calling `build()` or `validate()` recursively on its own active builder now
|
||||||
|
raises `BuildError` before starting another session.
|
||||||
|
|
||||||
|
`LibraryBuilder` is an authoring registry, not an `ILibraryView` or mapping.
|
||||||
|
Use membership, iteration, `keys()`, `get_name()`, assignment, and deletion to
|
||||||
|
manage declarations, then use the library returned by `build()` for reads and
|
||||||
|
hierarchy operations. Recipes that need an active library must receive the
|
||||||
|
builder-owned placeholder as a direct argument:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
|
return Pather(library=lib, ports='device').pattern
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
|
builder.cells.device = cell(factory)(hole_lib=builder.library)
|
||||||
|
```
|
||||||
|
|
||||||
|
The builder-owned `builder.library` placeholder is read-only. Only direct
|
||||||
|
positional and keyword values equal to it are substituted; placeholders nested
|
||||||
|
inside containers are not interpreted. A placeholder from another builder is
|
||||||
|
rejected when the recipe is assigned.
|
||||||
|
|
||||||
|
`IMaterializable` now identifies libraries which support explicit
|
||||||
|
`materialize()` and `materialize_many()` operations. `LibraryBuilder.add()`
|
||||||
|
borrows these marked inputs, while ordinary mappings and `ILibraryView`
|
||||||
|
instances are copied eagerly. Use `add_source()` to force borrowing of an
|
||||||
|
unmarked view. Wrapping a materializable library in `LibraryView` intentionally
|
||||||
|
erases the marker.
|
||||||
|
|
||||||
|
`IBorrowing` separately identifies composite views which retain direct source
|
||||||
|
libraries and expose them through `borrowed_sources()`. Keep those sources open
|
||||||
|
and unchanged for the lifetime of the borrowing view. Owner libraries such as
|
||||||
|
`LazyLibrary` and the lazy GDS readers are materializable but do not implement
|
||||||
|
`IBorrowing`. GDS raw-structure copy-through remains a separate,
|
||||||
|
format-specific capability.
|
||||||
|
|
||||||
|
`LibraryBuilder`, `OverlayLibrary`, and `PortsLibraryView` are new additive
|
||||||
|
library implementations. `LibraryBuilder` supports declarative `@cell` recipes
|
||||||
|
and dependency-aware builds; `OverlayLibrary` composes source libraries without
|
||||||
|
eagerly copying all patterns; `PortsLibraryView` overlays port metadata on a
|
||||||
|
read-only source.
|
||||||
|
|
||||||
|
Flattening with `flatten_ports=True` now rejects repeated refs whose target has
|
||||||
|
ports, because expanding them would create duplicate port names. Resolve the
|
||||||
|
repetition and assign unique port names before flattening, or use
|
||||||
|
`flatten_ports=False`.
|
||||||
|
|
||||||
|
## GDSII module and lazy-loading changes
|
||||||
|
|
||||||
|
`masque.file.gdsii` changed from a module into a package. The eager klamath
|
||||||
|
API remains available at the old import path, so ordinary `read`, `readfile`,
|
||||||
|
`write`, and `writefile` calls do not need to change:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from masque.file import gdsii
|
||||||
|
library, info = gdsii.readfile('layout.gds')
|
||||||
|
```
|
||||||
|
|
||||||
|
The old `gdsii.load_library()` and `gdsii.load_libraryfile()` entry points were
|
||||||
|
removed. Use the source-backed lazy reader instead:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# old
|
||||||
|
library, info = gdsii.load_libraryfile('layout.gds')
|
||||||
|
|
||||||
|
# new
|
||||||
|
from masque.file.gdsii import lazy
|
||||||
|
|
||||||
|
library, info = lazy.readfile('layout.gds')
|
||||||
|
try:
|
||||||
|
pattern = library['TOP']
|
||||||
|
finally:
|
||||||
|
library.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
`lazy.read(stream)` and `lazy.readfile(path, use_mmap=...)` return a read-only
|
||||||
|
`GdsLibrarySource`. It owns file resources when it opens them and also supports
|
||||||
|
the context-manager protocol. The old `full_load` and `postprocess` arguments
|
||||||
|
are gone; materialize/copy the desired cells and post-process them explicitly.
|
||||||
|
|
||||||
|
An optional Arrow/native backend is available through
|
||||||
|
`masque.file.gdsii.arrow` and `masque.file.gdsii.lazy_arrow`; install the new
|
||||||
|
`arrow` extra to use it. These modules are additive and are not a transparent
|
||||||
|
replacement unless their additional dependencies and native library are
|
||||||
|
available.
|
||||||
|
|
||||||
|
## Shape construction and geometry additions
|
||||||
|
|
||||||
|
The public `raw=True` constructor shortcut was removed from `Arc`, `Circle`,
|
||||||
|
`Ellipse`, `Path`, `Polygon`, `PolyCollection`, and `Text`. Call their normal
|
||||||
|
constructors without `raw`; `_from_raw()` is an internal fast path and is not a
|
||||||
|
compatibility API.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# old
|
||||||
|
polygon = Polygon(vertices, raw=True)
|
||||||
|
|
||||||
|
# new
|
||||||
|
polygon = Polygon(vertices)
|
||||||
|
```
|
||||||
|
|
||||||
|
`Arc` radii must now be strictly positive rather than merely non-negative.
|
||||||
|
`Arc.angle_ref` is additive and defaults to `Arc.AngleRef.Center`, preserving
|
||||||
|
the previous center-referenced angle interpretation.
|
||||||
|
|
||||||
|
`RectCollection` is a new shape for batches of axis-aligned rectangles and is
|
||||||
|
exported from both `masque` and `masque.shapes`. `Polygon.boolean()` and the
|
||||||
|
top-level `masque.boolean()` helper are also new; install the `boolean` extra
|
||||||
|
for their `pyclipper` dependency.
|
||||||
|
|
||||||
## Other user-facing changes
|
## Other user-facing changes
|
||||||
|
|
||||||
|
### File writers
|
||||||
|
|
||||||
|
SVG writing no longer polygonizes or flattens caller-owned patterns in place;
|
||||||
|
it works from detached copies. `svg.writefile(..., annotate_ports=True)` can
|
||||||
|
add port arrows. DXF writing now expands shape repetitions into individual DXF
|
||||||
|
entities, so callers no longer need to wrap repeated shapes solely for DXF
|
||||||
|
output.
|
||||||
|
|
||||||
### DXF environments
|
### DXF environments
|
||||||
|
|
||||||
If you install the DXF extra, the supported `ezdxf` baseline moved from
|
If you install the DXF extra, the supported `ezdxf` baseline moved from
|
||||||
`~=1.0.2` to `~=1.4`. Any pinned environments should be updated accordingly.
|
`~=1.0.2` to `~=1.4`. Any pinned environments should be updated accordingly.
|
||||||
|
|
||||||
|
### Optional dependency names
|
||||||
|
|
||||||
|
The misspelled `manhatanize_slow` extra was corrected to
|
||||||
|
`manhattanize_slow`. A separate `manhattanize` extra now installs the
|
||||||
|
scikit-image implementation. The `arrow` and `boolean` extras are also new.
|
||||||
|
|
||||||
### New exports
|
### New exports
|
||||||
|
|
||||||
These are additive, but available now from `masque` and `masque.builder`:
|
These are additive, but available now from `masque` and `masque.builder`:
|
||||||
|
|
||||||
- `PortPather`
|
- from `masque`: `RectCollection`, `boolean`, `OverlayLibrary`,
|
||||||
- `AutoTool`
|
`PortsLibraryView`, `IMaterializable`, `IBorrowing`, `LibraryBuilder`,
|
||||||
- `boolean`
|
`BuildReport`, `CellProvenance`, and `cell`
|
||||||
|
- from `masque.builder`: `CostCallable`, the concrete primitive-offer classes,
|
||||||
|
and structured route error/status types
|
||||||
|
|
||||||
## Minimal migration checklist
|
## Minimal migration checklist
|
||||||
|
|
||||||
|
|
@ -438,11 +726,15 @@ If your code uses the routing stack, do these first:
|
||||||
|
|
||||||
1. Replace `path`/`path_to`/`mpath`/`path_into` calls with
|
1. Replace `path`/`path_to`/`mpath`/`path_into` calls with
|
||||||
`trace`/`trace_to`/multi-port `trace`/`trace_into`.
|
`trace`/`trace_to`/multi-port `trace`/`trace_into`.
|
||||||
2. Replace `BasicTool` with `AutoTool`.
|
2. Replace `SimpleTool` and old `AutoTool` descriptor construction with the
|
||||||
|
new `AutoTool.add_*()` methods.
|
||||||
3. Fix imports that still reference `masque.builder.builder` or
|
3. Fix imports that still reference `masque.builder.builder` or
|
||||||
`masque.builder.renderpather`.
|
`masque.builder.renderpather`.
|
||||||
4. Audit any low-level `mirror()` usage, especially on `Port` and `Ref`.
|
4. Audit any low-level `mirror()` usage, especially on `Port` and `Ref`.
|
||||||
|
5. Move lazy GDS calls from `gdsii.load_library*()` to
|
||||||
|
`masque.file.gdsii.lazy`.
|
||||||
|
6. Remove `raw=True` from public shape constructors.
|
||||||
|
|
||||||
If your code only uses `Pattern`, `Library`, `place()`, and `plug()` without the
|
If your code only uses `Pattern`, `Library`, `place()`, and `plug()` without the
|
||||||
routing helpers, you may not need any changes beyond the transform audit and any
|
routing helpers, audit transforms, dangling-reference graph calls, raw shape
|
||||||
stale imports.
|
construction, and any stale imports.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ Contents
|
||||||
* Use `Pather` to snap ports together into a circuit
|
* Use `Pather` to snap ports together into a circuit
|
||||||
* Check for dangling references
|
* Check for dangling references
|
||||||
- [library](library.py)
|
- [library](library.py)
|
||||||
* Continue from `devices.py` by declaring a mixed library with `BuildLibrary`
|
* Continue from `devices.py` by declaring a mixed library with `LibraryBuilder`
|
||||||
* Import source-backed GDS cells and register python-generated recipes together
|
* Import source-backed GDS cells and register python-generated recipes together
|
||||||
* Call `build()` to produce a normal library and report 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()`
|
* Explore alternate ways of specifying a pattern for `.plug()` and `.place()`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Tutorial: authoring a mixed library with `BuildLibrary`.
|
Tutorial: authoring a mixed library with `LibraryBuilder`.
|
||||||
|
|
||||||
This example assumes you have already read `devices.py` and generated the
|
This example assumes you have already read `devices.py` and generated the
|
||||||
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
|
`circuit.gds` file it writes. The goal here is not the photonic-crystal geometry
|
||||||
|
|
@ -11,7 +11,7 @@ from typing import Any
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
|
||||||
from masque import BuildLibrary, Pather, Pattern, cell
|
from masque import ILibrary, LibraryBuilder, Pather, Pattern, cell
|
||||||
from masque.file.gdsii import writefile
|
from masque.file.gdsii import writefile
|
||||||
from masque.file.gdsii.lazy import readfile
|
from masque.file.gdsii.lazy import readfile
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ import devices
|
||||||
from basic_shapes import GDS_OPTS
|
from basic_shapes import GDS_OPTS
|
||||||
|
|
||||||
|
|
||||||
def make_mixed_waveguide(lib: BuildLibrary) -> Pattern:
|
def make_mixed_waveguide(lib: ILibrary) -> Pattern:
|
||||||
"""
|
"""
|
||||||
Recipe which assembles imported and generated cells behind the builder API.
|
Recipe which assembles imported and generated cells behind the builder API.
|
||||||
"""
|
"""
|
||||||
|
|
@ -42,7 +42,7 @@ def make_mixed_waveguide(lib: BuildLibrary) -> Pattern:
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
cells = builder.cells
|
cells = builder.cells
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
@ -72,8 +72,12 @@ def main() -> None:
|
||||||
cells.tri_wg28 = cell(devices.waveguide)(length=28, mirror_periods=5, **opts)
|
cells.tri_wg28 = cell(devices.waveguide)(length=28, mirror_periods=5, **opts)
|
||||||
cells.tri_bend0 = cell(devices.bend)(mirror_periods=5, **opts)
|
cells.tri_bend0 = cell(devices.bend)(mirror_periods=5, **opts)
|
||||||
cells.tri_ysplit = cell(devices.y_splitter)(mirror_periods=5, **opts)
|
cells.tri_ysplit = cell(devices.y_splitter)(mirror_periods=5, **opts)
|
||||||
cells.tri_l3cav = cell(devices.perturbed_l3)(xy_size=(4, 10), **opts, hole_lib=builder)
|
cells.tri_l3cav = cell(devices.perturbed_l3)(
|
||||||
cells.mixed_wg_cav = cell(make_mixed_waveguide)(builder)
|
xy_size=(4, 10),
|
||||||
|
**opts,
|
||||||
|
hole_lib=builder.library,
|
||||||
|
)
|
||||||
|
cells.mixed_wg_cav = cell(make_mixed_waveguide)(builder.library)
|
||||||
|
|
||||||
print('Declared cells waiting to be built:\n' + pformat(list(builder.keys())))
|
print('Declared cells waiting to be built:\n' + pformat(list(builder.keys())))
|
||||||
|
|
||||||
|
|
@ -110,6 +114,10 @@ def main() -> None:
|
||||||
print('Writing library to file...')
|
print('Writing library to file...')
|
||||||
writefile(built, 'library.gds', **GDS_OPTS)
|
writefile(built, 'library.gds', **GDS_OPTS)
|
||||||
|
|
||||||
|
# The default build output is an overlay which borrows its lazy sources.
|
||||||
|
# Close the owning GDS source only after the overlay is no longer needed.
|
||||||
|
gds_lib.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -59,13 +59,16 @@ from .pattern import (
|
||||||
from .utils.boolean import boolean as boolean
|
from .utils.boolean import boolean as boolean
|
||||||
|
|
||||||
from .library import (
|
from .library import (
|
||||||
|
INameView as INameView,
|
||||||
ILibraryView as ILibraryView,
|
ILibraryView as ILibraryView,
|
||||||
ILibrary as ILibrary,
|
ILibrary as ILibrary,
|
||||||
|
IBorrowing as IBorrowing,
|
||||||
|
IMaterializable as IMaterializable,
|
||||||
LibraryView as LibraryView,
|
LibraryView as LibraryView,
|
||||||
Library as Library,
|
Library as Library,
|
||||||
OverlayLibrary as OverlayLibrary,
|
OverlayLibrary as OverlayLibrary,
|
||||||
PortsLibraryView as PortsLibraryView,
|
PortsLibraryView as PortsLibraryView,
|
||||||
BuildLibrary as BuildLibrary,
|
LibraryBuilder as LibraryBuilder,
|
||||||
BuildReport as BuildReport,
|
BuildReport as BuildReport,
|
||||||
CellProvenance as CellProvenance,
|
CellProvenance as CellProvenance,
|
||||||
LazyLibrary as LazyLibrary,
|
LazyLibrary as LazyLibrary,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import mmap
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import klamath
|
import klamath
|
||||||
import numpy
|
|
||||||
from klamath import records
|
from klamath import records
|
||||||
|
|
||||||
from . import klamath as gdsii
|
from . import klamath as gdsii
|
||||||
|
|
@ -35,15 +34,16 @@ from ..utils import is_gzipped
|
||||||
from ...error import LibraryError
|
from ...error import LibraryError
|
||||||
from ...library import (
|
from ...library import (
|
||||||
ILibraryView,
|
ILibraryView,
|
||||||
LibraryView,
|
IMaterializable,
|
||||||
PortsLibraryView,
|
PortsLibraryView,
|
||||||
dangling_mode_t,
|
dangling_mode_t,
|
||||||
)
|
)
|
||||||
from ...utils import apply_transforms
|
from ...library.utils import _validate_dangling_mode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, Mapping, Sequence
|
||||||
|
|
||||||
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from ...pattern import Pattern
|
from ...pattern import Pattern
|
||||||
|
|
@ -122,7 +122,7 @@ def _scan_library(
|
||||||
return library_info, order, cells
|
return library_info, order, cells
|
||||||
|
|
||||||
|
|
||||||
class GdsLibrarySource(ILibraryView):
|
class GdsLibrarySource(ILibraryView, IMaterializable):
|
||||||
"""
|
"""
|
||||||
Read-only library backed by a seekable GDS stream.
|
Read-only library backed by a seekable GDS stream.
|
||||||
|
|
||||||
|
|
@ -163,7 +163,7 @@ class GdsLibrarySource(ILibraryView):
|
||||||
return cls(source=source, library_info=library_info, cell_order=cell_order, cells=cells)
|
return cls(source=source, library_info=library_info, cell_order=cell_order, cells=cells)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
return self._materialize_pattern(key, persist=True)
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
return iter(self._cell_order)
|
return iter(self._cell_order)
|
||||||
|
|
@ -177,19 +177,7 @@ class GdsLibrarySource(ILibraryView):
|
||||||
def source_order(self) -> tuple[str, ...]:
|
def source_order(self) -> tuple[str, ...]:
|
||||||
return self._cell_order
|
return self._cell_order
|
||||||
|
|
||||||
def materialize_many(
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
self,
|
|
||||||
names: Sequence[str],
|
|
||||||
*,
|
|
||||||
persist: bool = True,
|
|
||||||
) -> LibraryView:
|
|
||||||
mats = {
|
|
||||||
name: self._materialize_pattern(name, persist=persist)
|
|
||||||
for name in dict.fromkeys(names)
|
|
||||||
}
|
|
||||||
return LibraryView(mats)
|
|
||||||
|
|
||||||
def _materialize_pattern(self, name: str, *, persist: bool) -> Pattern:
|
|
||||||
if name in self._cache:
|
if name in self._cache:
|
||||||
return self._cache[name]
|
return self._cache[name]
|
||||||
|
|
||||||
|
|
@ -223,6 +211,7 @@ class GdsLibrarySource(ILibraryView):
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, set[str]]:
|
) -> dict[str, set[str]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
graph: dict[str, set[str]] = {}
|
graph: dict[str, set[str]] = {}
|
||||||
for name in self._cell_order:
|
for name in self._cell_order:
|
||||||
if name in self._cache:
|
if name in self._cache:
|
||||||
|
|
@ -243,41 +232,11 @@ class GdsLibrarySource(ILibraryView):
|
||||||
graph.setdefault(cast('str', child), set())
|
graph.setdefault(cast('str', child), set())
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
def parent_graph(
|
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
child_graph = self.child_graph(dangling='include' if dangling == 'include' else 'ignore')
|
|
||||||
existing = set(self.keys())
|
|
||||||
igraph: dict[str, set[str]] = {name: set() for name in child_graph}
|
|
||||||
for parent, children in child_graph.items():
|
|
||||||
for child in children:
|
|
||||||
if child in existing or dangling == 'include':
|
|
||||||
igraph.setdefault(child, set()).add(parent)
|
|
||||||
if dangling == 'error':
|
|
||||||
raw = self.child_graph(dangling='include')
|
|
||||||
dangling_refs = set().union(*(children - existing for children in raw.values()))
|
|
||||||
if dangling_refs:
|
|
||||||
raise self._dangling_refs_error(cast('set[str]', dangling_refs), 'building parent graph')
|
|
||||||
return igraph
|
|
||||||
|
|
||||||
def subtree(
|
def subtree(
|
||||||
self,
|
self,
|
||||||
tops: str | Sequence[str],
|
tops: str | Sequence[str],
|
||||||
) -> ILibraryView:
|
) -> ILibraryView:
|
||||||
if isinstance(tops, str):
|
return super().subtree(tops)
|
||||||
tops = (tops,)
|
|
||||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
|
||||||
keep |= set(tops)
|
|
||||||
return self.materialize_many(tuple(keep), persist=True)
|
|
||||||
|
|
||||||
def tops(self) -> list[str]:
|
|
||||||
graph = self.child_graph(dangling='ignore')
|
|
||||||
names = set(graph)
|
|
||||||
not_toplevel: set[str] = set()
|
|
||||||
for children in graph.values():
|
|
||||||
not_toplevel |= children
|
|
||||||
return list(names - not_toplevel)
|
|
||||||
|
|
||||||
def with_ports_from_data(
|
def with_ports_from_data(
|
||||||
self,
|
self,
|
||||||
|
|
@ -315,6 +274,7 @@ class GdsLibrarySource(ILibraryView):
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
||||||
if parent_graph is None:
|
if parent_graph is None:
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
|
|
@ -333,58 +293,11 @@ class GdsLibrarySource(ILibraryView):
|
||||||
for ref in self._cache[parent].refs.get(name, []):
|
for ref in self._cache[parent].refs.get(name, []):
|
||||||
instances[parent].append(ref.as_transforms())
|
instances[parent].append(ref.as_transforms())
|
||||||
continue
|
continue
|
||||||
pat = self._materialize_pattern(parent, persist=False)
|
pat = self.materialize(parent, persist=False)
|
||||||
for ref in pat.refs.get(name, []):
|
for ref in pat.refs.get(name, []):
|
||||||
instances[parent].append(ref.as_transforms())
|
instances[parent].append(ref.as_transforms())
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
def find_refs_global(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
order: list[str] | None = None,
|
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
|
||||||
if order is None:
|
|
||||||
order = self.child_order(dangling=graph_mode)
|
|
||||||
if parent_graph is None:
|
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
|
||||||
|
|
||||||
if name not in self:
|
|
||||||
if name not in parent_graph:
|
|
||||||
return {}
|
|
||||||
if dangling == 'error':
|
|
||||||
raise self._dangling_refs_error({name}, f'finding global refs for {name!r}')
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return {}
|
|
||||||
|
|
||||||
self_keys = set(self.keys())
|
|
||||||
transforms: dict[str, list[tuple[tuple[str, ...], NDArray[numpy.float64]]]]
|
|
||||||
transforms = defaultdict(list)
|
|
||||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph, dangling=dangling).items():
|
|
||||||
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
|
||||||
|
|
||||||
for next_name in order:
|
|
||||||
if next_name not in transforms:
|
|
||||||
continue
|
|
||||||
if not parent_graph.get(next_name, set()) & self_keys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph, dangling=dangling)
|
|
||||||
inners = transforms.pop(next_name)
|
|
||||||
for parent, outer in outers.items():
|
|
||||||
outer_tf = numpy.concatenate(outer)
|
|
||||||
for path, inner in inners:
|
|
||||||
combined = apply_transforms(outer_tf, inner)
|
|
||||||
transforms[parent].append(((next_name,) + path, combined))
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
for parent, targets in transforms.items():
|
|
||||||
for path, instances in targets:
|
|
||||||
result[(parent,) + path] = instances
|
|
||||||
return result
|
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self._source.close()
|
self._source.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,12 @@ from .lazy_write import write as write, writefile as writefile
|
||||||
from ..utils import is_gzipped
|
from ..utils import is_gzipped
|
||||||
from ...library import (
|
from ...library import (
|
||||||
ILibraryView,
|
ILibraryView,
|
||||||
|
IMaterializable,
|
||||||
LibraryView,
|
LibraryView,
|
||||||
PortsLibraryView,
|
PortsLibraryView,
|
||||||
dangling_mode_t,
|
dangling_mode_t,
|
||||||
)
|
)
|
||||||
from ...utils import apply_transforms
|
from ...library.utils import _validate_dangling_mode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, Mapping, Sequence
|
||||||
|
|
@ -197,7 +198,7 @@ def _expand_aref_row(
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
class ArrowLibrary(ILibraryView):
|
class ArrowLibrary(ILibraryView, IMaterializable):
|
||||||
"""
|
"""
|
||||||
Read-only library backed by the native lazy Arrow scan schema.
|
Read-only library backed by the native lazy Arrow scan schema.
|
||||||
|
|
||||||
|
|
@ -231,7 +232,7 @@ class ArrowLibrary(ILibraryView):
|
||||||
return cls(path=path, payload=payload, source=source)
|
return cls(path=path, payload=payload, source=source)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
return self._materialize_pattern(key, persist=True)
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
return iter(self._payload.cell_order)
|
return iter(self._payload.cell_order)
|
||||||
|
|
@ -299,7 +300,7 @@ class ArrowLibrary(ILibraryView):
|
||||||
materialized[name] = self._cache[name]
|
materialized[name] = self._cache[name]
|
||||||
return materialized
|
return materialized
|
||||||
|
|
||||||
def _materialize_pattern(self, name: str, *, persist: bool) -> 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)[name]
|
||||||
|
|
||||||
def _raw_children(self, name: str) -> set[str]:
|
def _raw_children(self, name: str) -> set[str]:
|
||||||
|
|
@ -346,6 +347,7 @@ class ArrowLibrary(ILibraryView):
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, set[str]]:
|
) -> dict[str, set[str]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
graph: dict[str, set[str]] = {}
|
graph: dict[str, set[str]] = {}
|
||||||
for name in self._payload.cell_order:
|
for name in self._payload.cell_order:
|
||||||
if name in self._cache:
|
if name in self._cache:
|
||||||
|
|
@ -366,41 +368,11 @@ class ArrowLibrary(ILibraryView):
|
||||||
graph.setdefault(cast('str', child), set())
|
graph.setdefault(cast('str', child), set())
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
def parent_graph(
|
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
child_graph = self.child_graph(dangling='include' if dangling == 'include' else 'ignore')
|
|
||||||
existing = set(self.keys())
|
|
||||||
igraph: dict[str, set[str]] = {name: set() for name in child_graph}
|
|
||||||
for parent, children in child_graph.items():
|
|
||||||
for child in children:
|
|
||||||
if child in existing or dangling == 'include':
|
|
||||||
igraph.setdefault(child, set()).add(parent)
|
|
||||||
if dangling == 'error':
|
|
||||||
raw = self.child_graph(dangling='include')
|
|
||||||
dangling_refs = set().union(*(children - existing for children in raw.values()))
|
|
||||||
if dangling_refs:
|
|
||||||
raise self._dangling_refs_error(cast('set[str]', dangling_refs), 'building parent graph')
|
|
||||||
return igraph
|
|
||||||
|
|
||||||
def subtree(
|
def subtree(
|
||||||
self,
|
self,
|
||||||
tops: str | Sequence[str],
|
tops: str | Sequence[str],
|
||||||
) -> ILibraryView:
|
) -> ILibraryView:
|
||||||
if isinstance(tops, str):
|
return super().subtree(tops)
|
||||||
tops = (tops,)
|
|
||||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
|
||||||
keep |= set(tops)
|
|
||||||
return self.materialize_many(tuple(keep), persist=True)
|
|
||||||
|
|
||||||
def tops(self) -> list[str]:
|
|
||||||
graph = self.child_graph(dangling='ignore')
|
|
||||||
names = set(graph)
|
|
||||||
not_toplevel: set[str] = set()
|
|
||||||
for children in graph.values():
|
|
||||||
not_toplevel |= children
|
|
||||||
return list(names - not_toplevel)
|
|
||||||
|
|
||||||
def with_ports_from_data(
|
def with_ports_from_data(
|
||||||
self,
|
self,
|
||||||
|
|
@ -452,6 +424,7 @@ class ArrowLibrary(ILibraryView):
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
||||||
if parent_graph is None:
|
if parent_graph is None:
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
|
|
@ -479,54 +452,6 @@ class ArrowLibrary(ILibraryView):
|
||||||
instances[parent].extend(rows)
|
instances[parent].extend(rows)
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
def find_refs_global(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
order: list[str] | None = None,
|
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
|
||||||
if order is None:
|
|
||||||
order = self.child_order(dangling=graph_mode)
|
|
||||||
if parent_graph is None:
|
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
|
||||||
|
|
||||||
if name not in self:
|
|
||||||
if name not in parent_graph:
|
|
||||||
return {}
|
|
||||||
if dangling == 'error':
|
|
||||||
raise self._dangling_refs_error({name}, f'finding global refs for {name!r}')
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return {}
|
|
||||||
|
|
||||||
self_keys = set(self.keys())
|
|
||||||
transforms: dict[str, list[tuple[tuple[str, ...], NDArray[numpy.float64]]]]
|
|
||||||
transforms = defaultdict(list)
|
|
||||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph, dangling=dangling).items():
|
|
||||||
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
|
||||||
|
|
||||||
for next_name in order:
|
|
||||||
if next_name not in transforms:
|
|
||||||
continue
|
|
||||||
if not parent_graph.get(next_name, set()) & self_keys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph, dangling=dangling)
|
|
||||||
inners = transforms.pop(next_name)
|
|
||||||
for parent, outer in outers.items():
|
|
||||||
outer_tf = numpy.concatenate(outer)
|
|
||||||
for path, inner in inners:
|
|
||||||
combined = apply_transforms(outer_tf, inner)
|
|
||||||
transforms[parent].append(((next_name,) + path, combined))
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
for parent, targets in transforms.items():
|
|
||||||
for path, instances in targets:
|
|
||||||
full_path = (parent,) + path
|
|
||||||
result[full_path] = instances
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def readfile(
|
def readfile(
|
||||||
filename: str | pathlib.Path,
|
filename: str | pathlib.Path,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ or remapped.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import IO, TYPE_CHECKING, Any, cast
|
from typing import IO, TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
@ -18,8 +18,8 @@ import klamath
|
||||||
from . import klamath as gdsii
|
from . import klamath as gdsii
|
||||||
from ..utils import tmpfile
|
from ..utils import tmpfile
|
||||||
from ...error import LibraryError
|
from ...error import LibraryError
|
||||||
from ...library import ILibraryView, OverlayLibrary
|
from ...library import IBorrowing, ILibraryView
|
||||||
from ...library.overlay import _SourceEntry, _materialize_detached_pattern
|
from ...library.overlay import _materialize_detached_pattern
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
|
@ -30,6 +30,23 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class _GdsInfoSource(Protocol):
|
||||||
|
"""Structural capability for propagating GDS header metadata."""
|
||||||
|
library_info: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class _GdsRawCellSource(Protocol):
|
||||||
|
"""GDS-specific raw-structure copy-through capability."""
|
||||||
|
|
||||||
|
def source_order(self) -> tuple[str, ...]: ...
|
||||||
|
|
||||||
|
def can_copy_raw_struct(self, name: str) -> bool: ...
|
||||||
|
|
||||||
|
def raw_struct_bytes(self, name: str) -> bytes: ...
|
||||||
|
|
||||||
|
|
||||||
def _get_write_info(
|
def _get_write_info(
|
||||||
library: Mapping[str, Pattern] | ILibraryView,
|
library: Mapping[str, Pattern] | ILibraryView,
|
||||||
*,
|
*,
|
||||||
|
|
@ -42,13 +59,16 @@ def _get_write_info(
|
||||||
|
|
||||||
infos: list[dict[str, Any]] = []
|
infos: list[dict[str, Any]] = []
|
||||||
stack: list[Mapping[str, Pattern] | ILibraryView] = [library]
|
stack: list[Mapping[str, Pattern] | ILibraryView] = [library]
|
||||||
|
seen: set[int] = set()
|
||||||
while stack:
|
while stack:
|
||||||
current = stack.pop()
|
current = stack.pop()
|
||||||
info = getattr(current, 'library_info', None)
|
if id(current) in seen:
|
||||||
if isinstance(info, dict):
|
continue
|
||||||
infos.append(info)
|
seen.add(id(current))
|
||||||
if isinstance(current, OverlayLibrary):
|
if isinstance(current, _GdsInfoSource) and isinstance(current.library_info, dict):
|
||||||
stack.extend(reversed([layer.library for layer in current._layers]))
|
infos.append(current.library_info)
|
||||||
|
if isinstance(current, IBorrowing):
|
||||||
|
stack.extend(reversed(current.borrowed_sources()))
|
||||||
|
|
||||||
if infos:
|
if infos:
|
||||||
unit_pairs = {(info['meters_per_unit'], info['logical_units_per_unit']) for info in infos}
|
unit_pairs = {(info['meters_per_unit'], info['logical_units_per_unit']) for info in infos}
|
||||||
|
|
@ -65,20 +85,6 @@ def _get_write_info(
|
||||||
return meters_per_unit, logical_units_per_unit, library_name
|
return meters_per_unit, logical_units_per_unit, library_name
|
||||||
|
|
||||||
|
|
||||||
def _can_copy_raw_cell(library: Mapping[str, Pattern] | ILibraryView, name: str) -> bool:
|
|
||||||
can_copy = getattr(library, 'can_copy_raw_struct', None)
|
|
||||||
if not callable(can_copy):
|
|
||||||
return False
|
|
||||||
return bool(can_copy(name))
|
|
||||||
|
|
||||||
|
|
||||||
def _raw_struct_bytes(library: Mapping[str, Pattern] | ILibraryView, name: str) -> bytes:
|
|
||||||
reader = getattr(library, 'raw_struct_bytes', None)
|
|
||||||
if not callable(reader):
|
|
||||||
raise TypeError('raw_struct_bytes')
|
|
||||||
return cast('bytes', reader(name))
|
|
||||||
|
|
||||||
|
|
||||||
def _write_pattern_struct(stream: IO[bytes], name: str, pat: Pattern) -> None:
|
def _write_pattern_struct(stream: IO[bytes], name: str, pat: Pattern) -> None:
|
||||||
elements: list[klamath.elements.Element] = []
|
elements: list[klamath.elements.Element] = []
|
||||||
elements += gdsii._shapes_to_elements(pat.shapes)
|
elements += gdsii._shapes_to_elements(pat.shapes)
|
||||||
|
|
@ -109,28 +115,10 @@ def write(
|
||||||
)
|
)
|
||||||
header.write(stream)
|
header.write(stream)
|
||||||
|
|
||||||
if isinstance(library, OverlayLibrary):
|
if isinstance(library, _GdsRawCellSource):
|
||||||
for name in library.source_order():
|
for name in library.source_order():
|
||||||
entry = library._entries[name]
|
if library.can_copy_raw_struct(name):
|
||||||
can_copy_overlay = False
|
stream.write(library.raw_struct_bytes(name))
|
||||||
if isinstance(entry, _SourceEntry) and name == entry.source_name:
|
|
||||||
layer = library._layers[entry.layer_index]
|
|
||||||
children = layer.child_graph.get(entry.source_name, set())
|
|
||||||
can_copy_overlay = (
|
|
||||||
_can_copy_raw_cell(layer.library, entry.source_name)
|
|
||||||
and all(library._effective_target(layer, child) == child for child in children)
|
|
||||||
)
|
|
||||||
if can_copy_overlay:
|
|
||||||
stream.write(_raw_struct_bytes(layer.library, entry.source_name))
|
|
||||||
else:
|
|
||||||
_write_pattern_struct(stream, name, library._materialize_pattern(name, persist=False))
|
|
||||||
klamath.records.ENDLIB.write(stream, None)
|
|
||||||
return
|
|
||||||
|
|
||||||
if hasattr(library, 'raw_struct_bytes'):
|
|
||||||
for name in library.source_order():
|
|
||||||
if _can_copy_raw_cell(library, name):
|
|
||||||
stream.write(_raw_struct_bytes(library, name))
|
|
||||||
else:
|
else:
|
||||||
_write_pattern_struct(stream, name, _materialize_detached_pattern(cast('ILibraryView', library), name))
|
_write_pattern_struct(stream, name, _materialize_detached_pattern(cast('ILibraryView', library), name))
|
||||||
klamath.records.ENDLIB.write(stream, None)
|
klamath.records.ENDLIB.write(stream, None)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Library classes for managing name-to-pattern mappings."""
|
"""Library classes for managing name-to-pattern mappings."""
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
INameView as INameView,
|
||||||
SINGLE_USE_PREFIX as SINGLE_USE_PREFIX,
|
SINGLE_USE_PREFIX as SINGLE_USE_PREFIX,
|
||||||
Tree as Tree,
|
Tree as Tree,
|
||||||
TreeView as TreeView,
|
TreeView as TreeView,
|
||||||
|
|
@ -12,6 +13,10 @@ from .base import (
|
||||||
ILibrary as ILibrary,
|
ILibrary as ILibrary,
|
||||||
ILibraryView as ILibraryView,
|
ILibraryView as ILibraryView,
|
||||||
)
|
)
|
||||||
|
from .capabilities import (
|
||||||
|
IBorrowing as IBorrowing,
|
||||||
|
IMaterializable as IMaterializable,
|
||||||
|
)
|
||||||
from .mapping import (
|
from .mapping import (
|
||||||
Library as Library,
|
Library as Library,
|
||||||
LibraryView as LibraryView,
|
LibraryView as LibraryView,
|
||||||
|
|
@ -21,7 +26,7 @@ from .overlay import (
|
||||||
PortsLibraryView as PortsLibraryView,
|
PortsLibraryView as PortsLibraryView,
|
||||||
)
|
)
|
||||||
from .build import (
|
from .build import (
|
||||||
BuildLibrary as BuildLibrary,
|
LibraryBuilder as LibraryBuilder,
|
||||||
BuildReport as BuildReport,
|
BuildReport as BuildReport,
|
||||||
CellProvenance as CellProvenance,
|
CellProvenance as CellProvenance,
|
||||||
cell as cell,
|
cell as cell,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ from graphlib import CycleError, TopologicalSorter
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import TYPE_CHECKING, Self, cast
|
from typing import TYPE_CHECKING, Self, cast
|
||||||
import copy
|
import copy
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
|
@ -18,7 +16,18 @@ from ..error import LibraryError, PatternError
|
||||||
from ..pattern import Pattern, map_layers
|
from ..pattern import Pattern, map_layers
|
||||||
from ..shapes import Polygon, Shape
|
from ..shapes import Polygon, Shape
|
||||||
from ..utils import apply_transforms, layer_t
|
from ..utils import apply_transforms, layer_t
|
||||||
from .utils import SINGLE_USE_PREFIX, TreeView, b64suffix, dangling_mode_t, _rename_patterns, visitor_function_t
|
from .utils import (
|
||||||
|
SINGLE_USE_PREFIX,
|
||||||
|
INameView,
|
||||||
|
TreeView,
|
||||||
|
b64suffix,
|
||||||
|
dangling_mode_t,
|
||||||
|
_plan_source_names,
|
||||||
|
_rename_patterns,
|
||||||
|
_source_rename_map,
|
||||||
|
_validate_dangling_mode,
|
||||||
|
visitor_function_t,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
|
@ -28,10 +37,7 @@ if TYPE_CHECKING:
|
||||||
from ..label import Label
|
from ..label import Label
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
class ILibraryView(Mapping[str, 'Pattern'], INameView, metaclass=ABCMeta):
|
||||||
|
|
||||||
|
|
||||||
class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|
||||||
"""
|
"""
|
||||||
Interface for a read-only library.
|
Interface for a read-only library.
|
||||||
|
|
||||||
|
|
@ -78,7 +84,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
def dangling_refs(
|
def dangling_refs(
|
||||||
self,
|
self,
|
||||||
tops: str | Sequence[str] | None = None,
|
tops: str | Sequence[str] | None = None,
|
||||||
) -> set[str | None]:
|
) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Get the set of all pattern names not present in the library but referenced
|
Get the set of all pattern names not present in the library but referenced
|
||||||
by `tops`, recursively traversing any refs.
|
by `tops`, recursively traversing any refs.
|
||||||
|
|
@ -101,8 +107,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
def referenced_patterns(
|
def referenced_patterns(
|
||||||
self,
|
self,
|
||||||
tops: str | Sequence[str] | None = None,
|
tops: str | Sequence[str] | None = None,
|
||||||
skip: set[str | None] | None = None,
|
skip: set[str] | None = None,
|
||||||
) -> set[str | None]:
|
) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Get the set of all pattern names referenced by `tops`. Recursively traverses into any refs.
|
Get the set of all pattern names referenced by `tops`. Recursively traverses into any refs.
|
||||||
|
|
||||||
|
|
@ -116,29 +122,78 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
Returns:
|
Returns:
|
||||||
Set of all referenced pattern names
|
Set of all referenced pattern names
|
||||||
"""
|
"""
|
||||||
|
graph = self.child_graph(dangling='include')
|
||||||
|
return self._referenced_patterns_from_graph(graph, tops=tops, skip=skip)
|
||||||
|
|
||||||
|
def _referenced_patterns_from_graph(
|
||||||
|
self,
|
||||||
|
graph: Mapping[str, set[str]],
|
||||||
|
*,
|
||||||
|
tops: str | Sequence[str] | None,
|
||||||
|
skip: set[str] | None = None,
|
||||||
|
) -> set[str]:
|
||||||
|
"""Traverse named references in an already-computed child graph."""
|
||||||
|
existing = set(self.keys())
|
||||||
if tops is None:
|
if tops is None:
|
||||||
tops = tuple(self.keys())
|
roots = tuple(existing)
|
||||||
|
elif isinstance(tops, str):
|
||||||
|
roots = (tops,)
|
||||||
|
else:
|
||||||
|
roots = tuple(tops)
|
||||||
|
|
||||||
|
for root in roots:
|
||||||
|
if root not in existing:
|
||||||
|
raise KeyError(root)
|
||||||
|
|
||||||
if skip is None:
|
if skip is None:
|
||||||
skip = {None}
|
skip = set()
|
||||||
|
root_set = set(roots)
|
||||||
|
skip |= root_set
|
||||||
|
|
||||||
if isinstance(tops, str):
|
targets: set[str] = set()
|
||||||
tops = (tops,)
|
stack = list(root_set)
|
||||||
tops = set(tops)
|
while stack:
|
||||||
skip |= tops # don't re-visit tops
|
name = stack.pop()
|
||||||
|
children = graph.get(name, set())
|
||||||
|
targets |= children
|
||||||
|
for child in children - skip:
|
||||||
|
skip.add(child)
|
||||||
|
if child in existing:
|
||||||
|
stack.append(child)
|
||||||
|
return targets
|
||||||
|
|
||||||
# Get referenced patterns for all tops
|
def _referenced_patterns_by_lookup(
|
||||||
targets = set()
|
self,
|
||||||
for top in set(tops):
|
tops: str | Sequence[str] | None = None,
|
||||||
targets |= self[top].referenced_patterns()
|
skip: set[str] | None = None,
|
||||||
|
) -> set[str]:
|
||||||
|
"""Traverse refs by loading only reachable patterns."""
|
||||||
|
if tops is None:
|
||||||
|
roots = tuple(self.keys())
|
||||||
|
elif isinstance(tops, str):
|
||||||
|
roots = (tops,)
|
||||||
|
else:
|
||||||
|
roots = tuple(tops)
|
||||||
|
|
||||||
# Perform recursive lookups, but only once for each name
|
if skip is None:
|
||||||
for target in targets - skip:
|
skip = set()
|
||||||
assert target is not None
|
root_set = set(roots)
|
||||||
skip.add(target)
|
skip |= root_set
|
||||||
if target in self:
|
|
||||||
targets |= self.referenced_patterns(target, skip=skip)
|
|
||||||
|
|
||||||
|
targets: set[str] = set()
|
||||||
|
stack = list(root_set)
|
||||||
|
while stack:
|
||||||
|
name = stack.pop()
|
||||||
|
children = {
|
||||||
|
target
|
||||||
|
for target in self[name].referenced_patterns()
|
||||||
|
if target is not None
|
||||||
|
}
|
||||||
|
targets |= children
|
||||||
|
for child in children - skip:
|
||||||
|
skip.add(child)
|
||||||
|
if child in self:
|
||||||
|
stack.append(child)
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
def subtree(
|
def subtree(
|
||||||
|
|
@ -154,19 +209,22 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
tops: Name(s) of patterns to keep
|
tops: Name(s) of patterns to keep
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A `LibraryView` containing only `tops` and the patterns they reference.
|
A borrowed view containing only `tops` and the patterns they reference.
|
||||||
|
|
||||||
|
The returned view snapshots membership and hierarchy, but borrows patterns
|
||||||
|
and source capabilities from this library. Keep this library open and do
|
||||||
|
not structurally mutate it for the lifetime of the returned view.
|
||||||
"""
|
"""
|
||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
|
|
||||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
graph = self.child_graph(dangling='include')
|
||||||
|
keep = self._referenced_patterns_from_graph(graph, tops=tops)
|
||||||
|
keep &= set(self.keys())
|
||||||
keep |= set(tops)
|
keep |= set(tops)
|
||||||
|
|
||||||
from .mapping import LibraryView # noqa: PLC0415
|
from .mapping import _SubtreeLibraryView # noqa: PLC0415
|
||||||
|
return _SubtreeLibraryView(self, names=keep, child_graph=graph)
|
||||||
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
|
||||||
new = LibraryView(filtered)
|
|
||||||
return new
|
|
||||||
|
|
||||||
def polygonize(
|
def polygonize(
|
||||||
self,
|
self,
|
||||||
|
|
@ -242,6 +300,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
def flatten_single(name: str) -> None:
|
def flatten_single(name: str) -> None:
|
||||||
flattened[name] = None
|
flattened[name] = None
|
||||||
pat = self[name].deepcopy()
|
pat = self[name].deepcopy()
|
||||||
|
pat.prune_refs()
|
||||||
refs_by_target = tuple((target, tuple(refs)) for target, refs in pat.refs.items())
|
refs_by_target = tuple((target, tuple(refs)) for target, refs in pat.refs.items())
|
||||||
|
|
||||||
for target, refs in refs_by_target:
|
for target, refs in refs_by_target:
|
||||||
|
|
@ -281,59 +340,6 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
assert None not in flattened.values()
|
assert None not in flattened.values()
|
||||||
return cast('dict[str, Pattern]', flattened)
|
return cast('dict[str, Pattern]', flattened)
|
||||||
|
|
||||||
def get_name(
|
|
||||||
self,
|
|
||||||
name: str = SINGLE_USE_PREFIX * 2,
|
|
||||||
sanitize: bool = True,
|
|
||||||
max_length: int = 32,
|
|
||||||
quiet: bool | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Find a unique name for the pattern.
|
|
||||||
|
|
||||||
This function may be overridden in a subclass or monkey-patched to fit the caller's requirements.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Preferred name for the pattern. Default is `SINGLE_USE_PREFIX * 2`.
|
|
||||||
sanitize: Allows only alphanumeric charaters and _?$. Replaces invalid characters with underscores.
|
|
||||||
max_length: Names longer than this will be truncated.
|
|
||||||
quiet: If `True`, suppress log messages. Default `None` suppresses messages only if
|
|
||||||
the name starts with `SINGLE_USE_PREFIX`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Name, unique within this library.
|
|
||||||
"""
|
|
||||||
if quiet is None:
|
|
||||||
quiet = name.startswith(SINGLE_USE_PREFIX)
|
|
||||||
|
|
||||||
if sanitize:
|
|
||||||
# Remove invalid characters
|
|
||||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
|
||||||
else:
|
|
||||||
sanitized_name = name
|
|
||||||
|
|
||||||
suffixed_name = sanitized_name
|
|
||||||
if sanitized_name in self:
|
|
||||||
ii = sum(1 for nn in self.keys() if nn.startswith(sanitized_name))
|
|
||||||
else:
|
|
||||||
ii = 0
|
|
||||||
while suffixed_name in self or suffixed_name == '':
|
|
||||||
suffixed_name = sanitized_name + b64suffix(ii)
|
|
||||||
ii += 1
|
|
||||||
|
|
||||||
if len(suffixed_name) > max_length:
|
|
||||||
if name == '':
|
|
||||||
raise LibraryError(f'No valid pattern names remaining within the specified {max_length=}')
|
|
||||||
|
|
||||||
cropped_name = self.get_name(sanitized_name[:-1], sanitize=sanitize, max_length=max_length, quiet=True)
|
|
||||||
else:
|
|
||||||
cropped_name = suffixed_name
|
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
logger.info(f'Requested name "{name}" changed to "{cropped_name}"')
|
|
||||||
|
|
||||||
return cropped_name
|
|
||||||
|
|
||||||
def tops(self) -> list[str]:
|
def tops(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of all patterns that are not referenced by any other pattern in the library.
|
Return the list of all patterns that are not referenced by any other pattern in the library.
|
||||||
|
|
@ -341,13 +347,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
Returns:
|
Returns:
|
||||||
A list of pattern names in which no pattern is referenced by any other pattern.
|
A list of pattern names in which no pattern is referenced by any other pattern.
|
||||||
"""
|
"""
|
||||||
|
graph = self.child_graph(dangling='ignore')
|
||||||
names = set(self.keys())
|
names = set(self.keys())
|
||||||
not_toplevel: set[str | None] = set()
|
referenced = set().union(*graph.values()) if graph else set()
|
||||||
for name in names:
|
return list(names - referenced)
|
||||||
not_toplevel |= set(self[name].refs.keys())
|
|
||||||
|
|
||||||
toplevel = list(names - not_toplevel)
|
|
||||||
return toplevel
|
|
||||||
|
|
||||||
def top(self) -> str:
|
def top(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
@ -412,7 +415,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
tuple of all parent-and-higher pattern names. Top pattern name may be
|
tuple of all parent-and-higher pattern names. Top pattern name may be
|
||||||
`None` if not provided in first call to .dfs()
|
`None` if not provided in first call to .dfs()
|
||||||
`transform`: numpy.ndarray containing cumulative
|
`transform`: numpy.ndarray containing cumulative
|
||||||
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
|
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1), scale]
|
||||||
for the instance being visited
|
for the instance being visited
|
||||||
`memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`)
|
`memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`)
|
||||||
|
|
||||||
|
|
@ -450,13 +453,13 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
if visit_before is not None:
|
if visit_before is not None:
|
||||||
pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
|
pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
|
||||||
|
|
||||||
for target in pattern.refs:
|
for target, refs in pattern.refs.items():
|
||||||
if target is None:
|
if target is None or not refs:
|
||||||
continue
|
continue
|
||||||
if target in hierarchy:
|
if target in hierarchy:
|
||||||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
||||||
|
|
||||||
for ref in pattern.refs[target]:
|
for ref in refs:
|
||||||
ref_transforms: list[bool] | NDArray[numpy.float64]
|
ref_transforms: list[bool] | NDArray[numpy.float64]
|
||||||
if transform is not False:
|
if transform is not False:
|
||||||
ref_transforms = apply_transforms(transform, ref.as_transforms())
|
ref_transforms = apply_transforms(transform, ref.as_transforms())
|
||||||
|
|
@ -509,6 +512,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
Returns:
|
Returns:
|
||||||
Mapping from pattern name to a set of all pattern names it references.
|
Mapping from pattern name to a set of all pattern names it references.
|
||||||
"""
|
"""
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
graph, dangling_refs = self._raw_child_graph()
|
graph, dangling_refs = self._raw_child_graph()
|
||||||
if dangling == 'error':
|
if dangling == 'error':
|
||||||
if dangling_refs:
|
if dangling_refs:
|
||||||
|
|
@ -538,13 +542,18 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
Returns:
|
Returns:
|
||||||
Mapping from pattern name to a set of all patterns which reference it.
|
Mapping from pattern name to a set of all patterns which reference it.
|
||||||
"""
|
"""
|
||||||
child_graph, dangling_refs = self._raw_child_graph()
|
_validate_dangling_mode(dangling)
|
||||||
|
graph_mode: dangling_mode_t = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
|
child_graph = self.child_graph(dangling=graph_mode)
|
||||||
|
existing = set(self.keys())
|
||||||
|
dangling_refs = set(child_graph) - existing
|
||||||
if dangling == 'error' and dangling_refs:
|
if dangling == 'error' and dangling_refs:
|
||||||
raise self._dangling_refs_error(dangling_refs, 'building parent graph')
|
raise self._dangling_refs_error(dangling_refs, 'building parent graph')
|
||||||
|
|
||||||
existing = set(child_graph)
|
graph_names = set(child_graph) if dangling == 'include' else existing
|
||||||
igraph: dict[str, set[str]] = {name: set() for name in existing}
|
igraph: dict[str, set[str]] = {name: set() for name in graph_names}
|
||||||
for parent, children in child_graph.items():
|
for parent in existing:
|
||||||
|
children = child_graph.get(parent, set())
|
||||||
for child in children:
|
for child in children:
|
||||||
if child in existing:
|
if child in existing:
|
||||||
igraph[child].add(parent)
|
igraph[child].add(parent)
|
||||||
|
|
@ -566,6 +575,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
Return:
|
Return:
|
||||||
Topologically sorted list of pattern names.
|
Topologically sorted list of pattern names.
|
||||||
"""
|
"""
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
try:
|
try:
|
||||||
return cast('list[str]', list(TopologicalSorter(self.child_graph(dangling=dangling)).static_order()))
|
return cast('list[str]', list(TopologicalSorter(self.child_graph(dangling=dangling)).static_order()))
|
||||||
except CycleError as exc:
|
except CycleError as exc:
|
||||||
|
|
@ -596,9 +606,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mapping of {parent_name: transform_list}, where transform_list
|
Mapping of {parent_name: transform_list}, where transform_list
|
||||||
is an Nx4 ndarray with rows
|
is an Nx5 ndarray with rows
|
||||||
`(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
`(x_offset, y_offset, rotation_ccw_rad, mirror_across_x, scale)`.
|
||||||
"""
|
"""
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
instances = defaultdict(list)
|
instances = defaultdict(list)
|
||||||
if parent_graph is None:
|
if parent_graph is None:
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
|
|
@ -648,9 +659,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
||||||
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx4 ndarray
|
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx5 ndarray
|
||||||
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x, scale)`.
|
||||||
"""
|
"""
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
if order is None:
|
if order is None:
|
||||||
order = self.child_order(dangling=graph_mode)
|
order = self.child_order(dangling=graph_mode)
|
||||||
|
|
@ -700,11 +712,15 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
class ILibrary(ILibraryView, metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
Interface for a writeable library.
|
Interface for an insertable and deletable library.
|
||||||
|
|
||||||
A library is a mapping from unique names (str) to collections of geometry (`Pattern`).
|
A library is a mapping from unique names (str) to collections of geometry (`Pattern`).
|
||||||
|
Assignment inserts new names but does not replace existing ones. `ILibrary`
|
||||||
|
intentionally does not implement `MutableMapping` or its generic mutation
|
||||||
|
helpers; use the library-specific insertion, deletion, rename, and add
|
||||||
|
operations instead.
|
||||||
"""
|
"""
|
||||||
# inherited abstract functions
|
# inherited abstract functions
|
||||||
#def __getitem__(self, key: str) -> 'Pattern':
|
#def __getitem__(self, key: str) -> 'Pattern':
|
||||||
|
|
@ -853,27 +869,35 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
def add(
|
def add(
|
||||||
self,
|
self,
|
||||||
other: Mapping[str, Pattern],
|
other: Mapping[str, Pattern],
|
||||||
rename_theirs: Callable[[ILibraryView, str], str] = _rename_patterns,
|
rename_theirs: Callable[[INameView, str], str] = _rename_patterns,
|
||||||
mutate_other: bool = False,
|
mutate_other: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Add items from another library into this one.
|
Add items from another library into this one.
|
||||||
|
|
||||||
If any name in `other` is already present in `self`, `rename_theirs(self, name)` is called
|
If any name in `other` is already present in the prospective destination,
|
||||||
to pick a new name for the newly-added pattern. If the new name still conflicts with a name
|
`rename_theirs(view, name)` is called to pick a new name for the newly-added pattern.
|
||||||
in `self` a `LibraryError` is raised. All references to the original name (within `other)`
|
The name-only `INameView` includes destination names and names reserved earlier in
|
||||||
are updated to the new name.
|
the same addition plan.
|
||||||
|
If the new name still conflicts, a `LibraryError` is raised. All references to the
|
||||||
|
original name within `other` are updated to the new name.
|
||||||
If `mutate_other=False` (default), all changes are made to a deepcopy of `other`.
|
If `mutate_other=False` (default), all changes are made to a deepcopy of `other`.
|
||||||
|
|
||||||
|
Name resolution, copying, and reference remapping complete before cells are inserted,
|
||||||
|
so failures during those phases do not partially modify either library. Custom
|
||||||
|
implementations of `_merge()` may still have partial effects if they raise while cells
|
||||||
|
are being committed.
|
||||||
|
|
||||||
By default, `rename_theirs` makes no changes to the name (causing a `LibraryError`) unless the
|
By default, `rename_theirs` makes no changes to the name (causing a `LibraryError`) unless the
|
||||||
name starts with `SINGLE_USE_PREFIX`. Prefixed names are truncated to before their first
|
name starts with `SINGLE_USE_PREFIX`. Prefixed names are truncated to before their first
|
||||||
non-prefix '$' and then passed to `self.get_name()` to create a new unique name.
|
non-prefix '$' and then passed to `self.get_name()` to create a new unique name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
other: The library to insert keys from.
|
other: The library to insert keys from.
|
||||||
rename_theirs: Called as rename_theirs(self, name) for each duplicate name
|
rename_theirs: Called as `rename_theirs(view, name)` for each duplicate name
|
||||||
encountered in `other`. Should return the new name for the pattern in
|
encountered in `other`, where `view` supports membership, iteration,
|
||||||
`other`. See above for default behavior.
|
`len()`, and `get_name()`, but not pattern lookup. Should return the new
|
||||||
|
name for the pattern in `other`. See above for default behavior.
|
||||||
mutate_other: If `True`, modify the original library and its contained patterns
|
mutate_other: If `True`, modify the original library and its contained patterns
|
||||||
(e.g. when renaming patterns and updating refs). Otherwise, operate on a deepcopy
|
(e.g. when renaming patterns and updating refs). Otherwise, operate on a deepcopy
|
||||||
(default).
|
(default).
|
||||||
|
|
@ -888,42 +912,34 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
from ..pattern import map_targets # noqa: PLC0415
|
from ..pattern import map_targets # noqa: PLC0415
|
||||||
from .mapping import Library # noqa: PLC0415
|
from .mapping import Library # noqa: PLC0415
|
||||||
|
|
||||||
duplicates = set(self.keys()) & set(other.keys())
|
source_order = tuple(other.keys())
|
||||||
|
source_to_visible = _plan_source_names(
|
||||||
if not duplicates:
|
self,
|
||||||
if mutate_other:
|
source_order,
|
||||||
temp = other
|
rename_theirs = rename_theirs,
|
||||||
else:
|
rename_when = 'conflict',
|
||||||
temp = Library(copy.deepcopy(dict(other)))
|
)
|
||||||
|
rename_map = _source_rename_map(source_to_visible)
|
||||||
for key in temp:
|
|
||||||
self._merge(key, temp, key)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if mutate_other:
|
if mutate_other:
|
||||||
if isinstance(other, Library):
|
if isinstance(other, Library):
|
||||||
temp = other
|
temp = other
|
||||||
else:
|
else:
|
||||||
temp = Library(dict(other))
|
temp = Library({name: other[name] for name in source_order})
|
||||||
else:
|
else:
|
||||||
temp = Library(copy.deepcopy(dict(other)))
|
temp = Library(copy.deepcopy({name: other[name] for name in source_order}))
|
||||||
rename_map = {}
|
|
||||||
for old_name in temp:
|
|
||||||
if old_name in self:
|
|
||||||
new_name = rename_theirs(self, old_name)
|
|
||||||
if new_name in self:
|
|
||||||
raise LibraryError(f'Unresolved duplicate key encountered in library merge: {old_name} -> {new_name}')
|
|
||||||
rename_map[old_name] = new_name
|
|
||||||
else:
|
|
||||||
new_name = old_name
|
|
||||||
|
|
||||||
self._merge(new_name, temp, old_name)
|
if rename_map:
|
||||||
|
target_map = cast('dict[str | None, str | None]', rename_map)
|
||||||
|
remapped_refs = {
|
||||||
|
source_name: map_targets(temp[source_name].refs, lambda target: target_map.get(target, target))
|
||||||
|
for source_name in source_order
|
||||||
|
}
|
||||||
|
for source_name, refs in remapped_refs.items():
|
||||||
|
temp[source_name].refs = refs
|
||||||
|
|
||||||
# Update references in the newly-added cells
|
for source_name in source_order:
|
||||||
for old_name in temp:
|
self._merge(source_to_visible[source_name], temp, source_name)
|
||||||
new_name = rename_map.get(old_name, old_name)
|
|
||||||
pat = self[new_name]
|
|
||||||
pat.refs = map_targets(pat.refs, lambda tt: cast('dict[str | None, str | None]', rename_map).get(tt, tt))
|
|
||||||
|
|
||||||
return rename_map
|
return rename_map
|
||||||
|
|
||||||
|
|
@ -934,21 +950,18 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
based on `add()`'s default `rename_theirs` argument).
|
based on `add()`'s default `rename_theirs` argument).
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
LibraryError if there is more than one topcell in `other`.
|
LibraryError if there is not exactly one topcell in `other`.
|
||||||
"""
|
"""
|
||||||
from .mapping import LibraryView # noqa: PLC0415
|
from .mapping import LibraryView # noqa: PLC0415
|
||||||
|
|
||||||
if len(other) == 1:
|
if not isinstance(other, ILibraryView):
|
||||||
name = next(iter(other))
|
other = LibraryView(other)
|
||||||
else:
|
|
||||||
if not isinstance(other, ILibraryView):
|
|
||||||
other = LibraryView(other)
|
|
||||||
|
|
||||||
tops = other.tops()
|
tops = other.tops()
|
||||||
if len(tops) > 1:
|
if len(tops) != 1:
|
||||||
raise LibraryError('Received a library containing multiple topcells!')
|
raise LibraryError(f'Received a library without exactly one topcell: {pformat(tops)}')
|
||||||
|
|
||||||
name = tops[0]
|
name = tops[0]
|
||||||
|
|
||||||
rename_map = self.add(other)
|
rename_map = self.add(other)
|
||||||
new_name = rename_map.get(name, name)
|
new_name = rename_map.get(name, name)
|
||||||
|
|
@ -960,7 +973,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
of just the pattern's name.
|
of just the pattern's name.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
LibraryError if there is more than one topcell in `other`.
|
LibraryError if there is not exactly one topcell in `other`.
|
||||||
"""
|
"""
|
||||||
new_name = self << other
|
new_name = self << other
|
||||||
return self.abstract(new_name)
|
return self.abstract(new_name)
|
||||||
|
|
@ -1178,7 +1191,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
|
|
||||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
keep = self.referenced_patterns(tops)
|
||||||
keep |= set(tops)
|
keep |= set(tops)
|
||||||
|
|
||||||
new = type(self)()
|
new = type(self)()
|
||||||
|
|
@ -1201,6 +1214,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
Returns:
|
Returns:
|
||||||
A set containing the names of all deleted patterns
|
A set containing the names of all deleted patterns
|
||||||
"""
|
"""
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
parent_graph = self.parent_graph(dangling=dangling)
|
parent_graph = self.parent_graph(dangling=dangling)
|
||||||
empty = {name for name, pat in self.items() if pat.is_empty()}
|
empty = {name for name, pat in self.items() if pat.is_empty()}
|
||||||
trimmed = set()
|
trimmed = set()
|
||||||
|
|
@ -1257,3 +1271,6 @@ class AbstractView(Mapping[str, Abstract]):
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self.library.__len__()
|
return self.library.__len__()
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
return key in self.library
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,25 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextvars import ContextVar
|
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pprint import pformat
|
from types import MappingProxyType
|
||||||
from typing import TYPE_CHECKING, Any, Literal, Self, cast
|
from typing import TYPE_CHECKING, Any, Literal, Self, cast
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from ..error import BuildError, LibraryError
|
from ..error import BuildError, LibraryError
|
||||||
from .base import ILibrary, ILibraryView
|
from .base import ILibrary, ILibraryView
|
||||||
from .utils import TreeView, dangling_mode_t, _plan_source_names, _rename_patterns, _source_rename_map
|
from .capabilities import IMaterializable
|
||||||
|
from .utils import INameView, TreeView, dangling_mode_t, _plan_source_names, _rename_patterns, _source_rename_map
|
||||||
from .mapping import Library, LibraryView
|
from .mapping import Library, LibraryView
|
||||||
from .overlay import OverlayLibrary
|
from .overlay import OverlayLibrary
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Iterator, Mapping, Sequence
|
from collections.abc import Callable, Iterator, KeysView, Mapping, Sequence
|
||||||
|
|
||||||
from ..abstract import Abstract
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
|
|
||||||
|
|
||||||
_ACTIVE_BUILD_SESSIONS: ContextVar[dict[int, _BuildSessionLibrary] | None] = ContextVar(
|
|
||||||
'masque_active_build_sessions',
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CellProvenance:
|
class CellProvenance:
|
||||||
"""
|
"""
|
||||||
|
|
@ -55,7 +48,7 @@ class CellProvenance:
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BuildReport:
|
class BuildReport:
|
||||||
"""
|
"""
|
||||||
Immutable summary of one `BuildLibrary.validate()` or `.build()` run.
|
Immutable summary of one `LibraryBuilder.validate()` or `.build()` run.
|
||||||
|
|
||||||
The report is designed to answer two questions after a build completes:
|
The report is designed to answer two questions after a build completes:
|
||||||
which declared cells depended on which other declared cells, and where each
|
which declared cells depended on which other declared cells, and where each
|
||||||
|
|
@ -72,6 +65,15 @@ class BuildReport:
|
||||||
provenance: Mapping[str, CellProvenance]
|
provenance: Mapping[str, CellProvenance]
|
||||||
dependency_graph: Mapping[str, frozenset[str]]
|
dependency_graph: Mapping[str, frozenset[str]]
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, 'requested_roots', tuple(self.requested_roots))
|
||||||
|
object.__setattr__(self, 'provenance', MappingProxyType(dict(self.provenance)))
|
||||||
|
object.__setattr__(
|
||||||
|
self,
|
||||||
|
'dependency_graph',
|
||||||
|
MappingProxyType({name: frozenset(deps) for name, deps in self.dependency_graph.items()}),
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _BuildRecipe:
|
class _BuildRecipe:
|
||||||
""" Captured deferred call to a pattern factory. """
|
""" Captured deferred call to a pattern factory. """
|
||||||
|
|
@ -98,42 +100,54 @@ def cell(func: Callable[..., Pattern]) -> Callable[..., _BuildRecipe]:
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class _LibraryPlaceholder:
|
||||||
|
"""Identity token replaced by this builder's active build-session library."""
|
||||||
|
__slots__ = ('_builder',)
|
||||||
|
|
||||||
|
def __init__(self, builder: LibraryBuilder) -> None:
|
||||||
|
self._builder = builder
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return '<LibraryBuilder.library>'
|
||||||
|
|
||||||
|
|
||||||
class BuildCellsView:
|
class BuildCellsView:
|
||||||
"""
|
"""
|
||||||
Attribute-based declaration namespace for `BuildLibrary`.
|
Attribute-based declaration namespace for `LibraryBuilder`.
|
||||||
|
|
||||||
This is the ergonomic authoring surface exposed as `builder.cells`. It is
|
This is the ergonomic authoring surface exposed as `builder.cells`. It is
|
||||||
intentionally write-focused: attribute assignment and deletion register
|
intentionally write-focused: attribute assignment and deletion register
|
||||||
declarations, while attribute reads fail with guidance to build first and
|
declarations, while attribute reads fail with guidance to build first and
|
||||||
use the returned library.
|
use the returned library.
|
||||||
"""
|
"""
|
||||||
|
__slots__ = ('_library',)
|
||||||
|
|
||||||
def __init__(self, library: BuildLibrary) -> None:
|
def __init__(self, library: LibraryBuilder) -> None:
|
||||||
object.__setattr__(self, '_library', library)
|
object.__setattr__(self, '_library', library)
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> Pattern:
|
def __getattr__(self, name: str) -> Pattern:
|
||||||
raise BuildError(
|
raise BuildError(
|
||||||
f'BuildLibrary.cells.{name} is write-only during authoring. '
|
f'LibraryBuilder.cells.{name} is write-only during authoring. '
|
||||||
'Call build() and index the returned library instead.'
|
'Call build() and index the returned library instead.'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: Pattern | _BuildRecipe) -> None:
|
def __setattr__(self, name: str, value: Pattern | _BuildRecipe) -> None:
|
||||||
if name.startswith('_'):
|
if name == '_library':
|
||||||
object.__setattr__(self, name, value)
|
object.__setattr__(self, name, value)
|
||||||
return
|
return
|
||||||
self._library[name] = value
|
self._library[name] = value
|
||||||
|
|
||||||
def __delattr__(self, name: str) -> None:
|
def __delattr__(self, name: str) -> None:
|
||||||
if name.startswith('_'):
|
if name == '_library':
|
||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
del self._library[name]
|
del self._library[name]
|
||||||
|
|
||||||
|
|
||||||
class BuildLibrary(ILibrary):
|
class LibraryBuilder(INameView):
|
||||||
"""
|
"""
|
||||||
Two-phase declaration surface for mixed imported/generated libraries.
|
Two-phase declaration surface for mixed imported/generated libraries.
|
||||||
|
|
||||||
A `BuildLibrary` collects three kinds of inputs:
|
A `LibraryBuilder` collects three kinds of inputs:
|
||||||
- direct declared `Pattern` objects
|
- direct declared `Pattern` objects
|
||||||
- deferred recipes created with `cell(...)`
|
- deferred recipes created with `cell(...)`
|
||||||
- imported source-backed library views added with `add_source(...)`
|
- imported source-backed library views added with `add_source(...)`
|
||||||
|
|
@ -147,112 +161,82 @@ class BuildLibrary(ILibrary):
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.cells = BuildCellsView(self)
|
self.cells = BuildCellsView(self)
|
||||||
|
self._library_placeholder = _LibraryPlaceholder(self)
|
||||||
self._frozen = False
|
self._frozen = False
|
||||||
|
self._building = False
|
||||||
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
|
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
|
||||||
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
||||||
self._names: dict[str, None] = {}
|
self._names: dict[str, None] = {}
|
||||||
|
|
||||||
def _active_session(self) -> _BuildSessionLibrary | None:
|
@property
|
||||||
sessions = _ACTIVE_BUILD_SESSIONS.get()
|
def library(self) -> _LibraryPlaceholder:
|
||||||
if sessions is None:
|
"""Read-only placeholder replaced by the active library in direct recipe arguments."""
|
||||||
return None
|
return self._library_placeholder
|
||||||
return sessions.get(id(self))
|
|
||||||
|
|
||||||
def _require_active_session(self, operation: str) -> _BuildSessionLibrary:
|
|
||||||
session = self._active_session()
|
|
||||||
if session is None:
|
|
||||||
raise BuildError(
|
|
||||||
f'BuildLibrary.{operation}() is only available while validate() or build() is running. '
|
|
||||||
'Use the built output library for reads.'
|
|
||||||
)
|
|
||||||
return session
|
|
||||||
|
|
||||||
def _assert_editable(self) -> None:
|
def _assert_editable(self) -> None:
|
||||||
if self._frozen:
|
if self._frozen:
|
||||||
raise BuildError('This BuildLibrary has already been built successfully and is now frozen.')
|
raise BuildError('This LibraryBuilder has already been built successfully and is now frozen.')
|
||||||
|
if self._building:
|
||||||
|
raise BuildError('Cannot modify a LibraryBuilder while validate() or build() is running.')
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
return iter(session)
|
|
||||||
return iter(self._names)
|
return iter(self._names)
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
return len(session)
|
|
||||||
return len(self._names)
|
return len(self._names)
|
||||||
|
|
||||||
def __contains__(self, key: object) -> bool:
|
def __contains__(self, key: object) -> bool:
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
return key in session
|
|
||||||
return key in self._names
|
return key in self._names
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def keys(self) -> KeysView[str]:
|
||||||
return self._require_active_session('__getitem__')[key]
|
return self._names.keys()
|
||||||
|
|
||||||
def __setitem__(
|
def __setitem__(
|
||||||
self,
|
self,
|
||||||
key: str,
|
key: str,
|
||||||
value: Pattern | _BuildRecipe,
|
value: Pattern | _BuildRecipe,
|
||||||
) -> None:
|
) -> None:
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
session[key] = value
|
|
||||||
return
|
|
||||||
|
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
if key in self._names:
|
if key in self._names:
|
||||||
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
||||||
|
|
||||||
if isinstance(value, _BuildRecipe):
|
if isinstance(value, _BuildRecipe):
|
||||||
|
placeholders = (
|
||||||
|
arg for arg in (*value.args, *value.kwargs.values())
|
||||||
|
if isinstance(arg, _LibraryPlaceholder)
|
||||||
|
)
|
||||||
|
if any(placeholder is not self.library for placeholder in placeholders):
|
||||||
|
raise BuildError('A recipe cannot use another LibraryBuilder.library placeholder.')
|
||||||
declaration = value
|
declaration = value
|
||||||
else:
|
else:
|
||||||
if callable(value):
|
if callable(value):
|
||||||
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
|
raise TypeError('LibraryBuilder recipes must be wrapped with cell(fn)(...) or @cell.')
|
||||||
declaration = value
|
declaration = value
|
||||||
|
|
||||||
self._declarations[key] = declaration
|
self._declarations[key] = declaration
|
||||||
self._names[key] = None
|
self._names[key] = None
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
def __delitem__(self, key: str) -> None:
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
del session[key]
|
|
||||||
return
|
|
||||||
|
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
if key not in self._declarations:
|
if key not in self._declarations:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
del self._declarations[key]
|
del self._declarations[key]
|
||||||
del self._names[key]
|
del self._names[key]
|
||||||
|
|
||||||
def _merge(self, key_self: str, other: Mapping[str, Pattern], key_other: str) -> None:
|
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
session._merge(key_self, other, key_other)
|
|
||||||
return
|
|
||||||
self[key_self] = copy.deepcopy(other[key_other])
|
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
self,
|
self,
|
||||||
other: Mapping[str, Pattern],
|
other: Mapping[str, Pattern],
|
||||||
rename_theirs: Callable[[ILibraryView, str], str] = _rename_patterns,
|
rename_theirs: Callable[[INameView, str], str] = _rename_patterns,
|
||||||
mutate_other: bool = False,
|
mutate_other: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
from ..pattern import map_targets # noqa: PLC0415
|
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)
|
|
||||||
|
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
|
|
||||||
source_backed = isinstance(other, ILibraryView) and not isinstance(other, Library | LibraryView)
|
materializable = isinstance(other, IMaterializable)
|
||||||
if source_backed:
|
if materializable:
|
||||||
if mutate_other:
|
if mutate_other:
|
||||||
raise BuildError('BuildLibrary.add(..., mutate_other=True) is not supported for source-backed inputs.')
|
raise BuildError('LibraryBuilder.add(..., mutate_other=True) is not supported for source-backed inputs.')
|
||||||
return self.add_source(
|
return self.add_source(
|
||||||
other,
|
other,
|
||||||
rename_theirs = rename_theirs,
|
rename_theirs = rename_theirs,
|
||||||
|
|
@ -263,7 +247,6 @@ class BuildLibrary(ILibrary):
|
||||||
source_to_visible = _plan_source_names(
|
source_to_visible = _plan_source_names(
|
||||||
self,
|
self,
|
||||||
source_order,
|
source_order,
|
||||||
self._names,
|
|
||||||
rename_theirs = rename_theirs,
|
rename_theirs = rename_theirs,
|
||||||
rename_when = 'conflict',
|
rename_when = 'conflict',
|
||||||
)
|
)
|
||||||
|
|
@ -272,95 +255,39 @@ class BuildLibrary(ILibrary):
|
||||||
if mutate_other:
|
if mutate_other:
|
||||||
temp = other
|
temp = other
|
||||||
else:
|
else:
|
||||||
temp = Library(copy.deepcopy(dict(other)))
|
temp = Library(copy.deepcopy({name: other[name] for name in source_order}))
|
||||||
|
|
||||||
|
if rename_map:
|
||||||
|
target_map = cast('dict[str | None, str | None]', rename_map)
|
||||||
|
remapped_refs = {
|
||||||
|
source_name: map_targets(temp[source_name].refs, lambda target: target_map.get(target, target))
|
||||||
|
for source_name in source_order
|
||||||
|
}
|
||||||
|
for source_name, refs in remapped_refs.items():
|
||||||
|
temp[source_name].refs = refs
|
||||||
|
|
||||||
for source_name in source_order:
|
for source_name in source_order:
|
||||||
visible_name = source_to_visible[source_name]
|
visible_name = source_to_visible[source_name]
|
||||||
pattern = temp[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
|
self[visible_name] = pattern
|
||||||
|
|
||||||
return rename_map
|
return rename_map
|
||||||
|
|
||||||
def __lshift__(self, other: TreeView) -> str:
|
def __lshift__(self, other: TreeView) -> str:
|
||||||
session = self._active_session()
|
|
||||||
if session is not None:
|
|
||||||
return session << other
|
|
||||||
|
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
if len(other) == 1:
|
view = other if isinstance(other, ILibraryView) else LibraryView(other)
|
||||||
name = next(iter(other))
|
tops = view.tops()
|
||||||
elif isinstance(other, ILibraryView) and not isinstance(other, Library | LibraryView):
|
if len(tops) != 1:
|
||||||
source_order = other.source_order()
|
raise LibraryError(f'Received a library without exactly one topcell: {tops}')
|
||||||
child_graph = other.child_graph(dangling='include')
|
top = tops[0]
|
||||||
referenced = set().union(*child_graph.values()) if child_graph else set()
|
rename_map = self.add(view)
|
||||||
tops = [candidate for candidate in source_order if candidate not in referenced]
|
return rename_map.get(top, top)
|
||||||
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,
|
|
||||||
old_name: str,
|
|
||||||
new_name: str,
|
|
||||||
move_references: bool = False,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Rename a helper cell during an active build session.
|
|
||||||
|
|
||||||
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:
|
|
||||||
session.rename(old_name, new_name, move_references=move_references)
|
|
||||||
return self
|
|
||||||
|
|
||||||
self._assert_editable()
|
|
||||||
if old_name == new_name:
|
|
||||||
return self
|
|
||||||
if old_name in self._declarations:
|
|
||||||
raise BuildError(
|
|
||||||
f'Cannot rename declared build cell "{old_name}" during authoring. '
|
|
||||||
'Register it under the intended final name instead.'
|
|
||||||
)
|
|
||||||
if old_name not in self._names:
|
|
||||||
raise LibraryError(f'"{old_name}" does not exist in the builder.')
|
|
||||||
raise BuildError(
|
|
||||||
f'Cannot rename imported source cell "{old_name}" during authoring. '
|
|
||||||
'Choose visible source names with add_source(..., rename_theirs=..., rename_when=...).'
|
|
||||||
)
|
|
||||||
|
|
||||||
def abstract(self, name: str) -> Abstract:
|
|
||||||
return self._require_active_session('abstract').abstract(name)
|
|
||||||
|
|
||||||
def resolve(
|
|
||||||
self,
|
|
||||||
other: Abstract | str | Pattern | TreeView,
|
|
||||||
append: bool = False,
|
|
||||||
) -> Abstract | Pattern:
|
|
||||||
return self._require_active_session('resolve').resolve(other, append=append)
|
|
||||||
|
|
||||||
def add_source(
|
def add_source(
|
||||||
self,
|
self,
|
||||||
source: Mapping[str, Pattern] | ILibraryView,
|
source: Mapping[str, Pattern] | ILibraryView,
|
||||||
*,
|
*,
|
||||||
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
rename_theirs: Callable[[INameView, str], str] | None = None,
|
||||||
rename_when: Literal['conflict', 'always'] = 'conflict',
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -369,13 +296,19 @@ class BuildLibrary(ILibrary):
|
||||||
The source is not materialized immediately. Its names are scanned once
|
The source is not materialized immediately. Its names are scanned once
|
||||||
to reserve visible builder names, then the source is read again when a
|
to reserve visible builder names, then the source is read again when a
|
||||||
build session starts. The source's cell membership must not be
|
build session starts. The source's cell membership must not be
|
||||||
structurally mutated between `add_source()` and `build()`/`validate()`.
|
closed or structurally mutated between `add_source()` and
|
||||||
|
`build()`/`validate()`.
|
||||||
Source cells may be renamed on entry to avoid collisions with existing
|
Source cells may be renamed on entry to avoid collisions with existing
|
||||||
declarations or other imported sources.
|
declarations or other imported sources.
|
||||||
|
|
||||||
|
Sources are borrowed rather than owned. An output built with
|
||||||
|
`output='overlay'` remains source-backed, so its sources must also stay
|
||||||
|
open and unchanged for the lifetime of that output.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rename_theirs: Function used to choose visible names for imported
|
rename_theirs: Function used to choose visible names for imported
|
||||||
source cells.
|
source cells. Its `INameView` argument contains existing and
|
||||||
|
previously reserved names, but does not support pattern lookup.
|
||||||
rename_when: If `'conflict'`, only conflicting names are renamed.
|
rename_when: If `'conflict'`, only conflicting names are renamed.
|
||||||
If `'always'`, every imported source name is passed through
|
If `'always'`, every imported source name is passed through
|
||||||
`rename_theirs`.
|
`rename_theirs`.
|
||||||
|
|
@ -384,8 +317,6 @@ class BuildLibrary(ILibrary):
|
||||||
Mapping of `{source_name: visible_name}` for imported names that
|
Mapping of `{source_name: visible_name}` for imported names that
|
||||||
were renamed while being added.
|
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()
|
self._assert_editable()
|
||||||
|
|
||||||
view = source if isinstance(source, ILibraryView) else LibraryView(source)
|
view = source if isinstance(source, ILibraryView) else LibraryView(source)
|
||||||
|
|
@ -393,7 +324,6 @@ class BuildLibrary(ILibrary):
|
||||||
source_to_visible = _plan_source_names(
|
source_to_visible = _plan_source_names(
|
||||||
self,
|
self,
|
||||||
source_order,
|
source_order,
|
||||||
self._names,
|
|
||||||
rename_theirs = rename_theirs,
|
rename_theirs = rename_theirs,
|
||||||
rename_when = rename_when,
|
rename_when = rename_when,
|
||||||
)
|
)
|
||||||
|
|
@ -406,7 +336,7 @@ class BuildLibrary(ILibrary):
|
||||||
|
|
||||||
def validate(
|
def validate(
|
||||||
self,
|
self,
|
||||||
names: Sequence[str] | None = None,
|
names: str | Sequence[str] | None = None,
|
||||||
*,
|
*,
|
||||||
allow_dangling: bool = False,
|
allow_dangling: bool = False,
|
||||||
) -> BuildReport:
|
) -> BuildReport:
|
||||||
|
|
@ -416,6 +346,13 @@ class BuildLibrary(ILibrary):
|
||||||
This is a dry run over the same dependency resolution and recipe
|
This is a dry run over the same dependency resolution and recipe
|
||||||
execution path used by `build()`. Any generated library is discarded
|
execution path used by `build()`. Any generated library is discarded
|
||||||
after validation completes.
|
after validation completes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
names: Declared root name or names to validate. `None` validates
|
||||||
|
every declaration. Duplicate names are ignored after their
|
||||||
|
first occurrence.
|
||||||
|
allow_dangling: If `False`, fail validation when the generated
|
||||||
|
library contains dangling references.
|
||||||
"""
|
"""
|
||||||
_session, report = self._run_build(names=names, allow_dangling=allow_dangling)
|
_session, report = self._run_build(names=names, allow_dangling=allow_dangling)
|
||||||
return report
|
return report
|
||||||
|
|
@ -431,13 +368,19 @@ class BuildLibrary(ILibrary):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
output: `'overlay'` preserves imported source-backed cells where
|
output: `'overlay'` preserves imported source-backed cells where
|
||||||
possible, while `'library'` eagerly materializes the full
|
possible and continues borrowing their sources. `'library'`
|
||||||
result.
|
eagerly materializes the full result and no longer needs the
|
||||||
|
sources afterward.
|
||||||
allow_dangling: If `False`, fail the build when the completed
|
allow_dangling: If `False`, fail the build when the completed
|
||||||
library still contains dangling references.
|
library still contains dangling references.
|
||||||
"""
|
"""
|
||||||
if output not in ('overlay', 'library'):
|
if output not in ('overlay', 'library'):
|
||||||
raise ValueError(f'Unknown build output mode: {output!r}')
|
raise ValueError(f'Unknown build output mode: {output!r}')
|
||||||
|
if self._building:
|
||||||
|
raise BuildError(
|
||||||
|
'Cannot call build() or validate() recursively on a LibraryBuilder '
|
||||||
|
'from one of its own recipes.'
|
||||||
|
)
|
||||||
self._assert_editable()
|
self._assert_editable()
|
||||||
session, report = self._run_build(names=None, allow_dangling=allow_dangling)
|
session, report = self._run_build(names=None, allow_dangling=allow_dangling)
|
||||||
if output == 'library':
|
if output == 'library':
|
||||||
|
|
@ -450,24 +393,37 @@ class BuildLibrary(ILibrary):
|
||||||
def _run_build(
|
def _run_build(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
names: Sequence[str] | None,
|
names: str | Sequence[str] | None,
|
||||||
allow_dangling: bool,
|
allow_dangling: bool,
|
||||||
) -> tuple[_BuildSessionLibrary, BuildReport]:
|
) -> tuple[_BuildSessionLibrary, BuildReport]:
|
||||||
roots = tuple(dict.fromkeys(names if names is not None else self._declarations.keys()))
|
if self._building:
|
||||||
|
raise BuildError(
|
||||||
|
'Cannot call build() or validate() recursively on a LibraryBuilder '
|
||||||
|
'from one of its own recipes.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if names is None:
|
||||||
|
requested_names = tuple(self._declarations)
|
||||||
|
elif isinstance(names, str):
|
||||||
|
requested_names = (names,)
|
||||||
|
else:
|
||||||
|
requested_names = tuple(names)
|
||||||
|
if any(not isinstance(name, str) for name in requested_names):
|
||||||
|
raise TypeError('Build roots must be strings.')
|
||||||
|
|
||||||
|
roots = tuple(dict.fromkeys(requested_names))
|
||||||
unknown = [name for name in roots if name not in self._names]
|
unknown = [name for name in roots if name not in self._names]
|
||||||
if unknown:
|
if unknown:
|
||||||
raise BuildError(f'Unknown build roots requested: {unknown}')
|
raise BuildError(f'Unknown build roots requested: {unknown}')
|
||||||
|
|
||||||
session = _BuildSessionLibrary(self)
|
self._building = True
|
||||||
sessions = dict(_ACTIVE_BUILD_SESSIONS.get() or {})
|
|
||||||
sessions[id(self)] = session
|
|
||||||
token = _ACTIVE_BUILD_SESSIONS.set(sessions)
|
|
||||||
try:
|
try:
|
||||||
|
session = _BuildSessionLibrary(self)
|
||||||
session.materialize_many(roots)
|
session.materialize_many(roots)
|
||||||
if not allow_dangling:
|
if not allow_dangling:
|
||||||
session.child_graph(dangling='error')
|
session.child_graph(dangling='error')
|
||||||
finally:
|
finally:
|
||||||
_ACTIVE_BUILD_SESSIONS.reset(token)
|
self._building = False
|
||||||
|
|
||||||
report = session.build_report(roots)
|
report = session.build_report(roots)
|
||||||
return session, report
|
return session, report
|
||||||
|
|
@ -475,7 +431,7 @@ class BuildLibrary(ILibrary):
|
||||||
|
|
||||||
class _BuildSessionLibrary(ILibrary):
|
class _BuildSessionLibrary(ILibrary):
|
||||||
"""
|
"""
|
||||||
Internal overlay-backed library used while a `BuildLibrary` is executing.
|
Internal overlay-backed library used while a `LibraryBuilder` is executing.
|
||||||
|
|
||||||
This object provides the mutable-library surface that recipes expect while
|
This object provides the mutable-library surface that recipes expect while
|
||||||
also tracking declared-cell dependencies, helper-cell provenance, and
|
also tracking declared-cell dependencies, helper-cell provenance, and
|
||||||
|
|
@ -483,7 +439,7 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
build run.
|
build run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, builder: BuildLibrary) -> None:
|
def __init__(self, builder: LibraryBuilder) -> None:
|
||||||
self._builder = builder
|
self._builder = builder
|
||||||
self._overlay = OverlayLibrary()
|
self._overlay = OverlayLibrary()
|
||||||
self._built: set[str] = set()
|
self._built: set[str] = set()
|
||||||
|
|
@ -512,7 +468,7 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
'Do not structurally mutate source libraries between add_source() and build()/validate().'
|
'Do not structurally mutate source libraries between add_source() and build()/validate().'
|
||||||
)
|
)
|
||||||
|
|
||||||
def rename_source(_lib: ILibraryView, name: str, *, mapping: Mapping[str, str] = source_to_visible) -> str:
|
def rename_source(_lib: INameView, name: str, *, mapping: Mapping[str, str] = source_to_visible) -> str:
|
||||||
return mapping[name]
|
return mapping[name]
|
||||||
|
|
||||||
self._overlay.add_source(
|
self._overlay.add_source(
|
||||||
|
|
@ -586,6 +542,8 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
|
if key not in self._names:
|
||||||
|
raise KeyError(key)
|
||||||
if key in self._builder._declarations:
|
if key in self._builder._declarations:
|
||||||
self._record_dependency(key)
|
self._record_dependency(key)
|
||||||
self._ensure_declared(key)
|
self._ensure_declared(key)
|
||||||
|
|
@ -637,7 +595,7 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
def add(
|
def add(
|
||||||
self,
|
self,
|
||||||
other: Mapping[str, Pattern],
|
other: Mapping[str, Pattern],
|
||||||
rename_theirs: Callable[[ILibraryView, str], str] = _rename_patterns,
|
rename_theirs: Callable[[INameView, str], str] = _rename_patterns,
|
||||||
mutate_other: bool = False,
|
mutate_other: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
||||||
|
|
@ -684,7 +642,15 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
if isinstance(declaration, _BuildRecipe):
|
if isinstance(declaration, _BuildRecipe):
|
||||||
for dep in declaration.explicit_dependencies:
|
for dep in declaration.explicit_dependencies:
|
||||||
self._ensure_named(dep)
|
self._ensure_named(dep)
|
||||||
pattern = declaration.func(*declaration.args, **declaration.kwargs)
|
args = tuple(
|
||||||
|
self if arg is self._builder.library else arg
|
||||||
|
for arg in declaration.args
|
||||||
|
)
|
||||||
|
kwargs = {
|
||||||
|
key: self if value is self._builder.library else value
|
||||||
|
for key, value in declaration.kwargs.items()
|
||||||
|
}
|
||||||
|
pattern = declaration.func(*args, **kwargs)
|
||||||
if not isinstance(pattern, Pattern):
|
if not isinstance(pattern, Pattern):
|
||||||
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern') # noqa: TRY301
|
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern') # noqa: TRY301
|
||||||
else:
|
else:
|
||||||
|
|
@ -722,6 +688,42 @@ class _BuildSessionLibrary(ILibrary):
|
||||||
) -> dict[str, set[str]]:
|
) -> dict[str, set[str]]:
|
||||||
return self._overlay.parent_graph(dangling=dangling)
|
return self._overlay.parent_graph(dangling=dangling)
|
||||||
|
|
||||||
|
def subtree(
|
||||||
|
self,
|
||||||
|
tops: str | Sequence[str],
|
||||||
|
) -> Self:
|
||||||
|
if isinstance(tops, str):
|
||||||
|
tops = (tops,)
|
||||||
|
|
||||||
|
keep = self._referenced_patterns_by_lookup(tops=tops)
|
||||||
|
keep &= set(self)
|
||||||
|
keep |= set(tops)
|
||||||
|
order = tuple(name for name in self._names if name in keep)
|
||||||
|
|
||||||
|
patterns = {name: self[name] for name in order}
|
||||||
|
new = object.__new__(type(self))
|
||||||
|
new._builder = self._builder
|
||||||
|
new._overlay = OverlayLibrary()
|
||||||
|
for name in order:
|
||||||
|
new._overlay[name] = patterns[name]
|
||||||
|
new._built = {
|
||||||
|
name for name in order
|
||||||
|
if name in self._builder._declarations
|
||||||
|
}
|
||||||
|
new._declared_stack = []
|
||||||
|
new._names = dict.fromkeys(order)
|
||||||
|
new._provenance = {
|
||||||
|
name: self._provenance[name]
|
||||||
|
for name in order
|
||||||
|
if name in self._provenance
|
||||||
|
}
|
||||||
|
new._dependency_graph = defaultdict(set, {
|
||||||
|
name: set(dependencies) & keep
|
||||||
|
for name, dependencies in self._dependency_graph.items()
|
||||||
|
if name in keep
|
||||||
|
})
|
||||||
|
return new
|
||||||
|
|
||||||
def build_report(self, requested_roots: Sequence[str]) -> BuildReport:
|
def build_report(self, requested_roots: Sequence[str]) -> BuildReport:
|
||||||
dependency_graph = {
|
dependency_graph = {
|
||||||
name: frozenset(self._dependency_graph.get(name, set()))
|
name: frozenset(self._dependency_graph.get(name, set()))
|
||||||
|
|
|
||||||
42
masque/library/capabilities.py
Normal file
42
masque/library/capabilities.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""Optional capabilities implemented by lazy and borrowing libraries."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from ..pattern import Pattern
|
||||||
|
from .base import ILibraryView
|
||||||
|
from .mapping import LibraryView
|
||||||
|
|
||||||
|
|
||||||
|
class IMaterializable(ABC):
|
||||||
|
"""Capability for libraries which support explicit pattern materialization."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
|
"""Materialize one pattern, optionally retaining it in the library's cache."""
|
||||||
|
|
||||||
|
def materialize_many(
|
||||||
|
self,
|
||||||
|
names: Sequence[str],
|
||||||
|
*,
|
||||||
|
persist: bool = True,
|
||||||
|
) -> LibraryView:
|
||||||
|
"""Materialize a de-duplicated sequence into a plain read-only view."""
|
||||||
|
from .mapping import LibraryView # noqa: PLC0415
|
||||||
|
|
||||||
|
return LibraryView({
|
||||||
|
name: self.materialize(name, persist=persist)
|
||||||
|
for name in dict.fromkeys(names)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class IBorrowing(ABC):
|
||||||
|
"""Capability for library views which directly borrow other libraries."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
|
"""Return the source views directly borrowed by this library."""
|
||||||
|
|
@ -7,9 +7,10 @@ import logging
|
||||||
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
from .base import ILibrary
|
from .base import ILibrary
|
||||||
|
from .capabilities import IMaterializable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Iterator, Mapping
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LazyLibrary(ILibrary):
|
class LazyLibrary(ILibrary, IMaterializable):
|
||||||
"""
|
"""
|
||||||
This class is usually used to create a library of Patterns by mapping names to
|
This class is usually used to create a library of Patterns by mapping names to
|
||||||
functions which generate or load the relevant `Pattern` object as-needed.
|
functions which generate or load the relevant `Pattern` object as-needed.
|
||||||
|
|
@ -56,6 +57,9 @@ class LazyLibrary(ILibrary):
|
||||||
del self.cache[key]
|
del self.cache[key]
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
|
def materialize(self, key: str, *, persist: bool = True) -> Pattern:
|
||||||
logger.debug(f'loading {key}')
|
logger.debug(f'loading {key}')
|
||||||
if key in self.cache:
|
if key in self.cache:
|
||||||
logger.debug(f'found {key} in cache')
|
logger.debug(f'found {key} in cache')
|
||||||
|
|
@ -76,7 +80,8 @@ class LazyLibrary(ILibrary):
|
||||||
pat = func()
|
pat = func()
|
||||||
finally:
|
finally:
|
||||||
self._lookups_in_progress.pop()
|
self._lookups_in_progress.pop()
|
||||||
self.cache[key] = pat
|
if persist:
|
||||||
|
self.cache[key] = pat
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
|
@ -88,6 +93,15 @@ class LazyLibrary(ILibrary):
|
||||||
def __contains__(self, key: object) -> bool:
|
def __contains__(self, key: object) -> bool:
|
||||||
return key in self.mapping
|
return key in self.mapping
|
||||||
|
|
||||||
|
def referenced_patterns(
|
||||||
|
self,
|
||||||
|
tops: str | Sequence[str] | None = None,
|
||||||
|
skip: set[str] | None = None,
|
||||||
|
) -> set[str]:
|
||||||
|
# Closure-backed cells do not have hierarchy metadata. Preserve laziness
|
||||||
|
# by loading only patterns reached from the requested roots.
|
||||||
|
return self._referenced_patterns_by_lookup(tops=tops, skip=skip)
|
||||||
|
|
||||||
def _merge(self, key_self: str, other: Mapping[str, Pattern], key_other: str) -> None:
|
def _merge(self, key_self: str, other: Mapping[str, Pattern], key_other: str) -> None:
|
||||||
if isinstance(other, LazyLibrary):
|
if isinstance(other, LazyLibrary):
|
||||||
self.mapping[key_self] = other.mapping[key_other]
|
self.mapping[key_self] = other.mapping[key_other]
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,19 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import TYPE_CHECKING, Self
|
from typing import TYPE_CHECKING, Any, Self, cast
|
||||||
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
from .base import ILibrary, ILibraryView
|
from .base import ILibrary, ILibraryView
|
||||||
|
from .capabilities import IBorrowing, IMaterializable
|
||||||
|
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
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,6 +49,106 @@ class LibraryView(ILibraryView):
|
||||||
return f'<LibraryView ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
|
return f'<LibraryView ({type(self.mapping)}) with keys\n' + pformat(list(self.keys())) + '>'
|
||||||
|
|
||||||
|
|
||||||
|
class _SubtreeLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
|
"""Borrowed subtree view with snapshotted membership and hierarchy."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
source: ILibraryView,
|
||||||
|
*,
|
||||||
|
names: set[str],
|
||||||
|
child_graph: Mapping[str, set[str]],
|
||||||
|
) -> None:
|
||||||
|
self._source = source
|
||||||
|
source_order = source.source_order()
|
||||||
|
ordered = list(dict.fromkeys(name for name in source_order if name in names))
|
||||||
|
seen = set(ordered)
|
||||||
|
ordered.extend(name for name in source if name in names and name not in seen)
|
||||||
|
self._order = tuple(ordered)
|
||||||
|
self._names = frozenset(self._order)
|
||||||
|
self._child_graph = {
|
||||||
|
name: set(child_graph.get(name, set()))
|
||||||
|
for name in self._order
|
||||||
|
}
|
||||||
|
if hasattr(source, 'library_info'):
|
||||||
|
self.library_info = cast('dict[str, Any]', source.library_info)
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
|
if key not in self._names:
|
||||||
|
raise KeyError(key)
|
||||||
|
return self._source[key]
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return iter(self._order)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._order)
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
return key in self._names
|
||||||
|
|
||||||
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
|
return (self._source,)
|
||||||
|
|
||||||
|
def source_order(self) -> tuple[str, ...]:
|
||||||
|
return self._order
|
||||||
|
|
||||||
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
|
if name not in self._names:
|
||||||
|
raise KeyError(name)
|
||||||
|
if isinstance(self._source, IMaterializable):
|
||||||
|
return self._source.materialize(name, persist=persist)
|
||||||
|
return self._source[name]
|
||||||
|
|
||||||
|
def child_graph(
|
||||||
|
self,
|
||||||
|
dangling: dangling_mode_t = 'error',
|
||||||
|
) -> dict[str, set[str]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
|
graph = {name: set(children) for name, children in self._child_graph.items()}
|
||||||
|
existing = set(graph)
|
||||||
|
dangling_refs = set().union(*(children - existing for children in graph.values())) if graph else set()
|
||||||
|
if dangling == 'error':
|
||||||
|
if dangling_refs:
|
||||||
|
raise self._dangling_refs_error(dangling_refs, 'building child graph')
|
||||||
|
return graph
|
||||||
|
if dangling == 'ignore':
|
||||||
|
return {
|
||||||
|
name: {child for child in children if child in existing}
|
||||||
|
for name, children in graph.items()
|
||||||
|
}
|
||||||
|
for target in dangling_refs:
|
||||||
|
graph.setdefault(target, set())
|
||||||
|
return graph
|
||||||
|
|
||||||
|
def find_refs_local(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
|
dangling: dangling_mode_t = 'error',
|
||||||
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
|
if parent_graph is None:
|
||||||
|
graph_mode: dangling_mode_t = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
|
parent_graph = self.parent_graph(dangling=graph_mode)
|
||||||
|
refs = self._source.find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
||||||
|
return {parent: transforms for parent, transforms in refs.items() if parent in self._names}
|
||||||
|
|
||||||
|
def raw_struct_bytes(self, name: str) -> bytes:
|
||||||
|
if name not in self._names:
|
||||||
|
raise KeyError(name)
|
||||||
|
reader = getattr(self._source, 'raw_struct_bytes', None)
|
||||||
|
if not callable(reader):
|
||||||
|
raise TypeError('raw_struct_bytes')
|
||||||
|
return cast('bytes', reader(name))
|
||||||
|
|
||||||
|
def can_copy_raw_struct(self, name: str) -> bool:
|
||||||
|
if name not in self._names:
|
||||||
|
return False
|
||||||
|
can_copy = getattr(self._source, 'can_copy_raw_struct', None)
|
||||||
|
return bool(callable(can_copy) and can_copy(name))
|
||||||
|
|
||||||
|
|
||||||
class Library(ILibrary):
|
class Library(ILibrary):
|
||||||
"""
|
"""
|
||||||
Default implementation for a writeable library.
|
Default implementation for a writeable library.
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,32 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
from typing import TYPE_CHECKING, Any, Literal, Self, cast
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
import numpy
|
|
||||||
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
from ..pattern import Pattern, map_targets
|
from ..pattern import Pattern, map_targets
|
||||||
from ..utils import apply_transforms, layer_t
|
|
||||||
from .base import ILibrary, ILibraryView
|
from .base import ILibrary, ILibraryView
|
||||||
from .utils import dangling_mode_t, _plan_source_names, _source_rename_map
|
from .capabilities import IBorrowing, IMaterializable
|
||||||
|
from .utils import INameView, dangling_mode_t, _plan_source_names, _source_rename_map, _validate_dangling_mode
|
||||||
from .mapping import LibraryView
|
from .mapping import LibraryView
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Iterator, Mapping, Sequence
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
||||||
|
|
||||||
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
|
from ..utils import layer_t
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _SourceLayer:
|
class _SourceLayer:
|
||||||
""" One imported source layer tracked by an `OverlayLibrary`. """
|
""" One imported source layer tracked by an `OverlayLibrary`. """
|
||||||
library: ILibraryView
|
library: ILibraryView
|
||||||
source_to_visible: dict[str, str]
|
source_target_map: dict[str, str]
|
||||||
visible_to_source: dict[str, str]
|
|
||||||
child_graph: dict[str, set[str]]
|
child_graph: dict[str, set[str]]
|
||||||
order: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -41,18 +39,19 @@ class _SourceEntry:
|
||||||
|
|
||||||
|
|
||||||
def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern:
|
def _materialize_detached_pattern(view: ILibraryView, name: str) -> Pattern:
|
||||||
func = getattr(view, '_materialize_pattern', None)
|
if isinstance(view, IMaterializable):
|
||||||
if callable(func):
|
return view.materialize(name, persist=False).deepcopy()
|
||||||
return cast('Pattern', func(name, persist=False))
|
|
||||||
return view[name].deepcopy()
|
return view[name].deepcopy()
|
||||||
|
|
||||||
|
|
||||||
class PortsLibraryView(ILibraryView):
|
class PortsLibraryView(ILibraryView, IMaterializable, IBorrowing):
|
||||||
"""
|
"""
|
||||||
Read-only view which imports or applies ports on first materialization.
|
Read-only view which imports or applies ports on first materialization.
|
||||||
|
|
||||||
The wrapped source remains untouched; this view owns a separate processed
|
The wrapped source remains untouched; this view owns a separate processed
|
||||||
cache so direct-copy workflows can continue to use the raw source view.
|
cache so direct-copy workflows can continue to use the raw source view.
|
||||||
|
The view borrows its source: callers must keep the source open for the
|
||||||
|
lifetime of the view and close the source themselves.
|
||||||
|
|
||||||
Graph queries, source ordering, and copy-through capabilities are delegated
|
Graph queries, source ordering, and copy-through capabilities are delegated
|
||||||
to the wrapped source whenever possible, while `__getitem__` and
|
to the wrapped source whenever possible, while `__getitem__` and
|
||||||
|
|
@ -84,7 +83,7 @@ class PortsLibraryView(ILibraryView):
|
||||||
self.library_info = cast('dict[str, Any]', source.library_info)
|
self.library_info = cast('dict[str, Any]', source.library_info)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
return self._materialize_pattern(key, persist=True)
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
return iter(self._source)
|
return iter(self._source)
|
||||||
|
|
@ -95,7 +94,7 @@ class PortsLibraryView(ILibraryView):
|
||||||
def __contains__(self, key: object) -> bool:
|
def __contains__(self, key: object) -> bool:
|
||||||
return key in self._source
|
return key in self._source
|
||||||
|
|
||||||
def _materialize_pattern(self, name: str, *, persist: bool) -> Pattern:
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern:
|
||||||
from ..utils.ports2data import data_to_ports # noqa: PLC0415
|
from ..utils.ports2data import data_to_ports # noqa: PLC0415
|
||||||
|
|
||||||
if name in self._cache:
|
if name in self._cache:
|
||||||
|
|
@ -134,72 +133,31 @@ class PortsLibraryView(ILibraryView):
|
||||||
self._cache[name] = pat
|
self._cache[name] = pat
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
def materialize_many(
|
|
||||||
self,
|
|
||||||
names: Sequence[str],
|
|
||||||
*,
|
|
||||||
persist: bool = True,
|
|
||||||
) -> LibraryView:
|
|
||||||
mats = {
|
|
||||||
name: self._materialize_pattern(name, persist=persist)
|
|
||||||
for name in dict.fromkeys(names)
|
|
||||||
}
|
|
||||||
return LibraryView(mats)
|
|
||||||
|
|
||||||
def source_order(self) -> tuple[str, ...]:
|
def source_order(self) -> tuple[str, ...]:
|
||||||
return self._source.source_order()
|
return self._source.source_order()
|
||||||
|
|
||||||
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
|
return (self._source,)
|
||||||
|
|
||||||
def child_graph(
|
def child_graph(
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, set[str]]:
|
) -> dict[str, set[str]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
return self._source.child_graph(dangling=dangling)
|
return self._source.child_graph(dangling=dangling)
|
||||||
|
|
||||||
def parent_graph(
|
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
return self._source.parent_graph(dangling=dangling)
|
|
||||||
|
|
||||||
def subtree(
|
|
||||||
self,
|
|
||||||
tops: str | Sequence[str],
|
|
||||||
) -> ILibraryView:
|
|
||||||
if isinstance(tops, str):
|
|
||||||
tops = (tops,)
|
|
||||||
keep = cast('set[str]', self._source.referenced_patterns(tops) - {None})
|
|
||||||
keep |= set(tops)
|
|
||||||
return self.materialize_many(tuple(keep), persist=True)
|
|
||||||
|
|
||||||
def tops(self) -> list[str]:
|
|
||||||
return self._source.tops()
|
|
||||||
|
|
||||||
def find_refs_local(
|
def find_refs_local(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
finder = getattr(self._source, 'find_refs_local', None)
|
finder = getattr(self._source, 'find_refs_local', None)
|
||||||
if callable(finder):
|
if callable(finder):
|
||||||
return cast('dict[str, list[NDArray[numpy.float64]]]', finder(name, parent_graph=parent_graph, dangling=dangling))
|
return cast('dict[str, list[NDArray[numpy.float64]]]', finder(name, parent_graph=parent_graph, dangling=dangling))
|
||||||
return super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
return super().find_refs_local(name, parent_graph=parent_graph, dangling=dangling)
|
||||||
|
|
||||||
def find_refs_global(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
order: list[str] | None = None,
|
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
|
||||||
finder = getattr(self._source, 'find_refs_global', None)
|
|
||||||
if callable(finder):
|
|
||||||
return cast(
|
|
||||||
'dict[tuple[str, ...], NDArray[numpy.float64]]',
|
|
||||||
finder(name, order=order, parent_graph=parent_graph, dangling=dangling),
|
|
||||||
)
|
|
||||||
return super().find_refs_global(name, order=order, parent_graph=parent_graph, dangling=dangling)
|
|
||||||
|
|
||||||
def raw_struct_bytes(self, name: str) -> bytes:
|
def raw_struct_bytes(self, name: str) -> bytes:
|
||||||
reader = getattr(self._source, 'raw_struct_bytes', None)
|
reader = getattr(self._source, 'raw_struct_bytes', None)
|
||||||
if not callable(reader):
|
if not callable(reader):
|
||||||
|
|
@ -207,30 +165,26 @@ class PortsLibraryView(ILibraryView):
|
||||||
return cast('bytes', reader(name))
|
return cast('bytes', reader(name))
|
||||||
|
|
||||||
def can_copy_raw_struct(self, name: str) -> bool:
|
def can_copy_raw_struct(self, name: str) -> bool:
|
||||||
|
if name in self._cache:
|
||||||
|
return False
|
||||||
can_copy = getattr(self._source, 'can_copy_raw_struct', None)
|
can_copy = getattr(self._source, 'can_copy_raw_struct', None)
|
||||||
if not callable(can_copy):
|
if not callable(can_copy):
|
||||||
return False
|
return False
|
||||||
return bool(can_copy(name))
|
return bool(can_copy(name))
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
closer = getattr(self._source, 'close', None)
|
|
||||||
if callable(closer):
|
|
||||||
closer()
|
|
||||||
|
|
||||||
def __enter__(self) -> PortsLibraryView:
|
class OverlayLibrary(ILibrary, IMaterializable, IBorrowing):
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *_args: object) -> None:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
class OverlayLibrary(ILibrary):
|
|
||||||
"""
|
"""
|
||||||
Mutable overlay over one or more source libraries.
|
Mutable overlay over one or more source libraries.
|
||||||
|
|
||||||
Source-backed cells remain lazy until accessed through `__getitem__`, at
|
Source-backed cells remain lazy until accessed through `__getitem__`, at
|
||||||
which point that visible cell is promoted into an overlay-owned materialized
|
which point that visible cell is promoted into an overlay-owned materialized
|
||||||
`Pattern`.
|
`Pattern`.
|
||||||
|
|
||||||
|
Source libraries must remain open and must not be mutated after they are
|
||||||
|
added. The overlay borrows each source and snapshots its names, hierarchy,
|
||||||
|
and initial visible-name mapping while retaining the source itself for lazy
|
||||||
|
pattern materialization.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
@ -249,7 +203,7 @@ class OverlayLibrary(ILibrary):
|
||||||
return key in self._entries
|
return key in self._entries
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> Pattern:
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
return self._materialize_pattern(key, persist=True)
|
return self.materialize(key, persist=True)
|
||||||
|
|
||||||
def __setitem__(
|
def __setitem__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -275,15 +229,19 @@ class OverlayLibrary(ILibrary):
|
||||||
self,
|
self,
|
||||||
source: Mapping[str, Pattern] | ILibraryView,
|
source: Mapping[str, Pattern] | ILibraryView,
|
||||||
*,
|
*,
|
||||||
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
rename_theirs: Callable[[INameView, str], str] | None = None,
|
||||||
rename_when: Literal['conflict', 'always'] = 'conflict',
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Add a source-backed library layer.
|
Add a source-backed library layer.
|
||||||
|
|
||||||
|
The source must remain open, and its names, hierarchy, and pattern
|
||||||
|
contents must remain unchanged for the lifetime of this overlay.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rename_theirs: Function used to choose visible names for imported
|
rename_theirs: Function used to choose visible names for imported
|
||||||
source cells.
|
source cells. Its `INameView` argument contains existing and
|
||||||
|
previously reserved names, but does not support pattern lookup.
|
||||||
rename_when: If `'conflict'`, only conflicting names are renamed.
|
rename_when: If `'conflict'`, only conflicting names are renamed.
|
||||||
If `'always'`, every imported source name is passed through
|
If `'always'`, every imported source name is passed through
|
||||||
`rename_theirs`.
|
`rename_theirs`.
|
||||||
|
|
@ -295,18 +253,13 @@ class OverlayLibrary(ILibrary):
|
||||||
source_to_visible = _plan_source_names(
|
source_to_visible = _plan_source_names(
|
||||||
self,
|
self,
|
||||||
source_order,
|
source_order,
|
||||||
self._entries,
|
|
||||||
rename_theirs = rename_theirs,
|
rename_theirs = rename_theirs,
|
||||||
rename_when = rename_when,
|
rename_when = rename_when,
|
||||||
)
|
)
|
||||||
visible_to_source = {visible: source_name for source_name, visible in source_to_visible.items()}
|
|
||||||
|
|
||||||
layer = _SourceLayer(
|
layer = _SourceLayer(
|
||||||
library=view,
|
library=view,
|
||||||
source_to_visible=source_to_visible,
|
source_target_map=dict(source_to_visible),
|
||||||
visible_to_source=visible_to_source,
|
|
||||||
child_graph=child_graph,
|
child_graph=child_graph,
|
||||||
order=[source_to_visible[name] for name in source_order],
|
|
||||||
)
|
)
|
||||||
layer_index = len(self._layers)
|
layer_index = len(self._layers)
|
||||||
self._layers.append(layer)
|
self._layers.append(layer)
|
||||||
|
|
@ -333,11 +286,6 @@ class OverlayLibrary(ILibrary):
|
||||||
|
|
||||||
entry = self._entries.pop(old_name)
|
entry = self._entries.pop(old_name)
|
||||||
self._entries[new_name] = entry
|
self._entries[new_name] = entry
|
||||||
if isinstance(entry, _SourceEntry):
|
|
||||||
layer = self._layers[entry.layer_index]
|
|
||||||
layer.source_to_visible[entry.source_name] = new_name
|
|
||||||
del layer.visible_to_source[old_name]
|
|
||||||
layer.visible_to_source[new_name] = entry.source_name
|
|
||||||
|
|
||||||
idx = self._order.index(old_name)
|
idx = self._order.index(old_name)
|
||||||
self._order[idx] = new_name
|
self._order[idx] = new_name
|
||||||
|
|
@ -375,10 +323,10 @@ class OverlayLibrary(ILibrary):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _effective_target(self, layer: _SourceLayer, target: str) -> str:
|
def _effective_target(self, layer: _SourceLayer, target: str) -> str:
|
||||||
visible = layer.source_to_visible.get(target, target)
|
visible = layer.source_target_map.get(target, target)
|
||||||
return self._resolve_target(visible)
|
return self._resolve_target(visible)
|
||||||
|
|
||||||
def _materialize_pattern(self, name: str, *, persist: bool) -> 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)
|
||||||
entry = self._entries[name]
|
entry = self._entries[name]
|
||||||
|
|
@ -386,7 +334,7 @@ class OverlayLibrary(ILibrary):
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
layer = self._layers[entry.layer_index]
|
layer = self._layers[entry.layer_index]
|
||||||
source_pat = layer.library[entry.source_name].deepcopy()
|
source_pat = _materialize_detached_pattern(layer.library, entry.source_name)
|
||||||
|
|
||||||
def remap(target: str | None) -> str | None:
|
def remap(target: str | None) -> str | None:
|
||||||
return None if target is None else self._effective_target(layer, target)
|
return None if target is None else self._effective_target(layer, target)
|
||||||
|
|
@ -402,6 +350,7 @@ class OverlayLibrary(ILibrary):
|
||||||
self,
|
self,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, set[str]]:
|
) -> dict[str, set[str]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
graph: dict[str, set[str]] = {}
|
graph: dict[str, set[str]] = {}
|
||||||
for name in self._order:
|
for name in self._order:
|
||||||
if name not in self._entries:
|
if name not in self._entries:
|
||||||
|
|
@ -427,33 +376,31 @@ class OverlayLibrary(ILibrary):
|
||||||
graph.setdefault(cast('str', child), set())
|
graph.setdefault(cast('str', child), set())
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
def parent_graph(
|
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
child_graph = self.child_graph(dangling='include' if dangling == 'include' else 'ignore')
|
|
||||||
existing = set(self.keys())
|
|
||||||
igraph: dict[str, set[str]] = {name: set() for name in child_graph}
|
|
||||||
for parent, children in child_graph.items():
|
|
||||||
for child in children:
|
|
||||||
if child in existing or dangling == 'include':
|
|
||||||
igraph.setdefault(child, set()).add(parent)
|
|
||||||
if dangling == 'error':
|
|
||||||
raw = self.child_graph(dangling='include')
|
|
||||||
dangling_refs = set().union(*(children - existing for children in raw.values()))
|
|
||||||
if dangling_refs:
|
|
||||||
raise self._dangling_refs_error(cast('set[str]', dangling_refs), 'building parent graph')
|
|
||||||
return igraph
|
|
||||||
|
|
||||||
def subtree(
|
def subtree(
|
||||||
self,
|
self,
|
||||||
tops: str | Sequence[str],
|
tops: str | Sequence[str],
|
||||||
) -> ILibraryView:
|
) -> Self:
|
||||||
if isinstance(tops, str):
|
if isinstance(tops, str):
|
||||||
tops = (tops,)
|
tops = (tops,)
|
||||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
|
||||||
|
graph = self.child_graph(dangling='include')
|
||||||
|
keep = self._referenced_patterns_from_graph(graph, tops=tops)
|
||||||
|
keep &= set(self)
|
||||||
keep |= set(tops)
|
keep |= set(tops)
|
||||||
return LibraryView({name: self[name] for name in keep})
|
|
||||||
|
new = type(self)()
|
||||||
|
new._layers = [
|
||||||
|
_SourceLayer(
|
||||||
|
library=layer.library,
|
||||||
|
source_target_map=dict(layer.source_target_map),
|
||||||
|
child_graph={name: set(children) for name, children in layer.child_graph.items()},
|
||||||
|
)
|
||||||
|
for layer in self._layers
|
||||||
|
]
|
||||||
|
new._order = [name for name in self._order if name in keep and name in self._entries]
|
||||||
|
new._entries = {name: self._entries[name] for name in new._order}
|
||||||
|
new._target_remap = dict(self._target_remap)
|
||||||
|
return new
|
||||||
|
|
||||||
def find_refs_local(
|
def find_refs_local(
|
||||||
self,
|
self,
|
||||||
|
|
@ -461,6 +408,7 @@ class OverlayLibrary(ILibrary):
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
dangling: dangling_mode_t = 'error',
|
dangling: dangling_mode_t = 'error',
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
|
_validate_dangling_mode(dangling)
|
||||||
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
instances: dict[str, list[NDArray[numpy.float64]]] = defaultdict(list)
|
||||||
if parent_graph is None:
|
if parent_graph is None:
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
||||||
|
|
@ -475,57 +423,34 @@ class OverlayLibrary(ILibrary):
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
for parent in parent_graph.get(name, set()):
|
for parent in parent_graph.get(name, set()):
|
||||||
pat = self._materialize_pattern(parent, persist=False)
|
pat = self.materialize(parent, persist=False)
|
||||||
for ref in pat.refs.get(name, []):
|
for ref in pat.refs.get(name, []):
|
||||||
instances[parent].append(ref.as_transforms())
|
instances[parent].append(ref.as_transforms())
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
def find_refs_global(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
order: list[str] | None = None,
|
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
|
||||||
if order is None:
|
|
||||||
order = self.child_order(dangling=graph_mode)
|
|
||||||
if parent_graph is None:
|
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
|
||||||
|
|
||||||
if name not in self:
|
|
||||||
if name not in parent_graph:
|
|
||||||
return {}
|
|
||||||
if dangling == 'error':
|
|
||||||
raise self._dangling_refs_error({name}, f'finding global refs for {name!r}')
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return {}
|
|
||||||
|
|
||||||
self_keys = set(self.keys())
|
|
||||||
transforms: dict[str, list[tuple[tuple[str, ...], NDArray[numpy.float64]]]]
|
|
||||||
transforms = defaultdict(list)
|
|
||||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph, dangling=dangling).items():
|
|
||||||
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
|
||||||
|
|
||||||
for next_name in order:
|
|
||||||
if next_name not in transforms:
|
|
||||||
continue
|
|
||||||
if not parent_graph.get(next_name, set()) & self_keys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph, dangling=dangling)
|
|
||||||
inners = transforms.pop(next_name)
|
|
||||||
for parent, outer in outers.items():
|
|
||||||
outer_tf = numpy.concatenate(outer)
|
|
||||||
for path, inner in inners:
|
|
||||||
combined = apply_transforms(outer_tf, inner)
|
|
||||||
transforms[parent].append(((next_name,) + path, combined))
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
for parent, targets in transforms.items():
|
|
||||||
for path, instances in targets:
|
|
||||||
result[(parent,) + path] = instances
|
|
||||||
return result
|
|
||||||
|
|
||||||
def source_order(self) -> tuple[str, ...]:
|
def source_order(self) -> tuple[str, ...]:
|
||||||
return tuple(name for name in self._order if name in self._entries)
|
return tuple(name for name in self._order if name in self._entries)
|
||||||
|
|
||||||
|
def borrowed_sources(self) -> tuple[ILibraryView, ...]:
|
||||||
|
return tuple(layer.library for layer in self._layers)
|
||||||
|
|
||||||
|
def can_copy_raw_struct(self, name: str) -> bool:
|
||||||
|
entry = self._entries.get(name)
|
||||||
|
if not isinstance(entry, _SourceEntry) or name != entry.source_name:
|
||||||
|
return False
|
||||||
|
layer = self._layers[entry.layer_index]
|
||||||
|
can_copy = getattr(layer.library, 'can_copy_raw_struct', None)
|
||||||
|
if not callable(can_copy) or not can_copy(entry.source_name):
|
||||||
|
return False
|
||||||
|
children = layer.child_graph.get(entry.source_name, set())
|
||||||
|
return all(self._effective_target(layer, child) == child for child in children)
|
||||||
|
|
||||||
|
def raw_struct_bytes(self, name: str) -> bytes:
|
||||||
|
entry = self._entries.get(name)
|
||||||
|
if not isinstance(entry, _SourceEntry):
|
||||||
|
raise TypeError('raw_struct_bytes')
|
||||||
|
layer = self._layers[entry.layer_index]
|
||||||
|
reader = getattr(layer.library, 'raw_struct_bytes', None)
|
||||||
|
if not callable(reader):
|
||||||
|
raise TypeError('raw_struct_bytes')
|
||||||
|
return cast('bytes', reader(entry.source_name))
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
"""Shared types and helpers for library implementations."""
|
"""Shared types and helpers for library implementations."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC
|
||||||
from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias
|
from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias
|
||||||
from collections.abc import Callable, Container, Mapping, MutableMapping, Sequence
|
from collections.abc import Callable, Collection, Iterator, Mapping, MutableMapping, Sequence
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from ..error import LibraryError
|
from ..error import LibraryError
|
||||||
|
|
||||||
|
|
@ -11,7 +14,80 @@ if TYPE_CHECKING:
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from .base import ILibraryView
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SINGLE_USE_PREFIX = '_'
|
||||||
|
"""
|
||||||
|
Names starting with this prefix are assumed to refer to single-use patterns,
|
||||||
|
which may be renamed automatically by `ILibrary.add()` (via
|
||||||
|
`rename_theirs=_rename_patterns()` )
|
||||||
|
"""
|
||||||
|
# TODO what are the consequences of making '_' special? maybe we can make this decision everywhere?
|
||||||
|
|
||||||
|
|
||||||
|
class INameView(Collection[str], ABC):
|
||||||
|
"""
|
||||||
|
Read-only collection of reserved names with a shared name allocator.
|
||||||
|
|
||||||
|
Name views support membership, iteration, `len()`, and `get_name()`. They
|
||||||
|
do not provide pattern lookup or the other operations of a library mapping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_name(
|
||||||
|
self,
|
||||||
|
name: str = SINGLE_USE_PREFIX * 2,
|
||||||
|
sanitize: bool = True,
|
||||||
|
max_length: int = 32,
|
||||||
|
quiet: bool | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Find a unique name.
|
||||||
|
|
||||||
|
This function may be overridden in a subclass or monkey-patched to fit
|
||||||
|
the caller's requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Preferred name. Default is `SINGLE_USE_PREFIX * 2`.
|
||||||
|
sanitize: Allow only alphanumeric characters and _?$, replacing
|
||||||
|
invalid characters with underscores.
|
||||||
|
max_length: Truncate names longer than this.
|
||||||
|
quiet: Suppress log messages when `True`. The default suppresses
|
||||||
|
messages only when `name` starts with `SINGLE_USE_PREFIX`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A name unique within this view.
|
||||||
|
"""
|
||||||
|
if quiet is None:
|
||||||
|
quiet = name.startswith(SINGLE_USE_PREFIX)
|
||||||
|
|
||||||
|
if sanitize:
|
||||||
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||||
|
else:
|
||||||
|
sanitized_name = name
|
||||||
|
|
||||||
|
suffixed_name = sanitized_name
|
||||||
|
if sanitized_name in self:
|
||||||
|
ii = sum(1 for nn in self if nn.startswith(sanitized_name))
|
||||||
|
else:
|
||||||
|
ii = 0
|
||||||
|
while suffixed_name in self or suffixed_name == '':
|
||||||
|
suffixed_name = sanitized_name + b64suffix(ii)
|
||||||
|
ii += 1
|
||||||
|
|
||||||
|
if len(suffixed_name) > max_length:
|
||||||
|
if name == '':
|
||||||
|
raise LibraryError(f'No valid pattern names remaining within the specified {max_length=}')
|
||||||
|
|
||||||
|
cropped_name = self.get_name(sanitized_name[:-1], sanitize=sanitize, max_length=max_length, quiet=True)
|
||||||
|
else:
|
||||||
|
cropped_name = suffixed_name
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
logger.info(f'Requested name "{name}" changed to "{cropped_name}"')
|
||||||
|
|
||||||
|
return cropped_name
|
||||||
|
|
||||||
|
|
||||||
class visitor_function_t(Protocol):
|
class visitor_function_t(Protocol):
|
||||||
|
|
@ -34,16 +110,9 @@ Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
||||||
|
|
||||||
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
||||||
""" How helpers should handle refs whose targets are not present in the library. """
|
""" How helpers should handle refs whose targets are not present in the library. """
|
||||||
SINGLE_USE_PREFIX = '_'
|
|
||||||
"""
|
|
||||||
Names starting with this prefix are assumed to refer to single-use patterns,
|
|
||||||
which may be renamed automatically by `ILibrary.add()` (via
|
|
||||||
`rename_theirs=_rename_patterns()` )
|
|
||||||
"""
|
|
||||||
# TODO what are the consequences of making '_' special? maybe we can make this decision everywhere?
|
|
||||||
|
|
||||||
|
|
||||||
def _rename_patterns(lib: ILibraryView, name: str) -> str:
|
def _rename_patterns(lib: INameView, name: str) -> str:
|
||||||
"""
|
"""
|
||||||
The default `rename_theirs` function for `ILibrary.add`.
|
The default `rename_theirs` function for `ILibrary.add`.
|
||||||
|
|
||||||
|
|
@ -67,12 +136,41 @@ def _rename_patterns(lib: ILibraryView, name: str) -> str:
|
||||||
return lib.get_name(SINGLE_USE_PREFIX + stem)
|
return lib.get_name(SINGLE_USE_PREFIX + stem)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dangling_mode(dangling: dangling_mode_t) -> None:
|
||||||
|
if dangling not in ('error', 'ignore', 'include'):
|
||||||
|
raise ValueError(
|
||||||
|
f'Unknown dangling-reference mode {dangling!r}; '
|
||||||
|
'expected one of "error", "ignore", or "include"'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _ProspectiveNames(INameView):
|
||||||
|
"""Target names plus names reserved earlier in an addition plan."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: INameView,
|
||||||
|
reserved: set[str],
|
||||||
|
) -> None:
|
||||||
|
self._target = target
|
||||||
|
self._reserved = reserved
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
yield from self._target
|
||||||
|
yield from self._reserved
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._target) + len(self._reserved)
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
return key in self._reserved or key in self._target
|
||||||
|
|
||||||
|
|
||||||
def _plan_source_names(
|
def _plan_source_names(
|
||||||
target: ILibraryView,
|
target: INameView,
|
||||||
source_order: Sequence[str],
|
source_order: Sequence[str],
|
||||||
existing_names: Container[str],
|
|
||||||
*,
|
*,
|
||||||
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
rename_theirs: Callable[[INameView, str], str] | None = None,
|
||||||
rename_when: Literal['conflict', 'always'] = 'conflict',
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
if rename_when not in ('conflict', 'always'):
|
if rename_when not in ('conflict', 'always'):
|
||||||
|
|
@ -81,21 +179,22 @@ def _plan_source_names(
|
||||||
raise TypeError('rename_theirs is required when rename_when="always"')
|
raise TypeError('rename_theirs is required when rename_when="always"')
|
||||||
|
|
||||||
source_to_visible: dict[str, str] = {}
|
source_to_visible: dict[str, str] = {}
|
||||||
visible_names: set[str] = set()
|
reserved: set[str] = set()
|
||||||
|
prospective = _ProspectiveNames(target, reserved)
|
||||||
|
|
||||||
for name in source_order:
|
for name in source_order:
|
||||||
visible = name
|
visible = name
|
||||||
if rename_when == 'always':
|
if rename_when == 'always':
|
||||||
assert rename_theirs is not None
|
assert rename_theirs is not None
|
||||||
visible = rename_theirs(target, name)
|
visible = rename_theirs(prospective, name)
|
||||||
elif visible in existing_names or visible in visible_names:
|
elif visible in prospective:
|
||||||
if rename_theirs is None:
|
if rename_theirs is None:
|
||||||
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
raise LibraryError(f'Conflicting name while adding source: {name!r}')
|
||||||
visible = rename_theirs(target, name)
|
visible = rename_theirs(prospective, name)
|
||||||
if visible in existing_names or visible in visible_names:
|
if visible in prospective:
|
||||||
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}')
|
||||||
source_to_visible[name] = visible
|
source_to_visible[name] = visible
|
||||||
visible_names.add(visible)
|
reserved.add(visible)
|
||||||
|
|
||||||
return source_to_visible
|
return source_to_visible
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,41 @@ from .ports import Port, PortList
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_ref_cycles(
|
||||||
|
pattern: 'Pattern',
|
||||||
|
library: Mapping[str, 'Pattern'],
|
||||||
|
*,
|
||||||
|
operation: str,
|
||||||
|
) -> None:
|
||||||
|
visited: set[str] = set()
|
||||||
|
active: list[str] = []
|
||||||
|
active_set: set[str] = set()
|
||||||
|
|
||||||
|
def visit(current: 'Pattern') -> None:
|
||||||
|
for target, refs in current.refs.items():
|
||||||
|
if target is None or not refs:
|
||||||
|
continue
|
||||||
|
if target in active_set:
|
||||||
|
cycle_start = active.index(target)
|
||||||
|
cycle = active[cycle_start:] + [target]
|
||||||
|
raise PatternError(
|
||||||
|
f'Circular reference while {operation}: {" -> ".join(cycle)}'
|
||||||
|
)
|
||||||
|
if target in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
active.append(target)
|
||||||
|
active_set.add(target)
|
||||||
|
try:
|
||||||
|
visit(library[target])
|
||||||
|
finally:
|
||||||
|
active.pop()
|
||||||
|
active_set.remove(target)
|
||||||
|
visited.add(target)
|
||||||
|
|
||||||
|
visit(pattern)
|
||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
"""
|
"""
|
||||||
|
|
@ -509,6 +544,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
layer: layer_t,
|
layer: layer_t,
|
||||||
flatten: bool = True,
|
flatten: bool = True,
|
||||||
library: Mapping[str, 'Pattern'] | None = None,
|
library: Mapping[str, 'Pattern'] | None = None,
|
||||||
|
*,
|
||||||
|
_cycle_checked: bool = False,
|
||||||
) -> list[Polygon]:
|
) -> list[Polygon]:
|
||||||
"""
|
"""
|
||||||
Collect all geometry effectively on a given layer as a list of polygons.
|
Collect all geometry effectively on a given layer as a list of polygons.
|
||||||
|
|
@ -524,9 +561,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of `Polygon` objects.
|
A list of `Polygon` objects.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PatternError: If `flatten=True` and the referenced hierarchy contains a cycle.
|
||||||
"""
|
"""
|
||||||
if flatten and self.has_refs() and library is None:
|
if flatten and self.has_refs() and library is None:
|
||||||
raise PatternError("Must provide a library to layer_as_polygons() when flatten=True")
|
raise PatternError("Must provide a library to layer_as_polygons() when flatten=True")
|
||||||
|
if flatten and self.has_refs() and not _cycle_checked:
|
||||||
|
assert library is not None
|
||||||
|
_check_ref_cycles(self, library, operation='collecting layer polygons')
|
||||||
|
|
||||||
polys: list[Polygon] = []
|
polys: list[Polygon] = []
|
||||||
|
|
||||||
|
|
@ -543,12 +586,17 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
if flatten and self.has_refs():
|
if flatten and self.has_refs():
|
||||||
assert library is not None
|
assert library is not None
|
||||||
for target, refs in self.refs.items():
|
for target, refs in self.refs.items():
|
||||||
if target is None:
|
if target is None or not refs:
|
||||||
continue
|
continue
|
||||||
target_pat = library[target]
|
target_pat = library[target]
|
||||||
for ref in refs:
|
for ref in refs:
|
||||||
# Get polygons from target pattern on the same layer
|
# Get polygons from target pattern on the same layer
|
||||||
ref_polys = target_pat.layer_as_polygons(layer, flatten=True, library=library)
|
ref_polys = target_pat.layer_as_polygons(
|
||||||
|
layer,
|
||||||
|
flatten=True,
|
||||||
|
library=library,
|
||||||
|
_cycle_checked=True,
|
||||||
|
)
|
||||||
# Apply ref transformations
|
# Apply ref transformations
|
||||||
for p in ref_polys:
|
for p in ref_polys:
|
||||||
p_pat = ref.as_pattern(Pattern(shapes={layer: [p]}))
|
p_pat = ref.as_pattern(Pattern(shapes={layer: [p]}))
|
||||||
|
|
@ -566,13 +614,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
Returns:
|
Returns:
|
||||||
A set of all pattern names referenced by this pattern.
|
A set of all pattern names referenced by this pattern.
|
||||||
"""
|
"""
|
||||||
return set(self.refs.keys())
|
return {target for target, refs in self.refs.items() if refs}
|
||||||
|
|
||||||
def get_bounds(
|
def get_bounds(
|
||||||
self,
|
self,
|
||||||
library: Mapping[str, 'Pattern'] | None = None,
|
library: Mapping[str, 'Pattern'] | None = None,
|
||||||
recurse: bool = True,
|
recurse: bool = True,
|
||||||
cache: MutableMapping[str, NDArray[numpy.float64] | None] | None = None,
|
cache: MutableMapping[str, NDArray[numpy.float64] | None] | None = None,
|
||||||
|
*,
|
||||||
|
_cycle_checked: bool = False,
|
||||||
) -> NDArray[numpy.float64] | None:
|
) -> NDArray[numpy.float64] | None:
|
||||||
"""
|
"""
|
||||||
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
||||||
|
|
@ -589,7 +639,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PatternError: If recursive bounds are requested for a cyclic hierarchy.
|
||||||
"""
|
"""
|
||||||
|
if recurse and self.has_refs() and library is not None and not _cycle_checked:
|
||||||
|
_check_ref_cycles(self, library, operation='calculating bounds')
|
||||||
|
|
||||||
if self.is_empty():
|
if self.is_empty():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -628,7 +684,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
if target in cache:
|
if target in cache:
|
||||||
unrot_bounds = cache[target]
|
unrot_bounds = cache[target]
|
||||||
elif any(numpy.isclose(ref.rotation % (pi / 2), 0) for ref in refs):
|
elif any(numpy.isclose(ref.rotation % (pi / 2), 0) for ref in refs):
|
||||||
unrot_bounds = library[target].get_bounds(library=library, recurse=recurse, cache=cache)
|
unrot_bounds = library[target].get_bounds(
|
||||||
|
library=library,
|
||||||
|
recurse=recurse,
|
||||||
|
cache=cache,
|
||||||
|
_cycle_checked=True,
|
||||||
|
)
|
||||||
cache[target] = unrot_bounds
|
cache[target] = unrot_bounds
|
||||||
|
|
||||||
for ref in refs:
|
for ref in refs:
|
||||||
|
|
@ -1127,8 +1188,17 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
overdraw: Whether to create a new figure or draw on a pre-existing one.
|
overdraw: Whether to create a new figure or draw on a pre-existing one.
|
||||||
filename: If provided, save the figure to this file instead of showing it.
|
filename: If provided, save the figure to this file instead of showing it.
|
||||||
ports: If True, annotate the plot with arrows representing the ports.
|
ports: If True, annotate the plot with arrows representing the ports.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PatternError: If the referenced hierarchy contains a cycle.
|
||||||
"""
|
"""
|
||||||
# TODO: add text labels to visualize()
|
# TODO: add text labels to visualize()
|
||||||
|
if self.has_refs() and library is None:
|
||||||
|
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
||||||
|
if self.has_refs():
|
||||||
|
assert library is not None
|
||||||
|
_check_ref_cycles(self, library, operation='visualizing pattern hierarchy')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from matplotlib import pyplot # type: ignore #noqa: PLC0415
|
from matplotlib import pyplot # type: ignore #noqa: PLC0415
|
||||||
import matplotlib.collections # type: ignore #noqa: PLC0415
|
import matplotlib.collections # type: ignore #noqa: PLC0415
|
||||||
|
|
@ -1137,9 +1207,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
+ 'Make sure to install masque with the [visualize] option to pull in the needed dependencies.')
|
+ 'Make sure to install masque with the [visualize] option to pull in the needed dependencies.')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if self.has_refs() and library is None:
|
|
||||||
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
|
||||||
|
|
||||||
# Cache for {Pattern object ID: List of local polygon vertex arrays}
|
# Cache for {Pattern object ID: List of local polygon vertex arrays}
|
||||||
# Polygons are stored relative to the pattern's origin (offset included)
|
# Polygons are stored relative to the pattern's origin (offset included)
|
||||||
poly_cache: dict[int, list[NDArray[numpy.float64]]] = {}
|
poly_cache: dict[int, list[NDArray[numpy.float64]]] = {}
|
||||||
|
|
@ -1203,7 +1270,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
|
|
||||||
# 3. Recurse into refs
|
# 3. Recurse into refs
|
||||||
for target, refs in pat.refs.items():
|
for target, refs in pat.refs.items():
|
||||||
if target is None:
|
if target is None or not refs:
|
||||||
continue
|
continue
|
||||||
assert library is not None
|
assert library is not None
|
||||||
target_pat = library[target]
|
target_pat = library[target]
|
||||||
|
|
|
||||||
|
|
@ -842,7 +842,8 @@ def test_autotool_s_offer_uses_absolute_jog_range_for_both_signs() -> None:
|
||||||
assert isinstance(pather._paths["A"][0].data, AutoTool.GeneratedData)
|
assert isinstance(pather._paths["A"][0].data, AutoTool.GeneratedData)
|
||||||
|
|
||||||
|
|
||||||
def test_autotool_sbend_registration_order_sets_priority() -> None:
|
@pytest.mark.parametrize('reverse', [False, True])
|
||||||
|
def test_autotool_sbend_cost_is_independent_of_registration_order(reverse: bool) -> None:
|
||||||
def first_sbend(jog: float) -> Pattern:
|
def first_sbend(jog: float) -> Pattern:
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||||
|
|
@ -855,19 +856,116 @@ def test_autotool_sbend_registration_order_sets_priority() -> None:
|
||||||
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
tool = AutoTool()
|
||||||
|
generators = (second_sbend, first_sbend) if reverse else (first_sbend, second_sbend)
|
||||||
|
for generator in generators:
|
||||||
|
tool.add_sbend(generator, "core", "A", "B", jog_range=(0, 1e8))
|
||||||
|
|
||||||
|
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||||
|
|
||||||
|
assert isinstance(data, AutoTool.GeneratedData)
|
||||||
|
assert data.fn is second_sbend
|
||||||
|
assert_allclose(out_port.offset, [5, 4])
|
||||||
|
|
||||||
|
|
||||||
|
def test_autotool_sbend_explicit_cost_can_override_geometric_cost() -> None:
|
||||||
|
def long_sbend(jog: float) -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||||
|
pat.ports["B"] = Port((20, jog), pi, ptype="core")
|
||||||
|
return pat
|
||||||
|
|
||||||
|
def short_sbend(jog: float) -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||||
|
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
||||||
|
return pat
|
||||||
|
|
||||||
tool = (
|
tool = (
|
||||||
AutoTool()
|
AutoTool()
|
||||||
.add_sbend(first_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
.add_sbend(short_sbend, "core", "A", "B", jog_range=(0, 1e8), cost=10)
|
||||||
.add_sbend(second_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
.add_sbend(long_sbend, "core", "A", "B", jog_range=(0, 1e8), cost=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||||
|
|
||||||
assert isinstance(data, AutoTool.GeneratedData)
|
assert isinstance(data, AutoTool.GeneratedData)
|
||||||
assert data.fn is first_sbend
|
assert data.fn is long_sbend
|
||||||
assert_allclose(out_port.offset, [20, 4])
|
assert_allclose(out_port.offset, [20, 4])
|
||||||
|
|
||||||
|
|
||||||
|
def test_autotool_add_methods_propagate_callable_cost_to_all_created_offers() -> None:
|
||||||
|
def cost(parameter: float, endpoint: Port) -> float:
|
||||||
|
return abs(parameter) + abs(endpoint.x) + abs(endpoint.y)
|
||||||
|
|
||||||
|
def make_straight(length: float) -> Pattern:
|
||||||
|
return _make_transition_straight(length, ptype="core")
|
||||||
|
|
||||||
|
def make_sbend(jog: float) -> Pattern:
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||||
|
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
||||||
|
return pat
|
||||||
|
|
||||||
|
lib = Library()
|
||||||
|
|
||||||
|
bend = Pattern()
|
||||||
|
bend.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||||
|
bend.ports["B"] = Port((2, -2), pi / 2, ptype="core")
|
||||||
|
lib["bend"] = bend
|
||||||
|
|
||||||
|
uturn = Pattern()
|
||||||
|
uturn.ports["A"] = Port((0, 0), 0, ptype="core")
|
||||||
|
uturn.ports["B"] = Port((3, 4), 0, ptype="core")
|
||||||
|
lib["uturn"] = uturn
|
||||||
|
|
||||||
|
transition = Pattern()
|
||||||
|
transition.ports["EXT"] = Port((0, 0), 0, ptype="external")
|
||||||
|
transition.ports["CORE"] = Port((1, 0), pi, ptype="core")
|
||||||
|
lib["transition"] = transition
|
||||||
|
|
||||||
|
tool = (
|
||||||
|
AutoTool()
|
||||||
|
.add_straight(make_straight, "core", "in", cost=cost)
|
||||||
|
.add_bend(lib.abstract("bend"), "A", "B", clockwise=True, cost=cost)
|
||||||
|
.add_sbend(
|
||||||
|
make_sbend,
|
||||||
|
"core",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
endpoint=lambda jog: Port((5, jog), pi, ptype="core"),
|
||||||
|
cost=cost,
|
||||||
|
)
|
||||||
|
.add_uturn(lib.abstract("uturn"), "A", "B", cost=cost)
|
||||||
|
.add_transition(lib.abstract("transition"), "EXT", "CORE", cost=cost)
|
||||||
|
)
|
||||||
|
|
||||||
|
offers = [
|
||||||
|
*(offer for offer in tool.primitive_offers("straight", in_ptype="core")
|
||||||
|
if offer.in_ptype == offer.out_ptype == "core"),
|
||||||
|
*tool.primitive_offers("bend", in_ptype="core", ccw=False),
|
||||||
|
*tool.primitive_offers("bend", in_ptype="core", ccw=True),
|
||||||
|
*(offer for offer in tool.primitive_offers("s", in_ptype="core")
|
||||||
|
if offer.in_ptype == offer.out_ptype == "core"),
|
||||||
|
*tool.primitive_offers("u", in_ptype="core"),
|
||||||
|
*(offer for offer in tool.primitive_offers("straight", in_ptype="external")
|
||||||
|
if offer.in_ptype == "external" and offer.out_ptype == "core"),
|
||||||
|
*(offer for offer in tool.primitive_offers("straight", in_ptype="core")
|
||||||
|
if offer.in_ptype == "core" and offer.out_ptype == "external"),
|
||||||
|
]
|
||||||
|
|
||||||
|
assert len(offers) == 9
|
||||||
|
assert all(offer.cost is cost for offer in offers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_autotool_validates_cost_before_registering_any_offers() -> None:
|
||||||
|
def unused_sbend(_jog: float) -> Pattern:
|
||||||
|
raise AssertionError('invalid cost should be rejected before metadata inference')
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match='cost factor'):
|
||||||
|
AutoTool().add_sbend(unused_sbend, jog_range=(-1, 1), cost=-1)
|
||||||
|
|
||||||
|
|
||||||
def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
||||||
tool = make_sbend_tool((4, 4))
|
tool = make_sbend_tool((4, 4))
|
||||||
offers = tool.primitive_offers('s', in_ptype="core")
|
offers = tool.primitive_offers('s', in_ptype="core")
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,20 @@ from collections.abc import Iterator
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ..builder import Pather
|
from ..builder import Pather
|
||||||
from ..error import BuildError
|
from ..error import BuildError, LibraryError
|
||||||
from ..library import BuildLibrary, BuildReport, ILibraryView, Library, cell, dangling_mode_t
|
from ..library import (
|
||||||
|
INameView,
|
||||||
|
ILibrary,
|
||||||
|
IMaterializable,
|
||||||
|
LibraryBuilder,
|
||||||
|
BuildReport,
|
||||||
|
CellProvenance,
|
||||||
|
ILibraryView,
|
||||||
|
Library,
|
||||||
|
LibraryView,
|
||||||
|
cell,
|
||||||
|
dangling_mode_t,
|
||||||
|
)
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
|
|
||||||
|
|
@ -42,16 +54,73 @@ class _MetadataSource(ILibraryView):
|
||||||
return self._child_graph
|
return self._child_graph
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
class _MaterializableMetadataSource(_MetadataSource, IMaterializable):
|
||||||
builder = BuildLibrary()
|
def materialize(self, name: str, *, persist: bool = True) -> Pattern: # noqa: ARG002
|
||||||
|
return self[name]
|
||||||
|
|
||||||
def make_parent(lib: BuildLibrary) -> Pattern:
|
|
||||||
|
def test_metadata_source_base_tops_stays_lazy() -> None:
|
||||||
|
source = _MetadataSource(
|
||||||
|
{"child": Pattern(), "top": Pattern()},
|
||||||
|
{"child": set(), "top": {"child"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert source.tops() == ["top"]
|
||||||
|
assert source.loads == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_metadata_source_reachability_and_subtree_stay_lazy() -> None:
|
||||||
|
source = _MetadataSource(
|
||||||
|
{"child": Pattern(), "top": Pattern(), "unused": Pattern()},
|
||||||
|
{"child": set(), "top": {"child"}, "unused": set()},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert source.referenced_patterns("top") == {"child"}
|
||||||
|
assert source.dangling_refs("top") == set()
|
||||||
|
subtree = source.subtree("top")
|
||||||
|
|
||||||
|
assert source.loads == 0
|
||||||
|
assert subtree.source_order() == ("child", "top")
|
||||||
|
_ = subtree["top"]
|
||||||
|
assert source.loads == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_report_defensively_freezes_mappings() -> None:
|
||||||
|
provenance = {
|
||||||
|
"top": CellProvenance(
|
||||||
|
requested_name="top",
|
||||||
|
kind="declared",
|
||||||
|
owner_declared_name="top",
|
||||||
|
build_chain=("top",),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
dependencies = {"top": frozenset({"child"})}
|
||||||
|
report = BuildReport(
|
||||||
|
requested_roots=("top",),
|
||||||
|
provenance=provenance,
|
||||||
|
dependency_graph=dependencies,
|
||||||
|
)
|
||||||
|
provenance.clear()
|
||||||
|
dependencies.clear()
|
||||||
|
|
||||||
|
assert set(report.provenance) == {"top"}
|
||||||
|
assert report.dependency_graph == {"top": frozenset({"child"})}
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
report.provenance["other"] = report.provenance["top"] # type: ignore[index]
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
report.dependency_graph["top"] = frozenset() # type: ignore[index]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
|
def make_parent(lib: ILibrary) -> Pattern:
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
pat.ref("child")
|
pat.ref("child")
|
||||||
assert lib.abstract("child").name == "child"
|
assert lib.abstract("child").name == "child"
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
builder.cells.parent = cell(make_parent)(builder)
|
builder.cells.parent = cell(make_parent)(builder.library)
|
||||||
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
builder["child"] = Pattern(ports={"p": Port((0, 0), 0)})
|
||||||
|
|
||||||
built, report = builder.build()
|
built, report = builder.build()
|
||||||
|
|
@ -62,10 +131,44 @@ def test_build_library_traces_declared_dependencies_out_of_order() -> None:
|
||||||
assert report.provenance["parent"].kind == "declared"
|
assert report.provenance["parent"].kind == "declared"
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
def test_build_cells_view_supports_underscore_declarations() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
builder.cells._helper = Pattern()
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
assert "_helper" in builder
|
||||||
|
with pytest.raises(BuildError, match="write-only"):
|
||||||
|
_value = builder.cells._helper
|
||||||
|
report = builder.validate()
|
||||||
|
assert report.provenance["_helper"].kind == "declared"
|
||||||
|
|
||||||
|
del builder.cells._helper
|
||||||
|
assert "_helper" not in builder
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_cells_view_builds_underscore_declaration() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder.cells._helper = Pattern()
|
||||||
|
|
||||||
|
built, _report = builder.build(output="library")
|
||||||
|
|
||||||
|
assert "_helper" in built
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_tree_merge_rejects_single_cell_cycle() -> None:
|
||||||
|
tree = Library({"loop": Pattern()})
|
||||||
|
tree["loop"].ref("loop")
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
|
with pytest.raises(LibraryError, match="exactly one topcell"):
|
||||||
|
builder << tree
|
||||||
|
|
||||||
|
assert not builder
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
tree = Library({"_helper": Pattern()})
|
tree = Library({"_helper": Pattern()})
|
||||||
name_a = lib << tree
|
name_a = lib << tree
|
||||||
name_b = lib << tree
|
name_b = lib << tree
|
||||||
|
|
@ -74,7 +177,7 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
|
||||||
top.ref(name_b)
|
top.ref(name_b)
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
_built, report = builder.build()
|
_built, report = builder.build()
|
||||||
|
|
||||||
helpers = [
|
helpers = [
|
||||||
|
|
@ -88,7 +191,7 @@ def test_build_library_tracks_helper_provenance_and_tree_merge_renames() -> None
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_authoring_tree_merge_renames_repeated_single_use_names() -> None:
|
def test_build_library_authoring_tree_merge_renames_repeated_single_use_names() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
tree = Library({"_helper": Pattern()})
|
tree = Library({"_helper": Pattern()})
|
||||||
|
|
||||||
name_a = builder << tree
|
name_a = builder << tree
|
||||||
|
|
@ -103,7 +206,7 @@ def test_build_library_authoring_tree_merge_renames_repeated_single_use_names()
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_authoring_tree_merge_remaps_internal_refs() -> None:
|
def test_build_library_authoring_tree_merge_remaps_internal_refs() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["_helper"] = Pattern()
|
builder["_helper"] = Pattern()
|
||||||
helper = Pattern()
|
helper = Pattern()
|
||||||
top = Pattern()
|
top = Pattern()
|
||||||
|
|
@ -117,12 +220,14 @@ def test_build_library_authoring_tree_merge_remaps_internal_refs() -> None:
|
||||||
assert any(name != "_helper" for name in built[top_name].refs)
|
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:
|
def test_library_builder_is_not_a_readable_library_and_freezes_after_build() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["leaf"] = Pattern()
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
with pytest.raises(BuildError, match="validate\\(\\) or build\\(\\)"):
|
assert isinstance(builder, INameView)
|
||||||
_ = builder["leaf"]
|
assert not isinstance(builder, ILibraryView)
|
||||||
|
with pytest.raises(TypeError, match="not subscriptable"):
|
||||||
|
_ = builder["leaf"] # type: ignore[index]
|
||||||
|
|
||||||
with pytest.raises(BuildError, match="write-only"):
|
with pytest.raises(BuildError, match="write-only"):
|
||||||
_ = builder.cells.leaf
|
_ = builder.cells.leaf
|
||||||
|
|
@ -139,15 +244,15 @@ def test_build_library_requires_build_session_for_reads_and_freezes_after_build(
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_validate_is_retryable_after_failure() -> None:
|
def test_build_library_validate_is_retryable_after_failure() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
def make_parent(lib: BuildLibrary) -> Pattern:
|
def make_parent(lib: ILibrary) -> Pattern:
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
pat.ref("child")
|
pat.ref("child")
|
||||||
lib.abstract("child")
|
lib.abstract("child")
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
builder.cells.parent = cell(make_parent)(builder)
|
builder.cells.parent = cell(make_parent)(builder.library)
|
||||||
|
|
||||||
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
with pytest.raises(BuildError, match='Failed while building declared cell "parent"'):
|
||||||
builder.validate()
|
builder.validate()
|
||||||
|
|
@ -159,7 +264,7 @@ def test_build_library_validate_is_retryable_after_failure() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["child"] = Pattern()
|
builder["child"] = Pattern()
|
||||||
|
|
||||||
def make_parent() -> Pattern:
|
def make_parent() -> Pattern:
|
||||||
|
|
@ -174,8 +279,64 @@ def test_build_library_depends_on_supports_hidden_dependencies_for_partial_valid
|
||||||
assert report.dependency_graph["parent"] == frozenset({"child"})
|
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_validate_accepts_single_string_and_deduplicates_roots() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder["top"] = Pattern()
|
||||||
|
|
||||||
|
single = builder.validate(names="top")
|
||||||
|
duplicate = builder.validate(names=("top", "top"))
|
||||||
|
|
||||||
|
assert single.requested_roots == ("top",)
|
||||||
|
assert duplicate.requested_roots == ("top",)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_validate_rejects_non_string_roots() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder["top"] = Pattern()
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="roots must be strings"):
|
||||||
|
builder.validate(names=("top", 1)) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("operation", ["build", "validate"])
|
||||||
|
def test_build_library_rejects_same_builder_reentrancy(operation: str) -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
def make_top() -> Pattern:
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
if operation == "build":
|
||||||
|
builder.build()
|
||||||
|
else:
|
||||||
|
builder.validate(names=())
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)()
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="recursively"):
|
||||||
|
builder.build()
|
||||||
|
assert calls == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_allows_nested_build_of_different_builder() -> None:
|
||||||
|
inner = LibraryBuilder()
|
||||||
|
inner["leaf"] = Pattern()
|
||||||
|
outer = LibraryBuilder()
|
||||||
|
|
||||||
|
def make_top() -> Pattern:
|
||||||
|
built, _report = inner.build(output="library")
|
||||||
|
assert "leaf" in built
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
outer.cells.top = cell(make_top)()
|
||||||
|
built, _report = outer.build(output="library")
|
||||||
|
|
||||||
|
assert "top" in built
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_validate_rejects_removed_output_argument() -> None:
|
def test_build_library_validate_rejects_removed_output_argument() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["leaf"] = Pattern()
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
|
@ -183,7 +344,7 @@ def test_build_library_validate_rejects_removed_output_argument() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_unknown_build_output_mode() -> None:
|
def test_build_library_rejects_unknown_build_output_mode() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["leaf"] = Pattern()
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Unknown build output mode"):
|
with pytest.raises(ValueError, match="Unknown build output mode"):
|
||||||
|
|
@ -191,10 +352,10 @@ def test_build_library_rejects_unknown_build_output_mode() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_allows_helper_writes_via_pather() -> None:
|
def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
builder["leaf"] = Pattern(ports={"a": Port((0, 0), 0)})
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
helper = Pather(library=lib, ports="leaf", name="_route")
|
helper = Pather(library=lib, ports="leaf", name="_route")
|
||||||
top = Pattern()
|
top = Pattern()
|
||||||
top.ref("_route")
|
top.ref("_route")
|
||||||
|
|
@ -202,7 +363,7 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||||
top.ports.update(helper.pattern.ports)
|
top.ports.update(helper.pattern.ports)
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
_built, report = builder.build()
|
_built, report = builder.build()
|
||||||
|
|
||||||
helper_prov = report.provenance["_route"]
|
helper_prov = report.provenance["_route"]
|
||||||
|
|
@ -211,11 +372,11 @@ def test_build_library_allows_helper_writes_via_pather() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_contains_tracks_active_session_names() -> None:
|
def test_build_library_contains_tracks_active_session_names() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["leaf"] = Pattern()
|
builder["leaf"] = Pattern()
|
||||||
builder.add_source(Library({"src": Pattern()}))
|
builder.add_source(Library({"src": Pattern()}))
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
assert "leaf" in lib
|
assert "leaf" in lib
|
||||||
assert "src" in lib
|
assert "src" in lib
|
||||||
assert "_helper" not in lib
|
assert "_helper" not in lib
|
||||||
|
|
@ -223,7 +384,7 @@ def test_build_library_contains_tracks_active_session_names() -> None:
|
||||||
assert "_helper" in lib
|
assert "_helper" in lib
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
built, _report = builder.build()
|
built, _report = builder.build()
|
||||||
|
|
||||||
assert "_helper" in built
|
assert "_helper" in built
|
||||||
|
|
@ -231,7 +392,7 @@ def test_build_library_contains_tracks_active_session_names() -> None:
|
||||||
|
|
||||||
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
|
def test_build_library_preserves_source_cells_and_records_source_provenance() -> None:
|
||||||
source = Library({"src": Pattern()})
|
source = Library({"src": Pattern()})
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder.add_source(source)
|
builder.add_source(source)
|
||||||
builder.cells.top = cell(lambda: Pattern())()
|
builder.cells.top = cell(lambda: Pattern())()
|
||||||
|
|
||||||
|
|
@ -241,6 +402,18 @@ def test_build_library_preserves_source_cells_and_records_source_provenance() ->
|
||||||
assert report.provenance["src"].kind == "source"
|
assert report.provenance["src"].kind == "source"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_library_add_reserves_all_planned_names() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder["_shape$A"] = Pattern()
|
||||||
|
builder["_shape$B"] = Pattern()
|
||||||
|
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||||
|
|
||||||
|
rename_map = builder.add(source)
|
||||||
|
|
||||||
|
assert len(set(rename_map.values())) == 2
|
||||||
|
assert set(rename_map.values()) <= set(builder)
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
||||||
source = Library()
|
source = Library()
|
||||||
source["child"] = Pattern()
|
source["child"] = Pattern()
|
||||||
|
|
@ -248,7 +421,7 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
||||||
parent.ref("child")
|
parent.ref("child")
|
||||||
source["parent"] = parent
|
source["parent"] = parent
|
||||||
|
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
rename_map = builder.add_source(
|
rename_map = builder.add_source(
|
||||||
source,
|
source,
|
||||||
rename_theirs=lambda _lib, name: f"mapped_{name}",
|
rename_theirs=lambda _lib, name: f"mapped_{name}",
|
||||||
|
|
@ -264,7 +437,7 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None:
|
||||||
assert report.provenance["mapped_child"].requested_name == "child"
|
assert report.provenance["mapped_child"].requested_name == "child"
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_authoring_tree_merge_keeps_source_view_lazy() -> None:
|
def test_library_builder_adds_an_ordinary_view_eagerly() -> None:
|
||||||
child = Pattern()
|
child = Pattern()
|
||||||
top = Pattern()
|
top = Pattern()
|
||||||
top.ref("child")
|
top.ref("child")
|
||||||
|
|
@ -273,16 +446,16 @@ def test_build_library_authoring_tree_merge_keeps_source_view_lazy() -> None:
|
||||||
{"child": set(), "top": {"child"}},
|
{"child": set(), "top": {"child"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
top_name = builder << source
|
top_name = builder << source
|
||||||
built, _report = builder.build()
|
built, _report = builder.build()
|
||||||
|
|
||||||
assert top_name == "top"
|
assert top_name == "top"
|
||||||
assert "top" in built
|
assert "top" in built
|
||||||
assert source.loads == 0
|
assert source.loads == 2
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_authoring_source_tree_merge_returns_renamed_top() -> None:
|
def test_library_builder_adds_and_renames_an_ordinary_view_eagerly() -> None:
|
||||||
existing = Pattern()
|
existing = Pattern()
|
||||||
source_top = Pattern()
|
source_top = Pattern()
|
||||||
source = _MetadataSource(
|
source = _MetadataSource(
|
||||||
|
|
@ -290,14 +463,41 @@ def test_build_library_authoring_source_tree_merge_returns_renamed_top() -> None
|
||||||
{"_helper": set()},
|
{"_helper": set()},
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["_helper"] = existing
|
builder["_helper"] = existing
|
||||||
top_name = builder << source
|
top_name = builder << source
|
||||||
built, _report = builder.build()
|
built, _report = builder.build()
|
||||||
|
|
||||||
assert top_name != "_helper"
|
assert top_name != "_helper"
|
||||||
assert top_name in built
|
assert top_name in built
|
||||||
|
assert source.loads == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_add_borrows_materializable_views() -> None:
|
||||||
|
source = _MaterializableMetadataSource(
|
||||||
|
{"child": Pattern(), "top": Pattern()},
|
||||||
|
{"child": set(), "top": {"child"}},
|
||||||
|
)
|
||||||
|
source.mapping["top"].ref("child")
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
|
builder.add(source)
|
||||||
|
built, _report = builder.build()
|
||||||
|
|
||||||
assert source.loads == 0
|
assert source.loads == 0
|
||||||
|
assert set(built) == {"child", "top"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_view_wrapper_intentionally_erases_materializable_marker() -> None:
|
||||||
|
source = _MaterializableMetadataSource(
|
||||||
|
{"top": Pattern()},
|
||||||
|
{"top": set()},
|
||||||
|
)
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
|
builder.add(LibraryView(source))
|
||||||
|
|
||||||
|
assert source.loads == 1
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_materialization() -> None:
|
def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_materialization() -> None:
|
||||||
|
|
@ -309,7 +509,7 @@ def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_mater
|
||||||
{"_helper": set(), "top": {"_helper"}},
|
{"_helper": set(), "top": {"_helper"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder["_helper"] = Pattern()
|
builder["_helper"] = Pattern()
|
||||||
top_name = builder << source
|
top_name = builder << source
|
||||||
built, _report = builder.build(output="library")
|
built, _report = builder.build(output="library")
|
||||||
|
|
@ -319,18 +519,19 @@ def test_build_library_authoring_source_tree_merge_remaps_renamed_child_on_mater
|
||||||
assert source.loads == 2
|
assert source.loads == 2
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_authoring_tree_le_before_mutating() -> None:
|
def test_library_builder_does_not_expose_library_hierarchy_operations() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
with pytest.raises(BuildError, match="__le__"):
|
with pytest.raises(TypeError):
|
||||||
_abstract = builder <= Library({"leaf": Pattern()})
|
_abstract = builder <= Library({"leaf": Pattern()})
|
||||||
|
assert not hasattr(builder, "abstract")
|
||||||
assert list(builder) == []
|
assert not hasattr(builder, "resolve")
|
||||||
|
assert not hasattr(builder, "rename")
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
||||||
source = Library({"src": Pattern()})
|
source = Library({"src": Pattern()})
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder.add_source(source)
|
builder.add_source(source)
|
||||||
source["late"] = Pattern()
|
source["late"] = Pattern()
|
||||||
|
|
||||||
|
|
@ -340,7 +541,7 @@ def test_build_library_rejects_source_cells_added_after_add_source() -> None:
|
||||||
|
|
||||||
def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
||||||
source = Library({"src": Pattern()})
|
source = Library({"src": Pattern()})
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
builder.add_source(source)
|
builder.add_source(source)
|
||||||
del source["src"]
|
del source["src"]
|
||||||
|
|
||||||
|
|
@ -349,45 +550,29 @@ def test_build_library_rejects_source_cells_removed_after_add_source() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_add_source_during_build() -> None:
|
def test_build_library_rejects_add_source_during_build() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top() -> Pattern:
|
||||||
lib.add_source(Library({"src": Pattern()}))
|
builder.add_source(Library({"src": Pattern()}))
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)()
|
||||||
|
|
||||||
with pytest.raises(BuildError, match="add_source"):
|
with pytest.raises(BuildError, match="Cannot modify"):
|
||||||
builder.build()
|
builder.build()
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_renaming_imported_source_cells_during_authoring() -> None:
|
|
||||||
builder = BuildLibrary()
|
|
||||||
builder.add_source(Library({"src": Pattern()}))
|
|
||||||
|
|
||||||
with pytest.raises(BuildError, match="add_source"):
|
|
||||||
builder.rename("src", "renamed_src", move_references=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_renaming_declared_cells_during_authoring() -> None:
|
|
||||||
builder = BuildLibrary()
|
|
||||||
builder["declared"] = Pattern()
|
|
||||||
|
|
||||||
with pytest.raises(BuildError, match='Cannot rename declared build cell "declared"'):
|
|
||||||
builder.rename("declared", "renamed_declared")
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
lib["_helper"] = Pattern()
|
lib["_helper"] = Pattern()
|
||||||
lib.rename("_helper", "final_helper")
|
lib.rename("_helper", "final_helper")
|
||||||
top = Pattern()
|
top = Pattern()
|
||||||
top.ref("final_helper")
|
top.ref("final_helper")
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
built, report = builder.build()
|
built, report = builder.build()
|
||||||
|
|
||||||
assert "final_helper" in built
|
assert "final_helper" in built
|
||||||
|
|
@ -401,14 +586,14 @@ def test_build_library_helper_rename_updates_provenance_owner() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
lib["_helper"] = Pattern()
|
lib["_helper"] = Pattern()
|
||||||
del lib["_helper"]
|
del lib["_helper"]
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
built, report = builder.build()
|
built, report = builder.build()
|
||||||
|
|
||||||
assert "_helper" not in built
|
assert "_helper" not in built
|
||||||
|
|
@ -417,9 +602,9 @@ def test_build_library_helper_delete_removes_provenance_and_ownership() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
def test_build_library_helper_rename_after_auto_rename_preserves_requested_name() -> None:
|
||||||
builder = BuildLibrary()
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
def make_top(lib: BuildLibrary) -> Pattern:
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
tree = Library({"_helper": Pattern()})
|
tree = Library({"_helper": Pattern()})
|
||||||
_ = lib << tree
|
_ = lib << tree
|
||||||
renamed = lib << tree
|
renamed = lib << tree
|
||||||
|
|
@ -429,7 +614,7 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
|
||||||
top.ref("final_helper")
|
top.ref("final_helper")
|
||||||
return top
|
return top
|
||||||
|
|
||||||
builder.cells.top = cell(make_top)(builder)
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
built, report = builder.build()
|
built, report = builder.build()
|
||||||
|
|
||||||
assert "final_helper" in built
|
assert "final_helper" in built
|
||||||
|
|
@ -438,48 +623,158 @@ def test_build_library_helper_rename_after_auto_rename_preserves_requested_name(
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
|
def test_build_library_rejects_renaming_declared_or_source_cells_during_build() -> None:
|
||||||
declared = BuildLibrary()
|
declared = LibraryBuilder()
|
||||||
declared["leaf"] = Pattern()
|
declared["leaf"] = Pattern()
|
||||||
|
|
||||||
def rename_declared(lib: BuildLibrary) -> Pattern:
|
def rename_declared(lib: ILibrary) -> Pattern:
|
||||||
lib.rename("leaf", "renamed_leaf")
|
lib.rename("leaf", "renamed_leaf")
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
declared.cells.top = cell(rename_declared)(declared)
|
declared.cells.top = cell(rename_declared)(declared.library)
|
||||||
with pytest.raises(BuildError, match='Cannot rename declared build cell "leaf"'):
|
with pytest.raises(BuildError, match='Cannot rename declared build cell "leaf"'):
|
||||||
declared.build()
|
declared.build()
|
||||||
|
|
||||||
source = BuildLibrary()
|
source = LibraryBuilder()
|
||||||
source.add_source(Library({"src": Pattern()}))
|
source.add_source(Library({"src": Pattern()}))
|
||||||
|
|
||||||
def rename_source(lib: BuildLibrary) -> Pattern:
|
def rename_source(lib: ILibrary) -> Pattern:
|
||||||
lib.rename("src", "renamed_src")
|
lib.rename("src", "renamed_src")
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
source.cells.top = cell(rename_source)(source)
|
source.cells.top = cell(rename_source)(source.library)
|
||||||
with pytest.raises(BuildError, match='Cannot rename imported source cell "src"'):
|
with pytest.raises(BuildError, match='Cannot rename imported source cell "src"'):
|
||||||
source.build()
|
source.build()
|
||||||
|
|
||||||
|
|
||||||
def test_build_library_rejects_deleting_declared_or_source_cells_during_build() -> None:
|
def test_build_library_rejects_deleting_declared_or_source_cells_during_build() -> None:
|
||||||
declared = BuildLibrary()
|
declared = LibraryBuilder()
|
||||||
declared["leaf"] = Pattern()
|
declared["leaf"] = Pattern()
|
||||||
|
|
||||||
def delete_declared(lib: BuildLibrary) -> Pattern:
|
def delete_declared(lib: ILibrary) -> Pattern:
|
||||||
del lib["leaf"]
|
del lib["leaf"]
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
declared.cells.top = cell(delete_declared)(declared)
|
declared.cells.top = cell(delete_declared)(declared.library)
|
||||||
with pytest.raises(BuildError, match='Cannot delete declared build cell "leaf"'):
|
with pytest.raises(BuildError, match='Cannot delete declared build cell "leaf"'):
|
||||||
declared.build()
|
declared.build()
|
||||||
|
|
||||||
source = BuildLibrary()
|
source = LibraryBuilder()
|
||||||
source.add_source(Library({"src": Pattern()}))
|
source.add_source(Library({"src": Pattern()}))
|
||||||
|
|
||||||
def delete_source(lib: BuildLibrary) -> Pattern:
|
def delete_source(lib: ILibrary) -> Pattern:
|
||||||
del lib["src"]
|
del lib["src"]
|
||||||
return Pattern()
|
return Pattern()
|
||||||
|
|
||||||
source.cells.top = cell(delete_source)(source)
|
source.cells.top = cell(delete_source)(source.library)
|
||||||
with pytest.raises(BuildError, match='Cannot delete imported source cell "src"'):
|
with pytest.raises(BuildError, match='Cannot delete imported source cell "src"'):
|
||||||
source.build()
|
source.build()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_replaces_its_library_placeholder_in_direct_recipe_arguments() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder["leaf"] = Pattern()
|
||||||
|
|
||||||
|
def make_top(positional: ILibrary, *, keyword: ILibrary) -> Pattern:
|
||||||
|
assert positional is keyword
|
||||||
|
assert positional.abstract("leaf").name == "leaf"
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder.library, keyword=builder.library)
|
||||||
|
built, _report = builder.build(output="library")
|
||||||
|
|
||||||
|
assert "top" in built
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_library_placeholder_is_read_only() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
placeholder = builder.library
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
builder.library = object() # type: ignore[misc]
|
||||||
|
|
||||||
|
assert builder.library is placeholder
|
||||||
|
builder.cells.top = cell(lambda _lib: Pattern())(builder.library)
|
||||||
|
built, _report = builder.build(output="library")
|
||||||
|
assert "top" in built
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_rejects_another_builders_direct_placeholder() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
other = LibraryBuilder()
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match="another LibraryBuilder"):
|
||||||
|
builder.cells.top = cell(lambda _lib: Pattern())(other.library)
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_does_not_substitute_nested_placeholders() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
|
||||||
|
def make_top(values: tuple[object, ...]) -> Pattern:
|
||||||
|
assert values == (builder.library,)
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)((builder.library,))
|
||||||
|
builder.build()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_keeps_context_free_recipes_and_name_queries() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder.cells.top = cell(Pattern)()
|
||||||
|
|
||||||
|
assert tuple(builder.keys()) == ("top",)
|
||||||
|
assert builder.get_name("top") != "top"
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_builder_uses_library_name_allocation() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
builder["cell_name"] = Pattern()
|
||||||
|
library = Library({"cell_name": Pattern()})
|
||||||
|
|
||||||
|
requests = (
|
||||||
|
("cell name", True, 32),
|
||||||
|
("a name that needs truncation", True, 12),
|
||||||
|
("unsanitized name", False, 32),
|
||||||
|
("", True, 32),
|
||||||
|
)
|
||||||
|
for name, sanitize, max_length in requests:
|
||||||
|
assert builder.get_name(name, sanitize=sanitize, max_length=max_length) == library.get_name(
|
||||||
|
name,
|
||||||
|
sanitize=sanitize,
|
||||||
|
max_length=max_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_session_subtree_preserves_type_and_builds_deferred_dependencies() -> None:
|
||||||
|
builder = LibraryBuilder()
|
||||||
|
child_calls = 0
|
||||||
|
|
||||||
|
def make_top(lib: ILibrary) -> Pattern:
|
||||||
|
subtree = lib.subtree("child")
|
||||||
|
assert type(subtree) is type(lib)
|
||||||
|
assert set(subtree) == {"child", "leaf"}
|
||||||
|
assert subtree["child"] is lib["child"]
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
_ = subtree["unrelated"]
|
||||||
|
subtree["_local"] = Pattern()
|
||||||
|
assert "_local" not in lib
|
||||||
|
|
||||||
|
top = Pattern()
|
||||||
|
top.ref("child")
|
||||||
|
return top
|
||||||
|
|
||||||
|
def make_child() -> Pattern:
|
||||||
|
nonlocal child_calls
|
||||||
|
child_calls += 1
|
||||||
|
child = Pattern()
|
||||||
|
child.ref("leaf")
|
||||||
|
return child
|
||||||
|
|
||||||
|
builder.cells.top = cell(make_top)(builder.library)
|
||||||
|
builder.cells.child = cell(make_child)()
|
||||||
|
builder.cells.leaf = Pattern()
|
||||||
|
builder.cells.unrelated = Pattern()
|
||||||
|
|
||||||
|
built, _report = builder.build(output="library")
|
||||||
|
|
||||||
|
assert child_calls == 1
|
||||||
|
assert set(built) == {"top", "child", "leaf", "unrelated"}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import io
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -6,9 +7,10 @@ from numpy.testing import assert_allclose
|
||||||
|
|
||||||
from ..file import gdsii
|
from ..file import gdsii
|
||||||
from ..file.gdsii import lazy as gdsii_lazy
|
from ..file.gdsii import lazy as gdsii_lazy
|
||||||
|
from ..error import LibraryError
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
from ..library import Library, OverlayLibrary
|
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
|
||||||
|
|
||||||
|
|
||||||
def _make_lazy_port_library() -> Library:
|
def _make_lazy_port_library() -> Library:
|
||||||
|
|
@ -29,6 +31,14 @@ def _make_lazy_port_library() -> Library:
|
||||||
return lib
|
return lib
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_write_ignores_non_mapping_library_info() -> None:
|
||||||
|
lib = Library({'top': Pattern()})
|
||||||
|
lib.library_info = None # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
with pytest.raises(LibraryError, match='required for non-GDS-backed lazy writes'):
|
||||||
|
gdsii_lazy.write(lib, io.BytesIO())
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_source.gds'
|
gds_file = tmp_path / 'lazy_source.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
@ -43,13 +53,74 @@ def test_gdsii_lazy_source_exposes_order_and_graph_without_materializing(tmp_pat
|
||||||
'child': {'leaf'},
|
'child': {'leaf'},
|
||||||
'top': {'child'},
|
'top': {'child'},
|
||||||
}
|
}
|
||||||
|
assert lib.parent_graph() == {
|
||||||
|
'leaf': {'child'},
|
||||||
|
'child': {'top'},
|
||||||
|
'top': set(),
|
||||||
|
}
|
||||||
|
assert lib.tops() == ['top']
|
||||||
|
global_refs = lib.find_refs_global('leaf')
|
||||||
|
assert_allclose(global_refs[('top', 'child', 'leaf')], [[110, 220, numpy.pi / 2, 0, 1]])
|
||||||
assert not lib._cache
|
assert not lib._cache
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='dangling-reference mode'):
|
||||||
|
lib.child_graph(dangling='typo')
|
||||||
|
with pytest.raises(ValueError, match='dangling-reference mode'):
|
||||||
|
lib.find_refs_local('leaf', dangling='typo')
|
||||||
|
|
||||||
child = lib['child']
|
child = lib['child']
|
||||||
assert list(child.refs.keys()) == ['leaf']
|
assert list(child.refs.keys()) == ['leaf']
|
||||||
assert set(lib._cache) == {'child'}
|
assert set(lib._cache) == {'child'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_subtree_stays_borrowed_and_preserves_write_metadata(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'lazy_subtree_source.gds'
|
||||||
|
src = _make_lazy_port_library()
|
||||||
|
src['unused'] = Pattern()
|
||||||
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-subtree')
|
||||||
|
|
||||||
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
|
subtree = raw.subtree('top')
|
||||||
|
|
||||||
|
assert isinstance(raw, IMaterializable)
|
||||||
|
assert not isinstance(raw, IBorrowing)
|
||||||
|
assert isinstance(subtree, IMaterializable)
|
||||||
|
assert isinstance(subtree, IBorrowing)
|
||||||
|
assert subtree.source_order() == ('leaf', 'child', 'top')
|
||||||
|
assert not raw._cache
|
||||||
|
|
||||||
|
out_file = tmp_path / 'lazy_subtree_out.gds'
|
||||||
|
gdsii_lazy.writefile(subtree, out_file)
|
||||||
|
assert not raw._cache
|
||||||
|
|
||||||
|
roundtrip, info = gdsii.readfile(out_file)
|
||||||
|
assert info['name'] == 'classic-subtree'
|
||||||
|
assert set(roundtrip) == {'leaf', 'child', 'top'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_overlay_subtree_preserves_source_laziness(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'lazy_overlay_subtree_source.gds'
|
||||||
|
src = _make_lazy_port_library()
|
||||||
|
src['unused'] = Pattern()
|
||||||
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='overlay-subtree')
|
||||||
|
|
||||||
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(raw)
|
||||||
|
subtree = overlay.subtree('top')
|
||||||
|
|
||||||
|
assert isinstance(subtree, OverlayLibrary)
|
||||||
|
assert subtree.borrowed_sources() == (raw,)
|
||||||
|
|
||||||
|
out_file = tmp_path / 'lazy_overlay_subtree_out.gds'
|
||||||
|
gdsii_lazy.writefile(subtree, out_file)
|
||||||
|
|
||||||
|
assert not raw._cache
|
||||||
|
roundtrip, info = gdsii.readfile(out_file)
|
||||||
|
assert info['name'] == 'overlay-subtree'
|
||||||
|
assert set(roundtrip) == {'leaf', 'child', 'top'}
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> None:
|
def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_ports.gds'
|
gds_file = tmp_path / 'lazy_ports.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
@ -67,6 +138,28 @@ def test_gdsii_lazy_ports_view_keeps_raw_source_unmodified(tmp_path: Path) -> No
|
||||||
assert not raw_top.ports
|
assert not raw_top.ports
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_ports_view_detaches_previously_cached_source(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'lazy_cached_ports.gds'
|
||||||
|
src = _make_lazy_port_library()
|
||||||
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='classic-cached-ports')
|
||||||
|
|
||||||
|
raw, _ = gdsii_lazy.readfile(gds_file)
|
||||||
|
raw_top = raw['top']
|
||||||
|
processed = raw.with_port_overrides({
|
||||||
|
'top': {
|
||||||
|
'P': Port((1, 2), rotation=0, ptype='wire'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
processed_top = processed['top']
|
||||||
|
|
||||||
|
assert processed_top is not raw_top
|
||||||
|
assert not raw_top.ports
|
||||||
|
assert set(processed_top.ports) == {'P'}
|
||||||
|
assert raw['top'] is raw_top
|
||||||
|
assert not hasattr(processed, 'close')
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_port_overrides_without_data_stay_lazy(tmp_path: Path) -> None:
|
def test_gdsii_lazy_port_overrides_without_data_stay_lazy(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'lazy_port_overrides.gds'
|
gds_file = tmp_path / 'lazy_port_overrides.gds'
|
||||||
src = _make_lazy_port_library()
|
src = _make_lazy_port_library()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import pytest
|
||||||
pytest.importorskip('pyarrow')
|
pytest.importorskip('pyarrow')
|
||||||
|
|
||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
from ..library import Library, OverlayLibrary
|
from ..library import IBorrowing, IMaterializable, Library, OverlayLibrary
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..repetition import Grid
|
from ..repetition import Grid
|
||||||
from ..file import gdsii
|
from ..file import gdsii
|
||||||
|
|
@ -223,6 +223,72 @@ def test_gdsii_lazy_arrow_untouched_write_is_copy_through(tmp_path: Path) -> Non
|
||||||
assert out_file.read_bytes() == gds_file.read_bytes()
|
assert out_file.read_bytes() == gds_file.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_arrow_processed_cell_edit_disables_raw_copy(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'processed_edit_source.gds'
|
||||||
|
src = _make_small_library()
|
||||||
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='processed-edit')
|
||||||
|
|
||||||
|
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||||
|
processed = raw.with_port_overrides({})
|
||||||
|
processed['top'].polygon((7, 0), vertices=[[0, 0], [4, 0], [0, 4]])
|
||||||
|
|
||||||
|
out_file = tmp_path / 'processed_edit_out.gds'
|
||||||
|
gdsii_lazy_arrow.writefile(processed, out_file)
|
||||||
|
|
||||||
|
roundtrip, _ = gdsii.readfile(out_file)
|
||||||
|
assert len(roundtrip['top'].shapes[(7, 0)]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_arrow_subtree_preserves_raw_copy_and_ref_queries(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'subtree_copy_source.gds'
|
||||||
|
src = _make_small_library()
|
||||||
|
src['unused'] = Pattern()
|
||||||
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='subtree-copy')
|
||||||
|
|
||||||
|
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||||
|
subtree = raw.subtree('top')
|
||||||
|
|
||||||
|
assert isinstance(raw, IMaterializable)
|
||||||
|
assert not isinstance(raw, IBorrowing)
|
||||||
|
assert isinstance(subtree, IMaterializable)
|
||||||
|
assert isinstance(subtree, IBorrowing)
|
||||||
|
assert subtree.source_order() == ('leaf', 'mid', 'top')
|
||||||
|
assert _global_refs_key(subtree.find_refs_global('leaf')) == _global_refs_key(raw.find_refs_global('leaf'))
|
||||||
|
assert not raw._cache
|
||||||
|
|
||||||
|
out_file = tmp_path / 'subtree_copy_out.gds'
|
||||||
|
gdsii_lazy_arrow.writefile(subtree, out_file)
|
||||||
|
|
||||||
|
assert not raw._cache
|
||||||
|
roundtrip, info = gdsii.readfile(out_file)
|
||||||
|
assert info['name'] == 'subtree-copy'
|
||||||
|
assert set(roundtrip) == {'leaf', 'mid', 'top'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_gdsii_lazy_arrow_overlay_subtree_preserves_raw_copy(tmp_path: Path) -> None:
|
||||||
|
gds_file = tmp_path / 'overlay_subtree_source.gds'
|
||||||
|
src = _make_small_library()
|
||||||
|
src['unused'] = Pattern()
|
||||||
|
gdsii.writefile(src, gds_file, meters_per_unit=1e-9, library_name='overlay-subtree-copy')
|
||||||
|
|
||||||
|
raw, _ = gdsii_lazy_arrow.readfile(gds_file)
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(raw)
|
||||||
|
subtree = overlay.subtree('top')
|
||||||
|
|
||||||
|
assert isinstance(subtree, OverlayLibrary)
|
||||||
|
assert subtree.borrowed_sources() == (raw,)
|
||||||
|
assert not raw._cache
|
||||||
|
|
||||||
|
out_file = tmp_path / 'overlay_subtree_out.gds'
|
||||||
|
gdsii_lazy_arrow.writefile(subtree, out_file)
|
||||||
|
|
||||||
|
assert not raw._cache
|
||||||
|
roundtrip, info = gdsii.readfile(out_file)
|
||||||
|
assert info['name'] == 'overlay-subtree-copy'
|
||||||
|
assert set(roundtrip) == {'leaf', 'mid', 'top'}
|
||||||
|
|
||||||
|
|
||||||
def test_gdsii_lazy_arrow_gzipped_copy_through(tmp_path: Path) -> None:
|
def test_gdsii_lazy_arrow_gzipped_copy_through(tmp_path: Path) -> None:
|
||||||
gds_file = tmp_path / 'copy_source.gds.gz'
|
gds_file = tmp_path / 'copy_source.gds.gz'
|
||||||
src = _make_small_library()
|
src = _make_small_library()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from collections.abc import Iterator, Mapping, MutableMapping
|
||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from numpy.testing import assert_allclose
|
from numpy.testing import assert_allclose
|
||||||
from ..library import Library, LazyLibrary
|
from ..library import IBorrowing, INameView, IMaterializable, ILibraryView, Library, LibraryView, LazyLibrary, OverlayLibrary, PortsLibraryView
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..error import LibraryError, PatternError
|
from ..error import LibraryError, PatternError
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
|
|
@ -18,6 +19,7 @@ def test_library_basic() -> None:
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
lib["cell1"] = pat
|
lib["cell1"] = pat
|
||||||
|
|
||||||
|
assert isinstance(lib, INameView)
|
||||||
assert "cell1" in lib
|
assert "cell1" in lib
|
||||||
assert lib["cell1"] is pat
|
assert lib["cell1"] is pat
|
||||||
assert len(lib) == 1
|
assert len(lib) == 1
|
||||||
|
|
@ -26,6 +28,22 @@ def test_library_basic() -> None:
|
||||||
lib["cell1"] = Pattern() # Overwriting not allowed
|
lib["cell1"] = Pattern() # Overwriting not allowed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary, OverlayLibrary])
|
||||||
|
def test_writable_libraries_are_restricted_mappings(
|
||||||
|
library_cls: type[Library] | type[LazyLibrary] | type[OverlayLibrary],
|
||||||
|
) -> None:
|
||||||
|
lib = library_cls()
|
||||||
|
|
||||||
|
assert isinstance(lib, Mapping)
|
||||||
|
assert not isinstance(lib, MutableMapping)
|
||||||
|
for method in ("update", "setdefault", "pop", "popitem", "clear"):
|
||||||
|
assert not hasattr(lib, method)
|
||||||
|
|
||||||
|
lib["top"] = Pattern()
|
||||||
|
del lib["top"]
|
||||||
|
assert not lib
|
||||||
|
|
||||||
|
|
||||||
def test_library_tops() -> None:
|
def test_library_tops() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["child"] = Pattern()
|
lib["child"] = Pattern()
|
||||||
|
|
@ -36,6 +54,23 @@ def test_library_tops() -> None:
|
||||||
assert lib.top() == "parent"
|
assert lib.top() == "parent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_ref_buckets_do_not_create_hierarchy_edges() -> None:
|
||||||
|
parent = Pattern()
|
||||||
|
parent.refs["ghost"]
|
||||||
|
parent.refs["parent"]
|
||||||
|
lib = Library({"parent": parent, "ghost": Pattern()})
|
||||||
|
|
||||||
|
assert not parent.has_refs()
|
||||||
|
assert parent.referenced_patterns() == set()
|
||||||
|
assert set(lib.tops()) == {"parent", "ghost"}
|
||||||
|
assert lib.child_graph() == {"parent": set(), "ghost": set()}
|
||||||
|
assert lib.parent_graph() == {"parent": set(), "ghost": set()}
|
||||||
|
|
||||||
|
lib.dfs(parent, hierarchy=("parent",))
|
||||||
|
flat = lib.flatten("parent")["parent"]
|
||||||
|
assert not flat.refs
|
||||||
|
|
||||||
|
|
||||||
def test_library_dangling() -> None:
|
def test_library_dangling() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["parent"] = Pattern()
|
lib["parent"] = Pattern()
|
||||||
|
|
@ -44,6 +79,16 @@ def test_library_dangling() -> None:
|
||||||
assert lib.dangling_refs() == {"missing"}
|
assert lib.dangling_refs() == {"missing"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_reachability_ignores_unnamed_refs() -> None:
|
||||||
|
pattern = Pattern()
|
||||||
|
pattern.ref(None)
|
||||||
|
lib = Library({"top": pattern})
|
||||||
|
|
||||||
|
assert pattern.referenced_patterns() == {None}
|
||||||
|
assert lib.referenced_patterns() == set()
|
||||||
|
assert lib.dangling_refs() == set()
|
||||||
|
|
||||||
|
|
||||||
def test_library_dangling_graph_modes() -> None:
|
def test_library_dangling_graph_modes() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["parent"] = Pattern()
|
lib["parent"] = Pattern()
|
||||||
|
|
@ -65,6 +110,24 @@ def test_library_dangling_graph_modes() -> None:
|
||||||
assert lib.child_order(dangling="include") == ["missing", "parent"]
|
assert lib.child_order(dangling="include") == ["missing", "parent"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("method", "args"),
|
||||||
|
[
|
||||||
|
("child_graph", ()),
|
||||||
|
("parent_graph", ()),
|
||||||
|
("child_order", ()),
|
||||||
|
("find_refs_local", ("top",)),
|
||||||
|
("find_refs_global", ("top",)),
|
||||||
|
("prune_empty", ()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_library_rejects_unknown_dangling_mode(method: str, args: tuple[object, ...]) -> None:
|
||||||
|
lib = Library({"top": Pattern()})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="dangling-reference mode"):
|
||||||
|
getattr(lib, method)(*args, dangling="typo")
|
||||||
|
|
||||||
|
|
||||||
def test_find_refs_with_dangling_modes() -> None:
|
def test_find_refs_with_dangling_modes() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["target"] = Pattern()
|
lib["target"] = Pattern()
|
||||||
|
|
@ -97,6 +160,16 @@ def test_find_refs_with_dangling_modes() -> None:
|
||||||
assert_allclose(global_target[("top", "mid", "target")], [[7, 0, 0, 0, 1]])
|
assert_allclose(global_target[("top", "mid", "target")], [[7, 0, 0, 0, 1]])
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_refs_global_includes_composed_scale_column() -> None:
|
||||||
|
lib = Library({"leaf": Pattern(), "child": Pattern(), "top": Pattern()})
|
||||||
|
lib["child"].ref("leaf", scale=2)
|
||||||
|
lib["top"].ref("child", scale=3)
|
||||||
|
|
||||||
|
transforms = lib.find_refs_global("leaf")
|
||||||
|
|
||||||
|
assert_allclose(transforms[("top", "child", "leaf")], [[0, 0, 0, 0, 6]])
|
||||||
|
|
||||||
|
|
||||||
def test_preflight_prune_empty_preserves_dangling_policy(caplog: pytest.LogCaptureFixture) -> None:
|
def test_preflight_prune_empty_preserves_dangling_policy(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
def make_lib() -> Library:
|
def make_lib() -> Library:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
|
|
@ -137,6 +210,42 @@ def test_library_flatten() -> None:
|
||||||
assert tuple(assert_vertices[0]) == (10.0, 10.0)
|
assert tuple(assert_vertices[0]) == (10.0, 10.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pattern_polygon_traversal_ignores_empty_dangling_bucket() -> None:
|
||||||
|
child = Pattern()
|
||||||
|
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
||||||
|
parent = Pattern()
|
||||||
|
parent.ref("child")
|
||||||
|
parent.refs["missing"]
|
||||||
|
lib = Library({"child": child, "parent": parent})
|
||||||
|
|
||||||
|
polygons = parent.layer_as_polygons((1, 0), flatten=True, library=lib)
|
||||||
|
|
||||||
|
assert len(polygons) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_recursive_geometry_rejects_reference_cycles() -> None:
|
||||||
|
lib = Library({"a": Pattern(), "b": Pattern()})
|
||||||
|
lib["a"].ref("b")
|
||||||
|
lib["b"].ref("a")
|
||||||
|
|
||||||
|
with pytest.raises(PatternError, match=r"calculating bounds: .* -> .* ->"):
|
||||||
|
lib["a"].get_bounds(library=lib)
|
||||||
|
with pytest.raises(PatternError, match=r"collecting layer polygons: .* -> .* ->"):
|
||||||
|
lib["a"].layer_as_polygons((1, 0), library=lib)
|
||||||
|
with pytest.raises(PatternError, match=r"visualizing pattern hierarchy: .* -> .* ->"):
|
||||||
|
lib["a"].visualize(library=lib)
|
||||||
|
with pytest.raises(PatternError, match="Circular reference"):
|
||||||
|
lib["a"].deepcopy().flatten(library=lib)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonrecursive_geometry_allows_reference_cycles() -> None:
|
||||||
|
lib = Library({"loop": Pattern()})
|
||||||
|
lib["loop"].ref("loop")
|
||||||
|
|
||||||
|
assert lib["loop"].get_bounds(library=lib, recurse=False) is None
|
||||||
|
assert lib["loop"].layer_as_polygons((1, 0), flatten=False, library=lib) == []
|
||||||
|
|
||||||
|
|
||||||
def test_library_flatten_preserves_ports_only_child() -> None:
|
def test_library_flatten_preserves_ports_only_child() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
child = Pattern(ports={"P1": Port((1, 2), 0)})
|
child = Pattern(ports={"P1": Port((1, 2), 0)})
|
||||||
|
|
@ -207,6 +316,42 @@ def test_lazy_library() -> None:
|
||||||
assert pat is pat2
|
assert pat is pat2
|
||||||
|
|
||||||
|
|
||||||
|
def test_lazy_library_reachability_loads_only_reachable_cells() -> None:
|
||||||
|
lib = LazyLibrary()
|
||||||
|
calls = {"top": 0, "child": 0, "unused": 0}
|
||||||
|
|
||||||
|
def make(name: str, target: str | None = None) -> Pattern:
|
||||||
|
calls[name] += 1
|
||||||
|
pattern = Pattern()
|
||||||
|
if target is not None:
|
||||||
|
pattern.ref(target)
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
lib["top"] = lambda: make("top", "child")
|
||||||
|
lib["child"] = lambda: make("child")
|
||||||
|
lib["unused"] = lambda: make("unused")
|
||||||
|
|
||||||
|
assert lib.referenced_patterns("top") == {"child"}
|
||||||
|
assert calls == {"top": 1, "child": 1, "unused": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_abstract_view_membership_does_not_materialize_lazy_cells() -> None:
|
||||||
|
lib = LazyLibrary()
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
def make_pat() -> Pattern:
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
return Pattern()
|
||||||
|
|
||||||
|
lib["lazy"] = make_pat
|
||||||
|
abstracts = lib.abstract_view()
|
||||||
|
|
||||||
|
assert "lazy" in abstracts
|
||||||
|
assert "missing" not in abstracts
|
||||||
|
assert calls == 0
|
||||||
|
|
||||||
|
|
||||||
def test_library_rename() -> None:
|
def test_library_rename() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["old"] = Pattern()
|
lib["old"] = Pattern()
|
||||||
|
|
@ -221,7 +366,7 @@ def test_library_rename() -> None:
|
||||||
assert "old" not in lib["parent"].refs
|
assert "old" not in lib["parent"].refs
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||||
def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||||
lib = library_cls()
|
lib = library_cls()
|
||||||
lib["top"] = Pattern()
|
lib["top"] = Pattern()
|
||||||
|
|
@ -235,7 +380,7 @@ def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibra
|
||||||
assert len(lib["parent"].refs["top"]) == 1
|
assert len(lib["parent"].refs["top"]) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||||
def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||||
lib = library_cls()
|
lib = library_cls()
|
||||||
lib["top"] = Pattern()
|
lib["top"] = Pattern()
|
||||||
|
|
@ -245,7 +390,7 @@ def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyL
|
||||||
assert list(lib.keys()) == ["top"]
|
assert list(lib.keys()) == ["top"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||||
def test_library_rename_missing_raises_library_error(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
def test_library_rename_missing_raises_library_error(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||||
lib = library_cls()
|
lib = library_cls()
|
||||||
lib["top"] = Pattern()
|
lib["top"] = Pattern()
|
||||||
|
|
@ -254,7 +399,7 @@ def test_library_rename_missing_raises_library_error(library_cls: type[Library]
|
||||||
lib.rename("missing", "new")
|
lib.rename("missing", "new")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("library_cls", (Library, LazyLibrary))
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
||||||
def test_library_move_references_same_target_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
def test_library_move_references_same_target_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
||||||
lib = library_cls()
|
lib = library_cls()
|
||||||
lib["top"] = Pattern()
|
lib["top"] = Pattern()
|
||||||
|
|
@ -338,6 +483,80 @@ def test_library_add_returns_only_renamed_entries() -> None:
|
||||||
assert "keep" not in rename_map
|
assert "keep" not in rename_map
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_add_name_failure_is_atomic() -> None:
|
||||||
|
destination = Library({"x": Pattern(), "y": Pattern()})
|
||||||
|
source_parent = Pattern()
|
||||||
|
source_parent.ref("x")
|
||||||
|
source = Library({"x": Pattern(), "y": source_parent})
|
||||||
|
|
||||||
|
with pytest.raises(LibraryError, match="Unresolved duplicate"):
|
||||||
|
destination.add(source, rename_theirs=lambda _lib, _name: "z", mutate_other=True)
|
||||||
|
|
||||||
|
assert set(destination) == {"x", "y"}
|
||||||
|
assert set(source) == {"x", "y"}
|
||||||
|
assert set(source["y"].refs) == {"x"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_add_callback_sees_earlier_name_reservations() -> None:
|
||||||
|
destination = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||||
|
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||||
|
callback_views: list[set[str]] = []
|
||||||
|
|
||||||
|
def rename(view: INameView, _name: str) -> str:
|
||||||
|
assert isinstance(view, INameView)
|
||||||
|
assert not isinstance(view, Mapping)
|
||||||
|
assert not hasattr(view, "__getitem__")
|
||||||
|
callback_views.append(set(view))
|
||||||
|
return view.get_name("_shape")
|
||||||
|
|
||||||
|
rename_map = destination.add(source, rename_theirs=rename)
|
||||||
|
|
||||||
|
assert len(set(rename_map.values())) == 2
|
||||||
|
assert rename_map["_shape$A"] in callback_views[1]
|
||||||
|
assert set(rename_map.values()) <= set(destination)
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_can_add_itself_with_mutate_other() -> None:
|
||||||
|
lib = Library({"_helper": Pattern()})
|
||||||
|
|
||||||
|
rename_map = lib.add(lib, mutate_other=True)
|
||||||
|
|
||||||
|
assert rename_map["_helper"] in lib
|
||||||
|
assert len(lib) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None:
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay["_shape$A"] = Pattern()
|
||||||
|
overlay["_shape$B"] = Pattern()
|
||||||
|
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
||||||
|
|
||||||
|
rename_map = overlay.add_source(
|
||||||
|
source,
|
||||||
|
rename_theirs=lambda view, _name: view.get_name("_shape"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(set(rename_map.values())) == 2
|
||||||
|
assert set(rename_map.values()) <= set(overlay)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ports_view_detaches_already_materialized_overlay_pattern() -> None:
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(Library({"top": Pattern()}))
|
||||||
|
raw = overlay["top"]
|
||||||
|
processed = PortsLibraryView(
|
||||||
|
overlay,
|
||||||
|
ports={"top": {"P": Port((1, 2), 0)}},
|
||||||
|
)
|
||||||
|
|
||||||
|
processed_top = processed["top"]
|
||||||
|
|
||||||
|
assert processed_top is not raw
|
||||||
|
assert not raw.ports
|
||||||
|
assert set(processed_top.ports) == {"P"}
|
||||||
|
assert not hasattr(processed, "close")
|
||||||
|
|
||||||
|
|
||||||
def test_library_subtree() -> None:
|
def test_library_subtree() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
lib["a"] = Pattern()
|
lib["a"] = Pattern()
|
||||||
|
|
@ -346,9 +565,230 @@ def test_library_subtree() -> None:
|
||||||
lib["a"].ref("b")
|
lib["a"].ref("b")
|
||||||
|
|
||||||
sub = lib.subtree("a")
|
sub = lib.subtree("a")
|
||||||
|
assert isinstance(sub, Library)
|
||||||
assert "a" in sub
|
assert "a" in sub
|
||||||
assert "b" in sub
|
assert "b" in sub
|
||||||
assert "c" not in sub
|
assert "c" not in sub
|
||||||
|
assert sub["a"] is lib["a"]
|
||||||
|
|
||||||
|
del sub["b"]
|
||||||
|
assert "b" in lib
|
||||||
|
|
||||||
|
|
||||||
|
def test_lazy_library_subtree_preserves_type_and_shares_materialized_patterns() -> None:
|
||||||
|
lib = LazyLibrary()
|
||||||
|
child = Pattern()
|
||||||
|
top = Pattern()
|
||||||
|
top.ref("child")
|
||||||
|
lib["child"] = lambda: child
|
||||||
|
lib["top"] = lambda: top
|
||||||
|
lib["unused"] = Pattern()
|
||||||
|
|
||||||
|
subtree = lib.subtree("top")
|
||||||
|
|
||||||
|
assert isinstance(subtree, LazyLibrary)
|
||||||
|
assert set(subtree) == {"child", "top"}
|
||||||
|
assert subtree["top"] is lib["top"]
|
||||||
|
del subtree["child"]
|
||||||
|
assert "child" in lib
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_subtree_preserves_type_sources_and_independent_promotion() -> None:
|
||||||
|
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
|
||||||
|
source["top"].ref("child")
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(source)
|
||||||
|
|
||||||
|
subtree = overlay.subtree("top")
|
||||||
|
|
||||||
|
assert isinstance(subtree, OverlayLibrary)
|
||||||
|
assert set(subtree) == {"child", "top"}
|
||||||
|
assert subtree.borrowed_sources() == overlay.borrowed_sources()
|
||||||
|
subtree_top = subtree["top"]
|
||||||
|
overlay_top = overlay["top"]
|
||||||
|
assert subtree_top is not overlay_top
|
||||||
|
|
||||||
|
shared_subtree = overlay.subtree("top")
|
||||||
|
assert shared_subtree["top"] is overlay_top
|
||||||
|
del shared_subtree["child"]
|
||||||
|
assert "child" in overlay
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_subtree_preserves_reference_remaps() -> None:
|
||||||
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||||
|
source["parent"].ref("leaf")
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(source)
|
||||||
|
overlay.rename("leaf", "renamed", move_references=True)
|
||||||
|
|
||||||
|
subtree = overlay.subtree("parent")
|
||||||
|
|
||||||
|
assert isinstance(subtree, OverlayLibrary)
|
||||||
|
assert set(subtree) == {"parent", "renamed"}
|
||||||
|
assert set(subtree["parent"].refs) == {"renamed"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_subtree_is_a_borrowed_subtree_view() -> None:
|
||||||
|
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
|
||||||
|
source["top"].ref("child")
|
||||||
|
view = LibraryView(source)
|
||||||
|
|
||||||
|
subtree = view.subtree("top")
|
||||||
|
|
||||||
|
assert subtree.source_order() == ("child", "top")
|
||||||
|
assert subtree["top"] is source["top"]
|
||||||
|
assert "unused" not in subtree
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
_ = subtree["unused"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_materialization_and_borrowing_capabilities() -> None:
|
||||||
|
lazy = LazyLibrary()
|
||||||
|
lazy["top"] = lambda: Pattern()
|
||||||
|
|
||||||
|
assert isinstance(lazy, IMaterializable)
|
||||||
|
assert not isinstance(lazy, IBorrowing)
|
||||||
|
assert not hasattr(lazy, "borrowed_sources")
|
||||||
|
transient = lazy.materialize("top", persist=False)
|
||||||
|
assert "top" not in lazy.cache
|
||||||
|
assert transient is not lazy["top"]
|
||||||
|
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(lazy)
|
||||||
|
ports = PortsLibraryView(overlay)
|
||||||
|
subtree = ports.subtree("top")
|
||||||
|
|
||||||
|
assert isinstance(overlay, IMaterializable)
|
||||||
|
assert isinstance(overlay, IBorrowing)
|
||||||
|
assert isinstance(ports, IMaterializable)
|
||||||
|
assert isinstance(ports, IBorrowing)
|
||||||
|
assert isinstance(subtree, IMaterializable)
|
||||||
|
assert isinstance(subtree, IBorrowing)
|
||||||
|
assert overlay.borrowed_sources() == (lazy,)
|
||||||
|
assert ports.borrowed_sources() == (overlay,)
|
||||||
|
assert subtree.borrowed_sources() == (ports,)
|
||||||
|
eager = Library({"top": Pattern()})
|
||||||
|
plain_view = LibraryView(lazy)
|
||||||
|
assert not isinstance(eager, IMaterializable | IBorrowing)
|
||||||
|
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
||||||
|
|
||||||
|
|
||||||
|
class _RawCopyView(ILibraryView):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.mapping = {"top": Pattern()}
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Pattern:
|
||||||
|
return self.mapping[key]
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
return iter(self.mapping)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.mapping)
|
||||||
|
|
||||||
|
def __contains__(self, key: object) -> bool:
|
||||||
|
return key in self.mapping
|
||||||
|
|
||||||
|
def raw_struct_bytes(self, name: str) -> bytes:
|
||||||
|
return name.encode()
|
||||||
|
|
||||||
|
def can_copy_raw_struct(self, name: str) -> bool:
|
||||||
|
return name in self.mapping
|
||||||
|
|
||||||
|
|
||||||
|
def test_ports_view_raw_copy_eligibility_tracks_persistent_materialization() -> None:
|
||||||
|
processed = PortsLibraryView(_RawCopyView())
|
||||||
|
|
||||||
|
assert processed.can_copy_raw_struct("top")
|
||||||
|
_ = processed.materialize_many(("top",), persist=False)
|
||||||
|
assert processed.can_copy_raw_struct("top")
|
||||||
|
|
||||||
|
subtree = processed.subtree("top")
|
||||||
|
assert subtree.can_copy_raw_struct("top")
|
||||||
|
_ = subtree["top"]
|
||||||
|
assert not processed.can_copy_raw_struct("top")
|
||||||
|
assert not subtree.can_copy_raw_struct("top")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("materialize_parent", [False, True])
|
||||||
|
@pytest.mark.parametrize("move_references", [False, True])
|
||||||
|
def test_overlay_rename_is_independent_of_materialization(
|
||||||
|
materialize_parent: bool,
|
||||||
|
move_references: bool,
|
||||||
|
) -> None:
|
||||||
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||||
|
source["parent"].ref("leaf")
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(source)
|
||||||
|
|
||||||
|
if materialize_parent:
|
||||||
|
_ = overlay["parent"]
|
||||||
|
|
||||||
|
overlay.rename("leaf", "renamed", move_references=move_references)
|
||||||
|
expected_target = "renamed" if move_references else "leaf"
|
||||||
|
|
||||||
|
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
|
||||||
|
assert set(overlay["parent"].refs) == {expected_target}
|
||||||
|
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_rejects_unknown_dangling_mode() -> None:
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(Library({"top": Pattern()}))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="dangling-reference mode"):
|
||||||
|
overlay.child_graph(dangling="typo")
|
||||||
|
with pytest.raises(ValueError, match="dangling-reference mode"):
|
||||||
|
overlay.find_refs_local("top", dangling="typo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_rename_preserves_initial_import_target_without_moving_refs() -> None:
|
||||||
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||||
|
source["parent"].ref("leaf")
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay["leaf"] = Pattern()
|
||||||
|
rename_map = overlay.add_source(source, rename_theirs=lambda lib, name: lib.get_name(name))
|
||||||
|
imported_leaf = rename_map["leaf"]
|
||||||
|
|
||||||
|
overlay.rename(imported_leaf, "renamed", move_references=False)
|
||||||
|
|
||||||
|
assert set(overlay["parent"].refs) == {imported_leaf}
|
||||||
|
|
||||||
|
|
||||||
|
def test_overlay_chained_rename_moves_unmaterialized_references() -> None:
|
||||||
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
||||||
|
source["parent"].ref("leaf")
|
||||||
|
overlay = OverlayLibrary()
|
||||||
|
overlay.add_source(source)
|
||||||
|
|
||||||
|
overlay.rename("leaf", "middle", move_references=True)
|
||||||
|
overlay.rename("middle", "final", move_references=True)
|
||||||
|
|
||||||
|
assert set(overlay["parent"].refs) == {"final"}
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_tree_merge_rejected(tree: Library) -> None:
|
||||||
|
destination = Library({"existing": Pattern()})
|
||||||
|
|
||||||
|
with pytest.raises(LibraryError, match="exactly one topcell"):
|
||||||
|
destination << tree
|
||||||
|
|
||||||
|
assert set(destination) == {"existing"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_tree_merge_rejects_empty_tree() -> None:
|
||||||
|
_assert_tree_merge_rejected(Library())
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_tree_merge_rejects_cycle_without_top() -> None:
|
||||||
|
tree = Library({"a": Pattern(), "b": Pattern()})
|
||||||
|
tree["a"].ref("b")
|
||||||
|
tree["b"].ref("a")
|
||||||
|
_assert_tree_merge_rejected(tree)
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_tree_merge_rejects_multiple_tops() -> None:
|
||||||
|
_assert_tree_merge_rejected(Library({"a": Pattern(), "b": Pattern()}))
|
||||||
|
|
||||||
|
|
||||||
def test_library_child_order_cycle_raises_library_error() -> None:
|
def test_library_child_order_cycle_raises_library_error() -> None:
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ class PreferredMinimumTool(PlanningOnlyTool):
|
||||||
BendOffer(
|
BendOffer(
|
||||||
in_ptype=in_ptype,
|
in_ptype=in_ptype,
|
||||||
out_ptype='wide',
|
out_ptype='wide',
|
||||||
priority_bias=100,
|
cost=100,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
length_domain=(2, 2),
|
length_domain=(2, 2),
|
||||||
endpoint_planner=lambda _length: Port((2, jog), rotation, ptype='wide'),
|
endpoint_planner=lambda _length: Port((2, jog), rotation, ptype='wide'),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -146,14 +147,15 @@ def test_straight_offer_generated_factory_uses_default_endpoint_and_data_bbox()
|
||||||
'wire',
|
'wire',
|
||||||
data_at,
|
data_at,
|
||||||
length_domain=(2, 8),
|
length_domain=(2, 8),
|
||||||
priority_bias=3,
|
cost=3,
|
||||||
bbox_for_data=lambda data: numpy.array([[0, 0], [data['length'], 1]]),
|
bbox_for_data=lambda data: numpy.array([[0, 0], [data['length'], 1]]),
|
||||||
)
|
)
|
||||||
|
|
||||||
endpoint = offer.endpoint_at(4)
|
endpoint = offer.endpoint_at(4)
|
||||||
|
|
||||||
assert offer.length_domain == (2, 8)
|
assert offer.length_domain == (2, 8)
|
||||||
assert offer.priority_bias == 3
|
assert offer.cost == 3
|
||||||
|
assert offer.cost_at(4) == 12
|
||||||
assert numpy.allclose(endpoint.offset, [4, 0])
|
assert numpy.allclose(endpoint.offset, [4, 0])
|
||||||
assert endpoint.rotation == pi
|
assert endpoint.rotation == pi
|
||||||
assert endpoint.ptype == 'wire'
|
assert endpoint.ptype == 'wire'
|
||||||
|
|
@ -256,9 +258,43 @@ def test_prebuilt_offer_factories_return_fresh_endpoint_copies() -> None:
|
||||||
assert numpy.allclose(offer.bbox_at(parameter), [[-1, -2], [6, 3]])
|
assert numpy.allclose(offer.bbox_at(parameter), [[-1, -2], [6, 3]])
|
||||||
|
|
||||||
|
|
||||||
def test_offer_rejects_negative_priority_bias() -> None:
|
@pytest.mark.parametrize('cost', [-1, numpy.inf, numpy.nan, 'expensive'])
|
||||||
with pytest.raises(BuildError, match='priority_bias must be nonnegative'):
|
def test_offer_rejects_invalid_cost_factor(cost: Any) -> None:
|
||||||
StraightOffer(in_ptype='wire', out_ptype='wire', priority_bias=-1)
|
with pytest.raises(BuildError, match='cost factor'):
|
||||||
|
StraightOffer(in_ptype='wire', out_ptype='wire', cost=cost)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('result', [-1, numpy.inf, numpy.nan, 'expensive'])
|
||||||
|
def test_offer_rejects_invalid_callable_cost_result(result: Any) -> None:
|
||||||
|
offer = StraightOffer(
|
||||||
|
in_ptype='wire',
|
||||||
|
out_ptype='wire',
|
||||||
|
cost=lambda _parameter, _endpoint: result,
|
||||||
|
**offer_callbacks(lambda length: (Port((length, 0), rotation=pi, ptype='wire'), None)),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(BuildError, match='Primitive cost'):
|
||||||
|
offer.cost_at(5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_offer_callable_cost_replaces_default_cost() -> None:
|
||||||
|
seen: list[tuple[float, Port]] = []
|
||||||
|
|
||||||
|
def cost(parameter: float, endpoint: Port) -> float:
|
||||||
|
seen.append((parameter, endpoint))
|
||||||
|
return parameter + endpoint.y
|
||||||
|
|
||||||
|
offer = SOffer(
|
||||||
|
in_ptype='wire',
|
||||||
|
out_ptype='wire',
|
||||||
|
cost=cost,
|
||||||
|
jog_domain=(2, 2),
|
||||||
|
**offer_callbacks(lambda jog: (Port((10, jog), rotation=pi, ptype='wire'), None)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert offer.cost_at(2 + 1e-13) == 4
|
||||||
|
assert seen[0][0] == 2
|
||||||
|
assert numpy.allclose(seen[0][1].offset, (10, 2))
|
||||||
|
|
||||||
|
|
||||||
def test_offer_rejects_one_sided_split_callbacks() -> None:
|
def test_offer_rejects_one_sided_split_callbacks() -> None:
|
||||||
|
|
@ -416,8 +452,8 @@ def test_pather_selects_lowest_cost_offer() -> None:
|
||||||
return Port((length, 0), rotation=pi, ptype=out_ptype or in_ptype), {'kind': 'low'}
|
return Port((length, 0), rotation=pi, ptype=out_ptype or in_ptype), {'kind': 'low'}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, priority_bias=10, **offer_callbacks(high)),
|
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, cost=10, **offer_callbacks(high)),
|
||||||
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, priority_bias=0, **offer_callbacks(low)),
|
StraightOffer(in_ptype=in_ptype, out_ptype=out_ptype, cost=1, **offer_callbacks(low)),
|
||||||
)
|
)
|
||||||
|
|
||||||
p = Pather(Library(), tools=MultiOfferTool(), render='deferred')
|
p = Pather(Library(), tools=MultiOfferTool(), render='deferred')
|
||||||
|
|
@ -429,6 +465,51 @@ def test_pather_selects_lowest_cost_offer() -> None:
|
||||||
assert numpy.allclose(p.ports['A'].offset, (-7, 0))
|
assert numpy.allclose(p.ports['A'].offset, (-7, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_planner_deduplication_distinguishes_offer_costs() -> None:
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class MarkedStraightOffer(StraightOffer):
|
||||||
|
marker: str = ''
|
||||||
|
|
||||||
|
def commit(self, parameter: float) -> dict[str, str | float]:
|
||||||
|
return {'kind': self.marker, 'length': self.canonicalize_parameter(parameter)}
|
||||||
|
|
||||||
|
def endpoint(length: float) -> Port:
|
||||||
|
return Port((length, 0), rotation=pi, ptype='wire')
|
||||||
|
|
||||||
|
def shared_commit(length: float) -> dict[str, float]:
|
||||||
|
return {'length': length}
|
||||||
|
|
||||||
|
class DuplicateGeometryTool(PlanningOnlyTool):
|
||||||
|
def primitive_offers(
|
||||||
|
self,
|
||||||
|
kind: Literal['straight', 'bend', 's', 'u'],
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> tuple[PrimitiveOffer, ...]:
|
||||||
|
_ = kwargs
|
||||||
|
if kind != 'straight':
|
||||||
|
return ()
|
||||||
|
common = {
|
||||||
|
'in_ptype': in_ptype,
|
||||||
|
'out_ptype': out_ptype,
|
||||||
|
'endpoint_planner': endpoint,
|
||||||
|
'commit_planner': shared_commit,
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
MarkedStraightOffer(cost=100, marker='expensive', **common),
|
||||||
|
MarkedStraightOffer(cost=1, marker='cheap', **common),
|
||||||
|
)
|
||||||
|
|
||||||
|
pather = Pather(Library(), tools=DuplicateGeometryTool(), render='deferred')
|
||||||
|
pather.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||||
|
|
||||||
|
pather.straight('A', 7)
|
||||||
|
|
||||||
|
assert pather._paths['A'][0].data == {'kind': 'cheap', 'length': 7}
|
||||||
|
|
||||||
|
|
||||||
class StrategyTieTool(PlanningOnlyTool):
|
class StrategyTieTool(PlanningOnlyTool):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.seen_kwargs: list[dict[str, Any]] = []
|
self.seen_kwargs: list[dict[str, Any]] = []
|
||||||
|
|
@ -591,7 +672,7 @@ def test_pather_commits_only_selected_offer() -> None:
|
||||||
if kind != 'straight':
|
if kind != 'straight':
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def make(label: str, priority: float) -> StraightOffer:
|
def make(label: str, cost: float) -> StraightOffer:
|
||||||
def endpoint(length: float) -> Port:
|
def endpoint(length: float) -> Port:
|
||||||
return Port((length, 0), rotation=pi, ptype=out_ptype or in_ptype)
|
return Port((length, 0), rotation=pi, ptype=out_ptype or in_ptype)
|
||||||
|
|
||||||
|
|
@ -602,12 +683,12 @@ def test_pather_commits_only_selected_offer() -> None:
|
||||||
return StraightOffer(
|
return StraightOffer(
|
||||||
in_ptype=in_ptype,
|
in_ptype=in_ptype,
|
||||||
out_ptype=out_ptype,
|
out_ptype=out_ptype,
|
||||||
priority_bias=priority,
|
cost=cost,
|
||||||
endpoint_planner=endpoint,
|
endpoint_planner=endpoint,
|
||||||
commit_planner=commit,
|
commit_planner=commit,
|
||||||
)
|
)
|
||||||
|
|
||||||
return (make('expensive', 100), make('cheap', 0))
|
return (make('expensive', 100), make('cheap', 1))
|
||||||
|
|
||||||
p = Pather(Library(), tools=RecordingTool(), render='deferred')
|
p = Pather(Library(), tools=RecordingTool(), render='deferred')
|
||||||
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,15 @@ def test_data_to_ports_hierarchical() -> None:
|
||||||
assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10)
|
assert_allclose(parent.ports["A"].rotation, numpy.pi / 2, atol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_data_to_ports_ignores_empty_dangling_ref_bucket() -> None:
|
||||||
|
parent = Pattern()
|
||||||
|
parent.refs["missing"]
|
||||||
|
|
||||||
|
data_to_ports([(10, 0)], Library(), parent, max_depth=1)
|
||||||
|
|
||||||
|
assert not parent.ports
|
||||||
|
|
||||||
|
|
||||||
def test_data_to_ports_hierarchical_scaled_ref() -> None:
|
def test_data_to_ports_hierarchical_scaled_ref() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,8 @@ def data_to_ports(
|
||||||
|
|
||||||
# Load ports for all subpatterns, and use any we find
|
# Load ports for all subpatterns, and use any we find
|
||||||
found_ports = False
|
found_ports = False
|
||||||
for target in pattern.refs:
|
for target, refs in pattern.refs.items():
|
||||||
if target is None:
|
if target is None or not refs:
|
||||||
continue
|
continue
|
||||||
pp = data_to_ports(
|
pp = data_to_ports(
|
||||||
layers = layers,
|
layers = layers,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue