Repository navigation
fix(wave): make incomparability explicit, closing the 0.0-sentinel class - #69
Merged
Merged
Conversation
The audit found more than #67 originally described. cosine_similarity collapsed FOUR distinct undefined conditions onto the same 0.0 that means "orthogonal": length mismatch, empty input, zero magnitude, and non-finite magnitude. A zero vector has no direction and a NaN-bearing vector has no meaningful magnitude — neither is orthogonal to anything, yet all four arrived at the caller as a plausible measurement. xi_repulsive_force had the same shape with worse polarity: its 0.0 means NO repulsion, so incomparable signatures read as identical. It failed toward "these are the same thing". Sharpest consequence, now rejected: a zero-magnitude embedding scored 0.0 against every peer, which reads as orthogonal to everything, i.e. maximal differentiation. A corpus containing an empty embedding did not merely lose information — it reported INFLATED Xi. Checked variants rather than changing the existing signatures. The delegation allowed Result "if blast radius allows like #65", and in-crate it does, but the comparison does not hold: compute_differentiation_xi had zero production callers, whereas cosine_similarity is the crate's hottest numeric primitive and downstream scores whole candidate sets with it in inner loops. Forcing `?` there to handle what is a caller bug taxes every well-behaved caller. So try_cosine_similarity / try_xi_repulsive_force are additive, and every in-crate production path moved to them — cosine_similarity is now absent from non-test code in metrics.rs, with a comment saying its reappearance would be the regression. Raw embeddings and Xi signatures needed DIFFERENT policies, which the first pass got wrong. compute_xi_signature legitimately returns the zero vector whenever the nonlinear commutator cancels — [1, 0] is enough — so propagating ZeroMagnitude from the Xi pass rejected perfectly ordinary axis-aligned corpora. Six tests caught it. Xi-signature comparison now tolerates ZeroMagnitude as 0.0; raw-embedding comparison propagates it. Deliberately not done, both documented rather than silently decided: - No debug_assert! in the lossy helpers. It would turn a silent wrong number into a panic in every downstream debug build — a behaviour change imposed on consumers, not a hardening. - A structureless Xi signature still scores 0.0 against peers, which reads as "maximally different in Xi" and nudges xi_variance up: the same inflation shape this closes for raw embeddings. Arguably it should be 1.0 (identically featureless). Changing it recalibrates Xi for every axis-aligned corpus, the same cost that kept #35 piecewise, so it is flagged on xi_signature_similarity for an explicit decision. Tests 94 -> 103. The ZeroMagnitude distinction is confirmed anti-vacuous: folding it back to Ok(0.0) fails three tests, and genuine orthogonality is asserted to remain Ok(0.0) rather than an error, so the checked form cannot pass by rejecting everything. Additive and non-breaking; 0.5.0 -> 0.6.0 for the new API and changed documented policy. XiError is now #[non_exhaustive] — it shipped in 0.5.0 one day ago, so no downstream match should exist yet. Fixes #67 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the delegated decision on #67. Do not merge — for review. Decision posted on the issue before any code was written.
The audit found more than the issue described
I filed #67 against one conflation.
cosine_similarityactually collapsed four distinct undefined conditions onto the same0.0that means "orthogonal":f32xi_repulsive_forcehad the same shape with worse polarity: its0.0means no repulsion, so incomparable signatures read as identical — failing toward "these are the same thing".The sharpest consequence, now rejected
A zero-magnitude embedding scored
0.0against every peer, which reads as orthogonal to everything — i.e. maximal differentiation. So a corpus containing one empty embedding didn't merely lose information, it reported inflated Xi. That's the same inversion class as #60, one layer down.Checked variants, not changed signatures
The decision allowed
Result"if blast radius allows like #65". In-crate it does — but the comparison to #65 doesn't hold, and I'd rather say so than quietly diverge.compute_differentiation_xihad zero production callers, so changing its signature cost nothing.cosine_similarityis the opposite: the crate's hottest numeric primitive, called from downstream inner loops that score whole candidate sets. Forcing?there to handle what is a caller bug taxes every well-behaved caller.So
try_cosine_similarity/try_xi_repulsive_forceare additive, and every in-crate production path moved to them.cosine_similarityno longer appears in non-test code inmetrics.rs, with a comment marking its reappearance as the regression.The two passes needed different policies — my first attempt got this wrong
compute_xi_signaturelegitimately returns the zero vector whenever the nonlinear commutator cancels, and[1, 0]is enough to trigger it. PropagatingZeroMagnitudefrom the Xi-signature pass therefore rejected perfectly ordinary axis-aligned corpora. Six tests caught it.Final policy: raw-embedding comparison propagates
ZeroMagnitude(an absent embedding is caller error); Xi-signature comparison tolerates it as0.0(a structureless signature is a real state). Both documented onxi_signature_similarity.Deliberately not done — documented, not silently decided
debug_assert!in the lossy helpers. It was floated for compute_differentiation_xi() turns mixed-dimension embeddings into xi=0 #60's legacy path. It would turn a silent wrong number into a panic in every downstream debug build — a behaviour change imposed on consumers who may have legitimately ragged data today, not a hardening.0.0against peers, which reads as "maximally different in Xi" and nudgesxi_varianceup — the very inflation shape this PR removes for raw embeddings. Arguably two structureless signatures should score1.0(identically featureless). Changing it recalibrates Xi for every axis-aligned corpus, the same cost that kept compute_differentiation_xi() collapses two-vector differentiation to 0 #35 piecewise, so it's flagged in-code for an explicit decision rather than folded into a hardening pass. Worth a look if you want this closed properly.Changes
Added:
wave::VectorCompareError,wave::try_cosine_similarity,metrics::try_xi_repulsive_force,XiSignature::try_repulsive_force,XiError::Incomparable+From<VectorCompareError>.Changed:
compute_differentiation_xirejects zero-magnitude and non-finite embeddings;XiErroris now#[non_exhaustive](it shipped in 0.5.0 one day ago, so no downstream match should exist yet);xi_diversity_boost's pass-through on incomparable signatures is now stated policy rather than an accident of0.0falling through both tier thresholds — numeric behaviour unchanged.Additive and non-breaking for existing signatures. 0.5.0 → 0.6.0 for the new API and changed documented policy.
Gates
Anti-vacuous, verified by mutation: folding
ZeroMagnitudeback toOk(0.0)fails three tests. And the suite asserts genuine orthogonality staysOk(0.0)rather than becoming an error — so the checked form cannot pass by rejecting everything, which is the obvious way this kind of test goes vacuous.🤖 Generated with Claude Code