@vessel-dsp/runtime 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +178 -0
  2. package/dist/admission.d.ts +94 -0
  3. package/dist/admission.d.ts.map +1 -0
  4. package/dist/admission.js +209 -0
  5. package/dist/admission.js.map +1 -0
  6. package/dist/calibrate.d.ts +104 -0
  7. package/dist/calibrate.d.ts.map +1 -0
  8. package/dist/calibrate.js +420 -0
  9. package/dist/calibrate.js.map +1 -0
  10. package/dist/chain-advisories.d.ts +71 -0
  11. package/dist/chain-advisories.d.ts.map +1 -0
  12. package/dist/chain-advisories.js +268 -0
  13. package/dist/chain-advisories.js.map +1 -0
  14. package/dist/chain-scale.d.ts +82 -0
  15. package/dist/chain-scale.d.ts.map +1 -0
  16. package/dist/chain-scale.js +121 -0
  17. package/dist/chain-scale.js.map +1 -0
  18. package/dist/chain-slot.d.ts +130 -0
  19. package/dist/chain-slot.d.ts.map +1 -0
  20. package/dist/chain-slot.js +105 -0
  21. package/dist/chain-slot.js.map +1 -0
  22. package/dist/chain.d.ts +113 -0
  23. package/dist/chain.d.ts.map +1 -0
  24. package/dist/chain.js +355 -0
  25. package/dist/chain.js.map +1 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +18 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/numeric-pivot.d.ts +4 -0
  31. package/dist/numeric-pivot.d.ts.map +1 -0
  32. package/dist/numeric-pivot.js +192 -0
  33. package/dist/numeric-pivot.js.map +1 -0
  34. package/dist/reference-runtime.d.ts +1080 -0
  35. package/dist/reference-runtime.d.ts.map +1 -0
  36. package/dist/reference-runtime.js +6743 -0
  37. package/dist/reference-runtime.js.map +1 -0
  38. package/dist/settle.d.ts +359 -0
  39. package/dist/settle.d.ts.map +1 -0
  40. package/dist/settle.js +900 -0
  41. package/dist/settle.js.map +1 -0
  42. package/dist/supply-ground.d.ts +22 -0
  43. package/dist/supply-ground.d.ts.map +1 -0
  44. package/dist/supply-ground.js +62 -0
  45. package/dist/supply-ground.js.map +1 -0
  46. package/dist/supply.d.ts +30 -0
  47. package/dist/supply.d.ts.map +1 -0
  48. package/dist/supply.js +10 -0
  49. package/dist/supply.js.map +1 -0
  50. package/dist/taper.d.ts +10 -0
  51. package/dist/taper.d.ts.map +1 -0
  52. package/dist/taper.js +34 -0
  53. package/dist/taper.js.map +1 -0
  54. package/dist/v2-wasm-engine.d.ts +91 -0
  55. package/dist/v2-wasm-engine.d.ts.map +1 -0
  56. package/dist/v2-wasm-engine.js +296 -0
  57. package/dist/v2-wasm-engine.js.map +1 -0
  58. package/dist/wasm/v2_dsp.cjs +22 -0
  59. package/dist/wasm/v2_dsp.wasm +0 -0
  60. package/package.json +62 -0
@@ -0,0 +1,1080 @@
1
+ import type { ClockLawParams, ControlId, DelayLengthSpec, Program } from "@vessel-dsp/compiler";
2
+ import { type RealtimeBudget } from "./admission";
3
+ import type { SupplyAddress, SupplyInfo } from "./supply";
4
+ /**
5
+ * What the solver did, so that failing to solve is never silent.
6
+ *
7
+ * A Newton loop that exhausts its iteration cap and then uses whatever iterate it
8
+ * happens to hold produces plausible audio for a circuit it did not actually solve --
9
+ * the same shape as a device law with no stamp, and just as invisible. Counting the
10
+ * failures is the minimum; a real-time runtime cannot throw mid-buffer, but it must
11
+ * never pretend.
12
+ */
13
+ export type RuntimeTelemetry = {
14
+ readonly samples: number;
15
+ /** Samples where Newton hit the iteration cap without meeting tolerance. */
16
+ readonly nonConvergedSamples: number;
17
+ /** Samples that stopped moving without satisfying the equations. See `stalledSamples`. */
18
+ readonly stalledSamples: number;
19
+ /** Samples that stopped moving WITH the equations satisfied, rejected only by a limiter flag. */
20
+ readonly solvedButFlaggedSamples: number;
21
+ /**
22
+ * Per-block Newton census, most-exhausted first. The only counter here that can say which
23
+ * packets a change to the iteration budget would touch — the two host-level ones cannot.
24
+ */
25
+ readonly blockNewtonCensus: readonly {
26
+ readonly blockId: string;
27
+ readonly samples: number;
28
+ readonly exhausted: number;
29
+ readonly peakIterations: number;
30
+ }[];
31
+ /** Samples where the solve produced a non-finite value and was discarded. */
32
+ readonly nonFiniteSamples: number;
33
+ /** Largest iteration count any sample needed. */
34
+ readonly peakIterations: number;
35
+ /**
36
+ * Blocks whose DC operating point did not solve, so they started from zero state and
37
+ * are charging through a transient the run will otherwise measure as the circuit.
38
+ */
39
+ readonly operatingPointFailures: number;
40
+ /**
41
+ * The largest supply current the DC solve drew, in amps, across every `dc-source` branch.
42
+ *
43
+ * A pedal draws single-digit milliamps. This exists because an **ideal source hides a
44
+ * catastrophic load**: measured, `ibanez-ts808` draws **193 A** through the short that
45
+ * `diode-forward-across-supply` warns about, and renders a plausible gain of 0.506 anyway,
46
+ * because a source with no impedance supplies whatever is asked of it. Nothing in the audio
47
+ * says so and no telemetry counter moved.
48
+ *
49
+ * Reported rather than judged: the runtime does not know what a plausible draw is for a
50
+ * circuit it was handed, and a threshold here would be a guess. A consumer comparing this
51
+ * against a pedal's datasheet current does know.
52
+ *
53
+ * **It is blind to an AC supply, by construction, and zero here is not "no short".** The
54
+ * operating point evaluates an `ac-source` at its `t = 0` value, which is zero volts, so a
55
+ * mains inlet with a dead short across it draws nothing at DC and this figure stays at 0.
56
+ * `renderedSupplyPeakAmps` is the figure that can see it.
57
+ */
58
+ readonly operatingPointSupplyAmps: number;
59
+ /**
60
+ * The largest supply current seen **while rendering**, in amps, across every `dc-source` and
61
+ * `ac-source` branch — or `null` when no sample has been processed.
62
+ *
63
+ * `null` rather than 0 for the unmeasured case, deliberately. The failure this field exists
64
+ * to prevent is a reader taking `operatingPointSupplyAmps: 0` for "no short", and a second
65
+ * zero would reproduce it exactly: a reader cannot tell "measured, and nothing flowed" from
66
+ * "nobody rendered anything". A skipped input is a skip, not a pass.
67
+ *
68
+ * It answers a question the DC figure cannot even ask. An AC supply is zero at the operating
69
+ * point, so its whole current lives in the render; and a DC short that only appears under
70
+ * drive — a diode that turns on when the signal arrives — is invisible at DC as well. On a
71
+ * static DC circuit the two figures agree, which is what makes this one checkable against the
72
+ * older one rather than a second opinion.
73
+ *
74
+ * A **peak** rather than a mean, for the same reason the Newton deadline reports peak
75
+ * iterations: a mean over a mains cycle hides the conduction pulse, which is where a
76
+ * rectifier's current is. Read from the solved branch unknowns, so a held sample contributes
77
+ * the last state that actually solved.
78
+ */
79
+ readonly renderedSupplyPeakAmps: number | null;
80
+ /**
81
+ * Iterations summed over every sample and every nonlinear block.
82
+ *
83
+ * Peak alone cannot tell a deadline apart from a spike: a circuit averaging 4
84
+ * iterations with one 1500-iteration sample and a circuit needing 1500 every sample
85
+ * report the same peak and have completely different real-time answers. The first
86
+ * needs a bounded solver, the second is not a real-time circuit at all.
87
+ */
88
+ readonly totalIterations: number;
89
+ /**
90
+ * Why the last non-converged sample failed, which the counts alone cannot say.
91
+ *
92
+ * Two failures look identical in `nonConvergedSamples` and need opposite fixes: a node
93
+ * still genuinely moving (`delta` large) is a solver or model problem, while a tiny
94
+ * `delta` with `limited` set means the answer had arrived and a limiter was still
95
+ * firing, so the convergence *test* is what refused. Guessing between them cost two
96
+ * wrong hypotheses on `boss-od-3`.
97
+ */
98
+ readonly lastFailure: {
99
+ readonly node: number;
100
+ readonly delta: number;
101
+ readonly limited: boolean;
102
+ /** Which element limited, as kind plus nodes, when one did. */
103
+ readonly limitedBy: string | null;
104
+ } | null;
105
+ };
106
+ /**
107
+ * A solved branch current, one per auxiliary unknown.
108
+ *
109
+ * **Sign convention: positive is current flowing into the element's first terminal from the
110
+ * circuit**, which is the MNA stamp's own orientation — a `dc-source` row asserts
111
+ * `v(positive) - v(negative) = volts` and contributes `+i` to the positive node's equation, so
112
+ * a supply *delivering* power to the circuit reads **negative**. Corroborated on the corpus:
113
+ * every compiled pedal's supply reads negative, `boss-bd-2-blues-driver` at `-9.92 mA`.
114
+ *
115
+ * This exists because the current through a supply is not derivable from node voltages: the
116
+ * source is ideal, so no impedance relates its voltage to its current, and the aux unknown is
117
+ * the only place the value lives. `nodeVoltageSnapshot` slices the aux tail off, which left rail
118
+ * current unreachable through the public API — the first thing the power-domain rail/sag
119
+ * contract in `thoughts/shared/experiments/power-domain-rail-sag-contract/` asks for.
120
+ */
121
+ export type RuntimeBranchCurrent = {
122
+ readonly blockId: string;
123
+ /** The stamp kind owning the unknown: the five that add an auxiliary row. */
124
+ readonly kind: "dc-source" | "ac-source" | "input-source" | "ideal-opamp" | "transformer";
125
+ readonly sourceIndex: number;
126
+ readonly amps: number;
127
+ };
128
+ export type RuntimeNodeVoltageSnapshot = {
129
+ readonly blockId: string;
130
+ readonly nodeCount: number;
131
+ /** `outputNode` and every index into `voltages` is a row, not a source node id. */
132
+ readonly outputNode: number | null;
133
+ readonly voltages: readonly number[];
134
+ /**
135
+ * The block's `nodeIds`, carried through so a caller can name a row.
136
+ *
137
+ * Without it a consumer holding source-keyed data -- the terminal labels
138
+ * `render-v2-audio.ts --held` prints beside each voltage, say -- has no way back from a
139
+ * row to the net the document authored, and indexing that data by the row instead
140
+ * attaches the wrong label to a real voltage. That reads as a circuit fault rather than
141
+ * a reporting one.
142
+ */
143
+ readonly nodeIds: readonly number[];
144
+ };
145
+ export declare class RuntimeError extends Error {
146
+ constructor(message: string);
147
+ }
148
+ /**
149
+ * Fundamental of a window by normalized autocorrelation (board-p3 row 6).
150
+ * Pure over its inputs so the estimator is unit-testable without a program:
151
+ * the lag with peak energy-normalized correlation, parabolically refined,
152
+ * converted to Hz. 0 when the window holds no energy. Harmonic complexes
153
+ * peak at the fundamental period (all partials agree there); octave errors
154
+ * are the documented mistuning mode, graded by the acceptance, not defined
155
+ * away here.
156
+ */
157
+ export declare function trackPitchFundamental(window: Float64Array, sampleRate: number): number;
158
+ /**
159
+ * How long a nonlinear block's Newton loop may keep trying before the iterate it has is the
160
+ * answer it returns.
161
+ *
162
+ * **A correctness bound, and nothing else.** It stopped being the admission gate's cost input
163
+ * on 2026-09-09 (`RealtimeBudget.budgetedIterationsPerSample`); while it was both, no value
164
+ * could satisfy both, which is why it sat at 64 below the requirement of several legitimate
165
+ * circuits.
166
+ *
167
+ * **64 was too low, and truncating does not merely return a worse answer.** It leaves a
168
+ * residual that re-seeds the next sample, so the packet never recovers:
169
+ * `moogerfooger-mf-102` needs **183** iterations once and then 2.1 per sample forever after,
170
+ * but capped at 64 it burned all 64 on every sample of a ten-second render — and
171
+ * `boss-tw-1` shipped audio **11,900x too loud** for the same reason.
172
+ *
173
+ * **The bound is 801** — `fender-twin-reverb`'s one-off transient, with `mxr-blue-box` at 356
174
+ * — so this sits above the observed maximum with headroom, because a bound equal to the
175
+ * population maximum is a coincidence rather than a bound.
176
+ */
177
+ export declare const DEFAULT_NEWTON_MAX_ITERATIONS = 1024;
178
+ /**
179
+ * A delay-tap read length, pure over its inputs (board-p3 row 4). Factored
180
+ * out of the interpreter so every mode is unit-testable without a program:
181
+ * the smoothing arithmetic, the modulation estimator, the parameter scale,
182
+ * and the capacity default mirror the delay kernels branch for branch, in
183
+ * the same priority. `min`/`headroom`/`round` carry the kernels' shaping --
184
+ * DDL rounds to whole samples above 1, BBD reads fractionally above 0 with
185
+ * two slots of headroom -- so one function serves both without blurring them.
186
+ */
187
+ export declare function composedDelayLength(input: {
188
+ readonly mode: DelayLengthSpec["mode"];
189
+ readonly capacity: number;
190
+ readonly current: number;
191
+ readonly target: number;
192
+ readonly sampleRate: number;
193
+ readonly paramScale: number | null;
194
+ /**
195
+ * A tapped length in samples (a `tapped` line's interval times its ratio), or null. In
196
+ * `parameter` mode it wins over `paramScale` and is clamped to `floor..capacity`.
197
+ */
198
+ readonly tapped: number | null;
199
+ readonly modVolts: number | null;
200
+ readonly modEstimate: number;
201
+ readonly modSeeded: boolean;
202
+ /**
203
+ * The open-OX2 law constants plus stage count, or null. Read only by the
204
+ * `clock-law` branch; every other mode ignores it, exactly as they ignore
205
+ * `modVolts` outside `modulation`. Optional so existing callers keep
206
+ * compiling; absent reads as no law, and the branch falls to capacity.
207
+ */
208
+ readonly clockLaw?: (ClockLawParams & {
209
+ readonly stages: number;
210
+ }) | null;
211
+ readonly min: number;
212
+ /**
213
+ * The shortest length, in samples, a `parameter` read may produce: the source's cited
214
+ * floor. The control sweeps `floor..capacity` rather than `0..capacity`. 0 reproduces the
215
+ * old `capacity x scale` reading exactly, and is what every registry decomposition passes.
216
+ */
217
+ readonly floor: number;
218
+ readonly headroom: number;
219
+ readonly round: boolean;
220
+ }): {
221
+ length: number;
222
+ current: number;
223
+ modEstimate: number;
224
+ modSeeded: boolean;
225
+ };
226
+ export declare class ReferenceRuntime {
227
+ private readonly program;
228
+ private sampleRate;
229
+ /**
230
+ * Solver sub-samples per host sample; 1 is the plain path and costs nothing.
231
+ *
232
+ * `sampleRate` above is the **solver's** rate, already multiplied by this, because every
233
+ * `dt`, delay length and pole coefficient in this file is derived from it and each is
234
+ * correct at the sub-sample rate. `hostSampleRate()` is the rate a caller passed in.
235
+ */
236
+ private oversample;
237
+ private readonly positions;
238
+ private readonly lastShiftedSample;
239
+ /**
240
+ * Build-order step 6: bumped by every `setControl` call. `eliminationScratch`'s cached
241
+ * factorisation, `Z` and `K_reduced` are valid only while this has not moved since they
242
+ * were built (see `setControl`'s own comment) -- starts at 0 and the cache's own
243
+ * `cachedGeneration` starts at -1, so the first sample always builds fresh regardless of
244
+ * whether a control was ever explicitly set.
245
+ */
246
+ private controlGeneration;
247
+ /**
248
+ * Block base-matrix rebuilds performed by `setSupply`, one per affected block
249
+ * per call. A call whose values already match, an empty address list, or a call
250
+ * before `prepare()` rebuilds nothing and leaves this unchanged.
251
+ */
252
+ private supplyRebuildCount;
253
+ private readonly capacitorState;
254
+ private readonly nodeVoltages;
255
+ /**
256
+ * Previous-iterate junction voltages, which junction limiting needs.
257
+ *
258
+ * Keyed by a packed integer (`packHistoryKey2`/`3`) rather than a `${block.id}:${node}...`
259
+ * string built fresh on every lookup -- same intent as the id-lookup maps built in the
260
+ * constructor, applied to the per-iteration, per-stamp case. **Keying by the stamp object
261
+ * itself was tried first and reverted**: it changes behaviour, not just performance.
262
+ * `boss-hm-2` has two distinct `diode` stamps sharing one node pair (anode 0, cathode 19)
263
+ * -- a real, if unusual, circuit shape -- and the string key's collision on them is not a
264
+ * latent bug to fix here, it is part of the exact trajectory this runtime is the reference
265
+ * for: the two diodes sharing one limiter history slot changes which of them a later
266
+ * iteration damps against, and that changed `boss-hm-2`'s digest. A packed integer of
267
+ * (block index, node indices) reproduces that exact collision -- two same-kind stamps at
268
+ * the same nodes still hash identically -- while costing an arithmetic combine instead of a
269
+ * string allocation. See `applyStamp`'s `bjt` case and `packHistoryKey2`/`3`'s own comment.
270
+ */
271
+ private readonly bjtHistory;
272
+ /**
273
+ * Previous-iterate grid and plate voltages, for the triode's step limiter.
274
+ *
275
+ * Shared with the `pentode` case below, and keyed the same way as `bjtHistory` -- which
276
+ * also means a triode and a pentode sharing grid/cathode/plate node numbers in the same
277
+ * block still collide, exactly as the old string key made them (see `bjtHistory`'s comment
278
+ * for why that is the correct thing to reproduce here, not a bug to fix).
279
+ */
280
+ private readonly triodeHistory;
281
+ /**
282
+ * Previous-iterate op-amp differentials, which the gain step needs damping against,
283
+ * plus the step that produced each -- a step's *sign* is what tells a march toward
284
+ * the solution apart from an oscillation straddling it. See the `ideal-opamp` case.
285
+ * Keyed the same way as `bjtHistory`.
286
+ */
287
+ private readonly opampHistory;
288
+ /**
289
+ * The op-amp dominant pole's raw open-loop state, in volts -- the backward-Euler memory the
290
+ * `ideal-opamp` case steps against, seeded from the operating point in `solveOperatingPoint`
291
+ * and advanced once per sample in `processBlock` rather than per Newton iterate, because it
292
+ * is a property of the circuit's clock, not of the iteration. Keyed like `opampHistory`.
293
+ */
294
+ private readonly opampRawState;
295
+ /**
296
+ * Previous-iterate diode junction voltages, which `limitJunction` damps against.
297
+ * Keyed the same way as `bjtHistory`.
298
+ */
299
+ private readonly diodeHistory;
300
+ /**
301
+ * Previous-iterate FET channel voltages, which both FET limiters need.
302
+ * Keyed the same way as `bjtHistory`.
303
+ */
304
+ private readonly fetHistory;
305
+ /**
306
+ * One composed block's delay lines, by line id (board-p3 row 4). Same ring
307
+ * discipline the retired macro state kept, but allocated from the composition's
308
+ * own `lines` table rather than a model's parameters, and with the
309
+ * clock-smoothing pair every delay line carries whether a macro or a
310
+ * composition owns it.
311
+ */
312
+ /**
313
+ * Per-block, per-position state keys, built once at prepare.
314
+ *
315
+ * `composedStateKey` concatenates, and `processComposedBlock` runs once per sample, so
316
+ * calling it there allocated a string 48,000 times a second per composed block. Measured
317
+ * as test timeouts on the brigade and reverb decks before this table existed.
318
+ */
319
+ private readonly composedStateKeys;
320
+ /**
321
+ * Per momentary control: the sample of its last press, and the interval between its last two.
322
+ * A press is a rising edge through `setControl`; the time is the sample clock at that call,
323
+ * which is a host block boundary, so a tap is timed to within one block (2.7 ms at 128 / 48k).
324
+ */
325
+ private readonly tapState;
326
+ /** The position each composed block ran last sample, to know when a mode was just selected. */
327
+ private readonly lastComposedPosition;
328
+ private readonly composedLineState;
329
+ /**
330
+ * Per-composed-block DC seed, carrying the `dcEstimate` /
331
+ * `dcOperatingPoint` pair (board-p3 row 4). The `macro-audio-source`
332
+ * stamp adds the operating point back to a block's published AC, so a
333
+ * composition must carry the same seed seeded from the same tap, or the
334
+ * shell solves a different source -- measured as a ULP divergence at the
335
+ * first echo on `ibanez-dl5`, whose shell carries real DC bias.
336
+ */
337
+ private readonly composedDcState;
338
+ /**
339
+ * One composed block's Schroeder sections, allocated at prepare from the
340
+ * composition's own comb/allpass ops (board-p3 row 4). Same buffers,
341
+ * indices, and gains the reverb kernel owns -- the interpreter factors
342
+ * the kernel's state per op rather than re-deriving it.
343
+ */
344
+ private readonly composedFilterState;
345
+ /**
346
+ * One pitch-shift op's resampling state, keyed by block then op position
347
+ * (board-p3 row 5). Absolute positions, never wrapped counters: the read
348
+ * pointer advances `ratio` per sample through a fixed history window and
349
+ * wraps by whole windows when it outruns or falls behind, which is the
350
+ * documented discontinuity -- no crossfade, fundamental preserved.
351
+ */
352
+ private readonly composedPitchState;
353
+ /**
354
+ * One pitch-tracker op's estimator state, keyed by block then op
355
+ * position (board-p3 row 6): the ring the autocorrelation window is
356
+ * copied from, samples since the last estimate, and the held output.
357
+ */
358
+ private readonly composedTrackerState;
359
+ /**
360
+ * Each macro's current write-back value, read by a `macro-audio-source` stamp in whatever
361
+ * block shares its audio-out node. Updated once per sample, when the macro block's own turn
362
+ * in `program.order` comes up -- and `couple.ts`'s corrected schedule (spec clause 3) places
363
+ * that turn strictly before the block reading this map, so the value read is this sample's,
364
+ * not last sample's. Written by whichever composition publishes into it -- today
365
+ * only the delay-line compositions.
366
+ */
367
+ private readonly macroOutputVolts;
368
+ /**
369
+ * Each spring reverb tank's mechanical state, keyed by {@link springKey}.
370
+ *
371
+ * A tank is not a macro block: it stays inside the MNA block its terminals belong to,
372
+ * because unlike a BBD it has no audio-in/audio-out *port* to couple -- it has two ordinary
373
+ * terminal pairs the surrounding driver and recovery stages are wired to. So its state
374
+ * lives here and advances with the block's other memory, once per solved sample.
375
+ */
376
+ private readonly springState;
377
+ /** Each tank's current pickup voltage, read by its own `spring-reverb` stamp. */
378
+ private readonly springOutputVolts;
379
+ private lastFailure;
380
+ /**
381
+ * Whether a limiter damped a step during the current Newton iteration.
382
+ *
383
+ * An iterate the limiter moved is a step towards the solution, not the solution, so
384
+ * it cannot be allowed to satisfy the convergence test. See `limitedIterate`.
385
+ */
386
+ private limitedIterate;
387
+ /**
388
+ * Which element limited, as kind plus its nodes.
389
+ *
390
+ * `lastFailure` said *that* a limiter was active and never *which*, and on `boss-ge-7`
391
+ * that was the whole distance between "the convergence test refused" and a fix: 7
392
+ * op-amps, 2 FETs and a BJT share one boolean, and a stationary iterate barred by any
393
+ * one of them looks identical. Nodes rather than a device id because a stamp does not
394
+ * carry one.
395
+ */
396
+ private limitedBy;
397
+ /**
398
+ * The op-amp whose limiter fired on the current iteration, with what a fold reseed needs.
399
+ * `folded` means its cap sat within an eighth of a linear width. See `OPAMP_FOLD_STREAK`.
400
+ */
401
+ private limitedOpamp;
402
+ private maxNewtonIterations;
403
+ /**
404
+ * Cap-hit diagnostics, **off unless `VESSEL_LIMITER_TRACE=1`**. Both are written once
405
+ * per Newton iteration, so leaving the per-iteration array on costs ~1.7M pushes on a
406
+ * 3 s 48 kHz render of a stiff amp -- it grows without bound and it inflates the cost
407
+ * of the very solver anyone is benchmarking. Nothing in the repository reads them; they
408
+ * exist for an external probe reaching in past `private`, which is why the gate is an
409
+ * env var rather than a `prepare()` option.
410
+ */
411
+ private readonly limiterTrace;
412
+ private limitedIterationCount;
413
+ private limitedIterationPerSample;
414
+ /** Series ohms of whatever drives the input jack -- see `prepare()`'s option. */
415
+ private inputSourceOhms;
416
+ /**
417
+ * Seconds since `prepare`, for the sample being processed. The program's only clock.
418
+ *
419
+ * A sine EMF needs absolute time, and nothing else in a program does: every other stamp is a
420
+ * function of node voltages and reactive state. It is advanced **once per sample** inside the
421
+ * sample loop rather than per buffer, so a callback boundary cannot shift the waveform --
422
+ * the invariance the power-domain rectifier experiment measures as `rmsDelta = 0` across
423
+ * callback sizes. It is not a program field: `t = n / sampleRate` is computed from the rate
424
+ * `prepare` was given, so one program still runs at any rate.
425
+ */
426
+ private timeSeconds;
427
+ private elapsedSamples;
428
+ private samples;
429
+ private nonConvergedSamples;
430
+ /**
431
+ * Newton iterations that stopped moving while the equations stayed **unsatisfied** — stuck
432
+ * against a non-smooth boundary rather than converged. `nonConvergedSamples` counts these
433
+ * together with iterations still moving when the budget ran out, and the two need opposite
434
+ * fixes: a stall wants damping or a formulation change, a still-moving one wants a larger
435
+ * budget. Diagnostic only — nothing branches on it.
436
+ */
437
+ private stalledSamples;
438
+ /**
439
+ * Newton iterations that stopped moving with the equations **satisfied**, which the current
440
+ * test rejects because a limiter fired. Diagnostic only, and deliberately so: see the
441
+ * comment at the convergence test for the 37-packet gate failure that keeps it that way.
442
+ */
443
+ private solvedButFlaggedSamples;
444
+ /**
445
+ * Per-block Newton census: how many samples each block solved, how many **exhausted its
446
+ * iteration budget**, and the most iterations it ever used.
447
+ *
448
+ * **Per block, because the level a counter aggregates at is part of its meaning.**
449
+ * `nonConvergedSamples` sums at the host and `peakIterations` at the host, so a *secondary*
450
+ * block running out of budget is invisible in both — which is how a cap raise moved ten
451
+ * packets that those two counters said it could not touch, `electro-harmonix-q-tron` among
452
+ * them at +0.9 dB with a clean bill of health from every failure counter.
453
+ *
454
+ * **And the identity is load-bearing, not decoration.** "Some block here exhausted" detects
455
+ * the affected class but cannot predict a packet's delta, and predicting each affected
456
+ * packet's delta in advance is the whole point of the two-sided gate. Which block, how many
457
+ * of its samples, and how badly is what supports a prediction.
458
+ */
459
+ /**
460
+ * Blocks on probation, and the sample index each one's probation ends at.
461
+ *
462
+ * A block lands here when one of its samples stalled, or was already solved, at the moment
463
+ * the budget ran out. Both verdicts say more iterations cannot help *that sample*, so the
464
+ * ones after it are bought at `NEWTON_UNPRODUCTIVE_ITERATIONS` instead of the full budget.
465
+ *
466
+ * **It is a probation and not a sentence, and the difference is a packet that plays.** This
467
+ * was a `Set` with no expiry until 2026-09-19: one qualifying sample revoked a block's budget
468
+ * for the rest of the render, so a block that needs a one-off large budget never got one
469
+ * again. `moogerfooger-mf-102` is the case -- it needs 183 iterations once and 2.1 per sample
470
+ * after that (see `DEFAULT_NEWTON_MAX_ITERATIONS`), and under the permanent form it was
471
+ * capped at 64 from its first flagged sample onward and rendered **digital silence** for the
472
+ * whole render, while the C++ console, which has no revocation at all, played it. Measured
473
+ * 2026-09-19 at 1 kHz/0.1 V over 4800 samples: RMS `0.00e+0` -> `3.10e-2`, non-converged
474
+ * samples 4800 of 4800 -> 22.
475
+ */
476
+ private readonly unproductiveUntilSample;
477
+ private readonly blockNewtonCensus;
478
+ private totalIterations;
479
+ private nonFiniteSamples;
480
+ private peakIterations;
481
+ /** Whether any block failed on the sample being processed, counted once per sample. */
482
+ private sampleNonConverged;
483
+ private sampleNonFinite;
484
+ /**
485
+ * Blocks whose operating point did not solve. Those keep zero state -- the old
486
+ * behaviour -- and this counts them so a silent fallback cannot look like a settled
487
+ * circuit.
488
+ */
489
+ private operatingPointFailures;
490
+ private operatingPointSupplyAmps;
491
+ private renderedSupplyPeakAmps;
492
+ /**
493
+ * Where each supply branch's current sits in each block's solution, resolved once.
494
+ *
495
+ * The per-sample cost of the rendered peak has to be a handful of array reads, not a walk of
496
+ * every stamp in the program: `branchCurrentSnapshot` does that walk and is a diagnostic
497
+ * called once, while this runs inside the sample loop.
498
+ */
499
+ private supplyBranchSlots;
500
+ /**
501
+ * The operating point is solved once, on the first `process` after `prepare`, and
502
+ * deliberately not on `setControl`.
503
+ *
504
+ * Deferring it that far is what lets a caller set its knobs first, so the circuit
505
+ * settles at the positions it will be played at. Not re-solving afterwards is the
506
+ * physical answer: powering on establishes an operating point, and turning a knob
507
+ * after that is a transient the solver follows. Re-solving would discard the reactive
508
+ * memory mid-stream, which is both a click and a circuit that does not exist.
509
+ */
510
+ private operatingPointPending;
511
+ /**
512
+ * Block lookup by id, built once here rather than searched per use.
513
+ *
514
+ * `process`'s sample loop walks `program.order` and previously resolved each id with
515
+ * `program.blocks.find(...)` -- a linear scan repeated for every scheduled block on every
516
+ * sample. `program.blocks` does not change after construction, so the scan's answer
517
+ * cannot change either; this trades an O(blocks) search, paid every sample, for one map
518
+ * build paid once.
519
+ */
520
+ private readonly blocksById;
521
+ /**
522
+ * Control lookup by id, built once for the same reason as `blocksById`.
523
+ *
524
+ * `applyStamp`'s `controlled-conductance`/`controlled-resistance` cases resolved the
525
+ * owning control with `program.controls.find(...)` to read its taper -- a scan repeated
526
+ * for every such stamp, every Newton iteration, every sample. Hotter than the block scan
527
+ * by a wide margin (stamps x iterations x samples versus blocks x samples), and the same
528
+ * fix applies: `program.controls` is fixed after construction.
529
+ */
530
+ private readonly controlsById;
531
+ /**
532
+ * A small integer per block, in place of `block.id` (a string), for the packed history
533
+ * keys `packHistoryKey2`/`3` build. Built once here from `program.blocks`'s fixed order,
534
+ * same as `blocksById`. See `bjtHistory`'s comment for why the key needs a block
535
+ * component at all: two blocks (a real shape -- `boss-hm-2` has two) can each stamp a
536
+ * device at the same local node numbers, and those must not collide with each other the
537
+ * way two co-located devices *within* one block are meant to.
538
+ */
539
+ private readonly blockIndexById;
540
+ /**
541
+ * Reusable Newton-iteration scratch, one entry per MNA block, sized once in `prepare`.
542
+ *
543
+ * Every Newton iteration used to allocate a fresh N x N matrix and an N-vector rhs
544
+ * (`zeros(size)`, `new Array(size).fill(0)`), and `solve` then made its own defensive
545
+ * copies of both before eliminating -- four allocations, up to N^2 numbers each, for
546
+ * every iteration of every block of every sample. Reused here: the same arrays are
547
+ * zeroed in place at the top of each iteration instead of replaced, and `solve` now
548
+ * eliminates directly on the caller's buffers (see its own comment for why that is safe).
549
+ *
550
+ * `solutionA`/`solutionB` close the gap that reuse left open: `solve` used to return a
551
+ * fresh N-length array every iteration (`iterate`'s closing comment used to say this was
552
+ * deliberate, because the alternative -- writing into one of two buffers `iterate` keeps
553
+ * across iterations -- hadn't been done yet). Measured on `pro-co-rat`, whose 33x33 block
554
+ * needs ~3.2 Newton iterations a sample on average: that line alone was 5.5% of total
555
+ * runtime self-time under `--cpu-prof-md`, on top of `solve`'s own 56%. `iterate` now pings
556
+ * between these two instead, so `solve` writes into whichever one is not currently `current`
557
+ * -- see `iterate`'s and `solve`'s own comments for why swapping is safe.
558
+ */
559
+ private readonly iterationScratch;
560
+ /**
561
+ * Per-block static elimination schedules, built once in `prepare()` and replayed by
562
+ * `iterate` in place of the dense `solve` -- see `buildSparseSchedule`.
563
+ *
564
+ * A block appears here only when the schedule is worth replaying (`solverPlan()` reports
565
+ * the split and its reason). Absent means the dense path, which is also where a block
566
+ * lands mid-solve if its pivot guard trips.
567
+ */
568
+ private readonly sparseSchedules;
569
+ /**
570
+ * Precomputed base matrices from constant stamps for each MNA block.
571
+ *
572
+ * Fold constant, control-free, dynamic-free stamp values into a precomputed base
573
+ * matrix and RHS vector per block, initialized with standard GMIN and ground row.
574
+ * During audio processing, iterate only copies the base matrix and applies the
575
+ * non-constant stamps.
576
+ */
577
+ private readonly baseMatrices;
578
+ /** Blocks that gave up their schedule at runtime, for `solverPlan()` to report. */
579
+ private readonly abandonedSchedules;
580
+ /**
581
+ * Blocks whose shipped elimination order divides by a pivot the assembled
582
+ * operating-point matrix cannot support, dropped to the dense solve at the
583
+ * first `process()` call. See `settlePivotOrders`.
584
+ */
585
+ private readonly droppedSchedules;
586
+ /**
587
+ * Blocks whose shipped order failed numeric validation and whose replacement
588
+ * order (threshold Markowitz on the assembled operating-point matrix) was
589
+ * adopted instead of dropping to dense. See `computeNumericRepivot`.
590
+ */
591
+ private readonly repivotedSchedules;
592
+ /** Set once the shipped orders have been validated against the real matrix. */
593
+ private pivotOrdersSettled;
594
+ /** Why each block took the path it did, for `solverPlan()`. */
595
+ private readonly solverPlanRows;
596
+ /**
597
+ * How many solves hit a pivot below `SCHEDULE_PIVOT_FLOOR` and ran again densely, against
598
+ * how many the schedule ran at all. A fallback is correct but not free: it pays for the
599
+ * schedule and then the dense solve. A rate that is not small means that block is not
600
+ * suited to a static order, not that the net is doing its job.
601
+ */
602
+ private scheduleFallbacks;
603
+ private scheduleSolves;
604
+ /**
605
+ * Build-order step 6, vertical slice: which blocks run through symbolic elimination
606
+ * instead of `iterate`'s full-system Newton solve.
607
+ *
608
+ * **Deliberately opt-in, per block id, and empty by default.** Every packet other than
609
+ * one named for this experiment takes the untouched `iterate` path; nothing here changes
610
+ * unless a caller explicitly asks for it. The plan's own rule ("the stamp interpreter is
611
+ * never deleted out from under that role until behavioural parity replaces it by
612
+ * decision") is honoured by construction: `iterate` is not touched, is still what
613
+ * `solveOperatingPoint`'s continuation methods use unconditionally, and is exactly what
614
+ * this path is diffed against -- see `scripts/report-elimination-parity.ts`.
615
+ */
616
+ private readonly eliminateBlocks;
617
+ /**
618
+ * Per-block port analysis, computed once in `prepare()` from the block's own stamps
619
+ * (`computeNewtonPortRows`) -- never re-derived per sample, since the set of rows a
620
+ * stamp's law can write to is a static fact about the program, not the operating point.
621
+ */
622
+ private readonly eliminationPorts;
623
+ /**
624
+ * Reused per-sample/per-iteration scratch for the eliminated path, sized once per block
625
+ * in `prepare()` -- the same discipline `iterationScratch` already follows, for the same
626
+ * reason: an allocation inside the hot loop is exactly the cost this path exists to
627
+ * remove.
628
+ *
629
+ * **`ll`/`permutation`/`z`/`kReduced` are control-generation-cached, not per-sample.**
630
+ * `M_lin[L][L]`'s factorisation, the port-response matrix `Z`, and `K_reduced` all depend
631
+ * only on control positions -- never on the time-varying input or on capacitor history --
632
+ * exactly the `linear && controlFree` distinction build-order step 3/4 already established
633
+ * for baking a whole block's realisation. `iterateEliminated` rebuilds these three only
634
+ * when `cachedGeneration` disagrees with `this.controlGeneration`; every sample still
635
+ * rebuilds `linear`/`linearRhs` (cheap, and needed for the RHS regardless) and always
636
+ * recomputes `z0`/`uReduced` fresh, since those depend on capacitor state and the sample's
637
+ * own input.
638
+ */
639
+ private readonly eliminationScratch;
640
+ constructor(program: Program, options?: {
641
+ readonly eliminateBlocks?: ReadonlySet<string>;
642
+ });
643
+ /**
644
+ * The only entry point that accepts a sample rate. No default exists.
645
+ *
646
+ * The Newton iteration cap is a real CPU-versus-accuracy knob, the same role
647
+ * SPICE's ITL settings play, so a host may lower it. Lowering it does not make
648
+ * the runtime lie: samples that then fail to converge are held and counted.
649
+ *
650
+ * `realtimeBudget` is the real-time admission gate (see `./admission.ts`): supplying
651
+ * one asks "can this program be shown to fit", refusing by name and by number when it
652
+ * cannot. Omitting it opts out of the gate entirely -- this runtime has no honest
653
+ * per-machine default to fall back on.
654
+ */
655
+ prepare(sampleRate: number, options?: {
656
+ readonly maxNewtonIterations?: number;
657
+ readonly realtimeBudget?: RealtimeBudget;
658
+ /**
659
+ * Series impedance of whatever drives the input jack, in ohms. **A console
660
+ * setting, not a program one** -- the same compiled pedal is driven by a guitar
661
+ * (~5-15 kOhm plus pickup inductance), a buffered board (~100 Ohm), or a stiff
662
+ * test source (0), and none of that belongs in the ROM.
663
+ *
664
+ * Defaults to 0, the historical stiff-drive behavior. It exists because a fuzz
665
+ * face's defining dynamics ARE the loading interaction between its low, nonlinear
666
+ * input impedance and the pickup's source impedance; driven from an ideal source
667
+ * it renders at constant maximum harshness -- found by ear on the 2026-08-18
668
+ * listening pass, twice, either side of the cabinet IR landing.
669
+ */
670
+ readonly inputSourceOhms?: number;
671
+ /**
672
+ * Solver sub-samples per host sample. **A console setting, not a program one**, for
673
+ * the same reason `inputSourceOhms` is: the rate a host runs at is not a property of
674
+ * the circuit, and decision 3 already puts sample rate on this side of the
675
+ * boundary.
676
+ *
677
+ * **Why it earns its place, measured 2026-09-07.** At 4x,
678
+ * `report:compiler-spice-parity` goes from 47 to 50 `agrees` over 118 packets, five
679
+ * packets move and every one improves, and none loses agreement. It also removes
680
+ * `boss-ce-5`'s full-scale limit cycle -- 4.000 V of AC from silence at 48 kHz,
681
+ * 1.01e-3 at 96 kHz -- and that cycle is *gone* rather than folded down, so raw,
682
+ * zero-order- and boxcar-decimated output are identical at every rate.
683
+ *
684
+ * **Why it is not on by default.** Cost is about 3.2x the solver work at the corpus
685
+ * median (2.6 iterations per output sample against 8.3), because everything floors
686
+ * at 2.0-2.4 iterations per sub-sample whatever the rate and only packets above that
687
+ * floor save anything. Those are the stiff ones: `boss-ce-5` costs 1.23x and has the
688
+ * limit cycle, `mxr-phase-45` costs 3.84x and already agrees at 0.9710. A global
689
+ * multiplier spends the most where it buys least, so the factor belongs with whoever
690
+ * knows the packet -- `report-newton-deadline` computes the stiffness signal.
691
+ *
692
+ * **What it does not do.** Decimation here is zero-order: the last sub-sample of each
693
+ * host sample is the output. That is what the 47-to-50 measurement was taken with. A
694
+ * band-limiting decimator is a real improvement for broadband content and is *not*
695
+ * implemented, because a single 1 kHz tone cannot justify a filter choice.
696
+ */
697
+ readonly oversample?: number;
698
+ }): void;
699
+ /** Read after processing. Non-zero failure counts mean the audio is not trustworthy. */
700
+ telemetry(): RuntimeTelemetry;
701
+ /** Read-only solved MNA node voltages for diagnostics after prepare/process. */
702
+ nodeVoltageSnapshot(): readonly RuntimeNodeVoltageSnapshot[];
703
+ /**
704
+ * Read-only solved branch currents after prepare/process. See `RuntimeBranchCurrent` for
705
+ * the sign convention.
706
+ *
707
+ * Ordered by block, then by the stamp order within it, so a caller pairing these against
708
+ * `program.blocks[].stamps` sees the same sequence.
709
+ */
710
+ branchCurrentSnapshot(): readonly RuntimeBranchCurrent[];
711
+ /**
712
+ * Get the physical, causal brightness of any LED or indicator in the circuit.
713
+ *
714
+ * Brightness is a normalized value between 0.0 (completely dark) and 1.0 (fully glowing),
715
+ * calculated in real-time from the physical forward current flowing through the LED diode junction.
716
+ */
717
+ getLedBrightness(deviceId: string): number;
718
+ setControl(id: ControlId, position: number): void;
719
+ /**
720
+ * Block base-matrix rebuilds performed by `setSupply` so far. Read-only:
721
+ * re-applying the values a stamp already has rebuilds nothing and leaves this
722
+ * unchanged.
723
+ */
724
+ get supplyRebuilds(): number;
725
+ /**
726
+ * Every addressable `dc-source` stamp in `program.blocks` order, then stamp
727
+ * order within each block. `blockIndex` is the index into `program.blocks`;
728
+ * pass `infos.map((info) => info.address)` straight to `setSupply`.
729
+ */
730
+ getSupplies(): readonly SupplyInfo[];
731
+ /**
732
+ * Retarget one or more supply stamps between `process()` calls: every named
733
+ * `dc-source` stamp solves `V(positive) - V(negative) - sourceOhms * i =
734
+ * volts` with the new values from the next sample on.
735
+ *
736
+ * Call between `process()` calls, never concurrently with one -- the same
737
+ * contract as `setControl`. Effect starts at the next sample: the affected
738
+ * blocks' cached base matrices are rebuilt exactly the way `prepare()`
739
+ * builds them, and `controlGeneration` is bumped so the eliminated-path
740
+ * factorisation, Z, and K_reduced rebuild on next use. The sparse schedule is
741
+ * untouched: its pattern is value-independent.
742
+ *
743
+ * Reactive state (capacitor and op-amp pole memory) carries over, which is
744
+ * the physically honest response to hot-swapping a battery, and the DC
745
+ * operating point is deliberately NOT re-solved -- same transient-following
746
+ * rule as `setControl`'s `operatingPointPending` comment.
747
+ *
748
+ * Validation mirrors `setControl` and is atomic: `volts` must be finite,
749
+ * `sourceOhms` must be finite and non-negative (a negative resistance is
750
+ * gain, not sag), and every address must name an existing `dc-source` stamp
751
+ * (unknown block index, non-MNA block, or a `sourceIndex` with no
752
+ * `dc-source` all throw `RuntimeError`). All addresses are validated before
753
+ * any state is touched, so a bad address leaves even the valid ones in the
754
+ * same call unchanged. An empty address list changes nothing, and a call
755
+ * whose values already match the stamps rebuilds nothing (`supplyRebuilds`
756
+ * unchanged).
757
+ *
758
+ * Never mutates the caller's `Program`: the touched stamp objects are
759
+ * replaced on this runtime's own copies (copy on write), so two runtimes
760
+ * built from one program, or a frozen program, never interact.
761
+ *
762
+ * NOTE (cross-repo, gated by the extraction plan): the C++ console needs the
763
+ * same setter -- update its loaded program's supply fields and re-derive
764
+ * whatever it caches from them, with parity rows in the workbench harness.
765
+ * This method changes no `Program` or `Stamp` shape, so it triggers no
766
+ * format bump.
767
+ */
768
+ setSupply(addresses: readonly SupplyAddress[], volts: number, sourceOhms: number): void;
769
+ /**
770
+ * Fold one block's constant stamps into its cached base matrix and RHS, with
771
+ * the standard GMIN and ground-row pinning. This is the `prepare()` build,
772
+ * factored so `setSupply` rebuilds exactly what `prepare()` built.
773
+ */
774
+ private rebuildBaseMatrix;
775
+ /**
776
+ * Decide, once per block, whether its static elimination schedule is worth replaying.
777
+ *
778
+ * The rule is a **cost ratio computed from the block's own pattern**, not a size cutoff.
779
+ * Two reasons, both measured. Dense cost is not a function of `size` alone -- `solve`
780
+ * skips a zero multiplier, so two 28-unknown blocks in this corpus differ by 4x in dense
781
+ * wall clock -- and node counts are about to shrink when the compiler stops sizing a block
782
+ * by its largest authored node *label*, which would silently move any absolute threshold.
783
+ * A ratio re-derives itself correctly on both counts.
784
+ *
785
+ * The floor is set where the measured wall-clock win stops being worth the branch: at a
786
+ * predicted saving of 4x the smallest corpus block that clears it measured 2.9x real, and
787
+ * the one that does not clear it measured 2.0x on a 4-unknown block that costs nothing
788
+ * either way.
789
+ */
790
+ private planSparseSchedule;
791
+ /**
792
+ * Assemble one block's audio-structure Jacobian at the solved operating
793
+ * point, for validating the shipped elimination order before the first
794
+ * audio sample replays it.
795
+ *
796
+ * This mirrors `iterate`'s standard audio pass exactly -- base-matrix copy,
797
+ * non-constant stamps at the operating-point voltages with zero input,
798
+ * ground-row overwrite -- so the matrix is what the first Newton iteration
799
+ * would stamp. `applyStamp` also advances limiter histories, divider state
800
+ * and clock/bucket state, which belong to the audio run, not to this probe:
801
+ * every mutable stamp state it touches is snapshotted and restored, and
802
+ * the state vector itself is passed as a copy, never live. A unit test
803
+ * pins that the audio is bit-identical with and without this call.
804
+ */
805
+ private assembleAudioMatrix;
806
+ /**
807
+ * Validate every admitted block's shipped elimination order against the
808
+ * assembled operating-point matrix, once, before the first audio sample.
809
+ *
810
+ * The order was chosen from the stamp pattern alone, where every node
811
+ * diagonal reads as a pivot candidate because the runtime adds `gmin` to
812
+ * it. A pivot of `1e-12` against entries of order `1e2` is a growth factor
813
+ * of `1e14` that dense partial pivoting would never choose, and replaying
814
+ * it is what makes the sparse and dense solves disagree on the row-6
815
+ * packets (`boss-aw-2` never converges sparsely and converges in four
816
+ * iterations densely).
817
+ *
818
+ * The gate is a comparison, not a pivot theory: both solves run on the
819
+ * same assembled matrix and their answers are compared. Per-pivot ratios
820
+ * do not separate healthy blocks from broken ones -- healthy `boss-ce-5`
821
+ * carries 18 pivots below any absolute-scaled threshold and still matches
822
+ * dense to `1e-10`, because a tiny pivot is harmless late in the order
823
+ * and fatal early -- so the ratios stay on as diagnostics while the
824
+ * disagreement drops the block. A dropped block renders exactly what the
825
+ * forced-dense control renders; correctness first, and the cost of the
826
+ * dense path belongs to phase 2.
827
+ *
828
+ * The tolerance sits in a measured canyon: the largest disagreement among
829
+ * kept corpus blocks is `boss-tw-1` at `7.1e-5` (whose sparse audio agrees
830
+ * with dense to `1e-9` and converges 100% -- dropping it would regress its
831
+ * 0.83x budget fix to ~850x, so the gate must NOT fire there), while the
832
+ * smallest dropped disagreement is `boss-aw-2` at `9.8e+3`. Dense is not
833
+ * assumed right -- `tw-1` diverges on 61 samples densely while converging
834
+ * sparsely -- the gate only fires where the replay demonstrably collapses.
835
+ */
836
+ private settlePivotOrders;
837
+ /**
838
+ * Which blocks took the static schedule and which kept the dense solve, and how often the
839
+ * pivot guard sent a solve back to the dense path.
840
+ *
841
+ * Exists so "the sparse solver landed" is never read as "every block got faster". Valid
842
+ * only after `prepare()`.
843
+ */
844
+ solverPlan(): {
845
+ readonly blocks: readonly {
846
+ readonly blockId: string;
847
+ readonly size: number;
848
+ readonly patternEntries: number;
849
+ readonly sparseOps: number;
850
+ readonly denseOps: number;
851
+ readonly unprovenPivots: number;
852
+ readonly path: "sparse" | "dense";
853
+ readonly reason: string;
854
+ readonly pivotViolations: number;
855
+ readonly worstPivotRatio: number | null;
856
+ readonly pivotDisagreement: number | null;
857
+ readonly repivoted: boolean;
858
+ }[];
859
+ /** Blocks that gave the schedule up mid-run after a run of pivot-guard trips. */
860
+ readonly abandoned: readonly string[];
861
+ /** Blocks whose shipped order failed numeric validation at the operating point. */
862
+ readonly dropped: readonly string[];
863
+ /** Blocks running a numeric re-pivot order instead of their shipped order. */
864
+ readonly repivoted: readonly string[];
865
+ readonly scheduleSolves: number;
866
+ readonly scheduleFallbacks: number;
867
+ };
868
+ process(input: Float64Array): Float64Array;
869
+ /**
870
+ * The rate the caller passed to `prepare`, as opposed to the solver's sub-sample rate.
871
+ *
872
+ * `this.sampleRate` is the rate the circuit is actually solved at; a console that needs to
873
+ * know what it asked for -- to size a buffer, or to report -- wants this one.
874
+ */
875
+ hostSampleRate(): number | null;
876
+ private processBlock;
877
+ /**
878
+ * The composition interpreter (board-p3 row 4): a data-driven graph of
879
+ * closed-vocabulary primitive ops, evaluated top to bottom each sample.
880
+ * No pedal id, no part number, no registry lookup -- the program the
881
+ * packet supplies is the whole instruction, which is what makes this a
882
+ * machine that runs a program rather than a library of classes the
883
+ * compiler dispatches to.
884
+ *
885
+ * Evaluation order is load-bearing for the bit-identical gate: float
886
+ * addition is not associative, so ops run strictly in listed order and a
887
+ * source may only name the block input, a constant, or an earlier op's
888
+ * output. Feedback flows through delay-line state, never through
889
+ * temporaries.
890
+ */
891
+ /**
892
+ * What a `parameter`-mode tap reads for one line: a 0..1 fraction of its range, or a tapped
893
+ * length in samples, or neither (the tap then reads capacity, the cited maximum).
894
+ *
895
+ * Three sources, never two. A line's own `sweep` wins: `scanned` reads its control by
896
+ * position through the control's taper, `tapped` reads the interval between the control's
897
+ * last two presses times `ratio`. With no sweep, the block's solved `parameter` node, which is
898
+ * a registry model's reading.
899
+ */
900
+ private composedLineReading;
901
+ /**
902
+ * A delay core's DC transfer, derived from its own ops for the
903
+ * operating-point solve: at steady state every delay line holds what is
904
+ * pushed into it, so taps read line values and pushes update them, iterated
905
+ * to a fixed point. Ops that are DC-blocking by construction
906
+ * (`filter-dcblock`) push 0, so a core behind a DC block correctly reports
907
+ * no transfer; `mix` applies its gains, including swept ones. Anything
908
+ * else (combs, allpasses, pitch, hold, trackers) makes the transfer
909
+ * ill-defined and returns null -- the macro then reads 0 during the
910
+ * solve, exactly as before, rather than a guess.
911
+ *
912
+ * `null` also for programs owning no delay lines (reverb owns combs, not
913
+ * lines): those keep their established no-DC behavior, which this
914
+ * derivation must reproduce rather than revisit. No per-chip flag, no
915
+ * name reading; the ops are the whole input. The loop is contractive
916
+ * exactly when the program's own feedback is (gain < 1); capped well
917
+ * past any such settling, with null on non-convergence.
918
+ */
919
+ private composedDcTransfer;
920
+ private processComposedBlock;
921
+ /**
922
+ * One Newton solve of a block, shared by a sample and by the operating point.
923
+ *
924
+ * Extracted rather than copied: the operating point differs from a sample only in
925
+ * which elements conduct, and a second copy of the limiter rule, the gmin row, the
926
+ * ground pin and the convergence test would drift from this one and then describe a
927
+ * circuit the sample loop does not solve.
928
+ */
929
+ private iterate;
930
+ /**
931
+ * Build-order step 6, vertical slice: the part of a block's matrix that is the same for
932
+ * every Newton iteration of this sample -- every stamp `newtonPortRowsOf` calls linear,
933
+ * plus `gmin` and the ground pin, in exactly `iterate`'s own order. Rebuilt once per
934
+ * sample (not once per iteration, and not once per program) because a control-bearing
935
+ * stamp's conductance can move between samples even though it never moves within one.
936
+ *
937
+ * Nonlinear stamps are skipped entirely here -- their current lives only in
938
+ * `reduceNonlinearContribution`, re-evaluated at the current iterate every time, exactly
939
+ * as `iterate` re-evaluates them every time. `solution` is passed as an all-zero
940
+ * placeholder because every linear stamp kind ignores it (verified by inspection of
941
+ * `applyStamp`'s own linear cases); a nonlinear stamp reaching here would read garbage,
942
+ * which is exactly why it must never be one of the stamps this loop calls.
943
+ */
944
+ private buildLinearBackground;
945
+ /**
946
+ * Build-order step 6, vertical slice: `iterate`'s exact per-sample contract, produced by
947
+ * symbolic elimination around the block's nonlinear devices instead of a full-system
948
+ * Newton solve every iteration.
949
+ *
950
+ * **The method, in one paragraph.** Partition every row into `P` (the union of rows a
951
+ * nonlinear law writes, `computeNewtonPortRows`) and `L` (everything else). `M_lin[L][L]`
952
+ * is eliminated once per sample, carrying the `P`-count port-response columns and the
953
+ * constant column `b_L` together (`solveMulti`) -- this is the factorisation `iterate`
954
+ * pays for fresh on every Newton iteration, paid here exactly once. `K_reduced` and
955
+ * `u_reduced` fold `M_lin[P][P]` and `b_lin[P]` against that response, giving the small
956
+ * `|P| x |P|` system every Newton iteration on the *port* voltages actually solves;
957
+ * `reduceNonlinearContribution` adds each iteration's own nonlinear current and Jacobian,
958
+ * chain-ruled through the same response so an L-node a law reads (an op-amp's inputs, a
959
+ * FET's gate) is exactly as correct as one it does not. `x_L` is reconstructed from the
960
+ * current port voltages every iteration via one cheap matrix-vector product, never solved
961
+ * for directly, so every node's value is always available to the laws and to the
962
+ * convergence test without re-deriving anything.
963
+ *
964
+ * **What must stay identical to `iterate` for the exactness gate to mean anything**: the
965
+ * convergence test (`withinTolerance` plus `!limitedIterate`, same functions), the
966
+ * limiter/history side effects (the same `applyStamp`, the same history maps, so a
967
+ * limited iterate here is limited for the identical reason it would be in `iterate`), and
968
+ * the return contract (`solution` is the latest iterate whether or not it converged,
969
+ * matching `iterate`'s own "advance the clock even on failure" rule one level up in
970
+ * `processBlock`).
971
+ */
972
+ private iterateEliminated;
973
+ /**
974
+ * Solve every block at DC and start the reactive elements from that answer.
975
+ *
976
+ * **Why this exists.** A run that starts from zero state starts with every capacitor
977
+ * at 0 V, which is a *short*, and a short across a bias network accidentally supplies
978
+ * a bias path the circuit does not have. So a stage that cannot hold an operating
979
+ * point still has real gain for the first fraction of a second, and then cuts off as
980
+ * the capacitors charge. Measured on named pedals: `ibanez-ts808` renders a gain of
981
+ * 0.748 cold, 0.206 after half a second, and 0.004 after two -- three different
982
+ * answers about one pedal, only the last of which is the pedal.
983
+ *
984
+ * At DC a capacitor is an open circuit and an inductor is a short, which is the only
985
+ * difference between this solve and a sample. The result is written back as the state
986
+ * a settled circuit would have reached: a capacitor holding its DC voltage and
987
+ * carrying no current, an inductor with no voltage across it and carrying the DC
988
+ * current the solve found.
989
+ *
990
+ * **What this does not do:** make a badly biased circuit sound right. If a packet has
991
+ * no DC path to a base, the operating point says so immediately instead of after two
992
+ * seconds of charging. That is the point -- it turns a wait into an answer -- but a
993
+ * recovered pedal is not among the things it promises.
994
+ *
995
+ * A block that fails to converge here keeps zero state, which is exactly the old
996
+ * behaviour, and is counted so the failure is never silent.
997
+ */
998
+ private solveOperatingPoint;
999
+ /**
1000
+ * One block's operating point, with **gmin stepping** as a fallback.
1001
+ *
1002
+ * SPICE's first continuation method, and the reason ngspice solves decks this runtime
1003
+ * refuses. A circuit that will not converge at `gmin = 1e-12` is often easy at
1004
+ * `1e-2`, because a hundred-ohm conductance from every node to ground swamps the
1005
+ * nonlinearities and leaves something close to a resistor network. Solve that, use its
1006
+ * answer to seed a solve at a tenth the conductance, and walk down: each problem is a
1007
+ * small deformation of the one already solved, so Newton starts inside the basin.
1008
+ *
1009
+ * `null` when even the final pass at the true `gmin` fails. **A raised gmin is not an
1010
+ * answer** -- it is a different circuit, with leakage the packet does not contain -- so
1011
+ * the walk is only ever a source of initial guesses, and the value returned is always
1012
+ * from a pass at `GMIN_SIEMENS`.
1013
+ *
1014
+ * The direct attempt comes first and short-circuits, so nothing that already converges pays
1015
+ * for either continuation. Before the corpus's declared transistor types were read, all 52
1016
+ * compiled packets solved on that direct pass and neither method was ever reached;
1017
+ * `boss-bd-2-blues-driver` is the first packet measured to need one, and it needs the
1018
+ * *second* -- gmin stepping fails on it and source stepping solves it, which is why both
1019
+ * exist rather than only the first.
1020
+ */
1021
+ private solveOperatingPointForBlock;
1022
+ /**
1023
+ * Continuation on the *devices*: walk down from a conductance that makes the circuit nearly
1024
+ * linear, each step seeding the next.
1025
+ *
1026
+ * That sequencing is the whole mechanism -- the chain of problems is continuous even though
1027
+ * any single jump would not converge on its own.
1028
+ */
1029
+ private solveByGminStepping;
1030
+ /**
1031
+ * Continuation on the *excitation*: power the circuit up from zero in linear steps.
1032
+ *
1033
+ * SPICE's second method, tried when gmin stepping has already failed, because the two fail on
1034
+ * different circuits. Gmin stepping swamps a nonlinearity with leakage, which works when the
1035
+ * difficulty is a device's curvature; it does nothing about a circuit whose difficulty is
1036
+ * *which* of several states the supplies put it in. With every supply at zero every junction
1037
+ * is unambiguously off -- a state Newton reaches trivially -- and each step up is a small
1038
+ * perturbation of an answer already in hand.
1039
+ *
1040
+ * An unconverged intermediate step still seeds the next, as in the gmin walk. The value
1041
+ * returned is always from a final pass at full supply and the true `gmin`, so a partially
1042
+ * powered circuit is only ever a source of initial guesses and never an answer.
1043
+ *
1044
+ * One inconsistency worth naming: an `ideal-opamp` carries its rails on the stamp, resolved
1045
+ * from the supply set at compile time, so its clipping levels do not ramp with the supplies.
1046
+ * That makes the intermediate steps physically incoherent for an op-amp circuit and does not
1047
+ * affect the result, which is taken at full supply.
1048
+ */
1049
+ private solveBySourceStepping;
1050
+ private clearLimiterHistories;
1051
+ private snapshotLimiterHistories;
1052
+ private restoreLimiterHistories;
1053
+ /** Advance reactive memory. Trapezoidal, so it carries both across and through. */
1054
+ /**
1055
+ * Allocate one tank's springs. Called on first use rather than in `prepare()` so the buffer
1056
+ * sizes come from the host's actual rate, which is the same reason the macro ring buffer is
1057
+ * sized there: a delay is a time, and only the host knows how many samples that is.
1058
+ */
1059
+ private springTankState;
1060
+ /**
1061
+ * One sample of tank motion: drive in, springs run, pickup out.
1062
+ *
1063
+ * Each spring is a delay line with its dispersion chain **inside** the feedback loop, which
1064
+ * is what makes successive reflections progressively more smeared instead of identically
1065
+ * filtered echoes. Stability is structural rather than tuned: an allpass has unity magnitude
1066
+ * at every frequency, so the loop gain is exactly `feedback`, which is below 1 for any
1067
+ * positive decay time.
1068
+ */
1069
+ private advanceSpringReverb;
1070
+ private advanceReactiveState;
1071
+ /**
1072
+ * One per-sample step of the op-amp dominant pole's raw state, advanced from the solved
1073
+ * differential exactly as `advanceReactiveState` advances the capacitor state -- once, after
1074
+ * the solve, on every sample whether it converged or not, because the pole's clock runs
1075
+ * either way. Unrailed op-amps are virtual shorts with no pole state, so they are skipped.
1076
+ */
1077
+ private advanceOpAmpRawState;
1078
+ private applyStamp;
1079
+ }
1080
+ //# sourceMappingURL=reference-runtime.d.ts.map