velxio/frontend/src/simulation/spice/MixedModeScheduler.ts

289 lines
10 KiB
TypeScript
Raw Normal View History

feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
/**
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* MixedModeScheduler — voltage event bus + solver orchestrator.
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* Architecture (Phase 1c onwards):
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* ┌─────────────────────────────┐
* │ CircuitSimulationService │ builds netlist, calls scheduler
* │ (or any caller of the │ loadCircuit + resolveDc / onMcu
* │ public methods below) │
* └────────────┬────────────────┘
* │
* ▼
* ┌─────────────────────────────┐
* │ MixedModeScheduler │
* │ • injects a SolverPort │ ── solver.loadCircuit / solve / alter
* │ • caches voltages │
* │ • fans out to subscribers │ ── SpiceResolvedPinResolver.onChange
* └────────────┬────────────────┘
* │ SolverPort
* ▼
* ┌─────────────────────────────┐
* │ NgSpiceWorkerAdapter (prod) │
* │ NgSpiceNodeAdapter (tests) │
* │ FakeSolverAdapter (unit) │
* └─────────────────────────────┘
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* The scheduler does NOT know about ngspice, WASM, or Web Workers —
* those are adapter concerns. Domain code (PinResolver, components)
* sees only the SpiceVoltageSource interface (`subscribe`,
* `getCurrentVoltage`).
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* The cache + fan-out semantics live here because they're tied to
* the (componentId, componentPinName) pair, which is a domain
* concept. The solver speaks SPICE-net names; the scheduler maps
* between the two via the pinNetMap.
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*/
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
import type { SolverPort, SolveAnalysis } from './ports/SolverPort';
import { NgSpiceWorkerAdapter } from './adapters/NgSpiceWorkerAdapter';
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples End-to-end pipeline fixes uncovered while auditing the /examples gallery. Each bug shipped past green unit + snapshot tests because none of those run firmware + render LEDs. Added scripts/visual-led-test.mjs as a CDP-driven visual harness that loads each example, runs the simulator, samples `wokwi-led.brightness`, and asserts toggle / gradient / initial-off invariants — exits non-zero on any regression. Frontend simulator - PinManager.updatePort: new optional ddrMask param. A pin is added to `outputPins` only if the DDR bit is set, so the PORTx write that enables INPUT_PULLUP (DDR=0, PORT=1) no longer falsely marks the pin as MCU output. AVRSimulator now reads DDRB/C/D (0x24/0x27/0x2A on Uno/Nano, 0x37 on ATtiny85, per-port table on Mega) and forwards it. - AVRSimulator: pass DDR mask alongside every port-listener fire. - BasicParts pushbutton{,-6mm}: seed pin HIGH in attachEvents so `digitalRead()` returns HIGH while idle. avr8js doesn't auto-simulate INPUT_PULLUP — without this the firmware reads LOW from boot and thinks the button is permanently pressed (the "LED is always on, pressing does nothing" UX bug). - connectMcuEdgesToService: suppress synthetic digital edges on pins with active PWM, AND subscribe to onPwmChange to re-tick the netlist on duty changes. Fade-LED now produces a true gradient (6 distinct brightness levels across a fade cycle) instead of a binary 0/full toggle. - CircuitSimulationService.handleMcuEdge: replace single-slot pendingMcuEdge with a per-pin Map. Multiple pins toggling during the same in-flight tick used to overwrite each other; now every pin's most-recent edge replays after the tick. Fixes Traffic-Light RED→ YELLOW→GREEN sequencing. - NetlistBuilder: new sanitizeSpiceId() helper replaces hyphens with underscores in V-source names. ngspice's interactive `alter` command treats `-` as an operator and silently no-ops on hyphenated source names, so mid-simulation MCU pin transitions stopped propagating after the first solve. MixedModeScheduler.onMcuPinChange and CircuitSimulationService self-heal use the same sanitizer so names stay consistent across emit/alter/lookup. Also added a regex-based fallback in step 2 so any board pin matching `GND.\d+` canonicalises to net "0" — ESP32-C3 dev kits expose up to 10 GND pins and the per-board `groundPinNames` list missed several, leaving wires floating instead of grounded. - collectPinStates: emit V-sources only for pins in `outputPins`, not every wired board pin. Leaves INPUT pins (analog sensors on A0, pull-down dividers, etc.) free for the SPICE solver instead of being shorted to 0 V by an ideal MCU V-source. - start.ts: extended __spiceDebug to also expose outputPinsByBoard + nodeVoltages + pinNetMapEntries for the visual harness. - ESP32 / RP2040 / RISC-V / C3 simulators: pass `'mcu'` source flag to triggerPinChange / setPinState so the new outputPins tracking fires on those boards too (was AVR-only before). - useSimulatorStore: stopBoard/resetBoard call pm.resetPinStates() so outputPins clears between runs; Esp32Bridge.onPinChange passes the `'mcu'` flag in all three places it's wired. - types/board.ts: ATtiny85 FQBN `clock=internal16mhz` → `clock=16pll` (ATTinyCore 1.5.2 renamed the option). Backend - esp-idf-template/main/CMakeLists.txt: skip the `-DLED_BUILTIN=2` fallback for esp32c3 and esp32s3 targets. Both variants already define LED_BUILTIN in pins_arduino.h via a self-define macro (`#define LED_BUILTIN LED_BUILTIN` + `static const uint8_t LED_BUILTIN = ...;`). Pre-defining the symbol from the command line expanded the static-const declaration to `static const uint8_t 2 = ...;` — a syntax error that broke every ESP32-C3 / S3 build (`expected unqualified-id before numeric constant`). Examples - examples.ts: bulk-fix 72 wire endpoints that referenced `componentId: 'nano-rp2040'` / `'esp32-c3'` etc. (boards that don't exist on the canvas). Replaced with `'arduino-uno'` (the canvas board-id convention) and converted `D<n>` pin names to `GP<n>` for Pico-style boards. Affects pico-blink, pico-i2c-scanner, pico-i2c-rtc-read, pico-spi-loopback, c3-blink and others. Tests - scripts/visual-led-test.mjs: CDP-driven harness. Default suite covers Blink (single-pin), Button (idle-OFF invariant — catches the INPUT_PULLUP regression), Traffic-Light (multi-pin sequencing), Fade-LED (PWM gradient — ≥3 distinct levels), RGB-LED (≥3 PWM pins driven). Run via `npm --prefix frontend run test:visual` against a Chrome on `:9222` + vite on `:5174` + backend on `:8001`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 07:47:45 +07:00
import { sanitizeSpiceId } from './NetlistBuilder';
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
import type { PinState, SpiceVoltageSource } from '../PinResolver';
/**
* Identity of a "pin of interest" — a place a SpiceResolvedPinResolver
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* is watching for voltage changes.
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*/
export interface NodeSubscription {
componentId: string;
componentPinName: string;
cb: (state: PinState, voltage: number) => void;
}
type SubscriptionToken = number;
/** Voltage cache key = `${componentId}|${componentPinName}`. */
function pinKey(componentId: string, componentPinName: string): string {
return `${componentId}|${componentPinName}`;
}
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
class MixedModeSchedulerImpl implements SpiceVoltageSource {
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
private solver: SolverPort | null = null;
private solverFactory: () => SolverPort = () => new NgSpiceWorkerAdapter();
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
private nextToken: SubscriptionToken = 1;
private subscriptions = new Map<SubscriptionToken, NodeSubscription>();
private voltages = new Map<string, number>();
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
/** `${componentId}:${pinName}` → SPICE net name (from NetlistBuilder). */
private pinNetMap = new Map<string, string>();
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
private running = false;
private initPromise: Promise<void> | null = null;
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** True while the scheduler is actively driving the solver. */
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
isRunning(): boolean {
return this.running;
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** Lazy-boot the solver (idempotent). */
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
async start(): Promise<void> {
if (this.running) return;
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
await this.ensureSolver();
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
this.running = true;
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
private async ensureSolver(): Promise<SolverPort> {
if (!this.solver) this.solver = this.solverFactory();
if (!this.initPromise) this.initPromise = this.solver.init();
await this.initPromise;
return this.solver;
}
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
/**
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* Load a SPICE netlist plus the (component, pin) → SPICE-net map
* produced by `NetlistBuilder.buildNetlist`. Replaces any
* previously loaded circuit; clears the voltage cache.
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
*/
async loadCircuit(netlist: string, pinNetMap: Map<string, string>): Promise<void> {
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
const solver = await this.ensureSolver();
await solver.loadCircuit(netlist);
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
this.pinNetMap = new Map(pinNetMap);
this.voltages.clear();
}
/**
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* Run a `.op` solve and publish voltages for every (component, pin)
* currently in pinNetMap. Ground pins (net = `0`) publish 0 V
* without a vector read.
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
*/
async resolveDc(): Promise<void> {
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
if (!this.solver) {
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
throw new Error('MixedModeScheduler.resolveDc(): call loadCircuit first');
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
await this.solveAndPublish({ kind: 'op' });
}
/**
* Run a `.tran` solve and publish the steady-state (last-sample)
* voltage for every (component, pin) in pinNetMap. The full
* waveform is available via `getLastResult()` for callers that need
* the time series.
*/
async resolveTran(step: string, stop: string): Promise<void> {
if (!this.solver) {
throw new Error('MixedModeScheduler.resolveTran(): call loadCircuit first');
}
await this.solveAndPublish({ kind: 'tran', step, stop });
}
private lastResult: import('./ports/SolverPort').SolveResult | null = null;
/**
* Last full solve result, for callers that need the raw vectors
* (e.g. CircuitSimulationService when populating useElectricalStore).
*/
getLastResult(): import('./ports/SolverPort').SolveResult | null {
return this.lastResult;
}
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) The service is the single owner of the simulation loop. Replaces the trio of wireElectricalSolver + connectLegacySolverToMixedMode + connectMixedModeSchedulerToStore once G* lands. Architecture: - Depends on PORTS only — SimulatorStorePort, ElectricalStorePort, MixedModeSchedulerPort. Zero coupling to useSimulatorStore / useElectricalStore / WASM. Easy to test with fakes (and that's what circuit-simulation-service.test.ts does). - Single tick(): build netlist → load → solve → extract → publish. Coalesces concurrent triggers so rapid store changes collapse to one trailing solve. - Domain ElectricalSnapshot type covers nodeVoltages + branchCurrents + pinNetMap + timeWaveforms + analysisMode + warnings. Shape matches what the 12 existing useElectricalStore consumers read. NetlistBuilder extension: BuildNetlistResult now reports `nets` (every non-ground SPICE net) and `voltageSources` (every V card the builder emitted). The service uses these to construct the full vectorsOfInterest list — every node voltage + every branch current — so the solver returns the data the legacy consumers want. Scheduler addition: `setExtraVectorsOfInterest(vectors)` lets the orchestrator add to the per-pin set. Branch currents (i(v_*)) flow through this hook. 8 service tests cover initial solve, branch current extraction, re-solve on store change, no-spurious-solve, coalescing, .tran waveforms, warnings forwarding, error-tolerance. Next: C1+C2 — extract ADC injection / waveform replay into a solver-agnostic module that just subscribes to useElectricalStore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:28:26 +07:00
private extraVectors: readonly string[] = [];
/**
* Let an orchestrator (CircuitSimulationService) ask the solver
* for vectors beyond the pin-net set — branch currents, internal
* nets, etc. Replaces any previous set; pass [] to clear.
*/
setExtraVectorsOfInterest(vectors: readonly string[]): void {
this.extraVectors = vectors;
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
private async solveAndPublish(analysis: SolveAnalysis): Promise<void> {
const solver = this.solver;
if (!solver) return;
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) The service is the single owner of the simulation loop. Replaces the trio of wireElectricalSolver + connectLegacySolverToMixedMode + connectMixedModeSchedulerToStore once G* lands. Architecture: - Depends on PORTS only — SimulatorStorePort, ElectricalStorePort, MixedModeSchedulerPort. Zero coupling to useSimulatorStore / useElectricalStore / WASM. Easy to test with fakes (and that's what circuit-simulation-service.test.ts does). - Single tick(): build netlist → load → solve → extract → publish. Coalesces concurrent triggers so rapid store changes collapse to one trailing solve. - Domain ElectricalSnapshot type covers nodeVoltages + branchCurrents + pinNetMap + timeWaveforms + analysisMode + warnings. Shape matches what the 12 existing useElectricalStore consumers read. NetlistBuilder extension: BuildNetlistResult now reports `nets` (every non-ground SPICE net) and `voltageSources` (every V card the builder emitted). The service uses these to construct the full vectorsOfInterest list — every node voltage + every branch current — so the solver returns the data the legacy consumers want. Scheduler addition: `setExtraVectorsOfInterest(vectors)` lets the orchestrator add to the per-pin set. Branch currents (i(v_*)) flow through this hook. 8 service tests cover initial solve, branch current extraction, re-solve on store change, no-spurious-solve, coalescing, .tran waveforms, warnings forwarding, error-tolerance. Next: C1+C2 — extract ADC injection / waveform replay into a solver-agnostic module that just subscribes to useElectricalStore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:28:26 +07:00
// Build vectorsOfInterest from pinNetMap (every non-ground net)
// plus whatever the orchestrator added.
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
const vectorsOfInterest = new Set<string>();
for (const net of this.pinNetMap.values()) {
if (net !== '0') vectorsOfInterest.add(`v(${net})`);
}
feat(sim): Phase 1c B1+B2+B3 — CircuitSimulationService (orchestrator) The service is the single owner of the simulation loop. Replaces the trio of wireElectricalSolver + connectLegacySolverToMixedMode + connectMixedModeSchedulerToStore once G* lands. Architecture: - Depends on PORTS only — SimulatorStorePort, ElectricalStorePort, MixedModeSchedulerPort. Zero coupling to useSimulatorStore / useElectricalStore / WASM. Easy to test with fakes (and that's what circuit-simulation-service.test.ts does). - Single tick(): build netlist → load → solve → extract → publish. Coalesces concurrent triggers so rapid store changes collapse to one trailing solve. - Domain ElectricalSnapshot type covers nodeVoltages + branchCurrents + pinNetMap + timeWaveforms + analysisMode + warnings. Shape matches what the 12 existing useElectricalStore consumers read. NetlistBuilder extension: BuildNetlistResult now reports `nets` (every non-ground SPICE net) and `voltageSources` (every V card the builder emitted). The service uses these to construct the full vectorsOfInterest list — every node voltage + every branch current — so the solver returns the data the legacy consumers want. Scheduler addition: `setExtraVectorsOfInterest(vectors)` lets the orchestrator add to the per-pin set. Branch currents (i(v_*)) flow through this hook. 8 service tests cover initial solve, branch current extraction, re-solve on store change, no-spurious-solve, coalescing, .tran waveforms, warnings forwarding, error-tolerance. Next: C1+C2 — extract ADC injection / waveform replay into a solver-agnostic module that just subscribes to useElectricalStore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:28:26 +07:00
for (const v of this.extraVectors) vectorsOfInterest.add(v);
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
const result = await solver.solve(analysis, {
vectorsOfInterest: Array.from(vectorsOfInterest),
});
this.lastResult = result;
// Publish the last sample per (component, pin). For .op that's
// the single point; for .tran it's the steady-state.
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
for (const [key, net] of this.pinNetMap) {
const idx = key.indexOf(':');
if (idx < 0) continue;
const componentId = key.slice(0, idx);
const pinName = key.slice(idx + 1);
if (net === '0') {
this.publishVoltage(componentId, pinName, 0);
continue;
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
const vec = result.vectors.get(`v(${net})`);
if (!vec) continue; // disconnected pin — leave unpublished
const v = vec.real[vec.real.length - 1] ?? 0;
this.publishVoltage(componentId, pinName, v);
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
}
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** Stop the scheduler. Engine stays warm so restart is cheap. */
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
stop(): void {
if (!this.running) return;
this.running = false;
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** Tear down the solver entirely. */
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
dispose(): void {
this.running = false;
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
if (this.solver) {
this.solver.dispose();
this.solver = null;
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
}
this.initPromise = null;
this.subscriptions.clear();
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
this.voltages.clear();
this.pinNetMap.clear();
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
this.lastResult = null;
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
}
/**
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* Register a component pin to receive voltage events. Implements
* `SpiceVoltageSource` so `createSpiceResolvedPinResolver` can use
* the scheduler directly.
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*/
subscribe(
componentId: string,
componentPinName: string,
cb: (state: PinState, voltage: number) => void,
): () => void {
const token = this.nextToken++;
this.subscriptions.set(token, { componentId, componentPinName, cb });
return () => {
this.subscriptions.delete(token);
};
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** Latest cached voltage for a (component, pin), or null. */
getCurrentVoltage(componentId: string, componentPinName: string): number | null {
const v = this.voltages.get(pinKey(componentId, componentPinName));
return v === undefined ? null : v;
}
/**
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* Publish a freshly-resolved voltage and notify subscribers.
* SpiceResolvedPinResolver does its own threshold conversion, so
* this layer forwards the raw volts with an `'UNKNOWN'` sentinel
* state — the resolver re-derives HIGH/LOW.
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*/
publishVoltage(componentId: string, componentPinName: string, voltage: number): void {
this.voltages.set(pinKey(componentId, componentPinName), voltage);
for (const sub of this.subscriptions.values()) {
if (sub.componentId === componentId && sub.componentPinName === componentPinName) {
sub.cb('UNKNOWN' as PinState, voltage);
}
}
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
}
/**
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
* MCU pin transition → alter the corresponding V source + re-resolve.
* Silent no-op when no solver has been started (lets legacy callers
* fire without crashing).
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
*/
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
async onMcuPinChange(
boardId: string,
pinName: string,
state: boolean,
vcc: number,
): Promise<void> {
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
if (!this.solver) return;
fix(spice+pipeline): LED visualization, INPUT_PULLUP, ESP32-C3, PWM fade, examples End-to-end pipeline fixes uncovered while auditing the /examples gallery. Each bug shipped past green unit + snapshot tests because none of those run firmware + render LEDs. Added scripts/visual-led-test.mjs as a CDP-driven visual harness that loads each example, runs the simulator, samples `wokwi-led.brightness`, and asserts toggle / gradient / initial-off invariants — exits non-zero on any regression. Frontend simulator - PinManager.updatePort: new optional ddrMask param. A pin is added to `outputPins` only if the DDR bit is set, so the PORTx write that enables INPUT_PULLUP (DDR=0, PORT=1) no longer falsely marks the pin as MCU output. AVRSimulator now reads DDRB/C/D (0x24/0x27/0x2A on Uno/Nano, 0x37 on ATtiny85, per-port table on Mega) and forwards it. - AVRSimulator: pass DDR mask alongside every port-listener fire. - BasicParts pushbutton{,-6mm}: seed pin HIGH in attachEvents so `digitalRead()` returns HIGH while idle. avr8js doesn't auto-simulate INPUT_PULLUP — without this the firmware reads LOW from boot and thinks the button is permanently pressed (the "LED is always on, pressing does nothing" UX bug). - connectMcuEdgesToService: suppress synthetic digital edges on pins with active PWM, AND subscribe to onPwmChange to re-tick the netlist on duty changes. Fade-LED now produces a true gradient (6 distinct brightness levels across a fade cycle) instead of a binary 0/full toggle. - CircuitSimulationService.handleMcuEdge: replace single-slot pendingMcuEdge with a per-pin Map. Multiple pins toggling during the same in-flight tick used to overwrite each other; now every pin's most-recent edge replays after the tick. Fixes Traffic-Light RED→ YELLOW→GREEN sequencing. - NetlistBuilder: new sanitizeSpiceId() helper replaces hyphens with underscores in V-source names. ngspice's interactive `alter` command treats `-` as an operator and silently no-ops on hyphenated source names, so mid-simulation MCU pin transitions stopped propagating after the first solve. MixedModeScheduler.onMcuPinChange and CircuitSimulationService self-heal use the same sanitizer so names stay consistent across emit/alter/lookup. Also added a regex-based fallback in step 2 so any board pin matching `GND.\d+` canonicalises to net "0" — ESP32-C3 dev kits expose up to 10 GND pins and the per-board `groundPinNames` list missed several, leaving wires floating instead of grounded. - collectPinStates: emit V-sources only for pins in `outputPins`, not every wired board pin. Leaves INPUT pins (analog sensors on A0, pull-down dividers, etc.) free for the SPICE solver instead of being shorted to 0 V by an ideal MCU V-source. - start.ts: extended __spiceDebug to also expose outputPinsByBoard + nodeVoltages + pinNetMapEntries for the visual harness. - ESP32 / RP2040 / RISC-V / C3 simulators: pass `'mcu'` source flag to triggerPinChange / setPinState so the new outputPins tracking fires on those boards too (was AVR-only before). - useSimulatorStore: stopBoard/resetBoard call pm.resetPinStates() so outputPins clears between runs; Esp32Bridge.onPinChange passes the `'mcu'` flag in all three places it's wired. - types/board.ts: ATtiny85 FQBN `clock=internal16mhz` → `clock=16pll` (ATTinyCore 1.5.2 renamed the option). Backend - esp-idf-template/main/CMakeLists.txt: skip the `-DLED_BUILTIN=2` fallback for esp32c3 and esp32s3 targets. Both variants already define LED_BUILTIN in pins_arduino.h via a self-define macro (`#define LED_BUILTIN LED_BUILTIN` + `static const uint8_t LED_BUILTIN = ...;`). Pre-defining the symbol from the command line expanded the static-const declaration to `static const uint8_t 2 = ...;` — a syntax error that broke every ESP32-C3 / S3 build (`expected unqualified-id before numeric constant`). Examples - examples.ts: bulk-fix 72 wire endpoints that referenced `componentId: 'nano-rp2040'` / `'esp32-c3'` etc. (boards that don't exist on the canvas). Replaced with `'arduino-uno'` (the canvas board-id convention) and converted `D<n>` pin names to `GP<n>` for Pico-style boards. Affects pico-blink, pico-i2c-scanner, pico-i2c-rtc-read, pico-spi-loopback, c3-blink and others. Tests - scripts/visual-led-test.mjs: CDP-driven harness. Default suite covers Blink (single-pin), Button (idle-OFF invariant — catches the INPUT_PULLUP regression), Traffic-Light (multi-pin sequencing), Fade-LED (PWM gradient — ≥3 distinct levels), RGB-LED (≥3 PWM pins driven). Run via `npm --prefix frontend run test:visual` against a Chrome on `:9222` + vite on `:5174` + backend on `:8001`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 07:47:45 +07:00
// Must match the sanitized name NetlistBuilder emits — see comment
// at the V-source emission site for why ngspice's `alter` command
// can't accept hyphens.
const sourceName = `V_${sanitizeSpiceId(boardId)}_${sanitizeSpiceId(pinName)}`;
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
const voltage = state ? vcc : 0;
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
await this.solver.alterSource(sourceName, voltage);
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
await this.resolveDc();
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
}
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** Singleton accessor. */
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
let instance: MixedModeSchedulerImpl | null = null;
export function getMixedModeScheduler(): MixedModeSchedulerImpl {
if (!instance) instance = new MixedModeSchedulerImpl();
return instance;
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/** Test helper — drop the singleton so each test starts clean. */
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
export function __resetMixedModeScheduler(): void {
if (instance) instance.dispose();
instance = null;
}
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
/**
* Test helper — inject a custom SolverPort factory. Must be called
* before any `start()` / `loadCircuit()` on the singleton.
*/
export function __setSchedulerSolverFactoryForTests(factory: () => SolverPort): void {
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
const sched = getMixedModeScheduler() as unknown as {
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
solverFactory: () => SolverPort;
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
};
feat(sim): Phase 1c A4+A5 — scheduler depends on SolverPort MixedModeScheduler now accepts any SolverPort implementation via solverFactory injection. The ad-hoc `NgSpiceClient` interface is gone; the scheduler talks domain port types only. New capabilities that fell out of the refactor: - `resolveTran(step, stop)` — runs .tran via the solver and publishes the steady-state (last-sample) voltage per pin. Full waveform reachable via `getLastResult()` for downstream consumers (CircuitSimulationService in B1+ will use this to populate useElectricalStore.timeWaveforms). - `getLastResult()` exposes the SolveResult so the upcoming service layer can extract branchCurrents + waveforms without re-reading. - `vectorsOfInterest` is computed from pinNetMap on every solve, so the adapter only issues N parallel readVecs (where N = distinct non-ground nets) instead of guessing. `__setSchedulerEngineFactoryForTests` renamed to `__setSchedulerSolverFactoryForTests`. Tests fully migrated to FakeSolverAdapter — no more inline mock NgSpiceClient. Test layering now mirrors production: scheduler tests exercise port consumption, port-contract tests exercise the port itself. 60 tests pass across mixed-mode-scheduler, solver-port-contract, mixed-mode-bjt-switch-integration (real ngspice), pin-resolver, pin-resolver-phase1b, connect-mixed-mode-scheduler-to-store, connect-legacy-solver-to-mixed-mode. tsc clean. Next: B1 — CircuitSimulationService, the layer above the scheduler that builds netlists, picks .op vs .tran, and publishes results to both useElectricalStore and the scheduler cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:24:12 +07:00
sched.solverFactory = factory;
feat(sim): Phase 1b continued, steps 2 + 3 — loadCircuit, resolveDc, onMcuPinChange Wires the second half of the mixed-mode event loop on top of the voltage cache that step 1 added. Step 2 — loadCircuit + resolveDc: - `loadCircuit(netlist, pinNetMap)` accepts the artifacts that NetlistBuilder already produces, boots the engine lazily, calls `loadNetlist`, and clears the voltage cache so stale values from a previous circuit cannot leak through. - `resolveDc()` runs `op` and walks the pinNetMap, calling readVec for each non-ground net and publishVoltage for each pin. Ground pins short-circuit to 0 V without an extra round-trip. Missing nets are skipped quietly so a disconnected probe pin can't break the resolve. Step 3 — onMcuPinChange: - Issues `alter V_<board>_<pin> dc <volts>` and re-resolves. Caller decides the volts: `state ? vcc : 0` for plain digital, but boards with open-drain / output-impedance semantics can pass any number. - Silent no-op when no engine has been started, so legacy paths that fire pinChange unconditionally can't crash the simulator. NgSpiceClient interface added and exported so unit tests can inject a fake engine that records alter() calls and returns canned readVec values — `__setSchedulerEngineFactoryForTests`. 7 new tests cover the load → resolve → alter → republish loop end-to-end without booting the real WASM worker. The orchestration layer (Zustand subscriber / DynamicComponent hook) that calls `loadCircuit` whenever the canvas changes is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:11:10 +07:00
}
feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection Adds the architecture pieces for mixed-mode coupling without yet driving the SPICE engine. Components on a path that crosses an active device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route through a new SPICE-resolved PinResolver variant; everything else keeps the digital fast-path from Phase 0. What ships: - simulation/PinResolver.ts * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES` list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay). * `DetailedPinTrace` / `DetailedPinTracer` types — the trace function now reports whether it crossed an active device, on top of the Arduino pin number. * `createSpiceResolvedPinResolver()` — new factory; reads voltages from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW with hysteresis (thresholdHigh != thresholdLow → Schmitt-like). - simulation/spice/MixedModeScheduler.ts (new) * Singleton orchestrator that holds the NgSpiceInteractive engine and the SpiceVoltageSource subscription registry. * `start()` / `stop()` / `dispose()` lifecycle. * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource. * `onMcuPinChange()` placeholder for the alter+tran event loop. * Skeleton: subscribers register but never receive events yet. Phase 1b continued will wire NgSpiceInteractive into the loop. - components/DynamicComponent.tsx * Trace function extended with `traceDetailed()` that tracks whether the BFS crossed an active component. * PinResolver factory branches: active-path → SPICE-resolved (uses the scheduler), digital-only → existing default impl. Default threshold = vcc/2 with no hysteresis; Phase 3 will replace with per-logic-family Vil/Vih. Phase 0 LED behavior intact (digital path). Phase 1b SPICE-resolved path falls back to FLOATING until Phase 1b continued wires the engine. Tests: - pin-resolver-phase1b.test.ts (new) — 8/8 passing. Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator metadata id; SPICE-resolved resolver state reporting, threshold conversion, hysteresis dead-band, unsubscribe. - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression). - tsc --noEmit on the new files: clean. No deploy in this commit — staged for end-of-session rebuild + push per user preference.
2026-05-15 21:38:30 +07:00
export type MixedModeScheduler = MixedModeSchedulerImpl;