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

194 lines
12 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
# Circuit Emulation — Architecture
## High-level data flow
Two pipelines coexist. Both share the same `avr8js` harness and the same Arduino program fixtures; they differ only in the analog solver.
```
┌────────────────────────── Shared ──────────────────────────┐
│ │
│ Arduino sketch source (.ino) or pre-built .hex │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ AVRHarness │ (wraps avr8js) │
│ │ (CPU + │ │
│ │ PORTs + │ │
│ │ ADC + │ │
│ │ Timers) │ │
│ └──────┬──────┘ │
│ │ │
│ pin state / PWM duty ADC channel voltages │
│ ▼ ▲ │
└──────────────────│────────────────────│─────────────────────┘
│ │
┌──────────────┼────────────────────┼──────────────┐
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ Pipeline A │ │ │
│ │ MNA Solver (JS) │──────────┤ │
│ │ │ │ │
│ │ - Node graph │ │ │
│ │ - Stamps │ │ │
│ │ - Newton loop │ │ │
│ │ - Transient │ │ │
│ │ (back-Euler) │ │ │
│ └─────────────────┘ │ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ Pipeline B │ │ │
│ │ SpiceEngine │─────────┘ │
│ │ (eecircuit-engine│ │
│ │ / ngspice-WASM) │ │
│ │ │ │
│ │ - Netlist in │ │
│ │ - ResultType out │ │
│ │ - AVRSpiceBridge │ │
│ │ for co-sim │ │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
```
## Module layout
```
test/test_circuit/
├── src/
│ ├── solver/ # Pipeline A — hand-rolled MNA
│ │ ├── linalg.js # Gaussian elimination with partial pivoting
│ │ └── Circuit.js # Circuit class, DC + transient solve
│ ├── components/ # Component library for Pipeline A
│ │ ├── passive.js # R, V, I, C, Pot, NTC, Switch
│ │ └── active.js # Diode, LED, BJT
│ ├── avr/ # Shared AVR harness
│ │ ├── intelHex.js # Intel HEX parser (same format as Velxio)
│ │ ├── AVRHarness.js # Thin wrapper over avr8js
│ │ ├── asm.js # Mini AVR assembler (LDI, OUT, STS, LDS, RJMP, SBRC/S, NOP)
│ │ └── programs.js # Hand-assembled Arduino programs (pot→PWM, adcRead)
│ ├── spice/ # Pipeline B — ngspice integration
│ │ ├── SpiceEngine.js # runNetlist() + vec()/dcValue() helpers
│ │ └── AVRSpiceBridge.js # Quasi-static AVR ↔ ngspice co-simulation
│ └── index.js # Re-exports for Pipeline A
├── fixtures/
│ └── blink.hex # Copied from frontend/src/__tests__/fixtures/
├── test/ # 14 test files, 47 tests total
├── plan/ # 8 markdown planning docs
└── autosearch/ # 4 findings docs
```
## Pipeline A — Hand-rolled MNA
Goal: a transparent, minimal-dependency baseline that we fully control. Useful for:
- Understanding every step of the solver
- Cross-checking ngspice results on toy problems
- Providing a fallback if `eecircuit-engine` cannot be loaded in a constrained environment
**Execution path:**
1. User constructs a `Circuit` object and adds `Component` instances.
2. `circuit.solveDC({maxIter, tol})` is called.
3. Solver identifies voltage-source components and allocates extra MNA rows.
4. For each Newton iteration:
- Zero the G matrix and b vector (size `N + M`).
- Add a small shunt conductance `GMIN = 1e-12` on every node for stability.
- Call `component.stampDC(G, b, ctx)` on every component.
- Solve `G · x = b` via Gaussian elimination.
- Apply damping (≤ 0.5 V step per node) and iterate until `max |Δx| < tol` or `maxIter` reached.
5. Store `nodeVoltages` and `branchCurrents` in `state`.
**Transient execution path:**
1. `circuit.runTransient(tEnd, dt, sampleEvery)` seeds `state.prev.nodeVoltages` from each capacitor's `Vinit` (the initial condition).
2. On every step, `state.prev` is the solution from the previous step.
3. The capacitor stamp uses backward Euler: conductance `G_c = C/Δt`, companion current `I_eq = (C/Δt)·V_prev`.
4. Solver runs `solveDC({dt})` with the capacitor stamps using `ctx.prev`.
See [circuit-emulation-mna-solver.md](circuit-emulation-mna-solver.md) for stamp-level detail.
## Pipeline B — ngspice-WASM
Goal: leverage the full, battle-tested ngspice engine (30+ years of development) without reinventing any physics.
**Execution path:**
1. First call to `getEngine()` or `runNetlist()` boots `eecircuit-engine` — downloads and instantiates ~39 MB of WASM+glue; takes ~400 ms.
2. Subsequent calls reuse the same `Simulation` singleton.
3. Each call:
- Compose a full SPICE netlist string (component cards + `.op` / `.tran` / `.ac` / `.dc` + `.end`).
- `sim.setNetList(netlist)`.
- `await sim.runSim()` → returns a `ResultType` with `variableNames[]` and `data[].values[]`.
4. Our wrapper exposes helpers:
- `vec(name)` → array of numbers (real data) or `[{real, img}]` (complex data from `.ac`).
- `dcValue(name)` → first value of the vector.
- `findVar(name)` → index in `variableNames[]`, matching either `"v(node)"` or just `"node"`.
See [circuit-emulation-ngspice.md](circuit-emulation-ngspice.md) for netlist authoring conventions and gotchas.
## Mixed-signal co-simulation
`avr8js` runs the MCU at ~1 MHz (in JS it's slower than real 16 MHz silicon, but cycle-accurate). ngspice runs transient analysis on the analog network. They are **not locked cycle-by-cycle**; instead the bridge operates **quasi-statically** in time slices.
Per slice (default: 1 ms of AVR time = 16 000 cycles):
```
┌─ slice n ────────────────────────────────────────────────┐
│ │
│ 1. avr.runCycles(16_000) │
│ → new pin states, new PWM duty cycles │
│ │
│ 2. snapshot = { pin6:{type:'pwm',duty:0.5}, │
│ pin13:{type:'digital',v:5}, ... } │
│ │
│ 3. buildNetlist(snapshot) → string │
│ V_PIN6 pin6 0 DC 2.5 ← duty × 5 │
│ V_PIN13 pin13 0 DC 5 │
│ ... passive circuit ... │
│ .tran 10u 1m │
│ .end │
│ │
│ 4. await runNetlist(netlist) → result │
│ │
│ 5. for each analog channel: inject final v(node) into │
│ avr.setAnalogVoltage(ch, v) │
│ │
└──────────────────────────────────────────────────────────┘
```
This is good enough for:
- ADC sampling of slow analog sources (NTC, pot, photoresistor)
- PWM filtered to DC via RC
- LED current driven by a digital or PWM pin
- Digital outputs driving logic or switches
It is **not good enough** for:
- Audio-rate feedback loops where the analog network reacts on sub-millisecond timescales
- Oscillators where the analog side drives a digital input and vice versa with tight timing
- Noise / jitter analysis
For the tight-coupling cases, a co-simulation framework would need to arbitrate time advancement between the two engines (see [Velxio Integration](circuit-emulation-velxio-integration.md) for future work).
## Shared Arduino harness
Both pipelines use `AVRHarness` to drive `avr8js`. This class **mirrors exactly** what Velxio's `frontend/src/simulation/AVRSimulator.ts` does:
- `new CPU(programUint16, sramBytes)`
- `new AVRIOPort(cpu, portBConfig/portCConfig/portDConfig)`
- `new AVRADC(cpu, adcConfig)``adc.channelValues[channel] = volts` to inject analog
- `new AVRTimer(cpu, timer0Config/...)`
- `new AVRUSART(cpu, usart0Config, 16_000_000)`
- Listeners per port: `port.addListener((newValue, oldValue) => ...)` with bit-by-bit diff and Arduino pin mapping (D0D7, D8D13, A0A5 via port C).
- PWM duty read via `cpu.data[ocrAddress]` where ocrAddress for Timer0A is `0x47`, Timer1AL is `0x88`, etc.
This parity is important: any lesson learned in the sandbox transfers one-for-one to Velxio's main app.
See [circuit-emulation-avr-bridge.md](circuit-emulation-avr-bridge.md) for the full harness reference.
## Fixtures and hand-assembled programs
To exercise the full sketch path (C++ → avr-gcc → hex) we would normally compile Arduino code. The sandbox does not have `avr-gcc` installed, so we use:
1. **Copied from Velxio**: `fixtures/blink.hex` — the same Intel HEX used by `frontend/src/__tests__/fixtures/avr-blink/avr-blink.ino.hex`. Tests that load this file exercise the full Arduino core init (reset vectors, library init, `setup()`, `loop()`).
2. **Hand-assembled programs**: `src/avr/programs.js` exposes `potToPwmProgram()` and `adcReadProgram()`, built with the mini-assembler in `src/avr/asm.js`. These bypass the Arduino core and directly configure ADC and Timer0 registers. They produce ~1822 instruction words each. Full opcode breakdown is in [circuit-emulation-appendix.md](circuit-emulation-appendix.md).