2026-05-15 22:06:20 +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 tests — exercises the cache + fan-out + solver
|
|
|
|
|
* orchestration on top of a FakeSolverAdapter (no WASM).
|
2026-05-15 22:06:20 +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
|
|
|
* Layer covered:
|
|
|
|
|
* • voltage cache + subscriber routing
|
|
|
|
|
* • loadCircuit + resolveDc / resolveTran via the SolverPort
|
|
|
|
|
* • onMcuPinChange → alterSource → re-resolve loop
|
2026-05-15 22:06:20 +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
|
|
|
* Real ngspice integration is covered by the BJT-switch test;
|
|
|
|
|
* SolverPort contract is covered by solver-port-contract.test.ts.
|
2026-05-15 22:06:20 +07:00
|
|
|
*/
|
|
|
|
|
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,
|
2026-05-15 22:06:20 +07:00
|
|
|
} 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';
|
2026-05-15 22:06:20 +07:00
|
|
|
|
|
|
|
|
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);
|
2026-05-15 22:06:20 +07:00
|
|
|
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
|
|
|
});
|
|
|
|
|
});
|