From 1d603dffadc6b5296e68aa166b020d86f7ca6218 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 1 Jun 2026 13:19:27 +0800 Subject: [PATCH] Removing mxpic_forge from dependency --- README.md | 41 +++++---- .../__pycache__/__init__.cpython-39.pyc | Bin 468 -> 497 bytes .../__pycache__/builder.cpython-39.pyc | Bin 20013 -> 22728 bytes .../__pycache__/eda_loader.cpython-39.pyc | Bin 7019 -> 7048 bytes .../__pycache__/technology.cpython-39.pyc | Bin 1242 -> 1249 bytes mxpic_router/builder.py | 85 +++++++++++++++++- .../test_eda_router_contract.cpython-39.pyc | Bin 10373 -> 14141 bytes tests/test_eda_router_contract.py | 56 ++++++++++++ 8 files changed, 164 insertions(+), 18 deletions(-) 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 30644776aab81d990891262d0d9559070443a245..3e760889bb287a84adcf38318044a00fb005da5e 100644 GIT binary patch delta 73 zcmcb@{E?YAk(ZZ?0SJ!$m&w|^k=Kcl@z&%3Mh^{MpjZ)z_+^@A6;qU2kYAixl3$dW YS{##GQIMG&UzA^3l3Elqc`>6Q02{d%tN;K2 delta 44 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 Shpfv?i3>YM)Zjp%{J#M&4ie}9 delta 5036 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 N+OC=O^S!ex`ajY{n_2(> diff --git a/mxpic_router/__pycache__/eda_loader.cpython-39.pyc b/mxpic_router/__pycache__/eda_loader.cpython-39.pyc index 6289e4ca8c9ad6f9be19df3c466370ef18c7d834..924c1f40b970ae4bd1e2d0615d62462630043f77 100644 GIT binary patch delta 764 zcmZvZzi-n}5XXHcKl&qyoHlixwkfDxa8;>7s#=K(iGisSK?ng=8k!jA6$Qn1xaUw< z8nJbM16voShykI1!~g>eBNJkHD+5D?#D7rk+)|_pTlcx|^ZD-Gy?3;BuwL-;`7wi@ zFUNC_mnv@y19N_T_i7zQHr^|Gu~wf-F6^kEr3x`Ubx>^57O%;qNiYDL>Tv4)`~%W> zpV1(=Y1|;EGhGj*XleDtzO?Akmp5dxL@*!)IT!;I3iMj!CjdS5&OYq-DC%Yq$>Ab{ z|39g{%Ib83W!92$HaT)CPM^VHwN3a)CMO90wza{g7HCtB`^hXEHw<;`q}BK7wbB+X z1}28&bhagSyd1KnPt~3B!pa_fJ)gtveIt`OfT@}`xK9!6GyYehrf$xZof?H*s8VO6i9%pEr`7(*cmA}s|;52YuKg+gn=lGw4KuGsaP^YuZRp!^s%Z)vm6vam(xF$#mLj4WN`ioqbd{;2I2ox!12gpfQ zqYv2`sL{uKihgIyu}v!_Jxxcsxd&?KiS`0mP)6Gg zOdk(4n-6`3mc&gk+-x6e#3JVT?`QC}scv 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 ec072e41010785a24ecb8fcfc27746ac688c69d0..ebb1c482f2085a75c2473815976c7a58785788fe 100644 GIT binary patch literal 14141 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 F{{_>6XnOzv delta 1938 zcmah~O>7%Q6!xy|W#hm8B(`HWsgwRhxGFSmleB4*HYG_zDU^f+LXE*}oGo=?J7&B= z2?A9d5E812!d!YQ6%rz-aIu6aRiqw}kdQbaaX@=OJ%BiI-~&NI9&e6vdX7+82mW z;Gf6GPNwoJ#cZa$q25-Nq+Dg#jur!c9ZNcpqx4<#&r z^!Ip?s#?0-5x9Qw1p0%3lYk+>2;dlC9KeZ2YZg~DM%@e4N;-kAxD=P_(Zm>6^&s%O zj%wGPA}s&j5%M-x65DwvG6JW<)Y1p3HY@sV=f*h)(ZLJnf+ewzKT65xw zdU*MVU3A(J+27jbk}DpC1b>!+i`272|Kk}fU@oUN-3nV7_=d?eJ-ZXlJLfHqVpE?Y@gaYJV&)TFUY@QH)Bu2@(;; z3z!A)7!&rqe9(O@2GxrsfH`;p){6j+8JC~zd-KE=4*pBlg3q(QKuV_f2dh$kfB)*$ zG}5yB<(kE-Kp*$0RUa>xQc(=+S2qfoD@!7sNNN>JWH7lKfR{zg6T~%f8Fu|<`q71Y z#T7u702*GRZ_>M&GHoROJvO(^m~j+w@|y$QLph{lX}7{+ee<1YLzgcGUxz%pyxF?S z<*m*m<>wCi6lF$cdM0`~*Q=ZJ;`A88#(=HoP0N@!ETc&xcF(=pi*a3SIgmY=b{J3q z6af~1*RKPUjqA}ut%OI9{wa3p`}A4;th_NW^R%tcm?dk}D47CbypHY48`;8kscd~H bzcjy`GfykXhyNAIr|8ayox86(F6qiY!vb$* 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() -- 2.52.0