# 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.