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,167 @@
1
+ """Meep FDTD adapter: run_optical behind the ToolAdapter port."""
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from ...execution.errors import ExecutionError
9
+ from ...interfaces import ToolAdapter
10
+ from ..errors import AdapterError
11
+
12
+ OPERATION = "run_optical"
13
+ _TOOL = "meep"
14
+ _DUMP = "meep_dump.json"
15
+ _TIMEOUT_S = 600
16
+ _FLUX_RE = re.compile(r"^(flux|force|(?:electric|magnetic|total)-energy"
17
+ r"|ldos|harminv)\d*:,", re.M)
18
+
19
+
20
+ class MeepAdapter(ToolAdapter):
21
+ """run_optical via meep: in-API dump plus HDF5 telemetry.
22
+
23
+ Serves the run_optical operation with tool `meep`. The driver
24
+ runs under subprocess python (pymeep env) and reports via
25
+ get_fluxes/get_dft_array/get_array/Harminv.modes dumped to
26
+ JSON; HDF5 field files stay as artifacts. Deterministic S1
27
+ telemetry lines are the fallback, never the first channel.
28
+
29
+ Job keys:
30
+ script (required): Driver python script path.
31
+ workdir: Run directory (default ".").
32
+ timeout_s: Budget in seconds (default 600).
33
+
34
+ Args:
35
+ runner: Optional isolated runner; direct subprocess when None.
36
+ """
37
+
38
+ def __init__(self, runner=None):
39
+ self._runner = runner
40
+
41
+ def capabilities(self, target):
42
+ """Report the served operation and tool.
43
+
44
+ Args:
45
+ target: Target the capabilities are reported for.
46
+
47
+ Returns:
48
+ Capability mapping with operations and tool.
49
+ """
50
+ return {"operations": [OPERATION], "tool": _TOOL, "dump": _DUMP}
51
+
52
+ def preflight(self, job):
53
+ """Check operation, script, and meep importability.
54
+
55
+ Args:
56
+ job: Solver job with operation run_optical plus script.
57
+
58
+ Returns:
59
+ Checked mapping with script and timeout.
60
+
61
+ Raises:
62
+ AdapterError: unsupported_operation, invalid_reference
63
+ for a missing script, missing_dependency with
64
+ BLOCKED-needs-host when meep is not importable.
65
+ """
66
+ if job.get("operation") != OPERATION:
67
+ raise AdapterError(
68
+ "unsupported_operation",
69
+ f"Meep handles {OPERATION}, got {job.get('operation')}.")
70
+ script = job.get("script")
71
+ if not script or not Path(script).is_file():
72
+ raise AdapterError(
73
+ "invalid_reference", f"Driver script not found: {script}.")
74
+ try:
75
+ __import__(_TOOL)
76
+ except ImportError as exc:
77
+ raise AdapterError(
78
+ "missing_dependency",
79
+ "BLOCKED-needs-host: meep not importable "
80
+ "(need pymeep==1.35.0 via the conda env).",
81
+ ) from exc
82
+ timeout = job.get("timeout_s", _TIMEOUT_S)
83
+ if (not isinstance(timeout, (int, float))
84
+ or isinstance(timeout, bool) or not timeout > 0):
85
+ raise AdapterError("invalid_command", f"Bad timeout: {timeout!r}.")
86
+ checked = {"script": str(script), "timeout_s": timeout}
87
+ if self._runner is not None:
88
+ try:
89
+ self._runner.preflight()
90
+ except ExecutionError as exc:
91
+ raise AdapterError(exc.code, str(exc)) from exc
92
+ return checked
93
+
94
+ def execute(self, job):
95
+ """Run the driver script under a timeout.
96
+
97
+ Args:
98
+ job: Solver job with operation run_optical plus script.
99
+
100
+ Returns:
101
+ Native output with exit_code, stdout/stderr, dump, file.
102
+ """
103
+ checked = self.preflight(job)
104
+ workdir = job.get("workdir", ".")
105
+ if not Path(workdir).is_dir():
106
+ raise AdapterError(
107
+ "invalid_reference", f"Workdir not found: {workdir}.")
108
+ try:
109
+ proc = subprocess.run(
110
+ ["python3", checked["script"]], capture_output=True,
111
+ text=True, timeout=checked["timeout_s"], cwd=workdir)
112
+ except subprocess.TimeoutExpired as exc:
113
+ raise AdapterError(
114
+ "runner_unavailable", "Meep run timed out.") from exc
115
+ dump_path = Path(workdir) / _DUMP
116
+ dump = None
117
+ if dump_path.is_file():
118
+ try:
119
+ dump = json.loads(dump_path.read_text())
120
+ except ValueError:
121
+ dump = None
122
+ return {"exit_code": proc.returncode, "stdout": proc.stdout,
123
+ "stderr": proc.stderr, "dump": dump,
124
+ "dump_file": _DUMP}
125
+
126
+ def parse(self, native_output):
127
+ """Parse the in-API dump, else S1 telemetry, else violation.
128
+
129
+ Args:
130
+ native_output: Native output with the dump mapping.
131
+
132
+ Returns:
133
+ {total, violations[], measurements[], source}.
134
+
135
+ Raises:
136
+ AdapterError: runner_unavailable with no dump and no
137
+ telemetry (a failed run must never parse as clean).
138
+ """
139
+ dump = native_output.get("dump")
140
+ if isinstance(dump, dict):
141
+ measurements = []
142
+ for flux in dump.get("fluxes") or []:
143
+ try:
144
+ measurements.append({"name": "flux", "value": float(flux),
145
+ "unit": "a.u."})
146
+ except (TypeError, ValueError):
147
+ continue
148
+ trans = dump.get("transmission")
149
+ if isinstance(trans, (int, float)) and not isinstance(trans, bool):
150
+ measurements.append({"name": "transmission",
151
+ "value": float(trans), "unit": "ratio"})
152
+ return {"total": 0, "violations": [], "measurements": measurements,
153
+ "source": "dump",
154
+ "dump_file": native_output.get("dump_file", _DUMP)}
155
+ text = (native_output.get("stdout") or "")
156
+ if _FLUX_RE.search(text):
157
+ return {"total": 0, "violations": [],
158
+ "measurements": [{"name": "telemetry", "value": 1.0,
159
+ "unit": "count"}],
160
+ "source": "stdio_fallback"}
161
+ if native_output.get("exit_code", 0) != 0:
162
+ return {"total": 1, "violations": [
163
+ {"rule": "optical_fail", "severity": "error",
164
+ "message": "Meep run left no dump or telemetry."}],
165
+ "measurements": [], "source": "stdio_fallback"}
166
+ raise AdapterError(
167
+ "runner_unavailable", "Meep output has no dump verdict.")
@@ -0,0 +1,7 @@
1
+ # MEOW eigenmode adapter
2
+
3
+ MeowAdapter serves run_em. It stages project-provided waveguide/device inputs and a simulation job, runs MEOW through its local mode-solving and SAX EME backends, and returns transmission, reflection, phase, mode, and convergence evidence.
4
+
5
+ The adapter is registered in Engine as the meow solver. Its focused qualification case is a silicon strip waveguide at 1550 nm with two modes: a matched guide transmits T=1/R=0, while an abrupt 0.5-to-1.0 μm width step lowers transmission and produces reflection. A missing dependency reports missing_dependency.
6
+
7
+ Engine code does not import the Tidy3D client or call its cloud service. The local smoke has a network-disabled check. FDTD and FEM support remain outside the proven scope. See the [MEOW-LIVE ticket](../../../docs/tickets/MEOW-LIVE.md).
@@ -0,0 +1,190 @@
1
+ """Meow EME smoke: smallest honest strip-waveguide S-matrix.
2
+
3
+ A 500x220nm Si strip on SiO2 at 1550nm, 2 modes solved with
4
+ meow's tidy3d FDE backend and cascaded over a 10um cell with
5
+ meow's SAX EME backend. Verified against meow 0.15.0 over
6
+ tidy3d 2.10.0: the matched guide transmits T=1.0/R=0.0, while
7
+ an abrupt 0.5->1.0um width step drops the fundamental to
8
+ T=0.877/R=0.0009 — the good-vs-bad signal. Solver imports stay
9
+ lazy so bare envs fail loud (`missing_dependency`), never
10
+ fake-green. Engine code never imports the tidy3d client: its
11
+ version is read from installed metadata, and the smoke path
12
+ runs with the network amputated (see the locality test). A
13
+ full `deck.tool: meow` Engine adapter is follow-up, out of
14
+ scope.
15
+ """
16
+
17
+ from importlib import metadata as _metadata
18
+
19
+ from ..errors import AdapterError
20
+
21
+ MEOW_PIN = "0.15.0"
22
+ TIDY3D_PIN = "2.10.0"
23
+
24
+ WL_UM = 1.55
25
+ CELL_UM = 10.0
26
+ NUM_MODES = 2
27
+ SI_HEIGHT_UM = 0.22
28
+ X_HALF_UM = 2.0
29
+ Y_HALF_UM = 2.0
30
+ NX = 61
31
+ NY = 41
32
+
33
+
34
+ def meow_version():
35
+ """Proven solver version, observable per run (uv.lock pin).
36
+
37
+ Returns:
38
+ Installed meow version, or "unknown" when the package
39
+ carries no version attribute.
40
+
41
+ Raises:
42
+ AdapterError: missing_dependency when meow is not
43
+ importable.
44
+ """
45
+ try:
46
+ import meow
47
+ except ImportError as exc:
48
+ raise AdapterError(
49
+ "missing_dependency",
50
+ f"meow is not importable: {exc} "
51
+ "(uv sync).",
52
+ ) from exc
53
+ return getattr(meow, "__version__", "unknown")
54
+
55
+
56
+ def tidy3d_version():
57
+ """FDE backend version, read from metadata (never a client import).
58
+
59
+ Returns:
60
+ Installed tidy3d version from package metadata.
61
+
62
+ Raises:
63
+ AdapterError: missing_dependency when tidy3d is not
64
+ installed.
65
+ """
66
+ try:
67
+ return _metadata.version("tidy3d")
68
+ except _metadata.PackageNotFoundError as exc:
69
+ raise AdapterError(
70
+ "missing_dependency",
71
+ f"tidy3d is not installed: {exc} "
72
+ "(uv sync).",
73
+ ) from exc
74
+
75
+
76
+ def run_eme_smoke(*, widths_um=(0.5,), wl_um=WL_UM, cell_length_um=CELL_UM,
77
+ num_modes=NUM_MODES):
78
+ """Run the EME smoke live; return the fundamental S-parameters.
79
+
80
+ Returns `{meow_version, tidy3d_version, wl_um, widths_um[],
81
+ cell_length_um, num_modes, neff_re[], neff_im[], transmission,
82
+ reflection, s21_phase_rad, ports}` — plain floats, JSON-friendly.
83
+ `transmission`/`reflection` are |S21|^2/|S11|^2 of the
84
+ fundamental; `neff_*` is the input guide's modal basis.
85
+
86
+ Args:
87
+ widths_um: Strip widths in um, one cross-section each.
88
+ wl_um: Wavelength in um (positive).
89
+ cell_length_um: EME cell length in um (positive).
90
+ num_modes: Modes per cross-section (at least 1).
91
+
92
+ Returns:
93
+ Fundamental S-parameter evidence mapping (versions,
94
+ widths, modal basis, transmission/reflection, ports).
95
+
96
+ Raises:
97
+ AdapterError: invalid_command on bad parameters,
98
+ missing_dependency where meow is uninstalled.
99
+ """
100
+ try:
101
+ widths = [float(w) for w in widths_um]
102
+ except (TypeError, ValueError) as exc:
103
+ raise AdapterError(
104
+ "invalid_command",
105
+ f"EME smoke needs a list of widths in um, got {widths_um!r}.",
106
+ ) from exc
107
+ if not widths or any(not w > 0 for w in widths):
108
+ raise AdapterError(
109
+ "invalid_command",
110
+ f"EME smoke needs positive widths in um, got {widths_um!r}.",
111
+ )
112
+ if not wl_um > 0 or not cell_length_um > 0:
113
+ raise AdapterError(
114
+ "invalid_command",
115
+ f"EME smoke needs positive wl/cell_length, got wl_um={wl_um!r} "
116
+ f"cell_length_um={cell_length_um!r}.",
117
+ )
118
+ if not isinstance(num_modes, int) or num_modes < 1:
119
+ raise AdapterError(
120
+ "invalid_command", f"EME smoke needs num_modes >= 1, got {num_modes!r}."
121
+ )
122
+ try:
123
+ import numpy as np
124
+
125
+ from meow import MATERIALS, CrossSection, Environment, Mesh2D, Rectangle
126
+ from meow.eme import compute_s_matrix
127
+ from meow.fde.tidy3d import compute_modes_tidy3d
128
+ from meow.structures import Structure2D
129
+ except ImportError as exc:
130
+ raise AdapterError(
131
+ "missing_dependency",
132
+ f"meow is not importable: {exc} "
133
+ "(uv sync).",
134
+ ) from exc
135
+ env = Environment(wl=wl_um)
136
+ mesh = Mesh2D(
137
+ x=np.linspace(-X_HALF_UM, X_HALF_UM, NX),
138
+ y=np.linspace(-Y_HALF_UM, Y_HALF_UM, NY),
139
+ )
140
+ silicon = MATERIALS["silicon"]
141
+ oxide = MATERIALS["silicon_oxide"]
142
+
143
+ def _strip(width):
144
+ return CrossSection(
145
+ structures=[
146
+ Structure2D(
147
+ material=oxide,
148
+ geometry=Rectangle(
149
+ x_min=-X_HALF_UM, x_max=X_HALF_UM,
150
+ y_min=-Y_HALF_UM, y_max=0.0,
151
+ ),
152
+ ),
153
+ Structure2D(
154
+ material=silicon,
155
+ geometry=Rectangle(
156
+ x_min=-width / 2, x_max=width / 2,
157
+ y_min=0.0, y_max=SI_HEIGHT_UM,
158
+ ),
159
+ ),
160
+ ],
161
+ mesh=mesh,
162
+ env=env,
163
+ )
164
+
165
+ molten = [
166
+ compute_modes_tidy3d(_strip(width), num_modes=num_modes)
167
+ for width in widths
168
+ ]
169
+ s_dense, port_map = compute_s_matrix(
170
+ molten, cell_lengths=[cell_length_um] * len(widths)
171
+ )
172
+ matrix = np.asarray(s_dense, dtype=complex)
173
+ i_in = port_map["left@0"]
174
+ i_out = port_map["right@0"]
175
+ s21 = matrix[i_out, i_in]
176
+ s11 = matrix[i_in, i_in]
177
+ return {
178
+ "meow_version": meow_version(),
179
+ "tidy3d_version": tidy3d_version(),
180
+ "wl_um": wl_um,
181
+ "widths_um": widths,
182
+ "cell_length_um": cell_length_um,
183
+ "num_modes": num_modes,
184
+ "neff_re": [float(np.real(mode.neff)) for mode in molten[0]],
185
+ "neff_im": [float(np.imag(mode.neff)) for mode in molten[0]],
186
+ "transmission": float(abs(s21) ** 2),
187
+ "reflection": float(abs(s11) ** 2),
188
+ "s21_phase_rad": float(np.angle(s21)),
189
+ "ports": dict(port_map),
190
+ }
@@ -0,0 +1,227 @@
1
+ """Staged meow EME driver: job JSON in, framed result envelope on stdout.
2
+
3
+ The adapter copies this file into the run workdir next to
4
+ `meow_job.json` and executes `python meow_driver.py meow_job.json`.
5
+ The envelope is framed between ROCKETEDA-ENVELOPE-BEGIN/END marker
6
+ lines; solver warnings may share stdout — the adapter extracts the
7
+ frame and logs the rest, never fatal.
8
+
9
+ Cross-sections are built from the project spec (`doc` widths_um /
10
+ wl_um / cell_length_um): nothing is hardcoded — widths, wavelength,
11
+ cell length, and both mode counts ride the job. Each device solves
12
+ twice (base then refined modes) so the envelope always carries
13
+ two-resolution convergence evidence. The mesh, materials, and strip
14
+ stack mirror the MEOW-LIVE smoke exactly (500x220nm Si on SiO2,
15
+ 2.0um half-window, 61x41 grid). Mode solving uses meow's tidy3d FDE
16
+ backend — the local CPU eigensolver proven in the smoke (no
17
+ `tidy3d.web` import anywhere on this path). FDTD/FEM stay out:
18
+ proposed, not proven.
19
+
20
+ Envelope: `{"ok": true, "meow_version, "tidy3d_version, "result":
21
+ {wl_um, widths_um[], cell_length_um, base, refined, convergence,
22
+ ports}}` or `{"ok": false, "error": {code, message, token,
23
+ line}}`. Exit 0 on sim success, 2 on a malformed device, 3 when
24
+ meow is not importable. Anything else explodes with a traceback
25
+ and exit 1, which parse() reports as runner_unavailable rather
26
+ than mistaking a tool crash for physics.
27
+ """
28
+
29
+ import json
30
+ import re
31
+ import sys
32
+
33
+ _QUOTED_RE = re.compile(r"""['\"]([^'\"]+)['\"]""")
34
+ _TOKEN_KEYS = ("widths_um", "wl_um", "cell_length_um", "modes", "device")
35
+
36
+ X_HALF_UM = 2.0
37
+ Y_HALF_UM = 2.0
38
+ NX = 61
39
+ NY = 41
40
+ SI_HEIGHT_UM = 0.22
41
+
42
+ _ENVELOPE_BEGIN = "ROCKETEDA-ENVELOPE-BEGIN"
43
+ _ENVELOPE_END = "ROCKETEDA-ENVELOPE-END"
44
+
45
+
46
+ def _token(doc, message):
47
+ names = list(_TOKEN_KEYS) + [str(doc.get("device") or "")]
48
+ for name in names:
49
+ if name and re.search(r"\b" + re.escape(name) + r"\b", message):
50
+ return name
51
+ for match in _QUOTED_RE.finditer(message):
52
+ return match.group(1)
53
+ return None
54
+
55
+
56
+ def _find_line(text, token):
57
+ if not token:
58
+ return None
59
+ for number, line in enumerate(text.splitlines(), 1):
60
+ if token in line:
61
+ return number
62
+ return None
63
+
64
+
65
+ def _emit(payload):
66
+ """Print one marker-framed JSON envelope on stdout.
67
+
68
+ Marker literals mirror src/adapters/meow/_envelope.py — this
69
+ file stages standalone, so change both or neither.
70
+ """
71
+ print(_ENVELOPE_BEGIN)
72
+ print(json.dumps(payload))
73
+ print(_ENVELOPE_END)
74
+
75
+
76
+ def _fail(code, message, token, line):
77
+ _emit(
78
+ {
79
+ "ok": False,
80
+ "error": {
81
+ "code": code,
82
+ "message": message,
83
+ "token": token,
84
+ "line": line,
85
+ },
86
+ }
87
+ )
88
+
89
+
90
+ def main(argv):
91
+ """Run one staged meow EME solve: job JSON in, envelope on stdout.
92
+
93
+ Args:
94
+ argv: Argument vector; argv[1] is the staged job JSON path.
95
+
96
+ Returns:
97
+ Process exit code: 0 on sim success, 2 on a malformed
98
+ device, 3 when meow is not importable.
99
+
100
+ Raises:
101
+ ValueError: Bad device geometry (raised and caught inside
102
+ main into the invalid_device envelope — never escapes).
103
+ """
104
+ with open(argv[1], encoding="utf-8") as handle:
105
+ job = json.load(handle)
106
+ text = job.get("text", "")
107
+ doc = job["doc"]
108
+ try:
109
+ import numpy as np
110
+
111
+ import meow
112
+ from meow import MATERIALS, CrossSection, Environment, Mesh2D, Rectangle
113
+ from meow.eme import compute_s_matrix
114
+ from meow.fde.tidy3d import compute_modes_tidy3d
115
+ from meow.structures import Structure2D
116
+ except ImportError as exc:
117
+ _fail(
118
+ "missing_dependency",
119
+ f"meow is not importable: {exc} "
120
+ "(uv sync).",
121
+ None,
122
+ None,
123
+ )
124
+ return 3
125
+ try:
126
+ widths = [float(w) for w in doc["widths_um"]]
127
+ wl_um = float(doc["wl_um"])
128
+ cell_um = float(doc["cell_length_um"])
129
+ if not widths or any(not w > 0 for w in widths):
130
+ raise ValueError(f"widths_um must be positive: {widths!r}.")
131
+ if not wl_um > 0 or not cell_um > 0:
132
+ raise ValueError("wl_um/cell_length_um must be positive.")
133
+ env = Environment(wl=wl_um)
134
+ mesh = Mesh2D(
135
+ x=np.linspace(-X_HALF_UM, X_HALF_UM, NX),
136
+ y=np.linspace(-Y_HALF_UM, Y_HALF_UM, NY),
137
+ )
138
+ silicon = MATERIALS["silicon"]
139
+ oxide = MATERIALS["silicon_oxide"]
140
+
141
+ def _strip(width):
142
+ return CrossSection(
143
+ structures=[
144
+ Structure2D(
145
+ material=oxide,
146
+ geometry=Rectangle(
147
+ x_min=-X_HALF_UM, x_max=X_HALF_UM,
148
+ y_min=-Y_HALF_UM, y_max=0.0,
149
+ ),
150
+ ),
151
+ Structure2D(
152
+ material=silicon,
153
+ geometry=Rectangle(
154
+ x_min=-width / 2, x_max=width / 2,
155
+ y_min=0.0, y_max=SI_HEIGHT_UM,
156
+ ),
157
+ ),
158
+ ],
159
+ mesh=mesh,
160
+ env=env,
161
+ )
162
+
163
+ def _solve(num_modes):
164
+ molten = [
165
+ compute_modes_tidy3d(_strip(width), num_modes=num_modes)
166
+ for width in widths
167
+ ]
168
+ dense, port_map = compute_s_matrix(
169
+ molten, cell_lengths=[cell_um] * len(widths)
170
+ )
171
+ matrix = np.asarray(dense, dtype=complex)
172
+ s21 = matrix[port_map["right@0"], port_map["left@0"]]
173
+ s11 = matrix[port_map["left@0"], port_map["left@0"]]
174
+ return {
175
+ "num_modes": num_modes,
176
+ "neff_re": [float(np.real(m.neff)) for m in molten[0]],
177
+ "neff_im": [float(np.imag(m.neff)) for m in molten[0]],
178
+ "transmission": float(abs(s21) ** 2),
179
+ "reflection": float(abs(s11) ** 2),
180
+ "s21_phase_rad": float(np.angle(s21)),
181
+ }, dict(port_map)
182
+
183
+ modes = doc["modes"]
184
+ base, ports = _solve(int(modes["base"]))
185
+ refined, _ = _solve(int(modes["refined"]))
186
+ except (ValueError, KeyError, TypeError) as exc:
187
+ message = f"{type(exc).__name__}: {exc}"
188
+ token = _token(doc, message)
189
+ _fail("invalid_device", message, token, _find_line(text, token))
190
+ return 2
191
+ try:
192
+ from importlib import metadata as _metadata
193
+
194
+ tidy3d_version = _metadata.version("tidy3d")
195
+ except Exception:
196
+ tidy3d_version = "unknown"
197
+ _emit(
198
+ {
199
+ "ok": True,
200
+ "meow_version": getattr(meow, "__version__", "unknown"),
201
+ "tidy3d_version": tidy3d_version,
202
+ "result": {
203
+ "wl_um": wl_um,
204
+ "widths_um": widths,
205
+ "cell_length_um": cell_um,
206
+ "base": base,
207
+ "refined": refined,
208
+ "convergence": {
209
+ "dT": abs(
210
+ refined["transmission"] - base["transmission"]
211
+ ),
212
+ "dR": abs(
213
+ refined["reflection"] - base["reflection"]
214
+ ),
215
+ "dneff": abs(
216
+ refined["neff_re"][0] - base["neff_re"][0]
217
+ ),
218
+ },
219
+ "ports": ports,
220
+ },
221
+ }
222
+ )
223
+ return 0
224
+
225
+
226
+ if __name__ == "__main__":
227
+ sys.exit(main(sys.argv))
@@ -0,0 +1,108 @@
1
+ """Marker-delimited stdout envelope extraction for the meow sim driver.
2
+
3
+ The driver contract used to be "exactly one JSON document on
4
+ stdout", but solver libraries print warnings there anyway: tidy3d
5
+ logs its unwritable-config warning to stdout on import, which
6
+ turned healthy solves into `runner_unavailable` (MEOW-STDOUT).
7
+ The driver now frames its envelope between BEGIN/END marker
8
+ lines; this module finds that frame and falls back to a tolerant
9
+ JSON scan for unmarked streams (older staged drivers, canned
10
+ test envelopes). Anything outside the envelope is solver
11
+ chatter: it is logged at warning level and never fatal.
12
+
13
+ Marker contract: the literal marker strings below are duplicated
14
+ in `src/adapters/meow/_driver.py`, which is staged standalone
15
+ into run workdirs and cannot import this module. Change both or
16
+ neither.
17
+ """
18
+
19
+ import json
20
+ import logging
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+ BEGIN_MARKER = "ROCKETEDA-ENVELOPE-BEGIN"
25
+ END_MARKER = "ROCKETEDA-ENVELOPE-END"
26
+ _CHATTER_PREVIEW = 500
27
+
28
+
29
+ def _log_chatter(chatter):
30
+ """Log solver chatter at warning level, truncated, never fatal."""
31
+ text = (chatter or "").strip()
32
+ if text:
33
+ _LOGGER.warning(
34
+ "meow driver stdout chatter (%d bytes, never fatal): %s",
35
+ len(text),
36
+ text[:_CHATTER_PREVIEW],
37
+ )
38
+
39
+
40
+ def _scan_candidates(stdout):
41
+ """[(value, start, end)] for every top-level JSON value in order."""
42
+ decoder = json.JSONDecoder()
43
+ found = []
44
+ index = 0
45
+ while index < len(stdout):
46
+ if stdout[index] not in "{[":
47
+ index += 1
48
+ continue
49
+ try:
50
+ value, end = decoder.raw_decode(stdout, index)
51
+ except ValueError:
52
+ index += 1
53
+ continue
54
+ found.append((value, index, end))
55
+ index = end if end > index else index + 1
56
+ return found
57
+
58
+
59
+ def extract_envelope(stdout):
60
+ """Extract the driver's JSON envelope from possibly noisy stdout.
61
+
62
+ Clean streams parse exactly as before; marked streams use the
63
+ BEGIN/END frame authoritatively (a decoy JSON object in the
64
+ chatter never wins); unmarked noisy streams fall back to a
65
+ tolerant scan preferring the envelope-shaped value. Solver
66
+ chatter is logged at warning level and never fatal.
67
+
68
+ Args:
69
+ stdout: Raw driver stdout, possibly with solver warnings
70
+ or banners around the JSON envelope.
71
+
72
+ Returns:
73
+ Parsed envelope value (the adapter validates its shape),
74
+ or None when no envelope is present.
75
+
76
+ Caution: A corrupt marked frame returns None rather than
77
+ scanning for a substitute — the frame is authoritative,
78
+ so garbage inside it is a corrupt envelope, not chatter.
79
+ """
80
+ if not isinstance(stdout, str):
81
+ return None
82
+ try:
83
+ return json.loads(stdout)
84
+ except ValueError:
85
+ pass
86
+ begin = stdout.find(BEGIN_MARKER)
87
+ if begin >= 0:
88
+ end = stdout.find(END_MARKER, begin + len(BEGIN_MARKER))
89
+ if end > begin:
90
+ frame = stdout[begin + len(BEGIN_MARKER):end]
91
+ try:
92
+ value = json.loads(frame.strip())
93
+ except ValueError:
94
+ _log_chatter(stdout)
95
+ return None
96
+ _log_chatter(stdout[:begin] + stdout[end + len(END_MARKER):])
97
+ return value
98
+ candidates = _scan_candidates(stdout)
99
+ if not candidates:
100
+ _log_chatter(stdout)
101
+ return None
102
+ for value, start, end in reversed(candidates):
103
+ if isinstance(value, dict) and "ok" in value:
104
+ _log_chatter(stdout[:start] + stdout[end:])
105
+ return value
106
+ value, start, end = candidates[-1]
107
+ _log_chatter(stdout[:start] + stdout[end:])
108
+ return value