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,609 @@
1
+ """Gdsfactory layout adapter: generate_views behind the ToolAdapter port.
2
+
3
+ Protocol, mirroring the SAX adapter: stage the views driver plus a job
4
+ JSON into the workdir, run `python gf_views_driver.py
5
+ gf_views_job.json` under a timeout, then parse the JSON envelope on
6
+ stdout. The verdict always comes from parsed output: an unknown cell,
7
+ an unreadable GDS, or an unsupported export format synthesizes an
8
+ error violation with file/line, and only a gdsfactory-less (or
9
+ PDK-less) interpreter raises missing_dependency.
10
+
11
+ Generalization law: project inputs in, identity mappings plus
12
+ diagnostics out. View specs (PDK, cells, params, export filenames),
13
+ project GDS files, and the macro pin map all ride the job — the MZI
14
+ smoke (`run_layout_smoke`) is the qualification fixture, and this op
15
+ generalizes it to any spec. GDS roundtrip readback is a diagnostic on
16
+ the export recipe, never the verdict. Macro pin/obstruction mappings
17
+ publish as workdir artifacts with provenance: a bound project pin_map
18
+ reconstructs them, otherwise the artifact carries the PRD boundary
19
+ note (import alone does not reconstruct them).
20
+
21
+ Tool resolution: an explicit job `tool_dir` containing a python
22
+ wins, else the engine interpreter (gdsfactory arrives via the pip
23
+ `layout` group). Where the tools live on disk is B10 scope.
24
+ """
25
+
26
+ import json
27
+ import subprocess
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ from ruamel.yaml import YAML
32
+
33
+ try:
34
+ from ...execution.errors import ExecutionError
35
+ except ImportError:
36
+ from rocket_engine.execution.errors import ExecutionError
37
+ try:
38
+ from ...execution.runner import tool_roots
39
+ except ImportError:
40
+ from rocket_engine.execution.runner import tool_roots
41
+ try:
42
+ from ...interfaces import ToolAdapter
43
+ except ImportError:
44
+ from rocket_engine.interfaces import ToolAdapter
45
+ try:
46
+ from ..envelope import extract_envelope
47
+ except ImportError:
48
+ from rocket_engine.adapters.envelope import extract_envelope
49
+ try:
50
+ from ..errors import AdapterError
51
+ except ImportError:
52
+ from rocket_engine.adapters.errors import AdapterError
53
+
54
+ OPERATION = "generate_views"
55
+ RECIPES = {
56
+ "import_gds": "Import project GDS files; map cells, publish pins.",
57
+ "build_cells": "Build PDK cells from a project view spec.",
58
+ "export_gds": "Build cells and stream GDS layouts to the workdir.",
59
+ "export_oasis": "Build cells and stream OASIS layouts to the workdir.",
60
+ }
61
+ _PDKS = ("generic", "cspdk.si340")
62
+ _EXPORT_EXT = {"export_gds": ".gds", "export_oasis": ".oas"}
63
+ _RULES = {
64
+ "invalid_cell": "views.unknown_cell",
65
+ "unreadable_source": "views.unreadable_source",
66
+ "unsupported_format": "views.unsupported_format",
67
+ }
68
+ _TIMEOUT_S = 120
69
+ _DRIVER_NAME = "gf_views_driver.py"
70
+ _JOB_NAME = "gf_views_job.json"
71
+
72
+
73
+ def _load_yaml(path, what):
74
+ try:
75
+ text = Path(path).read_text(encoding="utf-8")
76
+ except OSError as exc:
77
+ raise AdapterError(
78
+ "invalid_reference", f"{what} unreadable: {path} ({exc})."
79
+ ) from exc
80
+ try:
81
+ doc = YAML(typ="safe").load(text)
82
+ except Exception as exc:
83
+ mark = getattr(exc, "problem_mark", None) or getattr(
84
+ exc, "context_mark", None
85
+ )
86
+ where = (
87
+ f"{path}:{mark.line + 1}:{mark.column + 1}"
88
+ if mark is not None
89
+ else path
90
+ )
91
+ raise AdapterError(
92
+ "invalid_reference", f"{what} is not YAML: {where} ({exc})."
93
+ ) from exc
94
+ return text, doc
95
+
96
+
97
+ def _check_view_doc(doc, spec):
98
+ if not isinstance(doc, dict):
99
+ raise AdapterError(
100
+ "invalid_command", f"View spec is not a mapping: {spec}."
101
+ )
102
+ pdk = doc.get("pdk", "generic")
103
+ if pdk not in _PDKS:
104
+ raise AdapterError(
105
+ "invalid_command",
106
+ f"View spec pdk must be one of {', '.join(_PDKS)}, "
107
+ f"got {pdk!r}: {spec}.",
108
+ )
109
+ cells = doc.get("cells")
110
+ if not isinstance(cells, list) or not cells:
111
+ raise AdapterError(
112
+ "invalid_command", f"View spec needs a cells list: {spec}."
113
+ )
114
+ for entry in cells:
115
+ if not isinstance(entry, dict) or not entry.get("cell"):
116
+ raise AdapterError(
117
+ "invalid_command",
118
+ f"View spec cell entries need a cell name: {spec}.",
119
+ )
120
+ params = entry.get("params", {})
121
+ if not isinstance(params, dict):
122
+ raise AdapterError(
123
+ "invalid_command",
124
+ f"Cell {entry.get('cell')!r} params must be a "
125
+ f"mapping: {spec}.",
126
+ )
127
+ exports = doc.get("exports", [])
128
+ if not isinstance(exports, list):
129
+ raise AdapterError(
130
+ "invalid_command", f"View spec exports must be a list: {spec}."
131
+ )
132
+ for export in exports:
133
+ if not isinstance(export, dict) or not export.get("filename"):
134
+ raise AdapterError(
135
+ "invalid_command",
136
+ f"View spec exports need a filename: {spec}.",
137
+ )
138
+
139
+
140
+ def _check_pin_map(doc, path):
141
+ if not isinstance(doc, dict):
142
+ raise AdapterError(
143
+ "invalid_command", f"Pin map is not a mapping: {path}."
144
+ )
145
+ cells = doc.get("cells")
146
+ if not isinstance(cells, dict) or not cells:
147
+ raise AdapterError(
148
+ "invalid_command", f"Pin map needs a cells mapping: {path}."
149
+ )
150
+ for cell, binding in cells.items():
151
+ if not isinstance(binding, dict):
152
+ raise AdapterError(
153
+ "invalid_command",
154
+ f"Pin map cell {cell!r} must be a mapping: {path}.",
155
+ )
156
+ pins = binding.get("pins", {})
157
+ if not isinstance(pins, dict):
158
+ raise AdapterError(
159
+ "invalid_command",
160
+ f"Pin map cell {cell!r} pins must be a mapping: {path}.",
161
+ )
162
+ for name, pin in pins.items():
163
+ if not isinstance(pin, dict):
164
+ raise AdapterError(
165
+ "invalid_command",
166
+ f"Pin map pin {name!r} must be a mapping: {path}.",
167
+ )
168
+ obstructions = binding.get("obstructions", [])
169
+ if not isinstance(obstructions, list):
170
+ raise AdapterError(
171
+ "invalid_command",
172
+ f"Pin map cell {cell!r} obstructions must be a "
173
+ f"list: {path}.",
174
+ )
175
+
176
+
177
+ def _check_number_list(pair, what, where):
178
+ if (
179
+ not isinstance(pair, list)
180
+ or len(pair) != 2
181
+ or not all(
182
+ isinstance(v, (int, float)) and not isinstance(v, bool)
183
+ for v in pair
184
+ )
185
+ ):
186
+ raise AdapterError(
187
+ "runner_unavailable",
188
+ f"Views driver result has a bad {what}: {where}.",
189
+ )
190
+ return [float(pair[0]), float(pair[1])]
191
+
192
+
193
+ class GdsfactoryViewsAdapter(ToolAdapter):
194
+ """generate_views via gdsfactory: registered layout recipes.
195
+
196
+ Serves the generate_views operation with deck.tool `gdsfactory`
197
+ (the only tool). Stages the views driver plus a job JSON, runs
198
+ it under a timeout, and parses the JSON envelope: identity
199
+ mappings (requested source to built/imported component),
200
+ diagnostics (versions, PDK, roundtrip readback), and workdir
201
+ artifacts (layouts plus pin/obstruction mappings).
202
+
203
+ Job keys:
204
+ recipe (required): Registered recipe — import_gds,
205
+ build_cells, export_gds, or export_oasis.
206
+ sources (required): Project inputs — exactly one view-spec
207
+ YAML for build/export recipes, one or more .gds files
208
+ for import_gds.
209
+ pin_map: Project macro pin-map YAML (import_gds only);
210
+ binds pins/obstructions with provenance.
211
+ cspdk_path: Vendored cspdk tree when the package is not
212
+ installed.
213
+ timeout_s: Layout budget in seconds (default 120).
214
+ tool_dir: Directory holding the layout python; the engine
215
+ interpreter otherwise.
216
+ deck: Optional mapping; deck.tool must be gdsfactory here.
217
+ workdir: Run directory (default ".").
218
+
219
+ Args:
220
+ runner: Optional isolated runner; direct subprocess when None.
221
+
222
+ Caution: Unknown cells and unreadable GDS synthesize error
223
+ violations with file/line — the verdict always comes from
224
+ parsed output, never the exit code. GDS roundtrip readback
225
+ is a diagnostic, never the verdict.
226
+ """
227
+
228
+ def __init__(self, runner=None):
229
+ self._runner = runner
230
+
231
+ def capabilities(self, target):
232
+ """Report the served operation, tool, and recipes.
233
+
234
+ Args:
235
+ target: Target the capabilities are reported for
236
+ (accepted, unused — one spec per job).
237
+
238
+ Returns:
239
+ Capability mapping with operations, tool, and recipes.
240
+ """
241
+ return {
242
+ "operations": [OPERATION],
243
+ "tool": "gdsfactory",
244
+ "recipes": sorted(RECIPES),
245
+ }
246
+
247
+ def preflight(self, job):
248
+ """Check operation, recipe, sources, spec shape, and python.
249
+
250
+ Cheap deterministic checks first, tool resolution last, the
251
+ runner probe closing it out.
252
+
253
+ Args:
254
+ job: Solver job with operation generate_views plus the
255
+ recipe, project sources, and timeout inputs.
256
+
257
+ Returns:
258
+ Checked mapping with the resolved python, recipe, spec
259
+ (spec/text/doc for build/export, gds_sources plus
260
+ pin map for import), cspdk path, and timeout_s.
261
+
262
+ Raises:
263
+ AdapterError: unsupported_operation for another
264
+ operation, invalid_command for a bad recipe,
265
+ timeout, spec shape, pin map, or export filename,
266
+ invalid_reference unless the recipe's project
267
+ sources exist and parse, missing_dependency for an
268
+ unresolvable layout python.
269
+ """
270
+ if job.get("operation") != OPERATION:
271
+ raise AdapterError(
272
+ "unsupported_operation",
273
+ f"Views handles {OPERATION}, got {job.get('operation')}.",
274
+ )
275
+ recipe = job.get("recipe")
276
+ if recipe not in RECIPES:
277
+ raise AdapterError(
278
+ "invalid_command",
279
+ f"Unknown recipe {recipe!r}; want one of "
280
+ f"{', '.join(sorted(RECIPES))}.",
281
+ )
282
+ timeout = job.get("timeout_s", _TIMEOUT_S)
283
+ if (
284
+ not isinstance(timeout, (int, float))
285
+ or isinstance(timeout, bool)
286
+ or not timeout > 0
287
+ ):
288
+ raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
289
+ cspdk_path = job.get("cspdk_path")
290
+ if cspdk_path is not None and not Path(cspdk_path).is_dir():
291
+ raise AdapterError(
292
+ "invalid_reference", f"cspdk_path not found: {cspdk_path}."
293
+ )
294
+ pin_map = job.get("pin_map")
295
+ checked = {
296
+ "python": self._resolve(job),
297
+ "recipe": recipe,
298
+ "cspdk_path": cspdk_path,
299
+ "timeout_s": timeout,
300
+ }
301
+ sources = job.get("sources") or []
302
+ if recipe == "import_gds":
303
+ missing = [s for s in sources if not Path(s).is_file()]
304
+ if not sources or missing:
305
+ raise AdapterError(
306
+ "invalid_reference",
307
+ "import_gds needs project GDS files, got: "
308
+ f"{missing or sources}.",
309
+ )
310
+ for source in sources:
311
+ if Path(source).suffix.lower() != ".gds":
312
+ raise AdapterError(
313
+ "invalid_command",
314
+ f"import_gds needs .gds sources: {source}.",
315
+ )
316
+ checked["gds_sources"] = list(sources)
317
+ checked["spec"] = ""
318
+ checked["text"] = ""
319
+ checked["doc"] = None
320
+ if pin_map is None:
321
+ checked["pin_map_path"] = None
322
+ checked["pin_map_doc"] = None
323
+ else:
324
+ if not Path(pin_map).is_file():
325
+ raise AdapterError(
326
+ "invalid_reference",
327
+ f"Pin map not found: {pin_map}.",
328
+ )
329
+ pin_text, pin_doc = _load_yaml(pin_map, "Pin map")
330
+ _check_pin_map(pin_doc, pin_map)
331
+ checked["pin_map_path"] = pin_map
332
+ checked["pin_map_doc"] = pin_doc
333
+ checked["pin_map_text"] = pin_text
334
+ else:
335
+ if pin_map is not None:
336
+ raise AdapterError(
337
+ "invalid_command",
338
+ f"pin_map applies to import_gds, not {recipe}.",
339
+ )
340
+ missing = [s for s in sources if not Path(s).is_file()]
341
+ if len(sources) != 1 or missing:
342
+ raise AdapterError(
343
+ "invalid_reference",
344
+ "generate_views needs one view spec, got: "
345
+ f"{missing or sources}.",
346
+ )
347
+ spec = sources[0]
348
+ text, doc = _load_yaml(spec, "View spec")
349
+ _check_view_doc(doc, spec)
350
+ if recipe in _EXPORT_EXT:
351
+ want = _EXPORT_EXT[recipe]
352
+ for export in doc.get("exports") or []:
353
+ if not str(export["filename"]).endswith(want):
354
+ raise AdapterError(
355
+ "invalid_command",
356
+ f"{recipe} needs {want} filenames: "
357
+ f"{export['filename']}.",
358
+ )
359
+ checked["spec"] = spec
360
+ checked["text"] = text
361
+ checked["doc"] = doc
362
+ checked["gds_sources"] = []
363
+ checked["pin_map_path"] = None
364
+ checked["pin_map_doc"] = None
365
+ if self._runner is not None:
366
+ try:
367
+ self._runner.preflight()
368
+ except ExecutionError as exc:
369
+ raise AdapterError(exc.code, str(exc)) from exc
370
+ return checked
371
+
372
+ def execute(self, job):
373
+ """Stage the driver plus job JSON, then run the recipe.
374
+
375
+ Args:
376
+ job: Solver job with operation generate_views plus the
377
+ recipe, project sources, and timeout inputs.
378
+
379
+ Returns:
380
+ Native output with exit_code, stdout/stderr, recipe,
381
+ spec, spec_text, and sources.
382
+
383
+ Raises:
384
+ AdapterError: invalid_reference for a missing workdir,
385
+ runner_unavailable on a recipe timeout, plus the
386
+ preflight codes (execute re-checks first).
387
+ """
388
+ checked = self.preflight(job)
389
+ workdir = job.get("workdir", ".")
390
+ if not Path(workdir).is_dir():
391
+ raise AdapterError(
392
+ "invalid_reference", f"Workdir not found: {workdir}."
393
+ )
394
+ workdir_path = Path(workdir)
395
+ driver = Path(__file__).with_name("_driver.py").read_text(
396
+ encoding="utf-8"
397
+ )
398
+ (workdir_path / _DRIVER_NAME).write_text(driver, encoding="utf-8")
399
+ (workdir_path / _JOB_NAME).write_text(
400
+ json.dumps(
401
+ {
402
+ "recipe": checked["recipe"],
403
+ "spec": checked["spec"],
404
+ "text": checked["text"],
405
+ "doc": checked["doc"],
406
+ "gds_sources": checked["gds_sources"],
407
+ "pin_map_path": checked["pin_map_path"],
408
+ "pin_map_doc": checked["pin_map_doc"],
409
+ "cspdk_path": checked["cspdk_path"],
410
+ }
411
+ ),
412
+ encoding="utf-8",
413
+ )
414
+ roots = tool_roots([checked["python"]])
415
+ proc = self._run(
416
+ [checked["python"], _DRIVER_NAME, _JOB_NAME],
417
+ workdir,
418
+ checked["timeout_s"],
419
+ roots,
420
+ )
421
+ return {
422
+ "exit_code": proc.returncode,
423
+ "stdout": proc.stdout,
424
+ "stderr": proc.stderr,
425
+ "recipe": checked["recipe"],
426
+ "spec": checked["spec"],
427
+ "spec_text": checked["text"],
428
+ "sources": list(job.get("sources") or []),
429
+ }
430
+
431
+ def parse(self, native_output):
432
+ """Parse the driver envelope into mappings and diagnostics.
433
+
434
+ Unknown cells, unreadable GDS, and unsupported formats
435
+ synthesize one error violation with file/line rather than
436
+ parsing as clean; roundtrip readback stays a diagnostic.
437
+
438
+ Args:
439
+ native_output: Native output with the driver stdout
440
+ envelope, recipe, spec, spec_text, and sources.
441
+
442
+ Returns:
443
+ {total, violations[], measurements[], mappings[],
444
+ diagnostics{}, artifacts[],
445
+ gdsfactory_version} with identity mappings, per-cell
446
+ polygon/port/area measurements, and workdir
447
+ artifact paths.
448
+
449
+ Raises:
450
+ AdapterError: missing_dependency when the layout python
451
+ has no gdsfactory or PDK, runner_unavailable when
452
+ the driver printed no JSON envelope or no result.
453
+ """
454
+ spec = native_output.get("spec") or ""
455
+ if not spec:
456
+ sources = native_output.get("sources") or []
457
+ spec = sources[0] if sources else ""
458
+ envelope = extract_envelope(
459
+ native_output.get("stdout", ""), tool="views"
460
+ )
461
+ if not envelope["ok"]:
462
+ error = envelope.get("error") or {}
463
+ code = error.get("code")
464
+ message = error.get("message", "views run failed")
465
+ if code == "missing_dependency":
466
+ raise AdapterError("missing_dependency", message)
467
+ violation = {
468
+ "rule": _RULES.get(code, "views.failed"),
469
+ "severity": "error",
470
+ "message": message,
471
+ "file": spec,
472
+ }
473
+ if error.get("line") is not None:
474
+ violation["line"] = error["line"]
475
+ return {
476
+ "total": 1,
477
+ "violations": [violation],
478
+ "measurements": [],
479
+ "mappings": [],
480
+ "diagnostics": {
481
+ "recipe": native_output.get("recipe", "")
482
+ },
483
+ "artifacts": [],
484
+ "gdsfactory_version": envelope.get("gdsfactory_version"),
485
+ }
486
+ result = envelope.get("result") or {}
487
+ mappings = result.get("mappings")
488
+ artifacts = result.get("artifacts")
489
+ diagnostics = result.get("diagnostics")
490
+ if (
491
+ not isinstance(mappings, list)
492
+ or not mappings
493
+ or not isinstance(artifacts, list)
494
+ or not isinstance(diagnostics, dict)
495
+ ):
496
+ raise AdapterError(
497
+ "runner_unavailable", "views driver envelope has no result."
498
+ )
499
+ measurements = []
500
+ for mapping in mappings:
501
+ if not isinstance(mapping, dict):
502
+ raise AdapterError(
503
+ "runner_unavailable",
504
+ "views driver envelope has no result.",
505
+ )
506
+ try:
507
+ cell = mapping["component"]
508
+ ports = mapping["ports"]
509
+ polygons = mapping["n_polygons"]
510
+ bbox = mapping["bbox_um"]
511
+ except KeyError:
512
+ raise AdapterError(
513
+ "runner_unavailable",
514
+ "views driver envelope has no result.",
515
+ ) from None
516
+ if (
517
+ not isinstance(cell, str)
518
+ or not isinstance(ports, list)
519
+ or not isinstance(polygons, int)
520
+ or isinstance(polygons, bool)
521
+ or not isinstance(bbox, list)
522
+ or len(bbox) != 2
523
+ ):
524
+ raise AdapterError(
525
+ "runner_unavailable",
526
+ "views driver envelope has no result.",
527
+ )
528
+ lower = _check_number_list(bbox[0], "bbox", cell)
529
+ upper = _check_number_list(bbox[1], "bbox", cell)
530
+ area = (upper[0] - lower[0]) * (upper[1] - lower[1])
531
+ measurements.append(
532
+ {
533
+ "name": f"polygons({cell})",
534
+ "value": polygons,
535
+ "unit": "count",
536
+ }
537
+ )
538
+ measurements.append(
539
+ {
540
+ "name": f"ports({cell})",
541
+ "value": len(ports),
542
+ "unit": "count",
543
+ }
544
+ )
545
+ measurements.append(
546
+ {
547
+ "name": f"bbox_area_um2({cell})",
548
+ "value": area,
549
+ "unit": "um2",
550
+ }
551
+ )
552
+ for artifact in artifacts:
553
+ if (
554
+ not isinstance(artifact, dict)
555
+ or not isinstance(artifact.get("kind"), str)
556
+ or not isinstance(artifact.get("path"), str)
557
+ ):
558
+ raise AdapterError(
559
+ "runner_unavailable",
560
+ "views driver envelope has no result.",
561
+ )
562
+ return {
563
+ "total": 0,
564
+ "violations": [],
565
+ "measurements": measurements,
566
+ "mappings": mappings,
567
+ "diagnostics": {
568
+ "recipe": result.get("recipe", ""),
569
+ "active_pdk": result.get("active_pdk", ""),
570
+ "cspdk_version": result.get("cspdk_version"),
571
+ "roundtrip_match": diagnostics.get("roundtrip_match"),
572
+ "notes": list(diagnostics.get("notes") or []),
573
+ },
574
+ "artifacts": artifacts,
575
+ "gdsfactory_version": envelope.get("gdsfactory_version"),
576
+ }
577
+
578
+ @staticmethod
579
+ def _resolve(job):
580
+ tool_dir = job.get("tool_dir")
581
+ if tool_dir:
582
+ for name in ("python3", "python"):
583
+ candidate = Path(tool_dir) / name
584
+ if candidate.is_file():
585
+ return str(candidate)
586
+ raise AdapterError(
587
+ "missing_dependency", f"python not found in {tool_dir}."
588
+ )
589
+ return sys.executable
590
+
591
+ def _run(self, argv, workdir, timeout_s, extra_binds=()):
592
+ if self._runner is not None:
593
+ try:
594
+ return self._runner.run(argv, workdir, timeout_s, extra_binds)
595
+ except ExecutionError as exc:
596
+ raise AdapterError(exc.code, str(exc)) from exc
597
+ try:
598
+ return subprocess.run(
599
+ argv,
600
+ capture_output=True,
601
+ text=True,
602
+ timeout=timeout_s,
603
+ cwd=workdir,
604
+ )
605
+ except subprocess.TimeoutExpired as exc:
606
+ raise AdapterError(
607
+ "runner_unavailable",
608
+ f"views recipe timed out after {timeout_s}s.",
609
+ ) from exc