@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
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":"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"}
|
|
@@ -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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"}
|