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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user