Skip to content

compute_differentiation_xi() collapses two-vector differentiation to 0 #35

Closed

Description

@NickFlach

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;

fn main() {
    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

Activity

  1. NickFlach commented on Aug 10, 2026

    @NickFlach
    CollaboratorAuthor

    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:

    1. Special-case n == 2 to 1 - cos_sim (your suggestion) — simple, but piecewise.
    2. Use the uncorrected (sample) variance, dividing by count - 1 — still 0 at one pair, so it does not help here.
    3. 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.

  2. NickFlach commented on Aug 10, 2026

    @NickFlach
    CollaboratorAuthor

    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.

    • 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.

  3. NickFlach commented on Aug 10, 2026

    @NickFlach
    CollaboratorAuthor

    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]. 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.

    Implementing now on fix/metric-decisions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions