From 2a4c5c645ac5640e7eb0d3b2d6c6f5842c440d0d Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 12 Aug 2026 19:40:25 -0500 Subject: [PATCH] M9 foundation: relational-neighborhood geometry descriptor (read-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stone the operator/drift/occupation work stands on: express a relational neighborhood as the compact joint geometry Will specified (design §3/§5, node e94371bd) — semantic side (centroid, principal-axis ellipsoid via dual-PCA, radius) braided with the relational side (k-core skeleton, hub->periphery centrality gradient), plus soft membership and a co-registration diagnostic (corr of hebb strength vs semantic proximity — >0 reifies, <0 flags dreams). Built ONLY on the two standalone modules — engram_vindex (ANN, the cloud) and engram_store (embeddings + hebb adjacency, the skeleton). Pure C11 + libm; does not link or touch el_runtime.c. Strictly READ-ONLY: never mutates nodes, edges, activation, the index, or any retrieval path. Not yet wired into retrieval — foundation only. Self-contained test (test_geometry.c) synthesizes two known embedding clusters with intra-cluster hebb edges and verifies the descriptor recovers the shape: centroid on the seeded cluster, hub = relational center, skeleton = the strong intra-cluster wiring, positive co-registration, sorted axis extents. PERF + ASan/UBSan passes both green; needs no live data. --- engram/test/run_geometry_tests.sh | 21 ++ engram/test/test_geometry.c | 141 +++++++++++ lang/runtime/engram_geometry.c | 393 ++++++++++++++++++++++++++++++ lang/runtime/engram_geometry.h | 105 ++++++++ 4 files changed, 660 insertions(+) create mode 100755 engram/test/run_geometry_tests.sh create mode 100644 engram/test/test_geometry.c create mode 100644 lang/runtime/engram_geometry.c create mode 100644 lang/runtime/engram_geometry.h diff --git a/engram/test/run_geometry_tests.sh b/engram/test/run_geometry_tests.sh new file mode 100755 index 0000000..ee37b82 --- /dev/null +++ b/engram/test/run_geometry_tests.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Build + RUN the M9 FOUNDATION geometry-descriptor tests. Pure C11 (gcc/cc), +# stdlib + libm only. Standalone module — NOT folded through elb/elc. Two passes: +# 1. PERF — optimised (-O2, no sanitizer): the functional gate. +# 2. SAFETY — ASan + UBSan on the same suite (memory-safety is size-independent). +set -e +HERE=$(cd "$(dirname "$0")" && pwd) +RT="$HERE/../../lang/runtime" +CC=${CC:-cc} +SRC="$HERE/test_geometry.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c" +WARN="-std=c11 -Wall -Wextra" +TMP=$(mktemp -d) + +echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate" +$CC $WARN -O2 -I"$RT" $SRC -lm -o "$TMP/perf" +"$TMP/perf" + +echo +echo "### PASS 2: SAFETY (ASan/UBSan)" +$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -o "$TMP/safe" +ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe" diff --git a/engram/test/test_geometry.c b/engram/test/test_geometry.c new file mode 100644 index 0000000..6f7a3d7 --- /dev/null +++ b/engram/test/test_geometry.c @@ -0,0 +1,141 @@ +/* test_geometry.c — build + RUN gate for the M9 FOUNDATION geometry descriptor + * (engram_geometry.{c,h}). Self-contained: synthesizes a store with two KNOWN + * embedding clusters + intra-cluster hebb edges, then verifies the descriptor + * recovers the shape — centroid near the seeded cluster, skeleton = the strong + * intra-cluster edges, membership gradient, radius, positive co-registration. + * + * Pure C11; links engram_geometry.c + engram_store.c + engram_vindex.c; -lm. + * ASan/UBSan clean. Needs no live data. + */ +#include "engram_geometry.h" +#include "engram_store.h" +#include "engram_vindex.h" +#include +#include +#include +#include +#include +#include + +#define DIM 64 +static int g_fail=0; +#define CHECK(c,m) do{ if(!(c)){printf(" FAIL: %s\n",m); g_fail=1;} else printf(" ok: %s\n",m);}while(0) + +static uint64_t rs=0x1234abcdULL; +static uint64_t xr(void){ uint64_t z=(rs+=0x9E3779B97F4A7C15ULL); + z=(z^(z>>30))*0xBF58476D1CE4E5B9ULL; z=(z^(z>>27))*0x94D049BB133111EBULL; return z^(z>>31); } +static float jitter(void){ return (float)(((double)(xr()>>11)*(1.0/9007199254740992.0))-0.5)*0.15f; } + +/* two clusters: A centered on axis 0, B centered on axis 1. NA+NB nodes. */ +#define NA 40 +#define NB 40 + +int main(void){ + printf("=== engram_geometry (M9 foundation) test suite ===\n"); + char path[256]; snprintf(path,sizeof path,"/tmp/geo_test_store_%d.egm",(int)getpid()); + unlink(path); + EngramPagedStore* st=store_create(path); + if(!st){ printf("FAIL: store_create\n"); return 1; } + + char aids[NA][16], bids[NB][16]; + /* cluster A: near +e0 ; cluster B: near +e1 */ + for(int i=0;iB0 */ + int ei=0; + for(int i=1;i expect an A-dominated neighborhood */ + st=store_open(path); + const char* seeds[1]={aids[0]}; + GeoDescriptor* g=engram_geometry_descriptor(st, ix, ids, nids, seeds, 1, &P); + CHECK(g!=NULL, "descriptor computed"); + if(g){ + printf(" members=%d embedded=%d edges=%d k_core=%d radius=%.4f co_reg=%.3f n_axes=%d\n", + g->n_members,g->n_embedded,g->n_edges,g->k_core,g->radius,g->co_registration,g->n_axes); + + /* centroid near cluster-A center (+e0): centroid[0] should dominate */ + int argmax=0; for(int d=1;ddim;d++) if(fabsf(g->centroid[d])>fabsf(g->centroid[argmax])) argmax=d; + printf(" centroid dominant axis = %d (expect 0), centroid[0]=%.3f centroid[1]=%.3f\n", + argmax, g->centroid[0], g->centroid[1]); + CHECK(argmax==0, "centroid sits on cluster-A's axis (near members)"); + + /* hub should be A0 (the intra-A hub with NA-1 strong edges) */ + CHECK(g->hub_id && strcmp(g->hub_id,"A0")==0, "hub = the relational center A0"); + + /* membership: seed A0 == 1.0; A-members strong, B-members (if any) weaker */ + double seedw=-1, minA=2, maxB=-1; int na=0,nb=0; + for(int i=0;in_members;i++){ + const char* id=g->members[i].id; double w=g->members[i].membership; + if(strcmp(id,"A0")==0) seedw=w; + if(id[0]=='A'){ na++; if(wmaxB)maxB=w; } + } + printf(" A-members=%d B-members=%d seedw=%.3f\n", na,nb,seedw); + CHECK(fabs(seedw-1.0)<1e-9, "seed membership == 1.0"); + CHECK(na>=NA-1, "neighborhood recovers cluster A"); + + /* skeleton = the strong intra-A edges: every edge eff_weight>=threshold, + * and edges connect A-nodes (co-registration should be positive: wired + * pairs are semantically near). */ + int allstrong=1, allA=1; + for(int e=0;en_edges;e++){ + if(g->edges[e].eff_weight < P.edge_min_weight) allstrong=0; + const char* a=g->members[g->edges[e].a].id, *b=g->members[g->edges[e].b].id; + if(!(a[0]=='A'&&b[0]=='A')) { /* the lone eX cross edge is allowed */ + if(!((strcmp(a,"A0")==0&&strcmp(b,"B0")==0)||(strcmp(a,"B0")==0&&strcmp(b,"A0")==0))) allA=0; } + } + CHECK(allstrong, "skeleton holds only above-threshold (strong) edges"); + CHECK(allA, "skeleton backbone is the intra-cluster wiring"); + CHECK(g->co_registration>0.0, "co-registration positive (wired pairs are semantically near)"); + + /* principal axes: extents strictly non-increasing */ + int mono=1; for(int i=1;in_axes;i++) if(g->axes[i].extent>g->axes[i-1].extent+1e-9) mono=0; + CHECK(g->n_axes>0 && mono, "principal axes sorted by descending extent"); + CHECK(g->radius>0, "radius positive"); + } + engram_geo_free(g); + + /* edge cases: NULL store, no seeds, relational-only (NULL vindex) */ + CHECK(engram_geometry_descriptor(NULL,ix,ids,nids,seeds,1,&P)==NULL, "NULL store -> NULL"); + CHECK(engram_geometry_descriptor(st,ix,ids,nids,seeds,0,&P)==NULL, "zero seeds -> NULL"); + GeoDescriptor* g2=engram_geometry_descriptor(st, NULL, NULL, 0, seeds, 1, &P); + CHECK(g2!=NULL && g2->n_members>=NA-1, "relational-only path (no vindex) works"); + engram_geo_free(g2); + + for(int i=0;i +#include +#include + +/* Must match ENGRAM_HEBB_GAIN in el_runtime.c (eff = weight*(1+GAIN*hebb)). */ +#define GEO_HEBB_GAIN 0.5 +/* Internal cap on the m×m Jacobi eigensolve: above this we still give centroid + + * radius but skip principal axes (honest degradation, not a lie). */ +#define GEO_EIG_CAP 512 + +/* ───────────────────────── small dynamic member table ────────────────────── + * Neighborhoods are small (tens..few hundred), so linear-scan dedup is fine. */ +typedef struct { + char** id; /* strdup'd ids */ + double* memb; /* provisional membership */ + float** emb; /* L2-normalized emb copy (dim floats) or NULL */ + double* sal; /* stored salience */ + int n, cap, dim; +} MemSet; + +static int ms_init(MemSet* s, int dim){ + s->n=0; s->cap=16; s->dim=dim; + s->id=calloc(s->cap,sizeof*s->id); s->memb=calloc(s->cap,sizeof*s->memb); + s->emb=calloc(s->cap,sizeof*s->emb); s->sal=calloc(s->cap,sizeof*s->sal); + return (s->id&&s->memb&&s->emb&&s->sal)?0:-1; +} +static int ms_find(const MemSet* s, const char* id){ + for(int i=0;in;i++) if(strcmp(s->id[i],id)==0) return i; + return -1; +} +/* Insert or bump membership (keep the max). Returns member index or <0 on OOM. */ +static int ms_upsert(MemSet* s, const char* id, double memb){ + int i=ms_find(s,id); + if(i>=0){ if(memb>s->memb[i]) s->memb[i]=memb; return i; } + if(s->n==s->cap){ + int nc=s->cap*2; + char** a=realloc(s->id,nc*sizeof*a); if(!a) return -1; s->id=a; + double* b=realloc(s->memb,nc*sizeof*b); if(!b) return -1; s->memb=b; + float** c=realloc(s->emb,nc*sizeof*c); if(!c) return -1; s->emb=c; + double* d=realloc(s->sal,nc*sizeof*d); if(!d) return -1; s->sal=d; + s->cap=nc; + } + s->id[s->n]=strdup(id); if(!s->id[s->n]) return -1; + s->memb[s->n]=memb; s->emb[s->n]=NULL; s->sal[s->n]=0.0; + return s->n++; +} +static void ms_free(MemSet* s){ + for(int i=0;in;i++){ free(s->id[i]); free(s->emb[i]); } + free(s->id); free(s->memb); free(s->emb); free(s->sal); +} + +/* L2-normalize a copy of v into out (dim floats). Returns 0, or -1 if ~zero. */ +static int normcopy(const float* v, int dim, float* out){ + double s=0; for(int i=0;iid[i], &nn)!=1){ return; } + s->sal[i]=nn.salience; + if(nn.emb && nn.emb_dim==s->dim){ + float* e=malloc((size_t)s->dim*sizeof(float)); + if(e && normcopy(nn.emb,s->dim,e)==0) s->emb[i]=e; else free(e); + } + store_node_free(&nn); +} + +/* ───────────────────────── Jacobi symmetric eigensolver ───────────────────── + * Cyclic Jacobi on a dense symmetric m×m matrix A (row-major, overwritten). + * Eigenvalues -> w[m]; eigenvectors (columns) -> V[m*m]. Robust, libm-only. */ +static void jacobi_sym(double* A, int m, double* w, double* V){ + for(int i=0;iann_k=24; p->hop_relational=1; p->edge_min_weight=0.05; + p->kcore_k=0; p->top_axes=8; p->max_members=400; +} + +/* Effective hebb-weighted edge strength, matching eg_edge_eff_weight. */ +static double eff_w(double weight, double hebb){ + double w = weight * (1.0 + GEO_HEBB_GAIN*hebb); + if(w>1.0) w=1.0; if(w<0.0) w=0.0; return w; +} + +GeoDescriptor* engram_geometry_descriptor( + EngramPagedStore* store, VIndex* vindex, + char** vids, int n_vids, + const char* const* seed_ids, size_t n_seeds, + const GeoParams* params) +{ + if(!store || !seed_ids || n_seeds==0) return NULL; + GeoParams P; if(params) P=*params; else engram_geo_default_params(&P); + + int dim = 0; + /* infer dim from the first embedded seed */ + for(size_t i=0;i0) dim=nn.emb_dim; + } + store_node_free(&nn); + } + if(dim==0) dim = 768; /* no embedded seed: still build the relational side */ + + MemSet ms; if(ms_init(&ms,dim)!=0){ ms_free(&ms); return NULL; } + + /* 1. seeds (membership 1.0) */ + for(size_t i=0;i0 && prov_n>0){ + int k=P.ann_k*(int)n_seeds; if(kn_vids) k=n_vids; + uint64_t* rids=malloc((size_t)k*sizeof(uint64_t)); + float* dd=malloc((size_t)k*sizeof(float)); + if(rids&&dd){ + int got=vindex_search(vindex, prov, k, 0, rids, dd); + for(int r=0;r=(uint64_t)n_vids) continue; + double memb = 1.0 - (double)dd[r]; /* cosine sim in [-1,1] */ + if(memb<0) memb=0; + int mi=ms_upsert(&ms, vids[rids[r]], memb*0.9); /* <1: not a seed */ + if(mi>=0 && !ms.emb[mi]) ms_load_node(&ms,mi,store); + } + } + free(rids); free(dd); + } + free(prov); + + /* 3. relational expansion: seeds' hebb neighbors become members */ + if(P.hop_relational){ + for(int i=0;i=0 && !ms.emb[mi]) ms_load_node(&ms,mi,store); + } + } + store_edges_free(es,ne); + es=NULL; ne=0; + if(store_get_edges_to(store, ms.id[i], &es, &ne)==0 && es){ + for(size_t e=0;e=0 && !ms.emb[mi]) ms_load_node(&ms,mi,store); + } + } + store_edges_free(es,ne); + } + } + + /* optional cap: keep the highest-membership members (guards eigensolve) */ + if(P.max_members>0 && ms.n>P.max_members){ + /* simple selection: repeatedly drop the min-membership non-seed member */ + while(ms.n>P.max_members){ + int worst=-1; double wv=1e30; + for(int i=n_seed_members;i0 && normcopy(centroid,dim,cdir)==0); + for(int i=0;i1)cs=1; if(cs<-1)cs=-1; + distc[i]=1.0-cs; + } else distc[i]=-1.0; /* unknown */ + } + /* variance = mean squared Euclid distance of normalized embs to centroid */ + for(int j=0;j0?total_var:0); + + /* ── principal axes via dual PCA (Jacobi on the m×m Gram of centered embs) ── */ + int n_axes=0; GeoAxis* axes=NULL; + if(nemb>=2 && nemb<=GEO_EIG_CAP){ + int m=nemb; + /* centered, row-major m×dim */ + float* Xc=malloc((size_t)m*dim*sizeof(float)); + for(int j=0;jw[ord[a]]){int t=ord[a];ord[a]=ord[b];ord[b]=t;} + int keep=P.top_axes; if(keep>m) keep=m; if(keep<0) keep=0; + axes=calloc((size_t)keep,sizeof(GeoAxis)); + for(int t=0;t1e-12) for(int d=0;d1)cs=1; if(cs<-1)cs=-1; + double x=w, y=cs; + cr_n++; cr_sx+=x; cr_sy+=y; cr_sxx+=x*x; cr_syy+=y*y; cr_sxy+=x*y; + } + } + } + store_edges_free(es,ne); + } + double co_reg=0; + if(cr_n>=2){ + double cov=cr_sxy - cr_sx*cr_sy/cr_n; + double vx=cr_sxx - cr_sx*cr_sx/cr_n, vy=cr_syy - cr_sy*cr_sy/cr_n; + if(vx>1e-12 && vy>1e-12) co_reg=cov/sqrt(vx*vy); + } + + /* ── k-core: peel members by internal degree to get core numbers ── */ + int* core=calloc((size_t)M,sizeof(int)); + { + int* deg=malloc((size_t)M*sizeof(int)); + int* removed=calloc((size_t)M,sizeof(int)); + for(int i=0;i0){ + int progressed=0; + for(int i=0;i=0) deg[o]--; + } + } + } + if(!progressed) level++; + } + free(deg); free(removed); + } + int k_core=0; for(int i=0;ik_core) k_core=core[i]; + + /* hub = highest centrality (tie-break salience) */ + int hub=-1; double hv=-1; + for(int i=0;ihv){hv=v;hub=i;} } + if(hub<0) hub=0; + + /* ── assemble descriptor ── */ + GeoDescriptor* g=calloc(1,sizeof(GeoDescriptor)); + g->dim=dim; + g->hub_id = strdup(ms.id[hub]); + g->centroid = centroid; /* transfer ownership */ + g->n_axes=n_axes; g->axes=axes; + g->total_variance=total_var; g->radius=radius; + g->n_members=M; g->n_embedded=nemb; + g->members=calloc((size_t)M,sizeof(GeoMember)); + for(int i=0;imembers[i].id=strdup(ms.id[i]); + g->members[i].membership=ms.memb[i]; + g->members[i].centrality=centrality[i]; + g->members[i].salience=ms.sal[i]; + g->members[i].core=core[i]; + g->members[i].dist_centroid=distc[i]; + g->members[i].embedded=ms.emb[i]?1:0; + } + g->n_edges=n_edges; g->edges=edges; + g->k_core=(P.kcore_k>0?P.kcore_k:k_core); + g->co_registration=co_reg; + + free(centrality); free(degree); free(core); free(distc); free(eidx); + free(cdir); + ms_free(&ms); + return g; +} + +void engram_geo_free(GeoDescriptor* g){ + if(!g) return; + free(g->hub_id); free(g->centroid); + for(int i=0;in_axes;i++) free(g->axes[i].axis); + free(g->axes); + for(int i=0;in_members;i++) free(g->members[i].id); + free(g->members); free(g->edges); + free(g); +} diff --git a/lang/runtime/engram_geometry.h b/lang/runtime/engram_geometry.h new file mode 100644 index 0000000..84fcffc --- /dev/null +++ b/lang/runtime/engram_geometry.h @@ -0,0 +1,105 @@ +/* engram_geometry.h — M9 FOUNDATION: the relational-neighborhood GEOMETRY + * DESCRIPTOR (design doc §3, §5; memory node e94371bd). + * + * Computes, for a relational neighborhood grown from a seed set, the compact + * (KB-not-MB) joint geometry Will specified: the SEMANTIC geometry (centroid, + * covariance / principal axes, radius) braided with the RELATIONAL geometry + * (k-core skeleton, hub->periphery centrality gradient), plus soft membership. + * + * Two coordinate systems, one shape — "a constellation: bright prototype at the + * center, a cloud of members at varying distance, the strongest edges as a + * backbone, fading at the edges." + * + * Built ON the two standalone M-era modules only: + * - engram_vindex : semantic neighbors (the cloud) via ANN. + * - engram_store : node embeddings + hebb adjacency (the skeleton), read-only. + * It does NOT link or touch el_runtime.c, and it is a pure READ over the graph: + * it never modifies nodes, edges, activation, the index, or any retrieval path. + * + * Pure C11, stdlib + libm only. The descriptor is a foundation object; it is NOT + * wired into retrieval/priming yet (that is the next M9 step). + */ +#ifndef ENGRAM_GEOMETRY_H +#define ENGRAM_GEOMETRY_H + +#include +#include +#include "engram_store.h" +#include "engram_vindex.h" + +/* One member of the neighborhood + its place in the gradient. */ +typedef struct { + char* id; + double membership; /* soft membership in [0,1] (semantic+relational blend) */ + double centrality; /* skeleton weighted-degree — relational salience */ + double salience; /* the node's own stored salience */ + int core; /* k-core number (0 = fringe / not in any core) */ + double dist_centroid; /* cosine distance of member emb to centroid (semantic)*/ + int embedded; /* 1 if the member carried an emb vector */ +} GeoMember; + +/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb), + * clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */ +typedef struct { uint32_t a, b; double eff_weight; double hebb; } GeoEdge; + +/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent + * (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */ +typedef struct { float* axis; double extent; } GeoAxis; + +typedef struct { + int dim; + /* ── anchor ── */ + char* hub_id; /* highest-centrality member: the relational hub */ + float* centroid; /* v̄ ∈ R^dim: mean of L2-normalized member embs */ + /* ── shape (compact covariance): top principal axes + extents ── */ + int n_axes; + GeoAxis* axes; /* orientation + extents of the ellipsoid */ + double total_variance; /* trace(Σ) = mean squared member dist to centroid*/ + /* ── scale ── */ + double radius; /* sqrt(total_variance) — the neighborhood breadth*/ + /* ── members + gradient ── */ + int n_members; + GeoMember* members; /* soft membership {id->weight} + centrality/salience */ + /* ── skeleton ── */ + int n_edges; + GeoEdge* edges; /* strong internal hebb edges = the backbone */ + int k_core; /* the maximum core number present in the skeleton*/ + /* ── diagnostics ── */ + double co_registration;/* corr(hebb strength, semantic proximity) over */ + /* internal edges: >0 = geometries agree (reify); */ + /* <0 = disagree (surprising links / dream cands). */ + int n_embedded; /* members that carried an emb vector */ +} GeoDescriptor; + +typedef struct { + int ann_k; /* semantic expansion: ANN neighbors per seed (0=off) */ + int hop_relational; /* 1 = include seeds' hebb neighbors as members */ + double edge_min_weight; /* skeleton: ignore internal edges below this eff wt */ + int kcore_k; /* target k for the reported k-core (0 = auto/max) */ + int top_axes; /* principal axes to retain (default 8) */ + int max_members; /* cap neighborhood size (guards the eigensolve cost) */ +} GeoParams; + +/* Fill p with sane defaults: ann_k=24, hop_relational=1, edge_min_weight=0.05, + * kcore_k=0 (auto), top_axes=8, max_members=400. */ +void engram_geo_default_params(GeoParams* p); + +/* Compute the geometry descriptor of the neighborhood grown from seed_ids. + * READ-ONLY over store + vindex. + * store — an opened store (borrowed; not modified). + * vindex — optional ANN index for semantic expansion; NULL disables it. + * vids — the ordinal->store-id map returned by vindex_build_from_store + * (vids[node_id] == store id). Required iff vindex != NULL. + * n_vids — length of vids. + * params — NULL to use engram_geo_default_params. + * Returns a malloc'd descriptor (free with engram_geo_free), or NULL on error + * (no seeds resolvable, OOM). */ +GeoDescriptor* engram_geometry_descriptor( + EngramPagedStore* store, VIndex* vindex, + char** vids, int n_vids, + const char* const* seed_ids, size_t n_seeds, + const GeoParams* params); + +void engram_geo_free(GeoDescriptor* g); + +#endif /* ENGRAM_GEOMETRY_H */