Description file added
This commit is contained in:
@@ -1,3 +1,518 @@
|
||||
# mxpic_router
|
||||
# mxpic_router GDS Generation Logic
|
||||
|
||||
routing program for mxpic
|
||||
`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.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Import-safe mxpic router package used by mxpic_EDA."""
|
||||
|
||||
from .builder import build_project_gds
|
||||
from .eda_loader import (
|
||||
BundleSpec,
|
||||
CellSpec,
|
||||
InstanceSpec,
|
||||
LinkSpec,
|
||||
PinSpec,
|
||||
load_cell_spec,
|
||||
parse_cell_dict,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BundleSpec",
|
||||
"CellSpec",
|
||||
"InstanceSpec",
|
||||
"LinkSpec",
|
||||
"PinSpec",
|
||||
"build_project_gds",
|
||||
"load_cell_spec",
|
||||
"parse_cell_dict",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,587 @@
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from .eda_loader import CellSpec, InstanceSpec, LinkSpec, load_cell_spec
|
||||
from .technology import apply_technology_manifest, load_technology_manifest
|
||||
|
||||
|
||||
def build_project_gds(
|
||||
project_dir: str,
|
||||
output_path: str,
|
||||
pdk_root: str,
|
||||
technology_manifest_path: Optional[str] = None,
|
||||
prefer_full_gds: bool = False,
|
||||
target_cell_name: Optional[str] = None,
|
||||
) -> dict:
|
||||
import nazca as nd
|
||||
|
||||
Route = _import_mxpic_forge_route()
|
||||
manifest = load_technology_manifest(technology_manifest_path)
|
||||
apply_technology_manifest(manifest, nd)
|
||||
|
||||
specs = _load_project_specs(project_dir)
|
||||
if not specs:
|
||||
raise ValueError("No saved cell YAML files found for this project")
|
||||
|
||||
built_cells = {}
|
||||
warnings = []
|
||||
for spec in _ordered_specs(specs):
|
||||
built_cells[spec.name] = _build_cell(spec, built_cells, pdk_root, prefer_full_gds, Route, nd, warnings)
|
||||
|
||||
top = _select_top_spec(specs, target_cell_name)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
nd.export_gds(topcells=[built_cells[top.name]], filename=output_path)
|
||||
return {
|
||||
"output_path": output_path,
|
||||
"engine": "mxpic_router",
|
||||
"cells_built": [spec.name for spec in _ordered_specs(specs)],
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
|
||||
def _load_project_specs(project_dir: str) -> Dict[str, CellSpec]:
|
||||
specs = {}
|
||||
for filename in sorted(os.listdir(project_dir)):
|
||||
if not filename.lower().endswith((".yml", ".yaml")):
|
||||
continue
|
||||
spec = load_cell_spec(os.path.join(project_dir, filename))
|
||||
specs[spec.name] = spec
|
||||
return specs
|
||||
|
||||
|
||||
def _ordered_specs(specs: Dict[str, CellSpec]):
|
||||
composites = [spec for spec in specs.values() if spec.type != "project"]
|
||||
projects = [spec for spec in specs.values() if spec.type == "project"]
|
||||
return composites + projects
|
||||
|
||||
|
||||
def _select_top_spec(specs: Dict[str, CellSpec], target_cell_name: Optional[str]):
|
||||
if target_cell_name:
|
||||
if target_cell_name not in specs:
|
||||
raise ValueError(f"Target cell not found for routed build: {target_cell_name}")
|
||||
return specs[target_cell_name]
|
||||
return _ordered_specs(specs)[-1]
|
||||
|
||||
|
||||
def _build_cell(spec: CellSpec, built_cells: dict, pdk_root: str, prefer_full_gds: bool, Route, nd, warnings: list):
|
||||
pin_map = {}
|
||||
with nd.Cell(name=spec.name) as top:
|
||||
for instance_name, instance in spec.instances.items():
|
||||
if _is_basic_component(instance.component):
|
||||
basic_cell = _build_basic_component(instance, nd)
|
||||
placed = basic_cell.put(instance.x, instance.y, instance.rotation, flip=instance.flip, flop=instance.flop)
|
||||
for pin_name in getattr(placed, "pin", {}):
|
||||
if pin_name != "org":
|
||||
pin_map[(instance_name, pin_name)] = placed.pin[pin_name]
|
||||
continue
|
||||
|
||||
if instance.component in built_cells:
|
||||
placed = built_cells[instance.component].put(instance.x, instance.y, instance.rotation, flip=instance.flip, flop=instance.flop)
|
||||
for pin_name in getattr(placed, "pin", {}):
|
||||
if pin_name != "org":
|
||||
pin_map[(instance_name, pin_name)] = placed.pin[pin_name]
|
||||
continue
|
||||
|
||||
asset = _resolve_pdk_asset(pdk_root, instance.component, prefer_full_gds)
|
||||
if not asset.get("gds_path"):
|
||||
warnings.append(f"Missing GDS for {instance_name}: {instance.component}")
|
||||
continue
|
||||
loaded = nd.load_gds(asset["gds_path"])
|
||||
loaded.put(instance.x, instance.y, instance.rotation, flip=instance.flip, flop=instance.flop)
|
||||
_register_metadata_pins(pin_map, instance_name, instance, asset.get("metadata") or {}, nd, pdk_root)
|
||||
|
||||
for pin_name, pin in spec.pins.items():
|
||||
pin_out = nd.Pin(pin_name, width=pin.width).put(pin.x, pin.y, pin.angle)
|
||||
pin_in = pin_out.move(0, 0, 180)
|
||||
pin_map[("this", pin_name)] = pin_in
|
||||
pin_map[(pin_name, pin_name)] = pin_in
|
||||
|
||||
for element_name, element in spec.elements.items():
|
||||
_register_element_pins(pin_map, element_name, element, nd)
|
||||
|
||||
for bundle in spec.bundles.values():
|
||||
for link in bundle.links:
|
||||
_route_link(link, pin_map, Route, warnings)
|
||||
return top
|
||||
|
||||
|
||||
def _is_basic_component(component: str) -> bool:
|
||||
return component in {"waveguide", "90 bend", "180 bend", "cricle", "circle", "taper"}
|
||||
|
||||
|
||||
def _build_basic_component(instance: InstanceSpec, nd):
|
||||
settings = instance.settings or {}
|
||||
component = "cricle" if instance.component == "circle" else instance.component
|
||||
width = _safe_float(settings.get("width"), _safe_float(settings.get("width1"), 0.5)) or 0.5
|
||||
xs = settings.get("xsection") or settings.get("xs") or "strip"
|
||||
radius = _safe_float(settings.get("radius"), 10) or 10
|
||||
length = _safe_float(settings.get("length"), 100) or 100
|
||||
name = f"basic_{component.replace(' ', '_')}_{_safe_cell_name(instance.name)}"
|
||||
with nd.Cell(name=name, instantiate=False) as cell:
|
||||
if component == "waveguide":
|
||||
line = nd.strt(length=length, width=width, xs=xs).put(0, 0, 0)
|
||||
nd.Pin("a1", width=width, xs=xs).put(line.pin["a0"])
|
||||
nd.Pin("b1", width=width, xs=xs).put(line.pin["b0"])
|
||||
elif component == "90 bend":
|
||||
bend = nd.bend(radius=radius, width=width, angle=90, xs=xs).put(0, 0, 0)
|
||||
nd.Pin("a1", width=width, xs=xs).put(bend.pin["a0"])
|
||||
nd.Pin("b1", width=width, xs=xs).put(bend.pin["b0"])
|
||||
elif component == "180 bend":
|
||||
bend = nd.bend(radius=radius, width=width, angle=180, xs=xs).put(0, 0, 0)
|
||||
nd.Pin("a1", width=width, xs=xs).put(bend.pin["a0"])
|
||||
nd.Pin("b1", width=width, xs=xs).put(bend.pin["b0"])
|
||||
elif component == "cricle":
|
||||
bend = nd.bend(radius=radius, width=width, angle=360, xs=xs).put(0, 0, 0)
|
||||
nd.Pin("a1", width=width, xs=xs).put(bend.pin["a0"])
|
||||
nd.Pin("b1", width=width, xs=xs).put(bend.pin["b0"])
|
||||
elif component == "taper":
|
||||
width1 = _safe_float(settings.get("width1"), width) or width
|
||||
width2 = _safe_float(settings.get("width2"), width) or width
|
||||
taper = nd.taper(length=length, width1=width1, width2=width2, xs=xs).put(0, 0, 0)
|
||||
nd.Pin("a1", width=width1, xs=xs).put(taper.pin["a0"])
|
||||
nd.Pin("b1", width=width2, xs=xs).put(taper.pin["b0"])
|
||||
return cell
|
||||
|
||||
|
||||
def _register_element_pins(pin_map: dict, element_name: str, element, nd) -> None:
|
||||
element_cell = _build_element_cell(element_name, element, nd)
|
||||
placed = element_cell.put(element.x, element.y, element.angle)
|
||||
if element.type == "port":
|
||||
for pin in _port_element_pin_entries(element_name, element):
|
||||
pin_map[(element_name, pin["name"])] = placed.pin[f'{pin["name"]}_in']
|
||||
return
|
||||
|
||||
for pin in _anchor_element_pin_entries(element_name, element):
|
||||
pin_map[(element_name, pin["name"])] = placed.pin[pin["name"]]
|
||||
|
||||
|
||||
def _build_element_cell(element_name: str, element, nd):
|
||||
width = element.width or 0.5
|
||||
port_number = max(1, int(getattr(element, "port_number", 1) or 1))
|
||||
pitch = _safe_float(getattr(element, "pitch", 10.0), 10.0) or 10.0
|
||||
with nd.Cell(name=f"element_{_safe_cell_name(element_name)}", instantiate=False) as cell:
|
||||
if element.type == "port":
|
||||
for index, pin in enumerate(_port_element_pin_entries(element_name, element)):
|
||||
local_y = 0.0 if port_number == 1 else _element_port_offset(index, port_number, pitch)
|
||||
pin_out = nd.Pin(pin["name"], width=width).put(0.0, local_y, 180.0)
|
||||
pin_in = pin_out.move(0, 0, 180)
|
||||
nd.Pin(f'{pin["name"]}_in', width=width).put(pin_in.x, pin_in.y, pin_in.a)
|
||||
return cell
|
||||
|
||||
anchor_pins = _anchor_element_pin_entries(element_name, element)
|
||||
for index in range(port_number):
|
||||
local_y = -15.0 + _element_port_offset(index, port_number, pitch)
|
||||
left_pin = anchor_pins[index * 2]
|
||||
right_pin = anchor_pins[index * 2 + 1]
|
||||
nd.Pin(left_pin["name"], width=width).put(0.0, local_y, 180.0)
|
||||
nd.Pin(right_pin["name"], width=width).put(0.0, local_y, 0.0)
|
||||
return cell
|
||||
|
||||
|
||||
def _element_port_offset(index: int, count: int, pitch: float) -> float:
|
||||
return ((count - 1) / 2 - index) * pitch
|
||||
|
||||
|
||||
def _port_element_pin_entries(element_name: str, element) -> list:
|
||||
count = max(1, int(getattr(element, "port_number", 1) or 1))
|
||||
defaults = [{"role": f"io{index + 1}", "name": f"{_safe_cell_name(element_name)}_io{index + 1}"} for index in range(count)]
|
||||
return _named_pin_entries(defaults, getattr(element, "pins", None))
|
||||
|
||||
|
||||
def _anchor_element_pin_entries(element_name: str, element) -> list:
|
||||
count = max(1, int(getattr(element, "port_number", 1) or 1))
|
||||
defaults = []
|
||||
for index in range(count):
|
||||
number = index + 1
|
||||
defaults.append({"role": f"a{number}", "name": f"{_safe_cell_name(element_name)}_a{number}"})
|
||||
defaults.append({"role": f"b{number}", "name": f"{_safe_cell_name(element_name)}_b{number}"})
|
||||
return _named_pin_entries(defaults, getattr(element, "pins", None))
|
||||
|
||||
|
||||
def _named_pin_entries(defaults: list, overrides) -> list:
|
||||
by_role = {str(pin.get("role") or ""): str(pin.get("name") or "") for pin in overrides or []}
|
||||
by_index = [str(pin.get("name") or "") for pin in overrides or []]
|
||||
entries = []
|
||||
for index, default in enumerate(defaults):
|
||||
role = default["role"]
|
||||
name = by_role.get(role) or (by_index[index] if index < len(by_index) else "") or default["name"]
|
||||
entries.append({"role": role, "name": _safe_cell_name(name)})
|
||||
return entries
|
||||
|
||||
|
||||
def _transform_element_port(local_x: float, local_y: float, angle: float, element) -> tuple:
|
||||
rotation = math.radians(_safe_float(getattr(element, "angle", 0.0), 0.0) or 0.0)
|
||||
cos_v = math.cos(rotation)
|
||||
sin_v = math.sin(rotation)
|
||||
x = element.x + local_x * cos_v - local_y * sin_v
|
||||
y = element.y + local_x * sin_v + local_y * cos_v
|
||||
return x, y, angle
|
||||
|
||||
|
||||
def _safe_cell_name(value: str) -> str:
|
||||
return "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in str(value or "inst"))
|
||||
|
||||
|
||||
def _register_metadata_pins(pin_map, instance_name, instance, metadata: dict, nd, pdk_root: str) -> None:
|
||||
for pin_name, pin in _metadata_pins(metadata, pdk_root).items():
|
||||
x, y, angle = _transform_port(
|
||||
_safe_float(pin.get("x"), 0.0),
|
||||
_safe_float(pin.get("y"), 0.0),
|
||||
_safe_float(pin.get("a", pin.get("angle")), 0.0),
|
||||
instance.x,
|
||||
instance.y,
|
||||
instance.rotation,
|
||||
instance.flip,
|
||||
instance.flop,
|
||||
)
|
||||
width = _safe_float(pin.get("width"), 0.5) or 0.5
|
||||
pin_map[(instance_name, str(pin_name))] = nd.Pin(
|
||||
f"{instance_name}_{pin_name}",
|
||||
width=width,
|
||||
xs="strip",
|
||||
).put(x, y, angle)
|
||||
|
||||
|
||||
def _metadata_pins(metadata: dict, pdk_root: str) -> dict:
|
||||
if not metadata:
|
||||
return {}
|
||||
if isinstance(metadata.get("pins"), dict):
|
||||
return metadata.get("pins") or {}
|
||||
if isinstance(metadata.get("ports"), dict) and _allows_pdk_ports_as_pins(pdk_root):
|
||||
return metadata.get("ports") or {}
|
||||
return {}
|
||||
|
||||
|
||||
def _allows_pdk_ports_as_pins(pdk_root: str) -> bool:
|
||||
normalized = os.path.abspath(str(pdk_root or "")).replace("\\", "/").lower()
|
||||
return "/opt_pdk_public/" in f"{normalized}/" or "/opt_pdk_atlas/" in f"{normalized}/"
|
||||
|
||||
|
||||
def _metal_route_pcb_enabled(xsection: str) -> bool:
|
||||
normalized = str(xsection or "").strip().lower().replace("-", "_")
|
||||
return normalized in {"metal_1", "metal1", "metal_2", "metal2"}
|
||||
|
||||
|
||||
def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> None:
|
||||
route = Route(radius=link.radius or 10, width=link.width, xs=link.xsection, PCB=_metal_route_pcb_enabled(link.xsection))
|
||||
p1 = pin_map.get((link.src_inst, link.src_pin))
|
||||
p2 = pin_map.get((link.dst_inst, link.dst_pin))
|
||||
has_pin_endpoints = bool(link.src_inst or link.src_pin or link.dst_inst or link.dst_pin)
|
||||
if len(link.points or []) >= 2:
|
||||
if has_pin_endpoints:
|
||||
if p1 is None or p2 is None:
|
||||
warnings.append(f"Missing route pin for {link.src_inst}:{link.src_pin} -> {link.dst_inst}:{link.dst_pin}")
|
||||
return
|
||||
points = _route_points_with_pin_endpoints(link.points, p1, p2)
|
||||
else:
|
||||
points = link.points
|
||||
if _route_guided_link(link, route, warnings, points):
|
||||
return
|
||||
if p1 is None or p2 is None:
|
||||
warnings.append(f"Missing route pin for {link.src_inst}:{link.src_pin} -> {link.dst_inst}:{link.dst_pin}")
|
||||
return
|
||||
method_name = _route_method_name_for_pins(p1, p2)
|
||||
route_method = getattr(route, method_name, None)
|
||||
if route_method is None:
|
||||
warnings.append(f"Route method {method_name} unavailable; falling back to sbend_p2p")
|
||||
route_method = route.sbend_p2p
|
||||
route_method(
|
||||
pin1=p1,
|
||||
pin2=p2,
|
||||
width=link.width,
|
||||
radius=link.radius or 10,
|
||||
xs=link.xsection,
|
||||
arrow=False,
|
||||
).put()
|
||||
|
||||
def _route_guided_link(link: LinkSpec, route, warnings: list, points_override=None) -> bool:
|
||||
route_method = getattr(route, "strt_p2p", None)
|
||||
if route_method is None:
|
||||
warnings.append("Manual route points require Route.strt_p2p; falling back to automatic p2p route")
|
||||
return False
|
||||
bend_method = getattr(route, "bend_p2p", None)
|
||||
source_points = points_override if points_override is not None else (link.points or [])
|
||||
points = [
|
||||
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
|
||||
for point in source_points
|
||||
]
|
||||
points = [point for point in points if point["x"] is not None and point["y"] is not None]
|
||||
if len(points) < 2:
|
||||
return False
|
||||
if bend_method is None:
|
||||
warnings.append("Manual route bends require Route.bend_p2p; corners were exported as straight joins")
|
||||
plan = _guided_route_plan(points, (link.radius or 10) if bend_method is not None else 0)
|
||||
for straight in plan["straights"]:
|
||||
route_method(
|
||||
pin1=(straight["start"]["x"], straight["start"]["y"], straight["angle"]),
|
||||
pin2=(straight["end"]["x"], straight["end"]["y"], straight["angle"]),
|
||||
width=link.width,
|
||||
xs=link.xsection,
|
||||
arrow=False,
|
||||
).put()
|
||||
if bend_method is None:
|
||||
return True
|
||||
for bend in plan["bends"]:
|
||||
bend_method(
|
||||
pin1=(bend["start"]["x"], bend["start"]["y"], bend["angle_in"]),
|
||||
pin2=(bend["end"]["x"], bend["end"]["y"], bend["angle_out"]),
|
||||
radius=link.radius or 10,
|
||||
width=link.width,
|
||||
xs=link.xsection,
|
||||
arrow=False,
|
||||
).put()
|
||||
return True
|
||||
|
||||
|
||||
def _route_points_with_pin_endpoints(points: list, source_pin=None, target_pin=None) -> list:
|
||||
clean_points = [
|
||||
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
|
||||
for point in points or []
|
||||
if isinstance(point, dict)
|
||||
]
|
||||
clean_points = [point for point in clean_points if point["x"] is not None and point["y"] is not None]
|
||||
if len(clean_points) < 2:
|
||||
return clean_points
|
||||
source_point = _pin_point(source_pin)
|
||||
target_point = _pin_point(target_pin)
|
||||
if source_point is not None:
|
||||
clean_points[0] = source_point
|
||||
if target_point is not None:
|
||||
clean_points[-1] = target_point
|
||||
return clean_points
|
||||
|
||||
|
||||
def _pin_point(pin):
|
||||
for source in (pin, getattr(pin, "pointer", None)):
|
||||
if source is None:
|
||||
continue
|
||||
x = _safe_float(getattr(source, "x", None), None)
|
||||
y = _safe_float(getattr(source, "y", None), None)
|
||||
if x is not None and y is not None:
|
||||
return {"x": float(x), "y": float(y)}
|
||||
xya = getattr(source, "xya", None)
|
||||
if callable(xya):
|
||||
try:
|
||||
values = xya()
|
||||
except TypeError:
|
||||
values = None
|
||||
if values and len(values) >= 2:
|
||||
x = _safe_float(values[0], None)
|
||||
y = _safe_float(values[1], None)
|
||||
if x is not None and y is not None:
|
||||
return {"x": float(x), "y": float(y)}
|
||||
return None
|
||||
|
||||
|
||||
def _guided_route_plan(points: list, radius: float) -> dict:
|
||||
clean_points = [
|
||||
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
|
||||
for point in points or []
|
||||
if isinstance(point, dict)
|
||||
]
|
||||
clean_points = [point for point in clean_points if point["x"] is not None and point["y"] is not None]
|
||||
if len(clean_points) < 2:
|
||||
return {"straights": [], "bends": []}
|
||||
|
||||
trim_radius = max(_safe_float(radius, 0.0) or 0.0, 0.0)
|
||||
corner_trims = {}
|
||||
bends = []
|
||||
for index in range(1, len(clean_points) - 1):
|
||||
previous_point = clean_points[index - 1]
|
||||
corner = clean_points[index]
|
||||
next_point = clean_points[index + 1]
|
||||
angle_in = _segment_angle(previous_point, corner)
|
||||
angle_out = _segment_angle(corner, next_point)
|
||||
if angle_in is None or angle_out is None or angle_in == angle_out:
|
||||
continue
|
||||
turn_delta = abs(((angle_out - angle_in + 180) % 360) - 180)
|
||||
if turn_delta >= 179:
|
||||
continue
|
||||
previous_length = _distance(previous_point, corner)
|
||||
next_length = _distance(corner, next_point)
|
||||
trim = min(trim_radius, previous_length / 2.0, next_length / 2.0)
|
||||
if trim <= 1e-9:
|
||||
continue
|
||||
before = _point_toward(corner, previous_point, trim)
|
||||
after = _point_toward(corner, next_point, trim)
|
||||
corner_trims[index] = {"before": before, "after": after}
|
||||
bends.append({
|
||||
"start": before,
|
||||
"corner": {"x": float(corner["x"]), "y": float(corner["y"])},
|
||||
"end": after,
|
||||
"angle_in": angle_in,
|
||||
"angle_out": angle_out,
|
||||
})
|
||||
|
||||
straights = []
|
||||
for index in range(len(clean_points) - 1):
|
||||
start = corner_trims.get(index, {}).get("after", clean_points[index])
|
||||
end = corner_trims.get(index + 1, {}).get("before", clean_points[index + 1])
|
||||
angle = _segment_angle(start, end)
|
||||
if angle is None or _distance(start, end) <= 1e-9:
|
||||
continue
|
||||
straights.append({
|
||||
"start": {"x": float(start["x"]), "y": float(start["y"])},
|
||||
"end": {"x": float(end["x"]), "y": float(end["y"])},
|
||||
"angle": angle,
|
||||
})
|
||||
|
||||
return {"straights": straights, "bends": bends}
|
||||
|
||||
|
||||
def _distance(point1: dict, point2: dict) -> float:
|
||||
return math.hypot(point2["x"] - point1["x"], point2["y"] - point1["y"])
|
||||
|
||||
|
||||
def _point_toward(origin: dict, target: dict, distance: float) -> dict:
|
||||
total = _distance(origin, target)
|
||||
if total <= 1e-9:
|
||||
return {"x": float(origin["x"]), "y": float(origin["y"])}
|
||||
ratio = distance / total
|
||||
return {
|
||||
"x": float(origin["x"] + (target["x"] - origin["x"]) * ratio),
|
||||
"y": float(origin["y"] + (target["y"] - origin["y"]) * ratio),
|
||||
}
|
||||
|
||||
|
||||
def _segment_angle(point1: dict, point2: dict):
|
||||
dx = point2["x"] - point1["x"]
|
||||
dy = point2["y"] - point1["y"]
|
||||
if abs(dx) < 1e-9 and abs(dy) < 1e-9:
|
||||
return None
|
||||
if abs(dx) >= abs(dy):
|
||||
return 0 if dx >= 0 else 180
|
||||
return 90 if dy >= 0 else 270
|
||||
|
||||
|
||||
def _pins_have_same_rotation(pin1, pin2, tolerance: float = 1e-6) -> bool:
|
||||
angle1 = _pin_angle(pin1)
|
||||
angle2 = _pin_angle(pin2)
|
||||
if angle1 is None or angle2 is None:
|
||||
return False
|
||||
return abs(((angle1 - angle2 + 180) % 360) - 180) <= tolerance
|
||||
|
||||
|
||||
def _route_method_name_for_pins(pin1, pin2) -> str:
|
||||
angle1 = _pin_angle(pin1)
|
||||
angle2 = _pin_angle(pin2)
|
||||
if angle1 is None or angle2 is None:
|
||||
return "sbend_p2p"
|
||||
delta = (angle2 - angle1) % 360
|
||||
if delta <= 1e-6 or abs(delta - 360) <= 1e-6:
|
||||
return "ubend_p2p"
|
||||
if 120 < delta < 240:
|
||||
return "sbend_p2p"
|
||||
return "bend_p2p"
|
||||
|
||||
|
||||
def _pin_angle(pin):
|
||||
for source in (pin, getattr(pin, "pointer", None)):
|
||||
if source is None:
|
||||
continue
|
||||
for attr in ("a", "angle", "rot", "rotation"):
|
||||
value = getattr(source, attr, None)
|
||||
if callable(value):
|
||||
try:
|
||||
value = value()
|
||||
except TypeError:
|
||||
continue
|
||||
number = _safe_float(value, None)
|
||||
if number is not None:
|
||||
return number % 360
|
||||
xya = getattr(source, "xya", None)
|
||||
if callable(xya):
|
||||
try:
|
||||
values = xya()
|
||||
except TypeError:
|
||||
values = None
|
||||
if values and len(values) >= 3:
|
||||
number = _safe_float(values[2], None)
|
||||
if number is not None:
|
||||
return number % 360
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_pdk_asset(pdk_root: str, component: str, prefer_full_gds: bool) -> dict:
|
||||
key = (component or "").replace("\\", "/").strip("/")
|
||||
name = key.split("/")[-1]
|
||||
direct = os.path.join(pdk_root, *key.split("/"))
|
||||
search_dirs = [direct] if os.path.isdir(direct) else []
|
||||
if not search_dirs:
|
||||
for root, dirs, _ in os.walk(pdk_root):
|
||||
if os.path.basename(root) == name:
|
||||
search_dirs.append(root)
|
||||
dirs.clear()
|
||||
break
|
||||
if not search_dirs:
|
||||
return {}
|
||||
|
||||
search_dir = search_dirs[0]
|
||||
yaml_path = _first_existing(search_dir, [f"{name}.yml", f"{name}.yaml"])
|
||||
gds_path = _find_gds(search_dir, name, prefer_full_gds)
|
||||
metadata = {}
|
||||
if yaml_path:
|
||||
with open(yaml_path, "r", encoding="utf-8") as file:
|
||||
metadata = yaml.safe_load(file) or {}
|
||||
return {"yaml_path": yaml_path, "gds_path": gds_path, "metadata": metadata}
|
||||
|
||||
|
||||
def _find_gds(search_dir: str, name: str, prefer_full_gds: bool):
|
||||
preferred = [f"{name}.gds", f"{name}_BB.gds"] if prefer_full_gds else [f"{name}_BB.gds", f"{name}.gds"]
|
||||
found = _first_existing(search_dir, preferred)
|
||||
if found:
|
||||
return found
|
||||
files = sorted(filename for filename in os.listdir(search_dir) if filename.lower().endswith(".gds"))
|
||||
full = [filename for filename in files if not filename.lower().endswith("_bb.gds")]
|
||||
bb = [filename for filename in files if filename.lower().endswith("_bb.gds")]
|
||||
ordered = full + bb if prefer_full_gds else bb + full
|
||||
return os.path.join(search_dir, ordered[0]) if ordered else None
|
||||
|
||||
|
||||
def _first_existing(search_dir: str, filenames: list):
|
||||
for filename in filenames:
|
||||
path = os.path.join(search_dir, filename)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def _transform_port(px: float, py: float, pa: float, ix: float, iy: float, rotation: float, flip: bool = False, flop: bool = False):
|
||||
if flip:
|
||||
py = -py
|
||||
pa = -pa
|
||||
if flop:
|
||||
px = -px
|
||||
pa = 180 - pa
|
||||
theta = math.radians(rotation)
|
||||
c, s = math.cos(theta), math.sin(theta)
|
||||
return ix + px * c - py * s, iy + px * s + py * c, (pa + rotation) % 360
|
||||
|
||||
|
||||
def _safe_float(value, default=0.0):
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, str) and value.strip().lower() in {"", "none", "null"}:
|
||||
return default
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _import_mxpic_forge_route():
|
||||
forge_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "mxpic_forge"))
|
||||
if os.path.isdir(forge_root) and forge_root not in sys.path:
|
||||
sys.path.insert(0, forge_root)
|
||||
loaded_mxpic = sys.modules.get("mxpic")
|
||||
loaded_path = os.path.abspath(getattr(loaded_mxpic, "__file__", "")) if loaded_mxpic else ""
|
||||
if loaded_mxpic and forge_root not in loaded_path:
|
||||
for module_name in list(sys.modules):
|
||||
if module_name == "mxpic" or module_name.startswith("mxpic."):
|
||||
del sys.modules[module_name]
|
||||
from mxpic import Route
|
||||
return Route
|
||||
@@ -0,0 +1,227 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class PinSpec:
|
||||
name: str
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
angle: float = 0.0
|
||||
width: float = 0.5
|
||||
layer: str = "WG_CORE"
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstanceSpec:
|
||||
name: str
|
||||
component: str
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
rotation: float = 0.0
|
||||
flip: bool = False
|
||||
flop: bool = False
|
||||
mirror: bool = False
|
||||
settings: Dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ElementSpec:
|
||||
name: str
|
||||
type: str
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
angle: float = 0.0
|
||||
width: float = 0.5
|
||||
port_number: int = 1
|
||||
pitch: float = 10.0
|
||||
layer: str = "WG_CORE"
|
||||
description: str = ""
|
||||
pins: List[Dict[str, str]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkSpec:
|
||||
src_inst: str = ""
|
||||
src_pin: str = ""
|
||||
dst_inst: str = ""
|
||||
dst_pin: str = ""
|
||||
xsection: str = "strip"
|
||||
family: str = "optical"
|
||||
width: Optional[float] = None
|
||||
radius: Optional[float] = None
|
||||
routing_type: str = "euler_bend"
|
||||
points: List[Dict[str, float]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleSpec:
|
||||
name: str
|
||||
links: List[LinkSpec] = field(default_factory=list)
|
||||
routing_type: str = "euler_bend"
|
||||
radius: Optional[float] = None
|
||||
width: Optional[float] = None
|
||||
xsection: str = "strip"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CellSpec:
|
||||
name: str
|
||||
type: str = "composite"
|
||||
version: str = "1.0.0"
|
||||
pins: Dict[str, PinSpec] = field(default_factory=dict)
|
||||
elements: Dict[str, ElementSpec] = field(default_factory=dict)
|
||||
instances: Dict[str, InstanceSpec] = field(default_factory=dict)
|
||||
bundles: Dict[str, BundleSpec] = field(default_factory=dict)
|
||||
|
||||
|
||||
def load_cell_spec(path: str) -> CellSpec:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return parse_cell_dict(yaml.safe_load(file) or {})
|
||||
|
||||
|
||||
def parse_cell_dict(data: dict) -> CellSpec:
|
||||
spec = CellSpec(
|
||||
name=str(data.get("name") or "cell"),
|
||||
type=str(data.get("type") or "composite"),
|
||||
version=str(data.get("version") or "1.0.0"),
|
||||
)
|
||||
|
||||
for pin in data.get("pins", []) or []:
|
||||
name = str(pin.get("name") or "pin")
|
||||
spec.pins[name] = PinSpec(
|
||||
name=name,
|
||||
x=_float(pin.get("x")),
|
||||
y=_float(pin.get("y")),
|
||||
angle=_float(pin.get("angle", pin.get("a"))),
|
||||
width=_float(pin.get("width"), 0.5),
|
||||
layer=str(pin.get("layer") or "WG_CORE"),
|
||||
)
|
||||
|
||||
for element_name, element in (data.get("elements") or {}).items():
|
||||
spec.elements[str(element_name)] = ElementSpec(
|
||||
name=str(element_name),
|
||||
type=str(element.get("type") or "anchor"),
|
||||
x=_float(element.get("x")),
|
||||
y=_float(element.get("y")),
|
||||
angle=_float(element.get("angle", element.get("a"))),
|
||||
width=_float(element.get("width"), 0.5),
|
||||
port_number=_int(element.get("pin_number", element.get("pinNumber", element.get("port_number", element.get("portNumber")))), 1),
|
||||
pitch=_float(element.get("pitch"), 10.0),
|
||||
layer=str(element.get("layer") or "WG_CORE"),
|
||||
description=str(element.get("description") or ""),
|
||||
pins=_pins(element.get("pins")),
|
||||
)
|
||||
|
||||
for instance_name, instance in (data.get("instances") or {}).items():
|
||||
spec.instances[str(instance_name)] = InstanceSpec(
|
||||
name=str(instance_name),
|
||||
component=str(instance.get("component") or ""),
|
||||
x=_float(instance.get("x")),
|
||||
y=_float(instance.get("y")),
|
||||
rotation=_float(instance.get("rotation")),
|
||||
flip=_bool(instance.get("flip", instance.get("mirror", False))),
|
||||
flop=_bool(instance.get("flop", False)),
|
||||
mirror=_bool(instance.get("mirror", instance.get("flip", False))),
|
||||
settings=instance.get("settings") or {},
|
||||
)
|
||||
|
||||
for bundle_name, bundle_data in (data.get("bundles") or {}).items():
|
||||
bundle = BundleSpec(
|
||||
name=str(bundle_name),
|
||||
routing_type=str(bundle_data.get("routing_type") or "euler_bend"),
|
||||
radius=_optional_float(bundle_data.get("radius")),
|
||||
width=_optional_float(bundle_data.get("width")),
|
||||
xsection=str(bundle_data.get("xsection") or bundle_data.get("xs") or "strip"),
|
||||
)
|
||||
for link_data in bundle_data.get("links", []) or []:
|
||||
src_inst, src_pin = _endpoint(link_data.get("from"), link_data.get("src_inst"), link_data.get("src_pin"))
|
||||
dst_inst, dst_pin = _endpoint(link_data.get("to"), link_data.get("dst_inst"), link_data.get("dst_pin"))
|
||||
xsection = str(link_data.get("xsection") or link_data.get("xs") or bundle.xsection)
|
||||
bundle.links.append(LinkSpec(
|
||||
src_inst=src_inst,
|
||||
src_pin=src_pin,
|
||||
dst_inst=dst_inst,
|
||||
dst_pin=dst_pin,
|
||||
xsection=xsection,
|
||||
family=str(link_data.get("family") or _family_from_xsection(xsection)),
|
||||
width=_optional_float(link_data.get("width"), bundle.width),
|
||||
radius=_optional_float(link_data.get("radius"), bundle.radius),
|
||||
routing_type=str(link_data.get("routing_type") or bundle.routing_type),
|
||||
points=_points(link_data.get("points")),
|
||||
))
|
||||
spec.bundles[bundle.name] = bundle
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
def _endpoint(compact, inst, port):
|
||||
if compact:
|
||||
value = str(compact)
|
||||
if ":" in value:
|
||||
left, right = value.split(":", 1)
|
||||
return left, right
|
||||
if inst or port:
|
||||
return str(inst or ""), str(port or "")
|
||||
return "", ""
|
||||
|
||||
|
||||
def _family_from_xsection(xsection: str) -> str:
|
||||
return "electrical" if str(xsection).startswith("metal") else "optical"
|
||||
|
||||
|
||||
def _points(points) -> List[Dict[str, float]]:
|
||||
parsed = []
|
||||
for point in points or []:
|
||||
if not isinstance(point, dict):
|
||||
continue
|
||||
x = _optional_float(point.get("x"))
|
||||
y = _optional_float(point.get("y"))
|
||||
if x is None or y is None:
|
||||
continue
|
||||
parsed.append({"x": x, "y": y})
|
||||
return parsed
|
||||
|
||||
|
||||
def _pins(pins) -> List[Dict[str, str]]:
|
||||
parsed = []
|
||||
for pin in pins or []:
|
||||
if not isinstance(pin, dict):
|
||||
continue
|
||||
name = str(pin.get("name") or "").strip()
|
||||
role = str(pin.get("role") or "").strip()
|
||||
if name:
|
||||
parsed.append({"name": name, "role": role})
|
||||
return parsed
|
||||
|
||||
|
||||
def _optional_float(value, default=None):
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return _float(value, default)
|
||||
|
||||
|
||||
def _bool(value) -> bool:
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
return bool(value)
|
||||
|
||||
|
||||
def _float(value, default=0.0):
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _int(value, default=0):
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return max(1, int(float(value)))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def load_technology_manifest(path: str) -> dict:
|
||||
if not path or not os.path.exists(path):
|
||||
return {}
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
|
||||
|
||||
def apply_technology_manifest(manifest: dict, nd) -> None:
|
||||
if not manifest:
|
||||
return
|
||||
for name, layer_info in (manifest.get("layers") or {}).items():
|
||||
layer = layer_info.get("layer")
|
||||
datatype = layer_info.get("datatype", 0)
|
||||
if layer is None:
|
||||
continue
|
||||
nd.add_layer(name=name, layer=(int(layer), int(datatype)), overwrite=True)
|
||||
|
||||
for xsection, info in (manifest.get("xsections") or {}).items():
|
||||
nd.add_xsection(name=xsection)
|
||||
for layer_binding in info.get("layers", []) or []:
|
||||
layer_name = layer_binding.get("layer")
|
||||
if not layer_name:
|
||||
continue
|
||||
kwargs = {"xsection": xsection, "layer": layer_name, "overwrite": True}
|
||||
if "growx" in layer_binding:
|
||||
kwargs["growx"] = layer_binding.get("growx", 0)
|
||||
if "growy" in layer_binding:
|
||||
kwargs["growy"] = layer_binding.get("growy", 0)
|
||||
if "leftedge" in layer_binding:
|
||||
kwargs["leftedge"] = tuple(layer_binding["leftedge"])
|
||||
if "rightedge" in layer_binding:
|
||||
kwargs["rightedge"] = tuple(layer_binding["rightedge"])
|
||||
nd.add_layer2xsection(**kwargs)
|
||||
Binary file not shown.
@@ -0,0 +1,226 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
|
||||
class EdaRouterPinsContractTest(unittest.TestCase):
|
||||
def test_loader_uses_top_level_pins_and_ignores_legacy_ports(self):
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"pins": [
|
||||
{"name": "input_io1", "x": 1, "y": 2, "angle": 90, "width": 0.6},
|
||||
],
|
||||
"ports": [
|
||||
{"name": "legacy_port", "x": 9, "y": 9, "angle": 0},
|
||||
],
|
||||
})
|
||||
|
||||
self.assertIn("input_io1", spec.pins)
|
||||
self.assertNotIn("legacy_port", spec.pins)
|
||||
self.assertEqual(spec.pins["input_io1"].angle, 90.0)
|
||||
|
||||
def test_loader_accepts_pin_links_with_route_metadata(self):
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell_a",
|
||||
"instances": {
|
||||
"inst_a": {"component": "mmi", "x": 0, "y": 0},
|
||||
"inst_b": {"component": "mmi", "x": 100, "y": 0},
|
||||
},
|
||||
"bundles": {
|
||||
"output_bus": {
|
||||
"links": [{
|
||||
"from": "inst_a:out",
|
||||
"to": "inst_b:in",
|
||||
"xsection": "metal_1",
|
||||
"family": "electrical",
|
||||
"width": 5,
|
||||
"radius": 20,
|
||||
"routing_type": "standard_bend",
|
||||
"points": [{"x": 0, "y": 0}, {"x": 50, "y": 0}],
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
link = spec.bundles["output_bus"].links[0]
|
||||
self.assertEqual(link.src_inst, "inst_a")
|
||||
self.assertEqual(link.src_pin, "out")
|
||||
self.assertEqual(link.dst_inst, "inst_b")
|
||||
self.assertEqual(link.dst_pin, "in")
|
||||
self.assertEqual(link.xsection, "metal_1")
|
||||
self.assertEqual(link.width, 5)
|
||||
self.assertEqual(link.points[1], {"x": 50.0, "y": 0.0})
|
||||
|
||||
def test_port_element_creates_named_io_pins_and_inside_route_pins(self):
|
||||
from mxpic_router.builder import _register_element_pins
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
class FakePlacedPin:
|
||||
def __init__(self, name, x, y, a):
|
||||
self.name = name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.a = a
|
||||
|
||||
def move(self, dx, dy, da):
|
||||
return FakePlacedPin(f"{self.name}_in", self.x + dx, self.y + dy, self.a + da)
|
||||
|
||||
class FakePin:
|
||||
created_names = []
|
||||
current_cell = None
|
||||
|
||||
def __init__(self, name, width=None, xs=None):
|
||||
self.name = name
|
||||
self.width = width
|
||||
self.xs = xs
|
||||
self.created_names.append(name)
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakePlacedPin(self.name, x, y, a)
|
||||
if FakePin.current_cell is not None:
|
||||
FakePin.current_cell.pin[self.name] = placed
|
||||
return placed
|
||||
|
||||
class FakeCell:
|
||||
put_calls = []
|
||||
|
||||
def __init__(self, name=None, instantiate=True):
|
||||
self.name = name
|
||||
self.pin = {}
|
||||
|
||||
def __enter__(self):
|
||||
FakePin.current_cell = self
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
FakePin.current_cell = None
|
||||
return False
|
||||
|
||||
def put(self, x, y, a):
|
||||
self.put_calls.append((self.name, x, y, a))
|
||||
placed = FakeCell(name=f"{self.name}_placed")
|
||||
placed.pin = {
|
||||
name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a)
|
||||
for name, pin in self.pin.items()
|
||||
}
|
||||
return placed
|
||||
|
||||
class FakeNazca:
|
||||
Pin = FakePin
|
||||
Cell = FakeCell
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"elements": {
|
||||
"input": {
|
||||
"type": "port",
|
||||
"x": 5,
|
||||
"y": 10,
|
||||
"angle": 90,
|
||||
"width": 0.5,
|
||||
"port_number": 2,
|
||||
"pitch": 12,
|
||||
"pins": [
|
||||
{"name": "input_io1", "role": "io1"},
|
||||
{"name": "input_io2", "role": "io2"},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
pin_map = {}
|
||||
|
||||
_register_element_pins(pin_map, "input", spec.elements["input"], FakeNazca)
|
||||
|
||||
self.assertEqual(FakePin.created_names, ["input_io1", "input_io1_in", "input_io2", "input_io2_in"])
|
||||
self.assertEqual(FakeCell.put_calls, [("element_input", 5.0, 10.0, 90.0)])
|
||||
self.assertEqual(pin_map[("input", "input_io1")].a, 450.0)
|
||||
self.assertEqual(pin_map[("input", "input_io2")].a, 450.0)
|
||||
self.assertEqual((pin_map[("input", "input_io1")].x, pin_map[("input", "input_io1")].y), (5.0, 16.0))
|
||||
self.assertEqual((pin_map[("input", "input_io2")].x, pin_map[("input", "input_io2")].y), (5.0, 4.0))
|
||||
|
||||
def test_anchor_element_defaults_to_named_a_b_pins(self):
|
||||
from mxpic_router.builder import _register_element_pins
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
class FakePlacedPin:
|
||||
def __init__(self, name, x, y, a):
|
||||
self.name = name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.a = a
|
||||
|
||||
class FakePin:
|
||||
current_cell = None
|
||||
|
||||
def __init__(self, name, width=None, xs=None):
|
||||
self.name = name
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakePlacedPin(self.name, x, y, a)
|
||||
if FakePin.current_cell is not None:
|
||||
FakePin.current_cell.pin[self.name] = placed
|
||||
return placed
|
||||
|
||||
class FakeCell:
|
||||
def __init__(self, name=None, instantiate=True):
|
||||
self.name = name
|
||||
self.pin = {}
|
||||
|
||||
def __enter__(self):
|
||||
FakePin.current_cell = self
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
FakePin.current_cell = None
|
||||
return False
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakeCell(name=f"{self.name}_placed")
|
||||
placed.pin = {
|
||||
name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a)
|
||||
for name, pin in self.pin.items()
|
||||
}
|
||||
return placed
|
||||
|
||||
class FakeNazca:
|
||||
Pin = FakePin
|
||||
Cell = FakeCell
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"elements": {
|
||||
"anchor_1": {"type": "anchor", "x": 10, "y": 20, "angle": 0, "port_number": 2, "pitch": 12},
|
||||
},
|
||||
})
|
||||
pin_map = {}
|
||||
|
||||
_register_element_pins(pin_map, "anchor_1", spec.elements["anchor_1"], FakeNazca)
|
||||
|
||||
self.assertIn(("anchor_1", "anchor_1_a1"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_b1"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_a2"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_b2"), pin_map)
|
||||
self.assertEqual(pin_map[("anchor_1", "anchor_1_a1")].a, 180.0)
|
||||
self.assertEqual(pin_map[("anchor_1", "anchor_1_b1")].a, 0.0)
|
||||
|
||||
def test_pdk_metadata_ports_are_registered_as_pins_for_allowed_pdk_roots(self):
|
||||
from mxpic_router.builder import _metadata_pins
|
||||
|
||||
metadata = {"ports": {"a1": {"x": 0, "y": 0, "a": 180}}}
|
||||
|
||||
self.assertEqual(
|
||||
_metadata_pins(metadata, r"D:\mxpic\opt_pdk_public\foundries"),
|
||||
metadata["ports"],
|
||||
)
|
||||
self.assertEqual(_metadata_pins(metadata, r"D:\mxpic\some_project_layout"), {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user