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_router is this active router package.
  • mxpic_router_legacy is the old internal legacy package.
  • mxpic should resolve to mxpic_forge when that checkout is present, because mxpic_forge provides the preferred Route backend.

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 .yml or .yaml cell documents.
  • output_path: final GDS path.
  • pdk_root: role-scoped PDK root, usually one of:
    • opt_pdk_public/foundries
    • opt_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:

  1. 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.
  2. Previously built project or composite cells

    • If instance.component matches a cell built earlier in the project, the cell is placed hierarchically.
    • All placed pins except org are registered.
  3. External PDK GDS components

    • GDS geometry is loaded with nd.load_gds.
    • Routable pins are recreated from component metadata YAML.
  4. 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:
pin_map[("this", pin_name)] = inward_pin
pin_map[(pin_name, pin_name)] = inward_pin
  1. Canvas elements
    • Port elements create named external-facing pins and internal _in pins.
    • Routing uses the internal _in pins so links connect into the layout.
    • Anchor elements create pairs such as anchor_a1 and anchor_b1.

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.yml as 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 mxpic package inside this repo. When the forge checkout is present, the name mxpic must continue to resolve to mxpic_forge.
  • When adding a new routing family or xsection, update technology.yml first, 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.
S
Description
routing program for mxpic
Readme 226 KiB
Languages
Python 100%