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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- 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
|