velxio/docs/wiki/circuit-emulation-performan...

168 lines
7.4 KiB
Markdown
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
# Performance Analysis
Measurements on WSL2 / Ubuntu 22.04 / Node.js 22.22.2 / Intel i7-12700.
## Full suite
```
Test Files 14 passed (14)
Tests 47 passed (47)
Duration ~5 s total (wall clock)
~8 s user+sys CPU time
```
Breakdown by file (rounded):
| Suite | Tests | Duration |
|---|---|---|
| `avr_blink.test.js` | 2 | 2.4 s |
| `spice_transient.test.js` | 2 | 630 ms |
| `spice_555_astable.test.js` | 1 | 610 ms |
| `spice_active.test.js` | 6 | 600 ms |
| `spice_digital.test.js` | 4 | 420 ms |
| `spice_avr_mixed.test.js` | 3 | 800 ms |
| `spice_passive.test.js` | 3 | 360 ms |
| `spice_ac.test.js` | 2 | 340 ms |
| `ngspice_smoke.test.js` | 1 | 370 ms |
| `diodes.test.js` | 7 | 15 ms |
| `passive.test.js` | 11 | 10 ms |
| `e2e_pot_pwm_led.test.js` | 1 | 90 ms |
| `e2e_thermistor.test.js` | 1 | 120 ms |
| `transient_rc.test.js` | 3 | 25 ms |
Observations:
- **`avr_blink.test.js` dominates** (2.4 s of 5 s total). This is because it runs 2 × 2 seconds of real AVR time at 16 MHz = 32 M cycles per test, executed by `avr8js` in JavaScript. Nothing to optimise — that's the entire point of the test.
- **Hand-rolled MNA tests are essentially free** (passive: 10 ms for 11 tests, diodes: 15 ms for 7). A single `solveDC()` takes < 1 ms.
- **ngspice tests are dominated by the 400 ms first-boot cost**, which is paid once per test process. Each subsequent `runSim()` is 5500 ms.
- **`spice_transient.test.js` costs 630 ms** largely because the RLC ringing test runs 30 ms of transient at 10 µs step = 3000 points with an inductor requiring fine integration.
## Per-analysis benchmarks (ngspice)
One-off timings for common patterns. Boot cost of 400 ms amortized over all runs in a process.
| Analysis | Circuit | Points | Time |
|---|---|---|---|
| `.op` | 3 R + 1 V | 1 | 510 ms |
| `.op` | 1 D + 1 R + 1 V | 1 | 2030 ms |
| `.op` | 1 BJT + 3 R + 1 V | 1 | 5080 ms |
| `.op` | 1 MOSFET-L1 + 1 R + 1 V | 1 | 4060 ms |
| `.tran 10u 3m` | RC filter | 300 | 100150 ms |
| `.tran 10u 30m` | RLC ringing | 3000 | 450 ms |
| `.tran 0.1m 40m` | Bridge rectifier (4 diodes) | 400 | 280 ms |
| `.tran 0.5u 2m` | Relaxation oscillator | 4000 | 600 ms |
| `.ac dec 20 10 1Meg` | RC low-pass | 100 | 812 ms |
| `.ac dec 30 10 1Meg` | LC bandpass | 150 | 1015 ms |
## Hand-rolled MNA benchmarks
For the trivially small circuits we target (< 20 nodes):
| Operation | Time |
|---|---|
| `solveDC()` — 4 linear components | < 0.5 ms |
| `solveDC()` — 4 diodes + 3 R (Newton, 6 iterations) | ~3 ms |
| `solveTransient` — RC, 600 steps | ~20 ms |
| `solveTransient` — 4-diode bridge rectifier, 400 steps | *not implemented (no inductor, and BJT/diode-heavy circuits not profiled)* |
At larger circuit sizes (50+ nodes), the dense Gaussian elimination becomes quadratic and eventually uncompetitive with ngspice's sparse solver. For Velxio's expected circuit sizes (≤ 30 nodes in typical hobby circuits), hand-rolled is competitive with ngspice on DC but loses on transient.
## Memory footprint
| Pipeline | On-disk (npm install) | Runtime heap |
|---|---|---|
| Hand-rolled MNA | ~0 (no extra deps) | trivial |
| `avr8js` shared harness | ~1.5 MB | a few MB for CPU + SRAM + listeners |
| `eecircuit-engine` | **39 MB** | ~1520 MB when booted (WASM linear memory) |
**Velxio production impact**: adding ngspice-WASM grows the browser bundle by 39 MB (the WASM is bundled inside the JS module). Unacceptable as a hard dependency for every page load; tolerable behind a lazy-loaded "⚡ Electrical simulation" feature flag. The user clicks to activate; the browser fetches, caches, and initializes; ~400 ms later they can simulate.
## Scalability projections
Rough extrapolation of ngspice to larger circuits (based on ngspice's sparse solver being `O(n^1.2)` ish for well-conditioned circuits):
| # nodes | Expected `.op` time | Expected `.tran` (1000 steps) |
|---|---|---|
| 10 | 5 ms | 100 ms |
| 30 | 15 ms | 300 ms |
| 100 | 60 ms | 1.5 s |
| 300 | 300 ms | 8 s |
In Velxio, the expected typical circuit has 1030 components. Each component contributes 13 nodes (most are 2-terminal). So 2090 nodes. We are firmly in the sub-second-per-simulation zone.
For the Velxio UI to feel responsive, one `solveDC()` should complete in < 100 ms. That is the budget for a real-time "update voltages overlay" flow.
## Co-simulation overhead
`AVRSpiceBridge.run(10, buildNetlist)` runs 10 slices. Per slice:
| Phase | Time |
|---|---|
| `avr.runCycles(16000)` (1 ms of AVR) | ~3 ms in JS |
| Build netlist string | < 1 ms |
| `runNetlist()` (ngspice `.tran 10u 1m`) | 40100 ms |
| Inject ADC voltage | < 1 ms |
| Per-slice total | 45105 ms |
For 1 simulated second of real AVR time: 1000 slices × ~70 ms = **~70 seconds wall-clock**. That's a 70× slowdown. Acceptable for test scenarios; **too slow for live interactive use** in Velxio.
### Mitigations for live use in Velxio
1. **Larger slices**: 10 ms instead of 1 ms → 7× fewer ngspice calls. OK if the analog network's time constants are > 10 ms.
2. **Don't solve the analog circuit unless pins changed**: debounce `onPinChange` 50 ms; only `runNetlist` when the user moves a knob or the AVR toggles a pin.
3. **DC-only mode by default**: `.op` is ~10× faster than `.tran`. Only escalate to `.tran` when the user asks for a waveform view.
4. **Cache netlists**: if the component graph hasn't changed, reuse the previous netlist and only change the source values. Avoids string manipulation overhead.
5. **Web Worker**: move ngspice to a worker so the main thread stays responsive while simulations run.
With all mitigations, target: **< 100 ms of added simulation latency** per UI interaction.
## Solver comparison on the same problem
Voltage divider, V=9 V, R1=1k, R2=2k, R3=3k (parallel with R2):
| Solver | Wall-clock | Notes |
|---|---|---|
| Hand-rolled MNA | 0.4 ms | dense matrix |
| ngspice (first call) | 410 ms | includes 400 ms boot |
| ngspice (warm) | 8 ms | singleton reused |
RC charging, 1 ms of transient at 10 µs step (100 points):
| Solver | Wall-clock | Notes |
|---|---|---|
| Hand-rolled MNA | 3 ms | backward Euler |
| ngspice | 20 ms | trapezoidal default |
LED @ 220 Ω, 5 V (non-linear, 5 Newton iters):
| Solver | Wall-clock | Notes |
|---|---|---|
| Hand-rolled MNA | 2 ms | pnjlim + damping |
| ngspice | 35 ms | model setup + solve |
## What's fast and what's slow (rule of thumb)
**Fast** (≤ 10 ms):
- Hand-rolled `.op` on < 20 linear components
- ngspice `.op` on linear circuit
- ngspice `.ac` on linear circuit
**Medium** (10100 ms):
- ngspice `.op` on 13 non-linear devices
- ngspice `.tran` on < 500 time points, passive only
**Slow** (100 ms 1 s):
- ngspice `.tran` with non-linear devices and > 1000 time points
- ngspice oscillator simulations (need many cycles)
**Always slow** (> 1 s):
- `avr8js.runCycles(N)` where N > 10 million — this is the MCU side, not the analog side
## Optimizing a netlist
1. **Widen timesteps**. `.tran 1m 100m` with no fast edges will be fast. Add fast rise/fall times → slow.
2. **Avoid false non-linearity**. A comparator modeled with a B-source using `u()` is fast. A comparator modeled with a real op-amp macro is slow.
3. **Prefer `.op` over `.tran`** whenever the analysis doesn't need time-domain information.
4. **Remove unused components**. An unused LED model with temperature params slows down every iteration.
5. **Set `.options abstol=1n reltol=1m`** if you don't need 10-ppm accuracy — default tolerances are tight.