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

172 lines
6.0 KiB
TypeScript
Raw Normal View History

/**
* End-to-end test for the `readVoltmeter(..., timeWaveforms)` AC path.
*
* Sets up a 50 Hz, 5 Vpk sine source driving a 1 kΩ load with a voltmeter
* across it. Solves through CircuitScheduler (full `.tran` path), then
* invokes readVoltmeter with both the scalar solve result and the time-domain
* waveforms. The returned reading MUST contain `ac` stats with:
* - rms ≈ 5 / √2 ≈ 3.536 V
* - peak ≈ 5 V
* - dc ≈ 0 V
*/
import { describe, it, expect } from 'vitest';
import { buildPinNetLookup, readVoltmeter } from '../simulation/spice/probes';
import type { ComponentForSpice } from '../simulation/spice/types';
import type { Wire } from '../types/wire';
describe('readVoltmeter with .tran timeWaveforms', () => {
it(
'reports RMS ≈ Vpk/√2, peak ≈ Vpk, DC ≈ 0 for a centered sine',
{ timeout: 30_000 },
async () => {
feat(sim): Phase 1c G+F3 — retire legacy CircuitScheduler / eecircuit-engine The mixed-mode migration's endgame. After this commit there is ONE SPICE solver path in the codebase — the vendored ngspice WASM via SolverPort, behind both NgSpiceWorkerAdapter (production browser) and NgSpiceNodeAdapter (Vitest Node). Zero hybrids; zero legacy left to maintain. Deleted production files: • simulation/spice/CircuitScheduler.ts (200ms-poll legacy) • simulation/spice/SpiceEngine.ts (eecircuit-engine wrap) • simulation/spice/SpiceEngine.lazy.ts (lazy code-split) • simulation/spice/subscribeToStore.ts (legacy solve loop) • simulation/spice/connectLegacySolverToMixedMode.ts (bridge) • simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag) Deleted tests (no longer cover any live code): • connect-legacy-solver-to-mixed-mode.test.ts • connect-mixed-mode-scheduler-to-store.test.ts • spice-rectifier-live-bootstrap.test.ts Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to the new `__tests__/helpers/solveInput.ts` (same shape, backed by NgSpiceNodeAdapter). `useElectricalStore` rewritten as a pure state container: • setSolveResult(snapshot) — atomic publish from the service • paused / setPaused — UI control unchanged • reset — project unload • REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook • REMOVED: dependency on SpiceEngine.lazy preload EditorPage now mounts a single `startSimulation()` from `simulation/spice/start.ts`, which constructs CircuitSimulationService + ADC bridge + MCU edge bridge. Four useEffect calls collapsed to one. `circuitVerifier.ts` (production) and `runNetlist.ts` use an environment-aware factory: Web Worker in browser, in-proc WASM in Node tests. `/* @vite-ignore */` keeps the Node adapter chain (node:fs, node:url) out of the browser bundle while still letting Node resolve it dynamically. Removed `eecircuit-engine` from package.json dependencies. `collectPinStates` extracted to its own module so the service doesn't depend on the (now deleted) subscribeToStore.ts. Verification: • 1392/1392 tests pass across 103 files (28 pre-existing skips). • `tsc --noEmit` clean. • `vite build` succeeds (27 s, only the existing chunk-size warning that pre-dates this work). Phase 1c — COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:46:34 +07:00
const { solveInput } = await import('./helpers/solveInput');
const components: ComponentForSpice[] = [
{
id: 'sg1',
metadataId: 'signal-generator',
properties: { waveform: 'sine', frequency: 50, amplitude: 5, offset: 0 },
},
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
{ id: 'vm', metadataId: 'instr-voltmeter', properties: {} },
];
const wires: Wire[] = [
// SIG → R → GND
{
id: 'w1',
start: { componentId: 'sg1', pinName: 'SIG' },
end: { componentId: 'r1', pinName: '1' },
} as Wire,
{
id: 'w2',
start: { componentId: 'r1', pinName: '2' },
end: { componentId: 'sg1', pinName: 'GND' },
} as Wire,
// Voltmeter V+ across the hot net, V− to signal-generator GND
{
id: 'w3',
start: { componentId: 'vm', pinName: 'V+' },
end: { componentId: 'r1', pinName: '1' },
} as Wire,
{
id: 'w4',
start: { componentId: 'vm', pinName: 'V-' },
end: { componentId: 'sg1', pinName: 'GND' },
} as Wire,
];
feat(sim): Phase 1c G+F3 — retire legacy CircuitScheduler / eecircuit-engine The mixed-mode migration's endgame. After this commit there is ONE SPICE solver path in the codebase — the vendored ngspice WASM via SolverPort, behind both NgSpiceWorkerAdapter (production browser) and NgSpiceNodeAdapter (Vitest Node). Zero hybrids; zero legacy left to maintain. Deleted production files: • simulation/spice/CircuitScheduler.ts (200ms-poll legacy) • simulation/spice/SpiceEngine.ts (eecircuit-engine wrap) • simulation/spice/SpiceEngine.lazy.ts (lazy code-split) • simulation/spice/subscribeToStore.ts (legacy solve loop) • simulation/spice/connectLegacySolverToMixedMode.ts (bridge) • simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag) Deleted tests (no longer cover any live code): • connect-legacy-solver-to-mixed-mode.test.ts • connect-mixed-mode-scheduler-to-store.test.ts • spice-rectifier-live-bootstrap.test.ts Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to the new `__tests__/helpers/solveInput.ts` (same shape, backed by NgSpiceNodeAdapter). `useElectricalStore` rewritten as a pure state container: • setSolveResult(snapshot) — atomic publish from the service • paused / setPaused — UI control unchanged • reset — project unload • REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook • REMOVED: dependency on SpiceEngine.lazy preload EditorPage now mounts a single `startSimulation()` from `simulation/spice/start.ts`, which constructs CircuitSimulationService + ADC bridge + MCU edge bridge. Four useEffect calls collapsed to one. `circuitVerifier.ts` (production) and `runNetlist.ts` use an environment-aware factory: Web Worker in browser, in-proc WASM in Node tests. `/* @vite-ignore */` keeps the Node adapter chain (node:fs, node:url) out of the browser bundle while still letting Node resolve it dynamically. Removed `eecircuit-engine` from package.json dependencies. `collectPinStates` extracted to its own module so the service doesn't depend on the (now deleted) subscribeToStore.ts. Verification: • 1392/1392 tests pass across 103 files (28 pre-existing skips). • `tsc --noEmit` clean. • `vite build` succeeds (27 s, only the existing chunk-size warning that pre-dates this work). Phase 1c — COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:46:34 +07:00
const solve = await solveInput({
components,
wires,
boards: [],
analysis: { kind: 'tran', step: '1e-3', stop: '8e-2' },
});
expect(solve.analysisMode).toBe('tran');
expect(solve.timeWaveforms).toBeDefined();
// Build the same Union-Find-based net lookup the store uses so readVoltmeter
// can find the probed nets. signal-generator's GND is the canonical ground.
const netLookup = buildPinNetLookup(
wires.map((w) => ({ id: w.id, start: w.start, end: w.end })),
[{ componentId: 'sg1', pinName: 'GND' }],
[],
);
const vm = components.find((c) => c.id === 'vm')!;
const reading = readVoltmeter(vm, netLookup, solve, solve.timeWaveforms);
expect(reading.kind).toBe('voltmeter');
expect(reading.stale).toBe(false);
expect(reading.ac).toBeDefined();
if (!reading.ac) return;
// Classical AC relations for a centered sine of amplitude 5 V.
// ngspice uses adaptive time-stepping, so sample-based RMS has a small
// bias from non-uniform sampling — ±15 % window is plenty for a sanity check.
const idealRms = 5 / Math.sqrt(2);
expect(reading.ac.rms).toBeGreaterThan(idealRms * 0.85);
expect(reading.ac.rms).toBeLessThan(idealRms * 1.15);
expect(reading.ac.peak).toBeGreaterThan(4.5);
expect(reading.ac.peak).toBeLessThan(5.1);
expect(Math.abs(reading.ac.dc)).toBeLessThan(0.5);
// Display strings should lead with the RMS formatted value.
expect(reading.ac.rmsDisplay).toMatch(/^RMS /);
expect(reading.ac.peakDisplay).toMatch(/^pk /);
expect(reading.ac.dcDisplay).toMatch(/^DC /);
// The top-line `display` field should now be the RMS string (not the scalar).
expect(reading.display).toBe(reading.ac.rmsDisplay);
},
);
it('leaves `ac` undefined for a pure DC circuit (.op path)', { timeout: 30_000 }, async () => {
feat(sim): Phase 1c G+F3 — retire legacy CircuitScheduler / eecircuit-engine The mixed-mode migration's endgame. After this commit there is ONE SPICE solver path in the codebase — the vendored ngspice WASM via SolverPort, behind both NgSpiceWorkerAdapter (production browser) and NgSpiceNodeAdapter (Vitest Node). Zero hybrids; zero legacy left to maintain. Deleted production files: • simulation/spice/CircuitScheduler.ts (200ms-poll legacy) • simulation/spice/SpiceEngine.ts (eecircuit-engine wrap) • simulation/spice/SpiceEngine.lazy.ts (lazy code-split) • simulation/spice/subscribeToStore.ts (legacy solve loop) • simulation/spice/connectLegacySolverToMixedMode.ts (bridge) • simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag) Deleted tests (no longer cover any live code): • connect-legacy-solver-to-mixed-mode.test.ts • connect-mixed-mode-scheduler-to-store.test.ts • spice-rectifier-live-bootstrap.test.ts Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to the new `__tests__/helpers/solveInput.ts` (same shape, backed by NgSpiceNodeAdapter). `useElectricalStore` rewritten as a pure state container: • setSolveResult(snapshot) — atomic publish from the service • paused / setPaused — UI control unchanged • reset — project unload • REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook • REMOVED: dependency on SpiceEngine.lazy preload EditorPage now mounts a single `startSimulation()` from `simulation/spice/start.ts`, which constructs CircuitSimulationService + ADC bridge + MCU edge bridge. Four useEffect calls collapsed to one. `circuitVerifier.ts` (production) and `runNetlist.ts` use an environment-aware factory: Web Worker in browser, in-proc WASM in Node tests. `/* @vite-ignore */` keeps the Node adapter chain (node:fs, node:url) out of the browser bundle while still letting Node resolve it dynamically. Removed `eecircuit-engine` from package.json dependencies. `collectPinStates` extracted to its own module so the service doesn't depend on the (now deleted) subscribeToStore.ts. Verification: • 1392/1392 tests pass across 103 files (28 pre-existing skips). • `tsc --noEmit` clean. • `vite build` succeeds (27 s, only the existing chunk-size warning that pre-dates this work). Phase 1c — COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:46:34 +07:00
const { solveInput } = await import('./helpers/solveInput');
const components: ComponentForSpice[] = [
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
{ id: 'r2', metadataId: 'resistor', properties: { value: '2k' } },
{ id: 'vm', metadataId: 'instr-voltmeter', properties: {} },
];
const wires: Wire[] = [
{
id: 'w1',
start: { componentId: 'uno', pinName: '5V' },
end: { componentId: 'r1', pinName: '1' },
} as Wire,
{
id: 'w2',
start: { componentId: 'r1', pinName: '2' },
end: { componentId: 'r2', pinName: '1' },
} as Wire,
{
id: 'w3',
start: { componentId: 'r2', pinName: '2' },
end: { componentId: 'uno', pinName: 'GND' },
} as Wire,
{
id: 'w4',
start: { componentId: 'vm', pinName: 'V+' },
end: { componentId: 'r1', pinName: '2' },
} as Wire,
{
id: 'w5',
start: { componentId: 'vm', pinName: 'V-' },
end: { componentId: 'uno', pinName: 'GND' },
} as Wire,
];
feat(sim): Phase 1c G+F3 — retire legacy CircuitScheduler / eecircuit-engine The mixed-mode migration's endgame. After this commit there is ONE SPICE solver path in the codebase — the vendored ngspice WASM via SolverPort, behind both NgSpiceWorkerAdapter (production browser) and NgSpiceNodeAdapter (Vitest Node). Zero hybrids; zero legacy left to maintain. Deleted production files: • simulation/spice/CircuitScheduler.ts (200ms-poll legacy) • simulation/spice/SpiceEngine.ts (eecircuit-engine wrap) • simulation/spice/SpiceEngine.lazy.ts (lazy code-split) • simulation/spice/subscribeToStore.ts (legacy solve loop) • simulation/spice/connectLegacySolverToMixedMode.ts (bridge) • simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag) Deleted tests (no longer cover any live code): • connect-legacy-solver-to-mixed-mode.test.ts • connect-mixed-mode-scheduler-to-store.test.ts • spice-rectifier-live-bootstrap.test.ts Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to the new `__tests__/helpers/solveInput.ts` (same shape, backed by NgSpiceNodeAdapter). `useElectricalStore` rewritten as a pure state container: • setSolveResult(snapshot) — atomic publish from the service • paused / setPaused — UI control unchanged • reset — project unload • REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook • REMOVED: dependency on SpiceEngine.lazy preload EditorPage now mounts a single `startSimulation()` from `simulation/spice/start.ts`, which constructs CircuitSimulationService + ADC bridge + MCU edge bridge. Four useEffect calls collapsed to one. `circuitVerifier.ts` (production) and `runNetlist.ts` use an environment-aware factory: Web Worker in browser, in-proc WASM in Node tests. `/* @vite-ignore */` keeps the Node adapter chain (node:fs, node:url) out of the browser bundle while still letting Node resolve it dynamically. Removed `eecircuit-engine` from package.json dependencies. `collectPinStates` extracted to its own module so the service doesn't depend on the (now deleted) subscribeToStore.ts. Verification: • 1392/1392 tests pass across 103 files (28 pre-existing skips). • `tsc --noEmit` clean. • `vite build` succeeds (27 s, only the existing chunk-size warning that pre-dates this work). Phase 1c — COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:46:34 +07:00
const solve = await solveInput({
components,
wires,
boards: [
{
id: 'uno',
vcc: 5,
pins: {},
groundPinNames: ['GND'],
vccPinNames: ['5V'],
},
],
analysis: { kind: 'op' },
});
expect(solve.analysisMode).toBe('op');
expect(solve.timeWaveforms).toBeUndefined();
const netLookup = buildPinNetLookup(
wires.map((w) => ({ id: w.id, start: w.start, end: w.end })),
[{ componentId: 'uno', pinName: 'GND' }],
[{ componentId: 'uno', pinName: '5V' }],
);
const vm = components.find((c) => c.id === 'vm')!;
const reading = readVoltmeter(vm, netLookup, solve, solve.timeWaveforms);
expect(reading.ac).toBeUndefined();
// Midpoint divider: 5 V · 2k/(1k+2k) = 3.33 V. Display is the scalar.
expect(Math.abs(reading.value) + 0).toBeGreaterThan(0);
expect(reading.display).toMatch(/3\.3\d+ V/);
});
});