velxio/frontend/src/__tests__/spice-rectifier-integration...

157 lines
5.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.

/**
* Integration test for the Half-Wave Rectifier example.
*
* Exercises the full pipeline:
* storeAdapter → picks `.tran` because `signal-generator` is non-DC
* NetlistBuilder → emits SIN source + diode + load
* ngspice → returns v(out) waveform with clipped negative half
* CircuitScheduler → populates timeWaveforms in the result
*
* Bug this test guards: before the Phase 1-2 fix, `.op` evaluated SIN at t=0
* and returned 0 V, so `analogRead(A0)` read 0 in a loop — the user symptom.
*/
import { describe, it, expect } from 'vitest';
import { buildInputFromStore } from '../simulation/spice/storeAdapter';
import { solveInput } from './helpers/solveInput';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
function rectifierSnapshot() {
return {
components: [
{
id: 'sg1',
metadataId: 'signal-generator',
properties: { waveform: 'sine', frequency: 50, amplitude: 5, offset: 0 },
},
{ id: 'd1', metadataId: 'diode-1n4007', properties: {} },
{ id: 'rl', metadataId: 'resistor', properties: { value: '1000' } },
],
wires: [
{
id: 'w1',
start: { componentId: 'sg1', pinName: 'SIG' },
end: { componentId: 'd1', pinName: 'A' },
},
{
id: 'w2',
start: { componentId: 'd1', pinName: 'C' },
end: { componentId: 'rl', pinName: '1' },
},
{
id: 'w3',
start: { componentId: 'rl', pinName: '2' },
end: { componentId: 'arduino-uno', pinName: 'GND' },
},
{
id: 'w4',
start: { componentId: 'sg1', pinName: 'GND' },
end: { componentId: 'arduino-uno', pinName: 'GND' },
},
{
id: 'w5',
start: { componentId: 'd1', pinName: 'C' },
end: { componentId: 'arduino-uno', pinName: 'A0' },
},
],
boards: [
{
id: 'arduino-uno',
boardKind: 'arduino-uno' as const,
pinStates: {},
},
],
};
}
describe('half-wave rectifier — end-to-end pipeline', () => {
it('storeAdapter picks .tran and the netlist contains SIN + .tran cards', () => {
const input = buildInputFromStore(rectifierSnapshot());
expect(input.analysis.kind).toBe('tran');
const { netlist } = buildNetlist(input);
expect(netlist).toMatch(/SIN\(/);
expect(netlist).toMatch(/\.tran\s/);
});
it('CircuitScheduler returns a v(A0-net) waveform with positive peaks and clipped trough', async () => {
const input = buildInputFromStore(rectifierSnapshot());
const result = await solveInput(input);
expect(result.converged).toBe(true);
expect(result.analysisMode).toBe('tran');
expect(result.timeWaveforms).toBeDefined();
const { timeWaveforms, pinNetMap } = result;
if (!timeWaveforms) return;
// The A0 pin maps to the diode cathode / resistor 1 net (through the
// Union-Find). Look it up through pinNetMap rather than guessing.
const a0Net = pinNetMap.get('arduino-uno:A0');
expect(a0Net).toBeDefined();
if (!a0Net) return;
const samples = timeWaveforms.nodes.get(a0Net);
expect(samples).toBeDefined();
if (!samples) return;
// Drop the first period so the RC/decoupling transient doesn't bias the
// statistics — we want steady-state behavior.
const periodSamples = Math.floor(samples.length / 4); // ≈ one 50 Hz period
const steady = samples.slice(periodSamples);
const vMax = Math.max(...steady);
const vMin = Math.min(...steady);
// A real 1N4007 drops ~0.7-0.9 V, so the peak is around 5 0.8 ≈ 4.2 V.
// SPICE's default D model may give slightly different Vf; allow a loose
// range but demand we see a real positive swing.
expect(vMax).toBeGreaterThan(3.0);
// Reverse bias: the diode blocks, so the load voltage stays at/near 0.
expect(vMin).toBeGreaterThan(-0.2);
// The rectified waveform must have *both* positive excursions and near-
// zero valleys within one steady-state window — otherwise we're back to
// the `.op` bug where everything sits at one DC value.
const highs = steady.filter((v) => v > 1.5).length;
const lows = steady.filter((v) => v < 0.2).length;
expect(highs).toBeGreaterThanOrEqual(5);
expect(lows).toBeGreaterThanOrEqual(5);
}, 30_000);
it('all-DC circuit → .op, no timeWaveforms (regression guard for the >20 DC examples)', async () => {
const input = buildInputFromStore({
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '1000' } },
{ id: 'led', metadataId: 'led', properties: { color: 'red' } },
],
wires: [
{
id: 'w1',
start: { componentId: 'arduino-uno', pinName: '13' },
end: { componentId: 'r1', pinName: '1' },
},
{
id: 'w2',
start: { componentId: 'r1', pinName: '2' },
end: { componentId: 'led', pinName: 'A' },
},
{
id: 'w3',
start: { componentId: 'led', pinName: 'C' },
end: { componentId: 'arduino-uno', pinName: 'GND' },
},
],
boards: [
{
id: 'arduino-uno',
boardKind: 'arduino-uno' as const,
pinStates: { '13': { type: 'digital', v: 5 } },
},
],
});
expect(input.analysis.kind).toBe('op');
const result = await solveInput(input);
expect(result.analysisMode).toBe('op');
expect(result.timeWaveforms).toBeUndefined();
}, 30_000);
});