mxpic_router GDS Generation Logic
mxpic_router is the external layout build engine used by mxpic_EDA.
The EDA repo owns canvas editing, YAML export, login, and API routing. This
repo owns the Nazca-based GDS build flow: it reads saved cell YAML files,
loads the selected technology manifest, loads PDK GDS assets, registers
routable pins, connects bundle links through mxpic_forge.Route when available
or Nazca interconnects.Interconnect as a fallback, and exports the final GDS.
High Level Flow
Canvas Build Layout
-> frontend/canvas.html handleBuildLayout()
-> buildYamlForPage()
-> POST /api/save-layout
-> backend/server.py save_layout()
-> writes <project>/<cell>.yml
-> backend/routed_layout_preview.py create_routed_layout_svg()
-> mxpic_router.build_project_gds(..., target_cell_name=<cell>)
-> temporary .gds
-> gdstk.read_gds(...).top_level()[0].write_svg(...)
-> <project>/<cell>.svg preview
Canvas Build GDS
-> frontend/canvas.html handleBuildGds()
-> POST /api/build-gds
-> backend/server.py build_gds()
-> backend/gds_builder.py build_project_gds()
-> mxpic_router.build_project_gds(...)
-> Nazca export_gds(...)
-> downloadable <project>.gds
If mxpic_router, Nazca, or optional gdstk are absent, the
EDA server can still run canvas and login pages. Build actions are where the
router stack is required. mxpic_forge improves routing when present, but is
not required because the router falls back to Nazca Interconnect. For Build
Layout, the YAML is still saved and SVG preview is skipped when the required
router stack is missing.
Runtime Stack
The EDA backend checks build-time dependencies through
backend/router_dependency.py.
require_router_stack()
-> ensure_router_path()
-> adds sibling ../mxpic_router to sys.path if present
-> import mxpic_router
-> import nazca
-> optionally import gdstk for SVG preview
-> import mxpic_router.builder._import_route_backend()
-> first tries mxpic_router.builder._import_mxpic_forge_route()
-> adds sibling ../mxpic_forge to sys.path
-> removes any already-loaded non-forge mxpic modules
-> from mxpic import Route
-> if mxpic_forge is absent:
-> uses nazca.interconnects.Interconnect through a Route-compatible adapter
Important package naming:
mxpic_routeris this active router package.mxpic_router_legacyis the old internal legacy package.mxpicshould resolve tomxpic_forgewhen that checkout is present, becausemxpic_forgeprovides the preferredRoutebackend.
Inputs To mxpic_router
The public entry point is:
mxpic_router.build_project_gds(
project_dir,
output_path,
pdk_root,
technology_manifest_path=None,
prefer_full_gds=False,
target_cell_name=None,
)
Inputs:
project_dir: directory containing saved.ymlor.yamlcell documents.output_path: final GDS path.pdk_root: role-scoped PDK root, usually one of:opt_pdk_public/foundriesopt_pdk_atlas/foundries
technology_manifest_path: selected technology file, for example:opt_pdk_public/foundries/Silterra/EMO1_2ML_CU_Al_RDL/technology.yml
prefer_full_gds: manager/atlas users prefer full GDS files before black-box GDS files.target_cell_name: optional. Used by SVG preview to build one saved cell as the top cell.
The return value is a dictionary:
{
"output_path": output_path,
"engine": "mxpic_router",
"cells_built": [...],
"warnings": [...],
}
How Technology Is Loaded
Technology selection happens in mxpic_EDA, then the selected manifest path is
passed into mxpic_router.
backend/server.py list_technologies()
-> current_pdk_root()
-> scan <pdk_root>/<foundry>/<technology>/
-> expose only directories containing technology.yml
backend/server.py technology_manifest_path_for_project(project)
-> read project metadata "technology", such as "Silterra/EMO1_2ML_CU_Al_RDL"
-> build <active pdk_root>/<foundry>/<technology>/technology.yml
-> pass this path to mxpic_router
Inside this repo:
mxpic_router.builder.build_project_gds()
-> technology.load_technology_manifest(path)
-> yaml.safe_load(technology.yml)
-> technology.apply_technology_manifest(manifest, nazca)
-> for manifest.layers:
nd.add_layer(name=<layer_name>, layer=(layer, datatype), overwrite=True)
-> for manifest.xsections:
nd.add_xsection(name=<xsection>)
nd.add_layer2xsection(
xsection=<xsection>,
layer=<layer_name>,
growx/growy/leftedge/rightedge=...,
overwrite=True,
)
Expected technology.yml shape:
foundry: Silterra
technology: EMO1_2ML_CU_Al_RDL
layers:
WG_STRIP: {layer: 101, datatype: 251}
routing_types:
- euler_bend
- standard_bend
defaults:
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
xsections:
strip:
family: optical
default_width: 0.45
default_radius: 10
layers:
- {layer: WG_STRIP, growx: 4, growy: 4}
The frontend also reads the same manifest through
/api/technologies/<foundry>/<technology>/manifest so the canvas route editor
uses the same xsections, widths, radii, and routing types that Nazca will use
during GDS generation.
How Project YAML Is Loaded
The frontend serializes each canvas page into the mxPIC YAML format.
frontend/canvas.html buildYamlForPage()
-> buildCanvasPinsYaml()
-> buildInstancesYaml()
-> buildElementsYaml()
-> buildRouteBundlesYaml()
-> POST /api/save-layout
-> backend/server.py writes <project>/<cell>.yml
mxpic_router loads all saved YAML files from the project directory:
builder._load_project_specs(project_dir)
-> sorted os.listdir(project_dir)
-> every .yml/.yaml file
-> eda_loader.load_cell_spec(path)
-> yaml.safe_load(...)
-> eda_loader.parse_cell_dict(...)
-> CellSpec dataclass
The parser maps YAML into typed dataclasses:
CellSpec: name, type, version, pins, elements, instances, bundles.PinSpec: top-level exported cell pins.ElementSpec: canvas-only helper elements such as Port and Anchor.InstanceSpec: placed component instance, with x, y, rotation, flip, flop, mirror, and settings.BundleSpec: group of routed links.LinkSpec: one connection, including endpoints, xsection, family, width, radius, routing type, and optional manual points.
Endpoint formats accepted by the loader:
from: instance_name:pin_name
to: other_instance:pin_name
or equivalent explicit fields:
src_inst: instance_name
src_pin: pin_name
dst_inst: other_instance
dst_pin: pin_name
If a link xsection starts with metal, the loader defaults its route family to
electrical; otherwise it defaults to optical.
Build Order
builder.build_project_gds() builds cells in this order:
load all CellSpec objects
-> _ordered_specs()
-> composite cells first
-> project cells last
-> _build_cell(...) for each spec
-> _select_top_spec(...)
-> target_cell_name if provided
-> otherwise last ordered spec, normally the project cell
-> nd.export_gds(topcells=[built_cells[top.name]], filename=output_path)
This allows project-level YAML to instantiate composite cells that were built earlier in the same project directory.
How PDK Libraries Are Found And Loaded
The PDK tree is not stored inside mxpic_EDA or mxpic_router.
The active PDK root is selected by the EDA backend:
backend/pdk_access.py pdk_root_for_group()
-> manager -> MXPIC_PDK_ATLAS_ROOT or opt_pdk_atlas/foundries
-> developers -> MXPIC_PDK_PUBLIC_ROOT or opt_pdk_public/foundries
-> user -> MXPIC_PDK_PUBLIC_ROOT or opt_pdk_public/foundries
Expected PDK layout:
opt_pdk_public/
foundries/
Silterra/
EMO1_2ML_CU_Al_RDL/
technology.yml
primitives/
multimode_interferometers/
1x2MMI_.../
1x2MMI_....yml
1x2MMI_...._BB.gds
electronics/
composites/
The EDA library panel scans the selected project technology folder for
component folders. A folder containing a component .yml or .yaml file is a
component leaf. technology.yml is ignored during component scanning.
The YAML saved by the canvas stores each PDK component by path relative to the role PDK root. During GDS build:
builder._build_cell()
-> for each instance:
1. if built-in basic component:
-> create local Nazca primitive
2. else if instance.component is a previously built project cell:
-> place that built Cell
3. else:
-> _resolve_pdk_asset(pdk_root, instance.component, prefer_full_gds)
PDK asset resolution:
_resolve_pdk_asset()
-> direct path:
<pdk_root>/<component path from YAML>
-> fallback:
os.walk(pdk_root) until a folder basename matches the component name
-> metadata:
<component_name>.yml or <component_name>.yaml
-> GDS:
public/default: prefer <component_name>_BB.gds, then <component_name>.gds
manager/atlas: prefer <component_name>.gds, then <component_name>_BB.gds
fallback: first .gds in the folder using the same preference order
The PDK GDS is loaded and placed with Nazca:
loaded = nd.load_gds(asset["gds_path"])
loaded.put(instance.x, instance.y, instance.rotation, flip=..., flop=...)
The component metadata YAML is used to recover routable pins:
ports:
a1:
x: -28.5
y: 0.0
a: 180.0
width: 0.7
b1:
x: 58.5
y: 4.35
a: 0.0
width: 0.7
mxpic_router prefers a pins dictionary. For the external PDK roots
opt_pdk_public and opt_pdk_atlas, it also accepts ports as routable pins
for compatibility with generated PDK metadata.
Each metadata pin is transformed by the instance placement:
local pin x/y/a
-> apply flip/flop
-> rotate by instance.rotation
-> translate by instance.x / instance.y
-> register as pin_map[(instance_name, pin_name)]
How Ports Are Registered
During _build_cell(), every cell gets a local pin_map:
pin_map[(instance_name, pin_name)] = Nazca Pin
This map is the central contract between placement and routing.
Sources of pins:
-
Built-in basic components
waveguide,90 bend,180 bend,circle/cricle,taper- Created directly with Nazca primitives.
- Pins are registered from the placed primitive cell.
-
Previously built project or composite cells
- If
instance.componentmatches a cell built earlier in the project, the cell is placed hierarchically. - All placed pins except
orgare registered.
- If
-
External PDK GDS components
- GDS geometry is loaded with
nd.load_gds. - Routable pins are recreated from component metadata YAML.
- GDS geometry is loaded with
-
Top-level cell pins
- YAML
pins:become Nazca pins. - The builder also creates an inward-facing copy by rotating the pin 180 degrees.
- Registered as:
- YAML
pin_map[("this", pin_name)] = inward_pin
pin_map[(pin_name, pin_name)] = inward_pin
- Canvas elements
Portelements create named external-facing pins and internal_inpins.- Routing uses the internal
_inpins so links connect into the layout. Anchorelements create pairs such asanchor_a1andanchor_b1.
How Bundle Links Become GDS Routes
The canvas serializes React Flow edges into YAML under:
bundles:
output_bus:
routing_type: euler_bend
links:
- from: input:input_io1
to: mmi:a1
xsection: strip
family: optical
width: 0.45
radius: 10
routing_type: euler_bend
points:
- x: 100.0
y: 200.0
- x: 300.0
y: 200.0
For each link:
builder._route_link(link, pin_map, RouteBackend, warnings)
-> route = Route(
radius=link.radius or 10,
width=link.width,
xs=link.xsection,
PCB=True for metal_1/metal_2 else False,
)
-> p1 = pin_map[(link.src_inst, link.src_pin)]
-> p2 = pin_map[(link.dst_inst, link.dst_pin)]
Manual Point Routing
If link.points has at least two points:
if link also has pin endpoints:
first point is replaced by p1 coordinate
last point is replaced by p2 coordinate
_route_guided_link()
-> Route.strt_p2p(...) for straight segments
-> Route.bend_p2p(...) for corners when available
-> _guided_route_plan() trims corners by bend radius
If the route backend does not provide bend_p2p, corners are exported as
straight joins and a warning is added.
Automatic Pin To Pin Routing
If there are no manual points, the builder chooses the route method from pin angles:
same direction -> ubend_p2p
roughly opposite -> sbend_p2p
otherwise -> bend_p2p
missing method -> fallback to sbend_p2p
Then it calls the selected route backend method:
route_method(
pin1=p1,
pin2=p2,
width=link.width,
radius=link.radius or 10,
xs=link.xsection,
arrow=False,
).put()
If either endpoint pin is missing, the link is skipped and a warning is added:
Missing route pin for <src_inst>:<src_pin> -> <dst_inst>:<dst_pin>
Current Responsibilities By Module
mxpic_router/__init__.py
-> public exports, especially build_project_gds
mxpic_router/technology.py
-> read technology.yml
-> register Nazca layers
-> register Nazca xsections and layer mappings
mxpic_router/eda_loader.py
-> read saved cell YAML
-> parse pins, elements, instances, bundles, and links into dataclasses
-> normalize numbers, booleans, endpoints, points, and route family defaults
mxpic_router/builder.py
-> import Nazca
-> import mxpic_forge Route or Nazca Interconnect fallback
-> apply technology manifest
-> load all project YAML specs
-> build basic, project, and PDK instances
-> register routable pins
-> route bundle links
-> export final GDS
mxpic_router_legacy/
-> old legacy package, renamed so `mxpic` remains free for mxpic_forge
Maintenance Notes
- Keep
technology.ymlas the single source of truth for layer/xsection definitions. The frontend route editor and Nazca build path both consume it. - Keep PDK component metadata YAML beside its GDS files. The router needs that
metadata to recreate pins after
nd.load_gds. - Avoid reintroducing an internal
mxpicpackage inside this repo. When the forge checkout is present, the namemxpicmust continue to resolve tomxpic_forge. - When adding a new routing family or xsection, update
technology.ymlfirst, then verify that the selected route backend supports the required route method. - When adding a new component source, make sure it eventually contributes pins
to
pin_map; otherwise bundle links can load but cannot route.