/* 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 #include #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 the member embeddings in the * frame the descriptor operated in. When centered * (global_mean != NULL) this is the CENTERED * centroid (mean of L2-normalized embs minus the * global mean): the neighborhood's location in the * isotropic/whitened frame. Add global_mean back to * recover the raw prototype point. When uncentered * it is the raw mean of L2-normalized member embs. */ float* global_mean; /* the centering offset actually applied (dim floats), * or NULL if the descriptor ran in raw space. The §5 * operators (distance/overlap/Wasserstein) are only * discriminative in the centered frame — see notes. */ /* ── 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); /* ── Global-mean cache (mean-centering / whitening the anisotropic emb space) ── * The nomic-embed-text space over the engram corpus is strongly ANISOTROPIC: * every embedding sits in a narrow cone (mean pairwise cosine ~0.55), which * compresses cosine-based domain separation almost to nothing. Subtracting the * GLOBAL MEAN of the (L2-normalized) embeddings recenters the cloud on the * origin (mean pairwise cosine -> ~0), restoring isotropy so the §5 operators * discriminate. The mean is a store-level derived quantity, like the ANN index: * built once from the paged store, cached, and refreshed when the embedded set * drifts. It lives here (not in the store) so this stays a contained, read-only * addition; a runtime owns one GeoMeanCache per open store alongside its VIndex. */ typedef struct GeoMeanCache GeoMeanCache; /* Scan every live node in `store` and compute the mean of the L2-normalized * embeddings over the embed-eligible set (nodes carrying an emb vector; the * unembedded telemetry/system nodes are skipped). Returns a malloc'd cache, or * NULL on error / no embedded nodes. The offset vector is NOT renormalized — it * is a translation, applied by subtraction. */ GeoMeanCache* engram_geo_mean_build(EngramPagedStore* store); /* The cached offset (dim floats) — pass to engram_geometry_descriptor as * global_mean. Valid until the cache is freed/refreshed. */ const float* engram_geo_mean_vec(const GeoMeanCache* c); int engram_geo_mean_dim(const GeoMeanCache* c); uint64_t engram_geo_mean_count(const GeoMeanCache* c); /* #embedded nodes used */ /* Recompute the mean IN PLACE iff the embedded-node count has drifted by more * than `frac` (e.g. 0.10 = 10%) since the cache was built — "recompute on * significant change". Returns 1 if it rebuilt, 0 if unchanged, <0 on error. */ int engram_geo_mean_maybe_refresh(GeoMeanCache* c, EngramPagedStore* store, double frac); void engram_geo_mean_free(GeoMeanCache* c); /* 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. * global_mean — optional centering offset (dim floats, from engram_geo_mean_*). * When non-NULL the SEMANTIC geometry is computed in mean-centered * (isotropic) space: every normalized member emb has global_mean * subtracted before the centroid / cosine-distance / co-registration * math, so those operators discriminate. NULL = raw space (legacy). * NOTE: the ANN neighbor query still runs in RAW unit-vector space — * centering is a rigid translation that ~preserves neighborhood * MEMBERSHIP, so the index needs no rebuild; only the descriptor * STATISTICS move to the centered frame (co-registration choice (b)). * The eigen/covariance shape (axes, radius) is translation-invariant * and therefore identical in either frame. * 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, const float* global_mean); void engram_geo_free(GeoDescriptor* g); #endif /* ENGRAM_GEOMETRY_H */