/* 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 #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;i1)c=1; if(c<-1)c=-1; return c; } /* cosine of (a-gm) against a pre-centered UNIT direction `dir`. */ static double ccos_dir(const float* a, const float* gm, const float* dir, int dim){ double na=sqrt(cnorm2(a,gm,dim)); if(na<1e-12) return 0.0; double s=0; for(int d=0;d1)c=1; if(c<-1)c=-1; return c; } /* ───────────────────────── global-mean cache ─────────────────────────────── * Store-derived centering offset: the mean of the L2-normalized embeddings over * the embed-eligible set. See engram_geometry.h for the anisotropy rationale. */ struct GeoMeanCache { float* mean; int dim; uint64_t n; }; typedef struct { double* sum; int dim; uint64_t n; int err; } GeoMeanAcc; static void geo_mean_cb(const StoreNode* n, void* ctx){ GeoMeanAcc* a=ctx; if(a->err) return; if(!(n->emb && n->emb_dim>0)) return; /* skip unembedded */ if(a->dim==0){ a->dim=n->emb_dim; a->sum=calloc((size_t)a->dim,sizeof(double)); if(!a->sum){ a->err=1; return; } } if(n->emb_dim!=a->dim) return; /* skip off-dim */ double s=0; for(int d=0;ddim;d++) s+=(double)n->emb[d]*n->emb[d]; double nn=sqrt(s); if(nn<1e-12) return; /* skip ~zero */ for(int d=0;ddim;d++) a->sum[d]+=(double)n->emb[d]/nn; a->n++; } typedef struct { uint64_t n; } GeoCntAcc; static void geo_cnt_cb(const StoreNode* n, void* ctx){ if(n->emb && n->emb_dim>0) ((GeoCntAcc*)ctx)->n++; } GeoMeanCache* engram_geo_mean_build(EngramPagedStore* store){ if(!store) return NULL; GeoMeanAcc a; memset(&a,0,sizeof a); if(store_scan_nodes(store,geo_mean_cb,&a)<0){ free(a.sum); return NULL; } if(a.err || a.n==0 || !a.sum){ free(a.sum); return NULL; } GeoMeanCache* c=calloc(1,sizeof*c); if(!c){ free(a.sum); return NULL; } c->mean=malloc((size_t)a.dim*sizeof(float)); if(!c->mean){ free(a.sum); free(c); return NULL; } for(int d=0;dmean[d]=(float)(a.sum[d]/(double)a.n); c->dim=a.dim; c->n=a.n; free(a.sum); return c; } const float* engram_geo_mean_vec(const GeoMeanCache* c){ return c?c->mean:NULL; } int engram_geo_mean_dim(const GeoMeanCache* c){ return c?c->dim:0; } uint64_t engram_geo_mean_count(const GeoMeanCache* c){ return c?c->n:0; } int engram_geo_mean_maybe_refresh(GeoMeanCache* c, EngramPagedStore* store, double frac){ if(!c||!store) return -1; GeoCntAcc cn={0}; if(store_scan_nodes(store,geo_cnt_cb,&cn)<0) return -1; double base=(double)(c->n?c->n:1); double drift=fabs((double)cn.n-(double)c->n)/base; if(drift<=frac) return 0; /* no significant change */ GeoMeanAcc a; memset(&a,0,sizeof a); if(store_scan_nodes(store,geo_mean_cb,&a)<0){ free(a.sum); return -1; } if(a.err || a.n==0 || !a.sum){ free(a.sum); return -1; } float* nm=malloc((size_t)a.dim*sizeof(float)); if(!nm){ free(a.sum); return -1; } for(int d=0;dmean); c->mean=nm; c->dim=a.dim; c->n=a.n; return 1; } void engram_geo_mean_free(GeoMeanCache* c){ if(c){ free(c->mean); free(c); } } /* 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;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, const float* global_mean) { 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 */ /* Centering offset. When a global_mean is supplied the semantic cosine math * runs in mean-centered (isotropic) space; otherwise GM is an all-zeros * vector so the identical code path reproduces raw unit-space cosines. */ int centered = (global_mean != NULL); float* zeros = NULL; const float* GM; if(centered) GM = global_mean; else { zeros = calloc((size_t)dim,sizeof(float)); if(!zeros) return NULL; GM = zeros; } MemSet ms; if(ms_init(&ms,dim)!=0){ ms_free(&ms); free(zeros); 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(ccen,dim,cdir)==0); for(int i=0;i0?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;d=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 = ccen; /* CENTERED centroid; transfer ownership */ free(centroid); if(centered){ g->global_mean = malloc((size_t)dim*sizeof(float)); if(g->global_mean) memcpy(g->global_mean, GM, (size_t)dim*sizeof(float)); } else g->global_mean = NULL; 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); free(zeros); ms_free(&ms); return g; } void engram_geo_free(GeoDescriptor* g){ if(!g) return; free(g->hub_id); free(g->centroid); free(g->global_mean); 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); }