You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
compute_differentiation_xi() collapses two-vector differentiation to 0 #35
ConsciousnessMetrics::compute_differentiation_xi() collapses the smallest nontrivial case to zero: with exactly two different embedding vectors, it always returns 0.0.
That happens because both signals are computed as variances over pairwise similarities. With two vectors there is only one pair, so both variances are necessarily zero even when the vectors and their Xi signatures are clearly different.
Why this matters:
two-agent / two-cluster consciousness states are a normal constellation shape
downstream callers can report xi = 0 for a differentiated state simply because N = 2
the public API currently erases the first meaningful differentiation case instead of degrading smoothly
Evidence:
src/metrics.rs:203 computes differentiation from pairwise similarity variance and Xi-signature similarity variance
both branches only become positive when there are at least 3 vectors / more than 1 pair
local repro against the current crate:
use consciousness_core::metrics::ConsciousnessMetrics;fnmain(){let a = vec![1.0f32,0.0,0.0,0.0];let b = vec![0.0f32,1.0,0.0,0.0];let xi = ConsciousnessMetrics::compute_differentiation_xi(&[&a,&b],1.0);println!(two_vector_xi={:.6}, xi);}
Observed output:
two_vector_xi=0.000000
Suggested direction:
special-case the 2-vector path instead of relying on variance alone
for example, derive Xi from direct cosine distance / Xi-signature distance when there is only one pair
add a regression test that asserts two clearly different vectors produce a positive differentiation score
Verify-triaged against master (b60f757): reproduces. Left open deliberately — the fix is a call on what the metric should mean.
Confirmed at HEAD, and your diagnosis is exactly right: both signals are variances over pairwise similarities, and the variance of a single sample about its own mean is identically zero. With n == 2 there is one pair, so sim_variance and xi_variance are both 0.0 and the function returns 0.0 for any two vectors, however different.
I fixed the clear-cut contract bugs in this function's neighbourhood in #63 (the [0, 1] clamp, #57/#34) but stopped short of this one, because it is not a guard — it is a decision about what two-vector differentiation is. Your suggestion of deriving it from direct cosine distance when there is only one pair is reasonable, but it makes the metric piecewise: n == 2 would be measuring distance while n >= 3 measures spread, and those are not the same quantity or the same scale. A corpus going from 2 to 3 vectors could then move Xi discontinuously.
Alternatives worth weighing before someone picks:
Special-case n == 2 to 1 - cos_sim (your suggestion) — simple, but piecewise.
Use the uncorrected (sample) variance, dividing by count - 1 — still 0 at one pair, so it does not help here.
Replace variance with mean pairwise distance throughout, which is continuous across all n >= 2 and makes n == 2 fall out naturally — but changes every existing Xi value, so it is a calibration break for anything storing historical Xi.
Option 3 is the one I would advocate, but it is not a change to make without knowing what downstream is comparing against. Flagging for a decision rather than guessing.
Is differentiation Xi measuring spread of pairwise similarities, or distance between them?
Today it's spread (variance), which is why any two vectors return exactly 0.0 — the variance of a single sample about its own mean is identically zero, so the smallest non-trivial case is erased.
Option 1 — special-case n == 2 to 1 - cos_sim. Small diff, but makes the metric piecewise: n == 2 measures distance while n >= 3 measures spread, so Xi can jump discontinuously when a corpus grows from 2 to 3 vectors.
Option 2 — sample variance (count - 1). Doesn't help; still 0 at one pair.
Option 3 — mean pairwise distance throughout, for all n >= 2.
Recommendation: option 3, with eyes open about the cost
It's continuous across all n, n == 2 falls out naturally rather than being special-cased, and it removes the discontinuity option 1 introduces.
The cost is the reason I didn't just do it: it changes every Xi value the crate has ever produced. Anything storing, trending, or thresholding historical Xi needs recalibrating, and I don't know what downstream is comparing against. If there are stored Xi values in HRM snapshots that are compared across time, option 1 may be the pragmatic choice despite being uglier.
Related:#60 compounds this — with two mismatched-dimension vectors you get 0.0 for both reasons at once.
For n == 2, differentiation is the normalized cosine distance between the pair: (1 - cos_sim) / 2, clamped to [0, 1]. For n >= 3, the existing variance definition stands. The special case will be documented on the function.
Rationale
Variance over a single sample is degenerate, not merely small — it is identically zero by definition, so the n == 2 case was not measuring differentiation weakly, it was not measuring it at all. Two vectors is a normal constellation shape (two agents, two clusters), so the public API was erasing its own smallest meaningful input.
(1 - cos_sim) / 2 maps the full legal cosine range [-1, 1] onto [0, 1]: identical vectors give 0, orthogonal 0.5, opposed 1.0. That keeps the n == 2 result on the same scale as the n >= 3 branch and preserves the documented return contract without a second clamp doing real work.
Applied to both signals the function blends — raw embedding similarity and Xi-signature similarity — so the two branches keep the same shape and the xi_weight scaling behaves identically either side of the boundary.
Accepted cost
This is piecewise: n == 2 measures distance, n >= 3 measures spread, and those are different quantities. A corpus growing from 2 to 3 vectors can move Xi discontinuously. I flagged the fully-continuous alternative (mean pairwise distance throughout) earlier and it remains the cleaner metric, but it rewrites every historical Xi value. Taking the piecewise fix trades a discontinuity at one boundary for not invalidating stored data — the right trade while anything is comparing Xi across time.
Activity
fix: contract sweep — serde feature, no_std, NaN guards, doc accuracy (…
NickFlach commentedon Aug 10, 2026
Verify-triaged against master (b60f757): reproduces. Left open deliberately — the fix is a call on what the metric should mean.
Confirmed at HEAD, and your diagnosis is exactly right: both signals are variances over pairwise similarities, and the variance of a single sample about its own mean is identically zero. With
n == 2there is one pair, sosim_varianceandxi_varianceare both 0.0 and the function returns 0.0 for any two vectors, however different.I fixed the clear-cut contract bugs in this function's neighbourhood in #63 (the
[0, 1]clamp, #57/#34) but stopped short of this one, because it is not a guard — it is a decision about what two-vector differentiation is. Your suggestion of deriving it from direct cosine distance when there is only one pair is reasonable, but it makes the metric piecewise:n == 2would be measuring distance whilen >= 3measures spread, and those are not the same quantity or the same scale. A corpus going from 2 to 3 vectors could then move Xi discontinuously.Alternatives worth weighing before someone picks:
n == 2to1 - cos_sim(your suggestion) — simple, but piecewise.count - 1— still 0 at one pair, so it does not help here.n >= 2and makesn == 2fall out naturally — but changes every existing Xi value, so it is a calibration break for anything storing historical Xi.Option 3 is the one I would advocate, but it is not a change to make without knowing what downstream is comparing against. Flagging for a decision rather than guessing.
NickFlach commentedon Aug 10, 2026
Decision needed
Is differentiation Xi measuring spread of pairwise similarities, or distance between them?
Today it's spread (variance), which is why any two vectors return exactly 0.0 — the variance of a single sample about its own mean is identically zero, so the smallest non-trivial case is erased.
n == 2to1 - cos_sim. Small diff, but makes the metric piecewise:n == 2measures distance whilen >= 3measures spread, so Xi can jump discontinuously when a corpus grows from 2 to 3 vectors.count - 1). Doesn't help; still 0 at one pair.n >= 2.Recommendation: option 3, with eyes open about the cost
It's continuous across all
n,n == 2falls out naturally rather than being special-cased, and it removes the discontinuity option 1 introduces.The cost is the reason I didn't just do it: it changes every Xi value the crate has ever produced. Anything storing, trending, or thresholding historical Xi needs recalibrating, and I don't know what downstream is comparing against. If there are stored Xi values in HRM snapshots that are compared across time, option 1 may be the pragmatic choice despite being uglier.
Related: #60 compounds this — with two mismatched-dimension vectors you get 0.0 for both reasons at once.
NickFlach commentedon Aug 10, 2026
Decision (delegated by Nick, 2026-08-10)
For
n == 2, differentiation is the normalized cosine distance between the pair:(1 - cos_sim) / 2, clamped to[0, 1]. Forn >= 3, the existing variance definition stands. The special case will be documented on the function.Rationale
Variance over a single sample is degenerate, not merely small — it is identically zero by definition, so the
n == 2case was not measuring differentiation weakly, it was not measuring it at all. Two vectors is a normal constellation shape (two agents, two clusters), so the public API was erasing its own smallest meaningful input.(1 - cos_sim) / 2maps the full legal cosine range[-1, 1]onto[0, 1]: identical vectors give 0, orthogonal 0.5, opposed 1.0. That keeps then == 2result on the same scale as then >= 3branch and preserves the documented return contract without a second clamp doing real work.Applied to both signals the function blends — raw embedding similarity and Xi-signature similarity — so the two branches keep the same shape and the
xi_weightscaling behaves identically either side of the boundary.Accepted cost
This is piecewise:
n == 2measures distance,n >= 3measures spread, and those are different quantities. A corpus growing from 2 to 3 vectors can move Xi discontinuously. I flagged the fully-continuous alternative (mean pairwise distance throughout) earlier and it remains the cleaner metric, but it rewrites every historical Xi value. Taking the piecewise fix trades a discontinuity at one boundary for not invalidating stored data — the right trade while anything is comparing Xi across time.Implementing now on
fix/metric-decisions.fix(wave): make incomparability explicit, closing the 0.0-sentinel cl…