diff --git a/README.md b/README.md index 645745e..64a3de7 100644 --- a/README.md +++ b/README.md @@ -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 .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. diff --git a/mxpic_router/__pycache__/__init__.cpython-39.pyc b/mxpic_router/__pycache__/__init__.cpython-39.pyc index 3064477..3e76088 100644 Binary files a/mxpic_router/__pycache__/__init__.cpython-39.pyc and b/mxpic_router/__pycache__/__init__.cpython-39.pyc differ diff --git a/mxpic_router/__pycache__/builder.cpython-39.pyc b/mxpic_router/__pycache__/builder.cpython-39.pyc index 24ffc3d..293f2ec 100644 Binary files a/mxpic_router/__pycache__/builder.cpython-39.pyc and b/mxpic_router/__pycache__/builder.cpython-39.pyc differ diff --git a/mxpic_router/__pycache__/eda_loader.cpython-39.pyc b/mxpic_router/__pycache__/eda_loader.cpython-39.pyc index 6289e4c..924c1f4 100644 Binary files a/mxpic_router/__pycache__/eda_loader.cpython-39.pyc and b/mxpic_router/__pycache__/eda_loader.cpython-39.pyc differ diff --git a/mxpic_router/__pycache__/technology.cpython-39.pyc b/mxpic_router/__pycache__/technology.cpython-39.pyc index c324b41..550e880 100644 Binary files a/mxpic_router/__pycache__/technology.cpython-39.pyc and b/mxpic_router/__pycache__/technology.cpython-39.pyc differ diff --git a/mxpic_router/builder.py b/mxpic_router/builder.py index 860d910..18cc7ab 100644 --- a/mxpic_router/builder.py +++ b/mxpic_router/builder.py @@ -19,7 +19,7 @@ def build_project_gds( ) -> dict: import nazca as nd - Route = _import_mxpic_forge_route() + Route = _import_route_backend(nd) manifest = load_technology_manifest(technology_manifest_path) apply_technology_manifest(manifest, nd) @@ -573,6 +573,89 @@ def _safe_float(value, default=0.0): return default +class _NazcaInterconnectRoute: + """Small mxpic_forge.Route-compatible adapter around Nazca Interconnect.""" + + backend_name = "nazca Interconnect" + + def __init__(self, radius=None, width=None, xs=None, PCB=False): + self.radius = radius + self.width = width + self.xs = xs + self.PCB = PCB + self._interconnect = self._create_interconnect(radius=radius, width=width, xs=xs, PCB=PCB) + + @staticmethod + def _create_interconnect(radius=None, width=None, xs=None, PCB=False): + import nazca as nd + + return nd.interconnects.Interconnect(radius=radius, width=width, xs=xs, PCB=PCB) + + def strt_p2p(self, pin1=None, pin2=None, width=None, xs=None, arrow=True, **kwargs): + return self._interconnect.strt_p2p( + pin1=pin1, + pin2=pin2, + width=self._route_width(width), + xs=self._route_xs(xs), + arrow=arrow, + ) + + def sbend_p2p(self, pin1=None, pin2=None, width=None, radius=None, xs=None, arrow=True, **kwargs): + return self._interconnect.sbend_p2p( + pin1=pin1, + pin2=pin2, + width=self._route_width(width), + radius=self._route_radius(radius), + xs=self._route_xs(xs), + arrow=arrow, + ) + + def ubend_p2p(self, pin1=None, pin2=None, width=None, radius=None, xs=None, arrow=True, **kwargs): + return self._interconnect.ubend_p2p( + pin1=pin1, + pin2=pin2, + width=self._route_width(width), + radius=self._route_radius(radius), + xs=self._route_xs(xs), + arrow=arrow, + ) + + def bend_p2p(self, pin1=None, pin2=None, width=None, radius=None, xs=None, arrow=True, **kwargs): + route_method = getattr(self._interconnect, "bend_strt_bend_p2p", None) + if route_method is None: + route_method = getattr(self._interconnect, "strt_bend_strt_p2p") + return route_method( + pin1=pin1, + pin2=pin2, + width=self._route_width(width), + radius=self._route_radius(radius), + xs=self._route_xs(xs), + arrow=arrow, + ) + + def _route_width(self, width): + return self.width if width is None else width + + def _route_radius(self, radius): + return self.radius if radius is None else radius + + def _route_xs(self, xs): + return self.xs if xs is None else xs + + +def _import_route_backend(nd=None): + try: + route = _import_mxpic_forge_route() + setattr(route, "backend_name", getattr(route, "backend_name", "mxpic_forge Route")) + return route + except Exception: + if nd is None: + import nazca as nd + if not hasattr(nd, "interconnects") or not hasattr(nd.interconnects, "Interconnect"): + raise + return _NazcaInterconnectRoute + + 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: diff --git a/tests/__pycache__/test_eda_router_contract.cpython-39.pyc b/tests/__pycache__/test_eda_router_contract.cpython-39.pyc index ec072e4..ebb1c48 100644 Binary files a/tests/__pycache__/test_eda_router_contract.cpython-39.pyc and b/tests/__pycache__/test_eda_router_contract.cpython-39.pyc differ diff --git a/tests/test_eda_router_contract.py b/tests/test_eda_router_contract.py index 85c8d3e..6b4ed5c 100644 --- a/tests/test_eda_router_contract.py +++ b/tests/test_eda_router_contract.py @@ -221,6 +221,62 @@ class EdaRouterPinsContractTest(unittest.TestCase): ) self.assertEqual(_metadata_pins(metadata, r"D:\mxpic\some_project_layout"), {}) + def test_route_backend_falls_back_to_nazca_interconnect_when_forge_is_absent(self): + import mxpic_router.builder as builder + + class FakeInterconnect: + calls = [] + + def __init__(self, radius=None, width=None, xs=None, PCB=False): + self.radius = radius + self.width = width + self.xs = xs + self.PCB = PCB + + def strt_p2p(self, **kwargs): + self.calls.append(("strt_p2p", kwargs)) + return "straight" + + def sbend_p2p(self, **kwargs): + self.calls.append(("sbend_p2p", kwargs)) + return "sbend" + + def ubend_p2p(self, **kwargs): + self.calls.append(("ubend_p2p", kwargs)) + return "ubend" + + def bend_strt_bend_p2p(self, **kwargs): + self.calls.append(("bend_strt_bend_p2p", kwargs)) + return "bend" + + class FakeInterconnects: + Interconnect = FakeInterconnect + + class FakeNazca: + interconnects = FakeInterconnects + + original_import = builder._import_mxpic_forge_route + original_create = builder._NazcaInterconnectRoute._create_interconnect + try: + builder._import_mxpic_forge_route = lambda: (_ for _ in ()).throw(ImportError("no forge")) + builder._NazcaInterconnectRoute._create_interconnect = staticmethod( + lambda **kwargs: FakeInterconnect(**kwargs) + ) + + route_backend = builder._import_route_backend(FakeNazca) + route = route_backend(radius=20, width=0.7, xs="strip", PCB=False) + + self.assertEqual(route.backend_name, "nazca Interconnect") + self.assertEqual(route.strt_p2p(pin1="a", pin2="b", arrow=False), "straight") + self.assertEqual(route.bend_p2p(pin1="a", pin2="b", radius=10, arrow=False), "bend") + self.assertEqual(FakeInterconnect.calls[0][0], "strt_p2p") + self.assertEqual(FakeInterconnect.calls[0][1]["width"], 0.7) + self.assertEqual(FakeInterconnect.calls[1][0], "bend_strt_bend_p2p") + self.assertEqual(FakeInterconnect.calls[1][1]["radius"], 10) + finally: + builder._import_mxpic_forge_route = original_import + builder._NazcaInterconnectRoute._create_interconnect = original_create + if __name__ == "__main__": unittest.main()