519 lines
14 KiB
Markdown
519 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`, 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`, `mxpic_forge`, 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. For Build Layout, the YAML is still saved and SVG
|
|
preview is skipped when the 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_mxpic_forge_route()
|
|
-> adds sibling ../mxpic_forge to sys.path
|
|
-> removes any already-loaded non-forge mxpic modules
|
|
-> from mxpic import Route
|
|
```
|
|
|
|
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`, because `mxpic_forge` provides
|
|
`Route`.
|
|
|
|
## 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, Route, 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 `mxpic_forge.Route` 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
|
|
-> 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. 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 `mxpic_forge.Route` 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.
|