/* 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 #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; /* 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;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; 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=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) ── * 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;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); } /* ═══════════════════════════════════════════════════════════════════════════ * 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(ncn+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;in_axes;i++) sb_fmt(&b," %.9g", g->axes[i].extent); sb_puts(&b,"\n"); for(int i=0;in_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;inb;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;in;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 ") 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=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=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;ideg[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; oi0 && 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;mn_members;m++) if(g->members[m].membership>=P.cover_membership) ss_add(&claimed,g->members[m].id); /* build record: id = nbhd--, 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;dcentroid[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;kn_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;in;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;in;i++){ RNbhd* r=&ix->nb[i]; for(int m=0;mn_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;ddim;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;ddim;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;in;i++) ((GeoReifyIndex*)ix)->score[i]=0.0; int any=0; for(size_t s=0;sbuckets[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;in;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;dmean[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;in;i++){ RNbhd* r=&ix->nb[i]; if(!r->centroid_unit) continue; double s=0; for(int d=0;dcentroid_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;bnbuckets;b++){ RE* e=ix->buckets[b]; while(e){ RE* x=e->next; free(e); e=x; } } free(ix->buckets); } for(int i=0;in;i++){ RNbhd* r=&ix->nb[i]; free(r->id); free(r->hub_id); for(int m=0;mn_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); }