@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.
- package/README.md +178 -0
- package/dist/admission.d.ts +94 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +209 -0
- package/dist/admission.js.map +1 -0
- package/dist/calibrate.d.ts +104 -0
- package/dist/calibrate.d.ts.map +1 -0
- package/dist/calibrate.js +420 -0
- package/dist/calibrate.js.map +1 -0
- package/dist/chain-advisories.d.ts +71 -0
- package/dist/chain-advisories.d.ts.map +1 -0
- package/dist/chain-advisories.js +268 -0
- package/dist/chain-advisories.js.map +1 -0
- package/dist/chain-scale.d.ts +82 -0
- package/dist/chain-scale.d.ts.map +1 -0
- package/dist/chain-scale.js +121 -0
- package/dist/chain-scale.js.map +1 -0
- package/dist/chain-slot.d.ts +130 -0
- package/dist/chain-slot.d.ts.map +1 -0
- package/dist/chain-slot.js +105 -0
- package/dist/chain-slot.js.map +1 -0
- package/dist/chain.d.ts +113 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +355 -0
- package/dist/chain.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/numeric-pivot.d.ts +4 -0
- package/dist/numeric-pivot.d.ts.map +1 -0
- package/dist/numeric-pivot.js +192 -0
- package/dist/numeric-pivot.js.map +1 -0
- package/dist/reference-runtime.d.ts +1080 -0
- package/dist/reference-runtime.d.ts.map +1 -0
- package/dist/reference-runtime.js +6743 -0
- package/dist/reference-runtime.js.map +1 -0
- package/dist/settle.d.ts +359 -0
- package/dist/settle.d.ts.map +1 -0
- package/dist/settle.js +900 -0
- package/dist/settle.js.map +1 -0
- package/dist/supply-ground.d.ts +22 -0
- package/dist/supply-ground.d.ts.map +1 -0
- package/dist/supply-ground.js +62 -0
- package/dist/supply-ground.js.map +1 -0
- package/dist/supply.d.ts +30 -0
- package/dist/supply.d.ts.map +1 -0
- package/dist/supply.js +10 -0
- package/dist/supply.js.map +1 -0
- package/dist/taper.d.ts +10 -0
- package/dist/taper.d.ts.map +1 -0
- package/dist/taper.js +34 -0
- package/dist/taper.js.map +1 -0
- package/dist/v2-wasm-engine.d.ts +91 -0
- package/dist/v2-wasm-engine.d.ts.map +1 -0
- package/dist/v2-wasm-engine.js +296 -0
- package/dist/v2-wasm-engine.js.map +1 -0
- package/dist/wasm/v2_dsp.cjs +22 -0
- package/dist/wasm/v2_dsp.wasm +0 -0
- 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
|