158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* Production `runNetlist(netlist) → SpiceResult` utility, built on
|
||
|
|
* the SolverPort + NgSpiceWorkerAdapter stack. Replaces the legacy
|
||
|
|
* `SpiceEngine.ts` (eecircuit-engine wrapper) — same API, single
|
||
|
|
* solver path shared with the rest of the simulation subsystem.
|
||
|
|
*
|
||
|
|
* Phase 1c F3 of the mixed-mode migration.
|
||
|
|
*/
|
||
|
|
import { NgSpiceWorkerAdapter } from './adapters/NgSpiceWorkerAdapter';
|
||
|
|
|
||
|
|
export interface ComplexNumber {
|
||
|
|
real: number;
|
||
|
|
img: number;
|
||
|
|
}
|
||
|
|
export type VectorValue = number | ComplexNumber;
|
||
|
|
|
||
|
|
export interface SpiceResult {
|
||
|
|
variableNames: string[];
|
||
|
|
vec(name: string): VectorValue[];
|
||
|
|
dcValue(name: string): number;
|
||
|
|
vAtLast(name: string): VectorValue;
|
||
|
|
findVar(name: string): number;
|
||
|
|
}
|
||
|
|
|
||
|
|
let singleton: NgSpiceWorkerAdapter | null = null;
|
||
|
|
|
||
|
|
function getAdapter(): NgSpiceWorkerAdapter {
|
||
|
|
if (!singleton) singleton = new NgSpiceWorkerAdapter();
|
||
|
|
return singleton;
|
||
|
|
}
|
||
|
|
|
||
|
|
function detectAnalysis(netlist: string):
|
||
|
|
| { kind: 'op' }
|
||
|
|
| { kind: 'tran'; step: string; stop: string }
|
||
|
|
| { kind: 'ac'; sweep: 'dec' | 'oct' | 'lin'; points: number; fstart: number; fstop: number } {
|
||
|
|
for (const line of netlist.split('\n')) {
|
||
|
|
const trimmed = line.trim().toLowerCase();
|
||
|
|
if (trimmed.startsWith('.op')) return { kind: 'op' };
|
||
|
|
if (trimmed.startsWith('.tran ')) {
|
||
|
|
const parts = trimmed.split(/\s+/);
|
||
|
|
return { kind: 'tran', step: parts[1] ?? '1u', stop: parts[2] ?? '1m' };
|
||
|
|
}
|
||
|
|
if (trimmed.startsWith('.ac ')) {
|
||
|
|
const parts = trimmed.split(/\s+/);
|
||
|
|
return {
|
||
|
|
kind: 'ac',
|
||
|
|
sweep: (parts[1] as 'dec' | 'oct' | 'lin') ?? 'dec',
|
||
|
|
points: parseInt(parts[2] ?? '20', 10),
|
||
|
|
fstart: parseFloat(parts[3] ?? '1'),
|
||
|
|
fstop: parseFloat(parts[4] ?? '1e6'),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { kind: 'op' };
|
||
|
|
}
|
||
|
|
|
||
|
|
const SPECIAL_AXES = new Set(['time', 'frequency']);
|
||
|
|
|
||
|
|
function ngspiceNameFor(legacyName: string): string {
|
||
|
|
const l = legacyName.toLowerCase();
|
||
|
|
if (SPECIAL_AXES.has(l)) return l;
|
||
|
|
const mV = l.match(/^v\((.+)\)$/);
|
||
|
|
if (mV) return mV[1]!;
|
||
|
|
const mI = l.match(/^i\((.+)\)$/);
|
||
|
|
if (mI) return `${mI[1]!}#branch`;
|
||
|
|
return l;
|
||
|
|
}
|
||
|
|
|
||
|
|
function legacyNameFor(ngName: string): string {
|
||
|
|
const l = ngName.toLowerCase();
|
||
|
|
if (SPECIAL_AXES.has(l)) return l;
|
||
|
|
const m = l.match(/^(.+)#branch$/);
|
||
|
|
if (m) return `i(${m[1]!})`;
|
||
|
|
return `v(${l})`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Submit a netlist, run the embedded analysis directive, return
|
||
|
|
* cooked results. The vendored ngspice runs in a Web Worker so this
|
||
|
|
* is asynchronous; subsequent calls reuse the same worker (warm boot).
|
||
|
|
*/
|
||
|
|
export async function runNetlist(netlist: string): Promise<SpiceResult> {
|
||
|
|
const adapter = getAdapter();
|
||
|
|
await adapter.init();
|
||
|
|
await adapter.loadCircuit(netlist);
|
||
|
|
const analysis = detectAnalysis(netlist);
|
||
|
|
// First-pass solve to populate the plot. The worker adapter
|
||
|
|
// doesn't yet expose listCurrentVectors, so we fetch every node
|
||
|
|
// voltage + branch current the user might ask for via the
|
||
|
|
// adapter's parallel readVec batching. For browser-side
|
||
|
|
// circuitVerifier this is fine — the netlists are bounded by what
|
||
|
|
// the canvas can hold (~50 nets, ~10 V-sources).
|
||
|
|
await adapter.solve(analysis, { vectorsOfInterest: [] });
|
||
|
|
// We can't readAllCurrentVectors from the worker adapter today —
|
||
|
|
// it doesn't have that method. Fall back to requesting common
|
||
|
|
// patterns: every v(...) and i(...) we can guess from the netlist.
|
||
|
|
const guessed = new Set<string>();
|
||
|
|
for (const line of netlist.split('\n')) {
|
||
|
|
// Match V*, I* source declarations.
|
||
|
|
const mV = line.match(/^([Vv][_\w]+)\s+(\S+)\s+(\S+)/);
|
||
|
|
if (mV) {
|
||
|
|
guessed.add(`v(${mV[2]!.toLowerCase()})`);
|
||
|
|
guessed.add(`v(${mV[3]!.toLowerCase()})`);
|
||
|
|
guessed.add(`i(${mV[1]!.toLowerCase()})`);
|
||
|
|
}
|
||
|
|
// Match generic two-terminal cards (R, C, L, D, Q, M).
|
||
|
|
const mGen = line.match(/^[RCLDQMX][_\w]+\s+(\S+)\s+(\S+)/);
|
||
|
|
if (mGen) {
|
||
|
|
guessed.add(`v(${mGen[1]!.toLowerCase()})`);
|
||
|
|
guessed.add(`v(${mGen[2]!.toLowerCase()})`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
guessed.delete('v(0)'); // ground
|
||
|
|
if (analysis.kind === 'tran') guessed.add('time');
|
||
|
|
if (analysis.kind === 'ac') guessed.add('frequency');
|
||
|
|
|
||
|
|
const requested = Array.from(guessed).map(ngspiceNameFor);
|
||
|
|
const result = await adapter.solve(analysis, { vectorsOfInterest: requested });
|
||
|
|
|
||
|
|
const variableNames = Array.from(result.vectors.keys()).map(legacyNameFor);
|
||
|
|
const getVec = (name: string): VectorValue[] => {
|
||
|
|
const ngKey = ngspiceNameFor(name);
|
||
|
|
const vec = result.vectors.get(ngKey) ?? result.vectors.get(name.toLowerCase());
|
||
|
|
if (!vec) {
|
||
|
|
throw new Error(
|
||
|
|
`[runNetlist] Variable "${name}" not found. Available: ${variableNames.join(', ')}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (vec.imag) {
|
||
|
|
const arr: VectorValue[] = [];
|
||
|
|
for (let i = 0; i < vec.real.length; i++) {
|
||
|
|
arr.push({ real: vec.real[i] ?? 0, img: vec.imag[i] ?? 0 });
|
||
|
|
}
|
||
|
|
return arr;
|
||
|
|
}
|
||
|
|
return Array.from(vec.real);
|
||
|
|
};
|
||
|
|
return {
|
||
|
|
variableNames,
|
||
|
|
findVar(name) {
|
||
|
|
const l = name.toLowerCase();
|
||
|
|
let idx = variableNames.indexOf(l);
|
||
|
|
if (idx >= 0) return idx;
|
||
|
|
idx = variableNames.indexOf(`v(${l})`);
|
||
|
|
return idx;
|
||
|
|
},
|
||
|
|
vec: getVec,
|
||
|
|
dcValue(name) {
|
||
|
|
const v = getVec(name)[0];
|
||
|
|
return typeof v === 'number' ? v : (v as { real: number }).real;
|
||
|
|
},
|
||
|
|
vAtLast(name) {
|
||
|
|
const v = getVec(name);
|
||
|
|
return v[v.length - 1]!;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|