Description file added

This commit is contained in:
2026-06-01 11:34:00 +08:00
parent 20f773cec6
commit d0b6ac018a
15 changed files with 1618 additions and 2 deletions
+517 -2
View File
@@ -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.
+23
View File
@@ -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.
+587
View File
@@ -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
+227
View File
@@ -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
+38
View File
@@ -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)
+226
View File
@@ -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()