feat(sim): Phase 1d #6 — listCurrentVectors in Worker adapter, no more heuristic parsing
`runNetlist` was guessing what vectors to read by regex-matching
`V*/R*/L*/C*/D*/Q*/M*` lines in the netlist string. Fragile —
missed extra-card nets, custom prefixes, subckt-internal nets.
This commit gives the Worker adapter the same enumeration surface
the Node adapter already had:
• New `listVectors` message type in the worker, calling
`ngSpice_AllVecs(curPlot)` and decoding the NULL-terminated
char** result. Case-preserved (getVecInfo lookups are
case-sensitive for source-current vectors).
• `NgSpiceInteractive.listVectors()` exposes it to the adapter.
• `NgSpiceWorkerAdapter.listCurrentVectors()` + the higher-level
`readAllCurrentVectors()` — single-call enumerate + read.
• `runNetlist.ts` simplified: ONE solve, then read every vector
via the adapter. No more regex parsing. No more guess-set.
`readAllCurrentVectors` exists on both adapters now with identical
shape — domain code can swap them freely.
1461 tests pass. Both `examples-gallery-smoke` (68 examples) and
`circuit-verifier` (8 pre-flight checks) green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7d3ee95e4
commit
6c6dea3326
|
|
@ -140,6 +140,48 @@ export class NgSpiceWorkerAdapter implements SolverPort {
|
|||
await this.client.alter(name, dcValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate vector names in the current plot. Same surface as
|
||||
* `NgSpiceNodeAdapter.listCurrentVectors` so `runNetlist.ts` no
|
||||
* longer has to heuristic-parse netlist strings to guess what to
|
||||
* read. Phase 1d #6.
|
||||
*/
|
||||
async listCurrentVectors(): Promise<string[]> {
|
||||
await this.init();
|
||||
return this.client.listVectors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every vector in the current plot after an analysis has
|
||||
* already run. Critical for `runNetlist`-style consumers that
|
||||
* want every vector — re-running the analysis to read them would
|
||||
* create a NEW plot and invalidate the pointers.
|
||||
*/
|
||||
async readAllCurrentVectors(): Promise<{
|
||||
vectors: Map<string, import('../ports/SolverPort').SolveVector>;
|
||||
rawNames: string[];
|
||||
}> {
|
||||
await this.init();
|
||||
const rawNames = await this.client.listVectors();
|
||||
const vectors = new Map<string, import('../ports/SolverPort').SolveVector>();
|
||||
const reads = await Promise.allSettled(
|
||||
rawNames.map(async (name) => {
|
||||
const v = await this.client.readVec(name);
|
||||
return { requested: name, vec: v };
|
||||
}),
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
return { vectors, rawNames };
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.client.dispose();
|
||||
this.initialised = false;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,16 @@ function legacyNameFor(ngName: string): string {
|
|||
return `v(${l})`;
|
||||
}
|
||||
|
||||
interface AdapterWithRead {
|
||||
readAllCurrentVectors(): Promise<{
|
||||
vectors: Map<string, import('./ports/SolverPort').SolveVector>;
|
||||
rawNames: string[];
|
||||
}> | {
|
||||
vectors: Map<string, import('./ports/SolverPort').SolveVector>;
|
||||
rawNames: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a netlist, run the embedded analysis directive, return
|
||||
* cooked results. The vendored ngspice runs in a Web Worker so this
|
||||
|
|
@ -102,38 +112,23 @@ export async function runNetlist(netlist: string): Promise<SpiceResult> {
|
|||
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).
|
||||
// Single solve — populate the plot, then enumerate + read every
|
||||
// vector via `readAllCurrentVectors` so the pointers stay valid.
|
||||
// (Re-running the analysis to read vectors would create a new
|
||||
// plot and invalidate everything.)
|
||||
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 all = await (adapter as unknown as AdapterWithRead).readAllCurrentVectors();
|
||||
const result = {
|
||||
analysis,
|
||||
vectors: all.vectors,
|
||||
timeAxis:
|
||||
analysis.kind === 'tran'
|
||||
? all.vectors.get('time')?.real ?? new Float64Array(0)
|
||||
: new Float64Array(0),
|
||||
solveMs: 0,
|
||||
warnings: [] as string[],
|
||||
};
|
||||
const rawVecs = all.rawNames;
|
||||
|
||||
const variableNames = Array.from(result.vectors.keys()).map(legacyNameFor);
|
||||
const getVec = (name: string): VectorValue[] => {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,26 @@ export class NgSpiceInteractive {
|
|||
return this.request<VecResult>('readVec', { name }, 'vec');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate every vector name in the current plot. Returns
|
||||
* case-preserved names — `ngGet_Vec_Info` lookups are
|
||||
* case-sensitive for source-current vectors like `V_src#branch`.
|
||||
*
|
||||
* The worker reads via `ngSpice_AllVecs(curPlot)` and walks the
|
||||
* NUL-terminated char** array. Used by `NgSpiceWorkerAdapter` so
|
||||
* the production path doesn't have to heuristic-parse netlist
|
||||
* strings (Phase 1d #6).
|
||||
*/
|
||||
async listVectors(): Promise<string[]> {
|
||||
await this.init();
|
||||
const res = await this.request<{ names: string[] }>(
|
||||
'listVectors',
|
||||
{},
|
||||
'vector-list',
|
||||
);
|
||||
return res.names;
|
||||
}
|
||||
|
||||
/** Reset the engine to a clean state (drops netlist + plots). */
|
||||
async reset(): Promise<void> {
|
||||
await this.init();
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ self.addEventListener('message', async (event) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'listVectors') {
|
||||
handleListVectors(data.requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown message type: ${data.type}`);
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
|
|
@ -208,6 +213,28 @@ function handleReadVec(requestId, vectorName) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate every vector in the current plot. Phase 1d #6.
|
||||
* `ngSpice_AllVecs` returns a NULL-terminated char** array; we walk
|
||||
* it until the first 0 pointer and decode each cstring. Names are
|
||||
* case-preserved — getVecInfo lookups care about case.
|
||||
*/
|
||||
function handleListVectors(requestId) {
|
||||
const plot = api.curPlot && api.curPlot();
|
||||
const names = [];
|
||||
if (plot) {
|
||||
const arrPtr = api.allVecs(plot);
|
||||
if (arrPtr) {
|
||||
for (let i = 0; i < 4096; i++) {
|
||||
const ptr = HEAPU32[(arrPtr >> 2) + i];
|
||||
if (!ptr) break;
|
||||
names.push(Module.UTF8ToString(ptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.postMessage({ type: 'vector-list', requestId, names });
|
||||
}
|
||||
|
||||
// ── Command-capture machinery (Phase 1a) ────────────────────────────
|
||||
//
|
||||
// onPrint pushes to currentRun.log.stdout when a batch run is active.
|
||||
|
|
|
|||
Loading…
Reference in New Issue