Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d1994125 | |||
| eaea124b07 | |||
| bf483c0ea6 | |||
| 5f85b390ef | |||
| a9187e8a29 | |||
| f736cff672 | |||
| 7a76cb44cf | |||
| e7f707292a | |||
| d7698d1ee5 | |||
| 531c1f1f45 | |||
| f367fd4fc6 | |||
| 759a32cfc5 | |||
| 4d17ec7e9e | |||
| 92e818163c | |||
| 5ba7b66e9e | |||
| eb45d2f040 | |||
| 6272f6ac4f | |||
| 2a35095418 | |||
| 1d603dffad |
@@ -4,8 +4,8 @@
|
||||
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.
|
||||
routable pins, connects bundle links through `mxpic_forge.Route` when available
|
||||
or Nazca `interconnects.Interconnect` as a fallback, and exports the final GDS.
|
||||
|
||||
## High Level Flow
|
||||
|
||||
@@ -34,10 +34,12 @@ Canvas Build GDS
|
||||
-> downloadable <project>.gds
|
||||
```
|
||||
|
||||
If `mxpic_router`, `mxpic_forge`, Nazca, or optional `gdstk` are absent, the
|
||||
If `mxpic_router`, Nazca, or optional `gdstk` are absent, the
|
||||
EDA server can still run canvas and login pages. Build actions are where the
|
||||
router stack is required. For Build Layout, the YAML is still saved and SVG
|
||||
preview is skipped when the router stack is missing.
|
||||
router stack is required. `mxpic_forge` improves routing when present, but is
|
||||
not required because the router falls back to Nazca Interconnect. For Build
|
||||
Layout, the YAML is still saved and SVG preview is skipped when the required
|
||||
router stack is missing.
|
||||
|
||||
## Runtime Stack
|
||||
|
||||
@@ -51,18 +53,21 @@ require_router_stack()
|
||||
-> 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
|
||||
-> import mxpic_router.builder._import_route_backend()
|
||||
-> first tries mxpic_router.builder._import_mxpic_forge_route()
|
||||
-> adds sibling ../mxpic_forge to sys.path
|
||||
-> removes any already-loaded non-forge mxpic modules
|
||||
-> from mxpic import Route
|
||||
-> if mxpic_forge is absent:
|
||||
-> uses nazca.interconnects.Interconnect through a Route-compatible adapter
|
||||
```
|
||||
|
||||
Important package naming:
|
||||
|
||||
- `mxpic_router` is this active router package.
|
||||
- `mxpic_router_legacy` is the old internal legacy package.
|
||||
- `mxpic` should resolve to `mxpic_forge`, because `mxpic_forge` provides
|
||||
`Route`.
|
||||
- `mxpic` should resolve to `mxpic_forge` when that checkout is present,
|
||||
because `mxpic_forge` provides the preferred `Route` backend.
|
||||
|
||||
## Inputs To mxpic_router
|
||||
|
||||
@@ -415,7 +420,7 @@ bundles:
|
||||
For each link:
|
||||
|
||||
```text
|
||||
builder._route_link(link, pin_map, Route, warnings)
|
||||
builder._route_link(link, pin_map, RouteBackend, warnings)
|
||||
-> route = Route(
|
||||
radius=link.radius or 10,
|
||||
width=link.width,
|
||||
@@ -455,7 +460,7 @@ otherwise -> bend_p2p
|
||||
missing method -> fallback to sbend_p2p
|
||||
```
|
||||
|
||||
Then it calls the selected `mxpic_forge.Route` method:
|
||||
Then it calls the selected route backend method:
|
||||
|
||||
```python
|
||||
route_method(
|
||||
@@ -492,7 +497,7 @@ mxpic_router/eda_loader.py
|
||||
|
||||
mxpic_router/builder.py
|
||||
-> import Nazca
|
||||
-> import mxpic_forge Route
|
||||
-> import mxpic_forge Route or Nazca Interconnect fallback
|
||||
-> apply technology manifest
|
||||
-> load all project YAML specs
|
||||
-> build basic, project, and PDK instances
|
||||
@@ -510,9 +515,11 @@ mxpic_router_legacy/
|
||||
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`.
|
||||
- Avoid reintroducing an internal `mxpic` package inside this repo. When the
|
||||
forge checkout is present, the name `mxpic` must continue to resolve to
|
||||
`mxpic_forge`.
|
||||
- When adding a new routing family or xsection, update `technology.yml` first,
|
||||
then verify that `mxpic_forge.Route` supports the required route method.
|
||||
then verify that the selected route backend supports the required route
|
||||
method.
|
||||
- When adding a new component source, make sure it eventually contributes pins
|
||||
to `pin_map`; otherwise bundle links can load but cannot route.
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# Backend Routing Work Log - PENG KUN
|
||||
|
||||
|
||||
## 2026-06-02
|
||||
|
||||
### Scope
|
||||
|
||||
Backend routing generation and GDS build consistency in `mxpic_router`.
|
||||
|
||||
### Summary
|
||||
|
||||
Recent work focused on improving routing correctness in `mxpic_router/mxpic_router/builder.py`, including route width investigation, rotation convention alignment, and perpendicular routing behavior correction.
|
||||
|
||||
### Completed Work
|
||||
|
||||
#### 1. Route Width Issue Investigation
|
||||
|
||||
A mismatch was identified between the route width configured in the EDA frontend and the width exported into the layout YAML/GDS build flow.
|
||||
|
||||
Backend tracing showed that the router consumed the width value from the YAML as expected, while the exported YAML still used the default width in several cases. The issue was isolated to the frontend serialization/export logic and reported to the frontend owner for correction.
|
||||
|
||||
#### 2. Frontend/Backend Rotation Convention Alignment
|
||||
|
||||
A rotation inconsistency was identified between the EDA canvas representation and the generated GDS layout.
|
||||
|
||||
For rotated devices such as resistors, the frontend display and backend placement used different rotation conventions, which caused incorrect component orientation or port direction in the final GDS.
|
||||
|
||||
The backend build logic was updated to convert EDA instance rotation into layout rotation during component placement and metadata pin registration, while preserving the original behavior for element-based ports and anchors.
|
||||
|
||||
#### 3. Perpendicular Routing Behavior Correction
|
||||
|
||||
A routing issue was identified for port pairs with a 90-degree or 270-degree angle difference.
|
||||
|
||||
The previous automatic route selection fell back to the generic `bend_p2p` method, which could generate a `bend - diagonal straight - bend` path.
|
||||
|
||||
The routing method selection logic was updated so perpendicular port-angle cases use `strt_bend_strt_p2p`, producing the expected `straight - bend - straight` routing style.
|
||||
|
||||
### Modified Files
|
||||
|
||||
mxpic_router/mxpic_router/builder.py
|
||||
mxpic_router/tests/test_eda_router_contract.py
|
||||
|
||||
## 2026-06-04
|
||||
|
||||
### Scope
|
||||
|
||||
Backend routing spacing behavior and automatic S-bend refinement in `mxpic_router`.
|
||||
|
||||
### Summary
|
||||
|
||||
Recent work focused on reducing overlap and crossing risks for grouped automatic S-bend connections while keeping the generated GDS impact limited.
|
||||
|
||||
### Completed Work
|
||||
|
||||
#### 1. S-Bend Spacing Issue Investigation
|
||||
|
||||
A route overlap issue was investigated for layouts containing multiple parallel or near-parallel automatic S-bend routes.
|
||||
|
||||
The issue was most visible when several component output ports were connected to several port elements. In these cases, the initial straight section of multiple S-bends could overlap or become too close when source and target ports were arranged in parallel.
|
||||
|
||||
#### 2. Conservative `Lstart`-Based S-Bend Adjustment
|
||||
|
||||
Spacing control was implemented through the native Nazca `sbend_p2p` `Lstart` parameter.
|
||||
|
||||
The backend now groups compatible automatic S-bend routes and assigns different `Lstart` values before route generation. This preserves the native S-bend route style and avoids manually constructed detours.
|
||||
|
||||
The adjustment applies only to automatic `sbend_p2p` routes. Manual-point routes, U-bends, perpendicular `strt_bend_strt_p2p` routes, and generic bend routes are left unchanged.
|
||||
|
||||
#### 3. Bundle-Level Route Grouping and Ordering
|
||||
|
||||
Compatible S-bend routes are grouped by routing direction, xsection, width, and radius. The `Lstart` assignment logic was refined for both same-direction fan-out/fan-in routes and symmetric convergence/divergence routes, so outer routes can receive larger offsets when needed.
|
||||
|
||||
#### 4. Width-Aware Spacing Step
|
||||
|
||||
The base step is now calculated as:
|
||||
|
||||
```text
|
||||
route width + 10um
|
||||
```
|
||||
|
||||
For example, a 0.5um-wide route uses a 10.5um `Lstart` step, while a 40um-wide route uses a 50um `Lstart` step.
|
||||
|
||||
#### 5. Test Coverage Update
|
||||
|
||||
Contract tests were added and updated to cover automatic S-bend `Lstart` assignment, bundle-level grouping behavior, route ordering, width-aware spacing, and preservation of earlier rotation and perpendicular-routing fixes.
|
||||
|
||||
### Modified Files
|
||||
|
||||
mxpic_router/mxpic_router/builder.py
|
||||
mxpic_router/tests/test_eda_router_contract.py
|
||||
|
||||
|
||||
## 2026-06-05
|
||||
|
||||
### Scope
|
||||
|
||||
Backend S-bend grouping contract and `Lstart` ordering refinement in `mxpic_router`.
|
||||
|
||||
### Summary
|
||||
|
||||
Recent work focused on improving automatic S-bend spacing behavior for multi-port routing cases, especially MMI-to-port and resistor-array routing patterns.
|
||||
|
||||
### Completed Work
|
||||
|
||||
#### 1. Explicit Route Group Metadata Support
|
||||
|
||||
Backend support was added for explicit link grouping metadata from the layout YAML.
|
||||
|
||||
`LinkSpec` now records the parent bundle name and accepts optional route grouping fields such as `route_group`, `routeGroup`, `group`, `bundle_group`, and `bundleGroup`.
|
||||
|
||||
This prepares the backend for future frontend-provided bundle/group selection while preserving the existing geometry-based fallback behavior.
|
||||
|
||||
#### 2. S-Bend Stage Grouping Refinement
|
||||
|
||||
The S-bend grouping key was refined to avoid mixing routes from different routing stages within the same YAML bundle.
|
||||
|
||||
Automatic S-bend routes are now grouped by routing axis, direction, route stage/span, xsection, width, and radius unless an explicit route group is provided.
|
||||
|
||||
This prevents separate stages such as component-to-component routes and component-to-port routes from interfering with each other's `Lstart` ordering.
|
||||
|
||||
#### 3. Lstart Ordering Correction
|
||||
|
||||
The `Lstart` ordering logic was reviewed for resistor-array and MMI-to-port routing cases.
|
||||
|
||||
The sorting basis was corrected to use the source-side route coordinate for same-direction fan-out/fan-in routes, and S-bend axis detection was updated to prefer port direction over raw geometric span.
|
||||
|
||||
This fixes cases where routes with large vertical offset were incorrectly classified and assigned reversed `Lstart` values.
|
||||
|
||||
#### 4. Test Coverage Update
|
||||
|
||||
Contract tests were added and updated to cover explicit route group metadata parsing, route-stage separation, MMI-to-port two-output fan-out ordering, width-aware `Lstart` spacing, and preservation of earlier S-bend spacing behavior.
|
||||
|
||||
### Modified Files
|
||||
|
||||
mxpic_router/mxpic_router/eda_loader.py
|
||||
mxpic_router/mxpic_router/builder.py
|
||||
mxpic_router/tests/test_eda_router_contract.py
|
||||
|
||||
|
||||
## 2026-06-09
|
||||
|
||||
### Scope
|
||||
|
||||
Frontend/backend routing metadata integration and electrical routing behavior review.
|
||||
|
||||
### Summary
|
||||
|
||||
Recent work focused on aligning the backend route grouping logic with the updated EDA frontend bundle-group export format, and reviewing why resistor routes were not using PCB-style routing.
|
||||
|
||||
### Completed Work
|
||||
|
||||
#### 1. Frontend Bundle Group Integration
|
||||
|
||||
The updated frontend exports user-selected route bundle groups as YAML bundle keys instead of duplicating `bundle_group` inside each link.
|
||||
|
||||
The backend route grouping logic was updated to treat custom YAML bundle keys as explicit route groups for S-bend `Lstart` spacing.
|
||||
|
||||
Default groups such as `output_bus`, `free_wires`, and `free_wires_*` are still treated as fallback groups to avoid unintentionally merging unrelated free-wire routes.
|
||||
|
||||
#### 2. Contract Test Update
|
||||
|
||||
A contract test was added to verify that custom frontend bundle keys are recognized as backend route groups, while default bundle names remain excluded from explicit grouping.
|
||||
|
||||
#### 3. PCB Routing Condition Review
|
||||
|
||||
The PCB routing condition was reviewed for resistor-based test layouts.
|
||||
|
||||
The backend currently enables `PCB=True` based on route `xsection` only. Routes using `metal_1`, `metal1`, `metal_2`, or `metal2` enable PCB routing; routes exported as `strip` remain optical-style routes.
|
||||
|
||||
The current resistor test YAML was found to export `xsection: strip` and `family: optical`, so the issue was traced to frontend route metadata export rather than the backend PCB flag logic.
|
||||
|
||||
### Modified Files
|
||||
|
||||
mxpic_router/mxpic_router/builder.py
|
||||
mxpic_router/tests/test_eda_router_contract.py
|
||||
|
||||
|
||||
## 2026-06-10
|
||||
|
||||
### Scope
|
||||
|
||||
Backend routing condition alignment and automatic route direction normalization in `mxpic_router`.
|
||||
|
||||
### Summary
|
||||
|
||||
Recent work focused on aligning backend routing behavior with updated frontend metadata and reducing direction-related crossing issues in bundled automatic routes.
|
||||
|
||||
### Completed Work
|
||||
|
||||
#### 1. PCB Routing Condition Update
|
||||
|
||||
The PCB routing condition was updated to use the YAML link `family` field instead of route `xsection`.
|
||||
|
||||
Routes with `family: electrical` now enable `PCB=True`, regardless of the specific metal layer or xsection name. Optical routes remain unchanged.
|
||||
|
||||
#### 2. Automatic Route Direction Normalization
|
||||
|
||||
Automatic bundled routes are now normalized to route from the lower-x endpoint to the higher-x endpoint.
|
||||
|
||||
This keeps route direction consistent inside the same bundle and reduces crossing risks caused by mixed left-to-right and right-to-left link definitions.
|
||||
|
||||
#### 3. Test Coverage Update
|
||||
|
||||
Contract tests were added to verify electrical-family PCB routing and left-to-right automatic route ordering.
|
||||
|
||||
### Modified Files
|
||||
|
||||
mxpic_router/mxpic_router/builder.py
|
||||
mxpic_router/tests/test_eda_router_contract.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1206
-587
File diff suppressed because it is too large
Load Diff
+218
-207
@@ -1,227 +1,238 @@
|
||||
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)
|
||||
|
||||
|
||||
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"
|
||||
dst_pin: str = ""
|
||||
xsection: str = "strip"
|
||||
family: str = "optical"
|
||||
width: Optional[float] = None
|
||||
radius: Optional[float] = None
|
||||
routing_type: str = "euler_bend"
|
||||
bundle: str = ""
|
||||
route_group: str = ""
|
||||
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)
|
||||
|
||||
|
||||
@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,
|
||||
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),
|
||||
bundle=bundle.name,
|
||||
route_group=str(
|
||||
link_data.get("route_group")
|
||||
or link_data.get("routeGroup")
|
||||
or link_data.get("group")
|
||||
or link_data.get("bundle_group")
|
||||
or link_data.get("bundleGroup")
|
||||
or ""
|
||||
),
|
||||
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
|
||||
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
|
||||
|
||||
Binary file not shown.
+813
-195
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user