From a713db9865ab35f23406e459f9349b2028184e15 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 26 Feb 2026 11:05:00 -0500 Subject: [PATCH 1/4] fix: update weights in control points zones --- src/utils/calculateAdaptiveWeights.ts | 30 +++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/utils/calculateAdaptiveWeights.ts b/src/utils/calculateAdaptiveWeights.ts index 9486c2eb..d5775f34 100644 --- a/src/utils/calculateAdaptiveWeights.ts +++ b/src/utils/calculateAdaptiveWeights.ts @@ -61,6 +61,11 @@ export function calculateAdaptiveWeights( learningRate = 0.5, minWeight = 0.01, } = options; + + if (learningRate === 0) { + return weights; + } + const absResiduals = xAbsolute(xSubtract(yData, baseline)); const medAbsRes = xMedian(absResiduals); @@ -72,20 +77,19 @@ export function calculateAdaptiveWeights( rawWeights[i] = Math.exp(-((absResiduals[i] / threshold) ** 2)); } - let maxWeight = Number.MIN_SAFE_INTEGER; - const newWeights = Float64Array.from(weights); const oneMinusLearningRate = 1 - learningRate; - for (let i = 0; i < newWeights.length; i++) { - if (controlPoints && controlPoints[i] > 0) continue; - const weight = Math.max( - minWeight, - oneMinusLearningRate * weights[i] + learningRate * rawWeights[i], - ); - newWeights[i] = weight; - maxWeight = Math.max(maxWeight, weight); + for (let i = 0; i < weights.length; i++) { + let weight = weights[i]; + weight = (oneMinusLearningRate * weight + learningRate * rawWeights[i]) / 4; + + if (controlPoints && controlPoints[i] > 0) { + weight *= 4; + } + + weights[i] = Math.max(weight, minWeight); } - newWeights[0] = maxWeight; - newWeights[weights.length - 1] = maxWeight; + weights[0] = 1; + weights[weights.length - 1] = 1; - return newWeights; + return weights; } From de4f01cf9de98abb02434ffabf5a0497c1bbc352 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 27 Feb 2026 05:43:44 -0500 Subject: [PATCH 2/4] fix(smoothing): remove minWeight to prevent calculation errors The minWeight option was causing inconsistent results in the smoothing algorithm. Removing it ensures the behavior remains predictable across all edge cases --- src/utils/calculateAdaptiveWeights.ts | 15 +++------------ src/x/xWhittakerSmoother.ts | 8 -------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/utils/calculateAdaptiveWeights.ts b/src/utils/calculateAdaptiveWeights.ts index d5775f34..9b349cac 100644 --- a/src/utils/calculateAdaptiveWeights.ts +++ b/src/utils/calculateAdaptiveWeights.ts @@ -23,11 +23,7 @@ export interface CalculateAdaptiveWeightsOptions extends WeightsAndControlPoints * @default 0.5 */ learningRate?: number; - /** - * The minimum allowed weight value to prevent weights from becoming too small. - * @default 0.01 - */ - minWeight?: number; + /** * Factor used to calculate the threshold for determining outliers in the residuals. * Higher values mean more tolerance for outliers. The default value is based on noise follow the normal distribution @@ -55,12 +51,7 @@ export function calculateAdaptiveWeights( weights: NumberArray, options: CalculateAdaptiveWeightsOptions, ) { - const { - controlPoints, - factorStd = 3, - learningRate = 0.5, - minWeight = 0.01, - } = options; + const { controlPoints, factorStd = 3, learningRate = 0.5 } = options; if (learningRate === 0) { return weights; @@ -86,7 +77,7 @@ export function calculateAdaptiveWeights( weight *= 4; } - weights[i] = Math.max(weight, minWeight); + weights[i] = weight; } weights[0] = 1; weights[weights.length - 1] = 1; diff --git a/src/x/xWhittakerSmoother.ts b/src/x/xWhittakerSmoother.ts index 725cebb3..7181314d 100644 --- a/src/x/xWhittakerSmoother.ts +++ b/src/x/xWhittakerSmoother.ts @@ -35,12 +35,6 @@ export interface XWhittakerSmootherOptions extends CalculateAdaptiveWeightsOptio * @default 0.5 */ learningRate?: number; - - /** - * Minimum weight value to avoid division by zero or extremely small weights. - * @default 0.01 - */ - minWeight?: number; } /** @@ -59,7 +53,6 @@ export function xWhittakerSmoother( tolerance = 1e-6, factorStd = 3, learningRate = 0.5, - minWeight = 0.01, } = options; const size = yData.length; @@ -89,7 +82,6 @@ export function xWhittakerSmoother( weights = calculateAdaptiveWeights(yData, newBaseline, weights, { controlPoints, - minWeight, learningRate, factorStd, }); From 11788c28789a295f442f6cc0bd5b6b3f8fc70a53 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 27 Feb 2026 05:58:37 -0500 Subject: [PATCH 3/4] fix: avoid NaN when median in zero --- .../calculateAdaptiveWeights.test.ts | 145 ++++++++++++++++++ src/utils/calculateAdaptiveWeights.ts | 3 +- 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/utils/__tests__/calculateAdaptiveWeights.test.ts diff --git a/src/utils/__tests__/calculateAdaptiveWeights.test.ts b/src/utils/__tests__/calculateAdaptiveWeights.test.ts new file mode 100644 index 00000000..1cc20a07 --- /dev/null +++ b/src/utils/__tests__/calculateAdaptiveWeights.test.ts @@ -0,0 +1,145 @@ +import { expect, test } from 'vitest'; + +import { calculateAdaptiveWeights } from '../calculateAdaptiveWeights.ts'; + +test('basic functionality with default options', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1.1, 2.1, 3.1, 4.1, 5.1]); + const weights = new Float64Array([1, 1, 1, 1, 1]); + + const result = calculateAdaptiveWeights(yData, baseline, weights, {}); + + expect(result).toBeInstanceOf(Float64Array); + expect(result.length).toBe(5); + // First and last weights should be 1 + expect(result[0]).toBe(1); + expect(result[4]).toBe(1); + // Middle weights should be updated + expect(result[1]).toBeLessThan(1); + expect(result[2]).toBeLessThan(1); + expect(result[3]).toBeLessThan(1); +}); + +test('learning rate = 0 returns same weights', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1.1, 2.1, 3.1, 4.1, 5.1]); + const weights = new Float64Array([1, 1, 1, 1, 1]); + + const result = calculateAdaptiveWeights(yData, baseline, weights, { + learningRate: 0, + }); + + expect(result).toEqual(weights); +}); + +test('high learning rate affects weights more', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1.1, 2.1, 3.1, 4.1, 5.1]); + const weights1 = new Float64Array([1, 1, 1, 1, 1]); + const weights2 = new Float64Array([1, 1, 1, 1, 1]); + + calculateAdaptiveWeights(yData, baseline, weights1, { learningRate: 0.3 }); + calculateAdaptiveWeights(yData, baseline, weights2, { learningRate: 0.8 }); + + // Higher learning rate should result in more different weights from original + for (let i = 1; i < 4; i++) { + expect(Math.abs(1 - weights2[i])).toBeGreaterThan( + Math.abs(1 - weights1[i]), + ); + } +}); + +test('custom factorStd affects weight threshold', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1.1, 2.1, 3.1, 4.1, 5.1]); + const weights1 = new Float64Array([1, 1, 1, 1, 1]); + const weights2 = new Float64Array([1, 1, 1, 1, 1]); + + calculateAdaptiveWeights(yData, baseline, weights1, { factorStd: 2 }); + calculateAdaptiveWeights(yData, baseline, weights2, { factorStd: 5 }); + + // Different factorStd should produce different results + let different = false; + for (let i = 1; i < 4; i++) { + if (weights1[i] !== weights2[i]) { + different = true; + break; + } + } + expect(different).toBe(true); +}); + +test('control points increase weights', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1.1, 2.1, 3.1, 4.1, 5.1]); + const weights1 = new Float64Array([1, 1, 1, 1, 1]); + const weights2 = new Float64Array([1, 1, 1, 1, 1]); + + calculateAdaptiveWeights(yData, baseline, weights1, {}); + calculateAdaptiveWeights(yData, baseline, weights2, { + controlPoints: new Float64Array([0, 1, 0, 1, 0]), + }); + + // Weights at control points (index 1 and 3) should be higher + expect(weights2[1]).toBeGreaterThan(weights1[1]); + expect(weights2[3]).toBeGreaterThan(weights1[3]); + // Other weights should remain similar + expect(weights2[2]).toBeCloseTo(weights1[2], 3); +}); + +test('perfect fit (baseline equals yData)', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1, 2, 3, 4, 5]); + const weights = new Float64Array([1, 1, 1, 1, 1]); + + const result = calculateAdaptiveWeights(yData, baseline, weights, {}); + + expect(result).toBeInstanceOf(Float64Array); + expect(result[0]).toBe(1); + expect(result[4]).toBe(1); + // With perfect fit, weights should be high (close to 1 after normalization) + for (let i = 1; i < 4; i++) { + expect(result[i]).toBeGreaterThan(0.2); + } +}); + +test('large residuals reduce weights', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1, 2, 10, 4, 5]); // Large residual at index 2 + const weights = new Float64Array([1, 1, 1, 1, 1]); + + const result = calculateAdaptiveWeights(yData, baseline, weights, {}); + + expect(result[0]).toBe(1); + expect(result[4]).toBe(1); + // The weight at the large residual point should be reduced + expect(result[2]).toBeLessThan(result[1]); +}); + +test('modifies input weights array', () => { + const yData = new Float64Array([1, 2, 3, 4, 5]); + const baseline = new Float64Array([1.1, 2.1, 3.1, 4.1, 5.1]); + const weights = new Float64Array([1, 1, 1, 1, 1]); + + const result = calculateAdaptiveWeights(yData, baseline, weights, {}); + + // Should return the same array (modified in place) + expect(result).toBe(weights); +}); + +test('works with different array types', () => { + const yData = [1, 2, 3, 4, 5]; + const baseline = [1.1, 2.1, 3.1, 4.1, 5.1]; + const weights = [1, 1, 1, 1, 1]; + + const result = calculateAdaptiveWeights( + yData as any, + baseline as any, + weights as any, + {}, + ); + + expect(result).toBeDefined(); + expect(result[0]).toBe(1); + expect(result[4]).toBe(1); +}); diff --git a/src/utils/calculateAdaptiveWeights.ts b/src/utils/calculateAdaptiveWeights.ts index 9b349cac..2b4cade6 100644 --- a/src/utils/calculateAdaptiveWeights.ts +++ b/src/utils/calculateAdaptiveWeights.ts @@ -61,7 +61,7 @@ export function calculateAdaptiveWeights( const medAbsRes = xMedian(absResiduals); const mad = 1.4826 * medAbsRes; - const threshold = factorStd * mad; + const threshold = mad > 0 ? factorStd * mad : 1; const rawWeights = new Float64Array(absResiduals.length); for (let i = 0; i < absResiduals.length; i++) { @@ -81,6 +81,5 @@ export function calculateAdaptiveWeights( } weights[0] = 1; weights[weights.length - 1] = 1; - return weights; } From 43fbb5d8bbab9fab9847f1e0f8fef2b33945a9c1 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Wed, 4 Mar 2026 22:57:17 -0500 Subject: [PATCH 4/4] chore: fix eslint in test case --- src/utils/__tests__/calculateAdaptiveWeights.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/__tests__/calculateAdaptiveWeights.test.ts b/src/utils/__tests__/calculateAdaptiveWeights.test.ts index 1cc20a07..14c8fe2f 100644 --- a/src/utils/__tests__/calculateAdaptiveWeights.test.ts +++ b/src/utils/__tests__/calculateAdaptiveWeights.test.ts @@ -10,7 +10,7 @@ test('basic functionality with default options', () => { const result = calculateAdaptiveWeights(yData, baseline, weights, {}); expect(result).toBeInstanceOf(Float64Array); - expect(result.length).toBe(5); + expect(result).toHaveLength(5); // First and last weights should be 1 expect(result[0]).toBe(1); expect(result[4]).toBe(1); @@ -29,7 +29,7 @@ test('learning rate = 0 returns same weights', () => { learningRate: 0, }); - expect(result).toEqual(weights); + expect(result).toStrictEqual(weights); }); test('high learning rate affects weights more', () => { @@ -66,6 +66,7 @@ test('custom factorStd affects weight threshold', () => { break; } } + expect(different).toBe(true); }); @@ -97,6 +98,7 @@ test('perfect fit (baseline equals yData)', () => { expect(result).toBeInstanceOf(Float64Array); expect(result[0]).toBe(1); expect(result[4]).toBe(1); + // With perfect fit, weights should be high (close to 1 after normalization) for (let i = 1; i < 4; i++) { expect(result[i]).toBeGreaterThan(0.2); @@ -124,7 +126,7 @@ test('modifies input weights array', () => { const result = calculateAdaptiveWeights(yData, baseline, weights, {}); // Should return the same array (modified in place) - expect(result).toBe(weights); + expect(result).toStrictEqual(weights); }); test('works with different array types', () => {