/** * 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 { 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(); 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]!; }, }; }