sutra-dev 0.2.0__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.
- sutra_compiler/__init__.py +49 -0
- sutra_compiler/__main__.py +514 -0
- sutra_compiler/ast_nodes.py +553 -0
- sutra_compiler/codegen.py +1811 -0
- sutra_compiler/codegen_base.py +2436 -0
- sutra_compiler/codegen_pytorch.py +1472 -0
- sutra_compiler/diagnostics.py +145 -0
- sutra_compiler/inliner.py +581 -0
- sutra_compiler/lexer.py +821 -0
- sutra_compiler/parser.py +2112 -0
- sutra_compiler/review.py +322 -0
- sutra_compiler/simplify.py +1046 -0
- sutra_compiler/simplify_egglog.py +674 -0
- sutra_compiler/stdlib/axons.su +53 -0
- sutra_compiler/stdlib/embed.su +48 -0
- sutra_compiler/stdlib/javascript_object.su +18 -0
- sutra_compiler/stdlib/logic.su +202 -0
- sutra_compiler/stdlib/math.su +12 -0
- sutra_compiler/stdlib/memory.su +82 -0
- sutra_compiler/stdlib/numbers.su +99 -0
- sutra_compiler/stdlib/rotation.su +83 -0
- sutra_compiler/stdlib/similarity.su +97 -0
- sutra_compiler/stdlib/strings.su +56 -0
- sutra_compiler/stdlib/tensor.su +82 -0
- sutra_compiler/stdlib/vectors.su +119 -0
- sutra_compiler/stdlib_loader.py +219 -0
- sutra_compiler/sutradb_embedded.py +273 -0
- sutra_compiler/trace.py +135 -0
- sutra_compiler/validator.py +552 -0
- sutra_compiler/workspace.py +655 -0
- sutra_dev-0.2.0.dist-info/METADATA +80 -0
- sutra_dev-0.2.0.dist-info/RECORD +36 -0
- sutra_dev-0.2.0.dist-info/WHEEL +5 -0
- sutra_dev-0.2.0.dist-info/entry_points.txt +2 -0
- sutra_dev-0.2.0.dist-info/licenses/LICENSE +201 -0
- sutra_dev-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Embedded SutraDB wrapper — ctypes binding to sutra_ffi.dll.
|
|
2
|
+
|
|
3
|
+
Thin Python class around the SutraDB FFI: opens a `.sdb` file,
|
|
4
|
+
inserts labeled embeddings, and runs nearest-neighbor queries via
|
|
5
|
+
SPARQL+'s `VECTOR_SIMILAR` operator.
|
|
6
|
+
|
|
7
|
+
This module proves the embedding path works standalone. The Sutra
|
|
8
|
+
runtime does not yet route `argmax_cosine` through it; this file
|
|
9
|
+
gives codegen something to call once that wiring lands.
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
SutraDB is a Rust HNSW + RDF triplestore (`sutraDB/` in this repo).
|
|
14
|
+
Its C-ABI shared library (`sutra_ffi.dll` from `sutra-ffi` crate)
|
|
15
|
+
exposes opaque database handles + SPARQL execution. We `ctypes`-load
|
|
16
|
+
that library, open a database (in-memory `:memory:` or a `.sdb`
|
|
17
|
+
file), insert N-triples that bind labels to vector literals, and
|
|
18
|
+
query via SPARQL+ for nearest-neighbor lookup.
|
|
19
|
+
|
|
20
|
+
The "embedded" claim is real: no separate process, no HTTP, no
|
|
21
|
+
daemon. The compiled Sutra program loads the .dll at module init
|
|
22
|
+
and uses the database in-process.
|
|
23
|
+
|
|
24
|
+
## Vector encoding
|
|
25
|
+
|
|
26
|
+
SutraDB stores vectors as object literals typed `<http://sutra.dev/
|
|
27
|
+
f32vec>` with whitespace-separated floats:
|
|
28
|
+
|
|
29
|
+
<urn:label:cat> <urn:embedding> "0.23 -0.11 0.87 ..."^^<http://sutra.dev/f32vec> .
|
|
30
|
+
|
|
31
|
+
The FFI's `sutra_db_open` rebuilds HNSW indexes from any vector
|
|
32
|
+
triples in the existing store; the FFI's `sutra_insert_ntriples`
|
|
33
|
+
adds new vectors and updates the index.
|
|
34
|
+
|
|
35
|
+
## Querying
|
|
36
|
+
|
|
37
|
+
SPARQL+ exposes `VECTOR_SIMILAR(?s :pred ?vec, threshold)`. Today's
|
|
38
|
+
nearest-neighbor query for our use case:
|
|
39
|
+
|
|
40
|
+
SELECT ?s WHERE {
|
|
41
|
+
?s <urn:embedding> ?v .
|
|
42
|
+
VECTOR_SIMILAR(?s <urn:embedding> "<query_vec>"^^<http://sutra.dev/f32vec>, 0.0)
|
|
43
|
+
}
|
|
44
|
+
ORDER BY DESC(VECTOR_SCORE(?s <urn:embedding> "<query_vec>"^^<http://sutra.dev/f32vec>))
|
|
45
|
+
LIMIT 1
|
|
46
|
+
|
|
47
|
+
The threshold of 0.0 means "any cosine"; ORDER BY DESC + LIMIT 1
|
|
48
|
+
picks the closest. For k-NN, set LIMIT k.
|
|
49
|
+
|
|
50
|
+
## Known limitations
|
|
51
|
+
|
|
52
|
+
- SPARQL string concatenation per query is wasteful; for a hot
|
|
53
|
+
argmax_cosine loop this needs cached PreparedQuery shape (not yet
|
|
54
|
+
exposed by FFI).
|
|
55
|
+
- Vector literal encoding (whitespace-separated floats) round-trips
|
|
56
|
+
through string formatting on every insert. For large codebooks at
|
|
57
|
+
compile time this is fine; for runtime hot-path it isn't.
|
|
58
|
+
- The DLL load path assumes the build artifact is at
|
|
59
|
+
`sutraDB/target/release/sutra_ffi.dll` relative to the repo root.
|
|
60
|
+
Production deployment needs a packaging story (TODO).
|
|
61
|
+
"""
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import ctypes
|
|
65
|
+
import os
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
from typing import Optional, Sequence
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
71
|
+
# DLL discovery
|
|
72
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _default_dll_path() -> Path:
|
|
76
|
+
"""Locate sutra_ffi.dll relative to this file's repo root.
|
|
77
|
+
|
|
78
|
+
Override via env var SUTRA_FFI_DLL.
|
|
79
|
+
"""
|
|
80
|
+
env = os.environ.get("SUTRA_FFI_DLL")
|
|
81
|
+
if env:
|
|
82
|
+
return Path(env)
|
|
83
|
+
# this file is at sdk/sutra-compiler/sutra_compiler/sutradb_embedded.py;
|
|
84
|
+
# repo root is three parents up.
|
|
85
|
+
repo_root = Path(__file__).resolve().parents[3]
|
|
86
|
+
return repo_root / "sutraDB" / "target" / "release" / "sutra_ffi.dll"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
90
|
+
# ctypes signature setup
|
|
91
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _bind_sutra_ffi(dll_path: Path) -> ctypes.CDLL:
|
|
95
|
+
"""Load the sutra_ffi DLL and bind argument/return types."""
|
|
96
|
+
if not dll_path.exists():
|
|
97
|
+
raise FileNotFoundError(
|
|
98
|
+
f"sutra_ffi.dll not found at {dll_path}. Build with:\n"
|
|
99
|
+
f" cd {dll_path.parents[2]} && cargo build --release -p sutra-ffi\n"
|
|
100
|
+
f"or set SUTRA_FFI_DLL to override the path."
|
|
101
|
+
)
|
|
102
|
+
lib = ctypes.CDLL(str(dll_path))
|
|
103
|
+
|
|
104
|
+
# ── Lifecycle
|
|
105
|
+
lib.sutra_db_open.argtypes = [ctypes.c_char_p]
|
|
106
|
+
lib.sutra_db_open.restype = ctypes.c_void_p
|
|
107
|
+
lib.sutra_db_close.argtypes = [ctypes.c_void_p]
|
|
108
|
+
lib.sutra_db_close.restype = None
|
|
109
|
+
|
|
110
|
+
# ── Errors
|
|
111
|
+
lib.sutra_last_error.argtypes = []
|
|
112
|
+
lib.sutra_last_error.restype = ctypes.c_char_p
|
|
113
|
+
|
|
114
|
+
# ── Triples
|
|
115
|
+
lib.sutra_insert_ntriples.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
116
|
+
lib.sutra_insert_ntriples.restype = ctypes.c_int64
|
|
117
|
+
|
|
118
|
+
# ── Query
|
|
119
|
+
lib.sutra_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
120
|
+
lib.sutra_query.restype = ctypes.c_void_p
|
|
121
|
+
|
|
122
|
+
lib.sutra_result_column_count.argtypes = [ctypes.c_void_p]
|
|
123
|
+
lib.sutra_result_column_count.restype = ctypes.c_uint32
|
|
124
|
+
|
|
125
|
+
lib.sutra_result_row_count.argtypes = [ctypes.c_void_p]
|
|
126
|
+
lib.sutra_result_row_count.restype = ctypes.c_uint64
|
|
127
|
+
|
|
128
|
+
lib.sutra_result_value.argtypes = [
|
|
129
|
+
ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint32,
|
|
130
|
+
]
|
|
131
|
+
lib.sutra_result_value.restype = ctypes.c_void_p
|
|
132
|
+
|
|
133
|
+
lib.sutra_result_free.argtypes = [ctypes.c_void_p]
|
|
134
|
+
lib.sutra_result_free.restype = None
|
|
135
|
+
|
|
136
|
+
lib.sutra_string_free.argtypes = [ctypes.c_void_p]
|
|
137
|
+
lib.sutra_string_free.restype = None
|
|
138
|
+
|
|
139
|
+
return lib
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
143
|
+
# High-level wrapper
|
|
144
|
+
# ────────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
VECTOR_PRED = "<urn:sutra:embedding>"
|
|
148
|
+
F32VEC_TYPE = "<http://sutra.dev/f32vec>"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SutraDBEmbedded:
|
|
152
|
+
"""In-process SutraDB for vector lookup.
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
db = SutraDBEmbedded(":memory:")
|
|
156
|
+
db.add("cat", [0.23, -0.11, 0.87, ...])
|
|
157
|
+
db.add("dog", [0.31, -0.05, 0.42, ...])
|
|
158
|
+
label = db.nearest([0.25, -0.10, 0.85, ...]) # -> "cat"
|
|
159
|
+
db.close()
|
|
160
|
+
|
|
161
|
+
Reads from an existing `.sdb` file rebuild the HNSW index on open
|
|
162
|
+
so subsequent queries are fast.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
path: str = ":memory:",
|
|
168
|
+
dll_path: Optional[Path] = None,
|
|
169
|
+
) -> None:
|
|
170
|
+
self._lib = _bind_sutra_ffi(dll_path or _default_dll_path())
|
|
171
|
+
self._path = path
|
|
172
|
+
c_path = path.encode("utf-8")
|
|
173
|
+
self._db = self._lib.sutra_db_open(c_path)
|
|
174
|
+
if not self._db:
|
|
175
|
+
err = self._last_error()
|
|
176
|
+
raise RuntimeError(f"sutra_db_open failed: {err}")
|
|
177
|
+
self._closed = False
|
|
178
|
+
|
|
179
|
+
def _last_error(self) -> str:
|
|
180
|
+
ptr = self._lib.sutra_last_error()
|
|
181
|
+
if not ptr:
|
|
182
|
+
return "(no error message)"
|
|
183
|
+
return ctypes.c_char_p(ptr).value.decode("utf-8", errors="replace")
|
|
184
|
+
|
|
185
|
+
def add(self, label: str, vec: Sequence[float]) -> None:
|
|
186
|
+
"""Insert one (label, vector) pair as a triple.
|
|
187
|
+
|
|
188
|
+
Each label gets a unique IRI `<urn:sutra:label:LABEL>` (must be
|
|
189
|
+
URL-safe — the wrapper does not validate). The vector is stored
|
|
190
|
+
as the object of a triple with predicate `<urn:sutra:embedding>`,
|
|
191
|
+
typed `<http://sutra.dev/f32vec>`.
|
|
192
|
+
|
|
193
|
+
As of FFI build 2026-04-30 (queue item 2 piece 6),
|
|
194
|
+
`sutra_insert_ntriples` auto-declares vector predicates and
|
|
195
|
+
adds the vector to the HNSW index inline — no close+reopen
|
|
196
|
+
needed. The FFI rebuild on open still works; this just removes
|
|
197
|
+
the slow path for fresh inserts.
|
|
198
|
+
"""
|
|
199
|
+
if self._closed:
|
|
200
|
+
raise RuntimeError("SutraDBEmbedded already closed")
|
|
201
|
+
# Encode the vector literal: whitespace-separated floats.
|
|
202
|
+
vec_lit = " ".join(f"{float(x):.8g}" for x in vec)
|
|
203
|
+
# N-triple: <subj> <pred> "<vec>"^^<f32vec> .
|
|
204
|
+
triple = (
|
|
205
|
+
f"<urn:sutra:label:{label}> {VECTOR_PRED} "
|
|
206
|
+
f'"{vec_lit}"^^{F32VEC_TYPE} .\n'
|
|
207
|
+
)
|
|
208
|
+
n = self._lib.sutra_insert_ntriples(self._db, triple.encode("utf-8"))
|
|
209
|
+
if n < 0:
|
|
210
|
+
err = self._last_error()
|
|
211
|
+
raise RuntimeError(f"sutra_insert_ntriples failed: {err}")
|
|
212
|
+
|
|
213
|
+
def nearest(self, query: Sequence[float], k: int = 1) -> list[str]:
|
|
214
|
+
"""Return the `k` nearest labels to `query` by cosine similarity.
|
|
215
|
+
|
|
216
|
+
Result is ordered most-similar first.
|
|
217
|
+
"""
|
|
218
|
+
if self._closed:
|
|
219
|
+
raise RuntimeError("SutraDBEmbedded already closed")
|
|
220
|
+
vec_lit = " ".join(f"{float(x):.8g}" for x in query)
|
|
221
|
+
sparql = (
|
|
222
|
+
f"SELECT ?s WHERE {{ "
|
|
223
|
+
f"?s {VECTOR_PRED} ?v . "
|
|
224
|
+
f'VECTOR_SIMILAR(?s {VECTOR_PRED} "{vec_lit}"^^{F32VEC_TYPE}, 0.0) '
|
|
225
|
+
f"}} "
|
|
226
|
+
f'ORDER BY DESC(VECTOR_SCORE(?s {VECTOR_PRED} "{vec_lit}"^^{F32VEC_TYPE})) '
|
|
227
|
+
f"LIMIT {int(k)}"
|
|
228
|
+
)
|
|
229
|
+
result = self._lib.sutra_query(self._db, sparql.encode("utf-8"))
|
|
230
|
+
if not result:
|
|
231
|
+
err = self._last_error()
|
|
232
|
+
raise RuntimeError(f"sutra_query failed: {err}")
|
|
233
|
+
try:
|
|
234
|
+
n_rows = self._lib.sutra_result_row_count(result)
|
|
235
|
+
labels: list[str] = []
|
|
236
|
+
for i in range(n_rows):
|
|
237
|
+
val_ptr = self._lib.sutra_result_value(result, i, 0)
|
|
238
|
+
if not val_ptr:
|
|
239
|
+
continue
|
|
240
|
+
raw = ctypes.c_char_p(val_ptr).value
|
|
241
|
+
s = raw.decode("utf-8", errors="replace") if raw else ""
|
|
242
|
+
# Strip the IRI wrapping back to bare label. SutraDB
|
|
243
|
+
# returns IRIs without angle brackets in result rows.
|
|
244
|
+
prefix_naked = "urn:sutra:label:"
|
|
245
|
+
prefix_bracketed = "<urn:sutra:label:"
|
|
246
|
+
if s.startswith(prefix_bracketed) and s.endswith(">"):
|
|
247
|
+
s = s[len(prefix_bracketed):-1]
|
|
248
|
+
elif s.startswith(prefix_naked):
|
|
249
|
+
s = s[len(prefix_naked):]
|
|
250
|
+
labels.append(s)
|
|
251
|
+
self._lib.sutra_string_free(val_ptr)
|
|
252
|
+
return labels
|
|
253
|
+
finally:
|
|
254
|
+
self._lib.sutra_result_free(result)
|
|
255
|
+
|
|
256
|
+
def close(self) -> None:
|
|
257
|
+
if self._closed:
|
|
258
|
+
return
|
|
259
|
+
self._lib.sutra_db_close(self._db)
|
|
260
|
+
self._closed = True
|
|
261
|
+
self._db = None # type: ignore[assignment]
|
|
262
|
+
|
|
263
|
+
def __enter__(self) -> "SutraDBEmbedded":
|
|
264
|
+
return self
|
|
265
|
+
|
|
266
|
+
def __exit__(self, *exc: object) -> None:
|
|
267
|
+
self.close()
|
|
268
|
+
|
|
269
|
+
def __del__(self) -> None:
|
|
270
|
+
try:
|
|
271
|
+
self.close()
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
sutra_compiler/trace.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Sutra vector trace — records all vectors and operations during execution.
|
|
2
|
+
|
|
3
|
+
When the compiler runs with --trace, the generated Python includes a
|
|
4
|
+
_SutraTracer that wraps _NumpyVSA and records every vector allocation
|
|
5
|
+
and operation. After execution, call tracer.to_json() to get a
|
|
6
|
+
Three.js-ready trace with PCA-projected 3D positions.
|
|
7
|
+
|
|
8
|
+
The trace format:
|
|
9
|
+
{
|
|
10
|
+
"program": "fuzzy_branching.su",
|
|
11
|
+
"vectors": [
|
|
12
|
+
{"name": "v_hello", "type": "basis", "step": 0, "pos": [x, y, z]},
|
|
13
|
+
...
|
|
14
|
+
],
|
|
15
|
+
"operations": [
|
|
16
|
+
{"type": "bind", "inputs": [0, 1], "output": 2, "step": 2},
|
|
17
|
+
...
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import numpy as np
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_numpy(vec: Any) -> np.ndarray:
|
|
29
|
+
# Pytorch backend hands us torch.Tensor; numpy backend hands ndarray.
|
|
30
|
+
# Normalize at record time so PCA / serialization stay numpy-only.
|
|
31
|
+
if hasattr(vec, "detach"):
|
|
32
|
+
return vec.detach().cpu().numpy().copy()
|
|
33
|
+
return np.asarray(vec).copy()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SutraTracer:
|
|
37
|
+
"""Records vectors and operations for 3D visualization."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, program_name: str = ""):
|
|
40
|
+
self.program_name = program_name
|
|
41
|
+
self._vectors: list[dict] = [] # {name, type, step, raw_vec}
|
|
42
|
+
self._operations: list[dict] = [] # {type, inputs, output, step}
|
|
43
|
+
self._step = 0
|
|
44
|
+
self._vec_index: dict[int, int] = {} # id(input vec) → index
|
|
45
|
+
|
|
46
|
+
def record_vector(self, name: str, vec: Any, vtype: str = "other") -> int:
|
|
47
|
+
"""Record a named vector. Returns its index."""
|
|
48
|
+
idx = len(self._vectors)
|
|
49
|
+
self._vectors.append({
|
|
50
|
+
"name": name,
|
|
51
|
+
"type": vtype,
|
|
52
|
+
"step": self._step,
|
|
53
|
+
"raw": _to_numpy(vec),
|
|
54
|
+
})
|
|
55
|
+
self._vec_index[id(vec)] = idx
|
|
56
|
+
self._step += 1
|
|
57
|
+
return idx
|
|
58
|
+
|
|
59
|
+
def record_op(self, op_type: str, inputs: list[Any],
|
|
60
|
+
output: Any, output_name: str = "") -> int:
|
|
61
|
+
"""Record an operation linking input vectors to an output."""
|
|
62
|
+
out_idx = self.record_vector(output_name or f"_{op_type}_out", output, op_type)
|
|
63
|
+
in_indices = []
|
|
64
|
+
for inv in inputs:
|
|
65
|
+
vid = id(inv)
|
|
66
|
+
if vid in self._vec_index:
|
|
67
|
+
in_indices.append(self._vec_index[vid])
|
|
68
|
+
self._operations.append({
|
|
69
|
+
"type": op_type,
|
|
70
|
+
"inputs": in_indices,
|
|
71
|
+
"output": out_idx,
|
|
72
|
+
"step": self._step - 1,
|
|
73
|
+
})
|
|
74
|
+
return out_idx
|
|
75
|
+
|
|
76
|
+
def _pca_3d(self) -> np.ndarray:
|
|
77
|
+
"""Project all recorded vectors to 3D via PCA."""
|
|
78
|
+
if not self._vectors:
|
|
79
|
+
return np.zeros((0, 3))
|
|
80
|
+
raw = np.array([v["raw"] for v in self._vectors], dtype=np.float64)
|
|
81
|
+
# Center
|
|
82
|
+
mean = raw.mean(axis=0)
|
|
83
|
+
centered = raw - mean
|
|
84
|
+
# SVD for top-3 components
|
|
85
|
+
if centered.shape[0] < 2:
|
|
86
|
+
return np.zeros((centered.shape[0], 3))
|
|
87
|
+
U, S, Vt = np.linalg.svd(centered, full_matrices=False)
|
|
88
|
+
# Project onto top 3
|
|
89
|
+
k = min(3, Vt.shape[0])
|
|
90
|
+
proj = centered @ Vt[:k].T
|
|
91
|
+
# Pad if fewer than 3 components
|
|
92
|
+
if k < 3:
|
|
93
|
+
proj = np.hstack([proj, np.zeros((proj.shape[0], 3 - k))])
|
|
94
|
+
# Scale so points spread nicely in the scene
|
|
95
|
+
max_extent = np.abs(proj).max()
|
|
96
|
+
if max_extent > 0:
|
|
97
|
+
proj = proj * (8.0 / max_extent)
|
|
98
|
+
return proj
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict[str, Any]:
|
|
101
|
+
"""Return the trace as a JSON-serializable dict with 3D positions."""
|
|
102
|
+
positions = self._pca_3d()
|
|
103
|
+
vectors = []
|
|
104
|
+
for i, v in enumerate(self._vectors):
|
|
105
|
+
entry = {
|
|
106
|
+
"name": v["name"],
|
|
107
|
+
"type": v["type"],
|
|
108
|
+
"step": v["step"],
|
|
109
|
+
"pos": [round(float(positions[i, j]), 4) for j in range(3)],
|
|
110
|
+
}
|
|
111
|
+
vectors.append(entry)
|
|
112
|
+
return {
|
|
113
|
+
"program": self.program_name,
|
|
114
|
+
"vectors": vectors,
|
|
115
|
+
"operations": self._operations,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def to_json(self, indent: int = 2) -> str:
|
|
119
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
120
|
+
|
|
121
|
+
def to_html(self, template_path: str | None = None) -> str:
|
|
122
|
+
"""Generate a standalone HTML file with embedded trace data."""
|
|
123
|
+
import os
|
|
124
|
+
if template_path is None:
|
|
125
|
+
# Default: look for visualizer.html in the vscode-sutra media dir
|
|
126
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
127
|
+
template_path = os.path.join(
|
|
128
|
+
here, "..", "..", "vscode-sutra", "media", "visualizer.html"
|
|
129
|
+
)
|
|
130
|
+
with open(template_path, encoding="utf-8") as f:
|
|
131
|
+
html = f.read()
|
|
132
|
+
# Inject trace data before the module script
|
|
133
|
+
inject = f'<script>window.SUTRA_TRACE_DATA = {self.to_json()};</script>'
|
|
134
|
+
html = html.replace('<script type="module">', inject + '\n<script type="module">')
|
|
135
|
+
return html
|