Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ reference/di/*.wav
reference/schematics/
reference/speaker/*.jpg
reference/speaker/*.png # third-party impedance plots: local only (as the drawings, D-021)
build-xp/
90 changes: 90 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1081,3 +1081,93 @@ step halvings (was 2, D-042). The "max iterations ≤ configured limit" check fo
Consequences: 0 first-solve failures (0 unrescued) at HOT, every knob 1.0 and CRUNCH at 2x, 8x and 16x, and in the 2x plugin
model check (28 runs, max 56 iterations); accuracy and CPU unchanged (27.1 / 42.2 % in the check's timing). The budget is a
cap; ordinary samples still converge in a few iterations.

## D-065 Jacobian predictor from the kept LU factors; preamp J4 Thevenin resistance from them
Date: 2026-10-19 · Status: accepted (M7 step 1; owner: "start on the CPU optimisation")
Context: at the NAM settings (Preamp 10, Treble 8, Middle 7, Bass 4, Master 10, Presence 8) the power amp needed 3.46
Newton iterations per solve and the preamp 1.41 (the preamp's second J4 solve 1.18, almost never 0); the J4 Thevenin
re-evaluated the four triodes and solved an 8×8 system every sample. Tried and not adopted (ampwav, 96 kHz, 20 s DI):
relaxed step halving (accept growth ×2 … ×1e6: up to 150 k failures, more iterations), tighter or looser EL34 grid / plate
step limits (2 … 100 V: more iterations or failures), chord iterations with the kept factors (no gain, output moved),
half-step LU invalidation (slower).
Decision: the Newton keeps the LU factors of its last iteration (multipliers below the diagonal, pivot rows;
`detail::LuFactors`, `solver::solveFactored`). Every root predicts with one Newton step from the last solution using them
(`RootNetworkState::jacobianPredictor`, default on), with the device's linear source ports updated (`sourceCurrents`:
the preamp's given load current, the power amp's J4 source); extrapolation (D-029) when no factors exist. The preamp's
J4 Thevenin resistance is (J⁻¹·Z[:, y])_y from the factors when this sample's solve factored (else as before).
Consequences: iterations per solve at NAM 3.46 → 2.34 (power amp), 1.41 → 0.66 (preamp), 0 failures; engine 96 kHz
4398 → 3464 ns (NAM), 2552 → 2221 ns (default), 4920 → 3850 ns (every knob 1.0). Plugin (20 s DI, 2x Full) default
26.6 → 21.6 %, NAM 42.7 → 33.4 %, every knob 10 49.7 → 37.3 %. Output vs before: −105 dB default, −50.5 dB NAM (the
same order as a Newton tolerance change 1e-4 → 1e-6 V, −53 dB). Plugin model check unchanged (CRUNCH / DRIVE / HOT
−45.3 / −48.5 / −24.0 dB, unseen −20.8 / −39.0 dB, 0 failures, max iterations 56 → 38;
`M6/20261019_x2_rates_m7_step1_full2x.md`). The exact solution is unchanged; only the starting point moves.
Found on the way: at every knob 1.0 the engine is sensitive to any perturbation after ≈ 11 s of the 20 s clip (tolerance
1e-8 vs 1e-10 V: −180 dB, then −16 … −18 dB per second, spread around the 100 Hz harmonics, no discrete tone): sample-wise
comparisons are meaningless at that setting; it is compared by Newton failures and spectra.

## D-066 Device maths: fast Wright omega, E1^ex via exp / log; no outputs for the preamp's trial J4 solve
Date: 2026-10-19 · Status: accepted (M7 step 3a; results unchanged to rounding)
Context: after D-065 the device maths was ≈ 37 % of the engine at the NAM settings; the grid diode's Wright omega took an
exp / log1p start and 2 – 3 Fritsch–Shafer–Crowley steps (one log each) plus a confirming step whenever z ≥ −40 (one in
three calls at NAM), and pow was 10 %.
Decision: `wrightOmega` = the series e^z·(1 − y + 3/2·y² − 8/3·y³), y = e^z, below z = −10; on [−10, 40) a cubic Hermite
start from a 401-point table of the reference (`wrightOmegaReference`, kept) and one step; from 40 the asymptotic start and
one step. ≤ 1e-14 relative to the reference everywhere (the series is more accurate than the reference's iteration below
z ≈ −30), residual ≤ 8e-16·max(1, |z|) from z = −10; 5.4 vs 23 ns per call. Triode / pentode E1^ex as exp(ex·ln E1)
(≤ 1.2e-15 relative to pow, 26 % faster in a loop). The preamp's first J4 solve (re-solved after the power amp) skips its
node voltages and waves (`RootNetwork::solve(..., outputs = false)`).
Consequences: engine at 96 kHz, NAM 3464 → 3175 ns, default 2221 → 2139 ns; output unchanged at the comparison's
resolution; unit tests (device curves vs SPICE, finite-difference Jacobians) unchanged. A float LU for the Newton direction
was tried (no gain: 3163 ns) and not adopted.

## D-067 Preamp J4: the second solve replaced by a linear load correction when small
Date: 2026-10-19 · Status: accepted (M7, owner: "do A and B", option B)
Context: with exact J4 the preamp is solved, the power amp solved against its linearised Thevenin source, and the preamp
solved again with the power amp's load current. That second solve costs at least one evaluation of the four triodes.
Decision: `Preamp::correctLoad`: from the first solution, dv = −J⁻¹·Z[:, y]·Δi_y (the direction theveninAtY already has),
triode currents moved by their conductances at the solution, committed without Newton (`RootNetwork::commit`); a full
re-solve when any port would move by more than 10 mV (`kMaxLoadCorrection`).
Consequences: measured against the full re-solve (96 kHz, 20 s DI): 10 mV −115 dB (default knobs) / −126 dB (NAM), 30 mV
−51.6 dB at NAM, 0.1 V −36 dB, unlimited −31 dB with the error present from the first second (the grid diodes:
0.1 V ≈ 4 n·Vt). At 10 mV the engine is 11 % faster at default knobs and unchanged at NAM, where the load steps are
larger. Not a model change at the chosen limit.

## D-068 The linear J4 port out of both Newton systems
Date: 2026-10-19 · Status: accepted (M7, owner: "do A and B", option A)
Context: the preamp root's load port (given current) and the power amp's J4 source port (i = g·(v − vTh)) are linear but
were Newton ports: 9 and 11 ports instead of 8 and 10.
Decision: a device with a linear last port declares `linearPort(g, c)` (i = g·v + c; `solver::LinearLastPort`). The Newton
solves the other ports with Z' = Z_pp − Z_py·g·Z_yp / (1 + g·Z_yy) and computes the linear port exactly from them at every
evaluation (`solver::solveLinearPort`); its residual is zero by construction. The predictor and the kept factors use the
reduced system; the factors keep the conductances they were built from (`LuFactors::d`), which the preamp's Thevenin
resistance uses (rTh = Z_yy − Z_yp·D·(I + Z_pp·D)⁻¹·Z_py). Using the last iterate's conductances with the previous
iterate's factors instead was tried by mistake first and moved the NAM output by −31.5 dB.
Consequences: output vs before −128 dB (default knobs) / −56.9 dB (NAM: the convergence test no longer includes the
linear port's residual, a Newton-tolerance-level change). Engine 2–3 % faster.

## D-069 Plugin default Power Amp Rate Half (Full selectable)
Date: 2026-10-19 · Status: accepted (owner: "Make half the default while keeping full selectable"; revises D-060)
Context: after M7 steps 1, 3a, A and B (D-065 – D-068) the plugin at 2x Full needs 18.5 / 31.1 / 34.5 % of one core (default
knobs / NAM settings / every knob 10, 20 s DI); at Half 12.8 / 21.6 / 23.7 %. The Half plugin model check after M7
(`M6/20261019_x2_rates_m7_AB_half2x.md`): CRUNCH / DRIVE / HOT −37.7 / −37.2 / −18.9 dB, unseen settings worst −16.6 dB
(median −28.0), 0 Newton failures, non-harmonic power on 5 / 9 kHz sines −7.0 … −12.6 dB (Full: −45.3 / −48.5 / −24.0 dB,
−20.8 dB, −14.9 … −20.5 dB). The owner heard no Full / Half difference in M6 (before D-058 / D-059).
Decision: the Power Amp Rate parameter defaults to Half; Full stays selectable and remains the verified configuration.
Consequences: the default fails the plugin model check's −20 dB at HOT (−18.9 dB) and at the worst unseen setting
(−16.6 dB); deviation accepted by the owner, tolerances unchanged (VERIFICATION). Saved projects keep their stored
setting. `harness.plugin_null` and `harness.plugin_cpu` measure the new default. Reported latency at 48 kHz (FIR
oversampling) 49 → 58 samples (1.02 → 1.21 ms; the half-rate interpolator, D-053).

## D-070 Power-amp root: maxIter 48
Date: 2026-10-19 · Status: accepted (M7 verification; the owner's "do A and B" and D-069)
Context: the M7 automation check at Half (96 kHz, exact J4, 1e-4 V) showed 3 first-solve failures (all rescued) at every
knob 1.0 with Middle or Presence moving; Full: 0. Bisection over the M7 commits: 0 failures up to D-067 (max 24 – 26
iterations, pre-M7 23 – 24), 3 from D-068 (the reduced Newton takes another path through the same hard samples). The Jacobian
predictor contributes (with it off: 0 failures, max 31 – 32); guards that fall back to the trend when the prediction is far
from it remove the failures but cost 8 – 18 % at the NAM settings (trend difference 100 / 30 / 10 V); falling back only when
the prediction hits a step limit costs nothing but leaves 2 of 3 (both fail from the trend as well and converge from the
last solution in 21 iterations).
Decision: the power-amp root (`PowerAmp`, b56 and the full amp) allows 48 iterations (was 32, D-052); the check's limit
follows (b56 48; full amp 64 from D-064).
Consequences: the three samples converge in 33 – 34 iterations; automation 56 runs at Half and at Full: 0 non-finite, 0
failures; HOT acceptance (384 kHz exact J4) 0 FAIL; b56 0 FAIL. No effect on samples that converged within 32.
77 changes: 73 additions & 4 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,12 +557,13 @@ Notes:
first-solve Newton failures at 768 kHz, all rescued (D-042, D-045). 2x / 4x have no SPICE pass (D-047).

## M6 — Performance (owner scope 2026-10-14: performance work only; the other SPEC §7 M6 items stay open)
Closed by the owner on 2026-10-19 (report accepted). Steps 2–4 were not finished; the CPU work continues in M7.
- [x] Step 0: baseline at the 8x default (384 kHz)
- [x] Step 1: build settings (no change adopted)
- [~] Step 2: device equations
- [~] Step 3: dense linear algebra / SIMD
- [ ] Step 4: linear parts at a lower rate (analysis first)
- [x] Step 5: re-verification, M6 report (`harness/reports/M6/20261018_M6_report.md`, 2026-10-18; for owner review)
- [x] Step 2: device equations (studied; inline polynomials slower than libm, not adopted; rest moved to M7)
- [x] Step 3: dense linear algebra / SIMD (unrolled elimination adopted; rest moved to M7)
- [x] Step 4: linear parts at a lower rate (moved to M7)
- [x] Step 5: re-verification, M6 report (`harness/reports/M6/20261018_M6_report.md`, accepted 2026-10-19)
- 2026-10-18: owner-supplied measured 4x12 impedance curves (T1511, T1281, G12K-85) compared with D-059: match above ≈ 200 Hz,
resonance within their range; D-059 kept (D-063).
- Owner 2026-10-18: handle the remaining Newton first-solve failures (HOT DI RR at 384 kHz: 2; plugin model check at 2x:
Expand Down Expand Up @@ -858,6 +859,74 @@ Notes:
b34 94 PASS (0 FAIL each); knob jumps b12 −55.9 dB, b1234 −39.1 … −55.1 dB, b34 −53.1 … −67.6 dB; M4 full acceptance 107
PASS (768 kHz and 384 kHz exact J4); HOT 20 PASS / 1 FAIL (2 rescued first-solves, as after D-061); null test 26 / 26.

## M7 — CPU optimisation (owner 2026-10-19: "close M6 and start on the CPU optimisation")
Goal: lower the plugin's CPU at its defaults (2x, Power Amp Rate Full, exact J4, Newton tol 1e-4 V, half-steps on;
D-054 – D-060, D-064), above all at high Master, toward the 20 % working budget, without loosening any tolerance and
without audible change. Owner criteria: no added latency, authentic at all knobs, CPU headroom.

Baseline (2026-10-19, main 0e70f81; `amp2203_render plugin --factor 2`, DI GTR L, FIR oversampling):
- CPU: default knobs 26.6 % (60 s), NAM settings (Preamp 10, Treble 8, Middle 7, Bass 4, Master 10, Presence 8)
42.7 % (20 s), every knob 10 54.0 % (60 s). Master 10 alone 33.6 %, Presence 10 alone 26.4 %.
- Engine at 96 kHz (ampwav, plugin settings, 20 s): default 2491 ns / sample, NAM 4236 ns.
- Profile at NAM (macOS `sample`, audio thread): power amp ≈ 62 %, preamp (two solves per sample) ≈ 30 %. By kind:
libm (pow, log1p, exp, log, atan) + Wright omega ≈ 29 %, LU elimination ≈ 23 %, Newton loop (J build, residual,
inlined devices) ≈ 26 %, open-circuit products ≈ 7 %.
- Newton at NAM (temporary counters): power amp 1.15 solves per sample (half-steps), 3.46 iterations per solve
(default knobs 1.36), 1.03 M step halvings on 7.65 M iterations, step limits rarely active (0.6 %); preamp 2 solves
per sample, 1.41 iterations per solve. The power amp's iteration count is what grows with Master.

Plan (each step measured alone; adopted only if it pays):
1. Newton iterations in the power amp: initial guess (a predictor from the last Jacobian instead of linear
extrapolation), the step-halving rule, Jacobian reuse within a solve. Gate: 0 Newton failures (HOT, every knob 1.0,
plugin model check), output within the Newton tolerance of the current engine.
2. Fewer solves per sample: the half-step trigger at 2x Full; the preamp's second solve (exact J4) replaced by a
linear correction from its last Jacobian. Gate: plugin model check vs SPICE and 16x, HOT acceptance.
3. Code level, results unchanged to rounding: memory layout / padding for NEON, the linear rY port folded out of
the LU, derivatives only where used, vectorised device maths across tubes. Gate: null test vs the previous build.
4. Re-verification as M6 (full acceptance 768 / 384 kHz, HOT, block tests, knob jumps, automation, plugin model
check, null test, pluginval / auval), plugin CPU, M7 report.
Tools: `ampwav --stats` now writes the per-root iteration histograms; `harness.plugin_cpu`; `amp2203_render`.

- [x] Step 1 (D-065): Jacobian predictor from the kept LU factors, preamp J4 Thevenin resistance from them. Plugin 2x Full
(20 s DI): default 26.6 → 21.6 %, NAM 42.7 → 33.4 %, every knob 10 49.7 → 37.3 %; plugin model check unchanged
(`M6/20261019_x2_rates_m7_step1_full2x.md`). Tried without gain: step-halving rule, EL34 step limits, chord iterations.
- Profile after step 1 (NAM, inclusive): half-steps 21 % (15 % of samples at 2x cross the 100 V trigger), LU 17 %,
triode maths 17 %, pentode maths 14 %, open-circuit products 6 %.
- Half-step trigger at 2x Full (plugin model check): off: HOT −22.7 dB, unseen worst −19.8 dB, HOT cost 31.7 → 29.6 %;
300 V: HOT −23.4 dB, unseen −20.2 dB, 30.3 % (`M6/20261019_x2_rates_m7_hst0_full2x.md`, `…_hst300_…`). Kept at 100 V
(2 points of CPU for 1.3 dB of HOT margin and the unseen check).
- [x] Step 3a (D-066): fast Wright omega (series below z = −10, cubic Hermite table start + one Fritsch–Shafer–Crowley step,
≤ 1e-14 relative to the reference, 4.3× faster), E1^ex as exp(ex·ln E1) (≤ 1.2e-15 relative), no node voltages / waves
for the preamp's trial J4 solve. Engine NAM 3464 → 3175 ns. Tried without gain: float LU for the Newton direction
(3175 → 3163 ns: K = 9 / 11 is latency-bound; the double rule stays).
- After steps 1 + 3a: plugin (20 s DI, 2x Full) default 26.6 → 20.4 %, NAM 42.7 → 31.0 %, every knob 10 49.7 → 34.4 %
(60 s DI: every knob 10 54.0 → 42.1 %); plugin model check unchanged (CRUNCH / DRIVE / HOT −45.3 / −48.5 / −24.0 dB,
unseen −20.8 / −39.0 dB, 0 failures, `M6/20261019_x2_rates_m7_step3a_full2x.md`).
- Owner 2026-10-19: "do A and B". Plan: (B) the preamp's second J4 solve replaced by a linear correction from the first
(dv = −J⁻¹·Z[:, y]·Δi_y, currents moved by the device conductances at the first solution), with a full re-solve when the
correction is large; measured for time, output vs the current engine and the plugin model check. (A) the linear J4 port
out of both Newton systems: the preamp's given-current port solved after the 8 triode ports, the power amp's source port
folded into its Thevenin matrix per sample (results unchanged to rounding); null test vs the current engine.
- [x] B (D-067): linear load correction below 10 mV (vs the re-solve −115 dB default / −126 dB NAM; larger limits move the
output: 30 mV −51.6 dB, unlimited −31 dB). [x] A (D-068): the linear port out of both Newton systems (−128 / −56.9 dB vs
before). Plugin (20 s DI, 2x Full): default 20.4 → 18.5 %, NAM 31.0 → 31.1 %, every knob 10 34.4 → 34.5 %: the gain is
at default knobs; at high Master the load steps exceed 10 mV.
- Options C / D measured (owner: "how much ... could affect CPU", then "re-run the Half model check"). C (half-step trigger
300 V, engine): NAM −5 %, every knob 1.0 −8 %, default 0 %. D (Power Amp Rate Half, plugin 20 s): default 18.5 → 12.8 %,
NAM 31.1 → 21.6 %, every knob 10 34.5 → 23.7 %. Half model check after M7 (`M6/20261019_x2_rates_m7_AB_half2x.md`):
CRUNCH / DRIVE / HOT −37.7 / −37.2 / −18.9 dB (FAIL: HOT ≤ −20), unseen worst / median −16.6 / −28.0 dB, 0 failures
(before M7: 12, 1 unrescued); non-harmonic power on 5 / 9 kHz sines −7.0 … −12.6 dB (Full −14.9 … −20.5 dB). Accuracy as
before M7 (−18.9 / −16.7 dB).
- Owner 2026-10-19: Half becomes the plugin default, Full selectable (D-069; HOT −18.9 dB accepted). Latency 49 → 58 samples
at 48 kHz.
- M7 verification (2026-10-19): M4 full acceptance 768 kHz 0 FAIL; 384 kHz exact J4 0 FAIL; HOT 0 FAIL; b1234, b56, b6, b5open
0 FAIL (b6's 5 kHz 20 V Newton median now 3, D-043 item passes); knob jumps b1234 −39.1 … −55.1 dB, b56 −62.9 dB;
automation Full 0 failures, Half 3 rescued first-solve failures → D-070 (power-amp maxIter 48), then Half and Full 0.
- Null tests 2x Half / Full 0 FAIL; pluginval (VST3, AU) SUCCESS, auval SUCCEEDED; plugin CPU at the Half default: default 13.1 %,
every knob 10 24.0 %, worst automation 24.6 %. M7 report: `harness/reports/M7/20261019_M7_report.md` (for owner review).
Risks: changes to the Newton path move the output within the tolerance (verified end to end); gains unknown until
measured; any tolerance failure is reported with options, not loosened. No new dependencies without asking.

## Questions for the owner
_(Claude Code: add questions here and also ask them in the session.)_
- Q1 (M0): git init + baseline commit? — Answered 2026-10-12: yes; repo-local author CatastrophicCoder.
Expand Down
Loading
Loading