/* 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;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); }