M9 foundation: relational-neighborhood geometry descriptor (read-only)
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.
This commit is contained in:
Executable
+21
@@ -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"
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;i<NA;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
snprintf(aids[i],16,"A%d",i); n.id=aids[i]; n.node_type="Concept"; n.tier="Semantic";
|
||||
n.content="cluster-A"; n.salience=0.5+0.01*i;
|
||||
float v[DIM]; for(int d=0;d<DIM;d++) v[d]=jitter(); v[0]=1.0f+jitter();
|
||||
n.emb=v; n.emb_dim=DIM; store_put_node(st,&n);
|
||||
}
|
||||
for(int i=0;i<NB;i++){
|
||||
StoreNode n; memset(&n,0,sizeof n);
|
||||
snprintf(bids[i],16,"B%d",i); n.id=bids[i]; n.node_type="Concept"; n.tier="Semantic";
|
||||
n.content="cluster-B"; n.salience=0.3;
|
||||
float v[DIM]; for(int d=0;d<DIM;d++) v[d]=jitter(); v[1]=1.0f+jitter();
|
||||
n.emb=v; n.emb_dim=DIM; store_put_node(st,&n);
|
||||
}
|
||||
/* strong intra-A hebb edges (a chain + hub), weaker cross edges A0<->B0 */
|
||||
int ei=0;
|
||||
for(int i=1;i<NA;i++){
|
||||
StoreEdge e; memset(&e,0,sizeof e); char id[24]; snprintf(id,24,"eA%d",ei++);
|
||||
e.id=id; e.from_id=aids[0]; e.to_id=aids[i]; e.relation="assoc"; e.weight=0.9; e.hebb=0.4;
|
||||
store_put_edge(st,&e);
|
||||
}
|
||||
for(int i=1;i<NB;i++){
|
||||
StoreEdge e; memset(&e,0,sizeof e); char id[24]; snprintf(id,24,"eB%d",ei++);
|
||||
e.id=id; e.from_id=bids[0]; e.to_id=bids[i]; e.relation="assoc"; e.weight=0.9; e.hebb=0.4;
|
||||
store_put_edge(st,&e);
|
||||
}
|
||||
{ StoreEdge e; memset(&e,0,sizeof e); e.id=(char*)"eX"; e.from_id=aids[0]; e.to_id=bids[0];
|
||||
e.relation="assoc"; e.weight=0.5; e.hebb=0.0; store_put_edge(st,&e); }
|
||||
store_close(st);
|
||||
|
||||
VIndex* ix=vindex_create(DIM,0,0);
|
||||
char** ids=NULL; int nids=0;
|
||||
int ins=vindex_build_from_store(ix, path, &ids, &nids);
|
||||
CHECK(ins==NA+NB, "vindex built over all embedded nodes");
|
||||
|
||||
GeoParams P; engram_geo_default_params(&P); P.ann_k=20; P.max_members=0;
|
||||
|
||||
/* seed inside cluster A -> 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;d<g->dim;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;i<g->n_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(w<minA)minA=w; }
|
||||
if(id[0]=='B'){ nb++; if(w>maxB)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;e<g->n_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;i<g->n_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<nids;i++) free(ids[i]); free(ids);
|
||||
vindex_free(ix); store_close(st); unlink(path);
|
||||
printf("\n=== %s ===\n", g_fail?"FAILURES PRESENT":"ALL TESTS PASSED");
|
||||
return g_fail;
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
/* engram_geometry.c — M9 FOUNDATION: relational-neighborhood geometry descriptor.
|
||||
* See engram_geometry.h. Pure C11, stdlib + libm. READ-ONLY over store + vindex.
|
||||
*/
|
||||
#include "engram_geometry.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
/* 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;i<s->n;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;i<s->n;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;i<dim;i++) s+=(double)v[i]*v[i];
|
||||
double n=sqrt(s); if(n<1e-12) return -1;
|
||||
for(int i=0;i<dim;i++) out[i]=(float)(v[i]/n);
|
||||
return 0;
|
||||
}
|
||||
static double dotf(const float* a, const float* b, int dim){
|
||||
double s=0; for(int i=0;i<dim;i++) s+=(double)a[i]*b[i]; return s;
|
||||
}
|
||||
|
||||
/* Attach a member's emb (normalized) + salience by point-reading the store. */
|
||||
static void ms_load_node(MemSet* s, int i, EngramPagedStore* st){
|
||||
StoreNode nn; memset(&nn,0,sizeof nn);
|
||||
if(store_get_node(st, s->id[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;i<m;i++){ for(int j=0;j<m;j++) V[i*m+j]=(i==j)?1.0:0.0; }
|
||||
for(int sweep=0; sweep<100; sweep++){
|
||||
double off=0; for(int p=0;p<m;p++) for(int q=p+1;q<m;q++) off+=A[p*m+q]*A[p*m+q];
|
||||
if(off < 1e-18) break;
|
||||
for(int p=0;p<m;p++) for(int q=p+1;q<m;q++){
|
||||
double apq=A[p*m+q]; if(fabs(apq)<1e-300) continue;
|
||||
double app=A[p*m+p], aqq=A[q*m+q];
|
||||
double phi=0.5*atan2(2*apq, aqq-app);
|
||||
double c=cos(phi), sn=sin(phi);
|
||||
for(int k=0;k<m;k++){
|
||||
double akp=A[k*m+p], akq=A[k*m+q];
|
||||
A[k*m+p]=c*akp - sn*akq; A[k*m+q]=sn*akp + c*akq;
|
||||
}
|
||||
for(int k=0;k<m;k++){
|
||||
double apk=A[p*m+k], aqk=A[q*m+k];
|
||||
A[p*m+k]=c*apk - sn*aqk; A[q*m+k]=sn*apk + c*aqk;
|
||||
}
|
||||
for(int k=0;k<m;k++){
|
||||
double vkp=V[k*m+p], vkq=V[k*m+q];
|
||||
V[k*m+p]=c*vkp - sn*vkq; V[k*m+q]=sn*vkp + c*vkq;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(int i=0;i<m;i++) w[i]=A[i*m+i];
|
||||
}
|
||||
|
||||
void engram_geo_default_params(GeoParams* p){
|
||||
if(!p) return;
|
||||
p->ann_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;i<n_seeds && dim==0;i++){
|
||||
StoreNode nn; memset(&nn,0,sizeof nn);
|
||||
if(store_get_node(store, seed_ids[i], &nn)==1){
|
||||
if(nn.emb && nn.emb_dim>0) 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;i<n_seeds;i++) ms_upsert(&ms, seed_ids[i], 1.0);
|
||||
int n_seed_members = ms.n;
|
||||
for(int i=0;i<ms.n;i++) ms_load_node(&ms,i,store);
|
||||
|
||||
/* provisional centroid from seed embeddings (for the ANN query) */
|
||||
float* prov = calloc((size_t)dim,sizeof(float));
|
||||
int prov_n=0;
|
||||
for(int i=0;i<n_seed_members;i++) if(ms.emb[i]){
|
||||
for(int d=0;d<dim;d++) prov[d]+=ms.emb[i][d]; prov_n++;
|
||||
}
|
||||
if(prov_n){ for(int d=0;d<dim;d++) prov[d]/=(float)prov_n; }
|
||||
|
||||
/* 2. semantic expansion via vindex ANN around the provisional centroid */
|
||||
if(vindex && vids && P.ann_k>0 && prov_n>0){
|
||||
int k=P.ann_k*(int)n_seeds; if(k<P.ann_k) k=P.ann_k; if(k>n_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<got;r++){
|
||||
if(rids[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<n_seed_members;i++){
|
||||
StoreEdge* es=NULL; size_t ne=0;
|
||||
if(store_get_edges_from(store, ms.id[i], &es, &ne)==0 && es){
|
||||
for(size_t e=0;e<ne;e++){
|
||||
if(es[e].tombstoned || es[e].inhibitory) continue;
|
||||
double w=eff_w(es[e].weight, es[e].hebb);
|
||||
if(w < P.edge_min_weight) continue;
|
||||
int mi=ms_upsert(&ms, es[e].to_id, w);
|
||||
if(mi>=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<ne;e++){
|
||||
if(es[e].tombstoned || es[e].inhibitory) continue;
|
||||
double w=eff_w(es[e].weight, es[e].hebb);
|
||||
if(w < P.edge_min_weight) continue;
|
||||
int mi=ms_upsert(&ms, es[e].from_id, w);
|
||||
if(mi>=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;i<ms.n;i++) if(ms.memb[i]<wv){wv=ms.memb[i];worst=i;}
|
||||
if(worst<0) break;
|
||||
free(ms.id[worst]); free(ms.emb[worst]);
|
||||
ms.id[worst]=ms.id[ms.n-1]; ms.emb[worst]=ms.emb[ms.n-1];
|
||||
ms.memb[worst]=ms.memb[ms.n-1]; ms.sal[worst]=ms.sal[ms.n-1];
|
||||
ms.n--;
|
||||
}
|
||||
}
|
||||
|
||||
int M = ms.n;
|
||||
/* ── final centroid over all embedded members ── */
|
||||
float* centroid=calloc((size_t)dim,sizeof(float));
|
||||
int nemb=0; int* eidx=malloc((size_t)M*sizeof(int));
|
||||
for(int i=0;i<M;i++) if(ms.emb[i]){ eidx[nemb++]=i;
|
||||
for(int d=0;d<dim;d++) centroid[d]+=ms.emb[i][d]; }
|
||||
if(nemb){ for(int d=0;d<dim;d++) centroid[d]/=(float)nemb; }
|
||||
|
||||
/* ── radius + per-member cosine distance to centroid ── */
|
||||
double total_var=0;
|
||||
double* distc=calloc((size_t)M,sizeof(double));
|
||||
/* normalize centroid direction for cosine distances */
|
||||
float* cdir=malloc((size_t)dim*sizeof(float));
|
||||
int have_cdir = (nemb>0 && normcopy(centroid,dim,cdir)==0);
|
||||
for(int i=0;i<M;i++){
|
||||
if(ms.emb[i] && have_cdir){
|
||||
double cs=dotf(ms.emb[i],cdir,dim); if(cs>1)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;j<nemb;j++){
|
||||
int i=eidx[j]; double s=0;
|
||||
for(int d=0;d<dim;d++){ double df=(double)ms.emb[i][d]-centroid[d]; s+=df*df; }
|
||||
total_var+=s;
|
||||
}
|
||||
if(nemb) total_var/=nemb;
|
||||
double radius=sqrt(total_var>0?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;j<m;j++){ int i=eidx[j];
|
||||
for(int d=0;d<dim;d++) Xc[(size_t)j*dim+d]=ms.emb[i][d]-centroid[d]; }
|
||||
double* G=malloc((size_t)m*m*sizeof(double));
|
||||
for(int a=0;a<m;a++) for(int b=a;b<m;b++){
|
||||
double s=0; for(int d=0;d<dim;d++) s+=(double)Xc[(size_t)a*dim+d]*Xc[(size_t)b*dim+d];
|
||||
G[a*m+b]=s; G[b*m+a]=s;
|
||||
}
|
||||
double* w=malloc((size_t)m*sizeof(double));
|
||||
double* V=malloc((size_t)m*m*sizeof(double));
|
||||
jacobi_sym(G,m,w,V);
|
||||
/* sort eigenvalue indices descending */
|
||||
int* ord=malloc((size_t)m*sizeof(int));
|
||||
for(int i=0;i<m;i++) ord[i]=i;
|
||||
for(int a=0;a<m;a++) for(int b=a+1;b<m;b++) if(w[ord[b]]>w[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;t<keep;t++){
|
||||
int c=ord[t];
|
||||
double lam=w[c]; if(lam<0) lam=0;
|
||||
double eigcov = lam/(double)(m-1); /* covariance eigenvalue */
|
||||
/* principal axis in R^dim: a = Xc^T u_c, then unit-normalize */
|
||||
float* ax=calloc((size_t)dim,sizeof(float));
|
||||
for(int d=0;d<dim;d++){ double s=0;
|
||||
for(int j=0;j<m;j++) s+=(double)V[j*m+c]*Xc[(size_t)j*dim+d];
|
||||
ax[d]=(float)s; }
|
||||
double nn=0; for(int d=0;d<dim;d++) nn+=(double)ax[d]*ax[d]; nn=sqrt(nn);
|
||||
if(nn>1e-12) for(int d=0;d<dim;d++) ax[d]=(float)(ax[d]/nn);
|
||||
axes[t].axis=ax; axes[t].extent=sqrt(eigcov);
|
||||
n_axes++;
|
||||
}
|
||||
free(Xc); free(G); free(w); free(V); free(ord);
|
||||
}
|
||||
|
||||
/* ── skeleton: internal hebb edges among members + centrality gradient ── */
|
||||
GeoEdge* edges=NULL; int n_edges=0, cap_e=0;
|
||||
double* centrality=calloc((size_t)M,sizeof(double));
|
||||
int* degree=calloc((size_t)M,sizeof(int));
|
||||
/* co-registration accumulators */
|
||||
double cr_n=0, cr_sx=0, cr_sy=0, cr_sxx=0, cr_syy=0, cr_sxy=0;
|
||||
for(int i=0;i<M;i++){
|
||||
StoreEdge* es=NULL; size_t ne=0;
|
||||
if(store_get_edges_from(store, ms.id[i], &es, &ne)==0 && es){
|
||||
for(size_t e=0;e<ne;e++){
|
||||
if(es[e].tombstoned || es[e].inhibitory) continue;
|
||||
int j=ms_find(&ms, es[e].to_id);
|
||||
if(j<0 || j<=i) continue; /* internal, undirected, i<j only */
|
||||
double w=eff_w(es[e].weight, es[e].hebb);
|
||||
if(w < P.edge_min_weight) continue;
|
||||
if(n_edges==cap_e){ cap_e=cap_e?cap_e*2:32;
|
||||
GeoEdge* t=realloc(edges,(size_t)cap_e*sizeof(GeoEdge)); if(!t) break; edges=t; }
|
||||
edges[n_edges].a=(uint32_t)i; edges[n_edges].b=(uint32_t)j;
|
||||
edges[n_edges].eff_weight=w; edges[n_edges].hebb=es[e].hebb;
|
||||
n_edges++;
|
||||
centrality[i]+=w; centrality[j]+=w; degree[i]++; degree[j]++;
|
||||
/* co-registration: relational strength vs semantic proximity */
|
||||
if(ms.emb[i] && ms.emb[j]){
|
||||
double cs=dotf(ms.emb[i],ms.emb[j],dim); if(cs>1)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;i<M;i++) deg[i]=degree[i];
|
||||
int level=0, remaining=M;
|
||||
while(remaining>0){
|
||||
int progressed=0;
|
||||
for(int i=0;i<M;i++){
|
||||
if(!removed[i] && deg[i]<=level){
|
||||
core[i]=level; removed[i]=1; remaining--; progressed=1;
|
||||
/* decrement neighbors' working degree */
|
||||
for(int e=0;e<n_edges;e++){
|
||||
int o=-1;
|
||||
if((int)edges[e].a==i && !removed[edges[e].b]) o=edges[e].b;
|
||||
else if((int)edges[e].b==i && !removed[edges[e].a]) o=edges[e].a;
|
||||
if(o>=0) deg[o]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!progressed) level++;
|
||||
}
|
||||
free(deg); free(removed);
|
||||
}
|
||||
int k_core=0; for(int i=0;i<M;i++) if(core[i]>k_core) k_core=core[i];
|
||||
|
||||
/* hub = highest centrality (tie-break salience) */
|
||||
int hub=-1; double hv=-1;
|
||||
for(int i=0;i<M;i++){ double v=centrality[i]+1e-6*ms.sal[i];
|
||||
if(v>hv){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;i<M;i++){
|
||||
g->members[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;i<g->n_axes;i++) free(g->axes[i].axis);
|
||||
free(g->axes);
|
||||
for(int i=0;i<g->n_members;i++) free(g->members[i].id);
|
||||
free(g->members); free(g->edges);
|
||||
free(g);
|
||||
}
|
||||
@@ -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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
#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 */
|
||||
Reference in New Issue
Block a user