M9 §5: geometry OPERATORS as C functions + EL builtins (read-only, staged)

Bring the relational-neighborhood geometry OPERATORS from the viz proxy
(engram-geometry-proxy.py §5) into the C runtime as reusable primitives, and
expose each as an EL builtin so any CGI app / el program can use them — not just
the engram service internals.

engram_geometry.{h,c} (pure, libm-only, read-only over descriptors):
  - engram_geo_overlap   : shared-member Jaccard + centroid/scale proximity
                           score + intersection centroid.
  - engram_geo_subtract  : orthogonal-complement residual (project A onto
                           I - V_B V_Bᵀ), closed-form variance_explained_by_B,
                           residual ellipsoid + centroid-diff; set-diff variant.
  - engram_geo_combine   : pooled descriptor (exact law-of-total-variance mean +
                           covariance), re-eigendecomposed.
  - engram_geo_distance  : centroid L2 + cosine + closed-form Wasserstein-2
                           (Bures) — mirrors the proxy _wasserstein2.
  - engram_geo_analogy   : orthogonal Procrustes R = UVᵀ (SVD) aligning A's
                           principal frame to B's + apply helper.
The C descriptor is full-dim/centered with a low-rank covariance from its top
axes; operators mirror the proxy FORMULAS and do the Wasserstein/combine eigen
work inside the small joint-axis subspace (exact there). Reuses jacobi_sym.

EL builtins (el_runtime.{h,c}, el_seed.c native wrappers):
  engram_geo_{descriptor,overlap,subtract,combine,distance,analogy}_json —
  take comma-separated seed-id set(s), build the CENTERED descriptor against the
  true store-wide mean (ad-hoc path), run the operator, return JSON. Additive:
  no flag, no effect on activation/retrieval. Surfacing via engram.el + the elc
  fold is a cutover step (same elc-drift deferral as the P0/P5 builtins); the C
  table wiring is registered now.

Tested: synthetic closed-form unit suite (20/20) — Wasserstein, Jaccard/score,
orthogonal residual, set-diff, pooled combine, Procrustes recovery. ASan+UBSan
clean; 0 leaks. Read-only; no activation/retrieval behavior change.
This commit is contained in:
2026-08-13 00:26:32 -05:00
parent 77a4bc9326
commit 5336cfe0a6
5 changed files with 808 additions and 0 deletions
+179
View File
@@ -12216,6 +12216,185 @@ el_val_t engram_dreams_json(el_val_t since_ms) {
return el_wrap_str(b.buf);
}
/* ═══════════════════════════════════════════════════════════════════════════
* §5 GEOMETRY OPERATORS as EL BUILTINS (Will: "primitives any CGI application
* should be able to use"). READ-ONLY. Each builds the CENTERED descriptor for a
* comma-separated seed-id set (against the true store-wide mean, exactly as the
* ENGRAM_GEO_PRIMING_NOCACHE ad-hoc path does), then runs the pure operator from
* engram_geometry.c and serializes the result to JSON. Additive: no flag, no
* effect on activation/retrieval. Surfacing these through engram.el + the elc fold
* is a CUTOVER step (same elc-drift deferral as the M-INTEROCEPTION P0/P5 builtins);
* the C table wiring (native __ wrappers in el_seed.c) is in place now.
* */
/* Emit a dim-length float vector as a JSON array of %.6g. */
static void eg_geo_emit_vec(JsonBuf* b, const float* v, int dim) {
char t[48]; jb_putc(b, '[');
for (int i = 0; i < dim; i++) { snprintf(t, sizeof t, "%s%.6g", i ? "," : "", (double)v[i]); jb_puts(b, t); }
jb_putc(b, ']');
}
/* Build the centered geometry descriptor for a comma-separated seed-id list.
* Returns a malloc'd descriptor (engram_geo_free) or NULL if geometry is
* unavailable (no paged store / no embeddings / seeds unresolvable). */
static GeoDescriptor* eg_geo_build_desc(const char* csv) {
if (!csv || !*csv || !g_engram_store) return NULL;
EngramStore* g = engram_get();
if (!g || g->node_count <= 0) return NULL;
/* split CSV → seed id array (dup, trimmed of spaces). */
int cap = 8, ns = 0; char** ids = malloc((size_t)cap * sizeof(char*));
if (!ids) return NULL;
const char* p = csv;
while (*p) {
while (*p == ' ' || *p == ',') p++;
const char* s = p;
while (*p && *p != ',') p++;
const char* e = p; while (e > s && e[-1] == ' ') e--;
if (e > s) {
if (ns == cap) { cap *= 2; char** t = realloc(ids, (size_t)cap * sizeof(char*)); if (!t) break; ids = t; }
ids[ns] = strndup(s, (size_t)(e - s)); ns++;
}
}
if (ns == 0) { free(ids); return NULL; }
/* infer embedding dim from the first resolvable embedded seed. */
int32_t dim = 0;
for (int i = 0; i < ns && dim == 0; i++) {
int64_t idx = engram_find_node_index(ids[i]);
if (idx >= 0 && idx < g->node_count && g->nodes[idx].emb && g->nodes[idx].emb_dim > 0)
dim = g->nodes[idx].emb_dim;
}
GeoDescriptor* geo = NULL;
const float* gmean = (dim > 0) ? eg_geo_mean_sync(dim) : NULL;
char** vids = malloc((size_t)g->node_count * sizeof(char*));
if (gmean && vids) {
for (int64_t i = 0; i < g->node_count; i++) vids[i] = g->nodes[i].id;
geo = engram_geometry_descriptor(g_engram_store, _eg_vindex, vids, (int)g->node_count,
(const char* const*)ids, (size_t)ns, NULL, gmean);
}
free(vids);
for (int i = 0; i < ns; i++) free(ids[i]);
free(ids);
return geo;
}
static el_val_t eg_geo_err(const char* msg) {
JsonBuf b; jb_init(&b);
jb_puts(&b, "{\"error\":"); jb_emit_escaped(&b, msg); jb_putc(&b, '}');
return el_wrap_str(b.buf);
}
/* engram_geo_descriptor_json(seed_ids_csv) → the compact centered descriptor. */
el_val_t engram_geo_descriptor_json(el_val_t seeds) {
GeoDescriptor* g = eg_geo_build_desc(EL_CSTR(seeds));
if (!g) return eg_geo_err("geometry unavailable (no paged store / embeddings / seeds)");
JsonBuf b; jb_init(&b); char t[80];
jb_putc(&b, '{');
jb_puts(&b, "\"hub_id\":"); jb_emit_escaped(&b, g->hub_id ? g->hub_id : "");
snprintf(t, sizeof t, ",\"dim\":%d,\"radius\":%.6g,\"total_variance\":%.6g", g->dim, g->radius, g->total_variance); jb_puts(&b, t);
snprintf(t, sizeof t, ",\"k_core\":%d,\"co_registration\":%.6g", g->k_core, g->co_registration); jb_puts(&b, t);
snprintf(t, sizeof t, ",\"n_members\":%d,\"n_embedded\":%d,\"n_axes\":%d", g->n_members, g->n_embedded, g->n_axes); jb_puts(&b, t);
jb_puts(&b, ",\"axis_extents\":["); for (int i = 0; i < g->n_axes; i++) { snprintf(t, sizeof t, "%s%.6g", i ? "," : "", g->axes[i].extent); jb_puts(&b, t); } jb_putc(&b, ']');
jb_puts(&b, ",\"members\":[");
for (int i = 0; i < g->n_members; i++) {
if (i) jb_putc(&b, ',');
jb_puts(&b, "{\"id\":"); jb_emit_escaped(&b, g->members[i].id ? g->members[i].id : "");
snprintf(t, sizeof t, ",\"m\":%.4g,\"cent\":%.4g,\"core\":%d}", g->members[i].membership, g->members[i].centrality, g->members[i].core); jb_puts(&b, t);
}
jb_putc(&b, ']'); jb_putc(&b, '}');
engram_geo_free(g);
return el_wrap_str(b.buf);
}
/* engram_geo_overlap_json(a_csv, b_csv) → shared set + Jaccard + score. */
el_val_t engram_geo_overlap_json(el_val_t a_seeds, el_val_t b_seeds) {
GeoDescriptor* A = eg_geo_build_desc(EL_CSTR(a_seeds));
GeoDescriptor* B = eg_geo_build_desc(EL_CSTR(b_seeds));
if (!A || !B) { if (A) engram_geo_free(A); if (B) engram_geo_free(B); return eg_geo_err("geometry unavailable"); }
GeoOverlap o; char t[80]; JsonBuf b; jb_init(&b);
if (engram_geo_overlap(A, B, &o) != 0) { engram_geo_free(A); engram_geo_free(B); return eg_geo_err("dim mismatch"); }
jb_putc(&b, '{');
snprintf(t, sizeof t, "\"n_shared\":%d,\"n_union\":%d,\"jaccard\":%.6g", o.n_shared, o.n_union, o.jaccard); jb_puts(&b, t);
snprintf(t, sizeof t, ",\"centroid_distance\":%.6g,\"overlap_score\":%.6g", o.centroid_distance, o.overlap_score); jb_puts(&b, t);
jb_puts(&b, ",\"shared_ids\":["); for (int i = 0; i < o.n_shared; i++) { if (i) jb_putc(&b, ','); jb_emit_escaped(&b, o.shared_ids[i]); } jb_putc(&b, ']');
jb_putc(&b, '}');
engram_geo_overlap_free(&o); engram_geo_free(A); engram_geo_free(B);
return el_wrap_str(b.buf);
}
/* engram_geo_subtract_json(a_csv, b_csv, mode) — mode "setdiff" → set-difference,
* anything else orthogonal-complement residual. */
el_val_t engram_geo_subtract_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t mode) {
GeoDescriptor* A = eg_geo_build_desc(EL_CSTR(a_seeds));
GeoDescriptor* B = eg_geo_build_desc(EL_CSTR(b_seeds));
if (!A || !B) { if (A) engram_geo_free(A); if (B) engram_geo_free(B); return eg_geo_err("geometry unavailable"); }
const char* m = EL_CSTR(mode); char t[80]; JsonBuf b; jb_init(&b);
if (m && strcmp(m, "setdiff") == 0) {
GeoSetDiff s;
if (engram_geo_setdiff(A, B, &s) != 0) { engram_geo_free(A); engram_geo_free(B); return eg_geo_err("dim mismatch"); }
jb_putc(&b, '{'); jb_puts(&b, "\"mode\":\"setdiff\"");
snprintf(t, sizeof t, ",\"n_only\":%d,\"removed\":%d,\"centroid_diff_mag\":%.6g", s.n_only, s.removed, s.centroid_diff_mag); jb_puts(&b, t);
jb_puts(&b, ",\"only_ids\":["); for (int i = 0; i < s.n_only; i++) { if (i) jb_putc(&b, ','); jb_emit_escaped(&b, s.only_ids[i]); } jb_putc(&b, ']');
jb_putc(&b, '}');
engram_geo_setdiff_free(&s);
} else {
GeoResidual r;
if (engram_geo_subtract(A, B, 0, &r) != 0) { engram_geo_free(A); engram_geo_free(B); return eg_geo_err("dim mismatch"); }
jb_putc(&b, '{'); jb_puts(&b, "\"mode\":\"residual\"");
snprintf(t, sizeof t, ",\"variance_explained_by_B\":%.6g,\"residual_scale\":%.6g", r.variance_explained_by_B, r.residual_scale); jb_puts(&b, t);
snprintf(t, sizeof t, ",\"removed_dims\":%d,\"residual_n_axes\":%d,\"centroid_diff_mag\":%.6g", r.removed_dims, r.n_axes, r.centroid_diff_mag); jb_puts(&b, t);
jb_putc(&b, '}');
engram_geo_residual_free(&r);
}
engram_geo_free(A); engram_geo_free(B);
return el_wrap_str(b.buf);
}
/* engram_geo_combine_json(a_csv, b_csv) → merged descriptor summary. */
el_val_t engram_geo_combine_json(el_val_t a_seeds, el_val_t b_seeds) {
GeoDescriptor* A = eg_geo_build_desc(EL_CSTR(a_seeds));
GeoDescriptor* B = eg_geo_build_desc(EL_CSTR(b_seeds));
if (!A || !B) { if (A) engram_geo_free(A); if (B) engram_geo_free(B); return eg_geo_err("geometry unavailable"); }
GeoDescriptor* C = engram_geo_combine(A, B, 8);
engram_geo_free(A); engram_geo_free(B);
if (!C) return eg_geo_err("combine failed (dim mismatch / OOM)");
JsonBuf b; jb_init(&b); char t[80];
jb_putc(&b, '{');
jb_puts(&b, "\"hub_id\":"); jb_emit_escaped(&b, C->hub_id ? C->hub_id : "");
snprintf(t, sizeof t, ",\"dim\":%d,\"radius\":%.6g,\"total_variance\":%.6g", C->dim, C->radius, C->total_variance); jb_puts(&b, t);
snprintf(t, sizeof t, ",\"n_members\":%d,\"n_embedded\":%d,\"n_axes\":%d", C->n_members, C->n_embedded, C->n_axes); jb_puts(&b, t);
jb_puts(&b, ",\"axis_extents\":["); for (int i = 0; i < C->n_axes; i++) { snprintf(t, sizeof t, "%s%.6g", i ? "," : "", C->axes[i].extent); jb_puts(&b, t); } jb_putc(&b, ']');
jb_putc(&b, '}');
engram_geo_free(C);
return el_wrap_str(b.buf);
}
/* engram_geo_distance_json(a_csv, b_csv) → centroid + Wasserstein-2. */
el_val_t engram_geo_distance_json(el_val_t a_seeds, el_val_t b_seeds) {
GeoDescriptor* A = eg_geo_build_desc(EL_CSTR(a_seeds));
GeoDescriptor* B = eg_geo_build_desc(EL_CSTR(b_seeds));
if (!A || !B) { if (A) engram_geo_free(A); if (B) engram_geo_free(B); return eg_geo_err("geometry unavailable"); }
GeoDistance d; char t[96]; JsonBuf b; jb_init(&b);
if (engram_geo_distance(A, B, &d) != 0) { engram_geo_free(A); engram_geo_free(B); return eg_geo_err("dim mismatch"); }
snprintf(t, sizeof t, "{\"centroid_distance\":%.6g,\"centroid_cosine\":%.6g,\"wasserstein2\":%.6g}", d.centroid_distance, d.centroid_cosine, d.wasserstein2);
jb_puts(&b, t);
engram_geo_free(A); engram_geo_free(B);
return el_wrap_str(b.buf);
}
/* engram_geo_analogy_json(a_csv, b_csv) → orthogonal Procrustes residual + rank. */
el_val_t engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds) {
GeoDescriptor* A = eg_geo_build_desc(EL_CSTR(a_seeds));
GeoDescriptor* B = eg_geo_build_desc(EL_CSTR(b_seeds));
if (!A || !B) { if (A) engram_geo_free(A); if (B) engram_geo_free(B); return eg_geo_err("geometry unavailable"); }
GeoAnalogy an; char t[96]; JsonBuf b; jb_init(&b);
if (engram_geo_analogy(A, B, &an) != 0) { engram_geo_free(A); engram_geo_free(B); return eg_geo_err("dim mismatch"); }
snprintf(t, sizeof t, "{\"subspace_rank\":%d,\"residual\":%.6g}", an.r, an.residual);
jb_puts(&b, t);
engram_geo_analogy_free(&an);
engram_geo_free(A); engram_geo_free(B);
return el_wrap_str(b.buf);
}
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) {
/* Re-implement here directly so we serialize without going through
* the ElList path. Walks BFS to max_depth, emits {node, edge, hops}