velxio/frontend/src/__tests__/waveform-stats.test.ts

141 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Unit tests for `waveformStats.ts` — the time-domain helpers that instrument
* components (Voltmeter, Ammeter, Overlay) use to display AC values.
*/
import { describe, it, expect } from 'vitest';
import {
rms,
mean,
peak,
peakToPeak,
isAC,
subtract,
interpolateAt,
} from '../simulation/spice/waveformStats';
/** Generate `n` samples of a sine wave with amplitude `amp` and DC offset `dc`. */
function sine(n: number, amp: number, dc = 0): number[] {
const out: number[] = [];
for (let i = 0; i < n; i++) {
out.push(dc + amp * Math.sin((2 * Math.PI * i) / n));
}
return out;
}
describe('rms', () => {
it('returns 0 for an empty array', () => {
expect(rms([])).toBe(0);
});
it('returns |x| for a single DC sample', () => {
expect(rms([5])).toBe(5);
expect(rms([-3])).toBe(3);
});
it('returns |x| for a constant waveform', () => {
expect(rms([2, 2, 2, 2])).toBe(2);
});
it('returns amp/√2 for a pure sine (classic AC RMS)', () => {
const samples = sine(1024, 5);
expect(rms(samples)).toBeCloseTo(5 / Math.sqrt(2), 2);
});
it('returns sqrt(amp²/2 + dc²) for sine + DC offset', () => {
const samples = sine(1024, 5, 3);
const expected = Math.sqrt(25 / 2 + 9);
expect(rms(samples)).toBeCloseTo(expected, 2);
});
});
describe('mean', () => {
it('returns 0 for an empty array', () => {
expect(mean([])).toBe(0);
});
it('returns the arithmetic mean', () => {
expect(mean([1, 2, 3, 4])).toBe(2.5);
});
it('returns ≈ 0 for a pure centered sine', () => {
expect(mean(sine(1024, 5))).toBeCloseTo(0, 2);
});
it('recovers the DC offset of a biased sine', () => {
expect(mean(sine(1024, 5, 3))).toBeCloseTo(3, 2);
});
});
describe('peak', () => {
it('returns max absolute value', () => {
expect(peak([1, -3, 2, -0.5])).toBe(3);
});
it('returns 0 for an empty array', () => {
expect(peak([])).toBe(0);
});
it('returns amplitude for a pure sine', () => {
expect(peak(sine(1024, 4.2))).toBeCloseTo(4.2, 2);
});
});
describe('peakToPeak', () => {
it('returns max min', () => {
expect(peakToPeak([1, -3, 2, -0.5])).toBe(5);
});
it('returns 0 for constant waveform', () => {
expect(peakToPeak([7, 7, 7])).toBe(0);
});
});
describe('isAC', () => {
it('is false for constant DC', () => {
expect(isAC([3.3, 3.3, 3.3])).toBe(false);
});
it('is false for very small ripple below epsilon', () => {
expect(isAC([3.3, 3.30001, 3.3], 1e-4)).toBe(false);
});
it('is true for audible AC', () => {
expect(isAC(sine(256, 1))).toBe(true);
});
});
describe('subtract', () => {
it('element-wise subtracts same-length arrays', () => {
expect(subtract([5, 6, 7], [1, 2, 3])).toEqual([4, 4, 4]);
});
it('clips to the shorter length', () => {
expect(subtract([5, 6, 7, 8], [1, 2])).toEqual([4, 4]);
});
});
describe('interpolateAt', () => {
const ts = [0, 1, 2, 3, 4];
const vs = [0, 2, 4, 6, 8]; // linear y=2t
it('returns exact value at a knot', () => {
expect(interpolateAt(ts, vs, 2)).toBe(4);
});
it('linearly interpolates between knots', () => {
expect(interpolateAt(ts, vs, 1.5)).toBe(3);
});
it('clamps below the first knot', () => {
expect(interpolateAt(ts, vs, -99)).toBe(0);
});
it('clamps above the last knot', () => {
expect(interpolateAt(ts, vs, 999)).toBe(8);
});
it('handles empty arrays', () => {
expect(interpolateAt([], [], 5)).toBe(0);
});
});