masque/MIGRATION.md
2026-07-13 14:58:09 -07:00

31 KiB

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

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

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.

(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:

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

# 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

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

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:

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:

class MyTool(Tool):
    def path(self, ccw, length, **kwargs):
        ...

should now become:

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 ToolContractErrors 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:

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

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:

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:

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

Run this validation during Tool development, testing, or application startup. It is the comprehensive semantic preflight for custom Tools; those checks are intentionally not repeated for every offer evaluation in the routing hot path.

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. Bend-family requests search one-bend routes before three-bend routes; other families search zero-to-two-bend routes before four-bend routes. The first band with a legal route wins. Within that band, candidates are ordered by total primitive-offer cost, adapter count, step count, and deterministic discovery order. The route strategy affects only that final discovery-order tie-break.

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:

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:

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:

port.mirror(0)   # changed both offset and orientation

New behavior:

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:

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:

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:

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:

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:

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

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