velxio/test/test_circuit/src/components/passive.js

158 lines
4.7 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
// Passive linear components: Resistor, VoltageSource, CurrentSource, Capacitor.
// Each component exposes:
// name, nodes() → [a, b, ...], stampDC(G, b, ctx)
// flags: isVoltageSource, isNonlinear
export class Resistor {
constructor(name, a, b, resistance) {
this.name = name;
this.a = a;
this.b = b;
this.R = resistance;
}
nodes() { return [this.a, this.b]; }
stampDC(G, b, ctx) {
const g = 1 / this.R;
const ia = ctx.nodeIndex(this.a);
const ib = ctx.nodeIndex(this.b);
if (ia >= 0) G[ia][ia] += g;
if (ib >= 0) G[ib][ib] += g;
if (ia >= 0 && ib >= 0) { G[ia][ib] -= g; G[ib][ia] -= g; }
}
}
export class VoltageSource {
constructor(name, plus, minus, voltage) {
this.name = name;
this.plus = plus;
this.minus = minus;
this.V = voltage;
this.isVoltageSource = true;
}
nodes() { return [this.plus, this.minus]; }
setVoltage(v) { this.V = v; }
stampDC(G, b, ctx) {
const ip = ctx.nodeIndex(this.plus);
const im = ctx.nodeIndex(this.minus);
const iv = ctx.vsourceIndex(this.name);
if (ip >= 0) { G[ip][iv] += 1; G[iv][ip] += 1; }
if (im >= 0) { G[im][iv] -= 1; G[iv][im] -= 1; }
b[iv] += this.V;
}
}
export class CurrentSource {
constructor(name, from, to, current) {
this.name = name;
this.from = from; // current flows from → to inside the source (into 'to')
this.to = to;
this.I = current;
}
nodes() { return [this.from, this.to]; }
stampDC(G, b, ctx) {
const ifrom = ctx.nodeIndex(this.from);
const ito = ctx.nodeIndex(this.to);
if (ifrom >= 0) b[ifrom] -= this.I;
if (ito >= 0) b[ito] += this.I;
}
}
/**
* Capacitor (backward Euler companion model).
* G_eq = C / dt
* I_eq = (C / dt) * V_prev
* Stamped as a conductance + current source between a and b.
*/
export class Capacitor {
constructor(name, a, b, capacitance, initialV = 0) {
this.name = name;
this.a = a;
this.b = b;
this.C = capacitance;
this.Vinit = initialV;
}
nodes() { return [this.a, this.b]; }
stampDC(G, b, ctx) {
const ia = ctx.nodeIndex(this.a);
const ib = ctx.nodeIndex(this.b);
const dt = ctx.dt;
if (!dt) {
// Pure DC: treat as open circuit (very large R to keep matrix non-singular)
const g = 1e-12;
if (ia >= 0) G[ia][ia] += g;
if (ib >= 0) G[ib][ib] += g;
if (ia >= 0 && ib >= 0) { G[ia][ib] -= g; G[ib][ia] -= g; }
return;
}
const g = this.C / dt;
const Vprev = ctx.prev
? ((ctx.prev.nodeVoltages[this.a] ?? 0) - (ctx.prev.nodeVoltages[this.b] ?? 0))
: this.Vinit;
const Ieq = g * Vprev;
if (ia >= 0) G[ia][ia] += g;
if (ib >= 0) G[ib][ib] += g;
if (ia >= 0 && ib >= 0) { G[ia][ib] -= g; G[ib][ia] -= g; }
if (ia >= 0) b[ia] += Ieq;
if (ib >= 0) b[ib] -= Ieq;
}
}
/** Potentiometer: two resistors in series with a middle wiper node. */
export class Potentiometer {
constructor(name, top, wiper, bottom, totalR, wiperPos = 0.5) {
this.name = name;
this.top = top;
this.wiper = wiper;
this.bottom = bottom;
this.totalR = totalR;
this.wiperPos = wiperPos; // 0..1, 0 = wiper at bottom, 1 = wiper at top
}
nodes() { return [this.top, this.wiper, this.bottom]; }
setWiper(pos) { this.wiperPos = Math.max(0, Math.min(1, pos)); }
stampDC(G, b, ctx) {
const minR = 1; // avoid 0Ω
const Rtop = Math.max(minR, (1 - this.wiperPos) * this.totalR);
const Rbot = Math.max(minR, this.wiperPos * this.totalR);
new Resistor(this.name + '_top', this.top, this.wiper, Rtop).stampDC(G, b, ctx);
new Resistor(this.name + '_bot', this.wiper, this.bottom, Rbot).stampDC(G, b, ctx);
}
}
/** NTC thermistor. R(T) = R0 · exp(β · (1/T − 1/T0)), T in Kelvin. */
export class NTCThermistor {
constructor(name, a, b, { R0 = 10000, T0 = 298.15, beta = 3950 } = {}) {
this.name = name;
this.a = a;
this.b = b;
this.R0 = R0;
this.T0 = T0;
this.beta = beta;
this.TCelsius = 25;
}
nodes() { return [this.a, this.b]; }
setTemperatureC(c) { this.TCelsius = c; }
resistance() {
const T = this.TCelsius + 273.15;
return this.R0 * Math.exp(this.beta * (1 / T - 1 / this.T0));
}
stampDC(G, b, ctx) {
new Resistor(this.name + '_R', this.a, this.b, this.resistance()).stampDC(G, b, ctx);
}
}
/** Simple switch: R=0.001 closed, R=1e9 open. */
export class Switch {
constructor(name, a, b, closed = false) {
this.name = name;
this.a = a;
this.b = b;
this.closed = closed;
}
nodes() { return [this.a, this.b]; }
set(state) { this.closed = state; }
stampDC(G, b, ctx) {
const R = this.closed ? 1e-3 : 1e9;
new Resistor(this.name + '_R', this.a, this.b, R).stampDC(G, b, ctx);
}
}