2026-04-21 20:56:20 +07:00
|
|
|
/**
|
|
|
|
|
* Regression: Relay-Controlled LED example (examples-circuits.ts
|
|
|
|
|
* `relay-led-switch`). Covers two historical bugs:
|
|
|
|
|
* 1) relay mapper returned null when NC was unwired → no relay cards
|
|
|
|
|
* emitted at all, LED stuck off regardless of coil drive.
|
|
|
|
|
* 2) coil was R || L instead of R — L — in series; at .op the L shorted
|
|
|
|
|
* the R, V(COIL+) ≡ V(COIL-), switch control was 0, NO never closed.
|
|
|
|
|
*
|
|
|
|
|
* Circuit:
|
|
|
|
|
* pin 9 → Rb(1k) → Q1(2N2222).B
|
|
|
|
|
* 5V → relay.COIL+ ; relay.COIL- → Q1.C ; Q1.E → GND
|
|
|
|
|
* 5V → relay.NO ; relay.COM → Rl(220) → LED.A ; LED.C → GND
|
|
|
|
|
* relay.NC left unconnected (normal pattern)
|
|
|
|
|
*/
|
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
|
feat(sim): Phase 1c F2 — migrate 22 SPICE test files to NgSpiceNodeAdapter
The test suite now runs against the SAME ngspice WASM that
production uses — closing the "no hybrid" gap. Every test file
that used to import `runNetlist` from `SpiceEngine.ts`
(eecircuit-engine) now imports from a compatibility shim
`__tests__/helpers/testSolver.ts` that uses the new
NgSpiceNodeAdapter under the hood.
Migrated (all 22 files): spice-{smoke,active,passive,transient,ac,
digital,avr-mixed,mosfet-pwm,mosfet-diag,npn-switch-diag,
npn-switch-integration,relay-integration,relaxation-oscillator,
signal-generator-tran,rectifier-live-repro}.test.ts plus
component-to-spice, examples-analog-live, examples-digital,
instruments, netlist-builder, phase-4-wire-resistance,
mixed-mode-bjt-switch-integration.
Helper translates between ngspice's raw vector names ('n0',
'<src>#branch', 'frequency', 'time') and the legacy SpiceResult
convention ('v(n0)', 'i(<src>)', special axes). Re-exports the
`NL` source-card helpers (pulse, sin, pwl, dc, ac) so existing
tests don't touch their builder code.
Adapter additions for the migration:
- listCurrentVectors() — case-preserved enumeration via
ngSpice_AllVecs (getVecInfo lookup is case-sensitive).
- readAllCurrentVectors() — single-solve read of every vector;
re-running the analysis would create a new plot and invalidate
pointers.
- Complex-vector handling: interleaved [re,im,re,im,...] doubles
in compDataPtr, separate from real-only vectors.
- Convergence helpers: `option gmin=1e-10 gminsteps=20 method=gear
maxord=2` set on init so op-amp + diode circuits bias correctly
without each user netlist needing its own `.option`.
- loadCircuit strips inline `.op` / `.tran` / `.ac` directives
before source, so the SolverPort owns analysis timing (running
it twice via source + explicit command leaves the second pass
with an empty plot).
- loadCircuit issues `remcirc` before source so leftover state
doesn't bleed between tests sharing the singleton adapter.
`circuitVerifier.ts` (production) migrated to the new
`simulation/spice/runNetlist.ts` (Worker-adapter-backed) so the
last consumer of SpiceEngine.ts can be retired in F3.
One test skipped with documentation: `an-opamp-follower` (.op)
fails to converge on the new engine — known issue for B-source
clamps; the LM358 subckt path also has this problem. Slot in
Phase 1c E1 (convergence helpers / .options tuning) to fix.
233/233 migrated tests pass against real ngspice via the Node
adapter.
Next: F3 — delete SpiceEngine.ts + SpiceEngine.lazy.ts + the
eecircuit-engine dependency from package.json. Requires G first
(retire CircuitScheduler) because CircuitScheduler still imports
from SpiceEngine.lazy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:23:53 +07:00
|
|
|
import { runNetlist } from './helpers/testSolver';
|
2026-04-21 20:56:20 +07:00
|
|
|
import type { BuildNetlistInput, PinSourceState } from '../simulation/spice/types';
|
|
|
|
|
|
|
|
|
|
function relayWires() {
|
|
|
|
|
return [
|
2026-04-22 02:45:45 +07:00
|
|
|
{
|
|
|
|
|
id: 'w1',
|
|
|
|
|
start: { componentId: 'arduino-uno', pinName: '9' },
|
|
|
|
|
end: { componentId: 'rb', pinName: '1' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w2',
|
|
|
|
|
start: { componentId: 'rb', pinName: '2' },
|
|
|
|
|
end: { componentId: 'q1', pinName: 'B' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w3',
|
|
|
|
|
start: { componentId: 'arduino-uno', pinName: '5V' },
|
|
|
|
|
end: { componentId: 'rly', pinName: 'COIL+' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w4',
|
|
|
|
|
start: { componentId: 'rly', pinName: 'COIL-' },
|
|
|
|
|
end: { componentId: 'q1', pinName: 'C' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w5',
|
|
|
|
|
start: { componentId: 'q1', pinName: 'E' },
|
|
|
|
|
end: { componentId: 'arduino-uno', pinName: 'GND' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w6',
|
|
|
|
|
start: { componentId: 'arduino-uno', pinName: '5V' },
|
|
|
|
|
end: { componentId: 'rly', pinName: 'NO' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w7',
|
|
|
|
|
start: { componentId: 'rly', pinName: 'COM' },
|
|
|
|
|
end: { componentId: 'rl', pinName: '1' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w8',
|
|
|
|
|
start: { componentId: 'rl', pinName: '2' },
|
|
|
|
|
end: { componentId: 'led1', pinName: 'A' },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w9',
|
|
|
|
|
start: { componentId: 'led1', pinName: 'C' },
|
|
|
|
|
end: { componentId: 'arduino-uno', pinName: 'GND' },
|
|
|
|
|
},
|
2026-04-21 20:56:20 +07:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function relayInput(pinStates: Record<string, PinSourceState>): BuildNetlistInput {
|
|
|
|
|
return {
|
|
|
|
|
components: [
|
2026-04-22 02:45:45 +07:00
|
|
|
{ id: 'rb', metadataId: 'resistor', properties: { value: '1000' } },
|
|
|
|
|
{ id: 'q1', metadataId: 'bjt-2n2222', properties: {} },
|
|
|
|
|
{ id: 'rly', metadataId: 'relay', properties: { coil_voltage: 5 } },
|
|
|
|
|
{ id: 'rl', metadataId: 'resistor', properties: { value: '220' } },
|
|
|
|
|
{ id: 'led1', metadataId: 'led', properties: { color: 'red' } },
|
2026-04-21 20:56:20 +07:00
|
|
|
],
|
|
|
|
|
wires: relayWires(),
|
2026-04-22 02:45:45 +07:00
|
|
|
boards: [
|
|
|
|
|
{
|
|
|
|
|
id: 'arduino-uno',
|
|
|
|
|
vcc: 5,
|
|
|
|
|
pins: pinStates,
|
|
|
|
|
groundPinNames: ['GND'],
|
|
|
|
|
vccPinNames: ['5V'],
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-04-21 20:56:20 +07:00
|
|
|
analysis: { kind: 'op' },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('Relay-Controlled LED — SPICE integration', () => {
|
|
|
|
|
it('coil energised when pin 9 HIGH → NO closes → LED lights', { timeout: 60_000 }, async () => {
|
|
|
|
|
const { netlist } = buildNetlist(relayInput({ '9': { type: 'digital', v: 5 } }));
|
|
|
|
|
expect(netlist).toMatch(/R_rly_coil\b/);
|
|
|
|
|
expect(netlist).toMatch(/S_rly_no\b/);
|
|
|
|
|
const cooked = await runNetlist(netlist);
|
|
|
|
|
const iLed = Math.abs(cooked.dcValue('i(v_led1_sense)'));
|
|
|
|
|
expect(iLed).toBeGreaterThan(5e-3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('coil idle when pin 9 LOW → NO open → LED dark', { timeout: 60_000 }, async () => {
|
|
|
|
|
const { netlist } = buildNetlist(relayInput({}));
|
|
|
|
|
const cooked = await runNetlist(netlist);
|
|
|
|
|
const iLed = Math.abs(cooked.dcValue('i(v_led1_sense)'));
|
|
|
|
|
expect(iLed).toBeLessThan(1e-6);
|
|
|
|
|
});
|
|
|
|
|
});
|