/** * Reproduce the live-app failure of the "Half-Wave Rectifier" example. * * This test recreates every layer of Velxio's runtime pipeline so we can * pinpoint which step fails when `analogRead(A0)` always returns 0 in the * running app: * * L1. buildInputFromStore — does the adapter pick `.tran`? * L2. buildNetlist — does the netlist have SIN + diode? * does pinNetMap contain `arduino-uno:A0`? * L3. runNetlist (ngspice) — does the solve converge? produce a * rectified waveform on the A0 net? * L4. CircuitScheduler.solveNow — does the result propagate with * `timeWaveforms` populated? * L5. interpolation — does interpolateAt(ts, vs, t) return * real samples (not zero) at t ∈ [0, T)? * L6. setAdcVoltage → AVRADC — does the partUtils helper write into * channelValues[0] correctly? * L7. full RAF-replay + AVR loop — simulate the production replay loop * against a real AVRADC and confirm * `analogRead(A0)` reads varying values. * L8. wireElectricalSolver() — invoke the real function against the * live stores (just like EditorPage * mounts it) with the rectifier already * in setComponents/setWires. * * The AVR program (`adcReadProgram`) continuously triggers an ADC conversion * and writes ADCH/ADCL into r20/r21. By polling ADCH across simulated time, * we can prove whether the rectified waveform is reaching the MCU. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { buildInputFromStore } from '../simulation/spice/storeAdapter'; import { buildNetlist } from '../simulation/spice/NetlistBuilder'; import { solveInput } from './helpers/solveInput'; import { runNetlist } from './helpers/testSolver'; import { setAdcVoltage } from '../simulation/parts/partUtils'; import { AVRTestHarness, adcReadProgram } from './helpers/avrTestHarness'; // ── Snapshot mirroring examples-circuits.ts:403 ("Half-Wave Rectifier") ── // The shape is what loadExample.ts produces via // metadataId: comp.type.replace('wokwi-', '') 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: {}, // Arduino is just observing A0 — no driven pins }, ], }; } // Copy of subscribeToStore.ts `interpolateAt` so the test stays independent. function interpolateAt(ts: number[], vs: number[], t: number): number { if (t <= ts[0]) return vs[0]; const last = ts.length - 1; if (t >= ts[last]) return vs[last]; let lo = 0, hi = last; while (lo + 1 < hi) { const mid = (lo + hi) >> 1; if (ts[mid] <= t) lo = mid; else hi = mid; } const t0 = ts[lo], t1 = ts[hi]; if (t1 === t0) return vs[lo]; const a = (t - t0) / (t1 - t0); return vs[lo] * (1 - a) + vs[hi] * a; } describe('Half-Wave Rectifier — layer-by-layer reproduction', () => { it('traces every pipeline layer with logs so we can spot the failure point', async () => { // ── L1 ──────────────────────────────────────────────────────────────── const snap = rectifierSnapshot(); const input = buildInputFromStore(snap); console.log('\n=== L1 buildInputFromStore ==='); console.log('analysis:', input.analysis); console.log( 'components:', input.components.map((c) => ({ id: c.id, meta: c.metadataId })), ); console.log('boards[0]:', { id: input.boards[0].id, vcc: input.boards[0].vcc, pins: input.boards[0].pins, gnd: input.boards[0].groundPinNames, vccPins: input.boards[0].vccPinNames, }); expect(input.analysis.kind).toBe('tran'); expect(input.components.some((c) => c.metadataId === 'signal-generator')).toBe(true); // ── L2 ──────────────────────────────────────────────────────────────── const { netlist, pinNetMap } = buildNetlist(input); console.log('\n=== L2 buildNetlist ==='); console.log('netlist:\n' + netlist); console.log('pinNetMap entries:', [...pinNetMap.entries()]); const a0Key = 'arduino-uno:A0'; expect(pinNetMap.has(a0Key)).toBe(true); const a0Net = pinNetMap.get(a0Key)!; console.log('A0 pin resolves to net:', a0Net); expect(netlist).toMatch(/SIN\(/); expect(netlist).toMatch(/\.tran\b/); // ── L3 ──────────────────────────────────────────────────────────────── console.log('\n=== L3 runNetlist (ngspice) ==='); const cooked = await runNetlist(netlist); console.log('variableNames:', cooked.variableNames); const times = cooked.vec('time') as number[]; console.log('time points:', times.length, 'first:', times[0], 'last:', times[times.length - 1]); const wfName = `v(${a0Net})`; expect(cooked.variableNames.map((n) => n.toLowerCase())).toContain(wfName.toLowerCase()); const wf = cooked.vec(wfName) as number[]; console.log( `${wfName} samples: peak=${Math.max(...wf).toFixed(3)} V min=${Math.min(...wf).toFixed(3)} V mean=${(wf.reduce((a, b) => a + b, 0) / wf.length).toFixed(3)} V`, ); console.log( `${wfName} first 12 samples:`, wf.slice(0, 12).map((v) => v.toFixed(3)), ); const peak = Math.max(...wf); expect(peak).toBeGreaterThan(3.0); // ── L4 ──────────────────────────────────────────────────────────────── console.log('\n=== L4 solveInput ==='); const result = await solveInput(input); console.log('analysisMode:', result.analysisMode); console.log('converged:', result.converged, 'error:', result.error); console.log('nodeVoltage keys:', Object.keys(result.nodeVoltages)); console.log('pinNetMap keys:', [...result.pinNetMap.keys()]); console.log('timeWaveforms present:', !!result.timeWaveforms); if (result.timeWaveforms) { console.log('timeWaveforms nodes:', [...result.timeWaveforms.nodes.keys()]); console.log('timeWaveforms branches:', [...result.timeWaveforms.branches.keys()]); } expect(result.timeWaveforms).toBeDefined(); expect(result.timeWaveforms!.nodes.has(a0Net)).toBe(true); // ── L5 ──────────────────────────────────────────────────────────────── // `rtw.time[last]` is the `.tran` STOP time (~80 ms — four periods of the // 50 Hz signal), not the signal period. Sample 8 phases across one real // signal period (1/50 Hz = 20 ms); anything else aliases against the sine. console.log('\n=== L5 interpolateAt sanity at 8 phases ==='); const rtw = result.timeWaveforms!; const rSamples = rtw.nodes.get(a0Net)!; const signalFreqHz = 50; const signalPeriodS = 1 / signalFreqHz; const phases: Array<{ t: number; v: number }> = []; for (const q of [0, 1, 2, 3, 4, 5, 6, 7]) { const t = (q / 8) * signalPeriodS; const v = interpolateAt(rtw.time, rSamples, t); phases.push({ t, v }); console.log(` t = ${(t * 1000).toFixed(2)} ms → V(A0) = ${v.toFixed(3)} V`); } const vMax = Math.max(...phases.map((p) => p.v)); const vMin = Math.min(...phases.map((p) => p.v)); console.log(`interpolated vMax=${vMax.toFixed(3)} vMin=${vMin.toFixed(3)}`); expect(vMax).toBeGreaterThan(1.5); // ── L6 ──────────────────────────────────────────────────────────────── console.log('\n=== L6 setAdcVoltage → AVRADC ==='); const avr = new AVRTestHarness(); avr.loadProgram(adcReadProgram()); const mockSim = { getADC: () => avr.adc, getCurrentCycles: () => avr.cpu.cycles, } as unknown as Parameters[0]; const ok25 = setAdcVoltage(mockSim, 14, 2.5); console.log( 'setAdcVoltage(mockSim, 14, 2.5) returned', ok25, 'channelValues[0]=', avr.adc.channelValues[0], ); expect(ok25).toBe(true); expect(avr.adc.channelValues[0]).toBeCloseTo(2.5, 3); avr.runCycles(80_000); const adch25 = avr.reg(0x79); console.log( 'ADCH after AVR run with 2.5 V on ch0:', adch25, '(expected ~128 for ADLAR left-shift of 512/1024 ≈ 0.5)', ); expect(adch25).toBeGreaterThan(0); // ── L7 ──────────────────────────────────────────────────────────────── // Full RAF-replay simulation: step AVR through simulated time, replay // the rectified waveform into channelValues[0] at each frame. This is // the exact loop that runs inside subscribeToStore.ts:adcReplayFrame. console.log('\n=== L7 full replay loop over 80 ms of AVR time ==='); const freshAvr = new AVRTestHarness(); freshAvr.loadProgram(adcReadProgram()); const freshMock = { getADC: () => freshAvr.adc, getCurrentCycles: () => freshAvr.cpu.cycles, } as unknown as Parameters[0]; const CPU_HZ = 16_000_000; const STEP_CYCLES = 16_000; // 1 ms of AVR const STEPS = 200; // → 200 ms total const adcSeries: number[] = []; const adchSeries: number[] = []; for (let i = 0; i < STEPS; i++) { const simT = freshAvr.cpu.cycles / CPU_HZ; const t = simT % signalPeriodS; const v = interpolateAt(rtw.time, rSamples, t); setAdcVoltage(freshMock, 14, Math.max(0, Math.min(5, v))); freshAvr.runCycles(STEP_CYCLES); adcSeries.push(freshAvr.adc.channelValues[0]); adchSeries.push(freshAvr.reg(0x79)); } const hi = adcSeries.filter((v) => v > 1.5).length; const lo = adcSeries.filter((v) => v < 0.2).length; console.log(`channelValues[0] over ${STEPS} ms: highs(>1.5V)=${hi}, lows(<0.2V)=${lo}`); console.log( 'first 30 ADC voltages:', adcSeries.slice(0, 30).map((v) => v.toFixed(2)), ); console.log('first 30 ADCH reads:', adchSeries.slice(0, 30)); const maxAdch = Math.max(...adchSeries); console.log('max ADCH seen by AVR:', maxAdch); expect(hi).toBeGreaterThanOrEqual(20); expect(lo).toBeGreaterThanOrEqual(20); expect(maxAdch).toBeGreaterThan(100); }, 60_000); }); // ── L8 extracted to `spice-rectifier-live-bootstrap.test.ts` ───────────── // The live-bootstrap block ran against the real singleton ngspice-WASM // engine. When L1/L3 solved first in the same process, realloc exploded // with "Not enough memory or heap corruption" and the electrical store // fell back to `op`. Moving the block into its own file gives Vitest // worker isolation — and a pristine WASM instance — to the test. // ── L9 deleted in Phase 1c step C ──────────────────────────────────────── // The pre-existing flaky "wireElectricalSolver queues NO RAF" block was // removed when ADC injection moved into `connectAnalogInputsToMcu.ts`. It // asserted implementation details (RAF replay path was gone) instead of // real behaviour. End-to-end ADC bridge coverage lives in // circuit-simulation-service.test.ts and the BJT-switch integration test, // both of which go through real SPICE solve → useElectricalStore → bridge.