Files
2026-06-01 13:19:27 +08:00

526 lines
14 KiB
Markdown

# 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
```text
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
```
```text
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`.
```text
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:
```python
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:
```python
{
"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`.
```text
backend/server.py list_technologies()
-> current_pdk_root()
-> scan <pdk_root>/<foundry>/<technology>/
-> expose only directories containing technology.yml
```
```text
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:
```text
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:
```yaml
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.
```text
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:
```text
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:
```yaml
from: instance_name:pin_name
to: other_instance:pin_name
```
or equivalent explicit fields:
```yaml
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
_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:
```text
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:
```yaml
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:
```text
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`:
```python
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:
```python
pin_map[("this", pin_name)] = inward_pin
pin_map[(pin_name, pin_name)] = inward_pin
```
5. 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`.
## How Bundle Links Become GDS Routes
The canvas serializes React Flow edges into YAML under:
```yaml
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:
```text
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:
```text
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:
```text
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:
```python
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:
```text
Missing route pin for <src_inst>:<src_pin> -> <dst_inst>:<dst_pin>
```
## Current Responsibilities By Module
```text
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.