104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
|
|
/**
|
||
|
|
* NgSpiceNodeAdapter integration test — runs the SAME SolverPort
|
||
|
|
* contract suite as solver-port-contract.test.ts but against the
|
||
|
|
* real WASM running in the Vitest Node process.
|
||
|
|
*
|
||
|
|
* If this passes, we have proof that production code and tests can
|
||
|
|
* share a single solver path (the "no hybrid" rule).
|
||
|
|
*
|
||
|
|
* Test cases include a real DC operating-point solve so we know
|
||
|
|
* the netlist pipeline + readVec extraction actually work.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, afterAll } from 'vitest';
|
||
|
|
import { NgSpiceNodeAdapter } from '../simulation/spice/adapters/NgSpiceNodeAdapter';
|
||
|
|
|
||
|
|
const adapter = new NgSpiceNodeAdapter();
|
||
|
|
|
||
|
|
afterAll(() => {
|
||
|
|
adapter.dispose();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('NgSpiceNodeAdapter — SolverPort contract on real WASM', () => {
|
||
|
|
it('init is idempotent', { timeout: 30_000 }, async () => {
|
||
|
|
await adapter.init();
|
||
|
|
await adapter.init();
|
||
|
|
// No throw.
|
||
|
|
});
|
||
|
|
|
||
|
|
it(
|
||
|
|
'loads + solves an .op on a simple divider, returns the expected v(net)',
|
||
|
|
{ timeout: 30_000 },
|
||
|
|
async () => {
|
||
|
|
const netlist = [
|
||
|
|
'* simple divider',
|
||
|
|
'V1 1 0 DC 5',
|
||
|
|
'R1 1 mid 100',
|
||
|
|
'R2 mid 0 100',
|
||
|
|
'.end',
|
||
|
|
].join('\n');
|
||
|
|
await adapter.loadCircuit(netlist);
|
||
|
|
const result = await adapter.solve(
|
||
|
|
{ kind: 'op' },
|
||
|
|
{ vectorsOfInterest: ['v(1)', 'v(mid)'] },
|
||
|
|
);
|
||
|
|
expect(result.vectors.size).toBeGreaterThanOrEqual(2);
|
||
|
|
expect(result.vectors.get('v(1)')?.real[0]).toBeCloseTo(5, 3);
|
||
|
|
expect(result.vectors.get('v(mid)')?.real[0]).toBeCloseTo(2.5, 3);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
it(
|
||
|
|
'omits missing vectors silently',
|
||
|
|
{ timeout: 30_000 },
|
||
|
|
async () => {
|
||
|
|
const result = await adapter.solve(
|
||
|
|
{ kind: 'op' },
|
||
|
|
{ vectorsOfInterest: ['v(1)', 'v(does_not_exist)'] },
|
||
|
|
);
|
||
|
|
expect(result.vectors.has('v(1)')).toBe(true);
|
||
|
|
expect(result.vectors.has('v(does_not_exist)')).toBe(false);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
it(
|
||
|
|
'alterSource updates a V source and the next solve sees the new value',
|
||
|
|
{ timeout: 30_000 },
|
||
|
|
async () => {
|
||
|
|
// Same divider as before — V1 is still loaded.
|
||
|
|
await adapter.alterSource('V1', 10);
|
||
|
|
const result = await adapter.solve(
|
||
|
|
{ kind: 'op' },
|
||
|
|
{ vectorsOfInterest: ['v(mid)'] },
|
||
|
|
);
|
||
|
|
expect(result.vectors.get('v(mid)')?.real[0]).toBeCloseTo(5, 3);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
it(
|
||
|
|
'transient analysis returns a time axis + samples for an RC step',
|
||
|
|
{ timeout: 30_000 },
|
||
|
|
async () => {
|
||
|
|
// Single step at t=0 — uic forces V_cap(0)=0, then DC source
|
||
|
|
// charges through R into C. τ = R·C = 1k·1u = 1 ms.
|
||
|
|
const netlist = [
|
||
|
|
'* RC charge from a DC step',
|
||
|
|
'V1 1 0 DC 5',
|
||
|
|
'R1 1 mid 1k',
|
||
|
|
'C1 mid 0 1u IC=0',
|
||
|
|
'.end',
|
||
|
|
].join('\n');
|
||
|
|
await adapter.loadCircuit(netlist);
|
||
|
|
const result = await adapter.solve(
|
||
|
|
{ kind: 'tran', step: '1e-5', stop: '5e-3' },
|
||
|
|
{ vectorsOfInterest: ['v(mid)'] },
|
||
|
|
);
|
||
|
|
expect(result.timeAxis.length).toBeGreaterThan(10);
|
||
|
|
expect(result.vectors.get('v(mid)')?.real.length).toBe(result.timeAxis.length);
|
||
|
|
// After 5τ, vmid ≈ 0.993·5 ≈ 4.97 V.
|
||
|
|
const lastV = result.vectors.get('v(mid)')?.real.at(-1) ?? 0;
|
||
|
|
expect(lastV).toBeGreaterThan(4.5);
|
||
|
|
expect(lastV).toBeLessThan(5.1);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|