/** * NgSpiceWorkerAdapter — production SolverPort implementation. * * Wraps the vendored `NgSpiceInteractive` client (which itself wraps a * Web Worker that loads the ngspice-interactive WASM build). The * adapter translates between the domain `SolverPort` types and the * client's message-passing semantics. * * Single responsibility: this file is the *only* place in the codebase * that knows about Web Workers, the ngspice command syntax, or vector * lookup details. If we ever swap engines (XSpice fork, native build, * Falstad-style custom engine), the work is contained here. * * Concurrency: the underlying worker serialises commands itself, so * the adapter can fire many `readVec` calls in parallel during a * solve and they queue in the worker. The adapter awaits all of them * before returning a `SolveResult`. */ import { NgSpiceInteractive } from '../wasm/NgSpiceInteractive'; import type { SolverPort, SolveAnalysis, SolveResult, SolveOptions, SolveVector, } from '../ports/SolverPort'; /** * Build the ngspice command string for a given analysis. All formats * follow chapter 17 of the ngspice manual. */ function analysisToCommand(analysis: SolveAnalysis): string { switch (analysis.kind) { case 'op': return 'op'; case 'tran': // ngspice tran syntax: `tran [ [] [uic]]` return `tran ${analysis.step} ${analysis.stop}`; case 'ac': // ngspice ac syntax: `ac ` return `ac ${analysis.sweep} ${analysis.points} ${analysis.fstart} ${analysis.fstop}`; } } export class NgSpiceWorkerAdapter implements SolverPort { private readonly client: NgSpiceInteractive; private initialised = false; private initPromise: Promise | null = null; constructor(client?: NgSpiceInteractive) { this.client = client ?? new NgSpiceInteractive(); } async init(): Promise { if (this.initialised) return; if (!this.initPromise) { this.initPromise = this.client.init().then(async () => { // Convergence helpers — relaxed gmin lets op-amp + diode // circuits bias correctly without each user netlist needing // its own `.option`. Method=gear maxord=2 stabilises stiff // transient solves involving B-source clamps and reactive // networks. Mirrors NgSpiceNodeAdapter.initialiseNgspice so // production and tests run with identical solver tolerances. try { await this.client.command('set noaskquit'); await this.client.command( 'option gmin=1e-10 gminsteps=20 sourcesteps=10 method=gear maxord=2', ); } catch { // Ignore: the build always supports these options. If the // command path is dead, the actual solve will fail loudly // later anyway. } this.initialised = true; }); } return this.initPromise; } async loadCircuit(netlist: string): Promise { await this.init(); // Drop the previous circuit deck so leftover state doesn't leak // into the new solve. Mirrors the Node adapter's loadCircuit. try { await this.client.command('remcirc'); } catch { // No previous circuit — ignore. } await this.client.loadNetlist(netlist); } async solve(analysis: SolveAnalysis, options: SolveOptions): Promise { await this.init(); const t0 = performance.now(); const cmdResult = await this.client.command(analysisToCommand(analysis)); // ngspice writes convergence warnings to stderr; surface them so // the caller can decide to retry with relaxed options. const warnings = cmdResult.stderr.filter((l) => l.length > 0); // Parallel read of every requested vector. The worker serialises // them internally, so this still costs O(N · 100µs) wall-time, but // it avoids the await-each-then-await-next ping-pong. const vectors = new Map(); const reads = await Promise.allSettled( options.vectorsOfInterest.map(async (name) => { const v = await this.client.readVec(name); return { requested: name, vec: v }; }), ); let timeAxis: Float64Array = new Float64Array(0); for (const r of reads) { if (r.status !== 'fulfilled') continue; const { requested, vec } = r.value; vectors.set(requested.toLowerCase(), { name: requested.toLowerCase(), real: vec.real, imag: vec.imag, }); } // For .tran, fetch the time axis separately. ngspice always names // it `time` regardless of analysis name. if (analysis.kind === 'tran') { try { const t = await this.client.readVec('time'); timeAxis = t.real; } catch { // No time vector — shouldn't happen but tolerate it. } } const solveMs = performance.now() - t0; return { analysis, vectors, timeAxis, solveMs, warnings }; } async alterSource(name: string, dcValue: number): Promise { await this.init(); await this.client.alter(name, dcValue); } dispose(): void { this.client.dispose(); this.initialised = false; this.initPromise = null; } }