masque/MIGRATION.md

803 lines
31 KiB
Markdown

# Migration Guide
This guide covers changes between the `master` branch and the current tree.
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
API changes that may require code updates.
## Routing API: renamed and consolidated
The routing helpers were consolidated into a single implementation in
`masque/builder/pather.py`.
The biggest migration point is that the old routing verbs were renamed:
| Old API | New API |
| --- | --- |
| `Pather.path(...)` | `Pather.trace(...)` |
| `Pather.path_to(...)` | `Pather.trace_to(...)` |
| `Pather.mpath(...)` | `Pather.trace(...)` / `Pather.trace_to(...)` with multiple ports |
| `Pather.pathS(...)` | `Pather.jog(...)` |
| `Pather.pathU(...)` | `Pather.uturn(...)` |
| `Pather.path_into(...)` | `Pather.trace_into(...)` |
| `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` |
| `RenderPather.path(...)` | `Pather(..., render='deferred').trace(...)` |
| `RenderPather.path_to(...)` | `Pather(..., render='deferred').trace_to(...)` |
| `RenderPather.mpath(...)` | `Pather(..., render='deferred').trace(...)` / `Pather(..., render='deferred').trace_to(...)` |
| `RenderPather.pathS(...)` | `Pather(..., render='deferred').jog(...)` |
| `RenderPather.pathU(...)` | `Pather(..., render='deferred').uturn(...)` |
| `RenderPather.path_into(...)` | `Pather(..., render='deferred').trace_into(...)` |
| `RenderPather.path_from(src, dst)` | `Pather(..., render='deferred').at(src).trace_into(dst)` |
There are also new convenience wrappers:
- `straight(...)` for `trace_to(..., ccw=None, ...)`
- `ccw(...)` for `trace_to(..., ccw=True, ...)`
- `cw(...)` for `trace_to(..., ccw=False, ...)`
- `jog(...)` for S-bends
- `uturn(...)` for U-bends
Important: `Pather.path()` is no longer the routing API. It now forwards to
`Pattern.path()` and creates a geometric `Path` element. Any old routing code
that still calls `pather.path(...)` must be renamed.
### Common rewrites
```python
# old
pather.path('VCC', False, 6_000)
pather.path_to('VCC', None, x=0)
pather.mpath(['GND', 'VCC'], True, xmax=-10_000, spacing=5_000)
pather.pathS('VCC', offset=-2_000, length=8_000)
pather.pathU('VCC', offset=4_000, length=5_000)
pather.path_into('src', 'dst')
pather.path_from('src', 'dst')
# new
pather.cw('VCC', 6_000)
pather.straight('VCC', x=0)
pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
pather.jog('VCC', offset=-2_000, length=8_000)
pather.uturn('VCC', offset=4_000, length=5_000)
pather.trace_into('src', 'dst')
pather.at('src').trace_into('dst')
```
If you prefer the more explicit spelling, `trace(...)` and `trace_to(...)`
remain the underlying primitives:
```python
pather.trace('VCC', False, 6_000)
pather.trace_to('VCC', None, x=0)
```
## `PortPather` and `.at(...)`
Routing can now be written in a fluent style via `.at(...)`, which returns a
`PortPather`.
```python
(rpather.at('VCC')
.trace(False, length=6_000)
.trace_to(None, x=0)
)
```
This is additive, not required for migration. Existing code can stay with the
non-fluent `Pather` methods after renaming the verbs above.
Old `PortPather` helper names were also cleaned up:
| Old API | New API |
| --- | --- |
| `save_copy(...)` | `mark(...)` |
| `rename_to(...)` | `rename(...)` |
Example:
```python
# old
pp.save_copy('branch')
pp.rename_to('feed')
# new
pp.mark('branch')
pp.rename('feed')
```
## Imports and module layout
`Pather` now provides the remaining builder/routing surface in
`masque/builder/pather.py`. The old module files
`masque/builder/builder.py` and `masque/builder/renderpather.py` were removed.
Update imports like this:
```python
# old
from masque.builder.builder import Builder
from masque.builder.renderpather import RenderPather
# new
from masque.builder import Pather
builder = Pather(...)
deferred = Pather(..., render='deferred')
```
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
old `Builder` behavior. Use `Pather(..., render='deferred')` where you
previously used `RenderPather`.
## `SimpleTool` was removed and `AutoTool` registration changed
`SimpleTool` is no longer exported. `AutoTool` remains, but its old public
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 `AutoTool`
```python
from masque.builder import AutoTool
tool = AutoTool(
straights=[
AutoTool.Straight('m1wire', make_straight, 'input', 'output'),
],
bends=[
AutoTool.Bend(lib.abstract('bend'), 'input', 'output'),
],
sbends=[],
transitions={
('m2wire', 'm1wire'): AutoTool.Transition(
lib.abstract('via'), 'top', 'bottom'
),
},
default_out_ptype='m1wire',
)
```
### New `AutoTool`
```python
from masque.builder import AutoTool
tool = (
AutoTool()
.add_straight(make_straight, 'm1wire', 'input')
.add_bend(lib.abstract('bend'), 'input', 'output', clockwise=True)
.add_transition(lib.abstract('via'), 'top', 'bottom')
)
```
The key differences are:
- `SimpleTool` was removed; use `AutoTool` or implement the new `Tool`
primitive-offer interface
- `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 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
If you maintain your own `Tool` subclass, the interface changed:
- `primitive_offers()` is now the planning boundary
- `render()` consumes committed primitive render tokens
- `Tool.path(...)`, `traceL()`, `traceS()`, `traceU()`, `planL()`,
`planS()`, and `planU()` are no longer part of the public `Tool` API
In practice, a minimal old implementation like:
```python
class MyTool(Tool):
def path(self, ccw, length, **kwargs):
...
```
should now become:
```python
from collections.abc import Sequence
from typing import Any
from masque import Port
from masque.builder import RenderStep, StraightOffer, Tool
class MyTool(Tool):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs):
if kind != 'straight':
return ()
def endpoint(length):
ptype = out_ptype or in_ptype
return Port((length, 0), rotation=3.141592653589793, ptype=ptype)
def commit(length):
return {'length': length}
return (StraightOffer(
in_ptype=in_ptype,
out_ptype=out_ptype or in_ptype,
endpoint_planner=endpoint,
commit_planner=commit,
),)
def render(self, batch: Sequence[RenderStep]):
...
```
If a tool does not provide a primitive kind, return `()` for that kind. `Pather`
will compose available primitive offers where the route family allows it.
### Primitive offers
Tools describe legal routing primitives through `Tool.primitive_offers()`.
`Pather` composes those primitive offers to implement `trace()`, `jog()`,
`uturn()`, and `trace_into()`.
For custom tools, construct the concrete offer class that matches the primitive
you are exposing:
- `StraightOffer` for non-turning length-parameterized primitives
- `BendOffer` for single-turn length-parameterized primitives
- `SOffer` for S-like jog-parameterized primitives
- `UOffer` for U-like jog-parameterized primitives
`PrimitiveOffer` is the shared base type used for generic annotations and
common callback behavior. It is not the normal class users should instantiate.
The concrete offer classes carry the semantic fields (`length_domain`,
`jog_domain`, `ccw`) so tools do not need to encode primitive identity in
strings. Each concrete offer now also exposes a class-level canonical `kind`.
Tools must return the matching offer kind for the discovery query; for example,
`primitive_offers('s', ...)` must return only `SOffer` instances. Mismatches are
reported as fatal `ToolContractError`s rather than silently ignored.
`RenderStep` now stores that same canonical kind. Code that constructs render
steps directly must use the kind rather than a legacy opcode:
```python
# old
RenderStep('L', tool, start, end, data)
# new
RenderStep('straight', tool, start, end, data)
```
Use `'bend'`, `'s'`, `'u'`, or `'plug'` for the other step types. The read-only
`RenderStep.opcode` and `PrimitiveOffer.opcode` properties remain available for
code that only consumes steps, but opcodes are now derived rather than stored.
Minimal straight-only example:
```python
from collections.abc import Sequence
from typing import Literal
from masque import Port
from masque.builder import RenderStep, StraightOffer, Tool
class MyTool(Tool):
def primitive_offers(
self,
kind: Literal['straight', 'bend', 's', 'u'],
*,
in_ptype=None,
out_ptype=None,
**kwargs,
):
if kind != 'straight':
return ()
def endpoint(length):
ptype = out_ptype or in_ptype
return Port((length, 0), rotation=3.141592653589793, ptype=ptype)
def commit(length):
return {'length': length}
return (StraightOffer(
in_ptype=in_ptype,
out_ptype=out_ptype or in_ptype,
endpoint_planner=endpoint,
commit_planner=commit,
),)
def render(self, batch: Sequence[RenderStep]):
...
```
Routing entry points now name every supported route argument explicitly.
Custom per-route planning values must be placed under `tool_options`:
```python
pather.jog('A', 4, length=10, tool_options={'process_corner': 'slow'})
```
The mapping is unpacked only for `Tool.primitive_offers()`. Route arguments do
not leak into that namespace, and tool options are not forwarded to
`Tool.render()`. An offer that needs route-specific render behavior must capture
the selected value in its `commit()` result (`RenderStep.data`). `AutoTool`
does this automatically for generated straight and S-bend primitives: the
mapping is deep-copied per offer discovery and passed to its generator as
keyword arguments during rendering. Consequently every AutoTool option value
must be deep-copyable, and later mutation of nested caller-owned values does
not affect a pending route. AutoTool options must not change generated
ports or endpoint geometry. `PathTool` defines no tool options and rejects
nonempty mappings.
AutoTool does not validate generator keyword signatures during planning. A bad
keyword therefore raises when the generator runs, normally during rendering.
Generators that require route options must provide explicit port metadata at
registration; S-bend generators must also provide an explicit `endpoint`.
Primitive offers are local planning objects:
- `endpoint_at(parameter)` returns the local output `Port`
- `cost_at(parameter)` returns an additive scalar route-selection cost
- `bbox_at(parameter)` returns local primitive bounds when a footprint hook is supplied
- `parameterized_bbox` may carry opaque future-router footprint metadata
- `commit(parameter)` returns opaque render data consumed later by `render()`
- `(min, max)` parameter domains are half-open; `(value, value)` is a fixed singleton
- domains are validated when an offer is constructed, not when it is first evaluated
- selected parameter values must be finite; domains may use infinite open bounds but not `NaN`
- straight/bend domains require a finite nonnegative minimum; fixed singleton domains must be finite
- `None` and `"unk"` ptypes are wildcards; concrete ptype mismatches reject an offer
`AutoTool.add_straight(length_range=...)` and
`AutoTool.add_sbend(jog_range=...)` now validate their ranges before metadata
inference. `jog_range` is an absolute-magnitude range and must have a finite,
nonnegative lower bound; AutoTool creates the corresponding positive and
negative S offers itself. Invalid ranges now raise immediately instead of
registering no offers.
`ToolContractError` is exported from `masque` and `masque.builder`. It marks a
broken Tool contract—such as an endpoint ptype that disagrees with its offer,
an invalid evaluated cost, or rendered output that disagrees with the planned
endpoint—and is fatal to route fallback. Ordinary `BuildError` raised by a
planning callback remains a recoverable candidate rejection. Rendered output
rotations are compared modulo one full turn; a port facing exactly backward is
no longer accepted as equivalent. A `None` rotation remains an explicit
wildcard.
Custom Tool authors can exercise discovery, offer callbacks, committed data,
and one-step rendering without depending on pytest:
```python
from masque.builder import ToolContractCase, validate_tool_contract
validate_tool_contract(my_tool, (
ToolContractCase('straight', in_ptype='wire', probe_parameters=(10,)),
ToolContractCase('bend', in_ptype='wire', ccw=False),
ToolContractCase('bend', in_ptype='wire', ccw=True),
ToolContractCase('s', in_ptype='wire', require_offers=False),
))
```
Cases derive representative parameters from each returned offer domain and
may add explicit probes. Empty discovery is an error unless
`require_offers=False`; bbox support is required only with `check_bbox=True`.
Validation returns normally on success and otherwise raises an
`ExceptionGroup` of contextual `ToolContractError`s.
Positional routing bounds (`p`, `pos`, `position`, `x`, and `y`) now require a
nearly Manhattan input-port direction. Arbitrarily angled ports remain valid
for non-positional/extension routing.
Heterogeneous `StraightOffer` and `SOffer` objects may be used as ptype
adapters. Requested `out_ptype` constrains only the final route endpoint; any
intermediate ptypes are chosen by the route solver.
`Tool` subclasses must override `primitive_offers()` and return `()` themselves
for recognized unsupported kinds. There is no route-level `plan*()` fallback.
Omitted-length S/U behavior comes from direct `SOffer` and `UOffer` endpoint
domains or from composed straight/bend primitives.
Offer constructors accept split `endpoint_planner` and `commit_planner`
callbacks. Provide both callbacks or override the offer methods in a subclass;
partial callback configurations are rejected during offer construction.
When writing direct primitive offers, declare the actual endpoint ptype
produced by the offer if it can differ from the requested value; `Pather`
validates evaluated endpoints against the declared offer ptype.
Stable imports for custom tool authors live in `masque.builder`. The
`masque.builder.planner` module is an internal planner implementation; do not
import it from user code.
`trace_into()` uses the same primitive-offer route selection and now searches
bounded route topologies with up to four bend roles. This preserves the common
straight, bend, S-like, U-like, and dogleg cases while allowing routes that
need an additional bounded bend pair. Among legal bounded candidates,
`trace_into()` selects the lowest total primitive-offer cost; bend count and
step count are used only to break exact cost ties.
Explicit-length `jog()` routes may also be satisfied by composing a straight
primitive before or after an omitted-length native S primitive. `uturn()` routes
may compose a straight primitive before an omitted-length native U primitive.
These compositions are used when they are the lowest-cost legal route for the
explicit request.
`AutoTool` can attach `bbox_at()` hooks to its primitive offers by rendering the
selected primitive into a temporary pattern and measuring it. If the rendered
primitive contains reusable refs, pass the source library as `bbox_library=...`;
normal routing does not require this.
### Omitted-length routing
Single-port omitted-length calls now evaluate legal primitive routes at their
minimum legal length-like parameter, or at their intrinsic endpoint length when
the requested offset fixes the primitive geometry. Cost then selects among
those minimum-length candidates:
```python
pather.trace('A', None) # minimum straight-like route
pather.jog('A', offset=2) # minimum S-like route for that offset
pather.uturn('A', offset=4) # minimum U-like route for that offset
```
For U-turns, use explicit `length=0` to request the old zero-public-length
shape:
```python
pather.uturn('A', offset=4, length=0)
```
## Transform semantics changed
The other major user-visible change is that `mirror()` and `rotate()` are now
treated more consistently as intrinsic transforms on low-level objects.
The practical migration rule is:
- use `mirror()` / `rotate()` when you want to change the object relative to its
own origin
- use `flip_across(...)`, `rotate_around(...)`, or container-level transforms
when you want to move the object in its parent coordinate system
### Example: `Port`
Old behavior:
```python
port.mirror(0) # changed both offset and orientation
```
New behavior:
```python
port.mirror(0) # changes orientation only
port.flip_across(axis=0) # old "mirror in the parent pattern" behavior
```
### What to audit
Check code that calls:
- `Port.mirror(...)`
- `Ref.rotate(...)`
- `Ref.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
needs updating. For whole-pattern transforms, prefer calling `Pattern.mirror()`
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
### 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
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.
### 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
These are additive, but available now from `masque` and `masque.builder`:
- from `masque`: `RectCollection`, `boolean`, `OverlayLibrary`,
`PortsLibraryView`, `IMaterializable`, `IBorrowing`, `LibraryBuilder`,
`BuildReport`, `CellProvenance`, and `cell`
- from `masque.builder`: `CostCallable`, `RenderStepKind`, the concrete
primitive-offer classes, structured route error/status types,
`ToolContractCase`, and `validate_tool_contract`
## Minimal migration checklist
If your code uses the routing stack, do these first:
1. Replace `path`/`path_to`/`mpath`/`path_into` calls with
`trace`/`trace_to`/multi-port `trace`/`trace_into`.
2. Replace `SimpleTool` and old `AutoTool` descriptor construction with the
new `AutoTool.add_*()` methods.
3. Fix imports that still reference `masque.builder.builder` or
`masque.builder.renderpather`.
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
routing helpers, audit transforms, dangling-reference graph calls, raw shape
construction, and any stale imports.