28 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(...)fortrace_to(..., ccw=None, ...)ccw(...)fortrace_to(..., ccw=True, ...)cw(...)fortrace_to(..., ccw=False, ...)jog(...)for S-bendsuturn(...)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:
SimpleToolwas removed; useAutoToolor implement the newToolprimitive-offer interfaceAutoTool.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=Trueto 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 boundaryrender()consumes committed primitive render tokensTool.path(...),traceL(),traceS(),traceU(),planL(),planS(), andplanU()are no longer part of the publicToolAPI
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:
StraightOfferfor non-turning length-parameterized primitivesBendOfferfor single-turn length-parameterized primitivesSOfferfor S-like jog-parameterized primitivesUOfferfor 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.
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 flat
mapping is snapshotted per selected primitive and passed to its generator as
keyword arguments during rendering. 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 outputPortcost_at(parameter)returns an additive scalar route-selection costbbox_at(parameter)returns local primitive bounds when a footprint hook is suppliedparameterized_bboxmay carry opaque future-router footprint metadatacommit(parameter)returns opaque render data consumed later byrender()(min, max)parameter domains are half-open;(value, value)is a fixed singleton- selected parameter values must be finite; domains may use infinite open bounds but not
NaN Noneand"unk"ptypes are wildcards; concrete ptype mismatches reject an offer
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:
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, andcell - from
masque.builder:CostCallable, the concrete primitive-offer classes, and structured route error/status types
Minimal migration checklist
If your code uses the routing stack, do these first:
- Replace
path/path_to/mpath/path_intocalls withtrace/trace_to/multi-porttrace/trace_into. - Replace
SimpleTooland oldAutoTooldescriptor construction with the newAutoTool.add_*()methods. - Fix imports that still reference
masque.builder.builderormasque.builder.renderpather. - Audit any low-level
mirror()usage, especially onPortandRef. - Move lazy GDS calls from
gdsii.load_library*()tomasque.file.gdsii.lazy. - Remove
raw=Truefrom 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.