@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
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # @vessel-dsp/runtime
2
+
3
+ Headless real time MNA simulation runtime and solver console for compiled VesselDSP `Program` ROMs. It executes a `@vessel-dsp/compiler` program on audio streams with trapezoidal integration and damped Newton Raphson iteration. It depends on `@vessel-dsp/compiler` only.
4
+
5
+ ## Install
6
+
7
+ `@vessel-dsp/runtime` 0.1.0 is not yet on npm. Until the release is published, use the package from this repository's workspace rather than an install command that will 404.
8
+
9
+ ```bash
10
+ # once published:
11
+ bun add @vessel-dsp/runtime
12
+ ```
13
+
14
+ ## Minimal example
15
+
16
+ The constructor takes the program and optional block elimination only. The sample rate goes to `prepare()`, which has no default. `process()` takes a `Float64Array` and returns a new `Float64Array`.
17
+
18
+ ```ts
19
+ import { readFileSync } from "node:fs";
20
+ import { compile, pedalPartCatalog } from "@vessel-dsp/compiler";
21
+ import { ReferenceRuntime } from "@vessel-dsp/runtime";
22
+
23
+ const source = readFileSync("big-muff-pi.vdsp", "utf8");
24
+ const compiled = compile(source, { registry: pedalPartCatalog });
25
+ if (compiled.status !== "ok") throw new Error("compile failed");
26
+
27
+ const runtime = new ReferenceRuntime(compiled.program);
28
+ runtime.prepare(44100);
29
+
30
+ const input = new Float64Array(128);
31
+ for (let i = 0; i < input.length; i++) {
32
+ input[i] = 0.2 * Math.sin((2 * Math.PI * 220 * i) / 44100);
33
+ }
34
+ const output = runtime.process(input);
35
+ console.log("output length:", output.length);
36
+ console.log("output is Float64Array:", output instanceof Float64Array);
37
+ let peak = 0;
38
+ for (const v of output) peak = Math.max(peak, Math.abs(v));
39
+ console.log("output peak:", peak.toFixed(6));
40
+ ```
41
+
42
+ ```text
43
+ output length: 128
44
+ output is Float64Array: true
45
+ output peak: 0.265499
46
+ ```
47
+
48
+ The previous version of this example constructed `new ReferenceRuntime(program, 44100)` and called `process(inputBuffer, outputBuffer)` with `Float32Array` buffers. That throws `RuntimeError: prepare(sampleRate) was never called`, because the second constructor argument is an options object and the rate is only accepted by `prepare()`.
49
+
50
+ ## Construct, prepare, process
51
+
52
+ `new ReferenceRuntime(program, options?)` copies the program blocks it may later retarget (copy on write) and records control defaults. Options: `{ eliminateBlocks }`, a set of block ids solved by symbolic elimination.
53
+
54
+ `prepare(sampleRate, options?)` is the only place a rate enters. There is no default. It refuses unusable rates (`sample rate 5 is not usable; a program has no default rate`), programs needing operators or DSP models it does not implement (named in the message), and chains that fail the opt in admission gate. Options:
55
+
56
+ - `maxNewtonIterations` (default `DEFAULT_NEWTON_MAX_ITERATIONS`): CPU versus accuracy knob. Samples that exhaust it are held and counted in telemetry, never silently interpolated.
57
+ - `realtimeBudget`: opt in admission gate; omitted means no gating.
58
+ - `inputSourceOhms` (default 0): series impedance of whatever drives the input jack. A console setting, not a program one. A fuzz face driven from an ideal source renders at constant maximum harshness; a guitar source is several kOhm.
59
+ - `oversample` (default 1): integer sub samples per host sample. Zero order hold output (last sub sample wins); no band limiting decimator is implemented.
60
+
61
+ `process(input: Float64Array): Float64Array` renders one block and returns a new array. Any block length is accepted. `reset()` clears reactive state. `telemetry()` returns per run solver census: samples, non converged, stalled, solved but flagged, per block Newton census, non finite, peak iterations, operating point failures, `operatingPointSupplyAmps`, `renderedSupplyPeakAmps` (null until a sample renders), total iterations, last failure.
62
+
63
+ `setControl(id, position)` takes 0..1 and applies the program own taper. Unknown ids throw (`program has no control "NOPE"`); positions outside 0..1 throw (`control position 2 is outside 0..1`). There is no `getControl` on this class; the WASM engine has one, this class does not.
64
+
65
+ ## Supply control
66
+
67
+ `getSupplies()` lists every addressable `dc-source` stamp as `SupplyInfo` (`address`, `positive`, `negative`, `volts`, `sourceOhms`). `big-muff-pi` reports one supply: block 0, source 0, 9 V, 1 ohm.
68
+
69
+ `setSupply(addresses, volts, sourceOhms)` retargets supply stamps between `process()` calls with effect from the next sample. It validates every address before touching state, replaces the touched stamps on the runtime own copies (never the caller `Program`), rebuilds each affected block base matrix the way `prepare()` builds it, and bumps the control generation so eliminated path factorisation rebuilds. Re applying identical values rebuilds nothing. The DC operating point is deliberately not re solved and reactive state carries over.
70
+
71
+ What it does not do: no mutation of the caller `Program` (two runtimes from one program never interact; a frozen program never throws), no envelope follower or waveshaper, no audio domain processing of any kind. This is the TypeScript reference runtime only.
72
+
73
+ `supplyRebuilds` counts block base matrix rebuilds so far. Bad addresses throw: unknown block index, non MNA block, or missing source index, each naming the block and index. Non finite volts and negative or non finite `sourceOhms` throw.
74
+
75
+ ## Admission
76
+
77
+ `admissionVerdict(chain, sampleRate, budget)` answers whether an ordered list of programs can be shown to fit inside a host CPU budget. Costs add across slots; a one program caller passes a one element list. The budget carries `nsPerSolve`, `nsPerMacroSample` (required; an unpriced model is refused by name, never costed at zero), `budgetedIterationsPerSample` (default 64, a sustained policy allowance, not the solver cap), and `cpuBudgetFraction` (default 1). The verdict is `{ fits: true }` or `{ fits: false, reason }` with the worst offender named by slot, block, unknowns, and nanoseconds. `predictedWorstCaseNs` returns the same sum as a number, or null when a model has no price.
78
+
79
+ ## Settling and measurement
80
+
81
+ These render a program until its level stops moving, so startup transients are not measured as the circuit.
82
+
83
+ | Name | What it is |
84
+ | --- | --- |
85
+ | `findSettled` | First window at which the level holds still, or null. |
86
+ | `measureSettled` | Render until the output level stops moving and report it. |
87
+ | `measureSettledSignal` | Driven level with the silent part removed. |
88
+ | `measureSharedWindow` | Several drive levels over one shared settled window. |
89
+ | `measureInputAttributable` | Per node AC attributable to the input (driven minus silent). |
90
+ | `settledRender` | Render until still, in a value, refusal, or diverged shape. |
91
+ | `settledLevel` | `settledRender` level as a bare number, throwing when it cannot settle. |
92
+ | `settledSweep` | Settle once, then measure each control setting on the same runtime. |
93
+ | `settledSweepVerified` | Sweep with one point verified against an independent render. |
94
+ | `sweepMatchesIndependent` | Whether a sweep point matches an independent render. |
95
+ | `sweepIsOrderIndependent` | Whether sweep order changes the result. |
96
+ | `settleSelfCheck` | Known answer controls for the measurement primitives. |
97
+ | `SettleOptions` | Window, consecutive, stable dB, and ladder seconds. |
98
+ | `SETTLE_DEFAULTS` | Corpus measured defaults (0.5 s window, 2 consecutive, 0.25 dB). |
99
+ | `SettleCriterion` | Which criterion produced a measurement. |
100
+ | `Settled` | `{ rms, seconds }` of the settled window. |
101
+ | `SettledMeasurement` | `{ rms, peak, settleSeconds, renderedSeconds, flatAtZero }`. |
102
+ | `SettledRender` | Value, refusal, or diverged outcome of `settledRender`. |
103
+ | `SettledRenderOptions` | What to render. |
104
+ | `SettleRefusal` | Thrown by `settledLevel` instead of an unstandable level. |
105
+ | `SharedWindowMeasurement` | One drive behaviour over the shared window. |
106
+ | `AttributedMeasurement` | Driven level plus attributable fraction from one window. |
107
+ | `InputAttributableNode` | One node response with and without stimulus. |
108
+ | `FLAT_AT_ZERO_RMS` | Level below this (1e-6) is noise, not measurement. |
109
+ | `isFlatAtZero` | Whether a level is indistinguishable from noise. |
110
+
111
+ `findSettled([1, 1.001, 1.0005, 1.0001])` returns `{ rms: 1.0005, seconds: 1.5 }`; `findSettled([1, 2, 4, 8])` returns null. `measureSettled` on `big-muff-pi` at 48000 Hz with a 0.3 V 440 Hz sine reports rms about 0.2010, peak about 0.2491, settled in 1.5 s.
112
+
113
+ ## Chain runtime and advisories
114
+
115
+ | Name | What it is |
116
+ | --- | --- |
117
+ | `ChainRuntime` | Ordered slots where each slot output feeds the next. |
118
+ | `ChainSlot` | A program slot or an injected processor slot. |
119
+ | `ChainSlotTelemetry` | One slot telemetry labelled by position. |
120
+ | `programSlot` | A compiled circuit as a slot. |
121
+ | `processorSlot` | An injected processor (NAM, IR, cab) as a slot. |
122
+ | `ExternalProcessor` | A host supplied processor: NAM profile, impulse response, cab simulation. |
123
+ | `slotContract` | The contract a slot presents to seam arithmetic. |
124
+ | `SlotContract` | What the chain needs to know about a slot. |
125
+ | `slotLabel` | Human readable label for a slot. |
126
+ | `supplyGroundConflicts` | Slot pairs whose shared supply would short through grounds. |
127
+ | `SupplyGroundConflict` | One conflicting slot pair with message. |
128
+ | `chainAdvisories` | Seams where one side states no full scale, plus kind errors. |
129
+ | `ChainAdvisory` | One chain problem with code, slots, and message. |
130
+ | `chainScaleFactor` | Multiplier from two adjacent port full scales (null is unity). |
131
+ | `chainScaleAdvisories` | Boundaries where the chain fell back to unity scaling. |
132
+ | `ChainScaleAdvisory` | One unity fallback boundary with message. |
133
+ | `seamScale` | Multiplier applied to the signal crossing into a slot. |
134
+ | `seamDivider` | Resistive divider at a seam, or 1 when not computable. |
135
+ | `dacScaleFactor` | Worklet DAC multiplier so a rail swing does not peg output. |
136
+ | `outputDbfs` | Output level in dBFS at the DAC. |
137
+ | `outputConversionFullScale` | Declared 0 dBFS reference where stated, else derived ceiling. |
138
+ | `taperFraction` | Fraction of track at a 0..1 position for a taper kind. |
139
+ | `StageCoverage` | `instrument`, `preamp`, `speaker-electrical`, or `miked`. |
140
+
141
+ `ChainRuntime` gates the whole chain once against the admission budget, then prepares every slot ungated at the same rate. `process()` feeds slot outputs forward with `seamScale` between them. `setControl(slot, id, position)` addresses one slot; processor slots refuse with the slot named. `telemetry()` returns per slot telemetry, never blended.
142
+
143
+ `supplyGroundConflicts([muff, rangemaster])` reports that the positive ground treble booster and the negative ground muff cannot share one supply. `chainAdvisories` reports `unscalable-seam` where a full scale is null, `speaker-signal-into-input` where a speaker terminal feeds an instrument input, `cab-after-miked`, `undividable-seam`, and `instrument-into-speaker-stage`.
144
+
145
+ ## WASM engine
146
+
147
+ | Name | What it is |
148
+ | --- | --- |
149
+ | `V2WasmEngine` | Exported TypeScript wrapper around the C++ V2 console. Requires a build artifact the package does not ship; not usable from the npm package today. |
150
+ | `getV2WasmModule` | Loads the V2 Emscripten module from the missing build artifact. |
151
+
152
+ `V2WasmEngine` dynamically imports `../../build/v2_dsp.cjs`, a built artifact that is not in this repository and not in the package `files` (`dist`, `README.md`, `LICENSE.md`). From an installed tarball it fails with:
153
+
154
+ ```text
155
+ Error: Cannot find module
156
+ '/tmp/v2proof-proj/node_modules/@vessel-dsp/build/v2_dsp.cjs'
157
+ imported from
158
+ '/tmp/v2proof-proj/node_modules/@vessel-dsp/runtime/dist/v2-wasm-engine.js'
159
+ ```
160
+
161
+ Evidence: packed the runtime and compiler tarballs, installed both with npm into an empty project under `/tmp`, and called `getV2WasmModule()` and `V2WasmEngine.create()`. Both reject with the error above; the tarball contains `dist` only, no `build` directory. The export is kept as is (removing it is an API decision for the maintainer). Two options for the maintainer: ship the built artifact in the package `files`, or drop the export.
162
+
163
+ ## Reference runtime surface
164
+
165
+ | Name | What it is |
166
+ | --- | --- |
167
+ | `ReferenceRuntime` | The executable semantics of the program format. |
168
+ | `RuntimeTelemetry` | What the solver did, so failing to solve is never silent. |
169
+ | `RuntimeBranchCurrent` | One solved branch current per auxiliary unknown. |
170
+ | `RuntimeNodeVoltageSnapshot` | Per block node voltages. |
171
+ | `SupplyAddress` | `{ blockIndex, sourceIndex }` naming one `dc-source` stamp. |
172
+ | `SupplyInfo` | Address plus the stamp own positive, negative, volts, sourceOhms. |
173
+ | `DEFAULT_NEWTON_MAX_ITERATIONS` | How long a Newton loop may try before its iterate is the answer. |
174
+ | `RealtimeBudget` | Host measured solve costs plus iteration and fraction policy. |
175
+ | `AdmissionVerdict` | `{ fits: true }` or `{ fits: false, reason }`. |
176
+ | `admissionVerdict` | Whether a chain of programs fits inside a budget. |
177
+ | `predictedWorstCaseNs` | The admission worst case sum as a number, or null. |
178
+ | `RuntimeError` | Thrown for misuse: no prepare, bad rate, bad control, bad supply, unimplemented operator or model. |
@@ -0,0 +1,94 @@
1
+ import type { Program } from "@vessel-dsp/compiler";
2
+ /**
3
+ * What only a host can know, and what this repository has no honest default for.
4
+ *
5
+ * A laptop and an ESP32 differ here by orders of magnitude, and the *runtime
6
+ * implementation* is part of "machine" too -- this dense reference interpreter is
7
+ * measured at up to ~12x slower than the sparse plan it exists to be the reference for
8
+ * (`thoughts/shared/plans/2026-08-12-runtime-modules.md`, S3), so a constant baked in here
9
+ * would describe neither a real machine nor the shipped engine.
10
+ */
11
+ export type RealtimeBudget = {
12
+ /**
13
+ * This host's own measured cost, in nanoseconds, of one dense solve of a system with
14
+ * `unknownCount` unknowns, on the hardware and engine that will actually run the
15
+ * program. Called once per solved block per iteration it is charged for: a linear
16
+ * block is charged one call (it solves once per sample), a nonlinear block is charged
17
+ * `budgetedIterationsPerSample` calls -- the sustained allowance, not the solver's cap.
18
+ */
19
+ readonly nsPerSolve: (unknownCount: number) => number;
20
+ /**
21
+ * This host's own measured cost, in nanoseconds, of one per-sample step of the named DSP
22
+ * model -- a bucket brigade's ring-buffer read and write, say. Called once per executed
23
+ * `macro` block per sample (a macro does not iterate; it has no Newton loop to cap).
24
+ *
25
+ * **Required, not optional, because optional is how the hole got there.** A macro block
26
+ * contributes nothing to `CostPredictors.solvedBlocks`, so before this existed a macro's
27
+ * per-sample work was costed at exactly zero and a program of nothing but macro blocks was
28
+ * admitted unconditionally. An optional field would restore that: every existing caller
29
+ * would keep omitting it and keep getting the old silent zero. A host that plays no macro
30
+ * program never sees this called and can write `() => 0` honestly; a host that does play
31
+ * one has to have measured something.
32
+ *
33
+ * Return a non-finite or negative figure and the program is **refused by model name**,
34
+ * rather than costed at zero -- the same fail-closed shape `requiredModels` already has at
35
+ * `prepare()`. "The host has no price for this algorithm" is a refusal, not a free block.
36
+ */
37
+ readonly nsPerMacroSample: (modelId: string) => number;
38
+ /**
39
+ * Newton iterations per sample this host budgets for a nonlinear block: the **sustained
40
+ * allowance**, not the solver's cap.
41
+ *
42
+ * Host policy, which is why it lives here beside `cpuBudgetFraction` rather than being a
43
+ * constant this module invents — and separate from `maxNewtonIterations`, which is a
44
+ * correctness bound. See the header for why they were one number and could not stay so.
45
+ *
46
+ * Defaults to 64, the value the cap supplied when the two were the same, so a host that
47
+ * does not set it sees no change in verdict. The corpus median is ~2.5 iterations per
48
+ * sample and the worst sustained figure ~9.7, so 64 is very conservative; lowering it is
49
+ * a host's choice about how much spike headroom to keep.
50
+ */
51
+ readonly budgetedIterationsPerSample?: number;
52
+ /**
53
+ * Fraction of one sample period (`1e9 / sampleRate` ns) this host is willing to spend
54
+ * on this chain, leaving headroom for whatever else its audio thread does. Defaults
55
+ * to 1.0 -- the whole sample period, which is optimistic for any host running more
56
+ * than one program at once.
57
+ */
58
+ readonly cpuBudgetFraction?: number;
59
+ };
60
+ /** A verdict rather than a thrown error, so the caller decides how to fail. */
61
+ export type AdmissionVerdict = {
62
+ readonly fits: true;
63
+ } | {
64
+ readonly fits: false;
65
+ readonly reason: string;
66
+ };
67
+ /**
68
+ * Whether `chain` -- an ordered list of Programs, each solved every sample -- can be shown to
69
+ * fit inside `budget` at `sampleRate`, given a sustained allowance of
70
+ * `budgetedIterationsPerSample` Newton iterations for each nonlinear block.
71
+ *
72
+ * **The allowance is a policy, not the solver's cap** -- see the header. A sample that needs
73
+ * more is a spike the overrun policy covers (a bounded solver and a held sample), which is
74
+ * why raising the cap so a legitimate 183-iteration transient can converge no longer makes
75
+ * every chain 16x more expensive to admit.
76
+ *
77
+ * Still fail-closed on everything it can price: a macro block is charged the host's own
78
+ * per-step figure, an unpriced model is refused by name, and costs add across slots.
79
+ *
80
+ * **Costs add across slots and nothing here discounts them.** There is no shared work between
81
+ * two slots -- each holds its own program, its own state and its own solver scratch -- so the
82
+ * sum is the honest model, not a conservative one.
83
+ */
84
+ export declare function admissionVerdict(chain: readonly Program[], sampleRate: number, budget: RealtimeBudget): AdmissionVerdict;
85
+ /**
86
+ * The same worst-case sum the gate compares, returned as a number.
87
+ *
88
+ * Exists so a measurement harness can ask what admission *predicted* and hold it against a
89
+ * stopwatch, rather than parsing it back out of a refusal sentence -- and so the two can only
90
+ * ever disagree because the model is wrong, never because they were computed differently.
91
+ * `null` when a declared model has no price, which is the same condition the gate refuses on.
92
+ */
93
+ export declare function predictedWorstCaseNs(chain: readonly Program[], budget: RealtimeBudget): number | null;
94
+ //# sourceMappingURL=admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAuEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAQpD;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA8DrD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACpB,gBAAgB,CA8DlB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,MAAM,EAAE,cAAc,GACpB,MAAM,GAAG,IAAI,CAmBf"}
@@ -0,0 +1,209 @@
1
+ // Real-time admission: can a compiled program -- or a whole chain of them -- be shown to fit
2
+ // inside a host's CPU budget before it is allowed to run?
3
+ //
4
+ // **It takes a chain, not a program, and that is the load-bearing change.** The console runs
5
+ // an ordered list of Programs (see `./chain.ts`), every slot solving every sample, so the
6
+ // costs add. Gating each slot separately against the whole budget is the permissive failure:
7
+ // three pedals each measured at 0.9x of the sample period each pass alone and together need
8
+ // 2.7x, and the host finds out as clicks. One verdict over the whole list is the only shape
9
+ // that can refuse that, which is why there is no single-program entry point left -- a caller
10
+ // with one program passes a one-element list and gets an identical answer.
11
+ //
12
+ // The gap this closes: `Program.requiredOperators` (S1b) refuses a program a runtime
13
+ // cannot *execute at all*, by name, at `prepare()`. Nothing stopped a program a runtime
14
+ // *can* execute but cannot execute in time -- a packet at 350x real time loaded exactly
15
+ // as willingly as one at 0.15x, and the difference only showed up later as held samples
16
+ // and audible clicks, after a host had already committed to playing it. This is the same
17
+ // refusal shape -- fail closed, name what is missing -- applied to cost instead of
18
+ // capability.
19
+ //
20
+ // **The central tension this design has to hold, not paper over.** Real-time cost is not
21
+ // a property of the program alone: it is program x machine x sample rate x block size. A
22
+ // `.vdsp` that is playable on a laptop is not playable on an ESP32, so a portable artifact
23
+ // cannot honestly carry "playable: true" -- there is no fact of that shape a compiler
24
+ // could know. What it *can* carry is the rate-independent, machine-independent half:
25
+ // `Program.costPredictors` (`../compiler/types.ts`), computed once from circuit structure
26
+ // alone -- unknown counts, state size, block count, which blocks may iterate. The
27
+ // machine-dependent half -- how many nanoseconds one dense solve of a given size costs on
28
+ // THIS host's hardware and engine, and how much of the sample period this host is willing
29
+ // to spend -- can only come from the host, which is why it is a `prepare()` argument and
30
+ // never a default this module invents.
31
+ //
32
+ // **What this deliberately does not attempt.** It does not predict how many Newton
33
+ // iterations a sample will actually need -- that is a signal- and control-position-
34
+ // dependent number with no static derivation (`scripts/report-newton-deadline.ts` measures
35
+ // it empirically for exactly this reason: "iteration counts are rate-independent and are
36
+ // the right input to a CPU-budget decision, where wall-clock from the stamp interpreter is
37
+ // not" -- true, but the counts themselves still come from running a signal through, not
38
+ // from reading the circuit), and fabricating a bound here would be worse than omitting one.
39
+ //
40
+ // **It charges a BUDGETED iteration allowance, which is a policy number, and NOT the
41
+ // solver's cap.** Those were the same number until 2026-09-09 and it was a conflation:
42
+ // the cap is a *correctness* bound -- how long the solver may keep trying before the
43
+ // answer it has is the answer it returns -- and the admission charge is a *cost* policy.
44
+ // Tying them means any change to one silently rewrites the other, and it did: raising the
45
+ // cap from 64 to 1024 so that `moogerfooger-mf-102` (183 iterations once, then 2.1 per
46
+ // sample) and `mxr-blue-box` (356) could converge made every nonlinear block look 16x more
47
+ // expensive here, and chains that admit today would have been refused. Neither side was
48
+ // wrong; no single value satisfied both, which is the signature of a conflation rather than
49
+ // a mis-tuned constant.
50
+ //
51
+ // **The overrun policy, which is what makes a typical-cost charge honest.** A sample that
52
+ // needs more than the budgeted allowance is a *spike*, and a spike is not an inadmissible
53
+ // chain -- `report-newton-deadline.ts`'s own triage says so: `peak == mean` is a fixed cost
54
+ // to budget and ship; `peak >> mean` is "a bounded solver plus a held sample covers it, and
55
+ // the cost is a rare tiny error rather than a dropout"; only `mean at the cap` is not a
56
+ // real-time circuit. A rare held sample beats refusing to load, which is what a real-time
57
+ // audio system actually does. So the charge bounds the sustained case and the cap bounds
58
+ // the excursion, and the two no longer move together.
59
+ //
60
+ // Convergence and cost remain different questions: a held, unconverged sample still burns
61
+ // CPU, so `telemetry().nonConvergedSamples` (and now `stalledSamples`) remain the *quality*
62
+ // numbers while this stays the *cost* one.
63
+ //
64
+ // Fail-closed, in the sense that matters here: admission runs only when a host opts in by
65
+ // supplying a budget (there is no honest default for a per-machine nanosecond figure), but
66
+ // once supplied, a program that cannot be shown to fit is refused, naming the block and
67
+ // the numbers that did not fit -- never accepted and left to glitch. A caller that
68
+ // supplies no budget has explicitly chosen not to gate, the same way a caller that never
69
+ // reads `telemetry()` has chosen not to look at convergence: a skipped input is a skip,
70
+ // not a pass.
71
+ /**
72
+ * The sustained allowance a host gets when it does not state one: the value the solver's cap
73
+ * supplied while the two were the same number, so separating them changes no verdict.
74
+ */
75
+ const DEFAULT_BUDGETED_ITERATIONS = 64;
76
+ /** Worst-case per-sample cost of every unit in one slot's program. */
77
+ function costUnits(program, slot, budgetedIterationsPerSample, budget) {
78
+ const units = [];
79
+ for (const block of program.costPredictors.solvedBlocks) {
80
+ // A linear block solves once per sample; a nonlinear one is charged the sustained
81
+ // allowance rather than the solver's cap. See the header: the cap is a correctness
82
+ // bound and an excursion past the allowance is a spike the overrun policy covers,
83
+ // not a chain to refuse.
84
+ const iterations = block.linear ? 1 : budgetedIterationsPerSample;
85
+ units.push({
86
+ slot,
87
+ blockId: block.blockId,
88
+ description: `${block.unknownCount} unknowns, ${block.linear
89
+ ? "linear"
90
+ : `nonlinear, budgeted=${budgetedIterationsPerSample} it/sample`}`,
91
+ worstCaseNs: iterations * budget.nsPerSolve(block.unknownCount),
92
+ });
93
+ }
94
+ const unpriced = [];
95
+ for (const block of program.costPredictors.macroBlocks) {
96
+ const ns = budget.nsPerMacroSample(block.modelId);
97
+ // A host with no measured figure for this algorithm is refused by name, never costed
98
+ // at zero -- see `RealtimeBudget.nsPerMacroSample`. Zero itself is accepted: a host is
99
+ // allowed to say an algorithm is free on its hardware, and that is a claim it made.
100
+ if (!Number.isFinite(ns) || ns < 0) {
101
+ unpriced.push(block.modelId);
102
+ continue;
103
+ }
104
+ units.push({
105
+ slot,
106
+ blockId: block.blockId,
107
+ description: `DSP model "${block.modelId}", 1 step/sample`,
108
+ worstCaseNs: ns,
109
+ });
110
+ }
111
+ return { units, unpriced };
112
+ }
113
+ /**
114
+ * Whether `chain` -- an ordered list of Programs, each solved every sample -- can be shown to
115
+ * fit inside `budget` at `sampleRate`, given a sustained allowance of
116
+ * `budgetedIterationsPerSample` Newton iterations for each nonlinear block.
117
+ *
118
+ * **The allowance is a policy, not the solver's cap** -- see the header. A sample that needs
119
+ * more is a spike the overrun policy covers (a bounded solver and a held sample), which is
120
+ * why raising the cap so a legitimate 183-iteration transient can converge no longer makes
121
+ * every chain 16x more expensive to admit.
122
+ *
123
+ * Still fail-closed on everything it can price: a macro block is charged the host's own
124
+ * per-step figure, an unpriced model is refused by name, and costs add across slots.
125
+ *
126
+ * **Costs add across slots and nothing here discounts them.** There is no shared work between
127
+ * two slots -- each holds its own program, its own state and its own solver scratch -- so the
128
+ * sum is the honest model, not a conservative one.
129
+ */
130
+ export function admissionVerdict(chain, sampleRate, budget) {
131
+ const budgetedIterationsPerSample = budget.budgetedIterationsPerSample ?? DEFAULT_BUDGETED_ITERATIONS;
132
+ if (!Number.isFinite(budgetedIterationsPerSample) ||
133
+ budgetedIterationsPerSample < 1) {
134
+ return {
135
+ fits: false,
136
+ reason: `budgetedIterationsPerSample must be a finite number of at least 1, got ${budgetedIterationsPerSample}`,
137
+ };
138
+ }
139
+ const fraction = budget.cpuBudgetFraction ?? 1;
140
+ if (!(fraction > 0) || fraction > 1) {
141
+ return {
142
+ fits: false,
143
+ reason: `cpuBudgetFraction must be in (0, 1], got ${fraction}`,
144
+ };
145
+ }
146
+ const nsAvailable = (1e9 / sampleRate) * fraction;
147
+ const costed = [];
148
+ const unpriced = [];
149
+ for (const [slot, program] of chain.entries()) {
150
+ const result = costUnits(program, slot, budgetedIterationsPerSample, budget);
151
+ costed.push(...result.units);
152
+ for (const modelId of result.unpriced) {
153
+ unpriced.push({ slot, modelId });
154
+ }
155
+ }
156
+ // Before the arithmetic: a chain carrying an algorithm the budget cannot price has no
157
+ // worst case to compare, and summing the rest would produce a number that reads like a
158
+ // measurement of the whole chain while omitting a slot.
159
+ if (unpriced.length > 0) {
160
+ const named = unpriced
161
+ .map(({ slot, modelId }) => `slot ${slot} "${modelId}"`)
162
+ .join(", ");
163
+ return {
164
+ fits: false,
165
+ reason: `chain cannot be shown to fit: the budget declares no usable per-sample cost ` +
166
+ `for ${unpriced.length === 1 ? "DSP model" : "DSP models"} ${named}`,
167
+ };
168
+ }
169
+ const worstCaseNs = costed.reduce((sum, unit) => sum + unit.worstCaseNs, 0);
170
+ if (worstCaseNs <= nsAvailable) {
171
+ return { fits: true };
172
+ }
173
+ const worst = [...costed].sort((a, b) => b.worstCaseNs - a.worstCaseNs)[0];
174
+ const worstDescription = worst === undefined
175
+ ? "no costed block"
176
+ : `slot ${worst.slot} block "${worst.blockId}" (${worst.description}) alone ` +
177
+ `costing ${worst.worstCaseNs.toFixed(0)} ns/sample`;
178
+ return {
179
+ fits: false,
180
+ reason: `chain of ${chain.length} program(s) cannot be shown to fit inside ` +
181
+ `${nsAvailable.toFixed(0)} ns/sample at ${sampleRate} Hz ` +
182
+ `(cpuBudgetFraction=${fraction}): worst case is ${worstCaseNs.toFixed(0)} ` +
183
+ `ns/sample across ${costed.length} costed block(s), worst offender ` +
184
+ `${worstDescription}`,
185
+ };
186
+ }
187
+ /**
188
+ * The same worst-case sum the gate compares, returned as a number.
189
+ *
190
+ * Exists so a measurement harness can ask what admission *predicted* and hold it against a
191
+ * stopwatch, rather than parsing it back out of a refusal sentence -- and so the two can only
192
+ * ever disagree because the model is wrong, never because they were computed differently.
193
+ * `null` when a declared model has no price, which is the same condition the gate refuses on.
194
+ */
195
+ export function predictedWorstCaseNs(chain, budget) {
196
+ const budgetedIterationsPerSample = budget.budgetedIterationsPerSample ?? DEFAULT_BUDGETED_ITERATIONS;
197
+ let total = 0;
198
+ for (const [slot, program] of chain.entries()) {
199
+ const { units, unpriced } = costUnits(program, slot, budgetedIterationsPerSample, budget);
200
+ if (unpriced.length > 0) {
201
+ return null;
202
+ }
203
+ for (const unit of units) {
204
+ total += unit.worstCaseNs;
205
+ }
206
+ }
207
+ return total;
208
+ }
209
+ //# sourceMappingURL=admission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.js","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,0DAA0D;AAC1D,EAAE;AACF,6FAA6F;AAC7F,0FAA0F;AAC1F,6FAA6F;AAC7F,4FAA4F;AAC5F,4FAA4F;AAC5F,6FAA6F;AAC7F,2EAA2E;AAC3E,EAAE;AACF,qFAAqF;AACrF,wFAAwF;AACxF,wFAAwF;AACxF,wFAAwF;AACxF,yFAAyF;AACzF,mFAAmF;AACnF,cAAc;AACd,EAAE;AACF,yFAAyF;AACzF,yFAAyF;AACzF,2FAA2F;AAC3F,sFAAsF;AACtF,qFAAqF;AACrF,0FAA0F;AAC1F,kFAAkF;AAClF,0FAA0F;AAC1F,0FAA0F;AAC1F,yFAAyF;AACzF,uCAAuC;AACvC,EAAE;AACF,mFAAmF;AACnF,oFAAoF;AACpF,2FAA2F;AAC3F,yFAAyF;AACzF,2FAA2F;AAC3F,wFAAwF;AACxF,4FAA4F;AAC5F,EAAE;AACF,qFAAqF;AACrF,uFAAuF;AACvF,qFAAqF;AACrF,yFAAyF;AACzF,0FAA0F;AAC1F,uFAAuF;AACvF,2FAA2F;AAC3F,wFAAwF;AACxF,4FAA4F;AAC5F,wBAAwB;AACxB,EAAE;AACF,0FAA0F;AAC1F,0FAA0F;AAC1F,4FAA4F;AAC5F,4FAA4F;AAC5F,wFAAwF;AACxF,0FAA0F;AAC1F,yFAAyF;AACzF,sDAAsD;AACtD,EAAE;AACF,0FAA0F;AAC1F,4FAA4F;AAC5F,2CAA2C;AAC3C,EAAE;AACF,0FAA0F;AAC1F,2FAA2F;AAC3F,wFAAwF;AACxF,mFAAmF;AACnF,yFAAyF;AACzF,wFAAwF;AACxF,cAAc;AAId;;;GAGG;AACH,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAiFvC,sEAAsE;AACtE,SAAS,SAAS,CACjB,OAAgB,EAChB,IAAY,EACZ,2BAAmC,EACnC,MAAsB;IAEtB,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC;QACzD,kFAAkF;QAClF,mFAAmF;QACnF,kFAAkF;QAClF,yBAAyB;QACzB,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC;QAClE,KAAK,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,WAAW,EAAE,GAAG,KAAK,CAAC,YAAY,cACjC,KAAK,CAAC,MAAM;gBACX,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,uBAAuB,2BAA2B,YACtD,EAAE;YACF,WAAW,EAAE,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC;SAC/D,CAAC,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,cAAc,CAAC,WAAW,EAAE,CAAC;QACxD,MAAM,EAAE,GAAG,MAAM,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAClD,qFAAqF;QACrF,uFAAuF;QACvF,oFAAoF;QACpF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YACpC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,SAAS;QACV,CAAC;QACD,KAAK,CAAC,IAAI,CAAC;YACV,IAAI;YACJ,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,WAAW,EAAE,cAAc,KAAK,CAAC,OAAO,kBAAkB;YAC1D,WAAW,EAAE,EAAE;SACf,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAC/B,KAAyB,EACzB,UAAkB,EAClB,MAAsB;IAEtB,MAAM,2BAA2B,GAChC,MAAM,CAAC,2BAA2B,IAAI,2BAA2B,CAAC;IACnE,IACC,CAAC,MAAM,CAAC,QAAQ,CAAC,2BAA2B,CAAC;QAC7C,2BAA2B,GAAG,CAAC,EAC9B,CAAC;QACF,OAAO;YACN,IAAI,EAAE,KAAK;YACX,MAAM,EAAE,0EAA0E,2BAA2B,EAAE;SAC/G,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,iBAAiB,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACrC,OAAO;YACN,IAAI,EAAE,KAAK;YACX,MAAM,EAAE,4CAA4C,QAAQ,EAAE;SAC9D,CAAC;IACH,CAAC;IACD,MAAM,WAAW,GAAG,CAAC,GAAG,GAAG,UAAU,CAAC,GAAG,QAAQ,CAAC;IAClD,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAwC,EAAE,CAAC;IACzD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,2BAA2B,EAAE,MAAM,CAAC,CAAC;QAC7E,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC7B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAClC,CAAC;IACF,CAAC;IACD,sFAAsF;IACtF,uFAAuF;IACvF,wDAAwD;IACxD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,QAAQ;aACpB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,QAAQ,IAAI,KAAK,OAAO,GAAG,CAAC;aACvD,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,OAAO;YACN,IAAI,EAAE,KAAK;YACX,MAAM,EACL,8EAA8E;gBAC9E,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,EAAE;SACrE,CAAC;IACH,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC5E,IAAI,WAAW,IAAI,WAAW,EAAE,CAAC;QAChC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,gBAAgB,GACrB,KAAK,KAAK,SAAS;QAClB,CAAC,CAAC,iBAAiB;QACnB,CAAC,CAAC,QAAQ,KAAK,CAAC,IAAI,WAAW,KAAK,CAAC,OAAO,MAAM,KAAK,CAAC,WAAW,UAAU;YAC5E,WAAW,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;IACvD,OAAO;QACN,IAAI,EAAE,KAAK;QACX,MAAM,EACL,YAAY,KAAK,CAAC,MAAM,4CAA4C;YACpE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,UAAU,MAAM;YAC1D,sBAAsB,QAAQ,oBAAoB,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;YAC3E,oBAAoB,MAAM,CAAC,MAAM,mCAAmC;YACpE,GAAG,gBAAgB,EAAE;KACtB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CACnC,KAAyB,EACzB,MAAsB;IAEtB,MAAM,2BAA2B,GAChC,MAAM,CAAC,2BAA2B,IAAI,2BAA2B,CAAC;IACnE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,SAAS,CACpC,OAAO,EACP,IAAI,EACJ,2BAA2B,EAC3B,MAAM,CACN,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACb,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,KAAK,IAAI,IAAI,CAAC,WAAW,CAAC;QAC3B,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC"}
@@ -0,0 +1,104 @@
1
+ import type { RealtimeBudget } from "./admission";
2
+ /** Sizes measured when the caller states none. */
3
+ export declare const DEFAULT_CALIBRATION_SIZES: readonly number[];
4
+ export type CalibrateOptions = {
5
+ /** Factory: prepare a workload for `unknownCount` unknowns, return one solve. */
6
+ readonly solve: (unknownCount: number) => () => void;
7
+ /** System sizes to measure. Defaults to DEFAULT_CALIBRATION_SIZES. */
8
+ readonly sizes?: readonly number[];
9
+ /** Untimed solves per size before sampling. Default 50. Zero skips warmup. */
10
+ readonly warmupSolves?: number;
11
+ /** Starting solves per timed batch; grows until the clock resolves it. Default 100. */
12
+ readonly solvesPerSample?: number;
13
+ /** Timed batches per size. Default 7. */
14
+ readonly samples?: number;
15
+ /** Injected clock in nanoseconds. Default wraps `performance.now`. */
16
+ readonly now?: () => number;
17
+ /** Smallest honest clock difference in ns. Default 1000. */
18
+ readonly minTimerResolutionNs?: number;
19
+ };
20
+ export type CalibrationRow = {
21
+ readonly unknownCount: number;
22
+ /** Median ns per solve across samples, after the monotone envelope. */
23
+ readonly medianNsPerSolve: number;
24
+ /** Interquartile range over the median, before the envelope. */
25
+ readonly spreadFraction: number;
26
+ /** How many timed batches the median and spread were computed from. */
27
+ readonly samples: number;
28
+ };
29
+ export type CalibrationCorrection = {
30
+ readonly unknownCount: number;
31
+ readonly measuredNsPerSolve: number;
32
+ readonly correctedNsPerSolve: number;
33
+ };
34
+ export type ToRealtimeBudgetArgs = {
35
+ /**
36
+ * The host's own measured per-sample cost of the named DSP model.
37
+ * Calibration cannot measure this (it times dense solves, not models),
38
+ * so the host supplies it here; it is required, never defaulted, because
39
+ * admission refuses unpriced models and a silent zero would waive that.
40
+ */
41
+ readonly nsPerMacroSample: (modelId: string) => number;
42
+ /** Host policy, passed through when given; omitted otherwise. */
43
+ readonly budgetedIterationsPerSample?: number;
44
+ /** Host policy, passed through when given; omitted otherwise. */
45
+ readonly cpuBudgetFraction?: number;
46
+ };
47
+ export type CalibrationResult = {
48
+ readonly ok: true;
49
+ readonly table: readonly CalibrationRow[];
50
+ readonly corrections: readonly CalibrationCorrection[];
51
+ readonly nsPerSolve: (unknownCount: number) => number;
52
+ readonly toRealtimeBudget: (args: ToRealtimeBudgetArgs) => RealtimeBudget;
53
+ };
54
+ /** Closed reason codes; compare whole values, never message text. */
55
+ export type CalibrationReason = "no-sizes" | "bad-size" | "zero-solves" | "timer-too-coarse" | "solve-threw" | "non-finite-time";
56
+ export type CalibrationRefusal = {
57
+ readonly ok: false;
58
+ readonly reason: "no-sizes";
59
+ } | {
60
+ readonly ok: false;
61
+ readonly reason: "bad-size";
62
+ readonly size: number;
63
+ readonly index: number;
64
+ } | {
65
+ readonly ok: false;
66
+ readonly reason: "zero-solves";
67
+ readonly field: "warmupSolves" | "solvesPerSample" | "samples";
68
+ readonly value: number;
69
+ } | {
70
+ readonly ok: false;
71
+ readonly reason: "timer-too-coarse";
72
+ readonly unknownCount: number;
73
+ readonly resolutionNs: number;
74
+ readonly largestBatchSolves: number;
75
+ readonly elapsedNs: number;
76
+ } | {
77
+ readonly ok: false;
78
+ readonly reason: "solve-threw";
79
+ readonly unknownCount: number;
80
+ readonly thrown: unknown;
81
+ } | {
82
+ readonly ok: false;
83
+ readonly reason: "non-finite-time";
84
+ readonly unknownCount: number;
85
+ readonly t0Ns: number;
86
+ readonly t1Ns: number;
87
+ };
88
+ /**
89
+ * Measure this host's dense-solve cost per unknown count. Refuses with a
90
+ * typed reason instead of reporting a number the inputs cannot support.
91
+ */
92
+ export declare function calibrateNsPerSolve(options: CalibrateOptions): CalibrationResult | CalibrationRefusal;
93
+ /**
94
+ * A TEST WORKLOAD for calibration, not a claim about the real engine. It
95
+ * performs one dense Gaussian elimination with partial pivoting on a fixed
96
+ * well-conditioned diagonally dominant n by n system in plain TypeScript,
97
+ * deterministically (same entries every call), with no allocation inside the
98
+ * timed function (all working buffers are preallocated by the factory).
99
+ * Later the same harness times the WebAssembly console instead; figures from
100
+ * this workload describe the TypeScript loop under the calibrating host and
101
+ * nothing else.
102
+ */
103
+ export declare function denseLinearSolveWorkload(unknownCount: number): () => void;
104
+ //# sourceMappingURL=calibrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calibrate.d.ts","sourceRoot":"","sources":["../src/calibrate.ts"],"names":[],"mappings":"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"}