Files
el/lang/runtime/engram_geometry.c
T
will.anderson f6a0777f90 M10: reify dense neighborhoods into first-class persisted records; geometry-priming reads them (default OFF)
Reification, not a cache. Densely co-wired relational neighborhoods are crystallized
into DURABLE first-class store records that survive restart, load on boot, and evolve
via supersede+provenance -- so the geometry-priming hot path READS persisted structure
instead of computing a per-query descriptor (the M9 3.2x/13x latency blocker).

engram_geometry.{h,c}:
  - engram_geo_reify_store(): detect hub-anchored neighborhoods on the hebb-weighted
    graph (greedy non-redundant cover), compute each centered descriptor ONCE against
    the true store-wide mean, persist as node_type="Neighborhood" (raw centroid in emb,
    membership+scalars+axis-extents in a compact GEO1 metadata schema) + member edges,
    superseding any prior same-hub record. The mean is persisted once as "GeoMeanFrame".
  - resident loaded form (index_new/add/finalize/lookup): parses the durable records at
    boot (never recomputes geometry); O(seeds) membership lookup, miss -> centroid-nearest.
  - descriptor: skip the Jacobi eigensolve when top_axes==0; reject structural records as
    members (id-convention + node_type guards) so re-reify/ad-hoc stay clean.

el_runtime.c:
  - boot routes Neighborhood/GeoMeanFrame records OUT of the activation graph into the
    reify index, and skips member edges from adjacency -> ENGRAM_GEOMETRY_PRIMING OFF is
    byte-identical to M8/M9 (verified across 15 queries).
  - priming hot path reads the persisted membership; ENGRAM_GEO_PRIMING_NOCACHE=1 keeps
    the M9 per-query descriptor for ad-hoc geometries / A/B control.

A/B on a COPY (128 neighborhoods): priming ON is now FLAT latency (1.06x median / 1.03x
p90 vs OFF) where the M9 per-query path is 3.30x/10.5x. Reified records provably inert
when OFF. Restart survival + supersede verified. Build 0 warnings (my code); ASan/UBSan
clean on module and full server hot path. Recall quality re-eval against the TRUE mean
still shows no reliable gain (mean coherence -0.017), so geometry-priming STAYS default-OFF
-- but the latency blocker is removed and the durable structure now exists. See
docs/architecture/design/engram-m10-reification.md.
2026-08-12 23:06:49 -05:00

940 lines
42 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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>
#include <stdio.h>
#include <stdarg.h>
#include <time.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;
}
/* ── mean-centered cosine geometry (whitening the anisotropic emb space) ──────
* All three operate on RAW (unnormalized-here, but member embs are already unit)
* vectors, subtracting the global-mean offset `gm` on the fly. With gm all-zeros
* they reduce EXACTLY to the raw unit-space cosine — so the same code path serves
* both the centered and legacy-raw modes. */
static double cnorm2(const float* a, const float* gm, int dim){
double s=0; for(int d=0;d<dim;d++){ double v=(double)a[d]-gm[d]; s+=v*v; } return s;
}
static double cdot(const float* a, const float* b, const float* gm, int dim){
double s=0; for(int d=0;d<dim;d++){ double av=(double)a[d]-gm[d], bv=(double)b[d]-gm[d]; s+=av*bv; }
return s;
}
static double ccos(const float* a, const float* b, const float* gm, int dim){
double na=sqrt(cnorm2(a,gm,dim)), nb=sqrt(cnorm2(b,gm,dim));
if(na<1e-12||nb<1e-12) return 0.0;
double c=cdot(a,b,gm,dim)/(na*nb); if(c>1)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;d<dim;d++) s+=((double)a[d]-gm[d])*(double)dir[d];
double c=s/na; if(c>1)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;
/* The reified records (Neighborhood / GeoMeanFrame) carry an emb (centroid / mean)
* but are STRUCTURE, not corpus content — they must never pollute the store-wide
* mean, the hub scan, or the descriptor. One predicate, used everywhere. */
static int geo_is_reified_type(const char* nt){
return nt && (strcmp(nt,ENGRAM_GEO_NBHD_TYPE)==0 ||
strcmp(nt,ENGRAM_GEO_MEANFRAME_TYPE)==0);
}
/* Identify a structural record by its id convention (no store read needed), so the
* descriptor never admits a reified Neighborhood / GeoMeanFrame as a neighborhood
* MEMBER even when the ANN index or adjacency still references it (re-reify/refresh
* on a store that already holds reified records; ad-hoc descriptors alike). */
static int geo_is_structural_id(const char* id){
if(!id) return 0;
if(strcmp(id,ENGRAM_GEO_MEANFRAME_ID)==0) return 1;
size_t p=strlen(ENGRAM_GEO_NBHD_ID_PREFIX);
return strncmp(id,ENGRAM_GEO_NBHD_ID_PREFIX,p)==0;
}
static void geo_mean_cb(const StoreNode* n, void* ctx){
GeoMeanAcc* a=ctx; if(a->err) return;
if(geo_is_reified_type(n->node_type)) return; /* skip structural records */
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;d<a->dim;d++) s+=(double)n->emb[d]*n->emb[d];
double nn=sqrt(s); if(nn<1e-12) return; /* skip ~zero */
for(int d=0;d<a->dim;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;d<a.dim;d++) c->mean[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;d<a.dim;d++) nm[d]=(float)(a.sum[d]/(double)a.n);
free(a.sum); free(c->mean);
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;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,
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;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 */
/* 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;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;
if(geo_is_structural_id(vids[rids[r]])) continue; /* never a member */
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;
if(es[e].relation && strcmp(es[e].relation,ENGRAM_GEO_MEMBER_RELATION)==0) continue;
if(geo_is_structural_id(es[e].to_id)) 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;
if(es[e].relation && strcmp(es[e].relation,ENGRAM_GEO_MEMBER_RELATION)==0) continue;
if(geo_is_structural_id(es[e].from_id)) 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 (CENTERED frame) ── */
double total_var=0;
double* distc=calloc((size_t)M,sizeof(double));
/* centered centroid (= raw centroid - global mean) and its unit direction */
float* ccen=malloc((size_t)dim*sizeof(float));
for(int d=0;d<dim;d++) ccen[d]=centroid[d]-GM[d];
float* cdir=malloc((size_t)dim*sizeof(float));
int have_cdir = (nemb>0 && normcopy(ccen,dim,cdir)==0);
for(int i=0;i<M;i++){
if(ms.emb[i] && have_cdir){
double cs=ccos_dir(ms.emb[i],GM,cdir,dim);
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) ──
* Skipped entirely when top_axes==0: the eigensolve is the dominant cost, and
* priming needs only members+membership, so reified records that don't want the
* ellipsoid pass top_axes=0 and pay nothing here (centroid+radius still filled). */
int n_axes=0; GeoAxis* axes=NULL;
if(P.top_axes>0 && 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
* (semantic proximity measured in the CENTERED frame). */
if(ms.emb[i] && ms.emb[j]){
double cs=ccos(ms.emb[i],ms.emb[j],GM,dim);
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 = 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;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); 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;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);
}
/* ═══════════════════════════════════════════════════════════════════════════
* M10 — REIFICATION: persist / load / lookup first-class neighborhood records.
* ═══════════════════════════════════════════════════════════════════════════ */
static int64_t geo_now_ms(void){
struct timespec ts;
if(clock_gettime(CLOCK_REALTIME,&ts)==0)
return (int64_t)ts.tv_sec*1000 + ts.tv_nsec/1000000;
return (int64_t)time(NULL)*1000;
}
void engram_geo_reify_default_params(GeoReifyParams* p){
if(!p) return;
p->min_weighted_degree=0;
p->max_neighborhoods=128;
p->cover_membership=0.5;
p->persist_member_edges=1;
engram_geo_default_params(&p->descriptor);
p->descriptor.top_axes=4; /* keep a small ellipsoid summary; cheap */
p->descriptor.max_members=256; /* reified neighborhoods stay compact */
}
/* ── tiny growable string builder ─────────────────────────────────────────── */
typedef struct { char* s; size_t n, cap; } SB;
static int sb_reserve(SB* b, size_t add){
if(b->n+add+1<=b->cap) return 0;
size_t nc=b->cap?b->cap:256; while(nc<b->n+add+1) nc*=2;
char* t=realloc(b->s,nc); if(!t) return -1; b->s=t; b->cap=nc; return 0;
}
static int sb_puts(SB* b, const char* s){
size_t l=strlen(s); if(sb_reserve(b,l)) return -1;
memcpy(b->s+b->n,s,l); b->n+=l; b->s[b->n]=0; return 0;
}
static int sb_fmt(SB* b, const char* fmt, ...){
char tmp[512]; va_list ap; va_start(ap,fmt);
int k=vsnprintf(tmp,sizeof tmp,fmt,ap); va_end(ap);
if(k<0) return -1; if(k>=(int)sizeof tmp) k=sizeof tmp-1;
return sb_puts(b,tmp);
}
/* Serialize a descriptor's DURABLE geometry into the GEO1 metadata schema.
* (The raw centroid is stored separately as the record's emb.) */
static char* geo_nbhd_metadata(const GeoDescriptor* g, const char* hub,
const char* meanid){
SB b={0};
if(sb_puts(&b,"GEO1\n")) { free(b.s); return NULL; }
sb_fmt(&b,"hub %s\n", hub?hub:"");
sb_fmt(&b,"mean %s\n", meanid?meanid:"");
sb_fmt(&b,"s %.9g %.9g %d %.9g %d %d\n",
g->radius, g->total_variance, g->k_core, g->co_registration,
g->n_embedded, g->n_members);
sb_puts(&b,"e");
for(int i=0;i<g->n_axes;i++) sb_fmt(&b," %.9g", g->axes[i].extent);
sb_puts(&b,"\n");
for(int i=0;i<g->n_members;i++){
sb_fmt(&b,"m %s %.9g %.9g %d\n",
g->members[i].id, g->members[i].membership,
g->members[i].centrality, g->members[i].core);
}
return b.s; /* caller frees */
}
/* ── string set (greedy-cover claimed ids) + string→id list (hub→old nbhd) ──── */
static uint64_t geo_djb2(const char* s){
uint64_t h=5381; for(;*s;s++) h=((h<<5)+h)^(unsigned char)*s; return h;
}
typedef struct SSNode { char* key; struct SSNode* next; } SSNode;
typedef struct { SSNode** b; size_t nb; } SSet;
static void ss_init(SSet* s, size_t nb){ s->nb=nb; s->b=calloc(nb,sizeof*s->b); }
static int ss_has(const SSet* s, const char* k){
if(!s->b) return 0; SSNode* n=s->b[geo_djb2(k)%s->nb];
for(;n;n=n->next) if(strcmp(n->key,k)==0) return 1; return 0;
}
static void ss_add(SSet* s, const char* k){
if(!s->b||ss_has(s,k)) return; size_t i=geo_djb2(k)%s->nb;
SSNode* n=malloc(sizeof*n); if(!n) return; n->key=strdup(k); n->next=s->b[i]; s->b[i]=n;
}
static void ss_free(SSet* s){
if(!s->b) return;
for(size_t i=0;i<s->nb;i++){ SSNode* n=s->b[i]; while(n){ SSNode* x=n->next; free(n->key); free(n); n=x; } }
free(s->b); s->b=NULL;
}
typedef struct { char** id; int n, cap; } StrVec;
static void sv_push(StrVec* v, const char* s){
if(v->n==v->cap){ v->cap=v->cap?v->cap*2:64; v->id=realloc(v->id,(size_t)v->cap*sizeof*v->id); }
v->id[v->n++]=strdup(s);
}
static void sv_free(StrVec* v){ for(int i=0;i<v->n;i++) free(v->id[i]); free(v->id); }
/* pass 1 collector: all non-structural node ids; also record existing Neighborhood
* records as (hub -> old_id) so a re-reify supersedes the prior version. */
typedef struct {
StrVec cand; /* candidate node ids (content nodes) */
StrVec old_hub, old_id; /* parallel: existing nbhd hub + its id */
} ReifyScan;
static void geo_reify_scan_cb(const StoreNode* n, void* ctx){
ReifyScan* rs=ctx; if(!n->id||!n->node_type) { if(n->id) sv_push(&rs->cand,n->id); return; }
if(strcmp(n->node_type,ENGRAM_GEO_NBHD_TYPE)==0){
/* parse hub from metadata GEO1 (line "hub <id>") for supersede lineage */
const char* md=n->metadata?n->metadata:"";
const char* p=strstr(md,"hub ");
if(p && (p==md || p[-1]=='\n')){
p+=4; const char* e=p; while(*e && *e!='\n') e++;
char* hub=strndup(p,(size_t)(e-p));
sv_push(&rs->old_hub,hub); sv_push(&rs->old_id,n->id); free(hub);
}
return; /* structural: not a candidate */
}
if(strcmp(n->node_type,ENGRAM_GEO_MEANFRAME_TYPE)==0) return;
sv_push(&rs->cand,n->id);
}
/* weighted strong-edge degree of a node (from+to), matching eff_w/threshold. */
static double geo_weighted_degree(EngramPagedStore* st, const char* id, double emin){
double deg=0; StoreEdge* es=NULL; size_t ne=0;
if(store_get_edges_from(st,id,&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>=emin) deg+=w; }
}
store_edges_free(es,ne); es=NULL; ne=0;
if(store_get_edges_to(st,id,&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>=emin) deg+=w; }
}
store_edges_free(es,ne);
return deg;
}
int engram_geo_reify_store(EngramPagedStore* store, VIndex* vindex,
char** vids, int n_vids,
const GeoReifyParams* params){
if(!store) return -1;
GeoReifyParams P; if(params) P=*params; else engram_geo_reify_default_params(&P);
/* 1. true store-wide mean → persist the GeoMeanFrame record (once). */
GeoMeanCache* mc=engram_geo_mean_build(store);
if(!mc) return -2;
int dim=engram_geo_mean_dim(mc);
const float* mean=engram_geo_mean_vec(mc);
int64_t now=geo_now_ms();
{ StoreNode mf; memset(&mf,0,sizeof mf);
mf.id=(char*)ENGRAM_GEO_MEANFRAME_ID; mf.node_type=(char*)ENGRAM_GEO_MEANFRAME_TYPE;
mf.content=(char*)"geo-mean-frame"; mf.tier=(char*)"Semantic"; mf.metadata=(char*)"{}";
mf.emb=(float*)mean; mf.emb_dim=dim; mf.created_at=now; mf.updated_at=now;
if(store_put_node(store,&mf)<0){ engram_geo_mean_free(mc); return -3; }
}
/* 2. scan: candidate ids + existing (hub→old id) for supersede. */
ReifyScan rs; memset(&rs,0,sizeof rs);
if(store_scan_nodes(store,geo_reify_scan_cb,&rs)<0){
sv_free(&rs.cand); sv_free(&rs.old_hub); sv_free(&rs.old_id);
engram_geo_mean_free(mc); return -4;
}
/* 3. weighted degree per candidate; sort desc. */
int N=rs.cand.n;
double* deg=malloc((size_t)N*sizeof(double));
int* ord=malloc((size_t)N*sizeof(int));
for(int i=0;i<N;i++){ deg[i]=geo_weighted_degree(store,rs.cand.id[i],P.descriptor.edge_min_weight); ord[i]=i; }
/* simple insertion-ish selection sort by degree desc (N a few thousand, one-time) */
for(int a=0;a<N;a++){ int best=a; for(int b=a+1;b<N;b++) if(deg[ord[b]]>deg[ord[best]]) best=b;
int t=ord[a]; ord[a]=ord[best]; ord[best]=t; }
/* 4. greedy non-redundant cover: reify each qualifying hub once. */
SSet claimed; ss_init(&claimed, (size_t)(N>16?N:16));
int persisted=0;
for(int oi=0; oi<N && persisted<P.max_neighborhoods; oi++){
int i=ord[oi]; const char* hub=rs.cand.id[i];
if(P.min_weighted_degree>0 && deg[i]<(double)P.min_weighted_degree) break; /* sorted: rest smaller */
if(ss_has(&claimed,hub)) continue;
const char* seeds[1]={hub};
GeoDescriptor* g=engram_geometry_descriptor(store,vindex,vids,n_vids,
seeds,1,&P.descriptor,mean);
if(!g || g->n_members<=0){ if(g) engram_geo_free(g); continue; }
/* claim members above cover threshold (incl. the hub itself) */
for(int m=0;m<g->n_members;m++)
if(g->members[m].membership>=P.cover_membership) ss_add(&claimed,g->members[m].id);
/* build record: id = nbhd-<hub>-<now>, emb = RAW centroid = centered+mean */
char nid[512]; snprintf(nid,sizeof nid,"%s%s-%lld",ENGRAM_GEO_NBHD_ID_PREFIX,hub,(long long)now);
float* raw=NULL;
if(g->n_embedded>0 && g->centroid && g->global_mean){
raw=malloc((size_t)dim*sizeof(float));
if(raw) for(int d=0;d<dim;d++) raw[d]=g->centroid[d]+g->global_mean[d];
}
char* md=geo_nbhd_metadata(g,hub,ENGRAM_GEO_MEANFRAME_ID);
StoreNode nn; memset(&nn,0,sizeof nn);
nn.id=nid; nn.node_type=(char*)ENGRAM_GEO_NBHD_TYPE;
nn.content=(char*)"reified-neighborhood"; nn.tier=(char*)"Semantic";
nn.metadata=md?md:(char*)"{}"; nn.emb=raw; nn.emb_dim=raw?dim:0;
nn.created_at=now; nn.updated_at=now;
int wrc=store_put_node(store,&nn);
free(raw); free(md);
if(wrc<0){ engram_geo_free(g); continue; }
/* provenance: supersede any prior neighborhood for this hub. */
for(int k=0;k<rs.old_hub.n;k++) if(strcmp(rs.old_hub.id[k],hub)==0){
store_supersede(store, rs.old_id.id[k], nid);
}
/* member links (durable, but inert to activation — runtime skips them). */
if(P.persist_member_edges){
for(int m=0;m<g->n_members;m++){
char eid[600]; snprintf(eid,sizeof eid,"%s->%s",nid,g->members[m].id);
StoreEdge se; memset(&se,0,sizeof se);
se.id=eid; se.from_id=nid; se.to_id=g->members[m].id;
se.relation=(char*)ENGRAM_GEO_MEMBER_RELATION;
se.metadata=(char*)"{}"; se.weight=g->members[m].membership;
se.confidence=1.0; se.created_at=now; se.updated_at=now;
store_put_edge(store,&se);
}
}
engram_geo_free(g);
persisted++;
}
ss_free(&claimed);
free(deg); free(ord);
sv_free(&rs.cand); sv_free(&rs.old_hub); sv_free(&rs.old_id);
engram_geo_mean_free(mc);
return persisted;
}
/* ═══════════════ resident loaded form + hot-path lookup ═════════════════════ */
typedef struct {
char* id;
char* hub_id;
int n_members;
char** member_ids;
double* member_w;
double radius, co_reg;
int k_core, n_embedded;
float* centroid_raw; /* dim floats or NULL */
float* centroid_unit; /* centered+normalized (finalize) or NULL */
int dim;
GeoNeighborhood view;
} RNbhd;
typedef struct RE { char* id; int nbhd; double w; struct RE* next; } RE;
struct GeoReifyIndex {
RNbhd* nb; int n, cap;
float* mean; int mean_dim;
RE** buckets; size_t nbuckets;
double* score; /* scratch[n], reused per lookup */
};
GeoReifyIndex* engram_geo_reify_index_new(void){
GeoReifyIndex* ix=calloc(1,sizeof*ix); return ix;
}
/* parse a GEO1 metadata blob into an RNbhd (members + scalars). */
static int geo_parse_nbhd(const char* md, RNbhd* r){
if(!md) return -1;
if(strncmp(md,"GEO1",4)!=0) return -1;
/* count member lines to size arrays */
int cap=0; for(const char* p=md; (p=strstr(p,"\nm ")); p+=3) cap++;
r->member_ids=cap?calloc((size_t)cap,sizeof(char*)):NULL;
r->member_w =cap?calloc((size_t)cap,sizeof(double)):NULL;
r->n_members=0;
const char* line=md;
while(line && *line){
const char* nl=strchr(line,'\n');
size_t len= nl? (size_t)(nl-line) : strlen(line);
char buf[600]; if(len>=sizeof buf) len=sizeof buf-1;
memcpy(buf,line,len); buf[len]=0;
if(buf[0]=='h'&&buf[1]=='u'&&buf[2]=='b'&&buf[3]==' '){
free(r->hub_id); r->hub_id=strdup(buf+4);
} else if(buf[0]=='s'&&buf[1]==' '){
int kc=0,ne=0,nm=0; double rad=0,tv=0,cr=0;
sscanf(buf+2,"%lf %lf %d %lf %d %d",&rad,&tv,&kc,&cr,&ne,&nm);
r->radius=rad; r->co_reg=cr; r->k_core=kc; r->n_embedded=ne;
} else if(buf[0]=='m'&&buf[1]==' '){
char mid[512]; double w=0,c=0; int core=0;
if(sscanf(buf+2,"%511s %lf %lf %d",mid,&w,&c,&core)>=2 && r->member_ids){
r->member_ids[r->n_members]=strdup(mid);
r->member_w[r->n_members]=w;
r->n_members++;
}
}
line = nl? nl+1 : NULL;
}
return 0;
}
int engram_geo_reify_index_add(GeoReifyIndex* ix, const StoreNode* n){
if(!ix||!n||!n->node_type) return 0;
if(strcmp(n->node_type,ENGRAM_GEO_MEANFRAME_TYPE)==0){
if(n->emb && n->emb_dim>0){
free(ix->mean);
ix->mean=malloc((size_t)n->emb_dim*sizeof(float));
if(ix->mean){ memcpy(ix->mean,n->emb,(size_t)n->emb_dim*sizeof(float)); ix->mean_dim=n->emb_dim; }
}
return 0;
}
if(strcmp(n->node_type,ENGRAM_GEO_NBHD_TYPE)!=0) return 0;
if(ix->n==ix->cap){ ix->cap=ix->cap?ix->cap*2:16;
RNbhd* t=realloc(ix->nb,(size_t)ix->cap*sizeof*t); if(!t) return -1; ix->nb=t; }
RNbhd* r=&ix->nb[ix->n]; memset(r,0,sizeof*r);
r->id=strdup(n->id?n->id:"");
if(geo_parse_nbhd(n->metadata,r)!=0){ free(r->id); return 0; } /* skip malformed */
if(n->emb && n->emb_dim>0){
r->dim=n->emb_dim;
r->centroid_raw=malloc((size_t)n->emb_dim*sizeof(float));
if(r->centroid_raw) memcpy(r->centroid_raw,n->emb,(size_t)n->emb_dim*sizeof(float));
}
ix->n++;
return 0;
}
int engram_geo_reify_index_finalize(GeoReifyIndex* ix){
if(!ix) return -1;
/* member → neighborhood hash */
size_t total=0; for(int i=0;i<ix->n;i++) total+=(size_t)ix->nb[i].n_members;
ix->nbuckets = total? (total*2+1) : 1;
ix->buckets=calloc(ix->nbuckets,sizeof(RE*));
if(!ix->buckets) return -1;
for(int i=0;i<ix->n;i++){
RNbhd* r=&ix->nb[i];
for(int m=0;m<r->n_members;m++){
size_t b=geo_djb2(r->member_ids[m])%ix->nbuckets;
RE* e=malloc(sizeof*e); if(!e) continue;
e->id=r->member_ids[m]; e->nbhd=i; e->w=r->member_w[m]; e->next=ix->buckets[b]; ix->buckets[b]=e;
}
/* centered, normalized centroid for the nearest-fallback */
if(r->centroid_raw && ix->mean && ix->mean_dim==r->dim){
r->centroid_unit=malloc((size_t)r->dim*sizeof(float));
if(r->centroid_unit){
double nrm=0; for(int d=0;d<r->dim;d++){ double v=(double)r->centroid_raw[d]-ix->mean[d]; r->centroid_unit[d]=(float)v; nrm+=v*v; }
nrm=sqrt(nrm); if(nrm>1e-12) for(int d=0;d<r->dim;d++) r->centroid_unit[d]=(float)(r->centroid_unit[d]/nrm);
else { free(r->centroid_unit); r->centroid_unit=NULL; }
}
}
/* fill the borrowed view */
r->view.id=r->id; r->view.hub_id=r->hub_id; r->view.n_members=r->n_members;
r->view.member_ids=r->member_ids; r->view.member_w=r->member_w;
r->view.radius=r->radius; r->view.co_registration=r->co_reg;
r->view.k_core=r->k_core; r->view.n_embedded=r->n_embedded;
}
ix->score=ix->n?calloc((size_t)ix->n,sizeof(double)):NULL;
return 0;
}
static void geo__reify_load_cb(const StoreNode* n, void* ctx){
engram_geo_reify_index_add((GeoReifyIndex*)ctx, n);
}
GeoReifyIndex* engram_geo_reify_load(EngramPagedStore* store){
if(!store) return NULL;
GeoReifyIndex* ix=engram_geo_reify_index_new(); if(!ix) return NULL;
store_scan_nodes(store, geo__reify_load_cb, ix);
if(ix->n==0 && ix->mean==NULL){ engram_geo_reify_index_free(ix); return NULL; }
engram_geo_reify_index_finalize(ix);
return ix;
}
const GeoNeighborhood* engram_geo_reify_lookup(
const GeoReifyIndex* ix,
const char* const* seed_ids, size_t n_seeds,
const float* q_emb, int q_dim){
if(!ix||ix->n<=0) return NULL;
/* (a) membership route: score each neighborhood by summed seed membership. */
if(ix->score && ix->buckets && seed_ids && n_seeds>0){
for(int i=0;i<ix->n;i++) ((GeoReifyIndex*)ix)->score[i]=0.0;
int any=0;
for(size_t s=0;s<n_seeds;s++){
const char* id=seed_ids[s]; if(!id) continue;
for(RE* e=ix->buckets[geo_djb2(id)%ix->nbuckets]; e; e=e->next)
if(strcmp(e->id,id)==0){ ((GeoReifyIndex*)ix)->score[e->nbhd]+=e->w; any=1; }
}
if(any){
int best=-1; double bv=-1;
for(int i=0;i<ix->n;i++) if(ix->score[i]>bv){ bv=ix->score[i]; best=i; }
if(best>=0 && bv>0) return &ix->nb[best].view;
}
}
/* (b) centroid-nearest fallback (centered query vs centered centroids). */
if(q_emb && q_dim>0 && ix->mean && ix->mean_dim==q_dim){
double nq=0; float* cq=malloc((size_t)q_dim*sizeof(float));
if(!cq) return NULL;
for(int d=0;d<q_dim;d++){ double v=(double)q_emb[d]-ix->mean[d]; cq[d]=(float)v; nq+=v*v; }
nq=sqrt(nq);
if(nq>1e-12){
int best=-1; double bc=-1e9;
for(int i=0;i<ix->n;i++){ RNbhd* r=&ix->nb[i]; if(!r->centroid_unit) continue;
double s=0; for(int d=0;d<q_dim;d++) s+=(double)cq[d]*r->centroid_unit[d];
s/=nq; if(s>bc){ bc=s; best=i; } }
free(cq);
if(best>=0) return &ix->nb[best].view;
} else free(cq);
}
return NULL;
}
int engram_geo_reify_count(const GeoReifyIndex* ix){ return ix?ix->n:0; }
const float* engram_geo_reify_mean(const GeoReifyIndex* ix, int* dim){
if(!ix||!ix->mean){ if(dim)*dim=0; return NULL; }
if(dim)*dim=ix->mean_dim; return ix->mean;
}
void engram_geo_reify_index_free(GeoReifyIndex* ix){
if(!ix) return;
if(ix->buckets){
for(size_t b=0;b<ix->nbuckets;b++){ RE* e=ix->buckets[b]; while(e){ RE* x=e->next; free(e); e=x; } }
free(ix->buckets);
}
for(int i=0;i<ix->n;i++){ RNbhd* r=&ix->nb[i];
free(r->id); free(r->hub_id);
for(int m=0;m<r->n_members;m++) free(r->member_ids[m]);
free(r->member_ids); free(r->member_w);
free(r->centroid_raw); free(r->centroid_unit);
}
free(ix->nb); free(ix->score); free(ix->mean);
free(ix);
}