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,115 @@
|
|
|
1
|
+
"""Scoped reference resolution and connection checks (I02).
|
|
2
|
+
|
|
3
|
+
Resolution assumes a validated store: files here were accepted by
|
|
4
|
+
validate_document at ingestion. The scan therefore uses a plain safe
|
|
5
|
+
load and skips anything outside the root or unparseable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ruamel.yaml import YAML
|
|
11
|
+
|
|
12
|
+
from .documents import validate_document
|
|
13
|
+
from .errors import ContractError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _matches(doc, ref):
|
|
17
|
+
return (
|
|
18
|
+
isinstance(doc, dict)
|
|
19
|
+
and doc.get("scope") == ref.get("scope")
|
|
20
|
+
and doc.get("id") == ref.get("id")
|
|
21
|
+
and doc.get("revision") == ref.get("revision")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _scan(root):
|
|
26
|
+
yaml = YAML(typ="safe")
|
|
27
|
+
for path in sorted(root.rglob("*.yaml")):
|
|
28
|
+
try:
|
|
29
|
+
path.resolve().relative_to(root)
|
|
30
|
+
except ValueError:
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
doc = yaml.load(path.read_text())
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
if isinstance(doc, dict):
|
|
37
|
+
yield doc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_reference(ref, root, path=None):
|
|
41
|
+
"""Resolve an exact {scope, id, revision} reference inside root.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
ref: Reference mapping with scope, id, and revision.
|
|
45
|
+
root: Store root the resolution stays inside.
|
|
46
|
+
path: Optional direct path under root; scans the store
|
|
47
|
+
when omitted.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Matching validated record mapping.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ContractError: invalid_reference for an escape, a mismatch
|
|
54
|
+
at path, or an unknown reference; invalid_yaml when a
|
|
55
|
+
direct path cannot load.
|
|
56
|
+
"""
|
|
57
|
+
root = Path(root).resolve()
|
|
58
|
+
if path is not None:
|
|
59
|
+
target = (root / path).resolve()
|
|
60
|
+
try:
|
|
61
|
+
target.relative_to(root)
|
|
62
|
+
except ValueError:
|
|
63
|
+
raise ContractError(
|
|
64
|
+
"invalid_reference", f"Reference escapes store root: {path}."
|
|
65
|
+
) from None
|
|
66
|
+
try:
|
|
67
|
+
doc = YAML(typ="safe").load(target.read_text())
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
raise ContractError("invalid_yaml", f"Cannot load {path}: {exc}") from exc
|
|
70
|
+
if _matches(doc, ref):
|
|
71
|
+
return doc
|
|
72
|
+
raise ContractError("invalid_reference", f"No match at path: {path}.")
|
|
73
|
+
for doc in _scan(root):
|
|
74
|
+
if _matches(doc, ref):
|
|
75
|
+
return doc
|
|
76
|
+
raise ContractError(
|
|
77
|
+
"invalid_reference",
|
|
78
|
+
f"Unknown reference: {ref.get('scope')}/{ref.get('id')}@{ref.get('revision')}.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_connection(text, endpoint_a, endpoint_b):
|
|
83
|
+
"""Check two endpoints for unit/port compatibility.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
text: Validated-record YAML text holding the port table.
|
|
87
|
+
endpoint_a: First (instance, port) endpoint.
|
|
88
|
+
endpoint_b: Second (instance, port) endpoint.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Compatibility mapping (compatible True).
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ContractError: invalid_yaml for a bad record,
|
|
95
|
+
invalid_reference for an unknown port,
|
|
96
|
+
incompatible_port for mismatched wavelength units.
|
|
97
|
+
"""
|
|
98
|
+
data = validate_document(text)
|
|
99
|
+
ports = data.get("ports", {})
|
|
100
|
+
|
|
101
|
+
def _wavelength_unit(endpoint):
|
|
102
|
+
_, port = endpoint
|
|
103
|
+
if port not in ports:
|
|
104
|
+
raise ContractError("invalid_reference", f"Unknown port: {port}.")
|
|
105
|
+
props = ports[port].get("properties", {})
|
|
106
|
+
return props.get("wavelength", {}).get("unit")
|
|
107
|
+
|
|
108
|
+
unit_a = _wavelength_unit(endpoint_a)
|
|
109
|
+
unit_b = _wavelength_unit(endpoint_b)
|
|
110
|
+
if unit_a and unit_b and unit_a != unit_b:
|
|
111
|
+
raise ContractError(
|
|
112
|
+
"incompatible_port",
|
|
113
|
+
f"Incompatible wavelength units: {unit_a} vs {unit_b}.",
|
|
114
|
+
)
|
|
115
|
+
return {"compatible": True}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""I28 ingredient-closure pins for Wave-3 editor and testbench verbs.
|
|
2
|
+
|
|
3
|
+
Pure helpers (no I/O, no logic beyond digests): every Wave-3
|
|
4
|
+
mutation receipt carries an ``i28`` closure pinning the consumed
|
|
5
|
+
revisions (entities, templates, profiles) so a replay can prove
|
|
6
|
+
drift instead of assuming it. Stored digests are canonical-JSON
|
|
7
|
+
sha1, matching the ledger content-key discipline.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def digest_mapping(mapping):
|
|
16
|
+
"""Digest one JSON-shaped mapping for an I28 pin.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
mapping: JSON-shaped mapping to digest.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Twelve hex characters of canonical-JSON sha1.
|
|
23
|
+
"""
|
|
24
|
+
text = json.dumps(mapping, sort_keys=True, default=str)
|
|
25
|
+
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:12]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pin_entities(head, revisions):
|
|
29
|
+
"""Pin consumed entity revisions at one head revision.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
head: Ledger head revision the pin rides on.
|
|
33
|
+
revisions: Entity id to live entity revision mapping.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Pin with head revision plus the sorted entity map.
|
|
37
|
+
"""
|
|
38
|
+
return {
|
|
39
|
+
"revision": head,
|
|
40
|
+
"entities": {key: revisions[key] for key in sorted(revisions)},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pin_template(provenance):
|
|
45
|
+
"""Pin one template provenance into the closure.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
provenance: Template provenance mapping (kind,
|
|
49
|
+
template_version, digest).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Deep copy of the provenance plus its digest pin.
|
|
53
|
+
"""
|
|
54
|
+
kept = copy.deepcopy(dict(provenance or {}))
|
|
55
|
+
kept["digest"] = kept.get("digest") or digest_mapping(kept)
|
|
56
|
+
return kept
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def pin_closure(head, entities=None, template=None, profile=None):
|
|
60
|
+
"""Assemble one I28 closure pin for a Wave-3 receipt.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
head: Ledger head revision the pin rides on.
|
|
64
|
+
entities: Entity id to live revision mapping, if any.
|
|
65
|
+
template: Template provenance mapping, if consumed.
|
|
66
|
+
profile: Technology profile pin ({name, version}), if
|
|
67
|
+
consumed.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Closure pin with revision, entity pins, template pin,
|
|
71
|
+
and profile pin (each present only when consumed).
|
|
72
|
+
"""
|
|
73
|
+
pin = {"revision": head}
|
|
74
|
+
if entities:
|
|
75
|
+
pin["entities"] = {
|
|
76
|
+
key: entities[key] for key in sorted(entities)
|
|
77
|
+
}
|
|
78
|
+
if template is not None:
|
|
79
|
+
pin["template"] = pin_template(template)
|
|
80
|
+
if profile is not None:
|
|
81
|
+
pin["profile"] = copy.deepcopy(dict(profile))
|
|
82
|
+
pin["digest"] = digest_mapping(
|
|
83
|
+
{key: pin[key] for key in sorted(pin) if key != "digest"}
|
|
84
|
+
)
|
|
85
|
+
return pin
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Explicit run.request params schema (RUN-PARAMS).
|
|
2
|
+
|
|
3
|
+
Runs are explicit, always — no live-update path — so every knob a
|
|
4
|
+
run needs travels IN the run request, frozen into the run snapshot
|
|
5
|
+
like everything else. xschem keeps netlist knobs as live TCL vars;
|
|
6
|
+
the web split cannot do that without per-event round-trips, so they
|
|
7
|
+
become explicit `request` parameters: netlist type, flat/split
|
|
8
|
+
shape, the LVS-netlist flag, top-is-subckt, deck opts
|
|
9
|
+
passthrough, and the solver budget (null keeps the adapter
|
|
10
|
+
default). Without this schema run reproducibility has a hole:
|
|
11
|
+
two identical-looking runs could differ by a knob nobody recorded.
|
|
12
|
+
|
|
13
|
+
Validated at submit (`Engine.submit` and `Engine.run` normalize
|
|
14
|
+
`arguments["request"]` through `validate_run_request` before the
|
|
15
|
+
command text is frozen), replayed byte-identically through
|
|
16
|
+
`freeze_run_request`, and read back through `parse_run_request`.
|
|
17
|
+
Unknown fields are rejected loudly, never ignored — and every
|
|
18
|
+
caller (IDE, MCP, console) sends the same shape because validation
|
|
19
|
+
lives in the engine, not the transports.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import io
|
|
23
|
+
|
|
24
|
+
from ruamel.yaml import YAML
|
|
25
|
+
|
|
26
|
+
# Mirrors xschem's netlist_type TCL var: the netlist dialect the run
|
|
27
|
+
# netlists in. Canonical order is display order (error text, schema).
|
|
28
|
+
NETLIST_TYPES = ("spice", "spectre", "verilog", "vhdl")
|
|
29
|
+
|
|
30
|
+
# Solver-lane budget bounds, mirroring the python lane so one
|
|
31
|
+
# hanging tool cannot pin a drain worker forever. None means
|
|
32
|
+
# "adapter default" (a spice sim keeps 120s, a LibreLane flow
|
|
33
|
+
# keeps its hour); the engine stamps the job only when set.
|
|
34
|
+
MIN_TIMEOUT_S = 1
|
|
35
|
+
MAX_TIMEOUT_S = 1800
|
|
36
|
+
DEFAULT_TIMEOUT_S = None
|
|
37
|
+
|
|
38
|
+
# Canonical knob order: validation output, frozen text, and the
|
|
39
|
+
# schema all follow this order so replays compare byte-identically.
|
|
40
|
+
RUN_REQUEST_FIELDS = (
|
|
41
|
+
"netlist_type",
|
|
42
|
+
"flat_netlist",
|
|
43
|
+
"lvs_netlist",
|
|
44
|
+
"top_is_subckt",
|
|
45
|
+
"deck_opts",
|
|
46
|
+
"timeout_s",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
RUN_REQUEST_DEFAULTS = {
|
|
50
|
+
"netlist_type": "spice",
|
|
51
|
+
"flat_netlist": False,
|
|
52
|
+
"lvs_netlist": False,
|
|
53
|
+
"top_is_subckt": False,
|
|
54
|
+
"deck_opts": {},
|
|
55
|
+
"timeout_s": DEFAULT_TIMEOUT_S,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_BOOL_FLAGS = ("flat_netlist", "lvs_netlist", "top_is_subckt")
|
|
59
|
+
|
|
60
|
+
# Deck opts pass through to the solver deck untouched: flat scalar
|
|
61
|
+
# options only (a nested mapping would smuggle unvalidated shape
|
|
62
|
+
# into the tool call). None means "present but valueless".
|
|
63
|
+
_SCALAR_TYPES = (str, int, float, bool, type(None))
|
|
64
|
+
|
|
65
|
+
# Machine seam for api_spec/contract regen: the integrator injects
|
|
66
|
+
# this `request` property into solver-op schemas from here, the way
|
|
67
|
+
# expected_revision is injected from ProjectService.submit's arg
|
|
68
|
+
# text. additionalProperties False is the schema half of "unknown
|
|
69
|
+
# fields rejected, never ignored".
|
|
70
|
+
RUN_REQUEST_SCHEMA = {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"description": (
|
|
73
|
+
"Explicit run knobs (xschem live-var equivalents), frozen "
|
|
74
|
+
"into the run snapshot. Unknown fields are rejected."
|
|
75
|
+
),
|
|
76
|
+
"properties": {
|
|
77
|
+
"netlist_type": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
# Literal (not list(NETLIST_TYPES)): api_spec reads this
|
|
80
|
+
# schema via AST without importing; tests pin equality.
|
|
81
|
+
"enum": ["spice", "spectre", "verilog", "vhdl"],
|
|
82
|
+
"default": "spice",
|
|
83
|
+
"description": "Netlist dialect the run netlists in.",
|
|
84
|
+
},
|
|
85
|
+
"flat_netlist": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"default": False,
|
|
88
|
+
"description": "Flatten the hierarchy (else split per cell).",
|
|
89
|
+
},
|
|
90
|
+
"lvs_netlist": {
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"default": False,
|
|
93
|
+
"description": "Netlist for LVS comparison.",
|
|
94
|
+
},
|
|
95
|
+
"top_is_subckt": {
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"default": False,
|
|
98
|
+
"description": "Emit the top cell as a subcircuit.",
|
|
99
|
+
},
|
|
100
|
+
"deck_opts": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"default": {},
|
|
103
|
+
"description": "Solver deck options, flat scalar passthrough.",
|
|
104
|
+
},
|
|
105
|
+
"timeout_s": {
|
|
106
|
+
"type": ["number", "null"],
|
|
107
|
+
"default": None,
|
|
108
|
+
"description": (
|
|
109
|
+
"Solver budget in seconds; null keeps the adapter "
|
|
110
|
+
"default."
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
"additionalProperties": False,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RunParamsError(Exception):
|
|
119
|
+
"""Run.request violation with a stable machine code.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
code: Stable machine-readable error code
|
|
123
|
+
(unknown_run_param, invalid_run_param,
|
|
124
|
+
invalid_run_request).
|
|
125
|
+
message: Human-readable detail naming the field.
|
|
126
|
+
fields: Itemized fields, when the error names any.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, code, message, fields=None):
|
|
130
|
+
super().__init__(message)
|
|
131
|
+
self.code = code
|
|
132
|
+
self.fields = fields or []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _check_deck_opts(opts):
|
|
136
|
+
"""Validate deck opts: string keys, flat scalar values."""
|
|
137
|
+
if not isinstance(opts, dict):
|
|
138
|
+
raise RunParamsError(
|
|
139
|
+
"invalid_run_param",
|
|
140
|
+
f"deck_opts must be a mapping, got {opts!r}.",
|
|
141
|
+
fields=["deck_opts"],
|
|
142
|
+
)
|
|
143
|
+
for key, value in opts.items():
|
|
144
|
+
if not isinstance(key, str) or not key:
|
|
145
|
+
raise RunParamsError(
|
|
146
|
+
"invalid_run_param",
|
|
147
|
+
f"deck_opts keys must be non-empty strings, got {key!r}.",
|
|
148
|
+
fields=["deck_opts"],
|
|
149
|
+
)
|
|
150
|
+
if not isinstance(value, _SCALAR_TYPES):
|
|
151
|
+
raise RunParamsError(
|
|
152
|
+
"invalid_run_param",
|
|
153
|
+
f"deck_opts.{key} must be a scalar, got {value!r}.",
|
|
154
|
+
fields=["deck_opts"],
|
|
155
|
+
)
|
|
156
|
+
return dict(opts)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def validate_run_request(params):
|
|
160
|
+
"""Validate and normalize one run.request mapping.
|
|
161
|
+
|
|
162
|
+
Missing input (None) and `{}` both mean "all defaults": the
|
|
163
|
+
frozen snapshot always shows every knob explicitly, so old
|
|
164
|
+
callers that send no `request` still record what they ran with.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
params: Caller-supplied `request` mapping, or None when
|
|
168
|
+
the caller sent no `request` key.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Normalized mapping with every knob present in canonical
|
|
172
|
+
field order: caller values where given, schema defaults
|
|
173
|
+
elsewhere. A fresh mapping — the caller's dict is never
|
|
174
|
+
mutated.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
RunParamsError: unknown_run_param naming every unknown
|
|
178
|
+
field, invalid_run_param naming the mistyped field
|
|
179
|
+
(netlist_type outside the enum, a non-bool flag, a
|
|
180
|
+
malformed deck_opts), invalid_run_request for a
|
|
181
|
+
non-mapping `request`.
|
|
182
|
+
"""
|
|
183
|
+
if params is None:
|
|
184
|
+
params = {}
|
|
185
|
+
if not isinstance(params, dict):
|
|
186
|
+
raise RunParamsError(
|
|
187
|
+
"invalid_run_request",
|
|
188
|
+
f"request must be a mapping, got {params!r}.",
|
|
189
|
+
)
|
|
190
|
+
unknown = sorted(key for key in params if key not in RUN_REQUEST_FIELDS)
|
|
191
|
+
if unknown:
|
|
192
|
+
raise RunParamsError(
|
|
193
|
+
"unknown_run_param",
|
|
194
|
+
f"Unknown run.request field(s): {', '.join(map(repr, unknown))}; "
|
|
195
|
+
f"known fields: {', '.join(RUN_REQUEST_FIELDS)}.",
|
|
196
|
+
fields=unknown,
|
|
197
|
+
)
|
|
198
|
+
kind = params.get("netlist_type", RUN_REQUEST_DEFAULTS["netlist_type"])
|
|
199
|
+
if kind not in NETLIST_TYPES:
|
|
200
|
+
raise RunParamsError(
|
|
201
|
+
"invalid_run_param",
|
|
202
|
+
f"netlist_type must be one of {', '.join(NETLIST_TYPES)}, "
|
|
203
|
+
f"got {kind!r}.",
|
|
204
|
+
fields=["netlist_type"],
|
|
205
|
+
)
|
|
206
|
+
normalized = {"netlist_type": kind}
|
|
207
|
+
for flag in _BOOL_FLAGS:
|
|
208
|
+
value = params.get(flag, False)
|
|
209
|
+
if not isinstance(value, bool):
|
|
210
|
+
raise RunParamsError(
|
|
211
|
+
"invalid_run_param",
|
|
212
|
+
f"{flag} must be a boolean, got {value!r}.",
|
|
213
|
+
fields=[flag],
|
|
214
|
+
)
|
|
215
|
+
normalized[flag] = value
|
|
216
|
+
normalized["deck_opts"] = _check_deck_opts(
|
|
217
|
+
params.get("deck_opts", RUN_REQUEST_DEFAULTS["deck_opts"])
|
|
218
|
+
)
|
|
219
|
+
normalized["timeout_s"] = _check_timeout(
|
|
220
|
+
params.get("timeout_s", RUN_REQUEST_DEFAULTS["timeout_s"])
|
|
221
|
+
)
|
|
222
|
+
return normalized
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _check_timeout(value):
|
|
226
|
+
"""Validate the solver budget: a positive number in bounds, or None.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
value: Caller-supplied `timeout_s`, or None for the
|
|
230
|
+
adapter default.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The validated budget, unchanged.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
RunParamsError: invalid_run_param for a boolean,
|
|
237
|
+
non-number, or out-of-bounds budget.
|
|
238
|
+
"""
|
|
239
|
+
if value is None:
|
|
240
|
+
return None
|
|
241
|
+
if (
|
|
242
|
+
isinstance(value, bool)
|
|
243
|
+
or not isinstance(value, (int, float))
|
|
244
|
+
or not MIN_TIMEOUT_S <= value <= MAX_TIMEOUT_S
|
|
245
|
+
):
|
|
246
|
+
raise RunParamsError(
|
|
247
|
+
"invalid_run_param",
|
|
248
|
+
f"timeout_s must be {MIN_TIMEOUT_S}..{MAX_TIMEOUT_S} seconds "
|
|
249
|
+
f"or null, got {value!r}.",
|
|
250
|
+
fields=["timeout_s"],
|
|
251
|
+
)
|
|
252
|
+
return value
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def freeze_run_request(validated):
|
|
256
|
+
"""Render validated knobs as canonical inputs.yaml text.
|
|
257
|
+
|
|
258
|
+
Key order is canonical (schema field order, deck opts sorted),
|
|
259
|
+
so the same logical request replays byte-identically whatever
|
|
260
|
+
key order the caller sent.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
validated: Normalized mapping from `validate_run_request`.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Deterministic YAML text (`request:` mapping) for the run
|
|
267
|
+
snapshot: same knobs in, same bytes out.
|
|
268
|
+
"""
|
|
269
|
+
ordered = {field: validated[field] for field in RUN_REQUEST_FIELDS}
|
|
270
|
+
ordered["deck_opts"] = {
|
|
271
|
+
key: ordered["deck_opts"][key] for key in sorted(ordered["deck_opts"])
|
|
272
|
+
}
|
|
273
|
+
buf = io.StringIO()
|
|
274
|
+
YAML(typ="safe").dump({"request": ordered}, buf)
|
|
275
|
+
return buf.getvalue()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def parse_run_request(text):
|
|
279
|
+
"""Read frozen inputs.yaml text back into validated knobs.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
text: YAML text holding a top-level `request:` mapping, as
|
|
283
|
+
rendered by `freeze_run_request`.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Validated mapping (see `validate_run_request`).
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
RunParamsError: invalid_run_request for unparsable text or
|
|
290
|
+
a missing/non-mapping `request`, else whatever
|
|
291
|
+
`validate_run_request` raises for the stored knobs.
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
data = YAML(typ="safe").load(text)
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
raise RunParamsError(
|
|
297
|
+
"invalid_run_request", f"Cannot parse inputs.yaml: {exc}."
|
|
298
|
+
) from exc
|
|
299
|
+
if not isinstance(data, dict) or not isinstance(data.get("request"), dict):
|
|
300
|
+
raise RunParamsError(
|
|
301
|
+
"invalid_run_request",
|
|
302
|
+
"inputs.yaml must hold a top-level request mapping.",
|
|
303
|
+
)
|
|
304
|
+
return validate_run_request(data["request"])
|