Files
el/lang/runtime/engram_geometry.h
T
will.anderson 8cae0f94eb M9 refinement: operate geometry descriptor in mean-centered (isotropic) embedding space
The nomic-embed-text space over the corpus is strongly anisotropic (mean
pairwise cosine ~0.55), which compresses cosine-based domain separation almost
to nothing so the design-doc s5 operators (distance/overlap/Wasserstein) cannot
discriminate. Subtracting the global mean of the normalized embeddings restores
isotropy (mean pairwise cosine ~0) and sharpens the operators.

- add GeoMeanCache (engram_geo_mean_build / _maybe_refresh / _vec / _free): a
  store-derived centering offset over the embed-eligible set, cached and
  refreshed on significant drift; lives in geometry.c, not the store.
- engram_geometry_descriptor gains an optional global_mean: when supplied the
  centroid, per-member cosine distance, and co-registration run in centered
  space (GM=zeros reproduces the legacy raw path exactly).
- co-registration choice (b): the ANN query stays in raw unit space (index
  unchanged) since centering is a rigid translation that ~preserves neighborhood
  membership; only the descriptor statistics move to the centered frame.
  Covariance/axes/radius are translation-invariant and therefore unchanged.
- test: synthetic ground-truth suite stays green (PERF + ASan/UBSan), plus new
  centered/raw/mean-cache assertions.
- add bench_discrimination.c (env-gated, read-only, skips in CI): on a copy of
  the real store the two-domain overlap operator drops 1.13 -> 0.008 and
  cross-centroid cosine 0.899 -> 0.003 after centering, Euclid distance
  unchanged (translation-invariant control).

No change to activation/retrieval behavior; wiring geometry into retrieval is a
separate, behavior-changing cutover.
2026-08-12 19:54:41 -05:00

162 lines
9.0 KiB
C

/* 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 <stddef.h>
#include <stdint.h>
#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 */