rocket-engine 0.0.2b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,734 @@
1
+ """Shared schematic connectivity extraction (CONNECT-GRAPH).
2
+
3
+ One extraction feeds every netlist backend plus ERC: wires and
4
+ instance pins coalesce through a spatial hash, labels propagate net
5
+ names, unlabeled nets and instances get stable generated names, and
6
+ `lvs_ignore` instances stay out of the graph. Emitters and ERC never
7
+ name a net themselves — `Connectivity.net_name` is the single
8
+ authority they all read.
9
+
10
+ Schematic record (plain data, JSON-safe):
11
+
12
+ - cell: top cell name (default "top").
13
+ - wires: list of polylines; each a list of [x, y] integer points.
14
+ Zero-length segments are skipped.
15
+ - pins: [{instance, pin, xy}]; instance may be "" only when the
16
+ schematic holds exactly one unnamed instance.
17
+ - labels: [{name, xy}]; names propagate onto the nets they touch.
18
+ - instances: [{name?, symbol, attrs?, custom?, format?, generics?}];
19
+ attrs.lvs_ignore excludes the instance from graph and netlist.
20
+ - symbols: {name: {pins?, custom?, format?, generics?}}.
21
+ - ports: [{name, direction}]; direction is input/output/inout and
22
+ every port must name an extracted net.
23
+
24
+ Two wires connect when an endpoint of one lies on the other (shared
25
+ endpoints and T-junctions); crossing interiors do not connect.
26
+ """
27
+
28
+ import hashlib
29
+ import json
30
+
31
+ RULE_SHORT = "connectivity.short"
32
+ RULE_UNCONNECTED_PIN = "connectivity.unconnected_pin"
33
+ RULE_DANGLING_NET = "connectivity.dangling_net"
34
+ RULE_ORPHAN_LABEL = "connectivity.orphan_label"
35
+
36
+ DEFAULT_POLICY = {
37
+ RULE_SHORT: "error",
38
+ RULE_UNCONNECTED_PIN: "warning",
39
+ RULE_DANGLING_NET: "warning",
40
+ RULE_ORPHAN_LABEL: "warning",
41
+ }
42
+
43
+ _PORT_DIRECTIONS = ("input", "output", "inout")
44
+ _HASH_CELL = 16
45
+
46
+ _CACHE = {}
47
+
48
+
49
+ class ConnectGraphError(ValueError):
50
+ """Malformed schematic, unknown net id, or bad extraction request."""
51
+
52
+
53
+ class Net:
54
+ """One extracted net: canonical name plus its members.
55
+
56
+ Args:
57
+ id: Deterministic net id (nets sort by anchor member).
58
+ name: Canonical name from label propagation or generation.
59
+ pins: Sorted ((instance, pin)) members.
60
+ labels: Distinct sorted label names attached to this net.
61
+ has_wires: True when at least one wire segment joins.
62
+ """
63
+
64
+ def __init__(self, id, name, pins, labels, has_wires):
65
+ self.id = id
66
+ self.name = name
67
+ self.pins = tuple(pins)
68
+ self.labels = tuple(labels)
69
+ self.has_wires = has_wires
70
+
71
+ def __eq__(self, other):
72
+ if not isinstance(other, Net):
73
+ return NotImplemented
74
+ return (
75
+ self.id,
76
+ self.name,
77
+ self.pins,
78
+ self.labels,
79
+ self.has_wires,
80
+ ) == (
81
+ other.id,
82
+ other.name,
83
+ other.pins,
84
+ other.labels,
85
+ other.has_wires,
86
+ )
87
+
88
+ def __repr__(self):
89
+ return f"Net(id={self.id!r}, name={self.name!r})"
90
+
91
+
92
+ class ResolvedInstance:
93
+ """One netlisted instance with pins bound to net ids.
94
+
95
+ Args:
96
+ name: Instance name (generated when the schematic omits it).
97
+ symbol: Symbol name.
98
+ pins: ((pin, net_id)) in symbol pin order, else pin-name order.
99
+ generics: Symbol defaults overridden by instance values.
100
+ custom: Instance-level custom templates by language.
101
+ format: Instance-level format templates by language.
102
+ """
103
+
104
+ def __init__(self, name, symbol, pins, generics, custom=None, format=None):
105
+ self.name = name
106
+ self.symbol = symbol
107
+ self.pins = tuple(pins)
108
+ self.generics = dict(generics)
109
+ self.custom = dict(custom or {})
110
+ self.format = dict(format or {})
111
+
112
+ def __eq__(self, other):
113
+ if not isinstance(other, ResolvedInstance):
114
+ return NotImplemented
115
+ return (
116
+ self.name,
117
+ self.symbol,
118
+ self.pins,
119
+ self.generics,
120
+ self.custom,
121
+ self.format,
122
+ ) == (
123
+ other.name,
124
+ other.symbol,
125
+ other.pins,
126
+ other.generics,
127
+ other.custom,
128
+ other.format,
129
+ )
130
+
131
+ def __repr__(self):
132
+ return f"ResolvedInstance(name={self.name!r}, symbol={self.symbol!r})"
133
+
134
+
135
+ class Connectivity:
136
+ """Extracted nets with the shared net-naming authority.
137
+
138
+ Args:
139
+ cell: Top cell name.
140
+ nets: Nets in id order.
141
+ shorts: [{net, labels}] for nets with conflicting labels.
142
+ pin_index: (instance, pin) -> net id for graphed pins.
143
+ """
144
+
145
+ def __init__(self, cell, nets, shorts, pin_index):
146
+ self.cell = cell
147
+ self.nets = tuple(nets)
148
+ self.shorts = tuple(dict(s) for s in shorts)
149
+ self._pin_index = dict(pin_index)
150
+ self._names = {net.id: net.name for net in self.nets}
151
+
152
+ def net_name(self, net_id):
153
+ """Return the canonical name of one net.
154
+
155
+ The single naming authority: emitters and ERC resolve every
156
+ net through this method, never from their own tables.
157
+
158
+ Args:
159
+ net_id: Extracted net id.
160
+
161
+ Returns:
162
+ Canonical net name.
163
+
164
+ Raises:
165
+ ConnectGraphError: Unknown net id.
166
+ """
167
+ try:
168
+ return self._names[net_id]
169
+ except KeyError:
170
+ raise ConnectGraphError(f"Unknown net id: {net_id!r}.") from None
171
+
172
+ def pin_net(self, instance, pin):
173
+ """Return the net id of one instance pin.
174
+
175
+ Args:
176
+ instance: Instance name.
177
+ pin: Pin name.
178
+
179
+ Returns:
180
+ Net id, or None for pins of `lvs_ignore` instances.
181
+ """
182
+ return self._pin_index.get((instance, pin))
183
+
184
+
185
+ class NetlistStructs:
186
+ """Everything the emitters need: connectivity plus instances.
187
+
188
+ Args:
189
+ cell: Top cell name.
190
+ connectivity: Extracted nets and the naming authority.
191
+ instances: Resolved non-ignored instances, schematic order.
192
+ symbols: Normalized symbol mappings (pins/custom/format).
193
+ ports: [{name, direction}] in schematic order.
194
+ ignored: Ignored instance names in schematic order.
195
+ """
196
+
197
+ def __init__(self, cell, connectivity, instances, symbols, ports, ignored):
198
+ self.cell = cell
199
+ self.connectivity = connectivity
200
+ self.instances = tuple(instances)
201
+ self.symbols = dict(symbols)
202
+ self.ports = tuple(dict(p) for p in ports)
203
+ self.ignored = tuple(ignored)
204
+
205
+
206
+ def prepare_netlist_structs(schematic, run_key=None):
207
+ """Extract connectivity plus resolved instances from a schematic.
208
+
209
+ Wires and pins coalesce through a spatial hash, labels propagate
210
+ names, unlabeled nets become net1..netN in net order, unnamed
211
+ instances become X<n>, and `lvs_ignore` instances drop out.
212
+ Results cache only behind an explicit run key, and a changed
213
+ schematic under a reused key rebuilds instead of serving stale
214
+ nets.
215
+
216
+ Args:
217
+ schematic: Schematic record (see module docstring).
218
+ run_key: Explicit cache key (run/snapshot id); None
219
+ disables the cache and always rebuilds.
220
+
221
+ Returns:
222
+ NetlistStructs with connectivity, instances, and ports.
223
+
224
+ Raises:
225
+ ConnectGraphError: Malformed schematic, dangling port,
226
+ unknown symbol, or a non-string run key.
227
+ """
228
+ if run_key is not None and (not isinstance(run_key, str) or not run_key):
229
+ raise ConnectGraphError(f"run_key must be a non-empty string, got {run_key!r}.")
230
+ normalized = _normalize_schematic(schematic)
231
+ if run_key is None:
232
+ return _extract(normalized)
233
+ digest = _digest(normalized)
234
+ hit = _CACHE.get(run_key)
235
+ if hit is not None and hit[0] == digest:
236
+ return hit[1]
237
+ structs = _extract(normalized)
238
+ _CACHE[run_key] = (digest, structs)
239
+ return structs
240
+
241
+
242
+ def invalidate_run(run_key):
243
+ """Drop one cached extraction.
244
+
245
+ Args:
246
+ run_key: Cache key passed to prepare_netlist_structs.
247
+
248
+ Returns:
249
+ 1 when an entry was dropped, else 0.
250
+ """
251
+ return 1 if _CACHE.pop(run_key, None) is not None else 0
252
+
253
+
254
+ def clear_connectivity_cache():
255
+ """Drop every cached extraction.
256
+
257
+ Returns:
258
+ Number of entries dropped.
259
+ """
260
+ count = len(_CACHE)
261
+ _CACHE.clear()
262
+ return count
263
+
264
+
265
+ def erc_findings(connectivity, policy=None):
266
+ """List connectivity findings with authority-resolved net names.
267
+
268
+ Args:
269
+ connectivity: Extracted connectivity.
270
+ policy: rule -> severity overrides; defaults warn on
271
+ everything but shorts.
272
+
273
+ Returns:
274
+ Findings sorted by (rule, net id); every finding names its
275
+ net through `Connectivity.net_name`.
276
+
277
+ Raises:
278
+ ConnectGraphError: Unknown rule in the policy.
279
+ """
280
+ merged = dict(DEFAULT_POLICY)
281
+ for rule in (policy or {}):
282
+ if rule not in merged:
283
+ raise ConnectGraphError(f"Unknown ERC rule: {rule!r}.")
284
+ merged[rule] = policy[rule]
285
+ findings = []
286
+ for net in connectivity.nets:
287
+ name = connectivity.net_name(net.id)
288
+ if len(net.labels) > 1:
289
+ findings.append(
290
+ {
291
+ "rule": RULE_SHORT,
292
+ "severity": merged[RULE_SHORT],
293
+ "message": (
294
+ f"net {name} has conflicting labels: "
295
+ + ", ".join(net.labels)
296
+ ),
297
+ "net": name,
298
+ "net_id": net.id,
299
+ "labels": list(net.labels),
300
+ }
301
+ )
302
+ if len(net.pins) == 1 and not net.has_wires and not net.labels:
303
+ (instance, pin) = net.pins[0]
304
+ findings.append(
305
+ {
306
+ "rule": RULE_UNCONNECTED_PIN,
307
+ "severity": merged[RULE_UNCONNECTED_PIN],
308
+ "message": f"pin {instance}.{pin} on net {name} connects to nothing",
309
+ "net": name,
310
+ "net_id": net.id,
311
+ "pins": [{"instance": instance, "pin": pin}],
312
+ }
313
+ )
314
+ elif not net.pins and net.has_wires:
315
+ findings.append(
316
+ {
317
+ "rule": RULE_DANGLING_NET,
318
+ "severity": merged[RULE_DANGLING_NET],
319
+ "message": f"net {name} has wires but no pins",
320
+ "net": name,
321
+ "net_id": net.id,
322
+ }
323
+ )
324
+ elif not net.pins and not net.has_wires and net.labels:
325
+ findings.append(
326
+ {
327
+ "rule": RULE_ORPHAN_LABEL,
328
+ "severity": merged[RULE_ORPHAN_LABEL],
329
+ "message": f"label {net.labels[0]} on net {name} touches nothing",
330
+ "net": name,
331
+ "net_id": net.id,
332
+ "labels": list(net.labels),
333
+ }
334
+ )
335
+ findings.sort(key=lambda f: (f["rule"], f["net_id"]))
336
+ return findings
337
+
338
+
339
+ def _parse_point(value, what):
340
+ if (
341
+ not isinstance(value, (list, tuple))
342
+ or len(value) != 2
343
+ or any(not isinstance(c, int) or isinstance(c, bool) for c in value)
344
+ ):
345
+ raise ConnectGraphError(f"{what} must be [x, y] ints, got {value!r}.")
346
+ return (value[0], value[1])
347
+
348
+
349
+ def _parse_mapping(value, what):
350
+ if value is None:
351
+ return {}
352
+ if not isinstance(value, dict):
353
+ raise ConnectGraphError(f"{what} must be a mapping, got {value!r}.")
354
+ return dict(value)
355
+
356
+
357
+ def _normalize_schematic(schematic):
358
+ if not isinstance(schematic, dict):
359
+ raise ConnectGraphError(f"Schematic must be a mapping, got {schematic!r}.")
360
+ cell = schematic.get("cell", "top")
361
+ if not isinstance(cell, str) or not cell:
362
+ raise ConnectGraphError(f"cell must be a non-empty string, got {cell!r}.")
363
+
364
+ wires = schematic.get("wires", [])
365
+ if not isinstance(wires, list):
366
+ raise ConnectGraphError(f"wires must be a list, got {wires!r}.")
367
+ segments = []
368
+ for index, polyline in enumerate(wires):
369
+ if not isinstance(polyline, (list, tuple)) or len(polyline) < 2:
370
+ raise ConnectGraphError(
371
+ f"wires[{index}] must hold at least 2 points, got {polyline!r}."
372
+ )
373
+ points = [_parse_point(p, f"wires[{index}]") for p in polyline]
374
+ for a, b in zip(points, points[1:]):
375
+ if a != b:
376
+ segments.append((a, b))
377
+
378
+ symbols = schematic.get("symbols", {})
379
+ if not isinstance(symbols, dict):
380
+ raise ConnectGraphError(f"symbols must be a mapping, got {symbols!r}.")
381
+ normalized_symbols = {}
382
+ for name, symbol in symbols.items():
383
+ if not isinstance(symbol, dict):
384
+ raise ConnectGraphError(f"symbol {name!r} must be a mapping.")
385
+ pin_order = symbol.get("pins", [])
386
+ if not isinstance(pin_order, list) or not all(
387
+ isinstance(p, str) and p for p in pin_order
388
+ ):
389
+ raise ConnectGraphError(f"symbol {name!r} pins must be a str list.")
390
+ normalized_symbols[name] = {
391
+ "pins": list(pin_order),
392
+ "custom": _parse_mapping(symbol.get("custom"), f"symbol {name!r} custom"),
393
+ "format": _parse_mapping(symbol.get("format"), f"symbol {name!r} format"),
394
+ "generics": _parse_mapping(symbol.get("generics"), f"symbol {name!r} generics"),
395
+ }
396
+
397
+ instances = schematic.get("instances", [])
398
+ if not isinstance(instances, list):
399
+ raise ConnectGraphError(f"instances must be a list, got {instances!r}.")
400
+ normalized_instances = []
401
+ taken = set()
402
+ for index, instance in enumerate(instances):
403
+ if not isinstance(instance, dict):
404
+ raise ConnectGraphError(f"instances[{index}] must be a mapping.")
405
+ symbol = instance.get("symbol")
406
+ if not isinstance(symbol, str) or not symbol:
407
+ raise ConnectGraphError(f"instances[{index}] needs a symbol name.")
408
+ if symbol not in normalized_symbols:
409
+ raise ConnectGraphError(
410
+ f"instances[{index}] names unknown symbol {symbol!r}."
411
+ )
412
+ name = instance.get("name") or ""
413
+ if not isinstance(name, str):
414
+ raise ConnectGraphError(f"instances[{index}] name must be a string.")
415
+ if name:
416
+ if name in taken:
417
+ raise ConnectGraphError(f"Duplicate instance name: {name!r}.")
418
+ taken.add(name)
419
+ attrs = _parse_mapping(instance.get("attrs"), f"instances[{index}] attrs")
420
+ normalized_instances.append(
421
+ {
422
+ "name": name,
423
+ "symbol": symbol,
424
+ "ignored": bool(attrs.get("lvs_ignore", False)),
425
+ "custom": _parse_mapping(instance.get("custom"), f"instances[{index}] custom"),
426
+ "format": _parse_mapping(instance.get("format"), f"instances[{index}] format"),
427
+ "generics": _parse_mapping(
428
+ instance.get("generics"), f"instances[{index}] generics"
429
+ ),
430
+ }
431
+ )
432
+ unnamed = [i for i in normalized_instances if not i["name"]]
433
+ counter = 1
434
+ for instance in unnamed:
435
+ while f"X{counter}" in taken:
436
+ counter += 1
437
+ instance["name"] = f"X{counter}"
438
+ taken.add(instance["name"])
439
+ counter += 1
440
+
441
+ pins = schematic.get("pins", [])
442
+ if not isinstance(pins, list):
443
+ raise ConnectGraphError(f"pins must be a list, got {pins!r}.")
444
+ normalized_pins = []
445
+ seen_pins = set()
446
+ for index, pin in enumerate(pins):
447
+ if not isinstance(pin, dict):
448
+ raise ConnectGraphError(f"pins[{index}] must be a mapping.")
449
+ instance = pin.get("instance", "")
450
+ if not isinstance(instance, str):
451
+ raise ConnectGraphError(f"pins[{index}] instance must be a string.")
452
+ if not instance:
453
+ if len(unnamed) != 1:
454
+ raise ConnectGraphError(
455
+ f"pins[{index}] names no instance and the schematic holds "
456
+ f"{len(unnamed)} unnamed instances."
457
+ )
458
+ instance = unnamed[0]["name"]
459
+ if instance not in taken:
460
+ raise ConnectGraphError(f"pins[{index}] names unknown instance {instance!r}.")
461
+ pin_name = pin.get("pin")
462
+ if not isinstance(pin_name, str) or not pin_name:
463
+ raise ConnectGraphError(f"pins[{index}] needs a pin name.")
464
+ if (instance, pin_name) in seen_pins:
465
+ raise ConnectGraphError(f"Duplicate pin: {instance}.{pin_name}.")
466
+ seen_pins.add((instance, pin_name))
467
+ normalized_pins.append(
468
+ {"instance": instance, "pin": pin_name, "xy": _parse_point(pin.get("xy"), f"pins[{index}]")}
469
+ )
470
+
471
+ labels = schematic.get("labels", [])
472
+ if not isinstance(labels, list):
473
+ raise ConnectGraphError(f"labels must be a list, got {labels!r}.")
474
+ normalized_labels = []
475
+ for index, label in enumerate(labels):
476
+ if not isinstance(label, dict):
477
+ raise ConnectGraphError(f"labels[{index}] must be a mapping.")
478
+ name = label.get("name")
479
+ if not isinstance(name, str) or not name:
480
+ raise ConnectGraphError(f"labels[{index}] needs a name.")
481
+ normalized_labels.append(
482
+ {"name": name, "xy": _parse_point(label.get("xy"), f"labels[{index}]")}
483
+ )
484
+
485
+ ports = schematic.get("ports", [])
486
+ if not isinstance(ports, list):
487
+ raise ConnectGraphError(f"ports must be a list, got {ports!r}.")
488
+ normalized_ports = []
489
+ for index, port in enumerate(ports):
490
+ if not isinstance(port, dict):
491
+ raise ConnectGraphError(f"ports[{index}] must be a mapping.")
492
+ name = port.get("name")
493
+ direction = port.get("direction")
494
+ if not isinstance(name, str) or not name:
495
+ raise ConnectGraphError(f"ports[{index}] needs a name.")
496
+ if direction not in _PORT_DIRECTIONS:
497
+ raise ConnectGraphError(
498
+ f"ports[{index}] direction must be one of {_PORT_DIRECTIONS}, "
499
+ f"got {direction!r}."
500
+ )
501
+ normalized_ports.append({"name": name, "direction": direction})
502
+
503
+ return {
504
+ "cell": cell,
505
+ "segments": segments,
506
+ "pins": normalized_pins,
507
+ "labels": normalized_labels,
508
+ "instances": normalized_instances,
509
+ "symbols": normalized_symbols,
510
+ "ports": normalized_ports,
511
+ }
512
+
513
+
514
+ def _digest(normalized):
515
+ text = json.dumps(
516
+ {
517
+ "cell": normalized["cell"],
518
+ "segments": [sorted([list(a), list(b)]) for a, b in normalized["segments"]],
519
+ "pins": normalized["pins"],
520
+ "labels": normalized["labels"],
521
+ "instances": normalized["instances"],
522
+ "symbols": normalized["symbols"],
523
+ "ports": normalized["ports"],
524
+ },
525
+ sort_keys=True,
526
+ )
527
+ return hashlib.sha256(text.encode()).hexdigest()
528
+
529
+
530
+ def _point_on_segment(point, seg_a, seg_b):
531
+ (px, py), (ax, ay), (bx, by) = point, seg_a, seg_b
532
+ if (bx - ax) * (py - ay) != (by - ay) * (px - ax):
533
+ return False
534
+ return min(ax, bx) <= px <= max(ax, bx) and min(ay, by) <= py <= max(ay, by)
535
+
536
+
537
+ class _UnionFind:
538
+ """Union-find with path compression over small integer ids."""
539
+
540
+ def __init__(self):
541
+ self._parent = {}
542
+
543
+ def _root(self, node):
544
+ parent = self._parent.setdefault(node, node)
545
+ while parent != self._parent[parent]:
546
+ self._parent[node] = self._parent[parent]
547
+ node = parent
548
+ parent = self._parent[node]
549
+ return parent
550
+
551
+ def union(self, first, second):
552
+ """Merge the sets holding two nodes.
553
+
554
+ Args:
555
+ first: First node.
556
+ second: Second node.
557
+ """
558
+ root_first, root_second = self._root(first), self._root(second)
559
+ if root_first != root_second:
560
+ self._parent[root_second] = root_first
561
+
562
+ def groups(self):
563
+ """Return the disjoint node sets.
564
+
565
+ Returns:
566
+ List of node groups.
567
+ """
568
+ grouped = {}
569
+ for node in self._parent:
570
+ grouped.setdefault(self._root(node), []).append(node)
571
+ return list(grouped.values())
572
+
573
+
574
+ class _SpatialHash:
575
+ """Grid index from segment bboxes to segment ids."""
576
+
577
+ def __init__(self, segments):
578
+ self._segments = segments
579
+ self._cells = {}
580
+ for index, (a, b) in enumerate(segments):
581
+ for cx in range(
582
+ min(a[0], b[0]) // _HASH_CELL, max(a[0], b[0]) // _HASH_CELL + 1
583
+ ):
584
+ for cy in range(
585
+ min(a[1], b[1]) // _HASH_CELL, max(a[1], b[1]) // _HASH_CELL + 1
586
+ ):
587
+ self._cells.setdefault((cx, cy), []).append(index)
588
+
589
+ def segments_near(self, point):
590
+ """Return candidate segment ids whose bbox covers a point.
591
+
592
+ Args:
593
+ point: (x, y) query point.
594
+
595
+ Returns:
596
+ Segment ids indexed under the point's grid cell.
597
+ """
598
+ return self._cells.get((point[0] // _HASH_CELL, point[1] // _HASH_CELL), [])
599
+
600
+
601
+ def _extract(normalized):
602
+ ignored = {i["name"] for i in normalized["instances"] if i["ignored"]}
603
+ pins = [p for p in normalized["pins"] if p["instance"] not in ignored]
604
+ segments = normalized["segments"]
605
+ labels = normalized["labels"]
606
+
607
+ union = _UnionFind()
608
+ point_nodes = {}
609
+
610
+ def _claim(point, node):
611
+ union._root(node)
612
+ first = point_nodes.setdefault(point, node)
613
+ union.union(first, node)
614
+
615
+ for index, (a, b) in enumerate(segments):
616
+ _claim(a, ("s", index))
617
+ _claim(b, ("s", index))
618
+ for index, pin in enumerate(pins):
619
+ _claim(pin["xy"], ("p", index))
620
+ for index, label in enumerate(labels):
621
+ _claim(label["xy"], ("l", index))
622
+
623
+ grid = _SpatialHash(segments)
624
+ touch_points = (
625
+ [a for a, _ in segments]
626
+ + [b for _, b in segments]
627
+ + [p["xy"] for p in pins]
628
+ + [label["xy"] for label in labels]
629
+ )
630
+ for point in touch_points:
631
+ for index in grid.segments_near(point):
632
+ if _point_on_segment(point, *segments[index]):
633
+ union.union(point_nodes[point], ("s", index))
634
+
635
+ member_of = {}
636
+ for index, pin in enumerate(pins):
637
+ member_of[("p", index)] = ("pin", pin["instance"], pin["pin"])
638
+ for index, label in enumerate(labels):
639
+ member_of[("l", index)] = ("label", label["name"], index)
640
+ for index in range(len(segments)):
641
+ member_of[("s", index)] = ("seg", index)
642
+
643
+ nets = []
644
+ for group in union.groups():
645
+ members = sorted(member_of[n] for n in group)
646
+ pins_here = sorted((m[1], m[2]) for m in members if m[0] == "pin")
647
+ labels_here = sorted({m[1] for m in members if m[0] == "label"})
648
+ nets.append(
649
+ {"anchor": members[0], "pins": pins_here, "labels": labels_here,
650
+ "has_wires": any(m[0] == "seg" for m in members)}
651
+ )
652
+ nets.sort(key=lambda n: n["anchor"])
653
+
654
+ used_names = {label for net in nets for label in net["labels"]}
655
+ counter = 1
656
+ final = []
657
+ for net_id, net in enumerate(nets):
658
+ if net["labels"]:
659
+ name = net["labels"][0]
660
+ else:
661
+ while f"net{counter}" in used_names:
662
+ counter += 1
663
+ name = f"net{counter}"
664
+ used_names.add(name)
665
+ counter += 1
666
+ final.append(
667
+ Net(
668
+ id=net_id,
669
+ name=name,
670
+ pins=net["pins"],
671
+ labels=net["labels"],
672
+ has_wires=net["has_wires"],
673
+ )
674
+ )
675
+
676
+ pin_index = {}
677
+ for net in final:
678
+ for member in net.pins:
679
+ pin_index[member] = net.id
680
+ shorts = [
681
+ {"net": net.id, "labels": list(net.labels)}
682
+ for net in final
683
+ if len(net.labels) > 1
684
+ ]
685
+ connectivity = Connectivity(
686
+ cell=normalized["cell"], nets=final, shorts=shorts, pin_index=pin_index
687
+ )
688
+
689
+ canonical = {net.name for net in final}
690
+ for port in normalized["ports"]:
691
+ if port["name"] not in canonical:
692
+ raise ConnectGraphError(
693
+ f"Port {port['name']!r} names no extracted net "
694
+ f"(nets: {sorted(canonical)})."
695
+ )
696
+
697
+ by_instance = {}
698
+ for pin in normalized["pins"]:
699
+ if pin["instance"] in ignored:
700
+ continue
701
+ by_instance.setdefault(pin["instance"], {})[pin["pin"]] = pin_index[
702
+ (pin["instance"], pin["pin"])
703
+ ]
704
+ instances = []
705
+ for instance in normalized["instances"]:
706
+ if instance["ignored"]:
707
+ continue
708
+ symbol = normalized["symbols"][instance["symbol"]]
709
+ order = symbol["pins"]
710
+ pin_map = by_instance.get(instance["name"], {})
711
+ if order:
712
+ ordered = [(p, pin_map[p]) for p in order if p in pin_map]
713
+ ordered += sorted((p, n) for p, n in pin_map.items() if p not in order)
714
+ else:
715
+ ordered = sorted(pin_map.items())
716
+ instances.append(
717
+ ResolvedInstance(
718
+ name=instance["name"],
719
+ symbol=instance["symbol"],
720
+ pins=ordered,
721
+ generics={**symbol["generics"], **instance["generics"]},
722
+ custom=instance["custom"],
723
+ format=instance["format"],
724
+ )
725
+ )
726
+
727
+ return NetlistStructs(
728
+ cell=normalized["cell"],
729
+ connectivity=connectivity,
730
+ instances=instances,
731
+ symbols=normalized["symbols"],
732
+ ports=normalized["ports"],
733
+ ignored=[i["name"] for i in normalized["instances"] if i["ignored"]],
734
+ )