# 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 /.yml -> backend/routed_layout_preview.py create_routed_layout_svg() -> mxpic_router.build_project_gds(..., target_cell_name=) -> temporary .gds -> gdstk.read_gds(...).top_level()[0].write_svg(...) -> /.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 .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 /// -> 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 ///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=(layer, datatype), overwrite=True) -> for manifest.xsections: nd.add_xsection(name=) nd.add_layer2xsection( xsection=, layer=, 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///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 /.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: / -> fallback: os.walk(pdk_root) until a folder basename matches the component name -> metadata: .yml or .yaml -> GDS: public/default: prefer _BB.gds, then .gds manager/atlas: prefer .gds, then _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 : -> : ``` ## 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.