UNPKG

@claude-flow/embeddings

Version:

V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking

203 lines (182 loc) • 7.35 kB
/** * ADR-121 Phase 12 — averageEmbeddings (HyDE) tests. * * Coverage: * - Single vector returns (a normalized copy of) the input * - N orthogonal unit vectors average to their geometric centroid * - normalizeOutput=true produces a unit vector * - normalizeOutput=false returns the raw mean * - normalizeInputs=true makes magnitude irrelevant (only direction matters) * - normalizeInputs=false → larger-magnitude vectors dominate * - weights bias the result toward higher-weighted vectors * - all-zero weights returns zero vector (no NaN) * - dimension mismatch throws * - empty input throws * - negative weights throw * - weights length mismatch throws * - isUnitNorm helper correctness */ import { describe, it, expect } from 'vitest'; import { averageEmbeddings, isUnitNorm } from '../embedding-fusion.js'; function vec(n: number, fill: (i: number) => number): Float32Array { const v = new Float32Array(n); for (let i = 0; i < n; i++) v[i] = fill(i); return v; } describe('averageEmbeddings — basic contract', () => { it('throws on empty vectors array', () => { expect(() => averageEmbeddings([])).toThrow(/non-empty/); }); it('throws on dimension mismatch', () => { expect(() => averageEmbeddings([ new Float32Array([1, 0, 0]), new Float32Array([0, 1]), ])).toThrow(/dim/); }); it('throws on weights length mismatch', () => { expect(() => averageEmbeddings( [new Float32Array([1, 0]), new Float32Array([0, 1])], { weights: [1] }, )).toThrow(/weights\.length/); }); it('throws on negative weights', () => { expect(() => averageEmbeddings( [new Float32Array([1, 0])], { weights: [-1] }, )).toThrow(/negative/); }); }); describe('averageEmbeddings — single vector', () => { it('single vector returns the same direction (normalized)', () => { const v = new Float32Array([3, 0, 0, 0]); const out = averageEmbeddings([v]); expect(out[0]).toBeCloseTo(1, 6); expect(out[1]).toBeCloseTo(0, 6); expect(isUnitNorm(out)).toBe(true); }); it('does not mutate the input vector', () => { const v = new Float32Array([3, 0, 0, 0]); const copy = new Float32Array(v); averageEmbeddings([v]); expect(v).toEqual(copy); }); }); describe('averageEmbeddings — multi-vector centroid', () => { it('two orthogonal unit vectors average to their normalized midpoint', () => { const a = new Float32Array([1, 0, 0, 0]); const b = new Float32Array([0, 1, 0, 0]); const out = averageEmbeddings([a, b], { normalizeInputs: true, normalizeOutput: true }); // Mean = [0.5, 0.5, 0, 0]; norm = sqrt(0.5) ≈ 0.7071; // normalized = [0.7071, 0.7071, 0, 0] expect(out[0]).toBeCloseTo(Math.SQRT1_2, 5); expect(out[1]).toBeCloseTo(Math.SQRT1_2, 5); expect(out[2]).toBeCloseTo(0, 6); expect(isUnitNorm(out)).toBe(true); }); it('three vectors at orthogonal axes give the (1,1,1)/sqrt(3) centroid', () => { const a = new Float32Array([1, 0, 0]); const b = new Float32Array([0, 1, 0]); const c = new Float32Array([0, 0, 1]); const out = averageEmbeddings([a, b, c]); const v = 1 / Math.sqrt(3); expect(out[0]).toBeCloseTo(v, 5); expect(out[1]).toBeCloseTo(v, 5); expect(out[2]).toBeCloseTo(v, 5); }); }); describe('averageEmbeddings — normalizeInputs', () => { it('normalizeInputs=true: magnitude does not matter, only direction', () => { // Two vectors pointing the same way but with very different // magnitudes — with normalization, they're equivalent. const a = new Float32Array([10, 0, 0]); // big mag, x-axis const b = new Float32Array([0.001, 0, 0]); // tiny mag, x-axis const out = averageEmbeddings([a, b], { normalizeInputs: true, normalizeOutput: true }); expect(out[0]).toBeCloseTo(1, 5); expect(out[1]).toBeCloseTo(0, 5); }); it('normalizeInputs=false: larger-magnitude vectors dominate', () => { // Without normalization, the big-mag vector should pull the // result strongly toward its direction. const big = new Float32Array([100, 0, 0]); const small = new Float32Array([0, 1, 0]); const out = averageEmbeddings([big, small], { normalizeInputs: false, normalizeOutput: true, }); // Mean = [50, 0.5, 0]; normalized: x ≈ 0.99995, y ≈ 0.00999 expect(out[0]).toBeGreaterThan(0.99); expect(out[1]).toBeLessThan(0.05); }); }); describe('averageEmbeddings — weights', () => { it('uniform weights match default behavior', () => { const a = new Float32Array([1, 0, 0]); const b = new Float32Array([0, 1, 0]); const def = averageEmbeddings([a, b]); const uni = averageEmbeddings([a, b], { weights: [1, 1] }); expect(uni[0]).toBeCloseTo(def[0]!, 6); expect(uni[1]).toBeCloseTo(def[1]!, 6); }); it('weight bias shifts the result toward the heavier vector', () => { const a = new Float32Array([1, 0, 0]); const b = new Float32Array([0, 1, 0]); const out = averageEmbeddings([a, b], { weights: [10, 1] }); // With 10:1 bias, x-component should dominate expect(out[0]).toBeGreaterThan(out[1]!); }); it('weight=0 effectively removes a vector', () => { const a = new Float32Array([1, 0, 0]); const b = new Float32Array([0, 1, 0]); const out = averageEmbeddings([a, b], { weights: [1, 0] }); expect(out[0]).toBeCloseTo(1, 5); expect(out[1]).toBeCloseTo(0, 5); }); it('all-zero weights returns zero vector without NaN', () => { const a = new Float32Array([1, 0, 0]); const out = averageEmbeddings([a], { weights: [0] }); expect(out[0]).toBe(0); expect(Number.isNaN(out[0])).toBe(false); }); }); describe('averageEmbeddings — normalizeOutput', () => { it('normalizeOutput=true produces a unit vector', () => { const out = averageEmbeddings([ new Float32Array([1, 0, 0]), new Float32Array([0, 1, 0]), ], { normalizeOutput: true }); expect(isUnitNorm(out)).toBe(true); }); it('normalizeOutput=false returns the raw mean', () => { const out = averageEmbeddings([ new Float32Array([1, 0, 0]), new Float32Array([0, 1, 0]), ], { normalizeOutput: false, normalizeInputs: true }); // Mean of two unit vectors at right angles: [0.5, 0.5, 0] expect(out[0]).toBeCloseTo(0.5, 5); expect(out[1]).toBeCloseTo(0.5, 5); expect(isUnitNorm(out)).toBe(false); }); }); describe('averageEmbeddings — number[] input', () => { it('accepts number[] just like Float32Array', () => { const out = averageEmbeddings([[1, 0, 0], [0, 1, 0]]); expect(out[0]).toBeCloseTo(Math.SQRT1_2, 5); expect(out[1]).toBeCloseTo(Math.SQRT1_2, 5); }); }); describe('isUnitNorm', () => { it('true for unit vectors', () => { expect(isUnitNorm(new Float32Array([1, 0, 0]))).toBe(true); expect(isUnitNorm(new Float32Array([Math.SQRT1_2, Math.SQRT1_2]))).toBe(true); }); it('false for non-unit vectors', () => { expect(isUnitNorm(new Float32Array([2, 0, 0]))).toBe(false); expect(isUnitNorm(new Float32Array([0, 0, 0]))).toBe(false); }); it('respects tolerance', () => { // Within default 1e-5 tolerance: expect(isUnitNorm(new Float32Array([1 + 1e-7, 0, 0]))).toBe(true); // Outside: expect(isUnitNorm(new Float32Array([1.01, 0, 0]))).toBe(false); }); });