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

141 lines
3.4 KiB
TypeScript
Raw Normal View History

/**
* 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);
});
});