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.
@@ -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
@@ -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