velxio/test/test_circuit/src/spice/SpiceEngine.js

89 lines
2.6 KiB
JavaScript
Raw Normal View History

feat: electrical simulation via ngspice-WASM (eecircuit-engine) Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy- loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate with real analog behaviour: correct voltages on wires, real I–V curves on LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM driving RC filters, transistors, op-amps, diodes, MOSFETs, etc. Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false. Frontend additions: - simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with UnionFind over wires, componentToSpice mapping (24 metadataIds incl. real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148, 1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with debounced coalescing, AVRSpiceBridge for quasi-static co-simulation. - store/useElectricalStore: Zustand slice, feature-flag aware. - components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay. - components/components-instruments/: Voltmeter, Ammeter probes. - 62 tests (spice-*, netlist-builder, component-to-spice, instruments). Sandbox (test/test_circuit/): 47-test validation sandbox that proved the approach (hand-rolled MNA baseline + ngspice pipeline) before porting to the app. Kept as reference. Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering architecture, solvers, components, AVR bridge, gotchas, performance, integration plan, API reference, appendix) + electrical-simulation- user-guide.md (end-user facing). Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
import { Simulation } from 'eecircuit-engine';
/**
* Thin wrapper around eecircuit-engine (ngspice-WASM).
*
* Provides:
* - Boot-once singleton (ngspice WASM takes ~400 ms to spin up)
* - runNetlist(text) ResultType
* - result helpers: getVector(name), voltage(node, pointIdx), sweep(name)
*/
let singleton = null;
let bootPromise = null;
async function bootEngine() {
if (singleton) return singleton;
if (bootPromise) return bootPromise;
bootPromise = (async () => {
const sim = new Simulation();
await sim.start();
singleton = sim;
return sim;
})();
return bootPromise;
}
export async function getEngine() {
return bootEngine();
}
/**
* Submit a netlist, run it, return the raw ResultType plus a few helpers.
* The ngspice engine is serial running another simulation clobbers the prior one,
* so all tests that share state must await their own run.
*/
export async function runNetlist(netlist) {
const sim = await bootEngine();
sim.setNetList(netlist);
const raw = await sim.runSim();
const lowerNames = raw.variableNames.map(n => n.toLowerCase());
const findVar = (name) => {
const lname = name.toLowerCase();
let idx = lowerNames.indexOf(lname);
if (idx >= 0) return idx;
// Also accept v(node) ↔ node
idx = lowerNames.indexOf(`v(${lname})`);
if (idx >= 0) return idx;
// tran sweep variable is "time"; AC is "frequency"
return -1;
};
const vec = (name) => {
const idx = findVar(name);
if (idx < 0) throw new Error(`Variable "${name}" not found. Available: ${raw.variableNames.join(', ')}`);
if (raw.dataType === 'complex') {
return raw.data[idx].values; // [{real, img}]
}
return raw.data[idx].values; // number[]
};
const vAtLast = (name) => {
const v = vec(name);
const last = v[v.length - 1];
return typeof last === 'number' ? last : last;
};
const dcValue = (name) => {
const v = vec(name);
return v[0];
};
return { raw, vec, dcValue, vAtLast, findVar, variableNames: raw.variableNames };
}
/**
* Build common netlist snippets.
*/
export const NL = {
/** Generic PULSE source: V pin1 pin2 PULSE(V1 V2 TD TR TF PW PER) */
pulse: (name, p, n, v1, v2, td, tr, tf, pw, per) =>
`${name} ${p} ${n} PULSE(${v1} ${v2} ${td} ${tr} ${tf} ${pw} ${per})`,
/** Generic SIN source */
sin: (name, p, n, offset, amp, freq) =>
`${name} ${p} ${n} SIN(${offset} ${amp} ${freq})`,
/** Piece-wise linear source: pairs [[t0,v0],[t1,v1],...] */
pwl: (name, p, n, pairs) =>
`${name} ${p} ${n} PWL(${pairs.flat().join(' ')})`,
};