velxio/frontend/src/__tests__/mixed-mode-scheduler.test.ts

245 lines
8.9 KiB
TypeScript
Raw Normal View History

/**
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 tests — exercises the cache + fan-out + solver
* orchestration on top of a FakeSolverAdapter (no WASM).
*
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
* Layer covered:
* • voltage cache + subscriber routing
* • loadCircuit + resolveDc / resolveTran via the SolverPort
* • onMcuPinChange → alterSource → re-resolve loop
*
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
* Real ngspice integration is covered by the BJT-switch test;
* SolverPort contract is covered by solver-port-contract.test.ts.
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import {
getMixedModeScheduler,
__resetMixedModeScheduler,
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
__setSchedulerSolverFactoryForTests,
} from '../simulation/spice/MixedModeScheduler';
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 { FakeSolverAdapter } from '../simulation/spice/adapters/FakeSolverAdapter';
afterEach(() => {
__resetMixedModeScheduler();
});
describe('MixedModeScheduler — voltage cache', () => {
it('returns null until something is published', () => {
const sched = getMixedModeScheduler();
expect(sched.getCurrentVoltage('q1', 'C')).toBeNull();
});
it('returns the last published voltage per (component, pin)', () => {
const sched = getMixedModeScheduler();
sched.publishVoltage('q1', 'C', 4.5);
sched.publishVoltage('q1', 'B', 1.2);
sched.publishVoltage('q2', 'C', 0.3);
expect(sched.getCurrentVoltage('q1', 'C')).toBe(4.5);
expect(sched.getCurrentVoltage('q1', 'B')).toBe(1.2);
expect(sched.getCurrentVoltage('q2', 'C')).toBe(0.3);
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.publishVoltage('q1', 'C', 2.7);
expect(sched.getCurrentVoltage('q1', 'C')).toBe(2.7);
});
});
describe('MixedModeScheduler — subscribe / publish routing', () => {
it('fires the matching subscriber with the published voltage', () => {
const sched = getMixedModeScheduler();
const cb = vi.fn();
sched.subscribe('q1', 'C', cb);
sched.publishVoltage('q1', 'C', 4.7);
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith('UNKNOWN', 4.7);
});
it('does NOT fire subscribers watching a different pin', () => {
const sched = getMixedModeScheduler();
const cbMatching = vi.fn();
const cbOtherPin = vi.fn();
const cbOtherComp = vi.fn();
sched.subscribe('q1', 'C', cbMatching);
sched.subscribe('q1', 'B', cbOtherPin);
sched.subscribe('q2', 'C', cbOtherComp);
sched.publishVoltage('q1', 'C', 4.7);
expect(cbMatching).toHaveBeenCalledTimes(1);
expect(cbOtherPin).not.toHaveBeenCalled();
expect(cbOtherComp).not.toHaveBeenCalled();
});
it('supports multiple subscribers on the same pin (fan-out)', () => {
const sched = getMixedModeScheduler();
const cbA = vi.fn();
const cbB = vi.fn();
sched.subscribe('q1', 'C', cbA);
sched.subscribe('q1', 'C', cbB);
sched.publishVoltage('q1', 'C', 4.7);
expect(cbA).toHaveBeenCalledWith('UNKNOWN', 4.7);
expect(cbB).toHaveBeenCalledWith('UNKNOWN', 4.7);
});
it('unsubscribe handle detaches the callback', () => {
const sched = getMixedModeScheduler();
const cb = vi.fn();
const cancel = sched.subscribe('q1', 'C', cb);
sched.publishVoltage('q1', 'C', 4.7);
expect(cb).toHaveBeenCalledTimes(1);
cancel();
sched.publishVoltage('q1', 'C', 0.3);
expect(cb).toHaveBeenCalledTimes(1);
});
});
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
describe('MixedModeScheduler — loadCircuit + resolveDc', () => {
it('loadCircuit passes the netlist to the solver', async () => {
const fake = new FakeSolverAdapter();
__setSchedulerSolverFactoryForTests(() => fake);
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();
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 netlist = 'V1 1 0 DC 5\n.end\n';
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 sched.loadCircuit(netlist, new Map([['comp:p', '1']]));
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
expect(fake.calls.loadCircuit).toEqual([netlist]);
expect(fake.calls.init).toBe(1);
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
it('resolveDc requests the right vectors and publishes per pinNetMap', async () => {
const fake = new FakeSolverAdapter({
vectors: { 'v(net_drain)': 4.97, 'v(net_gate)': 0.5 },
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
__setSchedulerSolverFactoryForTests(() => fake);
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();
await sched.loadCircuit(
'* netlist',
new Map([
['q1:D', 'net_drain'],
['q1:G', 'net_gate'],
['q1:S', '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
const events: Array<{ pin: string; v: number }> = [];
sched.subscribe('q1', 'D', (_state, v) => events.push({ pin: 'D', v }));
sched.subscribe('q1', 'G', (_state, v) => events.push({ pin: 'G', v }));
sched.subscribe('q1', 'S', (_state, v) => events.push({ pin: 'S', 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
await sched.resolveDc();
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
expect(fake.calls.solve).toHaveLength(1);
expect(fake.calls.solve[0]?.analysis).toEqual({ kind: 'op' });
expect(new Set(fake.calls.solve[0]?.vectorsOfInterest)).toEqual(
new Set(['v(net_drain)', 'v(net_gate)']),
);
// Ground pin doesn't go through the solver — short-circuited to 0V.
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
expect(sched.getCurrentVoltage('q1', 'D')).toBeCloseTo(4.97);
expect(sched.getCurrentVoltage('q1', 'G')).toBeCloseTo(0.5);
expect(sched.getCurrentVoltage('q1', 'S')).toBe(0);
expect(events).toEqual(
expect.arrayContaining([
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
{ pin: 'D', v: expect.closeTo(4.97, 2) },
{ pin: 'G', v: expect.closeTo(0.5, 2) },
{ pin: 'S', v: 0 },
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
it('resolveDc tolerates pins whose net is not in the solver result', async () => {
const fake = new FakeSolverAdapter({ vectors: { 'v(net_present)': 3.3 } });
__setSchedulerSolverFactoryForTests(() => fake);
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();
await sched.loadCircuit(
'* netlist',
new Map([
['comp:P', 'net_present'],
['comp:M', 'net_missing'],
]),
);
await sched.resolveDc();
expect(sched.getCurrentVoltage('comp', 'P')).toBeCloseTo(3.3);
expect(sched.getCurrentVoltage('comp', 'M')).toBeNull();
});
it('resolveDc without loadCircuit first throws a clear error', async () => {
const sched = getMixedModeScheduler();
await expect(sched.resolveDc()).rejects.toThrow(/loadCircuit first/i);
});
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
it('resolveTran issues .tran and publishes the steady-state sample per pin', async () => {
const fake = new FakeSolverAdapter({
vectors: { 'v(out)': new Float64Array([0, 1, 2, 3, 4.5]) },
timeAxis: new Float64Array([0, 1e-4, 2e-4, 3e-4, 4e-4]),
});
__setSchedulerSolverFactoryForTests(() => fake);
const sched = getMixedModeScheduler();
await sched.loadCircuit('* netlist', new Map([['comp:OUT', 'out']]));
await sched.resolveTran('1e-4', '4e-4');
expect(fake.calls.solve[0]?.analysis).toEqual({
kind: 'tran',
step: '1e-4',
stop: '4e-4',
});
// Steady-state = last sample = 4.5
expect(sched.getCurrentVoltage('comp', 'OUT')).toBeCloseTo(4.5);
// Full waveform reachable via getLastResult for downstream consumers.
expect(sched.getLastResult()?.vectors.get('v(out)')?.real.length).toBe(5);
expect(sched.getLastResult()?.timeAxis.length).toBe(5);
});
it('loadCircuit replaces the previous circuit and clears the voltage cache', async () => {
const fake = new FakeSolverAdapter({ vectors: { 'v(net_a)': 1.1, 'v(net_b)': 2.2 } });
__setSchedulerSolverFactoryForTests(() => fake);
const sched = getMixedModeScheduler();
await sched.loadCircuit('first', new Map([['x:p', 'net_a']]));
await sched.resolveDc();
expect(sched.getCurrentVoltage('x', 'p')).toBeCloseTo(1.1);
await sched.loadCircuit('second', new Map([['y:q', 'net_b']]));
expect(sched.getCurrentVoltage('x', 'p')).toBeNull();
await sched.resolveDc();
expect(sched.getCurrentVoltage('y', 'q')).toBeCloseTo(2.2);
});
});
describe('MixedModeScheduler — onMcuPinChange', () => {
it('alters the matching V source and republishes voltages', async () => {
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
let drainV = 4.9;
let gateV = 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
const fake = new FakeSolverAdapter({
vectors: () => ({ 'v(net_drain)': drainV, 'v(net_gate)': gateV }),
});
fake.onAlter = (name, value) => {
if (name === 'V_uno_9') {
gateV = value;
drainV = value >= 1.6 ? 0.05 : 4.9;
}
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
__setSchedulerSolverFactoryForTests(() => fake);
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();
await sched.loadCircuit(
'* netlist',
new Map([
['q1:D', 'net_drain'],
['q1:G', 'net_gate'],
]),
);
await sched.resolveDc();
expect(sched.getCurrentVoltage('q1', 'D')).toBeCloseTo(4.9);
expect(sched.getCurrentVoltage('q1', 'G')).toBeCloseTo(0);
await sched.onMcuPinChange('uno', '9', true, 5);
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
expect(fake.calls.alterSource).toEqual([['V_uno_9', 5]]);
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
expect(sched.getCurrentVoltage('q1', 'G')).toBeCloseTo(5);
expect(sched.getCurrentVoltage('q1', 'D')).toBeCloseTo(0.05);
await sched.onMcuPinChange('uno', '9', false, 5);
expect(sched.getCurrentVoltage('q1', 'G')).toBeCloseTo(0);
expect(sched.getCurrentVoltage('q1', 'D')).toBeCloseTo(4.9);
});
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
it('is a no-op when no solver has been started', async () => {
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();
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 expect(sched.onMcuPinChange('uno', '9', true, 5)).resolves.toBeUndefined();
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
});
});