Initial release
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# HOT-Step Community Plugins
|
||||
|
||||
Drop custom Lua plugin files here to extend the engine without rebuilding.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
plugins/
|
||||
├── solvers/ ← Custom ODE/SDE solvers
|
||||
├── schedulers/ ← Custom noise schedules
|
||||
└── guidance/ ← Custom guidance modes
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Place `.lua` files in the appropriate subdirectory
|
||||
2. Restart the engine (or the app)
|
||||
3. Your plugin appears in the UI dropdown automatically
|
||||
|
||||
The engine scans `engine/plugins/` (built-in) first, then this `plugins/` directory.
|
||||
Duplicate names are skipped with a console warning.
|
||||
|
||||
## Writing a Plugin
|
||||
|
||||
Every plugin is a single `.lua` file that returns a table with metadata and a `step()` function.
|
||||
|
||||
### Solver Example
|
||||
|
||||
```lua
|
||||
return {
|
||||
name = "my_solver",
|
||||
display = "My Custom Solver",
|
||||
type = "solver",
|
||||
nfe = 1,
|
||||
accent = "pink",
|
||||
|
||||
-- Optional user-facing parameters
|
||||
params = {
|
||||
{ key = "strength", type = "slider", label = "Strength",
|
||||
default = 0.5, min = 0, max = 1, step = 0.01 },
|
||||
},
|
||||
|
||||
step = function(x, v, t, t_next, dt, params)
|
||||
-- x: current latent (FloatArray)
|
||||
-- v: velocity prediction (FloatArray)
|
||||
-- t, t_next, dt: timestep scalars
|
||||
-- params: table of user values { strength = "0.5", ... }
|
||||
for i = 0, x:size() - 1 do
|
||||
x:set(i, x:get(i) + dt * v:get(i))
|
||||
end
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
### Scheduler Example
|
||||
|
||||
```lua
|
||||
return {
|
||||
name = "my_schedule",
|
||||
display = "My Schedule",
|
||||
type = "scheduler",
|
||||
|
||||
schedule = function(n_steps, params)
|
||||
-- Return a table of n_steps+1 descending floats from 1.0 to 0.0
|
||||
local ts = {}
|
||||
for i = 0, n_steps do
|
||||
ts[i + 1] = 1.0 - i / n_steps
|
||||
end
|
||||
return ts
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
### Guidance Example
|
||||
|
||||
```lua
|
||||
return {
|
||||
name = "my_guidance",
|
||||
display = "My Guidance",
|
||||
type = "guidance",
|
||||
|
||||
guide = function(cond, uncond, scale, t, params)
|
||||
-- cond/uncond: FloatArray (conditional/unconditional predictions)
|
||||
-- scale: guidance scale (number)
|
||||
-- t: current timestep (0→1)
|
||||
-- Return guided prediction in cond (modified in-place)
|
||||
for i = 0, cond:size() - 1 do
|
||||
local c = cond:get(i)
|
||||
local u = uncond:get(i)
|
||||
cond:set(i, u + scale * (c - u))
|
||||
end
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
## Full-Loop Solvers
|
||||
|
||||
For advanced solvers that need to control the entire sampling iteration
|
||||
(e.g., adaptive dispatch, velocity caching, SDE restarts), set
|
||||
`owns_loop = true` and define a `sample()` function instead of `step()`.
|
||||
|
||||
### Full-Loop Solver Example
|
||||
|
||||
```lua
|
||||
solver = {
|
||||
name = "my_sampler",
|
||||
display = "My Sampler",
|
||||
nfe = 0, -- varies per step
|
||||
order = 1,
|
||||
stateful = true,
|
||||
owns_loop = true, -- takes over the sampling loop
|
||||
|
||||
params = {
|
||||
{ key = "my_param", type = "slider", label = "My Param",
|
||||
default = 0.5, min = 0, max = 1, step = 0.01 },
|
||||
},
|
||||
}
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
-- xt: FloatArray [n], mutable. Contains noise initially.
|
||||
-- vt_buf: FloatArray [n], mutable. model_fn writes velocity here.
|
||||
-- schedule: Lua table {t_1, t_2, ..., t_N} (1-indexed, N = num_steps)
|
||||
-- n: total element count
|
||||
-- model_fn: function(xt_array, t_val) → writes velocity to vt_buf
|
||||
--
|
||||
-- Globals: on_step, num_steps, batch_n, n_per, params
|
||||
--
|
||||
-- Contract:
|
||||
-- 1. Call model_fn(xt, t) to evaluate the model at any timestep
|
||||
-- 2. Read velocity from vt_buf after model_fn returns
|
||||
-- 3. After each step: call on_step(step_idx, t_curr, t_next) → bool
|
||||
-- Returns true if generation was cancelled (you should return)
|
||||
-- 4. When done, xt must contain the denoised output (x0)
|
||||
|
||||
local ns = #schedule
|
||||
for i = 1, ns do
|
||||
local t_curr = schedule[i]
|
||||
|
||||
-- Evaluate model
|
||||
model_fn(xt, t_curr)
|
||||
|
||||
if i < ns then
|
||||
-- Euler step (replace with your solver logic)
|
||||
local t_next = schedule[i + 1]
|
||||
local dt = t_curr - t_next
|
||||
for j = 0, n - 1 do
|
||||
xt[j] = xt[j] - vt_buf[j] * dt
|
||||
end
|
||||
-- Report step completion (engine applies DCW, repaint, etc.)
|
||||
if on_step(i - 1, t_curr, t_next) then return end
|
||||
else
|
||||
-- Final step: predict x0
|
||||
for j = 0, n - 1 do
|
||||
xt[j] = xt[j] - vt_buf[j] * t_curr
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
> **Note:** `on_step()` applies engine corrections (DCW, repaint, guidance
|
||||
> post-step) automatically. You don't need to handle these yourself.
|
||||
|
||||
## Parameter Types
|
||||
|
||||
| Type | Fields |
|
||||
|----------|--------------------------------------------------|
|
||||
| `slider` | `key`, `label`, `default`, `min`, `max`, `step` |
|
||||
| `select` | `key`, `label`, `default`, `options` |
|
||||
| `toggle` | `key`, `label`, `default` |
|
||||
| `text` | `key`, `label`, `default`, `hint` |
|
||||
|
||||
## Safety
|
||||
|
||||
Plugins run in a sandboxed Lua environment:
|
||||
- ❌ No `os`, `io`, `debug`, `dofile`, `loadfile`
|
||||
- ✅ `math`, `string`, `table`, `require` (for companion data files)
|
||||
- ✅ Full `FloatArray` API for zero-copy memory access
|
||||
|
||||
## Sharing Plugins
|
||||
|
||||
Share your `.lua` files with other HOT-Step users! Just drop them in the right folder.
|
||||
@@ -0,0 +1,247 @@
|
||||
# MD HT Scheduler — User Manual
|
||||
## MDMAchine | A&E Concepts | GPL v3
|
||||
### Plugin Version: V3 | Internal Version: v5.0
|
||||
|
||||
---
|
||||
|
||||
## What Does HT Do?
|
||||
|
||||
Most schedulers space your denoising steps evenly (uniform) or with a simple curve (Karras, exponential). HT uses physics to place steps where they actually matter.
|
||||
|
||||
It combines two density functions:
|
||||
|
||||
**HAP (Hamiltonian Action-Principle)** simulates a particle falling through a gravity well with drag. The particle accelerates as it falls (stretching steps in the mid-sigma structure zone) and slows as drag increases (compressing steps at the end for detail refinement).
|
||||
|
||||
**TPT (Thermodynamic Phase Transition)** creates a gravity well at a specific sigma level (the "critical temperature") where the latent undergoes its most important structural change. Steps cluster around this point so the model has finer control during the crystallization moment.
|
||||
|
||||
The result: more steps where they matter, fewer where they don't. Works from 12 to 150+ steps.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
If you just want it working, use these and go:
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| Kinetic Energy | 0.3 | Standard front-loading |
|
||||
| Damping Friction | 2.2 | Moderate tail compression |
|
||||
| Critical Temp | 0.6 | Cluster at the structure/detail boundary |
|
||||
| Phase Intensity | 1.0 | Moderate clustering |
|
||||
| Well Width | 0.25 | Balanced spread |
|
||||
| Density Floor | 0.1 | Gentle minimum everywhere |
|
||||
| Everything else | OFF / defaults | Turn on one at a time |
|
||||
|
||||
**For 12-step turbo:** add `Poly Slope = 0.8` (more structure steps) and `Uniformity Blend = 0.2` (gentle uniformity).
|
||||
|
||||
**For 50+ step runs:** try `SNR Space = ON` for perceptually-weighted step placement.
|
||||
|
||||
---
|
||||
|
||||
## The Controls
|
||||
|
||||
### HAP Controls (Base Schedule Shape)
|
||||
|
||||
#### Kinetic Energy
|
||||
How aggressively the schedule front-loads steps into the structure-formation zone.
|
||||
|
||||
- 0.0 = uniform (no front-loading)
|
||||
- 0.3 = standard (default, gentle front emphasis)
|
||||
- 1.0-1.5 = noticeable structure emphasis
|
||||
- 2.0+ = aggressive (lots of steps early, sparse late)
|
||||
|
||||
Higher values mean more steps during the "big decisions" phase of generation and fewer during detail refinement. Good for complex prompts that need strong early structure. Too high and the detail phase gets starved.
|
||||
|
||||
#### Damping Friction
|
||||
How fast the schedule compresses steps toward the end.
|
||||
|
||||
- 0.0 = no compression (uniform tail)
|
||||
- 2.2 = standard (default)
|
||||
- 4.0+ = heavy end compression (many detail steps)
|
||||
|
||||
Think of it as atmospheric drag on the particle. Higher drag = the particle slows down more at the end = more steps compressed into the final detail phase.
|
||||
|
||||
### TPT Controls (Phase Transition Clustering)
|
||||
|
||||
#### Critical Temp
|
||||
Where on the sigma axis the gravity well sits (as a fraction of 0-1).
|
||||
|
||||
- 0.6 = default (structure/detail boundary)
|
||||
- 0.3-0.4 = clusters steps later (detail-focused)
|
||||
- 0.7-0.8 = clusters steps earlier (structure-focused)
|
||||
|
||||
This is the moment in the generation where the latent "crystallizes" from noise into structure. Placing the well here gives the model more steps at the most information-dense moment.
|
||||
|
||||
#### Phase Intensity
|
||||
How strong the clustering effect is.
|
||||
|
||||
- 0.0 = off (pure HAP, no clustering)
|
||||
- 1.0 = moderate (default)
|
||||
- 2.0+ = strong (heavy step concentration at critical temp)
|
||||
|
||||
At 0 you get a pure HAP schedule. As you increase, more steps pile up around the critical temp and fewer are available elsewhere.
|
||||
|
||||
#### Well Width
|
||||
How wide the clustering zone spreads around the critical temp.
|
||||
|
||||
- 0.1 = tight (steps concentrated in a narrow band — can sound "overdriven")
|
||||
- 0.25 = balanced (default)
|
||||
- 0.4+ = broad (gentle clustering over a wide zone)
|
||||
|
||||
Pairs with Phase Intensity: intensity controls depth (how many steps cluster), width controls spread (how wide the cluster zone is). Both together shape the gravity well.
|
||||
|
||||
### Density Floor
|
||||
|
||||
Guarantees a minimum step density everywhere in the schedule. Without a floor, some zones can end up with very few steps (sparse gaps), which forces the solver to make oversized jumps that cause artifacts.
|
||||
|
||||
- 0.0 = no floor (old behavior, maximum clustering contrast)
|
||||
- 0.1 = gentle floor (default, prevents worst-case sparse gaps)
|
||||
- 0.3+ = significant floor (schedule becomes more uniform overall)
|
||||
|
||||
**This is the key setting for pairing with stabilization solvers** like Trajectory Anchor. If the output sounds "crispy" or harsh, increase the floor.
|
||||
|
||||
### SNR Space
|
||||
|
||||
When ON, the integration grid is built uniform in SNR (signal-to-noise ratio) space instead of sigma space. Steps automatically track perceptual importance since SNR maps to how much "useful information" vs "noise" the model is working with at each point.
|
||||
|
||||
- OFF = sigma-uniform grid (default, standard behavior)
|
||||
- ON = SNR-uniform grid (steps cluster where SNR changes fastest)
|
||||
|
||||
At 12 steps you'll barely notice the difference. At 50-150 steps it meaningfully improves how the schedule distributes effort across the perceptual range, especially for audio where mid-frequency detail matters more than extreme high or low sigma regions.
|
||||
|
||||
### Post-Processing Controls
|
||||
|
||||
These apply after the HT schedule is computed, in order. All are default off. They compose cleanly with each other and with any step count.
|
||||
|
||||
#### LINA Warp
|
||||
Time-axis resampling ported from the MD Causal scheduler. This is different from the native Shift Warp.
|
||||
|
||||
- **Shift Warp** transforms the sigma *values* (changes what sigma each step lands on)
|
||||
- **LINA Warp** transforms *where on the curve* each step samples from (resamples the schedule itself)
|
||||
|
||||
Settings:
|
||||
- 1.0 = off (default)
|
||||
- < 1.0 = front-load (more high-sigma / structure steps)
|
||||
- > 1.0 = back-load (more low-sigma / detail steps)
|
||||
|
||||
Subtle at small deviations from 1.0. Start with 0.9 or 1.1 and adjust.
|
||||
|
||||
#### Poly Slope
|
||||
Power curve applied to the sigma values after everything else.
|
||||
|
||||
- 1.0 = off (default, no change)
|
||||
- > 1.0 = compress toward zero (more detail steps, good for long runs 50+)
|
||||
- < 1.0 = compress toward one (more structure steps, good for 12-step turbo)
|
||||
|
||||
This is the simplest global shape control. If you're running a turbo model at 12 steps and need more structural emphasis, drop poly to 0.7-0.8. If you're running 150 steps and want finer detail distribution, push it to 1.1-1.3.
|
||||
|
||||
#### Uniformity Blend
|
||||
Blends the HT schedule with a pure linear uniform schedule.
|
||||
|
||||
- 0.0 = pure HT (default, maximum clustering character)
|
||||
- 0.2-0.3 = gentle uniformity (tames clustering, good for Trajectory Anchor)
|
||||
- 0.5 = half and half
|
||||
- 1.0 = pure uniform (no HT character, just linear)
|
||||
|
||||
**This is the most direct fix for "HT doesn't pair well with my solver."** If the output sounds over-processed, harsh, or unstable with your solver, increase the blend. You're trading HT's intelligent step placement for the safety of uniform spacing.
|
||||
|
||||
#### Schedule Smoothing
|
||||
Moving average on the final sigma sequence. Eliminates sharp transitions between dense and sparse zones.
|
||||
|
||||
- 0 = off (default)
|
||||
- 3 = mild smoothing
|
||||
- 5+ = heavy smoothing
|
||||
|
||||
Helps stabilization solvers (Trajectory Anchor's inertia engine, memory buffer) by giving them gradual step-size transitions instead of sudden jumps. Slight cost: smoothing blurs the clustering precision.
|
||||
|
||||
### Engine Controls
|
||||
|
||||
#### CDF Resolution
|
||||
Resolution of the integration grid used internally. Higher = smoother CDF sampling, slightly slower to compute.
|
||||
|
||||
- 1000 = default (fine for most uses)
|
||||
- 200 = fast but rough
|
||||
- 5000 = very smooth (overkill for < 50 steps)
|
||||
|
||||
#### Shift Warp
|
||||
Native HOT-Step sigma warp. Applied first in the post-processing chain, before LINA, poly, uniformity, and smoothing.
|
||||
|
||||
- 1.0 = off
|
||||
- > 1.0 = shifts sigma distribution
|
||||
|
||||
#### Verbose
|
||||
Prints per-step sigma values, step sizes (gaps), and the min/max gap ratio to the console. Turn this on when tuning — the gap ratio tells you at a glance how uniform your schedule is.
|
||||
|
||||
- Ratio 2:1 = gentle variation (very even)
|
||||
- Ratio 5:1 = moderate (noticeable clustering)
|
||||
- Ratio 10:1+ = aggressive (sparse gaps likely, may cause artifacts)
|
||||
|
||||
---
|
||||
|
||||
## Pairing Guide
|
||||
|
||||
### With Trajectory Anchor (recommended settings)
|
||||
Trajectory Anchor's stabilization stack needs reasonably uniform step sizes to work properly. Oversized gaps in the schedule fight the inertia engine and concept lock.
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| Density Floor | 0.1-0.2 | Prevents sparse gaps |
|
||||
| Uniformity Blend | 0.1-0.3 | Tames clustering |
|
||||
| Smooth Window | 3 | Gradual transitions |
|
||||
| Poly Slope | 0.8 (at 12 steps) | More structure steps for turbo |
|
||||
| Phase Intensity | 0.5-1.0 | Don't over-cluster |
|
||||
|
||||
### With STORM
|
||||
STORM handles stiffness internally and adapts per-step, so it's more tolerant of non-uniform schedules.
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| Density Floor | 0.0-0.1 | STORM handles gaps |
|
||||
| Uniformity Blend | 0.0 | Let HT do its thing |
|
||||
| Phase Intensity | 1.0-2.0 | STORM benefits from clustering |
|
||||
|
||||
### With Omni Relational
|
||||
Omni Relational is step-based like Trajectory Anchor but lighter (no memory buffer, no concept lock). More tolerant of non-uniform schedules.
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| Density Floor | 0.1 | Gentle minimum |
|
||||
| Uniformity Blend | 0.0-0.1 | Light touch |
|
||||
|
||||
### Step count guidelines
|
||||
|
||||
| Steps | Recommended adjustments |
|
||||
|---|---|
|
||||
| 8-12 (turbo) | Poly Slope 0.7-0.8, Uniformity Blend 0.2, Floor 0.1 |
|
||||
| 25-35 (standard) | Defaults work well |
|
||||
| 50-100 (quality) | SNR Space ON, Poly Slope 1.1 |
|
||||
| 100-150 (maximum) | SNR Space ON, Poly Slope 1.2, CDF Resolution 2000+ |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Output sounds "crispy" or harsh**
|
||||
Increase Density Floor (0.2-0.3). Add Uniformity Blend (0.2). The schedule has sparse gaps causing oversized solver steps.
|
||||
|
||||
**Output sounds over-smoothed or lacks punch**
|
||||
Reduce Uniformity Blend and Smooth Window. Increase Phase Intensity. You're flattening the schedule too much — let it cluster.
|
||||
|
||||
**Trajectory Anchor fighting the schedule**
|
||||
See the Trajectory Anchor pairing guide above. Floor + blend + smoothing, all gentle.
|
||||
|
||||
**No audible difference from uniform schedule**
|
||||
Phase Intensity is probably at 0 or very low. Increase to 1.0+. Also check that Uniformity Blend isn't at 1.0 (which = pure uniform).
|
||||
|
||||
**Not sure what the schedule looks like**
|
||||
Turn on Verbose. Read the console output. The gap ratio tells you everything — 2:1 is gentle, 10:1+ is aggressive.
|
||||
|
||||
**12-step turbo sounds thin / lacks structure**
|
||||
Poly Slope 0.7-0.8 shifts more steps into the structure phase. Also consider dropping Kinetic Energy to 0.0-0.1 so HAP doesn't front-load *too* aggressively at very low step counts.
|
||||
|
||||
**150-step run sounds no better than 50**
|
||||
Turn on SNR Space. At high step counts, sigma-uniform spacing wastes steps in perceptually unimportant regions. SNR-uniform puts them where they matter.
|
||||
|
||||
---
|
||||
|
||||
*© 2026 Alexander Allan (MDMAchine) — A&E Concepts — GPL v3*
|
||||
@@ -0,0 +1,301 @@
|
||||
# MD Trajectory Anchor — User Manual
|
||||
## MDMAchine | A&E Concepts | GPL v3
|
||||
### Plugin Version: V5 | Internal Version: v5.0
|
||||
|
||||
---
|
||||
|
||||
## What Does Trajectory Anchor Do?
|
||||
|
||||
Every other solver in the MD suite takes over the entire sampling loop and controls how the model steps from noise to audio. Trajectory Anchor is different. It's a **step() solver**, which means it works alongside whatever guidance module you have active (STORM Guidance, Clarity, APG, etc.) instead of replacing it. Your guider stays active.
|
||||
|
||||
What it actually does: at each denoising step, it applies a stack of stabilization systems to the latent — momentum, structure locking, tonal correction, energy management — that keep the trajectory from drifting, oscillating, or losing coherence during the generation.
|
||||
|
||||
Think of it like guardrails on a mountain road. The model drives, the guidance steers, and Trajectory Anchor keeps you from going off the cliff.
|
||||
|
||||
---
|
||||
|
||||
## What's New in V5
|
||||
|
||||
### Step-Budget Auto-Scaling (invisible, zero new params)
|
||||
|
||||
All system strengths now automatically adapt to your step count. The solver knows whether you're running 12 steps or 150, and adjusts how hard each system pushes per step.
|
||||
|
||||
- **12 steps (turbo):** each step matters a lot, systems push ~1.7x harder per step
|
||||
- **35 steps (standard):** baseline, no change
|
||||
- **150 steps (quality):** each step matters less, systems push ~0.5x per step
|
||||
|
||||
This is why V4 users noticed "more steps = better" — with V5, the solver explicitly accounts for step budget so you get cleaner results at every step count without retuning params.
|
||||
|
||||
### Anti-Ringing on Identity Anchor (invisible, zero new params)
|
||||
|
||||
When the identity anchor pulls the latent toward its captured snapshot, V4 could overshoot — the latent moves past the anchor, then gets pulled back, then overshoots again. This creates a subtle oscillation ("ringing") that sounds rough.
|
||||
|
||||
V5 detects when the latent is already moving toward the anchor and automatically reduces the pull. If the latent is moving away, full pull is maintained. Result: smooth convergence toward the anchor without oscillation.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start — Recommended Defaults
|
||||
|
||||
If you just want it to work well out of the box:
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| Warmup Steps | 2 | Skip first 2 steps (pure noise, nothing to stabilize) |
|
||||
| Inertia Engine | ON | Smooths trajectory direction changes |
|
||||
| Inertia Alpha | 0.15 | Gentle momentum (auto-scales with step count in V5) |
|
||||
| Concept Lock | ON | Protects settled structure from noise |
|
||||
| Tonal Anchor | ON | Keeps tonal balance from drifting |
|
||||
| Everything else | OFF / defaults | Turn on one at a time as you learn them |
|
||||
|
||||
**For 12-step turbo:** use these exact defaults. V5's auto-scaling handles the rest.
|
||||
|
||||
**For 150-step quality:** same defaults. V5 backs off automatically. Optionally enable Identity Anchor (blend 0.08) and Look-Back (lambda 0.15) for even more stability.
|
||||
|
||||
Pair with: **MD HT Scheduler V3** (recommended over HAP for Trajectory Anchor) and **STORM Guidance V2** or **Clarity** or **APG**.
|
||||
|
||||
---
|
||||
|
||||
## The Systems (What Each One Does)
|
||||
|
||||
Trajectory Anchor has 8 independent systems. Each can be toggled on or off. They run in order, top to bottom, every step. All system strengths are automatically scaled by the step-budget system in V5.
|
||||
|
||||
### 1. Warmup Steps
|
||||
|
||||
**What:** Disables all stateful systems (inertia, concept lock, anchors, memory) for the first N steps.
|
||||
|
||||
**Why:** At the very start of generation, the latent is pure noise. Anchoring into chaos doesn't help — it just locks you into random structure. Warmup lets the model find its footing before the stabilization kicks in.
|
||||
|
||||
**Setting:** 2 is the sweet spot. 0 means everything is active from step 1 (not recommended). 3-4 if you're running very few total steps (like 8-12).
|
||||
|
||||
### 2. Inertia Engine
|
||||
|
||||
**What:** Carries a fraction of the previous step's velocity into the current step. Like momentum in physics — the trajectory resists sudden direction changes.
|
||||
|
||||
**Why:** Without inertia, each step is independent and the trajectory can jitter between competing solutions. With inertia, the path smooths out and commits to a direction.
|
||||
|
||||
**Inertia Alpha** controls how much carry-over (before V5 auto-scaling):
|
||||
- 0.10 = subtle, barely noticeable
|
||||
- 0.15 = recommended starting point
|
||||
- 0.20-0.25 = noticeable smoothing, may soften transients
|
||||
- 0.30+ = strong, can make things sluggish
|
||||
|
||||
The solver automatically scales alpha down when the latent has low entropy (already structured), so it pushes harder during chaotic early steps and backs off during refinement. V5 additionally scales by step budget.
|
||||
|
||||
### 3. Memory Buffer
|
||||
|
||||
**What:** Keeps the last 3 step outputs in a ring buffer and blends their average into the current output.
|
||||
|
||||
**Why:** Suppresses step-to-step jitter without redirecting the trajectory. Different from inertia — inertia smooths the velocity (direction), memory smooths the position (output).
|
||||
|
||||
**Memory Blend** controls the blend fraction:
|
||||
- 0.12 = subtle (default)
|
||||
- 0.25+ = heavy, may soften transients and fast attacks in audio
|
||||
|
||||
**Default: OFF.** Turn this on if you're hearing jittery artifacts or instability in the output. Leave it off if things sound clean — it adds a slight smoothing cost.
|
||||
|
||||
### 4. Concept Lock
|
||||
|
||||
**What:** Detects which parts of the latent are "settled" (small step-to-step change) and gently pulls them back toward their previous state. Dynamic regions are left alone.
|
||||
|
||||
**Why:** Once a structural element crystallizes mid-generation, noise in subsequent steps can erode it. Concept lock protects what's already formed while letting unfinished parts keep evolving.
|
||||
|
||||
**Concept Lock Sigma Power** controls how fast the lock fades as sigma drops:
|
||||
- 1.0 = linear fade (default, recommended)
|
||||
- 2.0 = quadratic — lock concentrated on early structure steps only, off during detail phase
|
||||
- 0.5 = slow fade — lock persists deep into detail steps (more conservative, may over-constrain)
|
||||
|
||||
**Default: ON.** This is one of the most impactful systems. Leave it on unless you specifically want maximum creative freedom in the late steps.
|
||||
|
||||
### 5. Identity Anchor
|
||||
|
||||
**What:** At a specific sigma level (anchor_sigma), captures a full snapshot of the latent. Then on every subsequent step, gently pulls the latent back toward that snapshot.
|
||||
|
||||
**Why:** Prevents late-stage structural drift — the model sometimes "changes its mind" about the overall shape of the output in the last few steps. The anchor keeps it committed to the structure it chose at the anchor point.
|
||||
|
||||
**V5 improvement:** Anti-ringing automatically detects when the latent is already moving toward the anchor and reduces the pull. No more overshoot oscillation. This makes higher anchor_blend values safer to use.
|
||||
|
||||
**Anchor Sigma** = when the snapshot is taken:
|
||||
- 0.5 = mid-generation (default) — captures after initial structure but before fine detail
|
||||
- Lower (0.3) = captures more detail, locks in later
|
||||
- Higher (0.7) = captures coarser structure only
|
||||
|
||||
**Anchor Blend** = how hard it pulls (before V5 auto-scaling):
|
||||
- 0.08 = gentle (default, recommended)
|
||||
- 0.15 = noticeable pull (safer in V5 thanks to anti-ringing)
|
||||
- 0.20+ = strong (was risky in V4, more usable in V5)
|
||||
|
||||
**Default: OFF.** Turn this on if you're hearing late-stage structural drift (the output sounds like it "forgot" what it was doing toward the end). Start at 0.08 blend.
|
||||
|
||||
### 6. Tonal Anchor
|
||||
|
||||
**What:** Captures spectral centroid and band energy ratios at anchor_sigma (same timing as identity anchor). Applies a small tonal correction each step to prevent tonal balance from drifting.
|
||||
|
||||
**Why:** The model can gradually shift tonal balance during generation — bass gets louder, highs get softer, or vice versa. This corrects for that drift without changing the content.
|
||||
|
||||
**Tonal Strength:**
|
||||
- 0.10-0.20 = recommended for audio
|
||||
- Correction is hard-capped at 0.1% per step regardless of this value, so even high settings are gentle
|
||||
|
||||
**Default: ON.** Low-cost, high-value. Keeps tonal balance stable without audible artifacts.
|
||||
|
||||
### 7. Look-Back Smoother
|
||||
|
||||
**What:** Blends the current step output toward the previous step output, weighted by sigma. Heavy smoothing at high sigma (early steps, structure phase), fading to zero at low sigma (detail phase).
|
||||
|
||||
**Why:** Suppresses ODE manifold shearing — the technical root cause of the "metallic twinge" artifact in flow-matching audio. Same mechanism used in STORM internally.
|
||||
|
||||
**Look-Back Lambda** (before V5 auto-scaling):
|
||||
- 0.15 = gentle (default)
|
||||
- 0.35 = moderate (good for 35-step simple schedule)
|
||||
- 0.55 = strong (good for 25-step DDIM schedule)
|
||||
|
||||
**Look-Back SNR Power:**
|
||||
- 1.3 = standard (25-step DDIM)
|
||||
- 1.5 = faster fade (35-step simple)
|
||||
- Higher = smoothing concentrated on very early steps only
|
||||
|
||||
**Default: OFF.** Turn this on if you're hearing metallic or harsh artifacts. If you're already running STORM Guidance (which has its own CFG adaptation), you may not need this.
|
||||
|
||||
### 8. RMS Servo
|
||||
|
||||
**What:** Downward-only energy ceiling. If the latent RMS exceeds the target range, scales it down. Never scales up — only prevents energy runaway.
|
||||
|
||||
**Why:** Some configurations (high CFG, aggressive guidance, long generations) can cause the latent energy to ramp up over the course of generation, leading to clipping or distortion.
|
||||
|
||||
**RMS Target Min / Max:**
|
||||
- Min = ceiling at low sigma (detail phase). Start at 1.2-1.8.
|
||||
- Max = ceiling at high sigma (structure phase). Start at 2.5.
|
||||
- ACE-Step latents typically run ~2.0 RMS at x0.
|
||||
|
||||
**RMS Servo Gain:**
|
||||
- 0.6 = gradual correction (default, recommended)
|
||||
- 1.0 = hard snap each step (aggressive)
|
||||
|
||||
**Default: OFF.** Only turn this on if you're experiencing energy runaway (clipping, distortion, "blown out" sound). Most setups don't need it.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
### Latent Pressure
|
||||
|
||||
Like RMS Servo but smarter — monitors the entropy * RMS product (a measure of "how chaotic and how energetic") and nudges toward a target value. Correction capped at 0.05% per step, so it accumulates gently over many steps.
|
||||
|
||||
**Default: OFF.** Experimental. If you enable it, run with verbose output first to see what your latent's actual entropy distribution looks like before setting targets.
|
||||
|
||||
### Relational Weight (Barbour Best Matching)
|
||||
|
||||
Per-block velocity equalization from the Omni Relational solver, available here as an optional addon. Equalizes component magnitudes across 4 blocks of the velocity vector, fading with sigma.
|
||||
|
||||
- 0.0 = off (default)
|
||||
- 0.3-0.5 = balanced
|
||||
|
||||
**Important:** Do NOT use this if you're also running Confluence as your solver — the two velocity-reshaping systems fight each other.
|
||||
|
||||
### Eta (SDE Noise)
|
||||
|
||||
Stochastic noise injection per step. 0 = pure deterministic ODE (default). Small values (0.05-0.15) add subtle variation without overwhelming the stabilization. Scaled by sigma so it fades during detail phase.
|
||||
|
||||
### Safety Clamp
|
||||
|
||||
Hard absolute value clamp on the latent after all corrections. 2.5 is standard. Raise to 4.0+ if you hear clamping artifacts (sounds like hard limiting / pumping). NaN/Inf triggers a full rollback to raw Euler before clamping.
|
||||
|
||||
---
|
||||
|
||||
## V5 Step-Budget Scaling — How It Works
|
||||
|
||||
You don't need to touch anything for this to work. But if you're curious about the math:
|
||||
|
||||
The solver reads the total step count from the engine and computes a scaling factor:
|
||||
|
||||
```
|
||||
budget_scale = sqrt(35 / your_step_count)
|
||||
```
|
||||
|
||||
| Your Steps | Scale Factor | Effect |
|
||||
|---|---|---|
|
||||
| 8 | 2.09x | Systems push much harder per step |
|
||||
| 12 | 1.71x | Strong push (turbo sweet spot) |
|
||||
| 25 | 1.18x | Slight push |
|
||||
| 35 | 1.00x | Reference — no change |
|
||||
| 50 | 0.84x | Slight pullback |
|
||||
| 100 | 0.59x | Moderate pullback |
|
||||
| 150 | 0.48x | Systems very gentle per step |
|
||||
|
||||
This multiplier is applied to: Inertia Alpha, Memory Blend, Anchor Blend, Tonal Strength, and Look-Back Lambda. The values you set in the params are the *base* values at 35 steps. At other step counts, V5 adjusts them automatically.
|
||||
|
||||
**Why sqrt?** Linear scaling would be too aggressive — halving the steps would double the push, which overshoots. Square root gives diminishing returns that match how the trajectory actually behaves.
|
||||
|
||||
---
|
||||
|
||||
## Pairing Guide
|
||||
|
||||
### Best pairings (tested and validated)
|
||||
|
||||
**Trajectory Anchor V5 + HT Scheduler V3 + STORM Guidance V2:**
|
||||
The gold pairing as of V5. HT V3's density floor and uniformity blend prevent the sparse step gaps that fought V4's stabilization stack. STORM Guidance's CFG rolloff prevents late-step ringing. For 150 steps, enable HT's SNR Space mode.
|
||||
|
||||
**Trajectory Anchor V5 + HT Scheduler V3 + Clarity:**
|
||||
Lighter guidance. Good for exploring. HT V3 handles the schedule side, Clarity handles post-CFG cleanup.
|
||||
|
||||
**Trajectory Anchor V5 + any schedule + APG:**
|
||||
Safest baseline. APG is always clean. Use when other pairings aren't working and you need to isolate whether the issue is guidance or schedule.
|
||||
|
||||
**Trajectory Anchor V5 + double composite schedule:**
|
||||
Illynir's original preferred pairing. Still works well, though HT V3 has been validated as superior by the same tester.
|
||||
|
||||
### What NOT to pair with
|
||||
|
||||
- **Trajectory Anchor + Confluence** with relational_weight > 0 — two velocity-reshaping systems fighting. Keep relational_weight at 0 if using Confluence.
|
||||
- **Trajectory Anchor + another step() solver** — only one solver active at a time.
|
||||
- **Trajectory Anchor + MD HAP (standalone)** — HAP's clustering creates sparse gaps that fight the stabilization stack. Use HT V3 instead, which has density floor and uniformity blend to prevent this. If you must use HAP, keep kinetic_energy at 1.0 or below and damping_friction at 0.5 or below.
|
||||
|
||||
### Schedule recommendations by step count
|
||||
|
||||
| Steps | Recommended Schedule | Key Settings |
|
||||
|---|---|---|
|
||||
| 8-12 (turbo) | HT V3 | poly_slope=0.8, uniform_blend=0.2, floor=0.1 |
|
||||
| 25-35 (standard) | HT V3 or double composite | HT defaults work well |
|
||||
| 50-100 (quality) | HT V3 | snr_space=ON, poly_slope=1.1 |
|
||||
| 100-150 (maximum) | HT V3 | snr_space=ON, poly_slope=1.2, resolution=2000+ |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Several songs playing simultaneously"**
|
||||
You're running an old version of the plugins or your HOT-Step build is behind. Grab the latest release and redeploy the whole plugin batch together. Don't mix old and new files.
|
||||
|
||||
**Output sounds "mushy" or over-smoothed**
|
||||
Turn off Memory Buffer. Reduce Inertia Alpha. If Look-Back is on, reduce lambda. You're over-stabilizing. At high step counts (100+), V5's auto-scaling should prevent this — if it's still mushy, your base values are too high.
|
||||
|
||||
**Output sounds harsh or metallic**
|
||||
Turn on Look-Back Smoother (lambda 0.15-0.35). If already on, increase lambda. Also check your guidance module — Clarity or STORM Guidance V2 help here.
|
||||
|
||||
**Output sounds "rough" or "drafted" but musical**
|
||||
This is the Trajectory Anchor tradeoff — best musicality but can leave rough edges. Try enabling Identity Anchor (blend 0.08-0.15, safer in V5 thanks to anti-ringing). Also try increasing Concept Lock Sigma Power to 1.5-2.0 to lock structure more aggressively.
|
||||
|
||||
**Late-stage structural drift ("forgot what it was doing")**
|
||||
Turn on Identity Anchor (blend 0.08, sigma 0.5). V5's anti-ringing makes this safer than in V4.
|
||||
|
||||
**Tonal balance shifting during generation**
|
||||
Tonal Anchor should already be ON by default. If it is and you're still hearing drift, increase tonal_strength to 0.20-0.30.
|
||||
|
||||
**Energy runaway / clipping / distortion**
|
||||
Turn on RMS Servo (min 1.2, max 2.5, gain 0.6). If severe, also turn on Latent Pressure.
|
||||
|
||||
**"More steps sounds worse" (shouldn't happen in V5)**
|
||||
If you're experiencing this, the step-budget auto-scaling may not be reading the step count correctly. Check that you're on the latest HOT-Step build. As a manual workaround, reduce Inertia Alpha and other strengths proportionally when increasing steps.
|
||||
|
||||
**HAP schedule sounds bad with Trajectory Anchor**
|
||||
Switch to HT Scheduler V3. This is a known pairing issue — HAP's clustering creates step gaps that fight the stabilization stack. HT V3 has density floor and uniformity blend specifically designed to solve this.
|
||||
|
||||
---
|
||||
|
||||
## Community Findings
|
||||
|
||||
**Illynir (2026-07-16):** "By far the best in terms of musicality, beyond any doubt." Spent 7+ hours across two sessions testing V3/V4. Found that more steps consistently improved output quality (confirmed by V5's step-budget scaling design). HT V3 scheduler validated as superior to HAP and double composite for Trajectory Anchor pairing: "Much MUCH better than HAP" and "beats my two-stage composite."
|
||||
|
||||
---
|
||||
|
||||
*© 2026 Alexander Allan (MDMAchine) — A&E Concepts — GPL v3*
|
||||
@@ -0,0 +1,169 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Clarity V1 — Lightweight Post-CFG Cleanup Guidance
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- Simple spectral cleanup for flow-matching audio. Tames HF harshness,
|
||||
-- clamps magnitude spikes, optional orthogonal projection to keep
|
||||
-- corrections perpendicular to the original signal direction.
|
||||
--
|
||||
-- Designed to pair with MD solvers (STORM, Confluence, Hamiltonian, etc.)
|
||||
-- Drop-in guidance module. Minimal state, zero-allocation hot path.
|
||||
-- ============================================================================
|
||||
|
||||
guidance = {
|
||||
name = "md_clarity_v1",
|
||||
display = "MD Clarity V1",
|
||||
description = "Lightweight post-CFG cleanup. HF smoothing, spike clamping, orthogonal projection. Pairs with MD solvers.",
|
||||
params = {
|
||||
{ key = "strength", type = "slider", label = "Strength",
|
||||
default = 0.15, min = 0.0, max = 0.5, step = 0.01,
|
||||
hint = "Overall correction intensity. 0.10-0.20 for subtle cleanup." },
|
||||
{ key = "hf_smooth", type = "slider", label = "HF Smoothing",
|
||||
default = 0.25, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Laplacian HF damping. Tames harshness/metallic edge. 0=off." },
|
||||
{ key = "spike_clamp", type = "slider", label = "Spike Clamp",
|
||||
default = 2.5, min = 1.0, max = 6.0, step = 0.25,
|
||||
hint = "Hard clamp on per-element magnitude relative to mean. Lower=more aggressive." },
|
||||
{ key = "orthogonal", type = "toggle", label = "Orthogonal Projection",
|
||||
default = true,
|
||||
hint = "Project corrections perpendicular to original signal. Prevents reinforcing existing structure." },
|
||||
{ key = "preserve_energy", type = "slider", label = "Preserve Energy",
|
||||
default = 0.0, min = 0.0, max = 0.5, step = 0.05,
|
||||
hint = "Blend output back toward original. 0=full correction, 0.5=half." },
|
||||
},
|
||||
}
|
||||
|
||||
local EPSILON = 1e-8
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
-- ── HF SMOOTHING (Laplacian damping) ────────────────────────────────────────
|
||||
-- Applies a simple neighbor-averaging pass weighted by `blend`.
|
||||
-- Targets high-frequency oscillations without touching broadband energy.
|
||||
|
||||
local function smooth_hf(buf, n, blend)
|
||||
if blend <= 0.0 or n < 3 then return end
|
||||
|
||||
local prev = buf[0]
|
||||
local curr = buf[0]
|
||||
|
||||
for i = 0, n - 1 do
|
||||
local next_val = (i < n - 1) and buf[i + 1] or buf[i]
|
||||
curr = buf[i]
|
||||
local smoothed = (prev + curr + next_val) / 3.0
|
||||
buf[i] = curr * (1.0 - blend) + smoothed * blend
|
||||
prev = curr
|
||||
end
|
||||
end
|
||||
|
||||
-- ── SPIKE CLAMPING ──────────────────────────────────────────────────────────
|
||||
-- Clamps any element whose absolute value exceeds `threshold * mean_abs`.
|
||||
-- Prevents outlier magnitudes from dominating the latent.
|
||||
|
||||
local function clamp_spikes(buf, n, threshold)
|
||||
if threshold <= 0.0 then return end
|
||||
|
||||
local mean_abs = 0.0
|
||||
for i = 0, n - 1 do mean_abs = mean_abs + math.abs(buf[i]) end
|
||||
mean_abs = mean_abs / math.max(n, 1) + EPSILON
|
||||
|
||||
local limit = mean_abs * threshold
|
||||
for i = 0, n - 1 do
|
||||
buf[i] = clamp(buf[i], -limit, limit)
|
||||
end
|
||||
end
|
||||
|
||||
-- ── ORTHOGONAL PROJECTION ───────────────────────────────────────────────────
|
||||
-- Decomposes delta into components parallel and perpendicular to the original
|
||||
-- signal. Keeps only the perpendicular part (scaled to preserve magnitude).
|
||||
-- Standard Gram-Schmidt, nothing exotic.
|
||||
|
||||
local function project_orthogonal(delta, original, n)
|
||||
local dot_do = 0.0
|
||||
local dot_oo = 0.0
|
||||
local dot_dd = 0.0
|
||||
|
||||
for i = 0, n - 1 do
|
||||
dot_do = dot_do + delta[i] * original[i]
|
||||
dot_oo = dot_oo + original[i] * original[i]
|
||||
dot_dd = dot_dd + delta[i] * delta[i]
|
||||
end
|
||||
|
||||
if dot_oo < EPSILON then return end
|
||||
|
||||
local proj_scale = dot_do / dot_oo
|
||||
local ortho_sq = 0.0
|
||||
|
||||
for i = 0, n - 1 do
|
||||
delta[i] = delta[i] - proj_scale * original[i]
|
||||
ortho_sq = ortho_sq + delta[i] * delta[i]
|
||||
end
|
||||
|
||||
-- Rescale to preserve original delta magnitude
|
||||
if ortho_sq > EPSILON then
|
||||
local rescale = math.sqrt(dot_dd / ortho_sq)
|
||||
for i = 0, n - 1 do delta[i] = delta[i] * rescale end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── GUIDE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function guide(pred_cond, pred_uncond, guidance_scale, result, Oc, T, norm_threshold)
|
||||
local n = Oc * T
|
||||
local p = params or {}
|
||||
|
||||
-- HOT-Step integration fix: result is an OUTPUT buffer holding the previous
|
||||
-- step's stale velocity at entry -- guide() must produce the CFG combine
|
||||
-- itself. Route the base combine through native apg() (momentum smoothing,
|
||||
-- perpendicular projection, norm thresholding), then run the clarity
|
||||
-- cleanup on top of it -- true "post-CFG" as designed.
|
||||
apg(pred_cond, pred_uncond, guidance_scale, result, Oc, T, norm_threshold)
|
||||
|
||||
local strength = clamp((p.strength or 0.15), 0.0, 0.5)
|
||||
if strength <= 0.0 then return end -- result already holds the APG combine
|
||||
|
||||
local hf_blend = clamp((p.hf_smooth or 0.25), 0.0, 1.0)
|
||||
local spike_th = clamp((p.spike_clamp or 2.5), 1.0, 6.0)
|
||||
local f_ortho = p.orthogonal
|
||||
if f_ortho == nil then f_ortho = true end
|
||||
local preserve = clamp((p.preserve_energy or 0.0), 0.0, 0.5)
|
||||
|
||||
-- 1. Snapshot original
|
||||
local original = {}
|
||||
for i = 0, n - 1 do original[i] = result[i] end
|
||||
|
||||
-- 2. Compute delta (what APG/CFG added beyond unconditional)
|
||||
local delta = {}
|
||||
for i = 0, n - 1 do delta[i] = result[i] - pred_uncond[i] end
|
||||
|
||||
-- 3. HF smoothing on delta
|
||||
smooth_hf(delta, n, hf_blend)
|
||||
|
||||
-- 4. Spike clamping on delta
|
||||
clamp_spikes(delta, n, spike_th)
|
||||
|
||||
-- 5. Orthogonal projection (keep corrections perpendicular to signal)
|
||||
if f_ortho then
|
||||
project_orthogonal(delta, original, n)
|
||||
end
|
||||
|
||||
-- 6. Apply cleaned delta
|
||||
for i = 0, n - 1 do
|
||||
local cleaned = pred_uncond[i] + delta[i]
|
||||
local blended = original[i] + (cleaned - original[i]) * strength
|
||||
|
||||
if preserve > 0.0 then
|
||||
result[i] = blended * (1.0 - preserve) + original[i] * preserve
|
||||
else
|
||||
result[i] = blended
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,189 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
-- MD STORM Guidance v2.0
|
||||
-- Sigma-Aware CFG Adaptation + Normalized Attention Guidance (NAG)
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- Optimized: Strict Boundary State Resets & Engine Variable Safety
|
||||
--
|
||||
-- ── SYSTEM 1: SIGMA-AWARE CFG (guide()) ─────────────────────────────────────
|
||||
--
|
||||
-- Flat at full guidance_scale from step 0 until knee fraction of schedule.
|
||||
-- Past knee: tail rolloff via power curve down to floor.
|
||||
-- Generation boundary reset captures true sigma_max every new run.
|
||||
--
|
||||
-- ── SYSTEM 2: NAG POST-STEP SUPPRESSION (post_step()) ───────────────────────
|
||||
--
|
||||
-- Soft-clamps latent elements spiking above threshold relative to mean.
|
||||
-- Suppresses progressive attention amplification / harmonic hum in LoRA.
|
||||
-- step_index / step_idx dual-check prevents static dither mask.
|
||||
--
|
||||
-- ── RECOMMENDED SETTINGS FOR STORM + ACE-STEP LORA ─────────────────────────
|
||||
--
|
||||
-- CFG knee: 0.65 (rolloff starts in last quarter of schedule)
|
||||
-- CFG tail_power: 2.5 (quadratic rolloff)
|
||||
-- CFG floor: 0.60 (never drop below 60% of guidance_scale)
|
||||
-- NAG clamp: 0.20 (standard LoRA hum suppression)
|
||||
-- NAG threshold: 0.75 (fires on elements >x mean magnitude)
|
||||
-- NAG dither: true (always on for audio)
|
||||
--
|
||||
-- ============================================================================
|
||||
|
||||
guidance = {
|
||||
name = "md_storm_guidance",
|
||||
display = "MD STORM Guidance V2",
|
||||
description = "Companion guidance plugin for STORM. (1) Sigma-aware CFG adaptation — flat at full scale until knee, then tail rolloff to prevent late-step over-sharpening and harmonic ringing. (2) NAG post-step latent suppression — soft-clamps spiking elements to suppress resonance buildup in LoRA inference. Generation-boundary safe.",
|
||||
params = {
|
||||
-- ── CFG Adaptation ──────────────────────────────────────────────────
|
||||
{ key = "cfg_adapt_enabled", type = "toggle", label = "Sigma CFG Adaptation", default = true, hint = "Bleed guidance scale as sigma drops. Prevents harmonic ringing and over-sharpening at late steps." },
|
||||
{ key = "cfg_knee", type = "slider", label = "Rolloff Knee", default = 0.65, min = 0.1, max = 0.9, step = 0.05, hint = "Progress fraction where rolloff begins. 0.75 = last quarter of steps. Higher = later, gentler rolloff." },
|
||||
{ key = "cfg_tail_power", type = "slider", label = "Rolloff Shape", default = 2.5, min = 0.5, max = 5.0, step = 0.25, hint = "Rolloff curve power. 1.0=linear, 2.0=quadratic (smooth), 4.0=aggressive late cliff." },
|
||||
{ key = "cfg_floor", type = "slider", label = "Guidance Floor", default = 0.60, min = 0.0, max = 0.95, step = 0.05, hint = "Minimum guidance scale as fraction of set guidance_scale. 0.70 = never drop below 70%. Raise if detail collapses." },
|
||||
|
||||
-- ── NAG Suppression ─────────────────────────────────────────────────
|
||||
{ key = "nag_enabled", type = "toggle", label = "NAG Suppression", default = true, hint = "Suppress progressive attention amplification / harmonic resonance buildup in LoRA inference. Applied per step to xt." },
|
||||
{ key = "nag_clamp_intensity",type = "slider", label = "NAG Clamp Intensity", default = 0.20, min = 0.0, max = 1.0, step = 0.01, hint = "Blend strength toward normalized value for spiking elements. 0.15-0.25 recommended for audio LoRA." },
|
||||
{ key = "nag_spike_threshold",type = "slider", label = "NAG Spike Threshold", default = 0.75, min = 0.3, max = 0.99, step = 0.01, hint = "Relative magnitude above which suppression fires. 0.85 maps to ~6.7x mean. Lower = more aggressive." },
|
||||
{ key = "nag_dither", type = "toggle", label = "NAG Seed Dithering", default = true, hint = "Randomize spike mask edges to prevent sharp structural breaks at clamp boundary. Always recommended for audio." },
|
||||
{ key = "nag_dither_strength",type = "slider", label = "Dither Strength", default = 0.1, min = 0.0, max = 0.20, step = 0.01, hint = "Dither amplitude. 0.05 = standard. Softens mask edges without losing suppression effect." },
|
||||
{ key = "nag_sigma_gate", type = "slider", label = "NAG Sigma Gate", default = 0.0, min = 0.0, max = 0.8, step = 0.05, hint = "Only apply NAG when sigma is below this value. 0.0 = always active. 0.5 = refinement zone only." },
|
||||
},
|
||||
}
|
||||
|
||||
-- ── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
local EPSILON = 1e-6
|
||||
local _last_n = 0
|
||||
local _sigma_max = 1.0
|
||||
local _sigma_min = 0.0
|
||||
|
||||
-- ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
-- Seeded LCG (deterministic, no math.random dependency)
|
||||
local function make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
-- ── CFG Envelope ─────────────────────────────────────────────────────────────
|
||||
|
||||
local function compute_cfg_envelope(sigma_curr, sigma_max, sigma_min, knee, tail_power, floor_)
|
||||
local range = sigma_max - sigma_min + EPSILON
|
||||
local progress = clamp((sigma_max - sigma_curr) / range, 0.0, 1.0)
|
||||
|
||||
-- Flat at 1.0 until knee, tail rolloff only past knee
|
||||
local envelope = 1.0
|
||||
if progress > knee then
|
||||
local tail_progress = clamp((progress - knee) / math.max(1.0 - knee, EPSILON), 0.0, 1.0)
|
||||
envelope = 1.0 - tail_progress ^ tail_power
|
||||
end
|
||||
|
||||
return floor_ + (1.0 - floor_) * clamp(envelope, 0.0, 1.0)
|
||||
end
|
||||
|
||||
-- ── NAG Suppression (in-place on FloatArray) ─────────────────────────────────
|
||||
|
||||
local function apply_nag_inplace(xt, n, clamp_int, spike_thr, dither, dither_str, seed)
|
||||
local mean_norm = 0.0
|
||||
for i = 0, n - 1 do mean_norm = mean_norm + math.abs(xt[i]) end
|
||||
mean_norm = (mean_norm / n) + EPSILON
|
||||
|
||||
local thr = 1.0 / (1.0 - spike_thr + EPSILON)
|
||||
local rng = dither and make_rng(seed) or nil
|
||||
|
||||
for i = 0, n - 1 do
|
||||
local rel_mag = math.abs(xt[i]) / mean_norm
|
||||
local spike = (rel_mag > thr) and 1.0 or 0.0
|
||||
|
||||
if dither and rng ~= nil then
|
||||
spike = clamp(spike - rng() * dither_str, 0.0, 1.0)
|
||||
end
|
||||
|
||||
-- Skip blend entirely on non-spiking elements
|
||||
if spike > 0.0 then
|
||||
local sign = (xt[i] >= 0.0) and 1.0 or -1.0
|
||||
local norm_target = sign * mean_norm
|
||||
local blend = spike * clamp_int
|
||||
xt[i] = xt[i] * (1.0 - blend) + norm_target * blend
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── guide() — sigma-aware CFG ────────────────────────────────────────────────
|
||||
|
||||
function guide(pred_cond, pred_uncond, guidance_scale, result, Oc, T, norm_threshold)
|
||||
local n = Oc * T
|
||||
local cur_step = step_index or step_idx or 0
|
||||
local sigma = t_curr or 0.5
|
||||
|
||||
-- Strict generation boundary reset: captures true sigma_max every new run.
|
||||
-- Fires on step 0 OR if latent size changed (model switch).
|
||||
if n ~= _last_n or cur_step == 0 then
|
||||
_last_n = n
|
||||
_sigma_max = sigma
|
||||
_sigma_min = 0.0
|
||||
end
|
||||
|
||||
local cfg_on = (params and params.cfg_adapt_enabled)
|
||||
if cfg_on == nil then cfg_on = true end
|
||||
local knee = (params and params.cfg_knee) or 0.75
|
||||
local tail_power = (params and params.cfg_tail_power) or 2.0
|
||||
local floor_ = (params and params.cfg_floor) or 0.70
|
||||
|
||||
local effective_scale = guidance_scale
|
||||
|
||||
if cfg_on then
|
||||
local envelope = compute_cfg_envelope(sigma, _sigma_max, _sigma_min, knee, tail_power, floor_)
|
||||
effective_scale = guidance_scale * envelope
|
||||
end
|
||||
|
||||
apg(pred_cond, pred_uncond, effective_scale, result, Oc, T, norm_threshold)
|
||||
end
|
||||
|
||||
-- ── post_step() — NAG suppression ────────────────────────────────────────────
|
||||
|
||||
function post_step(xt, t, n, eval_cond, eval_uncond, vt_cond, vt_uncond)
|
||||
local nag_on = (params and params.nag_enabled)
|
||||
if nag_on == nil then nag_on = true end
|
||||
if not nag_on then return end
|
||||
|
||||
local clamp_int = (params and params.nag_clamp_intensity) or 0.20
|
||||
local spike_thr = (params and params.nag_spike_threshold) or 0.85
|
||||
local dither = (params and params.nag_dither)
|
||||
if dither == nil then dither = true end
|
||||
local dither_str = (params and params.nag_dither_strength) or 0.05
|
||||
local sigma_gate = (params and params.nag_sigma_gate) or 0.0
|
||||
|
||||
local sigma = t or 0.0
|
||||
local cur_step = step_index or step_idx or 0
|
||||
|
||||
if sigma_gate > 0.0 and sigma > sigma_gate then return end
|
||||
|
||||
-- step-varying seed: prevents identical dither mask every step
|
||||
local seed = math.floor(42 + cur_step * 7919)
|
||||
|
||||
apply_nag_inplace(xt, n, clamp_int, spike_thr, dither, dither_str, seed)
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
--[[
|
||||
md_audio_tiled.lua
|
||||
Postprocess plugin adapter for MD Audio Tiled Core
|
||||
|
||||
Wraps md_audio_tiled_core.lua (v3.0.1) to conform to the HOT-Step
|
||||
postprocess plugin contract. The core module is loaded via require()
|
||||
and exposes execute_tiled_decode() as the entry point.
|
||||
|
||||
© 2026 Alexander Allan (MDMAchine) | A&E Concepts
|
||||
GPL v3
|
||||
--]]
|
||||
|
||||
local core = require("md_audio_tiled_core")
|
||||
|
||||
postprocess = {
|
||||
name = "md_audio_tiled",
|
||||
display = "MD Audio Tiled Decoder",
|
||||
description = "Tiled VAE decode with OLA crossfade, dual-pass merge, LSS, and DSP chain",
|
||||
accent = "cyan",
|
||||
|
||||
params = {
|
||||
{ key = "dual_pass", type = "toggle", label = "Dual Pass",
|
||||
default = false,
|
||||
hint = "Two staggered decode passes merged with trapezoidal weights. Eliminates seam artifacts but doubles VAE decode time." },
|
||||
{ key = "lss_strength", type = "slider", label = "LSS Strength",
|
||||
default = 0.25, min = 0, max = 1, step = 0.01,
|
||||
hint = "Latent channel suppression. Reduces hum from low-variance VAE bias channels." },
|
||||
{ key = "stereo_width", type = "slider", label = "Stereo Width",
|
||||
default = 0.8, min = 0, max = 2, step = 0.01,
|
||||
hint = "M/S stereo width. 0=mono, 1=unity, 2=doubled side." },
|
||||
{ key = "hum_notch", type = "toggle", label = "Hum Notch Filter",
|
||||
default = true,
|
||||
hint = "Multi-band surgical cuts at 74/94/654Hz to remove Oobleck VAE hum." },
|
||||
{ key = "peak_normalize_db", type = "slider", label = "Peak Normalize",
|
||||
default = -1, min = -12, max = 0, step = 0.5,
|
||||
hint = "Transparent peak normalization (pure gain reduction, no distortion). Scales audio so the loudest peak = target dBFS. Applied before soft clip. Set to 0 to disable." },
|
||||
{ key = "soft_clip_db", type = "slider", label = "Soft Clip Ceiling",
|
||||
default = -3.0, min = -12, max = 0, step = 0.5,
|
||||
hint = "tanh saturation ceiling in dB. Acts as safety net after peak normalize. Set to 0 to disable." },
|
||||
},
|
||||
}
|
||||
|
||||
-- Entry point called by the engine via lua_call_postprocess()
|
||||
-- Args:
|
||||
-- latents: Lua table, 1-indexed [B * C_lat * W] flat row-major
|
||||
-- B: batch size (always 1 — engine calls per-batch-item)
|
||||
-- C_lat: latent channels (64)
|
||||
-- W: latent width (time frames)
|
||||
-- C_aud: audio channels (2)
|
||||
-- final_samples: expected audio samples per channel
|
||||
-- upscale_factor: 1920 (VAE upsample ratio)
|
||||
-- vae_decode_fn: callback(latent_table, T_latent) → audio_table, T_audio
|
||||
function process(latents, B, C_lat, W, C_aud, final_samples, upscale_factor, vae_decode_fn)
|
||||
-- Build params table from UI-injected globals
|
||||
local p = {}
|
||||
for k, v in pairs(core.DEFAULT_PARAMS) do
|
||||
p[k] = v
|
||||
end
|
||||
|
||||
-- Override from UI params (injected by lua_inject_params)
|
||||
if params then
|
||||
if params.dual_pass ~= nil then p.dual_pass = params.dual_pass end
|
||||
if params.lss_strength then p.lss_strength = params.lss_strength end
|
||||
if params.stereo_width then p.stereo_width = params.stereo_width end
|
||||
if params.hum_notch ~= nil then p.hum_notch_enabled = params.hum_notch end
|
||||
if params.peak_normalize_db then
|
||||
-- 0 dB means disabled (peak_normalize_db must be < 0 to activate)
|
||||
if params.peak_normalize_db < 0 then
|
||||
p.peak_normalize_db = params.peak_normalize_db
|
||||
else
|
||||
p.peak_normalize_db = nil -- disable
|
||||
end
|
||||
end
|
||||
if params.soft_clip_db then p.soft_clip_db = params.soft_clip_db end
|
||||
end
|
||||
|
||||
local audio = core.execute_tiled_decode(
|
||||
vae_decode_fn, latents, B, C_lat, W,
|
||||
C_aud, final_samples, upscale_factor, p)
|
||||
|
||||
-- Bridge expects two return values: audio_table, T_audio
|
||||
return audio, final_samples
|
||||
end
|
||||
@@ -0,0 +1,850 @@
|
||||
--[[
|
||||
md_audio_tiled_core.lua
|
||||
MD Audio VAE Tiled Decoder — Core Math Engine
|
||||
|
||||
© 2026 Alexander Allan (MDMAchine) | A&E Concepts
|
||||
GPL v3 — Public version.
|
||||
|
||||
Version: 3.0.1
|
||||
PARITY: Algorithmic parity with md_audio_tiled_core.py v3.0.1
|
||||
All coefficients, thresholds, and control flow identical.
|
||||
|
||||
Implements the host-side tiling arithmetic and DSP chain for Lua-based
|
||||
VAE runtimes (HotStep, custom scripting environments).
|
||||
|
||||
What this file provides:
|
||||
• Tile schedule builder (fixed and BPM-synced)
|
||||
• Fade-in window generation (Hann/Cosine/Linear)
|
||||
• Trapezoidal weight maps (dual-pass merge)
|
||||
• Latent Spectral Suppressor (LSS)
|
||||
• Biquad filter engine (peaking EQ + low shelf)
|
||||
• Hum notch chain (bass shelf + surgical cuts)
|
||||
• High-pass filter (Butterworth 2nd-order biquad)
|
||||
• Soft clipper (tanh saturation)
|
||||
• Stereo width (M/S)
|
||||
• OLA write primitive with crossfade
|
||||
• RMS leveling + absolute ceiling
|
||||
• Dual-pass trapezoidal merge
|
||||
|
||||
What this file does NOT provide (requires your VAE runtime):
|
||||
• vae.decode() — neural network inference
|
||||
• STFT-domain ops (HPC spectral crossfade, SCE, Wiener)
|
||||
• GPU tensor ops
|
||||
|
||||
TENSOR CONVENTION:
|
||||
All audio/latent buffers are flat Lua tables indexed [1..N].
|
||||
Layout: row-major [B, C, L] — B outermost, then C, then sample index.
|
||||
index(b, c, i, C, L) = (b-1)*C*L + (c-1)*L + i (1-indexed)
|
||||
|
||||
IMPORTANT: This is a reference/scripting port. For production use in a
|
||||
Lua JIT environment, profile biquad inner loops and cache
|
||||
filter states across tiles.
|
||||
--]]
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Lua 5.4 removed math.tanh — polyfill via exp identity
|
||||
local tanh = math.tanh or function(x)
|
||||
if x > 20 then return 1.0 end
|
||||
if x < -20 then return -1.0 end
|
||||
local e2x = math.exp(2 * x)
|
||||
return (e2x - 1) / (e2x + 1)
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- CONSTANTS
|
||||
-- =============================================================================
|
||||
|
||||
M.EPSILON = 1e-8
|
||||
M.GAIN_CLAMP_BASE = 0.05 -- ±5% base RMS ride
|
||||
M.GAIN_CLAMP_MAX = 0.20 -- ±20% max on entropy spikes
|
||||
M.RMS_ABS_CEIL = 0.35 -- Hard per-tile RMS ceiling
|
||||
M.DUAL_PASS_TAPER = 0.25 -- Trapezoidal edge ramp fraction
|
||||
M.MIN_OLA_SAMPLES = 8
|
||||
M.VAE_CONTEXT_FRAMES = 128 -- Oobleck causal warm-up prefix
|
||||
|
||||
-- =============================================================================
|
||||
-- UTILITY
|
||||
-- =============================================================================
|
||||
|
||||
local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end
|
||||
local function sign(v) return v > 0 and 1 or (v < 0 and -1 or 0) end
|
||||
|
||||
-- 1-indexed flat buffer index: [B, C, L] row-major
|
||||
local function idx(b, c, i, C, L) return (b-1)*C*L + (c-1)*L + i end
|
||||
|
||||
-- Allocate zeroed flat table of length N
|
||||
local function zeros(N)
|
||||
local t = {}
|
||||
for i = 1, N do t[i] = 0.0 end
|
||||
return t
|
||||
end
|
||||
|
||||
local function copy(src, N)
|
||||
local t = {}
|
||||
for i = 1, N do t[i] = src[i] end
|
||||
return t
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- WINDOW GENERATION
|
||||
-- =============================================================================
|
||||
|
||||
---Fade-in ramp [0 → 1], `length` samples.
|
||||
---@param length integer
|
||||
---@param mode string "Hann"|"Cosine"|"Linear"
|
||||
---@return table
|
||||
function M.make_fade_in(length, mode)
|
||||
local w = {}
|
||||
for i = 1, length do
|
||||
local t = (i - 1) / math.max(1, length - 1)
|
||||
if mode == "Hann" then
|
||||
w[i] = 0.5 * (1 - math.cos(math.pi * t))
|
||||
elseif mode == "Cosine" then
|
||||
w[i] = math.sin(math.pi / 2 * t)
|
||||
else -- Linear
|
||||
w[i] = t
|
||||
end
|
||||
end
|
||||
return w
|
||||
end
|
||||
|
||||
---Trapezoidal weight: flat 1.0 centre, ramps from 0.5 at both edges.
|
||||
---@param length integer
|
||||
---@param edge_frac number fraction of length used for ramp (default 0.25)
|
||||
---@return table
|
||||
function M.make_trapezoid(length, edge_frac)
|
||||
edge_frac = edge_frac or M.DUAL_PASS_TAPER
|
||||
local taper = math.max(M.MIN_OLA_SAMPLES, math.floor(length * edge_frac))
|
||||
taper = math.min(taper, math.floor(length / 2))
|
||||
local w = {}
|
||||
for i = 1, length do
|
||||
if i <= taper then
|
||||
w[i] = 0.5 + 0.5 * ((i - 1) / math.max(1, taper - 1))
|
||||
elseif i > length - taper then
|
||||
local j = length - i
|
||||
w[i] = 0.5 + 0.5 * (j / math.max(1, taper - 1))
|
||||
else
|
||||
w[i] = 1.0
|
||||
end
|
||||
end
|
||||
return w
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- TILE SCHEDULE BUILDERS
|
||||
-- =============================================================================
|
||||
|
||||
---Build a fixed (non-adaptive) tile schedule covering [1, W] in latent frames.
|
||||
---Returns list of {start, end_, overlap} tables (1-indexed start/end).
|
||||
---@param W integer total latent frames
|
||||
---@param tile_size integer
|
||||
---@param overlap integer
|
||||
---@param start_offset integer 0-indexed start position (default 0)
|
||||
---@return table[]
|
||||
function M.build_fixed_schedule(W, tile_size, overlap, start_offset)
|
||||
start_offset = start_offset or 0
|
||||
local schedule = {}
|
||||
local hop = tile_size - overlap
|
||||
if hop <= 0 then hop = math.max(1, math.floor(tile_size / 2)) end
|
||||
local cursor = start_offset
|
||||
while cursor < W do
|
||||
local e = math.min(W, cursor + tile_size)
|
||||
table.insert(schedule, {start = cursor, end_ = e, overlap = overlap})
|
||||
cursor = cursor + hop
|
||||
end
|
||||
return schedule
|
||||
end
|
||||
|
||||
---BPM-synced overlap in latent frames.
|
||||
---Downgrades bar count until overlap fits within tile_size / 2.
|
||||
---@param bpm integer
|
||||
---@param target_bars number e.g. 4.0 for "Max 4 Bars"
|
||||
---@param tile_size integer
|
||||
---@param latents_per_second number ACE-Step = 5.0
|
||||
---@return integer overlap in latent frames
|
||||
function M.bpm_sync_overlap(bpm, target_bars, tile_size, latents_per_second)
|
||||
latents_per_second = latents_per_second or 5.0
|
||||
local sec_per_bar = (60.0 / bpm) * 4.0
|
||||
local frames_per_bar = sec_per_bar * latents_per_second
|
||||
local bars = target_bars
|
||||
local calc = math.floor(sec_per_bar * bars * latents_per_second + 0.5)
|
||||
|
||||
while calc > math.floor(tile_size / 2) and bars > 0.25 do
|
||||
bars = bars / 2.0
|
||||
calc = math.floor(sec_per_bar * bars * latents_per_second + 0.5)
|
||||
end
|
||||
|
||||
-- Integer-multiple snap
|
||||
if frames_per_bar >= 1.0 then
|
||||
local n = math.floor(calc / frames_per_bar + 0.5)
|
||||
calc = n * math.floor(frames_per_bar)
|
||||
if calc < 8 then calc = 8 end
|
||||
end
|
||||
return calc
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- LSS: LATENT SPECTRAL SUPPRESSOR
|
||||
-- =============================================================================
|
||||
|
||||
---Suppress low-variance latent channels in-place.
|
||||
---Modifies `latents` (flat [B, C_lat, T] table) in place.
|
||||
---@param latents table flat [B, C, T] row-major
|
||||
---@param B integer batch size
|
||||
---@param C integer latent channels
|
||||
---@param T integer latent time frames
|
||||
---@param strength number suppression strength (0–1, gold standard 0.25)
|
||||
---@param var_threshold number normalized variance threshold (gold standard 0.12)
|
||||
---@param dc_remove boolean WARNING: causes metallic distortion — keep false
|
||||
function M.apply_lss(latents, B, C, T, strength, var_threshold, dc_remove)
|
||||
strength = strength or 0.25
|
||||
var_threshold = var_threshold or 0.12
|
||||
dc_remove = dc_remove or false
|
||||
if strength < 1e-4 then return end
|
||||
|
||||
-- Per-channel variance averaged over batch
|
||||
local ch_var = zeros(C)
|
||||
for b = 1, B do
|
||||
for c = 1, C do
|
||||
local s, sq = 0.0, 0.0
|
||||
for t = 1, T do
|
||||
local v = latents[idx(b, c, t, C, T)]
|
||||
s = s + v
|
||||
sq = sq + v * v
|
||||
end
|
||||
local mean = s / T
|
||||
ch_var[c] = ch_var[c] + (sq / T - mean * mean)
|
||||
end
|
||||
end
|
||||
for c = 1, C do ch_var[c] = ch_var[c] / B end
|
||||
|
||||
local var_min = math.huge
|
||||
local var_max = -math.huge
|
||||
for c = 1, C do
|
||||
if ch_var[c] < var_min then var_min = ch_var[c] end
|
||||
if ch_var[c] > var_max then var_max = ch_var[c] end
|
||||
end
|
||||
local var_rng = (var_max - var_min) + M.EPSILON
|
||||
|
||||
-- Per-channel suppression gain
|
||||
local gain = zeros(C)
|
||||
for c = 1, C do
|
||||
local var_norm = (ch_var[c] - var_min) / var_rng
|
||||
if var_norm < var_threshold then
|
||||
local smooth = (1 - strength) +
|
||||
strength * (var_norm / (var_threshold + M.EPSILON))
|
||||
smooth = clamp(smooth, 1 - strength, 1.0)
|
||||
gain[c] = smooth
|
||||
else
|
||||
gain[c] = 1.0
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply (with optional DC removal)
|
||||
for b = 1, B do
|
||||
for c = 1, C do
|
||||
if dc_remove then
|
||||
local s = 0.0
|
||||
for t = 1, T do s = s + latents[idx(b, c, t, C, T)] end
|
||||
local mean = s / T
|
||||
for t = 1, T do
|
||||
latents[idx(b, c, t, C, T)] = latents[idx(b, c, t, C, T)] - mean
|
||||
end
|
||||
end
|
||||
local g = gain[c]
|
||||
for t = 1, T do
|
||||
latents[idx(b, c, t, C, T)] = latents[idx(b, c, t, C, T)] * g
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- BIQUAD FILTER ENGINE
|
||||
-- =============================================================================
|
||||
|
||||
---Compute peaking EQ biquad coefficients (normalized, a[1]=1).
|
||||
---@return table b {b0,b1,b2}
|
||||
---@return table a {1, a1, a2}
|
||||
function M.peaking_biquad_coeffs(f0, gain_db, Q, sr)
|
||||
local A = 10 ^ (gain_db / 40)
|
||||
local w0 = 2 * math.pi * f0 / sr
|
||||
local cos_w0 = math.cos(w0)
|
||||
local sin_w0 = math.sin(w0)
|
||||
local alpha = sin_w0 / (2 * Q)
|
||||
local denom = 1 + alpha / A
|
||||
return
|
||||
{(1 + alpha * A) / denom, (-2 * cos_w0) / denom, (1 - alpha * A) / denom},
|
||||
{1.0, (-2 * cos_w0) / denom, (1 - alpha / A) / denom}
|
||||
end
|
||||
|
||||
---Compute low shelf biquad coefficients.
|
||||
---@return table b, table a
|
||||
function M.low_shelf_biquad_coeffs(f0, gain_db, slope, sr)
|
||||
local A = 10 ^ (gain_db / 40)
|
||||
local w0 = 2 * math.pi * f0 / sr
|
||||
local cos_w0 = math.cos(w0)
|
||||
local sin_w0 = math.sin(w0)
|
||||
local alpha = sin_w0 / 2 * math.sqrt((A + 1/A) * (1/slope - 1) + 2)
|
||||
local a0 = (A+1) + (A-1)*cos_w0 + 2*math.sqrt(A)*alpha
|
||||
local b = {
|
||||
A * ((A+1) - (A-1)*cos_w0 + 2*math.sqrt(A)*alpha) / a0,
|
||||
2*A*((A-1) - (A+1)*cos_w0) / a0,
|
||||
A * ((A+1) - (A-1)*cos_w0 - 2*math.sqrt(A)*alpha) / a0,
|
||||
}
|
||||
local a = {
|
||||
1.0,
|
||||
-2 * ((A-1) + (A+1)*cos_w0) / a0,
|
||||
((A+1) + (A-1)*cos_w0 - 2*math.sqrt(A)*alpha) / a0,
|
||||
}
|
||||
return b, a
|
||||
end
|
||||
|
||||
---Apply biquad IIR filter in-place to a single-channel flat buffer [1..L].
|
||||
---Returns updated biquad state {x1,x2,y1,y2}.
|
||||
---@param buf table [1..L] float samples
|
||||
---@param L integer
|
||||
---@param b table {b0,b1,b2}
|
||||
---@param a table {1,a1,a2}
|
||||
---@param state table {x1,x2,y1,y2} (pass {} to initialise fresh)
|
||||
---@return table state
|
||||
function M.biquad_filter_inplace(buf, L, b, a, state)
|
||||
local x1 = state.x1 or 0
|
||||
local x2 = state.x2 or 0
|
||||
local y1 = state.y1 or 0
|
||||
local y2 = state.y2 or 0
|
||||
for i = 1, L do
|
||||
local xn = buf[i]
|
||||
local yn = b[1]*xn + b[2]*x1 + b[3]*x2 - a[2]*y1 - a[3]*y2
|
||||
x2, x1 = x1, xn
|
||||
y2, y1 = y1, yn
|
||||
buf[i] = yn
|
||||
end
|
||||
return {x1=x1, x2=x2, y1=y1, y2=y2}
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- HUM NOTCH CHAIN
|
||||
-- =============================================================================
|
||||
|
||||
---Multi-band hum suppression. Modifies `audio` in place.
|
||||
---@param audio table flat [B, C, L] row-major
|
||||
---@param B integer
|
||||
---@param C integer
|
||||
---@param L integer
|
||||
---@param params table TiledDecodeParams-compatible
|
||||
function M.apply_hum_notch(audio, B, C, L, params)
|
||||
if not params.hum_notch_enabled then return end
|
||||
local sr = params.sample_rate or 48000
|
||||
|
||||
-- Build filter chain
|
||||
local chain = {} -- {b, a} pairs
|
||||
if params.hum_bass_shelf_enabled then
|
||||
local b, a = M.low_shelf_biquad_coeffs(
|
||||
params.hum_bass_shelf_hz or 120.0,
|
||||
params.hum_bass_shelf_db or -2.0,
|
||||
params.hum_bass_shelf_slope or 0.7,
|
||||
sr)
|
||||
table.insert(chain, {b=b, a=a})
|
||||
end
|
||||
local notches = {
|
||||
{en="hum_74_enabled", hz="hum_74_hz", db="hum_74_db", q="hum_74_q"},
|
||||
{en="hum_94_enabled", hz="hum_94_hz", db="hum_94_db", q="hum_94_q"},
|
||||
{en="hum_656_enabled", hz="hum_656_hz", db="hum_656_db", q="hum_656_q"},
|
||||
}
|
||||
for _, n in ipairs(notches) do
|
||||
if params[n.en] then
|
||||
local b, a = M.peaking_biquad_coeffs(
|
||||
params[n.hz], params[n.db], params[n.q], sr)
|
||||
table.insert(chain, {b=b, a=a})
|
||||
end
|
||||
end
|
||||
|
||||
if #chain == 0 then return end
|
||||
|
||||
-- Apply each band to each channel independently
|
||||
for b = 1, B do
|
||||
for c = 1, C do
|
||||
-- Extract channel slice into temp buffer
|
||||
local ch = {}
|
||||
local base = idx(b, c, 1, C, L)
|
||||
for i = 1, L do ch[i] = audio[base + i - 1] end
|
||||
-- Filter chain
|
||||
for _, filt in ipairs(chain) do
|
||||
M.biquad_filter_inplace(ch, L, filt.b, filt.a, {})
|
||||
end
|
||||
-- Write back
|
||||
for i = 1, L do audio[base + i - 1] = ch[i] end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- HIGH-PASS FILTER
|
||||
-- =============================================================================
|
||||
|
||||
---Butterworth 2nd-order high-pass filter (biquad). In-place.
|
||||
---@param audio table flat [B, C, L]
|
||||
---@param B integer
|
||||
---@param C integer
|
||||
---@param L integer
|
||||
---@param cutoff_hz number
|
||||
---@param sample_rate number
|
||||
function M.apply_highpass(audio, B, C, L, cutoff_hz, sample_rate)
|
||||
if cutoff_hz < 1.0 then return end
|
||||
local w0 = 2 * math.pi * cutoff_hz / sample_rate
|
||||
local cos_w0 = math.cos(w0)
|
||||
local sin_w0 = math.sin(w0)
|
||||
local alpha = sin_w0 / (2 * 0.7071) -- Q = 1/sqrt(2) Butterworth
|
||||
local denom = 1 + alpha
|
||||
local b = {
|
||||
(1 + cos_w0) / (2 * denom),
|
||||
-(1 + cos_w0) / denom,
|
||||
(1 + cos_w0) / (2 * denom),
|
||||
}
|
||||
local a = {
|
||||
1.0,
|
||||
(-2 * cos_w0) / denom,
|
||||
(1 - alpha) / denom,
|
||||
}
|
||||
|
||||
for bi = 1, B do
|
||||
for c = 1, C do
|
||||
local base = idx(bi, c, 1, C, L)
|
||||
local ch = {}
|
||||
for i = 1, L do ch[i] = audio[base + i - 1] end
|
||||
M.biquad_filter_inplace(ch, L, b, a, {})
|
||||
for i = 1, L do audio[base + i - 1] = ch[i] end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- SOFT CLIPPER
|
||||
-- =============================================================================
|
||||
|
||||
---tanh-based soft clipper. In-place.
|
||||
---@param audio table flat [B, C, L]
|
||||
---@param ceiling_db number e.g. -3.0 (must be < 0 to have effect)
|
||||
function M.apply_soft_clip(audio, ceiling_db)
|
||||
if ceiling_db >= 0 then return end
|
||||
local ceiling_lin = 10 ^ (ceiling_db / 20)
|
||||
local scale = ceiling_lin / tanh(1.0)
|
||||
for i = 1, #audio do
|
||||
audio[i] = scale * tanh(audio[i] / scale)
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- PEAK NORMALIZE
|
||||
-- =============================================================================
|
||||
|
||||
---Transparent peak normalization: scale entire audio so max |sample| = target.
|
||||
---Pure gain reduction — no waveform distortion, no waveshaping.
|
||||
---Only attenuates (never boosts). Skipped if peak is already below target.
|
||||
---@param audio table flat buffer
|
||||
---@param N integer total samples
|
||||
---@param target_db number target peak in dBFS (e.g., -1.0)
|
||||
function M.apply_peak_normalize(audio, N, target_db)
|
||||
if not target_db or target_db >= 0 then return end
|
||||
local target_lin = 10 ^ (target_db / 20)
|
||||
local peak = 0.0
|
||||
for i = 1, N do
|
||||
local v = math.abs(audio[i])
|
||||
if v > peak then peak = v end
|
||||
end
|
||||
if peak < 1e-8 then return end -- silence
|
||||
if peak <= target_lin then return end -- already below target
|
||||
local gain = target_lin / peak
|
||||
for i = 1, N do
|
||||
audio[i] = audio[i] * gain
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- STEREO WIDTH (M/S)
|
||||
-- =============================================================================
|
||||
|
||||
---M/S stereo width. Only operates when C == 2. In-place.
|
||||
---@param audio table flat [B, 2, L]
|
||||
---@param B integer
|
||||
---@param L integer
|
||||
---@param width number 1.0=unity, 0.0=mono, 2.0=doubled side
|
||||
function M.apply_stereo_width(audio, B, L, width)
|
||||
if math.abs(width - 1.0) < 1e-4 then return end
|
||||
for b = 1, B do
|
||||
local base_l = idx(b, 1, 1, 2, L)
|
||||
local base_r = idx(b, 2, 1, 2, L)
|
||||
for i = 0, L - 1 do
|
||||
local l = audio[base_l + i]
|
||||
local r = audio[base_r + i]
|
||||
local mid = (l + r) * 0.5
|
||||
local side = (l - r) * 0.5 * width
|
||||
audio[base_l + i] = mid + side
|
||||
audio[base_r + i] = mid - side
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- RMS UTILITIES
|
||||
-- =============================================================================
|
||||
|
||||
---Compute RMS of flat buffer.
|
||||
function M.compute_rms(buf, N)
|
||||
local s = 0.0
|
||||
for i = 1, N do local v = buf[i]; s = s + v*v end
|
||||
return math.sqrt(s / N + M.EPSILON)
|
||||
end
|
||||
|
||||
---Downward-only absolute RMS ceiling. In-place.
|
||||
---@param audio table flat buffer
|
||||
---@param ceiling number default M.RMS_ABS_CEIL
|
||||
function M.apply_rms_ceiling(audio, N, ceiling)
|
||||
ceiling = ceiling or M.RMS_ABS_CEIL
|
||||
local rms = M.compute_rms(audio, N)
|
||||
if rms > ceiling then
|
||||
local g = ceiling / rms
|
||||
for i = 1, N do audio[i] = audio[i] * g end
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- OLA WRITE PRIMITIVE
|
||||
-- =============================================================================
|
||||
|
||||
---Write decoded_chunk into output_audio at out_start with OLA crossfade.
|
||||
---All buffers flat [B, C, length] row-major (1-indexed).
|
||||
---@param output_audio table flat [B, C, final_samples]
|
||||
---@param B integer
|
||||
---@param C integer
|
||||
---@param final_samples integer
|
||||
---@param decoded_chunk table flat [B, C, decoded_len]
|
||||
---@param decoded_len integer
|
||||
---@param out_start integer 0-indexed write position in audio samples
|
||||
---@param overlap_audio integer audio-domain overlap samples
|
||||
---@param blend_mode string "Hann"|"Cosine"|"Linear"
|
||||
function M.ola_write(output_audio, B, C, final_samples,
|
||||
decoded_chunk, decoded_len,
|
||||
out_start, overlap_audio, blend_mode)
|
||||
local valid_len = math.min(decoded_len, final_samples - out_start)
|
||||
if valid_len <= 0 then return end
|
||||
local ov = math.min(overlap_audio, math.floor(valid_len / 2))
|
||||
|
||||
if out_start > 0 and ov > 0 then
|
||||
local fade_in = M.make_fade_in(ov, blend_mode)
|
||||
-- Cosine equal-power fade-out: sqrt(1 - f^2)
|
||||
-- Linear/Hann: simple 1-f
|
||||
local use_eqp = (blend_mode == "Cosine")
|
||||
|
||||
for b = 1, B do
|
||||
for c = 1, C do
|
||||
local out_base = idx(b, c, 1, C, final_samples)
|
||||
local in_base = idx(b, c, 1, C, decoded_len)
|
||||
|
||||
-- Crossfade
|
||||
for i = 1, ov do
|
||||
local fi = fade_in[i]
|
||||
local fo = use_eqp
|
||||
and math.sqrt(math.max(0, 1 - fi * fi))
|
||||
or (1 - fi)
|
||||
local out_i = out_start + i -- 1-indexed position in output
|
||||
if out_i >= 1 and out_i <= final_samples then
|
||||
output_audio[out_base + out_i - 1] =
|
||||
output_audio[out_base + out_i - 1] * fo +
|
||||
decoded_chunk[in_base + i - 1] * fi
|
||||
end
|
||||
end
|
||||
|
||||
-- Tail (straight copy after crossfade)
|
||||
for i = ov + 1, valid_len do
|
||||
local out_i = out_start + i
|
||||
if out_i >= 1 and out_i <= final_samples then
|
||||
output_audio[out_base + out_i - 1] =
|
||||
decoded_chunk[in_base + i - 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
-- First tile — straight write
|
||||
for b = 1, B do
|
||||
for c = 1, C do
|
||||
local out_base = idx(b, c, 1, C, final_samples)
|
||||
local in_base = idx(b, c, 1, C, decoded_len)
|
||||
for i = 1, valid_len do
|
||||
output_audio[out_base + out_start + i - 1] =
|
||||
decoded_chunk[in_base + i - 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- DUAL-PASS WEIGHT ACCUMULATION
|
||||
-- =============================================================================
|
||||
|
||||
---Build trapezoidal weight map for one pass's tile boundaries.
|
||||
---@param weight_buf table [1..final_samples] float, modified in place
|
||||
---@param final_samples integer
|
||||
---@param schedule table[] list of {start, end_, overlap}
|
||||
---@param boundaries table list of out_start integers (0-indexed audio positions)
|
||||
---@param upscale_factor number
|
||||
function M.fill_trapezoid_weights(weight_buf, final_samples,
|
||||
schedule, boundaries, upscale_factor)
|
||||
for i, bound in ipairs(boundaries) do
|
||||
local tile = schedule[i]
|
||||
if not tile then break end
|
||||
local lat_len = tile.end_ - tile.start
|
||||
local tile_len = math.floor(lat_len * upscale_factor + 0.5)
|
||||
local out_s = bound + 1 -- convert to 1-indexed
|
||||
local out_e = math.min(final_samples, out_s + tile_len - 1)
|
||||
local L = out_e - out_s + 1
|
||||
if L <= 0 then goto continue end
|
||||
|
||||
local trap = M.make_trapezoid(L, M.DUAL_PASS_TAPER)
|
||||
for j = 1, L do
|
||||
local pos = out_s + j - 1
|
||||
if pos >= 1 and pos <= final_samples then
|
||||
if trap[j] > weight_buf[pos] then weight_buf[pos] = trap[j] end
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- UPSCALE FACTOR SNAP
|
||||
-- =============================================================================
|
||||
|
||||
---Snap upscale factor to nearest integer if within 0.5%.
|
||||
---ACE-Step Oobleck always produces an exact integer ratio.
|
||||
---Sub-sample error compounds across tiles → audible timing drift.
|
||||
---@param raw number
|
||||
---@return number
|
||||
function M.snap_upscale_factor(raw)
|
||||
local snapped = math.floor(raw + 0.5)
|
||||
if math.abs(snapped - raw) / (raw + M.EPSILON) < 0.005 then
|
||||
return snapped
|
||||
end
|
||||
return raw
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- MASTER TILED DECODE ORCHESTRATOR
|
||||
-- =============================================================================
|
||||
|
||||
---Master tiled VAE decode engine.
|
||||
---
|
||||
---@param vae_decode_fn function(latent_slice, lat_len) -> audio_chunk, actual_len
|
||||
--- latent_slice: flat [B, C_lat, lat_len] table
|
||||
--- Returns: flat [B, C_aud, actual_len] table, integer actual_len
|
||||
---
|
||||
---@param latents table flat [B, C_lat, W] latent buffer (may be modified by LSS)
|
||||
---@param B integer batch size
|
||||
---@param C_lat integer latent channels
|
||||
---@param W integer latent frame count
|
||||
---@param C_aud integer audio channels (typically 2)
|
||||
---@param final_samples integer total output audio samples
|
||||
---@param upscale_factor number audio samples per latent frame
|
||||
---@param params table TiledDecodeParams-compatible:
|
||||
--- tile_size, overlap, context_prefix, dual_pass, rms_leveling,
|
||||
--- lss_enabled, lss_strength, lss_var_thresh, lss_dc_remove,
|
||||
--- hum_notch_enabled (+ per-band params), highpass_hz, soft_clip_db,
|
||||
--- stereo_width, sample_rate
|
||||
---
|
||||
---@return table flat [B, C_aud, final_samples] decoded audio
|
||||
function M.execute_tiled_decode(vae_decode_fn, latents, B, C_lat, W,
|
||||
C_aud, final_samples, upscale_factor, params)
|
||||
params = params or {}
|
||||
local tile_size = params.tile_size or 1024
|
||||
local overlap = params.overlap or 64
|
||||
local context_prefix = params.context_prefix or 512
|
||||
local dual_pass = params.dual_pass ~= false -- default true
|
||||
local rms_leveling = params.rms_leveling ~= false -- default true
|
||||
local sample_rate = params.sample_rate or 48000
|
||||
|
||||
-- ── LSS ──────────────────────────────────────────────────────────────────
|
||||
if params.lss_enabled ~= false then
|
||||
M.apply_lss(latents, B, C_lat, W,
|
||||
params.lss_strength or 0.25,
|
||||
params.lss_var_thresh or 0.12,
|
||||
params.lss_dc_remove or false)
|
||||
end
|
||||
|
||||
-- ── Tile schedules ────────────────────────────────────────────────────────
|
||||
local sched_a = M.build_fixed_schedule(W, tile_size, overlap, 0)
|
||||
local sched_b = dual_pass
|
||||
and M.build_fixed_schedule(W, tile_size, overlap,
|
||||
math.floor(tile_size / 2))
|
||||
or nil
|
||||
|
||||
-- ── Run one pass ──────────────────────────────────────────────────────────
|
||||
local function run_pass(schedule)
|
||||
local audio_out = zeros(B * C_aud * final_samples)
|
||||
local boundaries = {}
|
||||
local prev_rms = -1.0
|
||||
|
||||
for _, tile in ipairs(schedule) do
|
||||
local ctx_start = math.max(0, tile.start - context_prefix)
|
||||
local lat_len = tile.end_ - ctx_start
|
||||
|
||||
-- Extract latent slice
|
||||
local lat_slice = zeros(B * C_lat * lat_len)
|
||||
for b = 1, B do
|
||||
for c = 1, C_lat do
|
||||
local src_base = (b-1)*C_lat*W + (c-1)*W + ctx_start + 1
|
||||
local dst_base = (b-1)*C_lat*lat_len + (c-1)*lat_len + 1
|
||||
for t = 1, lat_len do
|
||||
lat_slice[dst_base + t - 1] = latents[src_base + t - 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- VAE decode
|
||||
local chunk, actual_len = vae_decode_fn(lat_slice, lat_len)
|
||||
if not chunk or actual_len <= 0 then goto next_tile end
|
||||
|
||||
-- ctx_skip: absorb VAE rounding into discarded context region
|
||||
local expected_write = math.floor((tile.end_ - tile.start) * upscale_factor + 0.5)
|
||||
local ctx_skip = math.max(0, actual_len - expected_write)
|
||||
local write_len = actual_len - ctx_skip
|
||||
if write_len <= 0 then goto next_tile end
|
||||
|
||||
-- Build write chunk (post ctx_skip)
|
||||
local write_chunk = zeros(B * C_aud * write_len)
|
||||
for b = 1, B do
|
||||
for c = 1, C_aud do
|
||||
local src_base = (b-1)*C_aud*actual_len + (c-1)*actual_len + ctx_skip + 1
|
||||
local dst_base = (b-1)*C_aud*write_len + (c-1)*write_len + 1
|
||||
for i = 1, write_len do
|
||||
write_chunk[dst_base + i - 1] = chunk[src_base + i - 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- RMS leveling
|
||||
if rms_leveling and prev_rms > 0 then
|
||||
local N = B * C_aud * write_len
|
||||
local crms = M.compute_rms(write_chunk, N)
|
||||
local gain = clamp(prev_rms / (crms + M.EPSILON),
|
||||
1 - M.GAIN_CLAMP_BASE, 1 + M.GAIN_CLAMP_BASE)
|
||||
for i = 1, N do write_chunk[i] = write_chunk[i] * gain end
|
||||
end
|
||||
|
||||
-- Absolute RMS ceiling
|
||||
M.apply_rms_ceiling(write_chunk, B * C_aud * write_len)
|
||||
|
||||
prev_rms = M.compute_rms(write_chunk, B * C_aud * write_len)
|
||||
|
||||
local out_start = math.floor(tile.start * upscale_factor + 0.5)
|
||||
local overlap_aud = math.floor(tile.overlap * upscale_factor + 0.5)
|
||||
table.insert(boundaries, out_start)
|
||||
|
||||
M.ola_write(audio_out, B, C_aud, final_samples,
|
||||
write_chunk, write_len,
|
||||
out_start, overlap_aud, "Cosine")
|
||||
|
||||
::next_tile::
|
||||
end
|
||||
|
||||
return audio_out, boundaries
|
||||
end
|
||||
|
||||
local audio_a, bounds_a = run_pass(sched_a)
|
||||
local audio_b, bounds_b = nil, {}
|
||||
if sched_b then
|
||||
audio_b, bounds_b = run_pass(sched_b)
|
||||
end
|
||||
|
||||
-- ── Dual-Pass Merge ───────────────────────────────────────────────────────
|
||||
local output_audio
|
||||
if dual_pass and audio_b then
|
||||
local weight_a = zeros(final_samples)
|
||||
local weight_b = zeros(final_samples)
|
||||
M.fill_trapezoid_weights(weight_a, final_samples, sched_a, bounds_a, upscale_factor)
|
||||
M.fill_trapezoid_weights(weight_b, final_samples, sched_b, bounds_b, upscale_factor)
|
||||
|
||||
output_audio = zeros(B * C_aud * final_samples)
|
||||
for b = 1, B do
|
||||
for c = 1, C_aud do
|
||||
local base = (b-1)*C_aud*final_samples + (c-1)*final_samples
|
||||
for i = 1, final_samples do
|
||||
local wa = weight_a[i]
|
||||
local wb = weight_b[i]
|
||||
local total = wa + wb + M.EPSILON
|
||||
output_audio[base + i] = audio_a[base + i] * (wa / total)
|
||||
+ audio_b[base + i] * (wb / total)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
output_audio = audio_a
|
||||
end
|
||||
|
||||
-- ── Post-Decode DSP Chain ─────────────────────────────────────────────────
|
||||
-- Order: hum notch → highpass → stereo width → soft clip
|
||||
M.apply_hum_notch(output_audio, B, C_aud, final_samples, params)
|
||||
M.apply_highpass(output_audio, B, C_aud, final_samples,
|
||||
params.highpass_hz or 20.0, sample_rate)
|
||||
if C_aud == 2 then
|
||||
M.apply_stereo_width(output_audio, B, final_samples,
|
||||
params.stereo_width or 0.8)
|
||||
end
|
||||
M.apply_peak_normalize(output_audio, B * C_aud * final_samples,
|
||||
params.peak_normalize_db) -- nil = skip
|
||||
M.apply_soft_clip(output_audio, params.soft_clip_db or -3.0)
|
||||
|
||||
return output_audio
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- DEFAULT PARAMS
|
||||
-- =============================================================================
|
||||
|
||||
---Gold-standard default parameters. Copy and override as needed.
|
||||
M.DEFAULT_PARAMS = {
|
||||
-- Tiling
|
||||
tile_size = 1024,
|
||||
overlap = 64,
|
||||
context_prefix = 512,
|
||||
dual_pass = true,
|
||||
rms_leveling = true,
|
||||
sample_rate = 48000,
|
||||
|
||||
-- LSS
|
||||
lss_enabled = true,
|
||||
lss_strength = 0.25,
|
||||
lss_var_thresh = 0.12,
|
||||
lss_dc_remove = false, -- WARNING: metallic distortion if true
|
||||
|
||||
-- DSP chain
|
||||
highpass_hz = 20.0,
|
||||
peak_normalize_db = nil, -- nil = disabled; e.g. -1.0 for -1dBFS peak target
|
||||
soft_clip_db = -3.0,
|
||||
stereo_width = 0.8,
|
||||
|
||||
-- Hum notch (gold standard: cuts only)
|
||||
hum_notch_enabled = true,
|
||||
hum_bass_shelf_enabled = true,
|
||||
hum_bass_shelf_hz = 120.0,
|
||||
hum_bass_shelf_db = -2.0,
|
||||
hum_bass_shelf_slope = 0.7,
|
||||
hum_74_enabled = true,
|
||||
hum_74_hz = 74.4,
|
||||
hum_74_db = -1.43,
|
||||
hum_74_q = 6.27,
|
||||
hum_94_enabled = true,
|
||||
hum_94_hz = 94.0,
|
||||
hum_94_db = -1.86,
|
||||
hum_94_q = 7.08,
|
||||
hum_656_enabled = true,
|
||||
hum_656_hz = 654.0,
|
||||
hum_656_db = -15.0,
|
||||
hum_656_q = 6.0,
|
||||
}
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,439 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Causal Scheduler v2.1 — LINA Time Warping + Multi-Mode Base Curves
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- Port of md_causal_scheduler_core.py to HOT-Step-CPP Lua.
|
||||
--
|
||||
-- 14 BASE SCHEDULE MODES:
|
||||
-- karras — power-law rho spacing (rho=7 default, Karras et al.)
|
||||
-- simple — smoothstep (cubic hermite: t*t*(3-2t))
|
||||
-- linear — uniform spacing
|
||||
-- exponential — exp decay: sigma_max * (sigma_min/sigma_max)^t
|
||||
-- polynomial — power curve: linspace of sigma^(1/power)
|
||||
-- beta — beta distribution curve (alpha, beta params)
|
||||
-- ays — AYS adaptive schedule (sigmoid + concentration blend)
|
||||
-- bong — tangent-based 2-phase schedule (pivot point)
|
||||
-- linear_quadratic — linear phase then quadratic phase
|
||||
-- ddim_uniform — DDIM-style uniform timestep mapping
|
||||
-- sgm_uniform — SGM uniform (linear 999→0 mapped to sigma range)
|
||||
-- blended — karras + linear blend by blend_factor
|
||||
-- variance_preserving — log-space interpolation
|
||||
-- kl_optimal — arctan-based KL-optimal spacing
|
||||
--
|
||||
-- LINA WARP:
|
||||
-- Post-processes any base schedule by warping the time axis:
|
||||
-- t_warped = t^shift (power warp on the CDF index)
|
||||
-- shift < 1: front-loads steps (more at high sigma)
|
||||
-- shift > 1: back-loads steps (more at low sigma)
|
||||
-- shift = 1: no warp (identity)
|
||||
-- ============================================================================
|
||||
|
||||
scheduler = {
|
||||
name = "md_causal",
|
||||
display = "MD Causal (LINA + 14 Modes)",
|
||||
description = "14 base schedule modes with LINA time-axis warp. Karras, smoothstep, beta, AYS, bong, DDIM, SGM, blended, variance-preserving, KL-optimal and more. Port of md_causal_scheduler_core v2.1.",
|
||||
params = {
|
||||
{
|
||||
key = "mode",
|
||||
type = "select",
|
||||
label = "Schedule Mode",
|
||||
default = "polynomial",
|
||||
options = {
|
||||
{ value = "karras", label = "Karras (rho)" },
|
||||
{ value = "simple", label = "Simple (Smoothstep)" },
|
||||
{ value = "linear", label = "Linear" },
|
||||
{ value = "exponential", label = "Exponential" },
|
||||
{ value = "polynomial", label = "Polynomial" },
|
||||
{ value = "beta", label = "Beta" },
|
||||
{ value = "ays", label = "AYS" },
|
||||
{ value = "bong", label = "Bong (Tangent)" },
|
||||
{ value = "linear_quadratic", label = "Linear-Quadratic" },
|
||||
{ value = "ddim_uniform", label = "DDIM Uniform" },
|
||||
{ value = "sgm_uniform", label = "SGM Uniform" },
|
||||
{ value = "blended", label = "Blended (Karras+Lin)" },
|
||||
{ value = "variance_preserving", label = "Variance Preserving" },
|
||||
{ value = "kl_optimal", label = "KL Optimal" },
|
||||
},
|
||||
hint = "Base schedule curve before LINA warp is applied.",
|
||||
},
|
||||
{
|
||||
key = "lina_shift",
|
||||
type = "slider",
|
||||
label = "LINA Shift",
|
||||
default = 1.2,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.05,
|
||||
hint = "Time-axis warp. 1.0=none. <1=front-load (more high-sigma steps). >1=back-load (more low-sigma steps).",
|
||||
},
|
||||
{
|
||||
key = "rho",
|
||||
type = "slider",
|
||||
label = "Rho (Karras)",
|
||||
default = 7.0,
|
||||
min = 1.0,
|
||||
max = 15.0,
|
||||
step = 0.5,
|
||||
hint = "Karras rho parameter. 7=default. Higher=more steps at low sigma.",
|
||||
visible_when = { key = "mode", equals = "karras" },
|
||||
},
|
||||
{
|
||||
key = "power",
|
||||
type = "slider",
|
||||
label = "Power (Polynomial)",
|
||||
default = 2.0,
|
||||
min = 0.5,
|
||||
max = 5.0,
|
||||
step = 0.1,
|
||||
hint = "Polynomial exponent. 2=quadratic, 1=linear.",
|
||||
visible_when = { key = "mode", equals = "polynomial" },
|
||||
},
|
||||
{
|
||||
key = "beta_alpha",
|
||||
type = "slider",
|
||||
label = "Beta Alpha",
|
||||
default = 0.6,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Beta distribution alpha parameter.",
|
||||
visible_when = { key = "mode", equals = "beta" },
|
||||
},
|
||||
{
|
||||
key = "beta_beta",
|
||||
type = "slider",
|
||||
label = "Beta Beta",
|
||||
default = 0.6,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Beta distribution beta parameter.",
|
||||
visible_when = { key = "mode", equals = "beta" },
|
||||
},
|
||||
{
|
||||
key = "blend_factor",
|
||||
type = "slider",
|
||||
label = "Blend Factor",
|
||||
default = 0.5,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Blend between Karras (0) and Linear (1).",
|
||||
visible_when = { key = "mode", equals = "blended" },
|
||||
},
|
||||
{
|
||||
key = "bong_pivot",
|
||||
type = "slider",
|
||||
label = "Bong Pivot",
|
||||
default = 0.5,
|
||||
min = 0.1,
|
||||
max = 0.9,
|
||||
step = 0.05,
|
||||
hint = "Bong: fraction of steps in compression phase.",
|
||||
visible_when = { key = "mode", equals = "bong" },
|
||||
},
|
||||
{
|
||||
key = "bong_slope_comp",
|
||||
type = "slider",
|
||||
label = "Bong Slope Comp",
|
||||
default = 1.2,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Bong: tangent slope in compression phase.",
|
||||
visible_when = { key = "mode", equals = "bong" },
|
||||
},
|
||||
{
|
||||
key = "bong_slope_detail",
|
||||
type = "slider",
|
||||
label = "Bong Slope Detail",
|
||||
default = 0.8,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Bong: tangent slope in detail phase.",
|
||||
visible_when = { key = "mode", equals = "bong" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local EPSILON = 1e-6
|
||||
local MONOTONIC_DECAY = 0.99
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
-- ── Base schedule generators ─────────────────────────────────────────────────
|
||||
|
||||
local function karras(n, s_min, s_max, rho)
|
||||
-- sigma[i] = (s_max^(1/rho) + i/(n-1) * (s_min^(1/rho) - s_max^(1/rho)))^rho
|
||||
local inv_rho = 1.0 / rho
|
||||
local max_inv = s_max ^ inv_rho
|
||||
local min_inv = s_min ^ inv_rho
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
s[i] = (max_inv + t * (min_inv - max_inv)) ^ rho
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function simple(n, s_min, s_max)
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
local smooth = t * t * (3.0 - 2.0 * t)
|
||||
s[i] = s_max - (s_max - s_min) * smooth
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function linear(n, s_min, s_max)
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
s[i] = s_max - (s_max - s_min) * (i / n)
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function exponential(n, s_min, s_max)
|
||||
local s = {}
|
||||
local safe_max = math.max(s_max, 1e-9)
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
s[i] = safe_max * (s_min / safe_max) ^ t
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function polynomial(n, s_min, s_max, power)
|
||||
local s = {}
|
||||
local inv_p = 1.0 / math.max(power, 0.1)
|
||||
local lo = s_min ^ inv_p
|
||||
local hi = s_max ^ inv_p
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
s[i] = (hi + t * (lo - hi)) ^ power
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function beta_sched(n, s_min, s_max, alpha, beta_)
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
local alpha_ = math.max(alpha, 0.1)
|
||||
local beta__ = math.max(beta_, 0.1)
|
||||
local beta_curve = clamp(1.0 - (1.0 - t ^ alpha_) ^ beta__, 0.0, 1.0)
|
||||
s[i] = s_max * (1.0 - beta_curve) + s_min * beta_curve
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function ays_sched(n, s_min, s_max)
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
-- sigmoid centered at 0.5, steepness 10
|
||||
local sig = 1.0 / (1.0 + math.exp(-10.0 * (t - 0.5)))
|
||||
-- AYS blend: sigmoid 0.7 + concentration (exp decay) 0.3
|
||||
local conc = math.exp(-2.0 * t)
|
||||
local ays = sig * 0.7 + conc * 0.3
|
||||
-- normalize and invert: high sigma at start
|
||||
s[i] = s_min + (s_max - s_min) * (1.0 - ays)
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function bong_sched(n, s_min, s_max, pivot, slope_comp, slope_det)
|
||||
local comp_steps = math.max(1, math.floor(n * pivot))
|
||||
local det_steps = math.max(1, n - comp_steps)
|
||||
|
||||
local sigmas = {}
|
||||
local pi_half = math.pi / 2.0 - 0.1
|
||||
|
||||
-- Compression phase
|
||||
for i = 0, comp_steps - 1 do
|
||||
local t = i / math.max(comp_steps - 1, 1)
|
||||
local angle = t * pi_half * slope_comp
|
||||
local warped = math.tan(angle) / math.tan(pi_half * slope_comp)
|
||||
sigmas[i] = s_max * (1.0 - warped * pivot)
|
||||
end
|
||||
|
||||
-- Detail phase
|
||||
for i = 0, det_steps - 1 do
|
||||
local t = i / math.max(det_steps - 1, 1)
|
||||
local angle = t * pi_half * slope_det
|
||||
local warped = math.tan(angle) / math.tan(pi_half * slope_det)
|
||||
local start = s_max * (1.0 - pivot)
|
||||
sigmas[comp_steps + i] = start * (1.0 - warped) + s_min * warped
|
||||
end
|
||||
|
||||
sigmas[n] = s_min
|
||||
|
||||
-- Enforce monotonic
|
||||
for i = 0, n - 1 do
|
||||
if sigmas[i] ~= nil and sigmas[i + 1] ~= nil then
|
||||
if sigmas[i] <= sigmas[i + 1] then
|
||||
sigmas[i + 1] = math.max(sigmas[i] * MONOTONIC_DECAY, sigmas[i] - EPSILON)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sigmas[0] = s_max; sigmas[n] = s_min
|
||||
return sigmas
|
||||
end
|
||||
|
||||
local function ddim_uniform(n, s_min, s_max)
|
||||
local max_ts = 1000
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
local ts = max_ts - i * (max_ts / n)
|
||||
s[i] = s_min + (s_max - s_min) * ((ts / max_ts) ^ 0.5)
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function sgm_uniform(n, s_min, s_max)
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
s[i] = s_min + (s_max - s_min) * (1.0 - t)
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function blended(n, s_min, s_max, rho, blend)
|
||||
local k = karras(n, s_min, s_max, rho)
|
||||
local l = linear(n, s_min, s_max)
|
||||
local s = {}
|
||||
for i = 0, n do
|
||||
s[i] = (1.0 - blend) * k[i] + blend * l[i]
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function variance_preserving(n, s_min, s_max)
|
||||
local s = {}
|
||||
local log_min = math.log(math.max(s_min, 1e-9))
|
||||
local log_max = math.log(math.max(s_max, 1e-9))
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
s[i] = math.exp((1.0 - t) * log_max + t * log_min)
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
local function kl_optimal(n, s_min, s_max)
|
||||
local s = {}
|
||||
local atan_min = math.atan(s_min)
|
||||
local atan_max = math.atan(s_max)
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
s[i] = math.tan((1.0 - t) * atan_max + t * atan_min)
|
||||
end
|
||||
s[0] = s_max; s[n] = s_min
|
||||
return s
|
||||
end
|
||||
|
||||
-- ── LINA warp ─────────────────────────────────────────────────────────────────
|
||||
|
||||
local function apply_lina_warp(sigmas, n, shift)
|
||||
if shift == 1.0 then return sigmas end
|
||||
local warped = {}
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
-- Warp: t_warped = t^shift → index into sigma array
|
||||
local t_w = t ^ shift
|
||||
local raw_idx = t_w * n
|
||||
local idx_lo = clamp(math.floor(raw_idx), 0, n)
|
||||
local idx_hi = clamp(idx_lo + 1, 0, n)
|
||||
local frac = raw_idx - idx_lo
|
||||
local s_lo = sigmas[idx_lo] or sigmas[n]
|
||||
local s_hi = sigmas[idx_hi] or sigmas[n]
|
||||
warped[i] = s_lo * (1.0 - frac) + s_hi * frac
|
||||
end
|
||||
warped[0] = sigmas[0]
|
||||
warped[n] = sigmas[n]
|
||||
return warped
|
||||
end
|
||||
|
||||
-- ── Required schedule() function ─────────────────────────────────────────────
|
||||
|
||||
function schedule(output, num_steps, shift)
|
||||
local mode = (params and params.mode) or "karras"
|
||||
local lina_shift = (params and params.lina_shift) or 1.0
|
||||
local rho = (params and params.rho) or 7.0
|
||||
local power = (params and params.power) or 2.0
|
||||
local ba = (params and params.beta_alpha) or 0.6
|
||||
local bb = (params and params.beta_beta) or 0.6
|
||||
local blend = (params and params.blend_factor) or 0.5
|
||||
local b_pivot = (params and params.bong_pivot) or 0.5
|
||||
local b_comp = (params and params.bong_slope_comp) or 1.2
|
||||
local b_det = (params and params.bong_slope_detail) or 0.8
|
||||
|
||||
local s_max = 1.0
|
||||
local s_min = 0.0
|
||||
|
||||
local sigmas
|
||||
if mode == "karras" then sigmas = karras(num_steps, s_min, s_max, rho)
|
||||
elseif mode == "simple" then sigmas = simple(num_steps, s_min, s_max)
|
||||
elseif mode == "linear" then sigmas = linear(num_steps, s_min, s_max)
|
||||
elseif mode == "exponential" then sigmas = exponential(num_steps, s_min, s_max)
|
||||
elseif mode == "polynomial" then sigmas = polynomial(num_steps, s_min, s_max, power)
|
||||
elseif mode == "beta" then sigmas = beta_sched(num_steps, s_min, s_max, ba, bb)
|
||||
elseif mode == "ays" then sigmas = ays_sched(num_steps, s_min, s_max)
|
||||
elseif mode == "bong" then sigmas = bong_sched(num_steps, s_min, s_max, b_pivot, b_comp, b_det)
|
||||
elseif mode == "ddim_uniform" then sigmas = ddim_uniform(num_steps, s_min, s_max)
|
||||
elseif mode == "sgm_uniform" then sigmas = sgm_uniform(num_steps, s_min, s_max)
|
||||
elseif mode == "blended" then sigmas = blended(num_steps, s_min, s_max, rho, blend)
|
||||
elseif mode == "variance_preserving" then sigmas = variance_preserving(num_steps, s_min, s_max)
|
||||
elseif mode == "kl_optimal" then sigmas = kl_optimal(num_steps, s_min, s_max)
|
||||
else sigmas = karras(num_steps, s_min, s_max, rho)
|
||||
end
|
||||
|
||||
-- LINA warp
|
||||
if lina_shift ~= 1.0 then
|
||||
sigmas = apply_lina_warp(sigmas, num_steps, lina_shift)
|
||||
end
|
||||
|
||||
-- Native shift warp
|
||||
if shift ~= 1.0 then
|
||||
for i = 0, num_steps do
|
||||
local t = sigmas[i]
|
||||
sigmas[i] = shift * t / (1.0 + (shift - 1.0) * t)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 0, num_steps - 1 do
|
||||
output[i] = sigmas[i] or 1.0 - i / num_steps
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
-- MD HAP Scheduler v1.0 — Hamiltonian Action-Principle
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- Port of hap_scheduler_core.py calculate_hap_sigmas() to HOT-Step-CPP Lua.
|
||||
--
|
||||
-- WHAT THIS DOES:
|
||||
-- Simulates a particle falling through a gravitational potential well with
|
||||
-- atmospheric drag. Maps the particle's velocity to sigma step sizes.
|
||||
--
|
||||
-- velocity(t) = (1 + kinetic_energy * t) * exp(-damping_friction * t)
|
||||
--
|
||||
-- - kinetic_energy: initial boost — stretches steps in the middle of the run
|
||||
-- (particle accelerates as it falls into the well)
|
||||
-- - damping_friction: atmospheric drag — compresses steps at the end
|
||||
-- (particle slows as drag increases with velocity)
|
||||
--
|
||||
-- distance = cumsum(velocity) → normalize → map to sigma space
|
||||
--
|
||||
-- HIGH kinetic_energy: more steps in the mid-sigma zone (structure formation)
|
||||
-- HIGH damping_friction: more steps compressed toward the end (detail refinement)
|
||||
--
|
||||
-- This is the HAP component of the HT scheduler (used standalone here).
|
||||
-- ============================================================================
|
||||
|
||||
scheduler = {
|
||||
name = "md_hap",
|
||||
display = "MD HAP (Hamiltonian Potential Well)",
|
||||
description = "Particle-in-potential-well sigma schedule. Kinetic energy stretches mid steps, damping friction compresses end steps. Port of hap_scheduler_core v1.0.",
|
||||
params = {
|
||||
{
|
||||
key = "kinetic_energy",
|
||||
type = "slider",
|
||||
label = "Kinetic Energy",
|
||||
default = 1.0,
|
||||
min = 0.0,
|
||||
max = 5.0,
|
||||
step = 0.1,
|
||||
hint = "Initial velocity boost. Stretches steps in the middle of the trajectory (structure formation zone).",
|
||||
},
|
||||
{
|
||||
key = "damping_friction",
|
||||
type = "slider",
|
||||
label = "Damping Friction",
|
||||
default = 0.5,
|
||||
min = 0.0,
|
||||
max = 8.0,
|
||||
step = 0.1,
|
||||
hint = "Atmospheric drag. Compresses steps toward the end (detail refinement zone). Higher=more end compression.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local EPSILON = 1e-6
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
function schedule(output, num_steps, shift)
|
||||
local ke = (params and params.kinetic_energy) or 1.5
|
||||
local df = (params and params.damping_friction) or 3.0
|
||||
|
||||
-- Compute velocity at each normalized time point
|
||||
local velocity = {}
|
||||
for i = 0, num_steps - 1 do
|
||||
local t = i / math.max(num_steps - 1, 1)
|
||||
local v = (1.0 + ke * t) * math.exp(-df * t)
|
||||
velocity[i] = math.max(v, EPSILON) -- never negative
|
||||
end
|
||||
|
||||
-- Integrate: cumulative distance
|
||||
local distance = {}
|
||||
distance[0] = 0.0
|
||||
local running = 0.0
|
||||
for i = 0, num_steps - 1 do
|
||||
running = running + velocity[i]
|
||||
distance[i + 1] = running
|
||||
end
|
||||
|
||||
-- Normalize and map to sigma [1.0 → 0.0]
|
||||
local total = distance[num_steps]
|
||||
if total < EPSILON then total = EPSILON end
|
||||
|
||||
local sigmas = {}
|
||||
for i = 0, num_steps do
|
||||
sigmas[i] = 1.0 - (distance[i] / total)
|
||||
end
|
||||
|
||||
sigmas[0] = 1.0
|
||||
sigmas[num_steps] = 0.0
|
||||
|
||||
-- Shift warp
|
||||
if shift ~= 1.0 then
|
||||
for i = 0, num_steps do
|
||||
local t = sigmas[i]
|
||||
sigmas[i] = shift * t / (1.0 + (shift - 1.0) * t)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 0, num_steps - 1 do
|
||||
output[i] = sigmas[i]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,392 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
-- MD HT Scheduler v5.0 — HAP + TPT Thermodynamic Timestep Schedule
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- Plugin version: V3 (HOT-Step UI / filename — what users see)
|
||||
-- Internal version: v5.0 (math/changelog — what developers track)
|
||||
-- These are different: plugin version bumps on breaking changes or major
|
||||
-- feature drops. Internal version bumps on any code change.
|
||||
--
|
||||
-- Sub-index CDF interpolation, parameter caching, post-CDF smoothing,
|
||||
-- density floor, LINA warp, SNR-space mode, poly slope, uniformity blend,
|
||||
-- verbose step-size diagnostics.
|
||||
--
|
||||
-- WHAT THIS DOES:
|
||||
-- Standard schedulers space timesteps linearly or with a simple power curve.
|
||||
-- HT uses two coupled density functions to place steps where they matter:
|
||||
--
|
||||
-- HAP (Hamiltonian Action-Principle):
|
||||
-- density_hap = 1 / ((1 + KE * t) * exp(-DF * t))
|
||||
-- High KE = front-loaded steps (aggressive early denoising)
|
||||
-- High DF = fast exponential damping toward formation
|
||||
--
|
||||
-- TPT (Thermodynamic Phase Transition):
|
||||
-- density_tpt = 1 / (|sigma - Tc| + well_width)
|
||||
-- Creates a "gravity well" that clusters steps near the critical temp
|
||||
-- where latent structure crystallizes.
|
||||
--
|
||||
-- Combined: density = density_hap + phase_intensity * density_tpt + floor
|
||||
-- Result: non-uniform sigma sequence. Works at any step count (12-150+).
|
||||
--
|
||||
-- POST-PROCESSING CHAIN (all optional, all default off):
|
||||
-- 1. Shift warp (native HOT-Step sigma warp)
|
||||
-- 2. LINA warp (time-axis resampling from MD Causal)
|
||||
-- 3. Poly slope (power curve on sigma values)
|
||||
-- 4. Uniformity blend (blend with linear uniform schedule)
|
||||
-- 5. Schedule smoothing (moving average on final sigmas)
|
||||
--
|
||||
-- CHANGELOG:
|
||||
-- v5.0: Density floor, LINA warp, SNR-space mode, poly slope, uniformity
|
||||
-- blend, post-CDF smoothing, verbose diagnostics. Additive HAP+TPT
|
||||
-- blending. Exposed well width. Restored descriptive header. Complete
|
||||
-- rework from v4.0 baseline.
|
||||
-- ============================================================================
|
||||
|
||||
scheduler = {
|
||||
name = "md_ht_scheduler V3",
|
||||
display = "MD HT Scheduler (HAP+TPT) V3",
|
||||
description = "Thermodynamic timestep schedule: HAP + TPT additive density, density floor, LINA warp, SNR-space, poly slope, uniformity blend, smoothing. 12 to 150+ steps.",
|
||||
params = {
|
||||
-- ── HAP ─────────────────────────────────────────────────────────────
|
||||
{ key = "kinetic_energy", type = "slider", label = "Kinetic Energy",
|
||||
default = 0.3, min = 0.0, max = 3.0, step = 0.05,
|
||||
hint = "HAP leading-edge sharpness. 0=uniform, 0.3=standard, 2+=aggressive front-loading." },
|
||||
{ key = "damping_friction", type = "slider", label = "Damping Friction",
|
||||
default = 2.2, min = 0.0, max = 6.0, step = 0.1,
|
||||
hint = "HAP tail compression. Higher = steps cluster toward the front." },
|
||||
-- ── TPT ─────────────────────────────────────────────────────────────
|
||||
{ key = "critical_temp", type = "slider", label = "Critical Temp",
|
||||
default = 0.6, min = 0.05, max = 0.95, step = 0.05,
|
||||
hint = "TPT phase transition center (sigma fraction). Steps cluster here." },
|
||||
{ key = "phase_intensity", type = "slider", label = "Phase Intensity",
|
||||
default = 1.0, min = 0.0, max = 3.0, step = 0.1,
|
||||
hint = "TPT clustering strength. 0=off (pure HAP). 1=moderate. 2+=strong." },
|
||||
{ key = "well_width", type = "slider", label = "Well Width",
|
||||
default = 0.25, min = 0.05, max = 0.5, step = 0.05,
|
||||
hint = "TPT softening radius. 0.1=tight. 0.25=balanced. 0.4+=broad." },
|
||||
-- ── Density Floor ───────────────────────────────────────────────────
|
||||
{ key = "density_floor", type = "slider", label = "Density Floor",
|
||||
default = 0.1, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Minimum density everywhere. Prevents sparse gaps. 0=off. 0.1=gentle. 0.3+=uniform-leaning." },
|
||||
-- ── SNR Space ───────────────────────────────────────────────────────
|
||||
{ key = "snr_space", type = "toggle", label = "SNR Space",
|
||||
default = false,
|
||||
hint = "Compute density on an SNR-uniform grid instead of sigma-uniform. Steps track perceptual importance. Better for audio at high step counts." },
|
||||
-- ── Post-Processing ─────────────────────────────────────────────────
|
||||
{ key = "lina_shift", type = "slider", label = "LINA Warp",
|
||||
default = 1.0, min = 0.5, max = 2.0, step = 0.05,
|
||||
hint = "Time-axis resampling (from MD Causal). 1.0=off. <1=front-load (more high-sigma steps). >1=back-load (more low-sigma steps). Different from shift warp — this resamples WHERE on the curve, not the sigma VALUES." },
|
||||
{ key = "poly_slope", type = "slider", label = "Poly Slope",
|
||||
default = 1.0, min = 0.5, max = 2.0, step = 0.05,
|
||||
hint = "Power curve on sigma values. 1.0=off. >1=compress toward zero (more detail steps, good for long runs). <1=compress toward one (more structure steps, good for 12-step turbo)." },
|
||||
{ key = "uniform_blend", type = "slider", label = "Uniformity Blend",
|
||||
default = 0.0, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Blend with linear uniform schedule. 0=pure HT. 0.3=gentle uniformity. 1.0=pure uniform. Tames HT clustering for stabilization solvers (Trajectory Anchor)." },
|
||||
{ key = "smooth_window", type = "slider", label = "Schedule Smoothing",
|
||||
default = 0, min = 0, max = 7, step = 1,
|
||||
hint = "Post-CDF moving average on final sigmas. 0=off. 3=mild. 5+=heavy. Smooths step-size transitions." },
|
||||
-- ── Engine ──────────────────────────────────────────────────────────
|
||||
{ key = "dense_steps", type = "slider", label = "CDF Resolution",
|
||||
default = 1000, min = 200, max = 5000, step = 100,
|
||||
hint = "Resolution of the integration grid." },
|
||||
{ key = "shift", type = "slider", label = "Shift Warp",
|
||||
default = 1.0, min = 0.5, max = 8.0, step = 0.1,
|
||||
hint = "Native HOT-Step sigma warp. Applied first in the post-processing chain." },
|
||||
{ key = "verbose", type = "toggle", label = "Verbose",
|
||||
default = false,
|
||||
hint = "Print per-step sigma values, step sizes, and gap ratio to console." },
|
||||
},
|
||||
}
|
||||
|
||||
-- ── Hoisted Buffers & Cache ──────────────────────────────────────────────────
|
||||
|
||||
local EPSILON = 1e-6
|
||||
|
||||
local _cache = { ke = -1, df = -1, tc = -1, pi = -1, ww = -1, fl = -1, snr = -1, dense_n = -1 }
|
||||
local _dense = {}
|
||||
local _cdf = {}
|
||||
local _sigmas = {}
|
||||
local _smooth_buf = {}
|
||||
local _last_num_steps = -1
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
-- ── SNR-Space Grid ──────────────────────────────────────────────────────────
|
||||
-- Build dense grid uniform in SNR space: snr = log(sigma / (1 - sigma)).
|
||||
-- Maps back to sigma via sigmoid: sigma = 1 / (1 + exp(-snr)).
|
||||
-- Endpoints clamped to avoid inf at sigma=0 and sigma=1.
|
||||
|
||||
local function build_snr_grid(dense, dense_n, sigma_max, sigma_min)
|
||||
local s_hi = clamp(sigma_max, 0.001, 0.999)
|
||||
local s_lo = clamp(sigma_min + 0.001, 0.001, 0.999)
|
||||
local snr_hi = math.log(s_hi / (1.0 - s_hi))
|
||||
local snr_lo = math.log(s_lo / (1.0 - s_lo))
|
||||
|
||||
for i = 0, dense_n - 1 do
|
||||
local snr = snr_hi + (snr_lo - snr_hi) * i / (dense_n - 1)
|
||||
dense[i] = 1.0 / (1.0 + math.exp(-snr))
|
||||
end
|
||||
dense[0] = sigma_max
|
||||
dense[dense_n - 1] = sigma_min
|
||||
end
|
||||
|
||||
-- ── LINA Warp (from MD Causal) ──────────────────────────────────────────────
|
||||
-- Time-axis resampling: t_warped = t^shift, then interpolate into the sigma
|
||||
-- array at the warped position. Shift < 1 front-loads, shift > 1 back-loads.
|
||||
-- Different from native shift warp which transforms sigma values directly.
|
||||
|
||||
local function apply_lina_warp(sigmas, n, shift)
|
||||
if shift == 1.0 then return end
|
||||
-- Read into scratch buffer first
|
||||
for i = 0, n do _smooth_buf[i] = sigmas[i] end
|
||||
|
||||
for i = 0, n do
|
||||
local t = i / n
|
||||
local t_w = t ^ shift
|
||||
local raw_idx = t_w * n
|
||||
local idx_lo = clamp(math.floor(raw_idx), 0, n)
|
||||
local idx_hi = clamp(idx_lo + 1, 0, n)
|
||||
local frac = raw_idx - idx_lo
|
||||
local s_lo = _smooth_buf[idx_lo]
|
||||
local s_hi = _smooth_buf[idx_hi] or _smooth_buf[n]
|
||||
sigmas[i] = s_lo * (1.0 - frac) + s_hi * frac
|
||||
end
|
||||
sigmas[0] = _smooth_buf[0]
|
||||
sigmas[n] = _smooth_buf[n]
|
||||
end
|
||||
|
||||
-- ── Post-CDF Schedule Smoothing ─────────────────────────────────────────────
|
||||
|
||||
local function smooth_schedule(sigmas, n, window)
|
||||
if window < 2 or n < window then return end
|
||||
local half = math.floor(window / 2)
|
||||
|
||||
for i = 1, n - 1 do
|
||||
local sum = 0.0
|
||||
local count = 0
|
||||
for j = math.max(0, i - half), math.min(n, i + half) do
|
||||
sum = sum + sigmas[j]
|
||||
count = count + 1
|
||||
end
|
||||
_smooth_buf[i] = sum / count
|
||||
end
|
||||
|
||||
for i = 1, n - 1 do
|
||||
sigmas[i] = _smooth_buf[i]
|
||||
end
|
||||
|
||||
-- Enforce monotonically decreasing
|
||||
for i = 1, n do
|
||||
if sigmas[i] >= sigmas[i - 1] then
|
||||
sigmas[i] = sigmas[i - 1] - EPSILON
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Core Schedule Builder ────────────────────────────────────────────────────
|
||||
|
||||
local function build_ht_schedule(num_steps, ke, df, tc_frac, pi, ww, fl, snr_mode, dense_n)
|
||||
local sigma_max = 1.0
|
||||
local sigma_min = 0.0
|
||||
local snr_flag = snr_mode and 1 or 0
|
||||
|
||||
-- Only rebuild CDF if params changed
|
||||
if ke ~= _cache.ke or df ~= _cache.df or tc_frac ~= _cache.tc
|
||||
or pi ~= _cache.pi or ww ~= _cache.ww or fl ~= _cache.fl
|
||||
or snr_flag ~= _cache.snr or dense_n ~= _cache.dense_n then
|
||||
|
||||
-- Build dense grid (sigma-uniform or SNR-uniform)
|
||||
if snr_mode then
|
||||
build_snr_grid(_dense, dense_n, sigma_max, sigma_min)
|
||||
else
|
||||
for i = 0, dense_n - 1 do
|
||||
_dense[i] = sigma_max - (sigma_max - sigma_min) * i / (dense_n - 1)
|
||||
end
|
||||
end
|
||||
|
||||
local critical_temp = sigma_min + tc_frac * (sigma_max - sigma_min)
|
||||
local running = 0.0
|
||||
|
||||
for i = 0, dense_n - 1 do
|
||||
local s = _dense[i]
|
||||
local t = (sigma_max - s) / (sigma_max - sigma_min + EPSILON)
|
||||
|
||||
-- HAP density
|
||||
local v_hap = (1.0 + ke * t) * math.exp(-df * t)
|
||||
local d_hap = 1.0 / (v_hap + EPSILON)
|
||||
|
||||
-- TPT density
|
||||
local dist_tc = math.abs(s - critical_temp)
|
||||
local d_tpt = 1.0 / (dist_tc + ww)
|
||||
|
||||
-- Additive blending + density floor
|
||||
running = running + d_hap + pi * d_tpt + fl
|
||||
_cdf[i] = running
|
||||
end
|
||||
|
||||
-- Normalize CDF to [0, 1]
|
||||
local cdf0 = _cdf[0]
|
||||
local cdf_n1 = _cdf[dense_n - 1]
|
||||
local range = cdf_n1 - cdf0 + EPSILON
|
||||
|
||||
for i = 0, dense_n - 1 do
|
||||
_cdf[i] = (_cdf[i] - cdf0) / range
|
||||
end
|
||||
|
||||
_cache.ke = ke
|
||||
_cache.df = df
|
||||
_cache.tc = tc_frac
|
||||
_cache.pi = pi
|
||||
_cache.ww = ww
|
||||
_cache.fl = fl
|
||||
_cache.snr = snr_flag
|
||||
_cache.dense_n = dense_n
|
||||
end
|
||||
|
||||
-- Pre-allocate
|
||||
if num_steps > _last_num_steps then
|
||||
for i = 0, num_steps do _sigmas[i] = 0.0 end
|
||||
for i = 0, num_steps do _smooth_buf[i] = 0.0 end
|
||||
_last_num_steps = num_steps
|
||||
end
|
||||
|
||||
-- Binary search
|
||||
local function searchsorted(target)
|
||||
local lo, hi = 0, dense_n - 1
|
||||
while lo < hi do
|
||||
local mid = math.floor((lo + hi) / 2)
|
||||
if _cdf[mid] < target then lo = mid + 1 else hi = mid end
|
||||
end
|
||||
return clamp(lo, 0, dense_n - 1)
|
||||
end
|
||||
|
||||
-- Sub-index interpolation
|
||||
for i = 0, num_steps do
|
||||
local tgt = i / num_steps
|
||||
local idx = searchsorted(tgt)
|
||||
|
||||
if idx == 0 then
|
||||
_sigmas[i] = _dense[0]
|
||||
else
|
||||
local c0 = _cdf[idx - 1]
|
||||
local c1 = _cdf[idx]
|
||||
local t_interp = (c1 > c0) and ((tgt - c0) / (c1 - c0)) or 0.0
|
||||
|
||||
local d0 = _dense[idx - 1]
|
||||
local d1 = _dense[idx]
|
||||
_sigmas[i] = d0 + t_interp * (d1 - d0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Force exact endpoints
|
||||
_sigmas[0] = sigma_max
|
||||
_sigmas[num_steps] = sigma_min
|
||||
|
||||
return _sigmas
|
||||
end
|
||||
|
||||
-- ── Required schedule() function ─────────────────────────────────────────────
|
||||
|
||||
function schedule(output, num_steps, shift_val)
|
||||
local ke = (params and params.kinetic_energy) or 0.3
|
||||
local df = (params and params.damping_friction) or 2.2
|
||||
local tc_frac = (params and params.critical_temp) or 0.6
|
||||
local pi_ = (params and params.phase_intensity) or 1.0
|
||||
local ww = (params and params.well_width) or 0.25
|
||||
local fl = (params and params.density_floor) or 0.1
|
||||
local snr_mode = (params and params.snr_space) or false
|
||||
local lina = (params and params.lina_shift) or 1.0
|
||||
local poly = (params and params.poly_slope) or 1.0
|
||||
local u_blend = (params and params.uniform_blend) or 0.0
|
||||
local sm_win = math.floor((params and params.smooth_window) or 0)
|
||||
local dense_n = math.floor((params and params.dense_steps) or 1000)
|
||||
local sh = (params and params.shift) or shift_val
|
||||
local verbose = (params and params.verbose) or false
|
||||
|
||||
local sigmas = build_ht_schedule(num_steps, ke, df, tc_frac, pi_, ww, fl, snr_mode, dense_n)
|
||||
|
||||
-- ── POST-PROCESSING CHAIN ───────────────────────────────────────────
|
||||
-- Order: shift → LINA → poly → uniformity → smooth
|
||||
-- Each is independent and default-off. Compose cleanly at any step count.
|
||||
|
||||
-- 1. Native shift warp (sigma-value transform)
|
||||
if sh ~= 1.0 then
|
||||
for i = 0, num_steps do
|
||||
local t = sigmas[i]
|
||||
sigmas[i] = sh * t / (1.0 + (sh - 1.0) * t)
|
||||
end
|
||||
end
|
||||
|
||||
-- 2. LINA warp (time-axis resampling)
|
||||
if lina ~= 1.0 then
|
||||
apply_lina_warp(sigmas, num_steps, lina)
|
||||
end
|
||||
|
||||
-- 3. Poly slope (power curve on sigma values)
|
||||
-- >1 = compress toward zero (more detail steps)
|
||||
-- <1 = compress toward one (more structure steps, good for 12-step turbo)
|
||||
if poly ~= 1.0 then
|
||||
for i = 1, num_steps - 1 do
|
||||
sigmas[i] = sigmas[i] ^ poly
|
||||
end
|
||||
-- Endpoints stay exact
|
||||
end
|
||||
|
||||
-- 4. Uniformity blend (blend with linear schedule)
|
||||
-- Tames HT clustering for stabilization solvers
|
||||
if u_blend > 0.0 then
|
||||
local inv = 1.0 - u_blend
|
||||
for i = 0, num_steps do
|
||||
local uniform_sigma = 1.0 - (i / num_steps)
|
||||
sigmas[i] = inv * sigmas[i] + u_blend * uniform_sigma
|
||||
end
|
||||
end
|
||||
|
||||
-- 5. Schedule smoothing (moving average)
|
||||
if sm_win >= 2 then
|
||||
smooth_schedule(sigmas, num_steps, sm_win)
|
||||
sigmas[0] = 1.0
|
||||
sigmas[num_steps] = 0.0
|
||||
end
|
||||
|
||||
-- ── WRITE OUTPUT (no trailing zero — engine contract) ───────────────
|
||||
for i = 0, num_steps - 1 do
|
||||
output[i] = sigmas[i]
|
||||
end
|
||||
|
||||
-- ── VERBOSE ─────────────────────────────────────────────────────────
|
||||
if verbose then
|
||||
print(string.format(
|
||||
"[HT V5] %d steps | ke=%.2f df=%.1f tc=%.2f pi=%.1f ww=%.2f fl=%.2f | snr=%s lina=%.2f poly=%.2f ub=%.2f sm=%d",
|
||||
num_steps, ke, df, tc_frac, pi_, ww, fl,
|
||||
snr_mode and "on" or "off", lina, poly, u_blend, sm_win))
|
||||
local min_gap, max_gap = 1.0, 0.0
|
||||
for i = 0, num_steps - 1 do
|
||||
local sigma_next = (i < num_steps - 1) and sigmas[i + 1] or 0.0
|
||||
local gap = sigmas[i] - sigma_next
|
||||
if gap < min_gap then min_gap = gap end
|
||||
if gap > max_gap then max_gap = gap end
|
||||
print(string.format(" step %02d: sigma=%.5f gap=%.5f", i, sigmas[i], gap))
|
||||
end
|
||||
print(string.format("[HT V5] Gap range: min=%.5f max=%.5f ratio=%.1f:1",
|
||||
min_gap, max_gap, max_gap / (min_gap + EPSILON)))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,984 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- MD Confluence V4 -- STORM / Trajectory Anchor hybrid solver
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- V4: Commons integration + relational velocity decomposition.
|
||||
-- Tonal ramp, look-back floor, RMS default on, anchor_blend 0.12.
|
||||
--
|
||||
-- owns_loop = true. Forks STORM's stiffness-gated multi-order dispatch AND
|
||||
-- Trajectory Anchor's full 13-stage stateful correction stack into one loop,
|
||||
-- blending their two x_next candidates per step via disagreement- and
|
||||
-- inertia-modulated mixing.
|
||||
--
|
||||
-- CANDIDATE MODEL:
|
||||
-- Both candidates are x_next (post-advance latents), NOT vt.
|
||||
-- v_curr is computed ONCE per step and shared by both candidates.
|
||||
--
|
||||
-- STATE-FEEDBACK RULE:
|
||||
-- STORM's v_cache stores velocity (v_curr, shared) -- no desync possible.
|
||||
-- Anchor's latent state (_anc_prev_out, _anc_history) is OVERWRITTEN with
|
||||
-- x_final (the blended result) so its memory/inertia/concept-lock math
|
||||
-- believes the blended trajectory is what happened. One-shot references
|
||||
-- (identity anchor snapshot, tonal anchor capture) fire against x_final too
|
||||
-- since they read whatever the actual trajectory is at anchor_sigma.
|
||||
-- ============================================================================
|
||||
|
||||
solver = {
|
||||
name = "md_confluence_v4",
|
||||
display = "MD Confluence V4",
|
||||
description = "STORM / Trajectory Anchor hybrid with batch-aware routing. Disagreement- and inertia-modulated latent blend. Per-batch tonal anchor, spectral guard, and RMS servo.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
-- ── Mix ──────────────────────────────────────────────────────────────
|
||||
{ key = "mix_amount", type = "slider", label = "Mix Amount",
|
||||
default = 50, min = 0, max = 100, step = 1,
|
||||
hint = "Base blend: 0 = pure STORM, 100 = pure Trajectory Anchor. Modulated at runtime by disagreement mode and inertia gating -- effective mix moves around this value, not on it." },
|
||||
{ key = "disagreement_mode", type = "select", label = "Disagreement Mode",
|
||||
default = "adaptive",
|
||||
options = {
|
||||
{ value = "damp", label = "Damp (consensus)" },
|
||||
{ value = "amplify", label = "Amplify (instability)" },
|
||||
{ value = "adaptive", label = "Adaptive (damp early, amplify late)" },
|
||||
},
|
||||
hint = "How blend reacts when STORM and Anchor candidates disagree. Damp = pull toward consensus. Amplify = disagreement becomes controlled texture. Adaptive = damp during structure, amplify during detail." },
|
||||
{ key = "damp_strength", type = "slider", label = "Damp Strength",
|
||||
default = 0.4, min = 0, max = 1, step = 0.05,
|
||||
hint = "How hard disagreement pulls mix toward consensus (damp/adaptive mode)." },
|
||||
{ key = "chaos_strength", type = "slider", label = "Chaos Strength",
|
||||
default = 0.3, min = 0, max = 1, step = 0.05,
|
||||
hint = "How hard disagreement pushes mix further from center (amplify/adaptive mode)." },
|
||||
{ key = "inertia_influence", type = "slider", label = "Inertia Influence",
|
||||
default = 0.7, min = 0, max = 1, step = 0.05,
|
||||
hint = "How much Anchor's inertia state gates the mix. 0 = pure user mix. 1 = full auto-gating (low inertia collapses toward STORM)." },
|
||||
{ key = "inertia_gate_low", type = "slider", label = "Inertia Gate Low",
|
||||
default = 0.15, min = 0, max = 1, step = 0.01,
|
||||
hint = "Smoothstep floor: inertia magnitude below this = mix fully gated toward STORM." },
|
||||
{ key = "inertia_gate_high", type = "slider", label = "Inertia Gate High",
|
||||
default = 0.6, min = 0, max = 1, step = 0.01,
|
||||
hint = "Smoothstep ceiling: inertia magnitude above this = user's stated mix takes over fully." },
|
||||
|
||||
-- ── STORM params ──────────────────────────────────────────────────
|
||||
{ key = "stiffness_threshold", type = "slider", label = "STORM: Detail Sensitivity",
|
||||
default = 0.15, min = 0.05, max = 0.50, step = 0.01,
|
||||
hint = "Stiffness threshold. Lower = more careful on transients." },
|
||||
{ key = "rk_order", type = "select", label = "STORM: Precision Level",
|
||||
default = "auto",
|
||||
options = {
|
||||
{ value = "auto", label = "Auto" },
|
||||
{ value = "2", label = "RK2" }, { value = "3", label = "RK3" },
|
||||
{ value = "4", label = "RK4" }, { value = "5", label = "RK5" },
|
||||
},
|
||||
hint = "STORK solver order when stiff." },
|
||||
{ key = "cache_depth", type = "slider", label = "STORM: History Memory",
|
||||
default = 5, min = 2, max = 10, step = 1,
|
||||
hint = "Velocity cache depth for STORM's multi-order dispatch." },
|
||||
{ key = "look_back_lambda_storm", type = "slider", label = "STORM: Look-Back Lambda",
|
||||
default = 0.15, min = 0, max = 1, step = 0.01,
|
||||
hint = "STORM's own look-back smoother weight. 0 = off." },
|
||||
{ key = "look_back_snr_power_storm", type = "slider", label = "STORM: Look-Back SNR Power",
|
||||
default = 1.5, min = 0.5, max = 3, step = 0.1,
|
||||
hint = "STORM look-back falloff exponent." },
|
||||
|
||||
-- ── Anchor params ──────────────────────────────────────────────────
|
||||
{ key = "warmup_steps", type = "slider", label = "Anchor: Warmup Steps",
|
||||
default = 2, min = 0, max = 6, step = 1,
|
||||
hint = "Skip Anchor stateful features for first N steps. Also gates inertia toward 0 during warmup." },
|
||||
{ key = "inertia_alpha", type = "slider", label = "Anchor: Inertia Alpha",
|
||||
default = 0.15, min = 0.0, max = 0.5, step = 0.01,
|
||||
hint = "Anchor velocity carry-over coefficient. Entropy-modulated at runtime." },
|
||||
{ key = "memory_blend", type = "slider", label = "Anchor: Memory Blend",
|
||||
default = 0.12, min = 0.0, max = 0.5, step = 0.01,
|
||||
hint = "3-step ring buffer blend fraction." },
|
||||
{ key = "concept_lock", type = "toggle", label = "Anchor: Concept Lock",
|
||||
default = true, hint = "Stability mask on settled regions." },
|
||||
{ key = "concept_sigma_power", type = "slider", label = "Anchor: Concept Sigma Power",
|
||||
default = 1.0, min = 0.25, max = 3.0, step = 0.25,
|
||||
hint = "Concept lock fade curve across sigma." },
|
||||
{ key = "identity_anchor", type = "toggle", label = "Anchor: Identity Anchor",
|
||||
default = false, hint = "Snapshot pull-back at anchor_sigma." },
|
||||
{ key = "anchor_sigma", type = "slider", label = "Anchor: Anchor Sigma",
|
||||
default = 0.5, min = 0.1, max = 0.9, step = 0.05,
|
||||
hint = "Sigma fraction for identity/tonal anchor capture." },
|
||||
{ key = "anchor_blend", type = "slider", label = "Anchor: Anchor Blend",
|
||||
default = 0.08, min = 0.01, max = 0.30, step = 0.01,
|
||||
hint = "Pull strength toward identity anchor." },
|
||||
{ key = "tonal_anchor", type = "toggle", label = "Anchor: Tonal Anchor",
|
||||
default = true, hint = "Spectral centroid drift correction." },
|
||||
{ key = "tonal_strength", type = "slider", label = "Anchor: Tonal Strength",
|
||||
default = 0.15, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Tonal correction scale (hard-capped 0.1%/step regardless)." },
|
||||
{ key = "look_back_enabled_anchor", type = "toggle", label = "Anchor: Look-Back Smoother",
|
||||
default = false, hint = "SNR-adaptive latent EMA." },
|
||||
{ key = "look_back_lambda_anchor", type = "slider", label = "Anchor: Look-Back Lambda",
|
||||
default = 0.15, min = 0.05, max = 1.0, step = 0.05,
|
||||
hint = "Max look-back weight at high sigma." },
|
||||
{ key = "look_back_snr_power_anchor", type = "slider", label = "Anchor: Look-Back SNR Power",
|
||||
default = 1.3, min = 0.5, max = 3.0, step = 0.1,
|
||||
hint = "Look-back falloff exponent." },
|
||||
{ key = "rms_servo", type = "toggle", label = "Anchor: RMS Servo",
|
||||
default = false, hint = "Downward-only RMS ceiling." },
|
||||
{ key = "rms_target_min", type = "slider", label = "Anchor: RMS Target Min",
|
||||
default = 1.2, min = 0.1, max = 3.0, step = 0.05, hint = "RMS ceiling at low sigma." },
|
||||
{ key = "rms_target_max", type = "slider", label = "Anchor: RMS Target Max",
|
||||
default = 2.5, min = 0.5, max = 5.0, step = 0.05, hint = "RMS ceiling at high sigma." },
|
||||
{ key = "rms_servo_gain", type = "slider", label = "Anchor: RMS Servo Gain",
|
||||
default = 0.6, min = 0.1, max = 1.0, step = 0.05, hint = "Servo correction aggressiveness." },
|
||||
{ key = "latent_pressure", type = "toggle", label = "Anchor: Latent Pressure",
|
||||
default = false, hint = "Entropy x RMS target correction (off by default)." },
|
||||
{ key = "pressure_target_rms", type = "slider", label = "Anchor: Pressure Target RMS",
|
||||
default = 2.0, min = 0.5, max = 4.0, step = 0.1, hint = "RMS component of pressure target." },
|
||||
{ key = "pressure_target_entropy", type = "slider", label = "Anchor: Pressure Target Entropy",
|
||||
default = 7.5, min = 1.0, max = 15.0, step = 0.5, hint = "Shannon entropy target." },
|
||||
|
||||
-- ── Post-Blend Shearing Control ──────────────────────────────────
|
||||
{ key = "post_blend_lookback", type = "slider", label = "Post-Blend Look-Back",
|
||||
default = 0.0, min = 0.0, max = 0.7, step = 0.05,
|
||||
hint = "SNR-adaptive EMA on x_final AFTER the blend. Neither sub-solver's look-back covers the blend seam -- this does. 0 = off. 0.25 = subtle anti-shear. Fades with sigma like anchor's look-back." },
|
||||
{ key = "post_blend_snr_power", type = "slider", label = "Post-Blend SNR Power",
|
||||
default = 1.0, min = 0.5, max = 3.0, step = 0.1,
|
||||
hint = "Falloff exponent for post-blend look-back. 1.0 = linear fade (more late-step smoothing than anchor's 1.3 default). Lower = more smoothing persists into detail steps." },
|
||||
{ key = "spectral_guard", type = "slider", label = "Spectral Blend Guard",
|
||||
default = 0.4, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Frequency-aware blend correction. When STORM and anchor disagree, their delta concentrates in high-freq (metallic) components. This attenuates the blend delta in the upper latent bands proportional to disagreement. 0 = off (flat blend). 0.4 = moderate HF damping. 1.0 = aggressive." },
|
||||
{ key = "late_damp_override", type = "slider", label = "Late Damp Override",
|
||||
default = 0.7, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "In adaptive mode, overrides amplify with damp for the final portion of the run. 0.7 = last 30% of steps forced to damp. 0 = no override (pure adaptive all the way). Prevents late-step disagreement amplification causing metallic ringing." },
|
||||
|
||||
-- ── SDE / Safety ──────────────────────────────────────────────────
|
||||
{ key = "eta", type = "slider", label = "Noise Injection (0 = ODE)",
|
||||
default = 0.0, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Post-blend SDE noise: scale = sigma_next * eta." },
|
||||
{ key = "seed", type = "slider", label = "Seed",
|
||||
default = 42, min = 0, max = 999999, step = 1,
|
||||
hint = "RNG seed for SDE noise." },
|
||||
{ key = "safety_clamp", type = "slider", label = "Safety Clamp",
|
||||
default = 2.5, min = 1.0, max = 5.0, step = 0.1,
|
||||
hint = "Max abs latent value post-blend." },
|
||||
{ key = "verbose", type = "toggle", label = "Verbose Logging",
|
||||
default = false,
|
||||
hint = "Per-step blend diagnostics: agreement, inertia, effective_mix, STORM mode." },
|
||||
{ key = "relational_weight", type = "slider", label = "Relational Weight",
|
||||
default = 0.0, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Barbour Best Matching velocity decomposition. 0 = off." },
|
||||
{ key = "relational_sigma_power", type = "slider", label = "Relational Sigma Decay",
|
||||
default = 1.0, min = 0.25, max = 4.0, step = 0.25,
|
||||
hint = "How fast relational weight fades." },
|
||||
},
|
||||
}
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- HELPERS (aliased from md_solver_commons)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local EPSILON = C.EPSILON
|
||||
local PRESSURE_CAP = 5e-4
|
||||
|
||||
local clamp = C.clamp
|
||||
local smoothstep = C.smoothstep
|
||||
local fa_to_tbl = C.fa_to_tbl
|
||||
local tbl_to_fa = C.tbl_to_fa
|
||||
local vec_norm = C.vec_norm
|
||||
local vec_sub_norm = C.vec_sub_norm
|
||||
local vec_dot = C.vec_dot
|
||||
local vec_clone = C.vec_clone
|
||||
local cosine_sim = C.cosine_sim
|
||||
local has_nan_inf = C.has_nan_inf
|
||||
local rms_range = C.rms_range
|
||||
local rms = C.rms
|
||||
|
||||
local function shannon_entropy(a, n)
|
||||
local sum = 0.0
|
||||
for i = 0, n - 1 do sum = sum + math.abs(a[i]) + 1e-7 end
|
||||
local inv_sum = 1.0 / (sum + 1e-8)
|
||||
local H = 0.0
|
||||
for i = 0, n - 1 do
|
||||
local p = (math.abs(a[i]) + 1e-7) * inv_sum
|
||||
H = H - p * math.log(p + EPSILON) / math.log(2.0)
|
||||
end
|
||||
H = math.max(0.05, H)
|
||||
if H ~= H or H == math.huge or H == -math.huge then H = 5.0 end
|
||||
return H
|
||||
end
|
||||
|
||||
local spectral_centroid = C.spectral_centroid
|
||||
local band_energy = C.band_energy
|
||||
local make_rng = C.make_rng
|
||||
local normal = C.normal
|
||||
local bool_param = C.bool_param
|
||||
local num_param = C.num_param
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- STORM INTERNALS (ported verbatim from storm_sampler_core.lua)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function compute_stiffness(v_curr, v_cache, step_idx, baseline, threshold, ema_alpha, n_calib, n)
|
||||
threshold = threshold or 0.15
|
||||
ema_alpha = ema_alpha or 0.3
|
||||
n_calib = n_calib or 4
|
||||
|
||||
if #v_cache < 1 then return true, baseline, nil end
|
||||
|
||||
local v_prev = v_cache[#v_cache].v
|
||||
local norm_delta = vec_sub_norm(v_curr, v_prev, n)
|
||||
local norm_curr = vec_norm(v_curr, n) + 1e-8
|
||||
local raw_ratio = norm_delta / norm_curr
|
||||
|
||||
local prev_ema = baseline.ema or raw_ratio
|
||||
local smoothed = ema_alpha * raw_ratio + (1.0 - ema_alpha) * prev_ema
|
||||
baseline.ema = smoothed
|
||||
|
||||
local dot = vec_dot(v_curr, v_prev, n)
|
||||
local nc = vec_norm(v_curr, n)
|
||||
local np_ = vec_norm(v_prev, n)
|
||||
local cos_sim_val = dot / (nc * np_ + 1e-8)
|
||||
|
||||
if step_idx < n_calib then
|
||||
baseline.sum = (baseline.sum or 0.0) + smoothed
|
||||
baseline.count = (baseline.count or 0) + 1
|
||||
baseline.last_ratio = smoothed
|
||||
return true, baseline, cos_sim_val
|
||||
end
|
||||
|
||||
local bmean = baseline.sum / math.max(baseline.count, 1)
|
||||
local adap_thr = threshold * (bmean / 0.15)
|
||||
adap_thr = clamp(adap_thr, 0.05, 0.50)
|
||||
|
||||
local stiff = smoothed > adap_thr
|
||||
baseline.last_ratio = smoothed
|
||||
baseline.last_threshold = adap_thr
|
||||
return stiff, baseline, cos_sim_val
|
||||
end
|
||||
|
||||
local function stork_step(v_cache, x, sigma_curr, sigma_next, v_curr, rk_order, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
local n_cache = #v_cache
|
||||
|
||||
local actual_order
|
||||
if rk_order == "auto" then
|
||||
actual_order = (n_cache >= 1) and math.min(n_cache + 1, 5) or 1
|
||||
else
|
||||
actual_order = (n_cache >= 1) and math.min(tonumber(rk_order), n_cache + 1) or 1
|
||||
end
|
||||
actual_order = math.max(actual_order, 1)
|
||||
|
||||
if n_cache < 1 or actual_order <= 1 then
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
return x_next, 1
|
||||
end
|
||||
|
||||
local e0 = v_cache[#v_cache]
|
||||
local v_prev_0 = e0.v
|
||||
local sigma_prev = e0.sigma
|
||||
|
||||
local dot = vec_dot(v_curr, v_prev_0, n)
|
||||
local nc = vec_norm(v_curr, n)
|
||||
local np_ = vec_norm(v_prev_0, n)
|
||||
local cos_sim_val = dot / (nc * np_ + 1e-8)
|
||||
local damping = clamp(cos_sim_val, 0.0, 1.0)
|
||||
|
||||
local denom = sigma_curr - sigma_prev
|
||||
if math.abs(denom) < 1e-8 then
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
return x_next, 2
|
||||
end
|
||||
local alpha = (sigma_next - sigma_curr) / denom
|
||||
|
||||
local x_next = {}
|
||||
|
||||
if actual_order == 2 then
|
||||
for i = 0, n - 1 do
|
||||
local v_extrap = v_curr[i] + (alpha * damping) * (v_curr[i] - v_prev_0[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * v_extrap)
|
||||
end
|
||||
|
||||
elseif actual_order == 3 and n_cache >= 2 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 then
|
||||
for i = 0, n - 1 do
|
||||
local ve = v_curr[i] + (alpha * damping) * (v_curr[i] - v1[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * ve)
|
||||
end
|
||||
actual_order = 2
|
||||
else
|
||||
local c0 = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do
|
||||
local v_pred = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (v_pred - v_curr[i]))
|
||||
end
|
||||
end
|
||||
|
||||
elseif actual_order == 4 and n_cache >= 3 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local v3, s3 = v_cache[#v_cache - 2].v, v_cache[#v_cache - 2].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
local h2 = s2 - s3
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 or math.abs(h2) < 1e-8 then
|
||||
local c0 = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 3
|
||||
else
|
||||
local c0 = 1.0 + dt / (2.0 * h) + dt ^ 2 / (3.0 * h * h1) + dt ^ 3 / (4.0 * h * h1 * h2)
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1 + dt ^ 2 / (2.0 * h1 * h2))
|
||||
local c2 = (dt ^ 2 / (3.0 * h * h1)) * (1.0 + dt / (2.0 * h2))
|
||||
local c3 = -(dt ^ 3) / (4.0 * h * h1 * h2)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i] + c3 * v3[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
end
|
||||
|
||||
elseif actual_order >= 5 and n_cache >= 4 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local v3, s3 = v_cache[#v_cache - 2].v, v_cache[#v_cache - 2].sigma
|
||||
local v4, s4 = v_cache[#v_cache - 3].v, v_cache[#v_cache - 3].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
local h2 = s2 - s3
|
||||
local h3 = s3 - s4
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 or math.abs(h2) < 1e-8 or math.abs(h3) < 1e-8 then
|
||||
local c0 = 1.0 + dt / (2.0 * h) + dt ^ 2 / (3.0 * h * h1) + dt ^ 3 / (4.0 * h * h1 * h2)
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1 + dt ^ 2 / (2.0 * h1 * h2))
|
||||
local c2 = (dt ^ 2 / (3.0 * h * h1)) * (1.0 + dt / (2.0 * h2))
|
||||
local c3 = -(dt ^ 3) / (4.0 * h * h1 * h2)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i] + c3 * v3[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 4
|
||||
else
|
||||
local c0 = 1.0 + dt / (2.0 * h) + dt ^ 2 / (3.0 * h * h1) + dt ^ 3 / (4.0 * h * h1 * h2) + dt ^ 4 / (5.0 * h * h1 * h2 * h3)
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1 + dt ^ 2 / (2.0 * h1 * h2) + dt ^ 3 / (3.0 * h1 * h2 * h3))
|
||||
local c2 = (dt ^ 2 / (3.0 * h * h1)) * (1.0 + dt / (2.0 * h2) + dt ^ 2 / (3.0 * h2 * h3))
|
||||
local c3 = -(dt ^ 3 / (4.0 * h * h1 * h2)) * (1.0 + dt / (2.0 * h3))
|
||||
local c4 = dt ^ 4 / (5.0 * h * h1 * h2 * h3)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i] + c3 * v3[i] + c4 * v4[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 5
|
||||
end
|
||||
|
||||
else
|
||||
-- Fallback AB2
|
||||
for i = 0, n - 1 do
|
||||
local ve = v_curr[i] + (alpha * damping) * (v_curr[i] - v_prev_0[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * ve)
|
||||
end
|
||||
actual_order = 2
|
||||
end
|
||||
|
||||
return x_next, actual_order
|
||||
end
|
||||
|
||||
local function dpmpp3m_step(v_cache, x, sigma_curr, sigma_next, v_curr, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
local x_next = {}
|
||||
|
||||
if #v_cache >= 2 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 then
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
else
|
||||
local cc = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * (cc * v_curr[i] + c1 * v1[i] + c2 * v2[i]) end
|
||||
end
|
||||
elseif #v_cache >= 1 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local h = sigma_curr - s1
|
||||
if math.abs(h) < 1e-8 then
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
else
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * (v_curr[i] + (dt / (2.0 * h)) * (v_curr[i] - v1[i])) end
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
end
|
||||
|
||||
return x_next
|
||||
end
|
||||
|
||||
-- STORM's own look-back (operates on its x_next candidate independently)
|
||||
local function storm_look_back(x_curr, x_prev, sigma_curr, sigma_max, lambda_base, snr_power, n)
|
||||
if x_prev == nil then return x_curr, 0.0 end
|
||||
local ratio = math.min(sigma_curr / math.max(sigma_max, 1e-8), 1.0)
|
||||
local lam = lambda_base * (ratio ^ snr_power)
|
||||
local out = {}
|
||||
for i = 0, n - 1 do out[i] = (1.0 - lam) * x_curr[i] + lam * x_prev[i] end
|
||||
return out, lam
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- ANCHOR STATE (module-level, reset per generation)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local _anc_sigma_max = nil
|
||||
local _anc_has_prev = false
|
||||
local _anc_has_velocity = false
|
||||
local _anc_has_anchor = false
|
||||
local _anc_tonal_ref_cent = {} -- per-batch
|
||||
local _anc_tonal_ref_bands = {} -- per-batch
|
||||
local _anc_tonal_captured = false
|
||||
local _anc_last_entropy = 7.5
|
||||
local _anc_hist_head = 1
|
||||
local _anc_hist_count = 0
|
||||
|
||||
-- Hoisted buffers (resized on n change)
|
||||
local _anc_out = {}
|
||||
local _anc_fallback = {}
|
||||
local _anc_vel_old = {}
|
||||
local _anc_vel_raw = {}
|
||||
local _anc_id_buf = {}
|
||||
local _anc_prev_out = {}
|
||||
local _anc_hist_mean = {}
|
||||
local _anc_history = { {}, {}, {} }
|
||||
|
||||
local function reset_anchor_state(n)
|
||||
_anc_sigma_max = nil
|
||||
_anc_has_prev = false
|
||||
_anc_has_velocity = false
|
||||
_anc_has_anchor = false
|
||||
_anc_tonal_ref_cent = {} -- per-batch: [b] = centroid
|
||||
_anc_tonal_ref_bands = {} -- per-batch: [b] = {band1..4}
|
||||
_anc_tonal_captured = false
|
||||
_anc_last_entropy = 7.5
|
||||
_anc_hist_head = 1
|
||||
_anc_hist_count = 0
|
||||
for i = 0, n - 1 do
|
||||
_anc_out[i] = 0.0
|
||||
_anc_fallback[i] = 0.0
|
||||
_anc_vel_old[i] = 0.0
|
||||
_anc_vel_raw[i] = 0.0
|
||||
_anc_id_buf[i] = 0.0
|
||||
_anc_prev_out[i] = 0.0
|
||||
_anc_hist_mean[i] = 0.0
|
||||
_anc_history[1][i] = 0.0
|
||||
_anc_history[2][i] = 0.0
|
||||
_anc_history[3][i] = 0.0
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- ANCHOR CANDIDATE (full 13-stage pipeline from md_trajectory_anchor.lua)
|
||||
-- Input: x (Lua table, current latent), v_curr (velocity), sigma_curr, sigma_next, n
|
||||
-- Reads/writes _anc_* state. Returns x_next_anchor as Lua table.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function anchor_candidate(x, v_curr, sigma_curr, sigma_next, step_idx, n, p, B, NPB)
|
||||
local warmup = math.floor(num_param(p, "warmup_steps", 2))
|
||||
local f_inertia = true -- always on in confluence (inertia_alpha=0 to disable)
|
||||
local inertia_a = num_param(p, "inertia_alpha", 0.15)
|
||||
local f_memory = true -- always on (memory_blend=0 to disable)
|
||||
local mem_blend = num_param(p, "memory_blend", 0.12)
|
||||
local f_concept = bool_param(p, "concept_lock", true)
|
||||
local concept_power = num_param(p, "concept_sigma_power", 1.0)
|
||||
local f_anchor = bool_param(p, "identity_anchor", false)
|
||||
local anchor_sigma = num_param(p, "anchor_sigma", 0.5)
|
||||
local anchor_blend = num_param(p, "anchor_blend", 0.08)
|
||||
local f_tonal = bool_param(p, "tonal_anchor", true)
|
||||
local tonal_str = num_param(p, "tonal_strength", 0.15)
|
||||
local f_lookback = bool_param(p, "look_back_enabled_anchor", false)
|
||||
local lb_lambda = num_param(p, "look_back_lambda_anchor", 0.15)
|
||||
local lb_snr_power = num_param(p, "look_back_snr_power_anchor", 1.3)
|
||||
local f_rms = bool_param(p, "rms_servo", false)
|
||||
local rms_tgt_min = num_param(p, "rms_target_min", 1.2)
|
||||
local rms_tgt_max = num_param(p, "rms_target_max", 2.5)
|
||||
local rms_gain = num_param(p, "rms_servo_gain", 0.6)
|
||||
local f_pressure = bool_param(p, "latent_pressure", false)
|
||||
local p_tgt_rms = num_param(p, "pressure_target_rms", 2.0)
|
||||
local p_tgt_entropy = num_param(p, "pressure_target_entropy", 7.5)
|
||||
local sclamp = num_param(p, "safety_clamp", 2.5)
|
||||
|
||||
if _anc_sigma_max == nil then _anc_sigma_max = sigma_curr end
|
||||
local sigma_ratio = clamp(sigma_curr / math.max(_anc_sigma_max, EPSILON), 0.0, 1.0)
|
||||
local past_warmup = (step_idx >= warmup)
|
||||
|
||||
-- 2. Entropy (from input x)
|
||||
_anc_last_entropy = shannon_entropy(x, n)
|
||||
|
||||
-- 3. Euler advance: dt = sigma_next - sigma_curr (negative in flow-matching)
|
||||
local dt = sigma_next - sigma_curr
|
||||
for i = 0, n - 1 do
|
||||
local v = x[i] + dt * v_curr[i]
|
||||
_anc_out[i] = v
|
||||
_anc_fallback[i] = v
|
||||
end
|
||||
|
||||
-- 4. Latent Pressure
|
||||
if f_pressure then
|
||||
local cur_rms = rms(_anc_out, n)
|
||||
local target_product = p_tgt_entropy * p_tgt_rms
|
||||
local cur_product = _anc_last_entropy * cur_rms
|
||||
local correction = clamp(
|
||||
(target_product - cur_product) / (target_product + EPSILON),
|
||||
-PRESSURE_CAP, PRESSURE_CAP)
|
||||
if math.abs(correction) > 1e-6 then
|
||||
for i = 0, n - 1 do _anc_out[i] = _anc_out[i] * (1.0 + correction) end
|
||||
end
|
||||
end
|
||||
|
||||
-- 5. Memory Buffer
|
||||
if mem_blend > 0 and past_warmup and _anc_hist_count > 0 then
|
||||
for i = 0, n - 1 do _anc_hist_mean[i] = 0.0 end
|
||||
local hw = 1.0 / _anc_hist_count
|
||||
for h = 1, _anc_hist_count do
|
||||
for i = 0, n - 1 do _anc_hist_mean[i] = _anc_hist_mean[i] + _anc_history[h][i] end
|
||||
end
|
||||
for i = 0, n - 1 do
|
||||
_anc_out[i] = (1.0 - mem_blend) * _anc_out[i] + mem_blend * (_anc_hist_mean[i] * hw)
|
||||
end
|
||||
end
|
||||
|
||||
-- 6. Inertia Engine
|
||||
if inertia_a > 0 and past_warmup and _anc_has_prev then
|
||||
for i = 0, n - 1 do _anc_vel_raw[i] = _anc_out[i] - _anc_prev_out[i] end
|
||||
if _anc_has_velocity then
|
||||
for i = 0, n - 1 do
|
||||
_anc_vel_old[i] = 0.8 * _anc_vel_old[i] + 0.2 * _anc_vel_raw[i]
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do _anc_vel_old[i] = _anc_vel_raw[i] end
|
||||
_anc_has_velocity = true
|
||||
end
|
||||
local alpha = inertia_a * clamp(_anc_last_entropy / 7.5, 0.0, 1.5)
|
||||
for i = 0, n - 1 do _anc_out[i] = _anc_out[i] + alpha * _anc_vel_old[i] end
|
||||
end
|
||||
|
||||
-- 7. Concept Lock
|
||||
if f_concept and past_warmup and _anc_has_prev then
|
||||
local sigma_mod = sigma_ratio ^ concept_power
|
||||
if sigma_mod > 1e-4 then
|
||||
for i = 0, n - 1 do
|
||||
local delta = math.abs(_anc_out[i] - _anc_prev_out[i])
|
||||
local lock_w = (1.0 / (1.0 + math.exp(delta * 40.0 - 2.0))) * sigma_mod
|
||||
_anc_out[i] = (1.0 - lock_w) * _anc_out[i] + lock_w * _anc_prev_out[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 8. Identity Anchor
|
||||
if f_anchor and past_warmup then
|
||||
if not _anc_has_anchor and sigma_ratio <= anchor_sigma then
|
||||
for i = 0, n - 1 do _anc_id_buf[i] = _anc_out[i] end
|
||||
_anc_has_anchor = true
|
||||
elseif _anc_has_anchor then
|
||||
for i = 0, n - 1 do
|
||||
_anc_out[i] = (1.0 - anchor_blend) * _anc_out[i] + anchor_blend * _anc_id_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 9. Tonal Anchor (per-batch centroid + band correction)
|
||||
if f_tonal and past_warmup then
|
||||
if not _anc_tonal_captured and sigma_ratio <= anchor_sigma then
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
_anc_tonal_ref_cent[b] = spectral_centroid(_anc_out, off, NPB)
|
||||
_anc_tonal_ref_bands[b] = band_energy(_anc_out, off, NPB)
|
||||
end
|
||||
_anc_tonal_captured = true
|
||||
elseif _anc_tonal_captured then
|
||||
local eff_str = tonal_str * sigma_ratio
|
||||
if eff_str > 1e-6 then
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
local curr_centroid = spectral_centroid(_anc_out, off, NPB)
|
||||
local curr_bands = band_energy(_anc_out, off, NPB)
|
||||
local drift_norm_val = (curr_centroid - _anc_tonal_ref_cent[b]) /
|
||||
(math.abs(_anc_tonal_ref_cent[b]) + EPSILON)
|
||||
local tilt = clamp(-drift_norm_val * eff_str, -1e-3, 1e-3)
|
||||
local center = (NPB - 1) / 2.0
|
||||
for i = off, off + NPB - 1 do
|
||||
local dist_w = ((i - off) - center) / (center + EPSILON)
|
||||
_anc_out[i] = _anc_out[i] + tilt * dist_w * math.abs(_anc_out[i])
|
||||
end
|
||||
local ref_total, curr_total = 0.0, 0.0
|
||||
for bb = 1, 4 do
|
||||
ref_total = ref_total + _anc_tonal_ref_bands[b][bb]
|
||||
curr_total = curr_total + curr_bands[bb]
|
||||
end
|
||||
if ref_total > EPSILON and curr_total > EPSILON then
|
||||
local bsize = math.floor(NPB / 4)
|
||||
for bb = 0, 3 do
|
||||
local ref_ratio = _anc_tonal_ref_bands[b][bb + 1] / ref_total
|
||||
local curr_ratio = curr_bands[bb + 1] / curr_total
|
||||
local band_corr = clamp((ref_ratio - curr_ratio) * eff_str, -1e-3, 1e-3)
|
||||
local blo = off + bb * bsize
|
||||
local bhi = (bb == 3) and (off + NPB - 1) or (blo + bsize - 1)
|
||||
for i = blo, bhi do
|
||||
_anc_out[i] = _anc_out[i] + band_corr * math.abs(_anc_out[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 10. Look-Back Smoother
|
||||
if f_lookback and past_warmup and _anc_has_prev then
|
||||
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
if lb_w > 1e-6 then
|
||||
for i = 0, n - 1 do
|
||||
_anc_out[i] = (1.0 - lb_w) * _anc_out[i] + lb_w * _anc_prev_out[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 11. RMS Servo (per-batch)
|
||||
if f_rms then
|
||||
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
local cur_rms = rms_range(_anc_out, off, NPB)
|
||||
if cur_rms > rms_target then
|
||||
local servo_rms = cur_rms + rms_gain * (rms_target - cur_rms)
|
||||
local scale = servo_rms / cur_rms
|
||||
for i = off, off + NPB - 1 do _anc_out[i] = _anc_out[i] * scale end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 12. Safety Clamp + NaN Guard
|
||||
if has_nan_inf(_anc_out, n) then
|
||||
for i = 0, n - 1 do _anc_out[i] = _anc_fallback[i] end
|
||||
end
|
||||
for i = 0, n - 1 do _anc_out[i] = clamp(_anc_out[i], -sclamp, sclamp) end
|
||||
|
||||
-- Return candidate (state feedback happens in main loop AFTER blend)
|
||||
local result = {}
|
||||
for i = 0, n - 1 do result[i] = _anc_out[i] end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Feed blended x_final back into anchor's state so its memory believes
|
||||
-- the blended trajectory is what happened
|
||||
local function anchor_state_feedback(x_final, step_idx, past_warmup, n)
|
||||
if past_warmup then
|
||||
for i = 0, n - 1 do _anc_prev_out[i] = x_final[i] end
|
||||
_anc_has_prev = true
|
||||
-- Ring buffer push
|
||||
for i = 0, n - 1 do _anc_history[_anc_hist_head][i] = x_final[i] end
|
||||
_anc_hist_head = _anc_hist_head + 1
|
||||
if _anc_hist_head > 3 then _anc_hist_head = 1 end
|
||||
if _anc_hist_count < 3 then _anc_hist_count = _anc_hist_count + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- CONFLUENCE BLEND
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function compute_effective_mix(user_mix, disagreement, inertia_mag, inertia_influence,
|
||||
gate_low, gate_high, mode, damp_str, chaos_str, t_frac)
|
||||
local base = user_mix / 100.0
|
||||
local gate = smoothstep(inertia_mag, gate_low, gate_high)
|
||||
local gated_low = base * 0.3
|
||||
local gated_full = base
|
||||
local gate_mixed = gated_low * (1 - gate) + gated_full * gate
|
||||
local gated = base * (1 - inertia_influence) + gate_mixed * inertia_influence
|
||||
|
||||
local function damp_term()
|
||||
return gated * (1 - disagreement * damp_str)
|
||||
end
|
||||
local function amplify_term()
|
||||
local push = disagreement * chaos_str
|
||||
local sign = (gated >= 0.5) and 1.0 or -1.0
|
||||
return clamp(gated + push * sign, 0, 1)
|
||||
end
|
||||
|
||||
local effective
|
||||
if mode == "damp" then
|
||||
effective = damp_term()
|
||||
elseif mode == "amplify" then
|
||||
effective = amplify_term()
|
||||
else -- adaptive
|
||||
effective = damp_term() * (1 - t_frac) + amplify_term() * t_frac
|
||||
end
|
||||
return clamp(effective, 0, 1)
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- SAMPLE -- full loop
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
local p = params or {}
|
||||
|
||||
local mix_amount = num_param(p, "mix_amount", 50)
|
||||
local disagreement_mode = p.disagreement_mode or "adaptive"
|
||||
local damp_str = num_param(p, "damp_strength", 0.4)
|
||||
local chaos_str = num_param(p, "chaos_strength", 0.3)
|
||||
local inertia_influence = num_param(p, "inertia_influence", 0.7)
|
||||
local gate_low = num_param(p, "inertia_gate_low", 0.15)
|
||||
local gate_high = num_param(p, "inertia_gate_high", 0.6)
|
||||
local stiffness_thr = num_param(p, "stiffness_threshold", 0.15)
|
||||
local rk_order = p.rk_order or "auto"
|
||||
local depth_max = math.floor(num_param(p, "cache_depth", 5))
|
||||
local lb_lambda_storm = num_param(p, "look_back_lambda_storm", 0.15)
|
||||
local lb_snr_storm = num_param(p, "look_back_snr_power_storm", 1.5)
|
||||
local warmup = math.floor(num_param(p, "warmup_steps", 2))
|
||||
local pb_lb_lambda = num_param(p, "post_blend_lookback", 0.0)
|
||||
local pb_lb_snr = num_param(p, "post_blend_snr_power", 1.0)
|
||||
local spec_guard = num_param(p, "spectral_guard", 0.4)
|
||||
local late_damp_at = num_param(p, "late_damp_override", 0.7)
|
||||
local eta = num_param(p, "eta", 0.0)
|
||||
local seed = math.floor(num_param(p, "seed", 42))
|
||||
local sclamp = num_param(p, "safety_clamp", 2.5)
|
||||
local verbose = bool_param(p, "verbose", false)
|
||||
local rw = num_param(p, "relational_weight", 0.0)
|
||||
local rw_sig_pow = num_param(p, "relational_sigma_power", 1.0)
|
||||
|
||||
local ns = #schedule
|
||||
-- Engine schedule has NO trailing 0 (fix ported from 46c081e): iterate all ns
|
||||
-- entries so the last iteration gets sigma_next = 0.0 and the terminal branch
|
||||
-- performs the final x0 projection. With ns - 1 that branch is dead code and
|
||||
-- the output keeps ~final-sigma noise.
|
||||
local n_steps = ns
|
||||
if n_steps < 1 then return end
|
||||
|
||||
-- Batch routing: engine exposes batch_n and n_per as globals
|
||||
local B = (batch_n and batch_n > 0) and batch_n or 1
|
||||
local NPB = (n_per and n_per > 0) and n_per or n
|
||||
if B * NPB ~= n then B = 1; NPB = n end
|
||||
|
||||
-- Reset both sub-solver states
|
||||
local v_cache = {}
|
||||
local baseline = { sum = 0.0, count = 0 }
|
||||
local hyst = 0.05
|
||||
local ema_a = 0.3
|
||||
local n_calib = math.max(2, math.min(5, math.floor(n_steps * 0.12)))
|
||||
|
||||
reset_anchor_state(n)
|
||||
|
||||
local sigma_max = schedule[1]
|
||||
local x = fa_to_tbl(xt, n)
|
||||
|
||||
-- STORM look-back state
|
||||
local storm_lb_prev = nil
|
||||
local lb_storm_enabled = (lb_lambda_storm > 0)
|
||||
|
||||
-- Post-blend look-back state
|
||||
local pb_prev = nil
|
||||
local pb_enabled = (pb_lb_lambda > 0)
|
||||
|
||||
if verbose then
|
||||
print(string.format("[CONFLUENCE V4] Schedule: %d steps | B=%d NPB=%d | Mix: %d | Mode: %s | RK: %s",
|
||||
n_steps, B, NPB, mix_amount, disagreement_mode, tostring(rk_order)))
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
local step_idx = i - 1
|
||||
|
||||
-- Terminal step: plain Euler, no blend
|
||||
if sigma_next == 0.0 then
|
||||
tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_final = fa_to_tbl(vt_buf, n)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
if verbose then print(string.format("[CONFLUENCE] Step %02d: TERMINAL (Euler)", step_idx)) end
|
||||
break
|
||||
end
|
||||
|
||||
-- Single model call, shared by both candidates
|
||||
tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_curr = fa_to_tbl(vt_buf, n)
|
||||
|
||||
-- Relational decomposition
|
||||
if rw > 0 then
|
||||
local sr = clamp(sigma_curr / math.max(sigma_max, EPSILON), 0.0, 1.0)
|
||||
C.apply_relational(v_curr, n, B, NPB, sr, sigma_max,
|
||||
rw, rw_sig_pow, false, 0.85, x)
|
||||
end
|
||||
|
||||
-- Save pre-step x for STORM look-back
|
||||
local x_before_storm = nil
|
||||
if lb_storm_enabled then x_before_storm = vec_clone(x, n) end
|
||||
|
||||
-- ── CANDIDATE A: STORM ──────────────────────────────────────────
|
||||
local stiff, cos_sim_out
|
||||
if #v_cache >= 1 then
|
||||
stiff, baseline, cos_sim_out = compute_stiffness(
|
||||
v_curr, v_cache, step_idx, baseline, stiffness_thr, ema_a, n_calib, n)
|
||||
else
|
||||
stiff, cos_sim_out = true, nil
|
||||
end
|
||||
|
||||
-- Hysteresis
|
||||
local prev_mode = baseline.prev_mode or "STORK"
|
||||
if prev_mode == "DPM++" and not stiff then
|
||||
if (baseline.last_ratio or 0) > (baseline.last_threshold or stiffness_thr) + hyst then
|
||||
stiff = true
|
||||
end
|
||||
end
|
||||
|
||||
local x_next_storm, actual_order, storm_mode
|
||||
if stiff then
|
||||
x_next_storm, actual_order = stork_step(v_cache, x, sigma_curr, sigma_next, v_curr, rk_order, n)
|
||||
storm_mode = "STORK"
|
||||
else
|
||||
x_next_storm = dpmpp3m_step(v_cache, x, sigma_curr, sigma_next, v_curr, n)
|
||||
storm_mode = "DPM++"
|
||||
actual_order = 3
|
||||
end
|
||||
|
||||
-- STORM NaN guard
|
||||
if has_nan_inf(x_next_storm, n) then
|
||||
local dt = sigma_next - sigma_curr
|
||||
x_next_storm = {}
|
||||
for j = 0, n - 1 do x_next_storm[j] = x[j] + dt * v_curr[j] end
|
||||
v_cache = {}
|
||||
actual_order = 1
|
||||
end
|
||||
|
||||
-- STORM look-back (its own, independent of anchor's)
|
||||
if lb_storm_enabled then
|
||||
x_next_storm = storm_look_back(x_next_storm, storm_lb_prev, sigma_curr, sigma_max, lb_lambda_storm, lb_snr_storm, n)
|
||||
storm_lb_prev = x_before_storm
|
||||
end
|
||||
|
||||
baseline.prev_mode = storm_mode
|
||||
|
||||
-- Update STORM v_cache (stores velocity, not latent -- no desync)
|
||||
table.insert(v_cache, { v = v_curr, sigma = sigma_curr })
|
||||
while #v_cache > depth_max do table.remove(v_cache, 1) end
|
||||
|
||||
-- ── CANDIDATE B: ANCHOR ─────────────────────────────────────────
|
||||
local x_next_anchor = anchor_candidate(x, v_curr, sigma_curr, sigma_next, step_idx, n, p, B, NPB)
|
||||
|
||||
-- ── DISAGREEMENT + INERTIA ──────────────────────────────────────
|
||||
local agreement = cosine_sim(x_next_storm, x_next_anchor, n)
|
||||
local disagreement = 1.0 - agreement
|
||||
local mag_ratio = vec_norm(x_next_anchor, n) / (vec_norm(x_next_storm, n) + EPSILON)
|
||||
|
||||
local inertia_mag = vec_norm(_anc_vel_old, n) / (vec_norm(v_curr, n) + EPSILON)
|
||||
inertia_mag = clamp(inertia_mag, 0, 1.5)
|
||||
|
||||
local t_frac = step_idx / math.max(n_steps - 1, 1)
|
||||
local past_warmup = (step_idx >= warmup)
|
||||
|
||||
-- Late damp override: force damp mode past late_damp_at fraction
|
||||
local active_mode = disagreement_mode
|
||||
if active_mode == "adaptive" and late_damp_at > 0 and t_frac >= late_damp_at then
|
||||
active_mode = "damp"
|
||||
end
|
||||
|
||||
local effective_mix = compute_effective_mix(
|
||||
mix_amount, disagreement, inertia_mag, inertia_influence,
|
||||
gate_low, gate_high, active_mode, damp_str, chaos_str, t_frac)
|
||||
|
||||
-- ── BLEND (with spectral guard) ──────────────────────────────────
|
||||
local x_final = {}
|
||||
|
||||
if spec_guard > 0 and disagreement > 0.01 then
|
||||
-- Frequency-aware blend: attenuate the blend delta in upper bands
|
||||
-- proportional to disagreement. Per-batch band assignment.
|
||||
local bsize = math.floor(NPB / 4)
|
||||
local atten = disagreement * spec_guard
|
||||
for j = 0, n - 1 do
|
||||
local local_idx = j % NPB
|
||||
local band = math.floor(local_idx / bsize)
|
||||
if band > 3 then band = 3 end
|
||||
-- band 0 (low) = no attenuation, band 3 (high) = full attenuation
|
||||
local band_atten = (band / 3.0) * atten
|
||||
local local_mix = effective_mix * (1.0 - clamp(band_atten, 0.0, 0.8))
|
||||
x_final[j] = (1.0 - local_mix) * x_next_storm[j] + local_mix * x_next_anchor[j]
|
||||
end
|
||||
else
|
||||
for j = 0, n - 1 do
|
||||
x_final[j] = (1.0 - effective_mix) * x_next_storm[j] + effective_mix * x_next_anchor[j]
|
||||
end
|
||||
end
|
||||
|
||||
-- Post-blend NaN guard
|
||||
if has_nan_inf(x_final, n) then
|
||||
if verbose then print(string.format("[CONFLUENCE] NaN post-blend step %d, using STORM", step_idx)) end
|
||||
for j = 0, n - 1 do x_final[j] = x_next_storm[j] end
|
||||
end
|
||||
for j = 0, n - 1 do x_final[j] = clamp(x_final[j], -sclamp, sclamp) end
|
||||
|
||||
-- ── POST-BLEND LOOK-BACK ─────────────────────────────────────────
|
||||
-- SNR-adaptive EMA on x_final itself. Covers the blend seam that
|
||||
-- neither sub-solver's own look-back touches.
|
||||
if pb_enabled and pb_prev ~= nil then
|
||||
local ratio = clamp(sigma_curr / math.max(sigma_max, EPSILON), 0.0, 1.0)
|
||||
local pb_w = pb_lb_lambda * (ratio ^ pb_lb_snr)
|
||||
if pb_w > 1e-6 then
|
||||
for j = 0, n - 1 do
|
||||
x_final[j] = (1.0 - pb_w) * x_final[j] + pb_w * pb_prev[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
if pb_enabled then pb_prev = vec_clone(x_final, n) end
|
||||
|
||||
-- ── STATE FEEDBACK ───────────────────────────────────────────────
|
||||
-- Anchor gets the blended result, not its own unblended candidate
|
||||
anchor_state_feedback(x_final, step_idx, past_warmup, n)
|
||||
|
||||
-- ── SDE NOISE (post-blend, same convention as anchor) ────────────
|
||||
if eta > 0.0 and sigma_next > EPSILON then
|
||||
local rng = make_rng(seed + step_idx * 7919)
|
||||
local scale = sigma_next * eta
|
||||
for j = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
x_final[j] = x_final[j] + normal(u1, u2) * scale
|
||||
end
|
||||
end
|
||||
|
||||
-- ── VERBOSE ──────────────────────────────────────────────────────
|
||||
if verbose then
|
||||
print(string.format(
|
||||
"[CONFLUENCE] step %02d %-5s RK%d | agree=%.3f mag=%.3f inertia=%.3f mix=%d->%.3f mode=%s t=%.2f",
|
||||
step_idx, storm_mode, actual_order, agreement, mag_ratio,
|
||||
inertia_mag, mix_amount, effective_mix, active_mode, t_frac))
|
||||
end
|
||||
|
||||
x = x_final
|
||||
tbl_to_fa(x, xt, n)
|
||||
tbl_to_fa(v_curr, vt_buf, n)
|
||||
|
||||
if on_step(step_idx, sigma_curr, sigma_next) then return end
|
||||
x = fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
tbl_to_fa(x, xt, n)
|
||||
end
|
||||
@@ -0,0 +1,249 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Dual-Time V2 -- Inner Convergence Loop Sampler
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- Zero-NFE iterative refinement using cached (v, sigma, x) tuples.
|
||||
-- Inverse-distance + sigma-proximity velocity interpolation. Sigma-adaptive
|
||||
-- inner blend protects early vocal separation. owns_loop = true. Single NFE.
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- ── VELOCITY INTERPOLATION (per-batch) ──────────────────────────────────────
|
||||
|
||||
local function interpolate_velocity_batch(x_cand, off, cnt, cache, cache_len, sigma_curr, sigma_wt)
|
||||
local v_interp = {}
|
||||
for i = 0, cnt - 1 do v_interp[i] = 0.0 end
|
||||
local total_weight = 0.0
|
||||
|
||||
for k = 1, cache_len do
|
||||
local entry = cache[k]
|
||||
local dist_sq = 0.0
|
||||
for i = 0, cnt - 1 do
|
||||
local d = x_cand[off + i] - entry.x[off + i]
|
||||
dist_sq = dist_sq + d * d
|
||||
end
|
||||
local pos_dist = math.sqrt(dist_sq / math.max(cnt, 1) + C.EPSILON)
|
||||
local sig_factor = 1.0 / (1.0 + sigma_wt * math.abs(sigma_curr - entry.sigma))
|
||||
local w = sig_factor / (pos_dist + C.EPSILON)
|
||||
total_weight = total_weight + w
|
||||
for i = 0, cnt - 1 do v_interp[i] = v_interp[i] + w * entry.v[off + i] end
|
||||
end
|
||||
|
||||
if total_weight > C.EPSILON then
|
||||
local inv_w = 1.0 / total_weight
|
||||
for i = 0, cnt - 1 do v_interp[i] = v_interp[i] * inv_w end
|
||||
end
|
||||
return v_interp
|
||||
end
|
||||
|
||||
-- ── INNER CONVERGENCE LOOP ──────────────────────────────────────────────────
|
||||
|
||||
local function inner_loop(x_start, x_euler, v_curr, dt, n, B, NPB,
|
||||
cache, cache_len, sigma_curr, sigma_wt,
|
||||
max_inner, conv_thresh, relaxation)
|
||||
local x_cand = C.vec_clone(x_euler, n)
|
||||
local init_resid, final_resid = 0.0, 0.0
|
||||
local converged = false
|
||||
local iters_used = 0
|
||||
|
||||
for k = 1, max_inner do
|
||||
iters_used = k
|
||||
|
||||
local v_interp_full = C.vec_clone(v_curr, n)
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
local v_batch = interpolate_velocity_batch(
|
||||
x_cand, off, NPB, cache, cache_len, sigma_curr, sigma_wt)
|
||||
for j = 0, NPB - 1 do v_interp_full[off + j] = v_batch[j] end
|
||||
end
|
||||
|
||||
local x_new = {}
|
||||
for j = 0, n - 1 do x_new[j] = x_start[j] + dt * v_interp_full[j] end
|
||||
|
||||
local corr_rms = 0.0
|
||||
for j = 0, n - 1 do
|
||||
local c = x_new[j] - x_cand[j]
|
||||
corr_rms = corr_rms + c * c
|
||||
end
|
||||
corr_rms = math.sqrt(corr_rms / math.max(n, 1))
|
||||
|
||||
if k == 1 then init_resid = corr_rms end
|
||||
final_resid = corr_rms
|
||||
|
||||
for j = 0, n - 1 do
|
||||
x_cand[j] = x_cand[j] + relaxation * (x_new[j] - x_cand[j])
|
||||
end
|
||||
|
||||
if C.has_nan_inf(x_cand, n) then
|
||||
for j = 0, n - 1 do x_cand[j] = x_euler[j] end
|
||||
break
|
||||
end
|
||||
|
||||
if corr_rms < conv_thresh then converged = true; break end
|
||||
end
|
||||
|
||||
return x_cand, iters_used, converged, init_resid, final_resid
|
||||
end
|
||||
|
||||
-- ── SOLVER DEFINITION ───────────────────────────────────────────────────────
|
||||
|
||||
solver = {
|
||||
name = "md_dual_time_v2",
|
||||
display = "MD Dual-Time V2",
|
||||
description = "Inner convergence loop sampler. Zero-NFE velocity history interpolation. Sigma-adaptive inner blend. Batch-aware, shared anchor stack.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
{ key = "max_inner", type = "slider", label = "Max Inner Iterations",
|
||||
default = 3, min = 1, max = 10, step = 1,
|
||||
hint = "Pseudo-time iterations per step." },
|
||||
{ key = "cache_depth", type = "slider", label = "Cache Depth",
|
||||
default = 6, min = 2, max = 12, step = 1,
|
||||
hint = "Number of (v, sigma, x) tuples stored." },
|
||||
{ key = "convergence_threshold", type = "slider", label = "Convergence Threshold",
|
||||
default = 0.005, min = 0.0005, max = 0.1, step = 0.0005,
|
||||
hint = "Per-element RMS for early exit." },
|
||||
{ key = "sigma_weight", type = "slider", label = "Sigma Proximity Weight",
|
||||
default = 4.0, min = 0.0, max = 8.0, step = 0.25,
|
||||
hint = "Sigma proximity influence. Higher = less phase ghosting." },
|
||||
{ key = "relaxation", type = "slider", label = "Relaxation Factor",
|
||||
default = 0.45, min = 0.1, max = 1.0, step = 0.05,
|
||||
hint = "Inner loop step size. Lower = less phase ghosting." },
|
||||
{ key = "sigma_gate", type = "slider", label = "Sigma Gate",
|
||||
default = 0.9, min = 0.5, max = 1.0, step = 0.05,
|
||||
hint = "Sigma fraction above which inner loop is disabled." },
|
||||
{ key = "inner_blend", type = "slider", label = "Inner Blend",
|
||||
default = 0.4, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Max Euler/converged mix. Sigma-adaptive: near-zero early, ramps quadratically." },
|
||||
},
|
||||
}
|
||||
|
||||
C.append_common_params(solver.params)
|
||||
|
||||
-- ── SAMPLE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
local p = params or {}
|
||||
local B, NPB = C.get_batch_routing(n)
|
||||
|
||||
local max_inner = math.floor(C.num_param(p, "max_inner", 3))
|
||||
local cache_depth = math.floor(C.num_param(p, "cache_depth", 6))
|
||||
local conv_thresh = C.num_param(p, "convergence_threshold", 0.005)
|
||||
local sigma_wt = C.num_param(p, "sigma_weight", 4.0)
|
||||
local relaxation = C.num_param(p, "relaxation", 0.45)
|
||||
local sigma_gate = C.num_param(p, "sigma_gate", 0.9)
|
||||
local inner_blend = C.num_param(p, "inner_blend", 0.4)
|
||||
local opts = C.read_common_opts(p)
|
||||
local state = C.new_state()
|
||||
|
||||
-- Engine schedule has NO trailing 0 (fix ported from 46c081e): iterate all ns
|
||||
-- entries so the last iteration gets sigma_next = 0.0 and the terminal branch
|
||||
-- performs the final x0 projection. With ns - 1 that branch is dead code and
|
||||
-- the output keeps ~final-sigma noise.
|
||||
local ns, n_steps = #schedule, #schedule
|
||||
if n_steps < 1 then return end
|
||||
|
||||
local sigma_max = schedule[1]
|
||||
local cache, cache_len, cache_pos, cache_max = {}, 0, 0, cache_depth
|
||||
for k = 1, cache_depth do cache[k] = nil end
|
||||
|
||||
local x = C.fa_to_tbl(xt, n)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[DUAL-TIME V2] Schedule: %d steps | B=%d NPB=%d | inner=%d cache=%d",
|
||||
n_steps, B, NPB, max_inner, cache_depth))
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
local step_idx = i - 1
|
||||
local sigma_ratio = C.clamp(sigma_curr / math.max(sigma_max, C.EPSILON), 0.0, 1.0)
|
||||
|
||||
if sigma_next == 0.0 then
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_final = C.fa_to_tbl(vt_buf, n)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
break
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_curr = C.fa_to_tbl(vt_buf, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
|
||||
-- Relational decomposition
|
||||
if opts.rw > 0 then
|
||||
C.apply_relational(v_curr, n, B, NPB, sigma_ratio, sigma_max,
|
||||
opts.rw, opts.rw_sigma_pow, opts.drift_on, opts.drift_thr, x)
|
||||
end
|
||||
|
||||
local x_euler = {}
|
||||
for j = 0, n - 1 do x_euler[j] = x[j] + dt * v_curr[j] end
|
||||
|
||||
local x_new = x_euler
|
||||
local iters_used, converged, init_resid, final_resid = 0, false, 0.0, 0.0
|
||||
|
||||
if cache_len >= 2 and sigma_ratio < sigma_gate then
|
||||
local cache_ordered = {}
|
||||
for k = 1, cache_len do
|
||||
local idx = ((cache_pos - cache_len + k - 1) % cache_max) + 1
|
||||
cache_ordered[k] = cache[idx]
|
||||
end
|
||||
|
||||
x_new, iters_used, converged, init_resid, final_resid = inner_loop(
|
||||
x, x_euler, v_curr, dt, n, B, NPB,
|
||||
cache_ordered, cache_len, sigma_curr, sigma_wt,
|
||||
max_inner, conv_thresh, relaxation)
|
||||
|
||||
-- Sigma-adaptive inner blend
|
||||
local blend_ramp = (1.0 - sigma_ratio) * (1.0 - sigma_ratio)
|
||||
local eff_blend = inner_blend * blend_ramp
|
||||
if eff_blend > 1e-6 and eff_blend < 1.0 - 1e-6 then
|
||||
for j = 0, n - 1 do
|
||||
x_new[j] = (1.0 - eff_blend) * x_euler[j] + eff_blend * x_new[j]
|
||||
end
|
||||
elseif eff_blend <= 1e-6 then
|
||||
for j = 0, n - 1 do x_new[j] = x_euler[j] end
|
||||
end
|
||||
end
|
||||
|
||||
-- Cache push
|
||||
cache_pos = (cache_pos % cache_max) + 1
|
||||
cache[cache_pos] = { v = C.vec_clone(v_curr, n), sigma = sigma_curr, x = C.vec_clone(x, n) }
|
||||
if cache_len < cache_max then cache_len = cache_len + 1 end
|
||||
|
||||
if C.has_nan_inf(x_new, n) then
|
||||
for j = 0, n - 1 do x_new[j] = x_euler[j] end
|
||||
end
|
||||
|
||||
opts.sigma_next = sigma_next
|
||||
opts.step_idx = step_idx
|
||||
C.post_advance(x_new, n, B, NPB, sigma_ratio, opts, state)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[DUAL-TIME V2] step %02d | inner=%d/%d %s | resid %.5f->%.5f | rms=%.3f",
|
||||
step_idx, iters_used, max_inner,
|
||||
converged and "CONV" or (iters_used > 0 and "max" or "skip"),
|
||||
init_resid, final_resid, C.rms(x_new, n)))
|
||||
end
|
||||
|
||||
x = x_new
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
C.tbl_to_fa(v_curr, vt_buf, n)
|
||||
if on_step(step_idx, sigma_curr, sigma_next) then return end
|
||||
x = C.fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
end
|
||||
@@ -0,0 +1,251 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Eigenflow V1 -- PCA Trajectory Filtering Sampler
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- PCA trajectory filtering via power iteration on velocity history window.
|
||||
-- Separates dominant denoising direction from oscillatory noise.
|
||||
-- Euler advance with filtered velocity. owns_loop = true. Single NFE.
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- ── POWER ITERATION (per-batch) ─────────────────────────────────────────────
|
||||
|
||||
local function power_iteration_batch(window, win_len, off, cnt, M, iters)
|
||||
local eigvecs, eigvals = {}, {}
|
||||
|
||||
for m = 1, M do
|
||||
local q = {}
|
||||
for i = 0, cnt - 1 do q[i] = window[1][off + i] end
|
||||
|
||||
for prev = 1, m - 1 do
|
||||
local d = 0.0
|
||||
for i = 0, cnt - 1 do d = d + q[i] * eigvecs[prev][i] end
|
||||
for i = 0, cnt - 1 do q[i] = q[i] - d * eigvecs[prev][i] end
|
||||
end
|
||||
|
||||
for _iter = 1, iters do
|
||||
local Cq = {}
|
||||
for i = 0, cnt - 1 do Cq[i] = 0.0 end
|
||||
|
||||
for k = 1, win_len do
|
||||
local dot = 0.0
|
||||
for i = 0, cnt - 1 do dot = dot + window[k][off + i] * q[i] end
|
||||
local scale = dot / win_len
|
||||
for i = 0, cnt - 1 do Cq[i] = Cq[i] + window[k][off + i] * scale end
|
||||
end
|
||||
|
||||
for prev = 1, m - 1 do
|
||||
local d = 0.0
|
||||
for i = 0, cnt - 1 do d = d + Cq[i] * eigvecs[prev][i] end
|
||||
for i = 0, cnt - 1 do Cq[i] = Cq[i] - d * eigvecs[prev][i] end
|
||||
end
|
||||
|
||||
local nrm = 0.0
|
||||
for i = 0, cnt - 1 do nrm = nrm + Cq[i] * Cq[i] end
|
||||
nrm = math.sqrt(nrm + C.EPSILON)
|
||||
for i = 0, cnt - 1 do q[i] = Cq[i] / nrm end
|
||||
end
|
||||
|
||||
local lam = 0.0
|
||||
for k = 1, win_len do
|
||||
local dot = 0.0
|
||||
for i = 0, cnt - 1 do dot = dot + window[k][off + i] * q[i] end
|
||||
lam = lam + dot * dot
|
||||
end
|
||||
eigvecs[m] = q
|
||||
eigvals[m] = lam / win_len
|
||||
end
|
||||
|
||||
return eigvecs, eigvals
|
||||
end
|
||||
|
||||
local function filter_velocity_batch(v_curr, off, cnt, eigvecs, M, ratio)
|
||||
local projections = {}
|
||||
for m = 1, M do
|
||||
local dot = 0.0
|
||||
for i = 0, cnt - 1 do dot = dot + v_curr[off + i] * eigvecs[m][i] end
|
||||
projections[m] = dot
|
||||
end
|
||||
|
||||
local filtered = {}
|
||||
for i = 0, cnt - 1 do
|
||||
local dominant = 0.0
|
||||
for m = 1, M do dominant = dominant + projections[m] * eigvecs[m][i] end
|
||||
filtered[i] = dominant + ratio * (v_curr[off + i] - dominant)
|
||||
end
|
||||
return filtered
|
||||
end
|
||||
|
||||
-- ── SOLVER DEFINITION ───────────────────────────────────────────────────────
|
||||
|
||||
solver = {
|
||||
name = "md_eigenflow_v1",
|
||||
display = "MD Eigenflow V1",
|
||||
description = "PCA trajectory filtering sampler. Power iteration on velocity history, batch-aware, shared anchor stack.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
{ key = "window_size", type = "slider", label = "Velocity Window Size",
|
||||
default = 6, min = 3, max = 12, step = 1,
|
||||
hint = "Velocity snapshots in sliding window." },
|
||||
{ key = "num_modes", type = "slider", label = "Principal Modes",
|
||||
default = 2, min = 1, max = 4, step = 1,
|
||||
hint = "Dominant eigenvectors to keep. 1 = aggressive, 3+ = conservative." },
|
||||
{ key = "power_iterations", type = "slider", label = "Power Iterations",
|
||||
default = 4, min = 2, max = 8, step = 1,
|
||||
hint = "Convergence iterations for power method." },
|
||||
{ key = "eigenflow_ratio", type = "slider", label = "Eigenflow Ratio",
|
||||
default = 0.3, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Residual to keep. 0 = pure dominant mode. 1 = passthrough (Euler)." },
|
||||
{ key = "sigma_warmup", type = "slider", label = "Sigma Warmup",
|
||||
default = 0.85, min = 0.5, max = 1.0, step = 0.05,
|
||||
hint = "Sigma fraction above which filtering is disabled." },
|
||||
{ key = "adaptive_ratio", type = "toggle", label = "Adaptive Ratio",
|
||||
default = true,
|
||||
hint = "Modulates eigenflow_ratio by dominance ratio." },
|
||||
},
|
||||
}
|
||||
|
||||
C.append_common_params(solver.params)
|
||||
|
||||
-- ── SAMPLE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
local p = params or {}
|
||||
local B, NPB = C.get_batch_routing(n)
|
||||
|
||||
local win_size = math.floor(C.num_param(p, "window_size", 6))
|
||||
local num_modes = math.floor(C.num_param(p, "num_modes", 2))
|
||||
local pw_iters = math.floor(C.num_param(p, "power_iterations", 4))
|
||||
local ef_ratio = C.num_param(p, "eigenflow_ratio", 0.3)
|
||||
local sigma_warmup = C.num_param(p, "sigma_warmup", 0.85)
|
||||
local f_adaptive = C.bool_param(p, "adaptive_ratio", true)
|
||||
local opts = C.read_common_opts(p)
|
||||
local state = C.new_state()
|
||||
|
||||
-- Engine schedule has NO trailing 0 (fix ported from 46c081e): iterate all ns
|
||||
-- entries so the last iteration gets sigma_next = 0.0 and the terminal branch
|
||||
-- performs the final x0 projection. With ns - 1 that branch is dead code and
|
||||
-- the output keeps ~final-sigma noise.
|
||||
local ns, n_steps = #schedule, #schedule
|
||||
if n_steps < 1 then return end
|
||||
|
||||
local sigma_max = schedule[1]
|
||||
local v_window, v_win_len, v_win_pos = {}, 0, 0
|
||||
for k = 1, win_size do v_window[k] = nil end
|
||||
|
||||
local x = C.fa_to_tbl(xt, n)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[EIGENFLOW V1] Schedule: %d steps | B=%d NPB=%d n=%d | win=%d modes=%d ratio=%.2f",
|
||||
n_steps, B, NPB, n, win_size, num_modes, ef_ratio))
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
local step_idx = i - 1
|
||||
local sigma_ratio = C.clamp(sigma_curr / math.max(sigma_max, C.EPSILON), 0.0, 1.0)
|
||||
|
||||
if sigma_next == 0.0 then
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_final = C.fa_to_tbl(vt_buf, n)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
break
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_curr = C.fa_to_tbl(vt_buf, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
|
||||
-- Relational decomposition (shape/scale cleanup on velocity)
|
||||
if opts.rw > 0 then
|
||||
C.apply_relational(v_curr, n, B, NPB, sigma_ratio, sigma_max,
|
||||
opts.rw, opts.rw_sigma_pow, opts.drift_on, opts.drift_thr, x)
|
||||
end
|
||||
|
||||
-- Push into ring buffer
|
||||
v_win_pos = (v_win_pos % win_size) + 1
|
||||
v_window[v_win_pos] = C.vec_clone(v_curr, n)
|
||||
if v_win_len < win_size then v_win_len = v_win_len + 1 end
|
||||
|
||||
-- Build ordered window
|
||||
local win_ordered = {}
|
||||
for k = 1, v_win_len do
|
||||
local idx = ((v_win_pos - v_win_len + k - 1) % win_size) + 1
|
||||
win_ordered[k] = v_window[idx]
|
||||
end
|
||||
|
||||
-- Eigenflow filtering
|
||||
local v_use = v_curr
|
||||
local filtered = false
|
||||
local dominance_ratio = 0.0
|
||||
|
||||
if v_win_len >= win_size and sigma_ratio < sigma_warmup then
|
||||
local actual_modes = math.min(num_modes, win_size - 1)
|
||||
local v_filtered = C.vec_clone(v_curr, n)
|
||||
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
local eigvecs, eigvals = power_iteration_batch(
|
||||
win_ordered, v_win_len, off, NPB, actual_modes, pw_iters)
|
||||
|
||||
if actual_modes >= 2 and eigvals[2] > C.EPSILON then
|
||||
local dr = eigvals[1] / eigvals[2]
|
||||
if dr > dominance_ratio then dominance_ratio = dr end
|
||||
end
|
||||
|
||||
local eff_ratio = ef_ratio
|
||||
if f_adaptive and dominance_ratio > 1.0 then
|
||||
eff_ratio = ef_ratio * C.clamp(1.0 / math.sqrt(dominance_ratio), 0.1, 1.0)
|
||||
end
|
||||
|
||||
local batch_filtered = filter_velocity_batch(
|
||||
v_curr, off, NPB, eigvecs, actual_modes, eff_ratio)
|
||||
for j = 0, NPB - 1 do v_filtered[off + j] = batch_filtered[j] end
|
||||
end
|
||||
|
||||
if not C.has_nan_inf(v_filtered, n) then
|
||||
v_use = v_filtered
|
||||
filtered = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Euler advance
|
||||
local x_new = {}
|
||||
for j = 0, n - 1 do x_new[j] = x[j] + dt * v_use[j] end
|
||||
|
||||
if C.has_nan_inf(x_new, n) then
|
||||
for j = 0, n - 1 do x_new[j] = x[j] + dt * v_curr[j] end
|
||||
end
|
||||
|
||||
-- Post-advance stack
|
||||
opts.sigma_next = sigma_next
|
||||
opts.step_idx = step_idx
|
||||
C.post_advance(x_new, n, B, NPB, sigma_ratio, opts, state)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[EIGENFLOW V1] step %02d | %s | dom=%.2f | rms=%.3f",
|
||||
step_idx, filtered and "FILTERED" or "raw", dominance_ratio, C.rms(x_new, n)))
|
||||
end
|
||||
|
||||
x = x_new
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
C.tbl_to_fa(v_curr, vt_buf, n)
|
||||
if on_step(step_idx, sigma_curr, sigma_next) then return end
|
||||
x = C.fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
end
|
||||
@@ -0,0 +1,286 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Hamiltonian V2 -- Energy-Conserving Momentum-Augmented Sampler
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- Euler-primary architecture with momentum correction layer, sigma-adaptive
|
||||
-- decay, confidence gating, spectral momentum, Hamiltonian energy tracking.
|
||||
-- Two look-backs (primary + post-step). owns_loop = true. Single NFE.
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- ── HAMILTONIAN ENERGY ──────────────────────────────────────────────────────
|
||||
|
||||
local function kinetic_energy(p, mass, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + p[i] * p[i] end
|
||||
return 0.5 * s / mass
|
||||
end
|
||||
|
||||
local function potential_energy(x, v_curr, sigma_ratio, n)
|
||||
return -C.vec_dot(v_curr, x, n) * sigma_ratio
|
||||
end
|
||||
|
||||
-- ── SOLVER DEFINITION ───────────────────────────────────────────────────────
|
||||
|
||||
solver = {
|
||||
name = "md_hamiltonian_v2",
|
||||
display = "MD Hamiltonian V2",
|
||||
description = "Energy-conserving momentum-augmented sampler. Euler + momentum correction, spectral weighting, Hamiltonian tracking. Shared anchor stack.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
-- Momentum
|
||||
{ key = "momentum_weight", type = "slider", label = "Momentum Weight",
|
||||
default = 0.20, min = 0.0, max = 0.8, step = 0.05,
|
||||
hint = "Momentum blend. 0 = pure Euler. Scaled by confidence gating and sigma fadeout." },
|
||||
{ key = "momentum_decay", type = "slider", label = "Momentum Decay",
|
||||
default = 0.85, min = 0.0, max = 0.99, step = 0.01,
|
||||
hint = "Step-to-step carry-over. Sigma-adaptive." },
|
||||
{ key = "momentum_ema_alpha", type = "slider", label = "Momentum EMA Alpha",
|
||||
default = 0.3, min = 0.05, max = 0.8, step = 0.05,
|
||||
hint = "Velocity absorption rate. Sigma-adaptive." },
|
||||
{ key = "mass", type = "slider", label = "Particle Mass",
|
||||
default = 1.0, min = 0.1, max = 5.0, step = 0.1,
|
||||
hint = "Inertial mass." },
|
||||
-- Energy
|
||||
{ key = "energy_tolerance", type = "slider", label = "Energy Tolerance",
|
||||
default = 0.05, min = 0.005, max = 0.5, step = 0.005,
|
||||
hint = "Hamiltonian drift before Metropolis correction." },
|
||||
{ key = "correction_strength", type = "slider", label = "Correction Strength",
|
||||
default = 0.7, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Metropolis momentum rescale. 0 = monitor only." },
|
||||
{ key = "energy_tracking", type = "select", label = "Energy Tracking",
|
||||
default = "adaptive",
|
||||
options = {
|
||||
{ value = "fixed", label = "Fixed" },
|
||||
{ value = "adaptive", label = "Adaptive" },
|
||||
{ value = "monitor", label = "Monitor Only" },
|
||||
},
|
||||
hint = "How H reference evolves." },
|
||||
-- Confidence
|
||||
{ key = "confidence_floor", type = "slider", label = "Confidence Floor",
|
||||
default = 0.2, min = 0.0, max = 0.8, step = 0.05, hint = "Min alignment for momentum." },
|
||||
{ key = "confidence_ceiling", type = "slider", label = "Confidence Ceiling",
|
||||
default = 0.7, min = 0.3, max = 1.0, step = 0.05, hint = "Full momentum alignment." },
|
||||
-- Spectral momentum
|
||||
{ key = "spectral_momentum", type = "toggle", label = "Spectral Momentum",
|
||||
default = true, hint = "Per-batch 4-band momentum weighting." },
|
||||
{ key = "spectral_hi_boost", type = "slider", label = "Spectral HF Boost",
|
||||
default = 1.4, min = 1.0, max = 4.0, step = 0.1, hint = "HF momentum multiplier." },
|
||||
{ key = "spectral_mid_cut", type = "slider", label = "Spectral Mid Cut",
|
||||
default = 0.6, min = 0.1, max = 1.0, step = 0.05, hint = "Mid momentum multiplier." },
|
||||
-- Post-step look-back (secondary)
|
||||
{ key = "post_look_back", type = "slider", label = "Post-Step Look-Back",
|
||||
default = 0.0, min = 0.0, max = 0.6, step = 0.05, hint = "Additional SNR-adaptive EMA. 0 = off (default)." },
|
||||
{ key = "post_look_back_snr", type = "slider", label = "Post-Step LB SNR Power",
|
||||
default = 1.0, min = 0.5, max = 3.0, step = 0.1, hint = "Falloff." },
|
||||
},
|
||||
}
|
||||
|
||||
C.append_common_params(solver.params)
|
||||
|
||||
-- ── SAMPLE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
local p = params or {}
|
||||
local B, NPB = C.get_batch_routing(n)
|
||||
|
||||
local mom_weight = C.num_param(p, "momentum_weight", 0.20)
|
||||
local mom_decay = C.num_param(p, "momentum_decay", 0.85)
|
||||
local mom_alpha = C.num_param(p, "momentum_ema_alpha", 0.3)
|
||||
local mass = C.num_param(p, "mass", 1.0)
|
||||
local energy_tol = C.num_param(p, "energy_tolerance", 0.05)
|
||||
local corr_str = C.num_param(p, "correction_strength", 0.7)
|
||||
local energy_mode = p.energy_tracking or "adaptive"
|
||||
local conf_floor = C.num_param(p, "confidence_floor", 0.2)
|
||||
local conf_ceil = C.num_param(p, "confidence_ceiling", 0.7)
|
||||
local f_spec_mom = C.bool_param(p, "spectral_momentum", true)
|
||||
local spec_hi = C.num_param(p, "spectral_hi_boost", 1.4)
|
||||
local spec_mid = C.num_param(p, "spectral_mid_cut", 0.6)
|
||||
local post_lb_lam = C.num_param(p, "post_look_back", 0.0)
|
||||
local post_lb_snr = C.num_param(p, "post_look_back_snr", 1.0)
|
||||
local opts = C.read_common_opts(p)
|
||||
local state = C.new_state()
|
||||
|
||||
-- Engine schedule has NO trailing 0 (fix ported from 46c081e): iterate all ns
|
||||
-- entries so the last iteration gets sigma_next = 0.0 and the terminal branch
|
||||
-- performs the final x0 projection. With ns - 1 that branch is dead code and
|
||||
-- the output keeps ~final-sigma noise.
|
||||
local ns, n_steps = #schedule, #schedule
|
||||
if n_steps < 1 then return end
|
||||
|
||||
local sigma_max = schedule[1]
|
||||
local momentum = nil
|
||||
local H_ref = nil
|
||||
local post_lb_enabled = (post_lb_lam > 0)
|
||||
|
||||
local x = C.fa_to_tbl(xt, n)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[HAMILTONIAN V2] Schedule: %d steps | B=%d NPB=%d | weight=%.2f decay=%.2f mass=%.1f",
|
||||
n_steps, B, NPB, mom_weight, mom_decay, mass))
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
local step_idx = i - 1
|
||||
local sigma_ratio = C.clamp(sigma_curr / math.max(sigma_max, C.EPSILON), 0.0, 1.0)
|
||||
|
||||
if sigma_next == 0.0 then
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_final = C.fa_to_tbl(vt_buf, n)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
break
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_curr = C.fa_to_tbl(vt_buf, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
|
||||
-- Relational decomposition
|
||||
if opts.rw > 0 then
|
||||
C.apply_relational(v_curr, n, B, NPB, sigma_ratio, sigma_max,
|
||||
opts.rw, opts.rw_sigma_pow, opts.drift_on, opts.drift_thr, x)
|
||||
end
|
||||
|
||||
-- 1. Euler advance
|
||||
local x_euler = {}
|
||||
for j = 0, n - 1 do x_euler[j] = x[j] + dt * v_curr[j] end
|
||||
|
||||
-- 2. Momentum update (sigma-adaptive)
|
||||
local decay_power = 1.0 + 2.0 * (1.0 - sigma_ratio)
|
||||
local effective_decay = mom_decay ^ decay_power
|
||||
local effective_alpha = mom_alpha + (1.0 - mom_alpha) * 0.5 * (1.0 - sigma_ratio)
|
||||
|
||||
if momentum == nil then
|
||||
momentum = {}
|
||||
for j = 0, n - 1 do momentum[j] = v_curr[j] * mass end
|
||||
else
|
||||
for j = 0, n - 1 do momentum[j] = momentum[j] * effective_decay end
|
||||
for j = 0, n - 1 do
|
||||
momentum[j] = (1.0 - effective_alpha) * momentum[j] + effective_alpha * v_curr[j] * mass
|
||||
end
|
||||
end
|
||||
|
||||
-- 3. Momentum-predicted position
|
||||
local x_mom = {}
|
||||
local inv_mass = 1.0 / mass
|
||||
for j = 0, n - 1 do x_mom[j] = x[j] + dt * momentum[j] * inv_mass end
|
||||
|
||||
-- 4. Confidence gating + sigma fadeout (linear)
|
||||
local mom_norm = C.vec_norm(momentum, n)
|
||||
local v_norm = C.vec_norm(v_curr, n)
|
||||
local alignment = 0.0
|
||||
if mom_norm > C.EPSILON and v_norm > C.EPSILON then
|
||||
alignment = C.vec_dot(momentum, v_curr, n) / (mom_norm * v_norm)
|
||||
end
|
||||
local confidence = C.smoothstep(alignment, conf_floor, conf_ceil)
|
||||
local sigma_fade = sigma_ratio
|
||||
local eff_weight = mom_weight * confidence * sigma_fade
|
||||
|
||||
-- Re-alignment when fighting
|
||||
if mom_norm > C.EPSILON and v_norm > C.EPSILON and alignment < 0.3 then
|
||||
local blend = 0.3 * (1.0 - alignment)
|
||||
for j = 0, n - 1 do
|
||||
momentum[j] = (1.0 - blend) * momentum[j] + blend * v_curr[j] * mass * math.abs(dt)
|
||||
end
|
||||
end
|
||||
|
||||
-- 5. Blend (per-batch spectral awareness)
|
||||
local x_new = {}
|
||||
if f_spec_mom and eff_weight > 1e-6 then
|
||||
local band_mults = { 1.0, spec_mid, spec_mid, spec_hi }
|
||||
local bsize = math.floor(NPB / 4)
|
||||
for j = 0, n - 1 do
|
||||
local local_idx = j % NPB
|
||||
local band = math.min(math.floor(local_idx / bsize), 3)
|
||||
local local_w = C.clamp(eff_weight * band_mults[band + 1], 0.0, 0.95)
|
||||
x_new[j] = (1.0 - local_w) * x_euler[j] + local_w * x_mom[j]
|
||||
end
|
||||
else
|
||||
for j = 0, n - 1 do
|
||||
x_new[j] = (1.0 - eff_weight) * x_euler[j] + eff_weight * x_mom[j]
|
||||
end
|
||||
end
|
||||
|
||||
if C.has_nan_inf(x_new, n) then
|
||||
for j = 0, n - 1 do x_new[j] = x_euler[j] end
|
||||
end
|
||||
|
||||
-- 6. Hamiltonian energy tracking
|
||||
local T = kinetic_energy(momentum, mass, n)
|
||||
local V = potential_energy(x_new, v_curr, sigma_ratio, n)
|
||||
local H = T + V
|
||||
local corrected = false
|
||||
|
||||
if H_ref == nil then
|
||||
H_ref = H
|
||||
else
|
||||
local rel_drift = math.abs(H - H_ref) / (math.abs(H_ref) + C.EPSILON)
|
||||
if energy_mode ~= "monitor" and rel_drift > energy_tol and corr_str > 0 then
|
||||
local T_target = H_ref - V
|
||||
if T_target < 0.01 then T_target = 0.01 end
|
||||
local scale = math.sqrt(T_target / (T + C.EPSILON))
|
||||
scale = 1.0 + corr_str * (scale - 1.0)
|
||||
scale = C.clamp(scale, 0.5, 2.0)
|
||||
for j = 0, n - 1 do momentum[j] = momentum[j] * scale end
|
||||
corrected = true
|
||||
end
|
||||
if energy_mode == "adaptive" then H_ref = 0.95 * H_ref + 0.05 * H end
|
||||
end
|
||||
|
||||
-- 7-8. Identity + tonal anchor (via commons)
|
||||
if opts.f_id_anchor then
|
||||
C.apply_identity_anchor(x_new, n, sigma_ratio, opts.anchor_sigma, opts.anchor_blend, state)
|
||||
end
|
||||
if opts.f_tonal then
|
||||
C.apply_tonal_anchor(x_new, n, B, NPB, sigma_ratio, opts.anchor_sigma, opts.tonal_str, state)
|
||||
end
|
||||
|
||||
-- 9. Primary look-back (via commons)
|
||||
if opts.f_lookback then
|
||||
C.apply_look_back(x_new, n, sigma_ratio, opts.lb_lambda, opts.lb_snr_power, state, "lb_prev")
|
||||
end
|
||||
|
||||
-- 10. RMS servo (via commons)
|
||||
if opts.f_rms then
|
||||
C.apply_rms_servo(x_new, n, B, NPB, sigma_ratio, opts.rms_tgt_min, opts.rms_tgt_max, opts.rms_gain)
|
||||
end
|
||||
|
||||
-- 11. Post-step look-back (secondary, via commons)
|
||||
if post_lb_enabled then
|
||||
C.apply_look_back(x_new, n, sigma_ratio, post_lb_lam, post_lb_snr, state, "lb2_prev")
|
||||
end
|
||||
|
||||
-- 12. SDE noise + safety clamp (via commons)
|
||||
C.apply_sde_noise(x_new, n, sigma_next, opts.eta, opts.seed, step_idx)
|
||||
C.apply_safety_clamp(x_new, n, opts.sclamp)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format(
|
||||
"[HAMILTONIAN V2] step %02d | H=%.2f %s | align=%.3f conf=%.2f ew=%.3f | rms=%.3f",
|
||||
step_idx, H, corrected and "CORR" or "ok",
|
||||
alignment, confidence, eff_weight, C.rms(x_new, n)))
|
||||
end
|
||||
|
||||
x = x_new
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
C.tbl_to_fa(v_curr, vt_buf, n)
|
||||
if on_step(step_idx, sigma_curr, sigma_next) then return end
|
||||
x = C.fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
end
|
||||
@@ -0,0 +1,280 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
-- ============================================================================
|
||||
|
||||
-- MD OmniRelational Solver v3.0 -- Barbour Best Matching + Sigma Decay + Look-Back
|
||||
-- MDMAchine | A&E Concepts 2026
|
||||
--
|
||||
-- WHAT IS NEW IN V3:
|
||||
--
|
||||
-- V1/V2 applied the same relational weight (rw) at every step regardless
|
||||
-- of where you are in the denoising schedule. This is suboptimal:
|
||||
--
|
||||
-- High sigma (early steps): structure formation phase. The latent is still
|
||||
-- mostly noise. Shape-preserving relational geometry matters most here --
|
||||
-- normalizing direction prevents any single component dominating.
|
||||
--
|
||||
-- Low sigma (late steps): detail refinement phase. The latent is close to
|
||||
-- x0. Raw velocity is more accurate. Relational re-injection here over-
|
||||
-- smooths fine detail.
|
||||
--
|
||||
-- SIGMA-ADAPTIVE RELATIONAL WEIGHT:
|
||||
-- rw_eff = rw * (t_curr / sigma_max) ^ sigma_power
|
||||
-- At t=sigma_max (first step): rw_eff = rw (full effect)
|
||||
-- At t=0 (final step): rw_eff = 0 (pure Euler)
|
||||
-- sigma_power controls the decay curve. 1.0 = linear, 2.0 = quadratic.
|
||||
--
|
||||
-- LOOK-BACK SNR SMOOTHER (arXiv:2602.09449):
|
||||
-- lambda_eff = lb_lambda * (t_curr / sigma_max) ^ lb_snr_power
|
||||
-- x_smooth = (1 - lambda_eff) * x_next + lambda_eff * x_prev
|
||||
-- Heavy at high sigma (blends structure), fades at low sigma (preserves
|
||||
-- detail). Same mechanism as STORM and PingPong. Off by default.
|
||||
--
|
||||
-- GENERATION STATE RESET:
|
||||
-- Module-level state (sigma_max, x_prev for look-back) resets on
|
||||
-- step_idx_==0 so same-size consecutive generations don't bleed.
|
||||
--
|
||||
-- DRIFT GUARD kept from V1 -- simple, clean, no hoisted buffer.
|
||||
|
||||
solver = {
|
||||
name = "md_omni_relational_V3",
|
||||
display = "MD OmniRelational V3 (Sigma Adaptive)",
|
||||
description = "Barbour Best Matching with sigma-adaptive relational weight and look-back smoother. rw fades toward pure Euler at low sigma. Proper state reset between generations.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
params = {
|
||||
{
|
||||
key = "relational_weight",
|
||||
type = "slider",
|
||||
label = "Relational Weight",
|
||||
default = 0.5,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Base relational weight at high sigma. Fades toward 0 at low sigma. 0=pure Euler always. 0.5=balanced at structure phase.",
|
||||
},
|
||||
{
|
||||
key = "sigma_power",
|
||||
type = "slider",
|
||||
label = "Sigma Decay Power",
|
||||
default = 1.0,
|
||||
min = 0.25,
|
||||
max = 4.0,
|
||||
step = 0.25,
|
||||
hint = "Controls how fast rw fades with sigma. 1.0=linear decay. 2.0=quadratic (faster fade). 0.5=slow fade. Higher = relational effect concentrated earlier.",
|
||||
},
|
||||
{
|
||||
key = "look_back_lambda",
|
||||
type = "slider",
|
||||
label = "Look-Back Lambda",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Look-back coherence smoother. 0=off. Blends current step with previous, fading out at low sigma. Suppresses trajectory shear. Start at 0.05-0.15.",
|
||||
},
|
||||
{
|
||||
key = "look_back_snr_power",
|
||||
type = "slider",
|
||||
label = "Look-Back SNR Power",
|
||||
default = 1.5,
|
||||
min = 0.5,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Controls how fast look-back fades with sigma. Higher = smoothing concentrated on early structure steps only.",
|
||||
},
|
||||
{
|
||||
key = "drift_guard",
|
||||
type = "toggle",
|
||||
label = "Drift Guard (AOS)",
|
||||
default = false,
|
||||
hint = "Project shape_vec onto orthogonal complement of x when cos_sim exceeds threshold. Prevents update reinforcing existing latent structure.",
|
||||
},
|
||||
{
|
||||
key = "drift_threshold",
|
||||
type = "slider",
|
||||
label = "Drift Threshold",
|
||||
default = 0.85,
|
||||
min = 0.1,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Cosine similarity ceiling before Gram-Schmidt projection fires. 0.85=standard. Only active when Drift Guard is on.",
|
||||
},
|
||||
{
|
||||
key = "eta",
|
||||
type = "slider",
|
||||
label = "Eta (SDE Noise)",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Ancestral noise injection. 0=deterministic ODE. Scales with t_prev.",
|
||||
},
|
||||
{
|
||||
key = "seed",
|
||||
type = "slider",
|
||||
label = "Seed",
|
||||
default = 42,
|
||||
min = 0,
|
||||
max = 999999,
|
||||
step = 1,
|
||||
hint = "RNG seed for SDE noise.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local EPSILON = 1e-8
|
||||
|
||||
-- Module state -- reset on step_idx_==0
|
||||
local _sigma_max = nil
|
||||
local _x_prev_lb = nil -- look-back previous x (before update)
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
local function make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
local function normal(u1, u2)
|
||||
return math.sqrt(-2.0 * math.log(math.max(u1, EPSILON))) * math.cos(2.0 * math.pi * u2)
|
||||
end
|
||||
|
||||
local function l2_norm(arr, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + arr[i] * arr[i] end
|
||||
return math.sqrt(s + EPSILON)
|
||||
end
|
||||
|
||||
-- Shape decomposition: returns unit direction table + L2 norm (mean_scale)
|
||||
local function decompose_shape(vt, n)
|
||||
local norm = l2_norm(vt, n)
|
||||
local inv = norm > EPSILON and (1.0 / norm) or 0.0
|
||||
local shape = {}
|
||||
for i = 0, n - 1 do shape[i] = vt[i] * inv end
|
||||
return shape, norm
|
||||
end
|
||||
|
||||
-- Gram-Schmidt drift guard: projects shape onto orthogonal complement of xt
|
||||
local function apply_drift_guard(shape, xt, n, threshold)
|
||||
local norm_x = l2_norm(xt, n)
|
||||
if norm_x < EPSILON then return shape end
|
||||
|
||||
local inv_x = 1.0 / norm_x
|
||||
local dot_sx = 0.0
|
||||
for i = 0, n - 1 do dot_sx = dot_sx + shape[i] * (xt[i] * inv_x) end
|
||||
|
||||
if math.abs(dot_sx) <= threshold then return shape end
|
||||
|
||||
local proj = {}
|
||||
for i = 0, n - 1 do
|
||||
proj[i] = shape[i] - dot_sx * (xt[i] * inv_x)
|
||||
end
|
||||
|
||||
local proj_norm = l2_norm(proj, n)
|
||||
if proj_norm < EPSILON then return shape end
|
||||
|
||||
local inv_proj = 1.0 / proj_norm
|
||||
for i = 0, n - 1 do proj[i] = proj[i] * inv_proj end
|
||||
return proj
|
||||
end
|
||||
|
||||
-- Look-back SNR smoother: blend x_curr toward x_prev, lambda fades with sigma
|
||||
local function look_back_smooth(x_curr, x_prev, t_curr, sigma_max, lb_lambda, snr_power, n)
|
||||
if x_prev == nil or lb_lambda < EPSILON then return x_curr, 0.0 end
|
||||
local ratio = math.min(t_curr / math.max(sigma_max, EPSILON), 1.0)
|
||||
local lam = lb_lambda * (ratio ^ snr_power)
|
||||
local out = {}
|
||||
for i = 0, n - 1 do
|
||||
out[i] = (1.0 - lam) * x_curr[i] + lam * x_prev[i]
|
||||
end
|
||||
return out, lam
|
||||
end
|
||||
|
||||
-- ── step() function ───────────────────────────────────────────────────────────
|
||||
|
||||
function step(xt, vt, t_curr, t_prev, n)
|
||||
local rw = clamp((params and params.relational_weight) or 0.5, 0.0, 1.0)
|
||||
local sig_power = clamp((params and params.sigma_power) or 1.0, 0.25, 4.0)
|
||||
local lb_lambda = clamp((params and params.look_back_lambda) or 0.0, 0.0, 0.5)
|
||||
local lb_snr_pow = clamp((params and params.look_back_snr_power) or 1.5, 0.5, 3.0)
|
||||
local drift_on = (params and params.drift_guard) or false
|
||||
local drift_thr = clamp((params and params.drift_threshold) or 0.85, 0.1, 1.0)
|
||||
local eta = clamp((params and params.eta) or 0.0, 0.0, 1.0)
|
||||
local seed = math.floor((params and params.seed) or 42)
|
||||
|
||||
local step_idx_ = step_index or 0
|
||||
|
||||
-- Reset state at start of each generation
|
||||
if step_idx_ == 0 then
|
||||
_sigma_max = t_curr
|
||||
_x_prev_lb = nil
|
||||
end
|
||||
if _sigma_max == nil then _sigma_max = t_curr end
|
||||
|
||||
-- Snapshot xt before update for look-back (copy to plain table)
|
||||
local x_curr_snapshot = nil
|
||||
if lb_lambda > EPSILON then
|
||||
x_curr_snapshot = {}
|
||||
for i = 0, n - 1 do x_curr_snapshot[i] = xt[i] end
|
||||
end
|
||||
|
||||
-- ── 1. Shape decomposition ────────────────────────────────────────────────
|
||||
local shape_vec, mean_scale = decompose_shape(vt, n)
|
||||
|
||||
-- ── 2. Optional drift guard ───────────────────────────────────────────────
|
||||
if drift_on and drift_thr < 1.0 then
|
||||
-- xt is a plain table in step() -- pass directly
|
||||
local xt_tbl = {}
|
||||
for i = 0, n - 1 do xt_tbl[i] = xt[i] end
|
||||
shape_vec = apply_drift_guard(shape_vec, xt_tbl, n, drift_thr)
|
||||
end
|
||||
|
||||
-- ── 3. Sigma-adaptive relational weight ───────────────────────────────────
|
||||
-- rw_eff = rw * (t_curr / sigma_max) ^ sigma_power
|
||||
-- At high sigma: rw_eff = rw (full relational). At low sigma: fades to 0.
|
||||
local sigma_ratio = math.min(t_curr / math.max(_sigma_max, EPSILON), 1.0)
|
||||
local rw_eff = rw * (sigma_ratio ^ sig_power)
|
||||
|
||||
-- ── 4. Blend + Euler update ───────────────────────────────────────────────
|
||||
local dt = t_prev - t_curr -- negative in flow-matching
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do
|
||||
local rel_i = shape_vec[i] * mean_scale
|
||||
local eff_vt_i = rw_eff * rel_i + (1.0 - rw_eff) * vt[i]
|
||||
x_next[i] = xt[i] + dt * eff_vt_i
|
||||
end
|
||||
|
||||
-- ── 5. Look-back smoother ─────────────────────────────────────────────────
|
||||
if lb_lambda > EPSILON then
|
||||
local lam
|
||||
x_next, lam = look_back_smooth(x_next, _x_prev_lb, t_curr, _sigma_max, lb_lambda, lb_snr_pow, n)
|
||||
_x_prev_lb = x_curr_snapshot
|
||||
end
|
||||
|
||||
-- ── 6. Write x_next back to xt ───────────────────────────────────────────
|
||||
for i = 0, n - 1 do xt[i] = x_next[i] end
|
||||
|
||||
-- ── 7. Optional SDE noise ─────────────────────────────────────────────────
|
||||
if eta > 0.0 and t_prev > EPSILON then
|
||||
local rng = make_rng(seed + step_idx_ * 7919)
|
||||
local scale = t_prev * eta
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
xt[i] = xt[i] + normal(u1, u2) * scale
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,360 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
-- MD PingPong Simple v1.1 — Ancestral Euler + Momentum + Look-Back Smoother
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- Port of MD_PingPong_Samplers.py (single-branch path) to HOT-Step-CPP Lua solver.
|
||||
--
|
||||
-- WHAT THIS DOES:
|
||||
-- Standard ODE solvers (Euler, Heun, STORM) advance the latent deterministically.
|
||||
-- PingPong injects ancestral (stochastic) noise at each step — the "ping" is
|
||||
-- the clean denoising step, the "pong" is the noise re-injection that keeps
|
||||
-- the trajectory alive and stochastic.
|
||||
--
|
||||
-- CORE STEP MATH:
|
||||
-- dt = t_prev - t_curr (negative — stepping down)
|
||||
-- x_euler = xt + dt * vt (standard Euler, matches STORM/OmniRelational)
|
||||
-- x_next = x_euler + noise * |dt| * ancestral_strength
|
||||
--
|
||||
-- MOMENTUM:
|
||||
-- Latent velocity (x - x_prev) carried forward at each step. Maintains
|
||||
-- flow continuity across the ODE trajectory — reduces erratic jumps between
|
||||
-- steps, especially at low step counts.
|
||||
--
|
||||
-- NOISE COHERENCE:
|
||||
-- Blends fresh Gaussian noise with the previous step's noise at ratio
|
||||
-- noise_coherence. 0=fully fresh, 1=fully carried. Useful for temporal
|
||||
-- smoothness in audio; keep low (0-0.2) to avoid spectral smearing.
|
||||
--
|
||||
-- LOOK-BACK SNR SMOOTHER:
|
||||
-- λ(σ) = lambda_base * (σ/σ_max)^snr_power — heavy at high sigma, fades to
|
||||
-- zero at low sigma. Suppresses ODE manifold shearing and harmonic hum.
|
||||
-- Reference: arXiv:2602.09449
|
||||
--
|
||||
-- RMS SERVO:
|
||||
-- Downward-only energy ceiling that follows a smooth curve from rms_max
|
||||
-- (high sigma) to rms_min (low sigma). Domain-tunable: image latents
|
||||
-- typically sit around 0.75-0.97; audio latents around 0.3-0.7.
|
||||
-- Servo is DOWNWARD ONLY — never boosts energy, only clamps excess.
|
||||
--
|
||||
-- SOLVER API NOTE:
|
||||
-- HOT-Step-CPP passes velocity vt where dt = t_prev - t_curr is NEGATIVE
|
||||
-- (stepping from high sigma to low sigma). Euler update is:
|
||||
-- x_next = xt + dt * vt (same convention as STORM and OmniRelational)
|
||||
-- Ancestral noise is added as: noise * |dt| * strength
|
||||
-- Do NOT use xt - t_curr * vt for denoised — sign convention mismatch.
|
||||
--
|
||||
-- PARAMS:
|
||||
-- ancestral_strength — noise injection scale. 1.0=standard ancestral, 0=pure ODE
|
||||
-- noise_coherence — step-to-step noise carry. 0=fresh, 0.2=subtle temporal link
|
||||
-- momentum_strength — latent velocity carry-over. 0.15=subtle, 0.3=strong
|
||||
-- look_back_enabled — SNR smoother toggle
|
||||
-- look_back_lambda — max smoothing weight (0.55=25-step, 0.35=35-step)
|
||||
-- look_back_snr_power — falloff exponent (1.3=25-step, 1.5=35-step)
|
||||
-- rms_servo — energy ceiling toggle
|
||||
-- rms_target_min — servo floor at low sigma (audio: ~0.3, image: ~0.75)
|
||||
-- rms_target_max — servo ceiling at high sigma (audio: ~0.7, image: ~0.97)
|
||||
-- rms_servo_gain — correction aggressiveness (0.6=default, 1.0=hard snap)
|
||||
-- seed — RNG seed
|
||||
-- ============================================================================
|
||||
|
||||
solver = {
|
||||
name = "md_pingpong_simple",
|
||||
display = "MD PingPong Simple (Ancestral)",
|
||||
description = "Ancestral Euler with momentum, noise coherence, look-back SNR smoother, and domain-tunable RMS servo. Single-branch stochastic sampler. Port of MD_PingPong_Samplers v3.5.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
params = {
|
||||
{
|
||||
key = "ancestral_strength",
|
||||
type = "slider",
|
||||
label = "Ancestral Strength",
|
||||
default = 0.2,
|
||||
min = 0.0,
|
||||
max = 1.5,
|
||||
step = 0.05,
|
||||
hint = "Noise injection strength. 1.0=standard ancestral. 0=pure ODE (no noise).",
|
||||
},
|
||||
{
|
||||
key = "noise_coherence",
|
||||
type = "slider",
|
||||
label = "Noise Coherence",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Step-to-step noise correlation. 0=fresh noise each step. 0.2=subtle temporal link. Keep low for audio to avoid smearing.",
|
||||
},
|
||||
{
|
||||
key = "momentum_strength",
|
||||
type = "slider",
|
||||
label = "Momentum",
|
||||
default = 0.1,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Latent velocity carry-over. 0.1=subtle flow continuity. 0.3=strong.",
|
||||
},
|
||||
{
|
||||
key = "look_back_enabled",
|
||||
type = "toggle",
|
||||
label = "Look-Back Smoother",
|
||||
default = true,
|
||||
hint = "SNR-adaptive latent EMA. Suppresses ODE manifold shearing and harmonic hum. arXiv:2602.09449.",
|
||||
},
|
||||
{
|
||||
key = "look_back_lambda",
|
||||
type = "slider",
|
||||
label = "Look-Back Lambda",
|
||||
default = 0.55,
|
||||
min = 0.1,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Max smoothing weight. Active when Look-Back Smoother is on. 0.55=25-step, 0.35=35-step.",
|
||||
},
|
||||
{
|
||||
key = "look_back_snr_power",
|
||||
type = "slider",
|
||||
label = "SNR Power",
|
||||
default = 1.3,
|
||||
min = 0.5,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Falloff exponent. Active when Look-Back Smoother is on. Higher=smoother fade at low sigma.",
|
||||
},
|
||||
{
|
||||
key = "rms_servo",
|
||||
type = "toggle",
|
||||
label = "RMS Servo",
|
||||
default = true,
|
||||
hint = "Downward-only energy ceiling. Prevents latent energy accumulation. Off by default — tune min/max for your domain before enabling.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_min",
|
||||
type = "slider",
|
||||
label = "RMS Target Min",
|
||||
default = 1.0,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at low sigma (late steps). Active when RMS Servo is on. ACE-Step latents ~2.0 RMS. Start at 1.2-1.8.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_max",
|
||||
type = "slider",
|
||||
label = "RMS Target Max",
|
||||
default = 2.2,
|
||||
min = 0.5,
|
||||
max = 4.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at high sigma (early steps). Active when RMS Servo is on. ACE-Step latents ~2.0 RMS. Start at 2.0-2.5.",
|
||||
},
|
||||
{
|
||||
key = "rms_servo_gain",
|
||||
type = "slider",
|
||||
label = "Servo Gain",
|
||||
default = 0.75,
|
||||
min = 0.1,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Servo correction aggressiveness. Active when RMS Servo is on. 0.6=soft, 1.0=hard snap.",
|
||||
},
|
||||
{
|
||||
key = "seed",
|
||||
type = "slider",
|
||||
label = "Seed",
|
||||
default = 42,
|
||||
min = 0,
|
||||
max = 999999,
|
||||
step = 1,
|
||||
hint = "RNG seed for noise generation.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- ── State (file-level locals, reset when n changes) ──────────────────────────
|
||||
|
||||
local _prev_x = nil -- for momentum
|
||||
local _prev_noise = nil -- for noise coherence
|
||||
local _look_back_xp = nil -- for look-back smoother
|
||||
local _sigma_max = nil -- captured at step 0
|
||||
local _last_n = 0
|
||||
|
||||
-- Hoisted scratch tables — reused every step to avoid GC pressure
|
||||
-- Initialized on first step or when n changes
|
||||
local _noise_buf = {} -- reusable noise array
|
||||
local _x_next_buf = {} -- reusable output array
|
||||
local _x_copy_buf = {} -- reusable momentum copy
|
||||
|
||||
local EPSILON = 1e-8
|
||||
|
||||
-- ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
-- Seeded LCG RNG — deterministic, no dependency on math.random state
|
||||
local function make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
-- Box-Muller: two uniform [0,1] → one standard normal sample
|
||||
local function normal(u1, u2)
|
||||
return math.sqrt(-2.0 * math.log(math.max(u1, EPSILON))) * math.cos(2.0 * math.pi * u2)
|
||||
end
|
||||
|
||||
-- Array RMS
|
||||
local function rms(arr, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + arr[i] * arr[i] end
|
||||
return math.sqrt(s / n + EPSILON)
|
||||
end
|
||||
|
||||
-- ── Required step() function ──────────────────────────────────────────────────
|
||||
|
||||
function step(xt, vt, t_curr, t_prev, n)
|
||||
local step_idx_ = step_index or 0
|
||||
|
||||
-- Reset state on new generation: n change OR step 0 of any new run.
|
||||
-- Must check step_idx_==0 because same-length generations won't trigger n change,
|
||||
-- causing momentum/look-back to bleed finished audio from the previous run into
|
||||
-- the noise of the new one — explosive velocity on step 1.
|
||||
if n ~= _last_n or step_idx_ == 0 then
|
||||
_prev_x = nil
|
||||
_prev_noise = nil
|
||||
_look_back_xp = nil
|
||||
_sigma_max = nil
|
||||
_last_n = n
|
||||
-- Pre-size scratch tables for this n
|
||||
for i = 0, n - 1 do
|
||||
_noise_buf[i] = 0.0
|
||||
_x_next_buf[i] = 0.0
|
||||
_x_copy_buf[i] = 0.0
|
||||
end
|
||||
end
|
||||
|
||||
-- Read params with safe fallbacks
|
||||
local anc_strength = (params and params.ancestral_strength) or 1.0
|
||||
local noise_coh = (params and params.noise_coherence) or 0.0
|
||||
local mom_str = (params and params.momentum_strength) or 0.1
|
||||
local lb_enabled = (params and params.look_back_enabled) or false
|
||||
local lb_lambda = (params and params.look_back_lambda) or 0.55
|
||||
local lb_snr_power = (params and params.look_back_snr_power) or 1.3
|
||||
local rms_servo_on = (params and params.rms_servo) or true
|
||||
local rms_tgt_min = (params and params.rms_target_min) or 1.2
|
||||
local rms_tgt_max = (params and params.rms_target_max) or 2.2
|
||||
local rms_servo_gain = (params and params.rms_servo_gain) or 0.6
|
||||
local seed = math.floor((params and params.seed) or 42)
|
||||
|
||||
-- Capture sigma_max on first step for ratio computation
|
||||
if _sigma_max == nil then _sigma_max = t_curr end
|
||||
local sigma_max = _sigma_max
|
||||
local sigma_ratio = clamp(t_curr / math.max(sigma_max, EPSILON), 0.0, 1.0)
|
||||
|
||||
-- dt = t_prev - t_curr. In flow-matching, t steps DOWN (1→0),
|
||||
-- HOT-Step API: t_curr=high sigma, t_prev=lower target. t_curr > t_prev. dt=t_prev-t_curr is NEGATIVE.
|
||||
local dt = t_prev - t_curr
|
||||
|
||||
-- Save current xt for momentum (reuse hoisted buffer)
|
||||
for i = 0, n - 1 do _x_copy_buf[i] = xt[i] end
|
||||
|
||||
-- ── NOISE GENERATION ──────────────────────────────────────────────────────
|
||||
-- Write into hoisted buffer — no table allocation per step
|
||||
local rng = make_rng(seed + step_idx_ * 7919)
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
_noise_buf[i] = normal(u1, u2)
|
||||
end
|
||||
|
||||
-- Noise coherence: blend with carried noise from previous step
|
||||
if noise_coh > 0.0 and _prev_noise ~= nil then
|
||||
for i = 0, n - 1 do
|
||||
_noise_buf[i] = _noise_buf[i] * (1.0 - noise_coh) + _prev_noise[i] * noise_coh
|
||||
end
|
||||
end
|
||||
-- Store for next step — reuse _prev_noise table if same size
|
||||
if _prev_noise == nil then _prev_noise = {} end
|
||||
for i = 0, n - 1 do _prev_noise[i] = _noise_buf[i] end
|
||||
|
||||
-- ── ANCESTRAL STEP ────────────────────────────────────────────────────────
|
||||
-- Variance-preserving SDE noise for flow matching:
|
||||
-- noise_scale = sqrt(t_prev^2 - t_curr^2) * anc_strength
|
||||
-- t_curr > t_prev, so t_curr^2 - t_prev^2 > 0. Confirmed numerically.
|
||||
local noise_scale = math.sqrt(math.max(t_curr * t_curr - t_prev * t_prev, 0.0)) * anc_strength
|
||||
if noise_scale > EPSILON then
|
||||
for i = 0, n - 1 do
|
||||
_x_next_buf[i] = xt[i] + dt * vt[i] + _noise_buf[i] * noise_scale
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do _x_next_buf[i] = xt[i] + dt * vt[i] end
|
||||
end
|
||||
|
||||
-- ── MOMENTUM ──────────────────────────────────────────────────────────────
|
||||
if mom_str > 0.0 and _prev_x ~= nil then
|
||||
for i = 0, n - 1 do
|
||||
local vel = _x_copy_buf[i] - _prev_x[i]
|
||||
_x_next_buf[i] = _x_next_buf[i] + vel * mom_str
|
||||
end
|
||||
end
|
||||
|
||||
-- ── LOOK-BACK SNR SMOOTHER ────────────────────────────────────────────────
|
||||
-- λ(σ) = lb_lambda * (σ/σ_max)^lb_snr_power — heavy early, fades late.
|
||||
if lb_enabled then
|
||||
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
if _look_back_xp == nil then
|
||||
_look_back_xp = {}
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
_look_back_xp[i] = _x_next_buf[i] + normal(u1, u2) * sigma_max * 0.1
|
||||
end
|
||||
end
|
||||
for i = 0, n - 1 do
|
||||
_x_next_buf[i] = _x_next_buf[i] * (1.0 - lb_w) + _look_back_xp[i] * lb_w
|
||||
end
|
||||
-- Update look-back buffer in-place
|
||||
for i = 0, n - 1 do _look_back_xp[i] = _x_next_buf[i] end
|
||||
end
|
||||
|
||||
-- ── RMS SERVO (DOWNWARD ONLY) ─────────────────────────────────────────────
|
||||
if rms_servo_on then
|
||||
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
|
||||
local cur_rms = rms(_x_next_buf, n)
|
||||
if cur_rms > rms_target then
|
||||
local servo_rms = cur_rms + rms_servo_gain * (rms_target - cur_rms)
|
||||
local scale = servo_rms / cur_rms
|
||||
for i = 0, n - 1 do _x_next_buf[i] = _x_next_buf[i] * scale end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── UPDATE STATE & WRITE OUTPUT ───────────────────────────────────────────
|
||||
-- Store momentum reference — reuse table, copy values
|
||||
if _prev_x == nil then _prev_x = {} end
|
||||
for i = 0, n - 1 do _prev_x[i] = _x_copy_buf[i] end
|
||||
for i = 0, n - 1 do xt[i] = _x_next_buf[i] end
|
||||
end
|
||||
@@ -0,0 +1,557 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
-- ============================================================================
|
||||
|
||||
-- md_solver_commons.lua -- Shared infrastructure for MD solver plugins
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- Provides:
|
||||
-- Level 1: Pure helpers (stateless math, array ops, param readers)
|
||||
-- Level 2: Stateful stages (identity anchor, tonal anchor, look-back,
|
||||
-- RMS servo, SDE noise, safety clamp) -- each takes state table
|
||||
-- Level 3: post_advance() convenience -- calls all stages in order
|
||||
-- Param defs: standard param definitions solvers can append
|
||||
--
|
||||
-- DEFAULTS REVISED (2026-07-14 listening tests, Rob/scragnog):
|
||||
-- Look-back floor REMOVED -- the 0.15 floor never faded out and smeared the
|
||||
-- final detail steps (main garble source). Look-back now fades to zero.
|
||||
-- look_back_enabled, identity_anchor, rms_servo: default OFF (opt-in).
|
||||
-- anchor_blend back to 0.08 (was 0.12). Tonal ramp kept:
|
||||
-- 0.3 + 0.7 * (1 - sigma_ratio)
|
||||
-- ============================================================================
|
||||
|
||||
local C = {}
|
||||
|
||||
C.EPSILON = 1e-8
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- LEVEL 1: PURE HELPERS
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function C.clamp(x, lo, hi) return math.max(lo, math.min(hi, x)) end
|
||||
|
||||
function C.smoothstep(x, lo, hi)
|
||||
if hi <= lo then return (x >= hi) and 1.0 or 0.0 end
|
||||
local t = C.clamp((x - lo) / (hi - lo), 0.0, 1.0)
|
||||
return t * t * (3.0 - 2.0 * t)
|
||||
end
|
||||
|
||||
function C.vec_norm(v, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + v[i] * v[i] end
|
||||
return math.sqrt(s)
|
||||
end
|
||||
|
||||
function C.vec_dot(a, b, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + a[i] * b[i] end
|
||||
return s
|
||||
end
|
||||
|
||||
function C.vec_sub_norm(a, b, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do local d = a[i] - b[i]; s = s + d * d end
|
||||
return math.sqrt(s)
|
||||
end
|
||||
|
||||
function C.cosine_sim(a, b, n)
|
||||
local dot, na, nb = 0.0, 0.0, 0.0
|
||||
for i = 0, n - 1 do
|
||||
dot = dot + a[i] * b[i]
|
||||
na = na + a[i] * a[i]
|
||||
nb = nb + b[i] * b[i]
|
||||
end
|
||||
return dot / (math.sqrt(na) * math.sqrt(nb) + C.EPSILON)
|
||||
end
|
||||
|
||||
function C.shannon_entropy(a, n)
|
||||
local sum_abs = 0.0
|
||||
for i = 0, n - 1 do sum_abs = sum_abs + math.abs(a[i]) end
|
||||
if sum_abs < C.EPSILON then return 0.0 end
|
||||
local H = 0.0
|
||||
for i = 0, n - 1 do
|
||||
local p = math.abs(a[i]) / sum_abs
|
||||
if p > C.EPSILON then H = H - p * math.log(p) end
|
||||
end
|
||||
return H
|
||||
end
|
||||
|
||||
function C.vec_clone(v, n)
|
||||
local c = {}
|
||||
for i = 0, n - 1 do c[i] = v[i] end
|
||||
return c
|
||||
end
|
||||
|
||||
function C.fa_to_tbl(fa, n)
|
||||
local t = {}
|
||||
for i = 0, n - 1 do t[i] = fa[i] end
|
||||
return t
|
||||
end
|
||||
|
||||
function C.tbl_to_fa(t, fa, n)
|
||||
for i = 0, n - 1 do fa[i] = t[i] end
|
||||
end
|
||||
|
||||
function C.has_nan_inf(v, n)
|
||||
for i = 0, n - 1 do
|
||||
if v[i] ~= v[i] or math.abs(v[i]) == math.huge then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function C.rms_range(a, off, cnt)
|
||||
local s = 0.0
|
||||
for i = off, off + cnt - 1 do s = s + a[i] * a[i] end
|
||||
return math.sqrt(s / math.max(cnt, 1) + C.EPSILON)
|
||||
end
|
||||
|
||||
function C.rms(a, n) return C.rms_range(a, 0, n) end
|
||||
|
||||
function C.spectral_centroid(a, off, cnt)
|
||||
local sum_mag, sum_w = 0.0, 0.0
|
||||
for i = 0, cnt - 1 do
|
||||
local m = math.abs(a[off + i])
|
||||
sum_mag = sum_mag + m
|
||||
sum_w = sum_w + m * i
|
||||
end
|
||||
if sum_mag < C.EPSILON then return 0.0 end
|
||||
return sum_w / sum_mag
|
||||
end
|
||||
|
||||
function C.band_energy(a, off, cnt)
|
||||
local bands = {0.0, 0.0, 0.0, 0.0}
|
||||
local bsize = math.floor(cnt / 4)
|
||||
for b = 0, 3 do
|
||||
local s = 0.0
|
||||
local lo = off + b * bsize
|
||||
local hi = (b == 3) and (off + cnt - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do s = s + math.abs(a[i]) end
|
||||
bands[b + 1] = s / math.max(hi - lo + 1, 1)
|
||||
end
|
||||
return bands
|
||||
end
|
||||
|
||||
function C.make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
function C.normal(u1, u2)
|
||||
return math.sqrt(-2.0 * math.log(math.max(u1, C.EPSILON))) * math.cos(2.0 * math.pi * u2)
|
||||
end
|
||||
|
||||
function C.num_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return tonumber(p[key]) or default
|
||||
end
|
||||
|
||||
function C.bool_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return p[key]
|
||||
end
|
||||
|
||||
-- Batch routing: reads engine globals, returns B, NPB with sanity fallback
|
||||
function C.get_batch_routing(n)
|
||||
local B = (batch_n and batch_n > 0) and batch_n or 1
|
||||
local NPB = (n_per and n_per > 0) and n_per or n
|
||||
if B * NPB ~= n then B = 1; NPB = n end
|
||||
return B, NPB
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- RELATIONAL DECOMPOSITION (from OmniRelational V3)
|
||||
-- Barbour Best Matching: separates velocity into unit direction (shape)
|
||||
-- and magnitude (scale). Blends shape-recomposed velocity with raw velocity
|
||||
-- using sigma-adaptive weight. Prevents any single latent component from
|
||||
-- dominating. Optional Gram-Schmidt drift guard.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Per-batch shape decomposition: returns unit direction + L2 norm
|
||||
local function decompose_shape_batch(v, off, cnt)
|
||||
local s = 0.0
|
||||
for i = off, off + cnt - 1 do s = s + v[i] * v[i] end
|
||||
local norm = math.sqrt(s + C.EPSILON)
|
||||
local inv = (norm > C.EPSILON) and (1.0 / norm) or 0.0
|
||||
local shape = {}
|
||||
for i = 0, cnt - 1 do shape[i] = v[off + i] * inv end
|
||||
return shape, norm
|
||||
end
|
||||
|
||||
-- Optional drift guard: projects shape onto orthogonal complement of x
|
||||
local function drift_guard_batch(shape, x, off, cnt, threshold)
|
||||
local norm_x = 0.0
|
||||
for i = off, off + cnt - 1 do norm_x = norm_x + x[i] * x[i] end
|
||||
norm_x = math.sqrt(norm_x + C.EPSILON)
|
||||
if norm_x < C.EPSILON then return shape end
|
||||
|
||||
local inv_x = 1.0 / norm_x
|
||||
local dot_sx = 0.0
|
||||
for i = 0, cnt - 1 do dot_sx = dot_sx + shape[i] * (x[off + i] * inv_x) end
|
||||
|
||||
if math.abs(dot_sx) <= threshold then return shape end
|
||||
|
||||
local proj = {}
|
||||
for i = 0, cnt - 1 do proj[i] = shape[i] - dot_sx * (x[off + i] * inv_x) end
|
||||
|
||||
local proj_norm = 0.0
|
||||
for i = 0, cnt - 1 do proj_norm = proj_norm + proj[i] * proj[i] end
|
||||
proj_norm = math.sqrt(proj_norm + C.EPSILON)
|
||||
if proj_norm < C.EPSILON then return shape end
|
||||
|
||||
local inv_proj = 1.0 / proj_norm
|
||||
for i = 0, cnt - 1 do proj[i] = proj[i] * inv_proj end
|
||||
return proj
|
||||
end
|
||||
|
||||
-- Apply relational decomposition to velocity (in-place, per-batch).
|
||||
-- v_out is modified: blends shape-recomposed velocity with raw velocity.
|
||||
-- x_curr needed only when drift_guard is enabled.
|
||||
function C.apply_relational(v_out, n, B, NPB, sigma_ratio, sigma_max,
|
||||
rw, sigma_power, drift_on, drift_thr, x_curr)
|
||||
if rw < 1e-6 then return v_out end
|
||||
|
||||
-- Sigma-adaptive relational weight: fades toward pure raw at low sigma
|
||||
local sr = math.min(sigma_ratio, 1.0)
|
||||
local rw_eff = rw * (sr ^ sigma_power)
|
||||
if rw_eff < 1e-6 then return v_out end
|
||||
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
|
||||
-- Decompose into shape (unit direction) and scale (L2 norm)
|
||||
local shape, scale = decompose_shape_batch(v_out, off, NPB)
|
||||
|
||||
-- Optional drift guard
|
||||
if drift_on and drift_thr < 1.0 and x_curr ~= nil then
|
||||
shape = drift_guard_batch(shape, x_curr, off, NPB, drift_thr)
|
||||
end
|
||||
|
||||
-- Blend: rw_eff * (shape * scale) + (1 - rw_eff) * raw
|
||||
for i = 0, NPB - 1 do
|
||||
local rel_v = shape[i] * scale
|
||||
v_out[off + i] = rw_eff * rel_v + (1.0 - rw_eff) * v_out[off + i]
|
||||
end
|
||||
end
|
||||
|
||||
return v_out
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- STATE MANAGEMENT
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Creates a fresh state table for one solver run.
|
||||
-- Solvers call this once at the top of sample() and pass it to stage functions.
|
||||
function C.new_state()
|
||||
return {
|
||||
-- Identity anchor
|
||||
has_anchor = false,
|
||||
id_buf = {},
|
||||
-- Tonal anchor
|
||||
tonal_captured = false,
|
||||
tonal_ref_cent = {},
|
||||
tonal_ref_bands = {},
|
||||
-- Look-back (primary)
|
||||
lb_prev = nil,
|
||||
-- Look-back (secondary, for solvers that need two)
|
||||
lb2_prev = nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- LEVEL 2: STATEFUL STAGES
|
||||
-- Each operates on x_new (table, 0-indexed), modifies in place, returns x_new.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Identity anchor: captures snapshot at anchor_sigma, then pulls back
|
||||
function C.apply_identity_anchor(x_new, n, sigma_ratio, anchor_sigma, anchor_blend, state)
|
||||
if not state.has_anchor and sigma_ratio <= anchor_sigma then
|
||||
state.id_buf = C.vec_clone(x_new, n)
|
||||
state.has_anchor = true
|
||||
elseif state.has_anchor then
|
||||
for j = 0, n - 1 do
|
||||
x_new[j] = (1.0 - anchor_blend) * x_new[j] + anchor_blend * state.id_buf[j]
|
||||
end
|
||||
end
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- Tonal anchor: per-batch spectral centroid + 4-band energy ratio correction
|
||||
-- Uses detail-phase ramp: 0.3 + 0.7 * (1 - sigma_ratio)
|
||||
function C.apply_tonal_anchor(x_new, n, B, NPB, sigma_ratio, anchor_sigma, tonal_str, state)
|
||||
if not state.tonal_captured and sigma_ratio <= anchor_sigma then
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
state.tonal_ref_cent[b] = C.spectral_centroid(x_new, off, NPB)
|
||||
state.tonal_ref_bands[b] = C.band_energy(x_new, off, NPB)
|
||||
end
|
||||
state.tonal_captured = true
|
||||
elseif state.tonal_captured then
|
||||
local tonal_ramp = 0.3 + 0.7 * (1.0 - sigma_ratio)
|
||||
local eff_str = tonal_str * tonal_ramp
|
||||
if eff_str > 1e-6 then
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
|
||||
-- Centroid drift correction
|
||||
local curr_centroid = C.spectral_centroid(x_new, off, NPB)
|
||||
local drift_norm_val = (curr_centroid - state.tonal_ref_cent[b]) /
|
||||
(math.abs(state.tonal_ref_cent[b]) + C.EPSILON)
|
||||
local tilt = C.clamp(-drift_norm_val * eff_str, -1e-3, 1e-3)
|
||||
local center = (NPB - 1) / 2.0
|
||||
for j = off, off + NPB - 1 do
|
||||
local dist_w = ((j - off) - center) / (center + C.EPSILON)
|
||||
x_new[j] = x_new[j] + tilt * dist_w * math.abs(x_new[j])
|
||||
end
|
||||
|
||||
-- Band energy ratio correction
|
||||
local curr_bands = C.band_energy(x_new, off, NPB)
|
||||
local ref_total, curr_total = 0.0, 0.0
|
||||
for bb = 1, 4 do
|
||||
ref_total = ref_total + state.tonal_ref_bands[b][bb]
|
||||
curr_total = curr_total + curr_bands[bb]
|
||||
end
|
||||
if ref_total > C.EPSILON and curr_total > C.EPSILON then
|
||||
local bsize = math.floor(NPB / 4)
|
||||
for bb = 0, 3 do
|
||||
local ref_ratio = state.tonal_ref_bands[b][bb + 1] / ref_total
|
||||
local curr_ratio = curr_bands[bb + 1] / curr_total
|
||||
local band_corr = C.clamp((ref_ratio - curr_ratio) * eff_str, -1e-3, 1e-3)
|
||||
local blo = off + bb * bsize
|
||||
local bhi = (bb == 3) and (off + NPB - 1) or (blo + bsize - 1)
|
||||
for j = blo, bhi do
|
||||
x_new[j] = x_new[j] + band_corr * math.abs(x_new[j])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- Look-back smoother, SNR-adaptive, fades to zero at low sigma
|
||||
-- slot: "lb_prev" (primary) or "lb2_prev" (secondary)
|
||||
function C.apply_look_back(x_new, n, sigma_ratio, lb_lambda, lb_snr_power, state, slot)
|
||||
slot = slot or "lb_prev"
|
||||
local prev = state[slot]
|
||||
if prev ~= nil then
|
||||
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
if lb_w > 1e-6 then
|
||||
for j = 0, n - 1 do
|
||||
x_new[j] = (1.0 - lb_w) * x_new[j] + lb_w * prev[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
state[slot] = C.vec_clone(x_new, n)
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- RMS servo: per-batch downward-only ceiling
|
||||
function C.apply_rms_servo(x_new, n, B, NPB, sigma_ratio, rms_tgt_min, rms_tgt_max, rms_gain)
|
||||
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
local cur_rms = C.rms_range(x_new, off, NPB)
|
||||
if cur_rms > rms_target then
|
||||
local servo_rms = cur_rms + rms_gain * (rms_target - cur_rms)
|
||||
local scale = servo_rms / cur_rms
|
||||
for j = off, off + NPB - 1 do x_new[j] = x_new[j] * scale end
|
||||
end
|
||||
end
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- SDE noise injection
|
||||
function C.apply_sde_noise(x_new, n, sigma_next, eta, seed, step_idx)
|
||||
if eta > 0.0 and sigma_next > C.EPSILON then
|
||||
local rng = C.make_rng(seed + step_idx * 7919)
|
||||
local scale = sigma_next * eta
|
||||
for j = 0, n - 1 do
|
||||
local u1 = math.max(rng(), C.EPSILON)
|
||||
local u2 = rng()
|
||||
x_new[j] = x_new[j] + C.normal(u1, u2) * scale
|
||||
end
|
||||
end
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- Safety clamp
|
||||
function C.apply_safety_clamp(x_new, n, sclamp)
|
||||
for j = 0, n - 1 do x_new[j] = C.clamp(x_new[j], -sclamp, sclamp) end
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- LEVEL 3: POST-ADVANCE CONVENIENCE
|
||||
-- Calls all stages in standard order. opts table keys:
|
||||
-- f_id_anchor, anchor_sigma, anchor_blend,
|
||||
-- f_tonal, tonal_str,
|
||||
-- f_lookback, lb_lambda, lb_snr_power,
|
||||
-- f_rms, rms_tgt_min, rms_tgt_max, rms_gain,
|
||||
-- eta, seed, step_idx, sigma_next,
|
||||
-- sclamp
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function C.post_advance(x_new, n, B, NPB, sigma_ratio, opts, state)
|
||||
-- Identity anchor
|
||||
if opts.f_id_anchor then
|
||||
C.apply_identity_anchor(x_new, n, sigma_ratio,
|
||||
opts.anchor_sigma, opts.anchor_blend, state)
|
||||
end
|
||||
|
||||
-- Tonal anchor
|
||||
if opts.f_tonal then
|
||||
C.apply_tonal_anchor(x_new, n, B, NPB, sigma_ratio,
|
||||
opts.anchor_sigma, opts.tonal_str, state)
|
||||
end
|
||||
|
||||
-- Look-back
|
||||
if opts.f_lookback then
|
||||
C.apply_look_back(x_new, n, sigma_ratio,
|
||||
opts.lb_lambda, opts.lb_snr_power, state, "lb_prev")
|
||||
end
|
||||
|
||||
-- RMS servo
|
||||
if opts.f_rms then
|
||||
C.apply_rms_servo(x_new, n, B, NPB, sigma_ratio,
|
||||
opts.rms_tgt_min, opts.rms_tgt_max, opts.rms_gain)
|
||||
end
|
||||
|
||||
-- SDE noise
|
||||
C.apply_sde_noise(x_new, n, opts.sigma_next, opts.eta, opts.seed, opts.step_idx)
|
||||
|
||||
-- Safety clamp
|
||||
C.apply_safety_clamp(x_new, n, opts.sclamp)
|
||||
|
||||
return x_new
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- PARAM DEFINITIONS
|
||||
-- Solvers call C.append_common_params(params_table) to add these.
|
||||
-- Defaults reflect cross-cutting fixes (anchor_blend=0.12, rms=true).
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
C.RELATIONAL_PARAMS = {
|
||||
{ key = "relational_weight", type = "slider", label = "Relational Weight",
|
||||
default = 0.0, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Barbour Best Matching: shape/scale decomposition on velocity. 0 = off (raw velocity). 0.3-0.5 = balanced. Sigma-adaptive: fades to raw at low sigma." },
|
||||
{ key = "relational_sigma_power", type = "slider", label = "Relational Sigma Decay",
|
||||
default = 1.0, min = 0.25, max = 4.0, step = 0.25,
|
||||
hint = "How fast relational weight fades. 1.0 = linear. 2.0 = quadratic (faster fade)." },
|
||||
{ key = "drift_guard", type = "toggle", label = "Drift Guard",
|
||||
default = false,
|
||||
hint = "Gram-Schmidt projection prevents velocity reinforcing existing latent structure." },
|
||||
{ key = "drift_threshold", type = "slider", label = "Drift Threshold",
|
||||
default = 0.85, min = 0.1, max = 1.0, step = 0.05,
|
||||
hint = "Cosine similarity ceiling before drift guard fires." },
|
||||
}
|
||||
|
||||
C.ANCHOR_PARAMS = {
|
||||
{ key = "identity_anchor", type = "toggle", label = "Identity Anchor",
|
||||
default = false,
|
||||
hint = "Captures latent snapshot at anchor_sigma. Gently pulls output back." },
|
||||
{ key = "anchor_sigma", type = "slider", label = "Anchor Sigma",
|
||||
default = 0.5, min = 0.1, max = 0.9, step = 0.05,
|
||||
hint = "Sigma fraction for identity/tonal anchor capture." },
|
||||
{ key = "anchor_blend", type = "slider", label = "Anchor Blend",
|
||||
default = 0.08, min = 0.01, max = 0.30, step = 0.01,
|
||||
hint = "Pull strength toward identity anchor snapshot." },
|
||||
{ key = "tonal_anchor", type = "toggle", label = "Tonal Anchor",
|
||||
default = true,
|
||||
hint = "Per-batch spectral centroid + 4-band energy ratio drift correction. Ramps up in detail phase." },
|
||||
{ key = "tonal_strength", type = "slider", label = "Tonal Strength",
|
||||
default = 0.20, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Tonal correction scale. Detail-phase ramp built in. Hard-capped 0.1%/step." },
|
||||
}
|
||||
|
||||
C.LOOKBACK_PARAMS = {
|
||||
{ key = "look_back_enabled", type = "toggle", label = "Look-Back Smoother",
|
||||
default = false, hint = "SNR-adaptive latent EMA. Fades to zero at low sigma." },
|
||||
{ key = "look_back_lambda", type = "slider", label = "Look-Back Lambda",
|
||||
default = 0.15, min = 0.05, max = 1.0, step = 0.05, hint = "Max smoothing at high sigma." },
|
||||
{ key = "look_back_snr_power", type = "slider", label = "Look-Back SNR Power",
|
||||
default = 1.3, min = 0.5, max = 3.0, step = 0.1, hint = "Falloff exponent." },
|
||||
}
|
||||
|
||||
C.RMS_PARAMS = {
|
||||
{ key = "rms_servo", type = "toggle", label = "RMS Servo",
|
||||
default = false, hint = "Per-batch downward-only RMS ceiling. ACE-Step latents run ~2.0 RMS -- calibrate targets before enabling." },
|
||||
{ key = "rms_target_min", type = "slider", label = "RMS Target Min",
|
||||
default = 1.2, min = 0.1, max = 3.0, step = 0.05, hint = "RMS ceiling at low sigma." },
|
||||
{ key = "rms_target_max", type = "slider", label = "RMS Target Max",
|
||||
default = 2.5, min = 0.5, max = 5.0, step = 0.05, hint = "RMS ceiling at high sigma." },
|
||||
{ key = "rms_servo_gain", type = "slider", label = "RMS Servo Gain",
|
||||
default = 0.6, min = 0.1, max = 1.0, step = 0.05, hint = "Servo correction aggressiveness." },
|
||||
}
|
||||
|
||||
C.SDE_PARAMS = {
|
||||
{ key = "eta", type = "slider", label = "Noise Injection (0 = ODE)",
|
||||
default = 0.0, min = 0.0, max = 1.0, step = 0.05, hint = "Post-step SDE noise." },
|
||||
{ key = "seed", type = "slider", label = "Seed",
|
||||
default = 42, min = 0, max = 999999, step = 1, hint = "RNG seed." },
|
||||
{ key = "safety_clamp", type = "slider", label = "Safety Clamp",
|
||||
default = 2.5, min = 1.0, max = 5.0, step = 0.1, hint = "Max abs latent value." },
|
||||
}
|
||||
|
||||
C.VERBOSE_PARAM = {
|
||||
{ key = "verbose", type = "toggle", label = "Verbose Logging",
|
||||
default = false, hint = "Per-step diagnostics." },
|
||||
}
|
||||
|
||||
-- Appends param definitions to a solver's params table.
|
||||
-- Usage: C.append_common_params(solver.params)
|
||||
-- Adds: anchor, look-back, RMS, SDE, verbose (in that order)
|
||||
function C.append_common_params(params_table)
|
||||
local sets = { C.RELATIONAL_PARAMS, C.ANCHOR_PARAMS, C.LOOKBACK_PARAMS, C.RMS_PARAMS, C.SDE_PARAMS, C.VERBOSE_PARAM }
|
||||
for _, set in ipairs(sets) do
|
||||
for _, p in ipairs(set) do
|
||||
params_table[#params_table + 1] = p
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Reads all common params from the params global into an opts table
|
||||
-- suitable for passing to post_advance().
|
||||
function C.read_common_opts(p)
|
||||
return {
|
||||
-- Relational
|
||||
rw = C.num_param(p, "relational_weight", 0.0),
|
||||
rw_sigma_pow = C.num_param(p, "relational_sigma_power", 1.0),
|
||||
drift_on = C.bool_param(p, "drift_guard", false),
|
||||
drift_thr = C.num_param(p, "drift_threshold", 0.85),
|
||||
-- Anchors
|
||||
f_id_anchor = C.bool_param(p, "identity_anchor", false),
|
||||
anchor_sigma = C.num_param(p, "anchor_sigma", 0.5),
|
||||
anchor_blend = C.num_param(p, "anchor_blend", 0.08),
|
||||
f_tonal = C.bool_param(p, "tonal_anchor", true),
|
||||
tonal_str = C.num_param(p, "tonal_strength", 0.20),
|
||||
f_lookback = C.bool_param(p, "look_back_enabled", false),
|
||||
lb_lambda = C.num_param(p, "look_back_lambda", 0.15),
|
||||
lb_snr_power = C.num_param(p, "look_back_snr_power", 1.3),
|
||||
f_rms = C.bool_param(p, "rms_servo", false),
|
||||
rms_tgt_min = C.num_param(p, "rms_target_min", 1.2),
|
||||
rms_tgt_max = C.num_param(p, "rms_target_max", 2.5),
|
||||
rms_gain = C.num_param(p, "rms_servo_gain", 0.6),
|
||||
eta = C.num_param(p, "eta", 0.0),
|
||||
seed = math.floor(C.num_param(p, "seed", 42)),
|
||||
sclamp = C.num_param(p, "safety_clamp", 2.5),
|
||||
verbose = C.bool_param(p, "verbose", false),
|
||||
-- These are set per-step by the solver before calling post_advance:
|
||||
sigma_next = 0.0,
|
||||
step_idx = 0,
|
||||
}
|
||||
end
|
||||
|
||||
return C
|
||||
@@ -0,0 +1,574 @@
|
||||
--[[
|
||||
md_storm_core.lua
|
||||
STORM -- Stabilized Taylor Oscillation with Runge-Kutta Memory
|
||||
V4: Commons integration + relational decomposition
|
||||
|
||||
© 2026 Alexander Allan (MDMAchine) | A&E Concepts
|
||||
GPL v3
|
||||
--]]
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
solver = {
|
||||
name = "md_storm_V4",
|
||||
display = "MD STORM V4",
|
||||
description = "Adaptive STORK/DPM++3M hybrid with relational velocity decomposition, pseudo-LTE error estimation, look-back SNR smoother, stiffness dispatch.",
|
||||
accent = "cyan",
|
||||
nfe = 0,
|
||||
order = 5,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = false,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
-- ── Pseudo-LTE Research Controls ──
|
||||
{ key = "attenuation_k", type = "slider", label = "Attenuation K",
|
||||
default = 25.0, min = 1.0, max = 30.0, step = 0.5,
|
||||
hint = "Exponential decay factor for highest-order memory term in pseudo-LTE estimation." },
|
||||
{ key = "hyst_downgrade", type = "slider", label = "Downgrade Threshold",
|
||||
default = 0.40, min = 0.10, max = 0.80, step = 0.02,
|
||||
hint = "Pseudo-LTE above which solver drops order. Lower = more cautious." },
|
||||
{ key = "hyst_upgrade", type = "slider", label = "Upgrade Threshold",
|
||||
default = 0.25, min = 0.05, max = 0.40, step = 0.01,
|
||||
hint = "Pseudo-LTE below which solver regains trust and considers upgrading order." },
|
||||
{ key = "stability_window", type = "slider", label = "Stability Window",
|
||||
default = 4, min = 1, max = 8, step = 1,
|
||||
hint = "Consecutive stable steps required before order upgrade. Higher = more conservative." },
|
||||
|
||||
-- ── Stiffness Detection ──
|
||||
{ key = "stiffness_threshold", type = "slider", label = "Stiffness Threshold",
|
||||
default = 0.15, min = 0.05, max = 0.50, step = 0.01,
|
||||
hint = "Base threshold for STORK/DPM++ dispatch. Lower = more STORK (precise), higher = more DPM++ (smooth)." },
|
||||
{ key = "stiffness_hysteresis", type = "slider", label = "Stiffness Hysteresis",
|
||||
default = 0.05, min = 0.0, max = 0.20, step = 0.01,
|
||||
hint = "Dead zone preventing rapid mode switching. Higher = stickier dispatch." },
|
||||
{ key = "stiffness_ema", type = "slider", label = "Stiffness EMA",
|
||||
default = 0.4, min = 0.05, max = 0.8, step = 0.05,
|
||||
hint = "EMA smoothing for stiffness ratio. Lower = more reactive, higher = more stable." },
|
||||
|
||||
-- ── Look-Back Smoother ──
|
||||
{ key = "look_back_lambda", type = "slider", label = "Look-Back Lambda",
|
||||
default = 0.15, min = 0.0, max = 1.0, step = 0.01,
|
||||
hint = "Inter-step smoothing strength. 0=off (raw). 0.15=standard. Higher = smoother but softer detail." },
|
||||
{ key = "look_back_snr_power", type = "slider", label = "Look-Back SNR Power",
|
||||
default = 1.2, min = 0.5, max = 3.0, step = 0.1,
|
||||
hint = "Concentrates smoothing on early noisy steps. Higher = heavier early smoothing, leaves late detail alone." },
|
||||
|
||||
-- ── Solver Order & Cache ──
|
||||
{ key = "rk_order", type = "select", label = "Precision Level",
|
||||
default = "auto",
|
||||
options = {
|
||||
{ value = "auto", label = "Auto (Recommended)" },
|
||||
{ value = "2", label = "Low (RK2)" },
|
||||
{ value = "3", label = "Medium (RK3)" },
|
||||
{ value = "4", label = "High (RK4)" },
|
||||
{ value = "5", label = "Maximum (RK5)" },
|
||||
},
|
||||
hint = "Max STORK order. Auto ramps up as cache fills. DPM++3M always uses order 3." },
|
||||
{ key = "cache_depth", type = "slider", label = "Cache Depth",
|
||||
default = 5, min = 2, max = 10, step = 1,
|
||||
hint = "Velocity history size. More = higher order available, diminishing returns past 5." },
|
||||
|
||||
-- ── Diagnostics ──
|
||||
{ key = "telemetry", type = "toggle", label = "Output Telemetry",
|
||||
default = false,
|
||||
hint = "Print JSON diagnostic logs to console at generation end." },
|
||||
{ key = "verbose", type = "toggle", label = "Verbose Logging",
|
||||
default = false,
|
||||
hint = "Print per-step solver decisions to console (debug)." },
|
||||
{ key = "relational_weight", type = "slider", label = "Relational Weight",
|
||||
default = 0.0, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Barbour Best Matching velocity decomposition. 0 = off. 0.3-0.5 = balanced." },
|
||||
{ key = "relational_sigma_power", type = "slider", label = "Relational Sigma Decay",
|
||||
default = 1.0, min = 0.25, max = 4.0, step = 0.25,
|
||||
hint = "How fast relational weight fades. 1.0 = linear." },
|
||||
},
|
||||
}
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- HELPERS (aliased from md_solver_commons)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
local EPSILON = C.EPSILON
|
||||
local fa_to_tbl = C.fa_to_tbl
|
||||
local tbl_to_fa = C.tbl_to_fa
|
||||
local vec_norm = C.vec_norm
|
||||
local vec_sub_norm = C.vec_sub_norm
|
||||
local vec_dot = C.vec_dot
|
||||
local vec_clone = C.vec_clone
|
||||
local has_nan_inf_tbl = C.has_nan_inf
|
||||
local clamp = C.clamp
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- LOOK-BACK SMOOTHER (arXiv:2602.09449)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
local function look_back_smooth(x_curr, x_prev, sigma_curr, sigma_max, lambda_base, snr_power, n)
|
||||
if x_prev == nil then return x_curr, 0.0 end
|
||||
local ratio = math.min(sigma_curr / math.max(sigma_max, 1e-8), 1.0)
|
||||
local lam = lambda_base * math.max(ratio ^ snr_power, 0.15)
|
||||
local out = {}
|
||||
for i = 0, n - 1 do out[i] = (1.0 - lam) * x_curr[i] + lam * x_prev[i] end
|
||||
return out, lam
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- STIFFNESS DETECTION (from deployed STORM v3.0)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
local function compute_stiffness(v_curr, v_cache, step_idx, baseline, threshold, ema_alpha, n_calib, n)
|
||||
if #v_cache < 1 then return true, baseline, nil end
|
||||
|
||||
local v_prev = v_cache[#v_cache].v
|
||||
local norm_delta = vec_sub_norm(v_curr, v_prev, n)
|
||||
local norm_curr = vec_norm(v_curr, n) + 1e-8
|
||||
local raw_ratio = norm_delta / norm_curr
|
||||
|
||||
local prev_ema = baseline.ema or raw_ratio
|
||||
local smoothed = ema_alpha * raw_ratio + (1.0 - ema_alpha) * prev_ema
|
||||
baseline.ema = smoothed
|
||||
|
||||
local dot = vec_dot(v_curr, v_prev, n)
|
||||
local nc = vec_norm(v_curr, n)
|
||||
local np_ = vec_norm(v_prev, n)
|
||||
local cos_sim = dot / (nc * np_ + 1e-8)
|
||||
|
||||
if step_idx < n_calib then
|
||||
baseline.sum = (baseline.sum or 0.0) + smoothed
|
||||
baseline.count = (baseline.count or 0) + 1
|
||||
baseline.last_ratio = smoothed
|
||||
return true, baseline, cos_sim
|
||||
end
|
||||
|
||||
local bmean = baseline.sum / math.max(baseline.count, 1)
|
||||
local adap_thr = threshold * (bmean / 0.15)
|
||||
adap_thr = clamp(adap_thr, 0.05, 0.50)
|
||||
|
||||
local stiff = smoothed > adap_thr
|
||||
baseline.last_ratio = smoothed
|
||||
baseline.last_threshold = adap_thr
|
||||
return stiff, baseline, cos_sim
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- STORK MULTI-ORDER (AB2-AB5, cosine-similarity damping, from deployed STORM)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
local function stork_step(v_cache, x, sigma_curr, sigma_next, v_curr, rk_order, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
local n_cache = #v_cache
|
||||
|
||||
local actual_order
|
||||
if rk_order == "auto" then
|
||||
actual_order = (n_cache >= 1) and math.min(n_cache + 1, 5) or 1
|
||||
else
|
||||
actual_order = (n_cache >= 1) and math.min(tonumber(rk_order), n_cache + 1) or 1
|
||||
end
|
||||
actual_order = math.max(actual_order, 1)
|
||||
|
||||
-- Euler fallback
|
||||
if n_cache < 1 or actual_order <= 1 then
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
return x_next, 1
|
||||
end
|
||||
|
||||
local e0 = v_cache[#v_cache]
|
||||
local v_prev_0 = e0.v
|
||||
local sigma_prev = e0.sigma
|
||||
|
||||
-- Cosine-similarity damping (deployed STORM's approach)
|
||||
local dot = vec_dot(v_curr, v_prev_0, n)
|
||||
local nc = vec_norm(v_curr, n)
|
||||
local np_ = vec_norm(v_prev_0, n)
|
||||
local cos_sim = dot / (nc * np_ + 1e-8)
|
||||
local damping = clamp(cos_sim, 0.0, 1.0)
|
||||
|
||||
local denom = sigma_curr - sigma_prev
|
||||
if math.abs(denom) < 1e-8 then
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
return x_next, 2
|
||||
end
|
||||
local alpha = (sigma_next - sigma_curr) / denom
|
||||
|
||||
local x_next = {}
|
||||
|
||||
if actual_order == 2 then
|
||||
for i = 0, n - 1 do
|
||||
local v_extrap = v_curr[i] + (alpha * damping) * (v_curr[i] - v_prev_0[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * v_extrap)
|
||||
end
|
||||
|
||||
elseif actual_order == 3 and n_cache >= 2 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 then
|
||||
for i = 0, n - 1 do
|
||||
local ve = v_curr[i] + (alpha * damping) * (v_curr[i] - v1[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * ve)
|
||||
end
|
||||
actual_order = 2
|
||||
else
|
||||
local c0 = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do
|
||||
local v_pred = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (v_pred - v_curr[i]))
|
||||
end
|
||||
end
|
||||
|
||||
elseif actual_order == 4 and n_cache >= 3 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local v3, s3 = v_cache[#v_cache - 2].v, v_cache[#v_cache - 2].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
local h2 = s2 - s3
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 or math.abs(h2) < 1e-8 then
|
||||
local c0 = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 3
|
||||
else
|
||||
local c0 = 1.0 + dt/(2.0*h) + dt^2/(3.0*h*h1) + dt^3/(4.0*h*h1*h2)
|
||||
local c1 = -(dt/(2.0*h)) * (1.0 + dt/h1 + dt^2/(2.0*h1*h2))
|
||||
local c2 = (dt^2/(3.0*h*h1)) * (1.0 + dt/(2.0*h2))
|
||||
local c3 = -(dt^3) / (4.0*h*h1*h2)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0*v_curr[i] + c1*v1[i] + c2*v2[i] + c3*v3[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
end
|
||||
|
||||
elseif actual_order >= 5 and n_cache >= 4 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local v3, s3 = v_cache[#v_cache - 2].v, v_cache[#v_cache - 2].sigma
|
||||
local v4, s4 = v_cache[#v_cache - 3].v, v_cache[#v_cache - 3].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
local h2 = s2 - s3
|
||||
local h3 = s3 - s4
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 or math.abs(h2) < 1e-8 or math.abs(h3) < 1e-8 then
|
||||
local c0 = 1.0 + dt/(2.0*h) + dt^2/(3.0*h*h1) + dt^3/(4.0*h*h1*h2)
|
||||
local c1 = -(dt/(2.0*h)) * (1.0 + dt/h1 + dt^2/(2.0*h1*h2))
|
||||
local c2 = (dt^2/(3.0*h*h1)) * (1.0 + dt/(2.0*h2))
|
||||
local c3 = -(dt^3) / (4.0*h*h1*h2)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0*v_curr[i] + c1*v1[i] + c2*v2[i] + c3*v3[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 4
|
||||
else
|
||||
local c0 = 1.0 + dt/(2.0*h) + dt^2/(3.0*h*h1) + dt^3/(4.0*h*h1*h2) + dt^4/(5.0*h*h1*h2*h3)
|
||||
local c1 = -(dt/(2.0*h)) * (1.0 + dt/h1 + dt^2/(2.0*h1*h2) + dt^3/(3.0*h1*h2*h3))
|
||||
local c2 = (dt^2/(3.0*h*h1)) * (1.0 + dt/(2.0*h2) + dt^2/(3.0*h2*h3))
|
||||
local c3 = -(dt^3/(4.0*h*h1*h2)) * (1.0 + dt/(2.0*h3))
|
||||
local c4 = dt^4 / (5.0*h*h1*h2*h3)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0*v_curr[i] + c1*v1[i] + c2*v2[i] + c3*v3[i] + c4*v4[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 5
|
||||
end
|
||||
|
||||
else
|
||||
-- Fallback AB2
|
||||
for i = 0, n - 1 do
|
||||
local ve = v_curr[i] + (alpha * damping) * (v_curr[i] - v_prev_0[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * ve)
|
||||
end
|
||||
actual_order = 2
|
||||
end
|
||||
|
||||
return x_next, actual_order
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- DPM++3M (smooth schedule path, from deployed STORM)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
local function dpmpp3m_step(v_cache, x, sigma_curr, sigma_next, v_curr, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
local x_next = {}
|
||||
|
||||
if #v_cache >= 2 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 then
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
else
|
||||
local cc = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * (cc * v_curr[i] + c1 * v1[i] + c2 * v2[i]) end
|
||||
end
|
||||
elseif #v_cache >= 1 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local h = sigma_curr - s1
|
||||
if math.abs(h) < 1e-8 then
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
else
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * (v_curr[i] + (dt / (2.0 * h)) * (v_curr[i] - v1[i])) end
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
end
|
||||
|
||||
return x_next
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- PSEUDO-LTE ESTIMATION (Phase 1 research addition)
|
||||
-- Computes kinetic-floored relative error from the highest-order AB term.
|
||||
-- Returns rel_epsilon and attenuation weight w_t for telemetry.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
local function estimate_pseudo_lte(v_cache, v_curr, ema_vel, k_atten, n)
|
||||
local n_cache = #v_cache
|
||||
if n_cache < 1 then return 0.0, 1.0 end
|
||||
|
||||
-- Use the oldest cached velocity as the "highest order contribution" proxy
|
||||
local v_oldest = v_cache[1].v
|
||||
local oldest_norm = vec_norm(v_oldest, n)
|
||||
|
||||
-- Extrapolation norm: current velocity (proxy for full polynomial magnitude)
|
||||
local extrap_norm = vec_norm(v_curr, n)
|
||||
|
||||
local safe_den = math.max(extrap_norm, ema_vel * 0.5)
|
||||
local rel_epsilon = oldest_norm / (safe_den + EPSILON)
|
||||
local w_t = math.exp(-k_atten * rel_epsilon)
|
||||
|
||||
return rel_epsilon, w_t
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- SAMPLE — Full-loop entry point
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
local p = params or {}
|
||||
|
||||
-- Pseudo-LTE research params
|
||||
local k_atten = p.attenuation_k or 10.0
|
||||
local thr_down = p.hyst_downgrade or 0.40
|
||||
local thr_up = p.hyst_upgrade or 0.15
|
||||
local stab_window = math.floor(p.stability_window or 3)
|
||||
|
||||
-- Stiffness detection params
|
||||
local stiff_thr = p.stiffness_threshold or 0.15
|
||||
local hyst = p.stiffness_hysteresis or 0.05
|
||||
local ema_a = p.stiffness_ema or 0.3
|
||||
|
||||
-- Look-back smoother params
|
||||
local lb_lambda = p.look_back_lambda or 0.15
|
||||
local lb_snr_pow = p.look_back_snr_power or 1.5
|
||||
|
||||
-- Solver order & cache params
|
||||
local rk_order = p.rk_order or "auto"
|
||||
local depth_max = math.floor(p.cache_depth or 5)
|
||||
|
||||
-- Diagnostics
|
||||
local do_tele = p.telemetry or false
|
||||
local verbose = p.verbose or false
|
||||
local rw = C.num_param(p, "relational_weight", 0.0)
|
||||
local rw_sig_pow = C.num_param(p, "relational_sigma_power", 1.0)
|
||||
|
||||
-- Derived constants
|
||||
local calib_frac = 0.12
|
||||
local ns = #schedule
|
||||
-- Engine schedule has NO trailing 0 (fix ported from 46c081e): iterate all ns
|
||||
-- entries so the last iteration gets sigma_next = 0.0 and the terminal branch
|
||||
-- performs the final x0 projection. With ns - 1 that branch is dead code and
|
||||
-- the output keeps ~final-sigma noise.
|
||||
local n_steps = ns
|
||||
if n_steps < 1 then return end
|
||||
|
||||
local v_cache = {}
|
||||
local baseline = { sum = 0.0, count = 0 }
|
||||
local sigma_max = schedule[1]
|
||||
local n_calib = math.max(2, math.min(5, math.floor(n_steps * calib_frac)))
|
||||
local lb_enabled = (lb_lambda > 0)
|
||||
|
||||
-- Pseudo-LTE state
|
||||
local current_order = 1
|
||||
local stability_counter = 0
|
||||
local ema_vel = 0.0
|
||||
local telemetry_data = {}
|
||||
|
||||
if verbose then
|
||||
print(string.format("[STORM] Schedule: %d steps | Calib: %d | RK: %s | Cache: %d | LB: %.2f^%.1f",
|
||||
n_steps, n_calib, tostring(rk_order), depth_max, lb_lambda, lb_snr_pow))
|
||||
end
|
||||
|
||||
-- Working copy
|
||||
local x = fa_to_tbl(xt, n)
|
||||
|
||||
-- Seed x_prev for look-back (jittered copy, same as deployed STORM)
|
||||
local x_prev_lb = nil
|
||||
if lb_enabled then
|
||||
x_prev_lb = {}
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(1e-12, math.random())
|
||||
local u2 = math.random()
|
||||
local r = (sigma_max * 0.1) * math.sqrt(-2.0 * math.log(u1))
|
||||
x_prev_lb[i] = x[i] + r * math.cos(2 * math.pi * u2)
|
||||
end
|
||||
end
|
||||
|
||||
-- Model eval helper
|
||||
local function eval_at(x_tbl, t_val)
|
||||
tbl_to_fa(x_tbl, xt, n)
|
||||
model_fn(xt, t_val)
|
||||
return fa_to_tbl(vt_buf, n)
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
|
||||
-- Terminal step: Euler denoise to x0
|
||||
if sigma_next == 0.0 then
|
||||
local v_final = eval_at(x, sigma_curr)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
if verbose then
|
||||
print(string.format("[STORM] Step %02d: FINAL (Euler terminal)", i - 1))
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
-- Snapshot for look-back (before this step modifies x)
|
||||
local x_prev_lb_before = nil
|
||||
if lb_enabled then x_prev_lb_before = vec_clone(x, n) end
|
||||
|
||||
-- Evaluate velocity
|
||||
local v_curr = eval_at(x, sigma_curr)
|
||||
|
||||
-- Relational decomposition
|
||||
local sigma_ratio = clamp(sigma_curr / math.max(sigma_max, EPSILON), 0.0, 1.0)
|
||||
if rw > 0 then
|
||||
C.apply_relational(v_curr, n, 1, n, sigma_ratio, sigma_max,
|
||||
rw, rw_sig_pow, false, 0.85, x)
|
||||
end
|
||||
|
||||
local cur_vel_norm = vec_norm(v_curr, n)
|
||||
|
||||
-- Update kinetic floor EMA
|
||||
if i == 1 then ema_vel = cur_vel_norm
|
||||
else ema_vel = 0.8 * ema_vel + 0.2 * cur_vel_norm end
|
||||
|
||||
-- ── Stiffness detection (deployed STORM) ──
|
||||
local stiff, cos_sim_out
|
||||
if #v_cache >= 1 then
|
||||
stiff, baseline, cos_sim_out = compute_stiffness(
|
||||
v_curr, v_cache, i - 1, baseline, stiff_thr, ema_a, n_calib, n)
|
||||
else
|
||||
stiff, cos_sim_out = true, nil
|
||||
end
|
||||
|
||||
-- Hysteresis: prevent rapid STORK↔DPM++ switching
|
||||
local prev_mode = baseline.prev_mode or "STORK"
|
||||
if prev_mode == "DPM++" and not stiff then
|
||||
if (baseline.last_ratio or 0) > (baseline.last_threshold or stiff_thr) + hyst then
|
||||
stiff = true
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Dispatch: STORK (stiff) or DPM++3M (smooth) ──
|
||||
local x_next, actual_order, mode
|
||||
if stiff then
|
||||
x_next, actual_order = stork_step(v_cache, x, sigma_curr, sigma_next, v_curr, rk_order, n)
|
||||
mode = "STORK"
|
||||
else
|
||||
x_next = dpmpp3m_step(v_cache, x, sigma_curr, sigma_next, v_curr, n)
|
||||
mode = "DPM++"
|
||||
actual_order = 3
|
||||
end
|
||||
|
||||
-- ── Pseudo-LTE estimation (Phase 1 research) ──
|
||||
local rel_epsilon, w_t = estimate_pseudo_lte(v_cache, v_curr, ema_vel, k_atten, n)
|
||||
|
||||
-- Asymmetric hysteresis order management
|
||||
if rel_epsilon > thr_down then
|
||||
current_order = math.max(1, current_order - 1)
|
||||
stability_counter = 0
|
||||
elseif rel_epsilon < thr_up then
|
||||
stability_counter = stability_counter + 1
|
||||
if stability_counter >= stab_window then
|
||||
current_order = math.min(current_order + 1, 5)
|
||||
stability_counter = 0
|
||||
end
|
||||
else
|
||||
stability_counter = 0
|
||||
end
|
||||
|
||||
-- Verbose logging
|
||||
if verbose then
|
||||
local lr = baseline.last_ratio or 0.0
|
||||
local lt = baseline.last_threshold or stiff_thr
|
||||
local cs = cos_sim_out and string.format("%.4f", cos_sim_out) or "N/A"
|
||||
local tag = (stiff and prev_mode == "DPM++") and " -> CURVATURE SPIKE" or ""
|
||||
print(string.format("[STORM] Step %02d: %-5s RK%d | Stiff: %.3f/%.3f | cos: %s | LTE: %.4f w=%.3f ord=%d%s",
|
||||
i - 1, mode, actual_order, lr, lt, cs, rel_epsilon, w_t, current_order, tag))
|
||||
end
|
||||
|
||||
-- Telemetry
|
||||
if do_tele then
|
||||
table.insert(telemetry_data, string.format(
|
||||
'{"step":%d,"sigma":%.4f,"mode":"%s","rk_order":%d,"stiff_ratio":%.5f,"stiff_thr":%.5f,"cos_sim":%s,"rel_eps":%.5f,"w_t":%.5f,"lte_order":%d,"vel_norm":%.5f}',
|
||||
i, sigma_curr, mode, actual_order,
|
||||
baseline.last_ratio or 0, baseline.last_threshold or stiff_thr,
|
||||
cos_sim_out and string.format("%.5f", cos_sim_out) or "null",
|
||||
rel_epsilon, w_t, current_order, cur_vel_norm))
|
||||
end
|
||||
|
||||
-- ── NaN guard ──
|
||||
if has_nan_inf_tbl(x_next, n) then
|
||||
print(string.format("[STORM] NaN/Inf at step %d. Flushing cache, Euler fallback.", i - 1))
|
||||
local dt = sigma_next - sigma_curr
|
||||
x_next = {}
|
||||
for j = 0, n - 1 do x_next[j] = x[j] + dt * v_curr[j] end
|
||||
v_cache = {}
|
||||
baseline.prev_mode = "STORK"
|
||||
current_order = 1
|
||||
stability_counter = 0
|
||||
end
|
||||
|
||||
-- Update cache
|
||||
table.insert(v_cache, { v = v_curr, sigma = sigma_curr })
|
||||
while #v_cache > depth_max do table.remove(v_cache, 1) end
|
||||
baseline.prev_mode = mode
|
||||
|
||||
x = x_next
|
||||
|
||||
-- ── Look-Back smoothing ──
|
||||
if lb_enabled and x_prev_lb ~= nil then
|
||||
local lam
|
||||
x, lam = look_back_smooth(x, x_prev_lb, sigma_curr, sigma_max, lb_lambda, lb_snr_pow, n)
|
||||
if verbose then
|
||||
print(string.format("[STORM] LookBack lambda=%.4f @ sigma=%.3f", lam, sigma_curr))
|
||||
end
|
||||
end
|
||||
x_prev_lb = x_prev_lb_before
|
||||
|
||||
-- Write back for on_step hooks (DCW, repaint)
|
||||
tbl_to_fa(x, xt, n)
|
||||
tbl_to_fa(v_curr, vt_buf, n)
|
||||
|
||||
-- Report step
|
||||
if on_step(i - 1, sigma_curr, sigma_next) then return end
|
||||
|
||||
-- Re-read in case hooks modified xt
|
||||
x = fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
-- Write final x0
|
||||
tbl_to_fa(x, xt, n)
|
||||
|
||||
if do_tele then
|
||||
print("\nSTORM_DATA:[" .. table.concat(telemetry_data, ",") .. "]\n")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,739 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Trajectory Anchor v2.0 — Latent Path Stabilizer
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- A stateful trajectory stabilization solver for HOT-Step-CPP.
|
||||
-- Runs via step() — NOT owns_loop. The guider pipeline (APG, ADG, PMG, etc.)
|
||||
-- remains fully active. Receives the pre-guided vt from the engine and applies
|
||||
-- stateful corrections on top of the advancing latent.
|
||||
--
|
||||
-- WHY step() INSTEAD OF owns_loop:
|
||||
-- All features here (inertia, concept lock, anchors, look-back) only need
|
||||
-- xt — the latent tensor — which step() provides directly. owns_loop was
|
||||
-- used in V1/V2 because guidance features needed cond/uncond, but those
|
||||
-- have been removed. step() is the correct, minimal contract for this work.
|
||||
-- Guiders run normally alongside this solver.
|
||||
--
|
||||
-- PIPELINE PER STEP:
|
||||
-- xt → Euler advance (xt + dt * vt) → _out_buf
|
||||
-- → [entropy measurement] — Shannon H from xt (step 0+)
|
||||
-- → [latent pressure] — entropy×RMS correction (toggle, off)
|
||||
-- → [memory buffer] — 3-step ring buffer smoothing
|
||||
-- → [inertia engine] — EMA velocity carry-over
|
||||
-- → [concept lock] — stability mask, sigma-adaptive
|
||||
-- → [identity anchor] — mid-sigma snapshot pull-back
|
||||
-- → [tonal anchor] — spectral centroid correction, sigma-adaptive
|
||||
-- → [look-back smoother] — SNR-adaptive EMA (arXiv:2602.09449)
|
||||
-- → [RMS servo] — descending RMS ceiling (toggle, off)
|
||||
-- → [safety clamp + NaN guard] — abs ceiling + Euler rollback on NaN
|
||||
-- → write _out_buf to xt
|
||||
--
|
||||
-- STATE RESET:
|
||||
-- All module-level state resets on step_index == 0 OR n change.
|
||||
-- Same-length consecutive generations do not bleed state.
|
||||
--
|
||||
-- INERTIA EMA FIX (V2 regression):
|
||||
-- V2 computed: vel = 0.8 * vel + 0.2 * vel (no-op, same buffer).
|
||||
-- V1.0 uses two separate buffers: _vel_old_buf (EMA) and _vel_raw_buf (delta).
|
||||
-- EMA: _vel_old_buf[i] = 0.8 * _vel_old_buf[i] + 0.2 * _vel_raw_buf[i]
|
||||
--
|
||||
-- SIGMA-ADAPTIVE FEATURES (from OmniRelational V3 pattern):
|
||||
-- concept lock strength = full * (sigma_ratio ^ concept_sigma_power)
|
||||
-- tonal correction scale = tonal_strength * sigma_ratio
|
||||
-- look-back weight = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
-- All three are heavy at high sigma (structure phase), fade to zero at sigma=0.
|
||||
--
|
||||
-- PARAMS:
|
||||
-- warmup_steps — skip stateful features for first N steps
|
||||
-- inertia_engine — EMA latent velocity carry-over
|
||||
-- inertia_alpha — base velocity coefficient, entropy-modulated
|
||||
-- memory_buffer — 3-step ring buffer output smoothing
|
||||
-- memory_blend — history blend fraction
|
||||
-- concept_lock — stability mask on settled regions
|
||||
-- concept_sigma_power — how fast lock fades with sigma
|
||||
-- identity_anchor — captures xt snapshot at anchor_sigma, pulls back
|
||||
-- anchor_sigma — sigma fraction at which anchors are captured
|
||||
-- anchor_blend — pull strength toward identity anchor
|
||||
-- tonal_anchor — spectral centroid drift correction
|
||||
-- tonal_strength — correction scale (per-element hard cap 0.1%)
|
||||
-- look_back_enabled — SNR-adaptive latent EMA smoother
|
||||
-- look_back_lambda — max smoothing weight at high sigma
|
||||
-- look_back_snr_power — falloff exponent
|
||||
-- rms_servo — descending RMS ceiling (off by default)
|
||||
-- rms_target_min — RMS ceiling at low sigma
|
||||
-- rms_target_max — RMS ceiling at high sigma
|
||||
-- rms_servo_gain — servo correction aggressiveness
|
||||
-- latent_pressure — entropy×RMS target correction (off by default)
|
||||
-- pressure_target_rms — RMS target for pressure correction
|
||||
-- pressure_target_entropy — entropy target for pressure weighting
|
||||
-- safety_clamp — max absolute latent value
|
||||
-- ============================================================================
|
||||
|
||||
solver = {
|
||||
name = "md_trajectory_anchor",
|
||||
display = "MD Trajectory Anchor",
|
||||
description = "Latent path stabilizer. step() solver — guiders stay active. Inertia engine, concept lock, identity anchor, tonal anchor, memory buffer, look-back smoother, RMS servo. All stateful. State resets cleanly between generations. Eta=0 for pure ODE, eta>0 for SDE noise.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
params = {
|
||||
-- ── Warmup ────────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "warmup_steps",
|
||||
type = "slider",
|
||||
label = "Warmup Steps",
|
||||
default = 2,
|
||||
min = 0,
|
||||
max = 6,
|
||||
step = 1,
|
||||
hint = "Skip stateful features (inertia, concept lock, anchors) for first N steps. Latent is mostly noise at high sigma — anchoring into chaos makes things worse. 2=recommended. 0=always active.",
|
||||
},
|
||||
-- ── Inertia Engine ────────────────────────────────────────────────────
|
||||
{
|
||||
key = "inertia_engine",
|
||||
type = "toggle",
|
||||
label = "Inertia Engine",
|
||||
default = true,
|
||||
hint = "EMA-smoothed latent velocity carry-over. Adds step-to-step momentum — reduces abrupt trajectory direction changes. Alpha is entropy-modulated: less inertia when latent is already structured (low entropy).",
|
||||
},
|
||||
{
|
||||
key = "inertia_alpha",
|
||||
type = "slider",
|
||||
label = "Inertia Alpha",
|
||||
default = 0.15,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Base velocity carry-over coefficient. 0.10=subtle. 0.20=noticeable. 0.30+=strong. Scaled down at runtime when entropy is low (structured latent needs less push).",
|
||||
},
|
||||
-- ── Memory Buffer ─────────────────────────────────────────────────────
|
||||
{
|
||||
key = "memory_buffer",
|
||||
type = "toggle",
|
||||
label = "Memory Buffer",
|
||||
default = true,
|
||||
hint = "Blends last 3 step outputs into the current step output. Suppresses step-to-step jitter without redirecting the trajectory. Ring buffer, zero-alloc.",
|
||||
},
|
||||
{
|
||||
key = "memory_blend",
|
||||
type = "slider",
|
||||
label = "Memory Blend",
|
||||
default = 0.12,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Fraction of 3-step history mean blended into each step output. 0.12=subtle. 0.25+=heavy smoothing (may soften transients in audio).",
|
||||
},
|
||||
-- ── Concept Lock ──────────────────────────────────────────────────────
|
||||
{
|
||||
key = "concept_lock",
|
||||
type = "toggle",
|
||||
label = "Concept Lock",
|
||||
default = true,
|
||||
hint = "Stability mask: elements with small step-to-step delta are pulled back toward their previous state. Protects settled structure from noise. Sigma-adaptive — full strength at high sigma, fades at low sigma (detail phase).",
|
||||
},
|
||||
{
|
||||
key = "concept_sigma_power",
|
||||
type = "slider",
|
||||
label = "Concept Lock Sigma Power",
|
||||
default = 1.0,
|
||||
min = 0.25,
|
||||
max = 3.0,
|
||||
step = 0.25,
|
||||
hint = "Controls how fast concept lock fades as sigma decreases. 1.0=linear decay. 2.0=quadratic (lock concentrated on early structure steps only). 0.5=slow fade (lock persists into detail steps).",
|
||||
},
|
||||
-- ── Identity Anchor ───────────────────────────────────────────────────
|
||||
{
|
||||
key = "identity_anchor",
|
||||
type = "toggle",
|
||||
label = "Identity Anchor",
|
||||
default = true,
|
||||
hint = "Captures a snapshot of xt at anchor_sigma, then gently pulls toward it on all subsequent steps. Prevents late-stage structural drift. Tonal anchor fires at the same sigma.",
|
||||
},
|
||||
{
|
||||
key = "anchor_sigma",
|
||||
type = "slider",
|
||||
label = "Anchor Sigma",
|
||||
default = 0.5,
|
||||
min = 0.1,
|
||||
max = 0.9,
|
||||
step = 0.05,
|
||||
hint = "Sigma level (as fraction of sigma_max) at which the identity and tonal anchors are captured. 0.5=mid-generation. Lower=locks in more detail. Higher=locks coarser structure only.",
|
||||
},
|
||||
{
|
||||
key = "anchor_blend",
|
||||
type = "slider",
|
||||
label = "Anchor Blend",
|
||||
default = 0.08,
|
||||
min = 0.01,
|
||||
max = 0.30,
|
||||
step = 0.01,
|
||||
hint = "Pull strength toward identity anchor per step. 0.08=gentle (recommended). 0.15=noticeable. Setting too high constrains creative refinement after anchor capture.",
|
||||
},
|
||||
-- ── Tonal Anchor ──────────────────────────────────────────────────────
|
||||
{
|
||||
key = "tonal_anchor",
|
||||
type = "toggle",
|
||||
label = "Tonal Anchor",
|
||||
default = true,
|
||||
hint = "Captures spectral centroid and band energy ratios at anchor_sigma. Applies centroid drift correction and band ratio correction on subsequent steps. Sigma-adaptive — correction strength fades proportionally with sigma.",
|
||||
},
|
||||
{
|
||||
key = "tonal_strength",
|
||||
type = "slider",
|
||||
label = "Tonal Strength",
|
||||
default = 0.15,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Tonal correction scale. Each per-element correction is hard-capped at 0.1% per step regardless of this value. 0.10-0.20=recommended for audio. Higher values widen the correction window but the cap still applies.",
|
||||
},
|
||||
-- ── Look-Back Smoother ────────────────────────────────────────────────
|
||||
{
|
||||
key = "look_back_enabled",
|
||||
type = "toggle",
|
||||
label = "Look-Back Smoother",
|
||||
default = true,
|
||||
hint = "SNR-adaptive latent EMA. Blends current output toward previous step output, weighted heavily at high sigma (structure), fading to zero at low sigma (detail). Suppresses ODE manifold shearing and harmonic hum. arXiv:2602.09449.",
|
||||
},
|
||||
{
|
||||
key = "look_back_lambda",
|
||||
type = "slider",
|
||||
label = "Look-Back Lambda",
|
||||
default = 0.55,
|
||||
min = 0.05,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Max smoothing weight at sigma=sigma_max. 0.55=25-step DDIM (default). 0.35=35-step simple. Always fades to zero at sigma=0 regardless of this value.",
|
||||
},
|
||||
{
|
||||
key = "look_back_snr_power",
|
||||
type = "slider",
|
||||
label = "Look-Back SNR Power",
|
||||
default = 1.3,
|
||||
min = 0.5,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Falloff exponent for look-back weight. 1.3=25-step DDIM. 1.5=35-step simple. Higher = smoothing concentrated on early structure steps only.",
|
||||
},
|
||||
-- ── RMS Servo ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "rms_servo",
|
||||
type = "toggle",
|
||||
label = "RMS Servo",
|
||||
default = false,
|
||||
hint = "Downward-only RMS ceiling. Prevents latent energy runaway without hard clipping. Off by default — calibrate target_min and target_max for your domain before enabling. ACE-Step latents run ~2.0 RMS.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_min",
|
||||
type = "slider",
|
||||
label = "RMS Target Min",
|
||||
default = 1.2,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at low sigma (late/detail steps). ACE-Step latents ~2.0 RMS at x0. Start at 1.2-1.8 and observe results.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_max",
|
||||
type = "slider",
|
||||
label = "RMS Target Max",
|
||||
default = 2.5,
|
||||
min = 0.5,
|
||||
max = 5.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at high sigma (early/structure steps). Should be >= target_min. ACE-Step early sigma ~2.5-3.5. Servo only fires downward.",
|
||||
},
|
||||
{
|
||||
key = "rms_servo_gain",
|
||||
type = "slider",
|
||||
label = "RMS Servo Gain",
|
||||
default = 0.6,
|
||||
min = 0.1,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Servo correction aggressiveness. 0.6=gradual correction. 1.0=hard snap to target each step. Lower is smoother but slower to converge.",
|
||||
},
|
||||
-- ── Latent Pressure ───────────────────────────────────────────────────
|
||||
{
|
||||
key = "latent_pressure",
|
||||
type = "toggle",
|
||||
label = "Latent Pressure",
|
||||
default = false,
|
||||
hint = "Applies a small per-step RMS correction weighted by Shannon entropy. Nudges latent toward a healthy entropy×RMS product. Off by default — tune target params before enabling. Correction capped at 0.05% per step.",
|
||||
},
|
||||
{
|
||||
key = "pressure_target_rms",
|
||||
type = "slider",
|
||||
label = "Pressure Target RMS",
|
||||
default = 2.0,
|
||||
min = 0.5,
|
||||
max = 4.0,
|
||||
step = 0.1,
|
||||
hint = "RMS component of pressure target. ACE-Step ~2.0. Correction direction flips if current entropy×RMS is above target.",
|
||||
},
|
||||
{
|
||||
key = "pressure_target_entropy",
|
||||
type = "slider",
|
||||
label = "Pressure Target Entropy",
|
||||
default = 7.5,
|
||||
min = 1.0,
|
||||
max = 15.0,
|
||||
step = 0.5,
|
||||
hint = "Shannon entropy component of pressure target. 7.5=image-domain default. Audio domain may differ — run with verbose output and measure entropy distribution before setting this.",
|
||||
},
|
||||
-- ── SDE Noise ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "eta",
|
||||
type = "slider",
|
||||
label = "Noise Injection (0 = ODE)",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "0 = pure deterministic ODE (default, recommended). >0 = SDE mode — injects ancestral noise scaled by t_prev × eta each step. Try 0.05-0.15 for subtle stochasticity. Higher values may overpower the stabilization features.",
|
||||
},
|
||||
{
|
||||
key = "seed",
|
||||
type = "slider",
|
||||
label = "Seed",
|
||||
default = 42,
|
||||
min = 0,
|
||||
max = 999999,
|
||||
step = 1,
|
||||
hint = "RNG seed for SDE noise (only used when Noise Injection > 0). Deterministic per-step via seed + step_index × 7919.",
|
||||
},
|
||||
-- ── Safety ────────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "safety_clamp",
|
||||
type = "slider",
|
||||
label = "Safety Clamp",
|
||||
default = 2.5,
|
||||
min = 1.0,
|
||||
max = 5.0,
|
||||
step = 0.1,
|
||||
hint = "Max absolute latent value after all corrections. NaN/Inf triggers a full rollback to raw Euler output before clamping. 2.5=standard. Raise to 4.0+ if clamping is audible.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- ── Constants ─────────────────────────────────────────────────────────────────
|
||||
local EPSILON = 1e-8
|
||||
local PRESSURE_CAP = 5e-4 -- max pressure correction per step (0.05%)
|
||||
|
||||
local function make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
local function normal(u1, u2)
|
||||
return math.sqrt(-2.0 * math.log(math.max(u1, EPSILON))) * math.cos(2.0 * math.pi * u2)
|
||||
end
|
||||
|
||||
-- ── Hoisted Buffers (Zero Allocation Hot Loop) ────────────────────────────────
|
||||
-- Sized on first run or n-change. Reused every step — no GC pressure.
|
||||
local _last_n = 0
|
||||
local _out_buf = {} -- working output for this step
|
||||
local _fallback_buf = {} -- raw Euler output (NaN rollback)
|
||||
local _vel_old_buf = {} -- EMA velocity (carries across steps)
|
||||
local _vel_raw_buf = {} -- raw velocity delta (computed this step)
|
||||
local _anchor_buf = {} -- identity anchor snapshot (frozen at anchor_sigma)
|
||||
local _prev_out_buf = {} -- previous step final output (inertia + concept lock + look-back)
|
||||
local _hist_mean_buf = {} -- history mean scratch
|
||||
local _history = { {}, {}, {} } -- ring buffer (3 slots, 0-indexed elements)
|
||||
|
||||
-- ── Module State (reset on n change or step_index == 0) ───────────────────────
|
||||
local _sigma_max = nil
|
||||
local _has_prev = false -- true after first step output is stored
|
||||
local _has_velocity = false -- true after first EMA velocity is initialized
|
||||
local _has_anchor = false -- true after identity anchor is captured
|
||||
local _tonal_ref_centroid = nil
|
||||
local _tonal_ref_bands = nil
|
||||
local _last_entropy = 7.5
|
||||
local _hist_head = 1
|
||||
local _hist_count = 0
|
||||
|
||||
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
local function bool_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return p[key]
|
||||
end
|
||||
|
||||
local function num_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return tonumber(p[key]) or default
|
||||
end
|
||||
|
||||
local function rms(a, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + a[i] * a[i] end
|
||||
return math.sqrt(s / n + EPSILON)
|
||||
end
|
||||
|
||||
local function shannon_entropy(a, n)
|
||||
local sum = 0.0
|
||||
for i = 0, n - 1 do sum = sum + math.abs(a[i]) + 1e-7 end
|
||||
local inv_sum = 1.0 / (sum + 1e-8)
|
||||
local H = 0.0
|
||||
for i = 0, n - 1 do
|
||||
local p = (math.abs(a[i]) + 1e-7) * inv_sum
|
||||
H = H - p * math.log(p + EPSILON) / math.log(2.0)
|
||||
end
|
||||
H = math.max(0.05, H)
|
||||
if H ~= H or H == math.huge or H == -math.huge then H = 5.0 end
|
||||
return H
|
||||
end
|
||||
|
||||
local function spectral_centroid(a, n)
|
||||
local sum_mag, sum_w = 0.0, 0.0
|
||||
for i = 0, n - 1 do
|
||||
local m = math.abs(a[i])
|
||||
sum_mag = sum_mag + m
|
||||
sum_w = sum_w + m * i
|
||||
end
|
||||
if sum_mag < EPSILON then return 0.0 end
|
||||
return sum_w / sum_mag
|
||||
end
|
||||
|
||||
local function band_energy(a, n)
|
||||
local bands = {0.0, 0.0, 0.0, 0.0}
|
||||
local bsize = math.floor(n / 4)
|
||||
for b = 0, 3 do
|
||||
local s = 0.0
|
||||
local lo = b * bsize
|
||||
local hi = (b == 3) and (n - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do s = s + math.abs(a[i]) end
|
||||
bands[b + 1] = s / math.max(hi - lo + 1, 1)
|
||||
end
|
||||
return bands
|
||||
end
|
||||
|
||||
local function is_safe(a, n)
|
||||
for i = 0, n - 1 do
|
||||
local v = a[i]
|
||||
if v ~= v or v == math.huge or v == -math.huge then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- ── step() ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function step(xt, vt, t_curr, t_prev, n)
|
||||
|
||||
-- ── 0. Read params ────────────────────────────────────────────────────────
|
||||
local warmup = math.floor(num_param(params, "warmup_steps", 2))
|
||||
local f_inertia = bool_param(params, "inertia_engine", true)
|
||||
local inertia_a = num_param(params, "inertia_alpha", 0.15)
|
||||
local f_memory = bool_param(params, "memory_buffer", true)
|
||||
local mem_blend = num_param(params, "memory_blend", 0.12)
|
||||
local f_concept = bool_param(params, "concept_lock", true)
|
||||
local concept_power = num_param(params, "concept_sigma_power", 1.0)
|
||||
local f_anchor = bool_param(params, "identity_anchor", true)
|
||||
local anchor_sigma = num_param(params, "anchor_sigma", 0.5)
|
||||
local anchor_blend = num_param(params, "anchor_blend", 0.08)
|
||||
local f_tonal = bool_param(params, "tonal_anchor", true)
|
||||
local tonal_str = num_param(params, "tonal_strength", 0.15)
|
||||
local f_lookback = bool_param(params, "look_back_enabled", true)
|
||||
local lb_lambda = num_param(params, "look_back_lambda", 0.55)
|
||||
local lb_snr_power = num_param(params, "look_back_snr_power", 1.3)
|
||||
local f_rms = bool_param(params, "rms_servo", false)
|
||||
local rms_tgt_min = num_param(params, "rms_target_min", 1.2)
|
||||
local rms_tgt_max = num_param(params, "rms_target_max", 2.5)
|
||||
local rms_gain = num_param(params, "rms_servo_gain", 0.6)
|
||||
local f_pressure = bool_param(params, "latent_pressure", false)
|
||||
local p_tgt_rms = num_param(params, "pressure_target_rms", 2.0)
|
||||
local p_tgt_entropy = num_param(params, "pressure_target_entropy", 7.5)
|
||||
local eta = num_param(params, "eta", 0.0)
|
||||
local seed = math.floor(num_param(params, "seed", 42))
|
||||
local sclamp = num_param(params, "safety_clamp", 2.5)
|
||||
|
||||
local step_idx = step_index or 0
|
||||
|
||||
-- ── 1. State reset (generation start or n change) ─────────────────────────
|
||||
-- n change: new latent shape (different duration/channels)
|
||||
-- step_idx == 0: new generation with same shape — must reset or prev
|
||||
-- generation's final state bleeds into next run's step 1
|
||||
if n ~= _last_n or step_idx == 0 then
|
||||
_sigma_max = nil
|
||||
_has_prev = false
|
||||
_has_velocity = false
|
||||
_has_anchor = false
|
||||
_tonal_ref_centroid = nil
|
||||
_tonal_ref_bands = nil
|
||||
_last_entropy = 7.5
|
||||
_hist_head = 1
|
||||
_hist_count = 0
|
||||
-- Resize hoisted buffers
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = 0.0
|
||||
_fallback_buf[i] = 0.0
|
||||
_vel_old_buf[i] = 0.0
|
||||
_vel_raw_buf[i] = 0.0
|
||||
_anchor_buf[i] = 0.0
|
||||
_prev_out_buf[i] = 0.0
|
||||
_hist_mean_buf[i] = 0.0
|
||||
_history[1][i] = 0.0
|
||||
_history[2][i] = 0.0
|
||||
_history[3][i] = 0.0
|
||||
end
|
||||
_last_n = n
|
||||
end
|
||||
|
||||
-- Capture sigma_max on first step of this generation
|
||||
if _sigma_max == nil then _sigma_max = t_curr end
|
||||
|
||||
-- sigma_ratio: 1.0 at high sigma (early), 0.0 at sigma=0 (final step)
|
||||
local sigma_ratio = clamp(t_curr / math.max(_sigma_max, EPSILON), 0.0, 1.0)
|
||||
|
||||
-- Warmup gate: stateful features are skipped for first `warmup` steps
|
||||
local past_warmup = (step_idx >= warmup)
|
||||
|
||||
-- ── 2. Entropy measurement (always, from step 0) ──────────────────────────
|
||||
-- Measured from xt (input), not the output. Represents current latent state.
|
||||
_last_entropy = shannon_entropy(xt, n)
|
||||
|
||||
-- ── 3. Euler advance ──────────────────────────────────────────────────────
|
||||
-- dt = t_prev - t_curr. t decrements each step, so dt < 0 (standard).
|
||||
-- x_next = xt + dt * vt
|
||||
local dt = t_prev - t_curr
|
||||
for i = 0, n - 1 do
|
||||
local v = xt[i] + dt * vt[i]
|
||||
_out_buf[i] = v
|
||||
_fallback_buf[i] = v -- save raw Euler for NaN rollback
|
||||
end
|
||||
|
||||
-- ── 4. Latent Pressure (always if enabled, from step 0) ───────────────────
|
||||
-- Nudges latent RMS toward pressure_target_rms, weighted by entropy proximity
|
||||
-- to pressure_target_entropy. Correction hard-capped at PRESSURE_CAP per step.
|
||||
if f_pressure then
|
||||
local cur_rms = rms(_out_buf, n)
|
||||
local target_product = p_tgt_entropy * p_tgt_rms
|
||||
local cur_product = _last_entropy * cur_rms
|
||||
local correction = clamp(
|
||||
(target_product - cur_product) / (target_product + EPSILON),
|
||||
-PRESSURE_CAP, PRESSURE_CAP
|
||||
)
|
||||
if math.abs(correction) > 1e-6 then
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] * (1.0 + correction) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Stateful features below: all gated on past_warmup AND _has_prev ────────
|
||||
|
||||
-- ── 5. Memory Buffer ──────────────────────────────────────────────────────
|
||||
-- Blends mean of last 3 step outputs into current output.
|
||||
-- Ring buffer: _hist_head cycles 1→2→3→1. _hist_count tracks fill level.
|
||||
if f_memory and past_warmup and _hist_count > 0 then
|
||||
for i = 0, n - 1 do _hist_mean_buf[i] = 0.0 end
|
||||
local hw = 1.0 / _hist_count
|
||||
for h = 1, _hist_count do
|
||||
for i = 0, n - 1 do _hist_mean_buf[i] = _hist_mean_buf[i] + _history[h][i] end
|
||||
end
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - mem_blend) * _out_buf[i] + mem_blend * (_hist_mean_buf[i] * hw)
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 6. Inertia Engine ─────────────────────────────────────────────────────
|
||||
-- EMA velocity = smoothed step-to-step output delta.
|
||||
-- Velocity raw this step: _out_buf - _prev_out_buf (output delta).
|
||||
-- EMA update: vel_old = 0.8 * vel_old + 0.2 * vel_raw (two separate buffers)
|
||||
-- Alpha entropy-modulated: less inertia when latent is structured (low H).
|
||||
if f_inertia and past_warmup and _has_prev then
|
||||
-- Compute raw velocity delta into _vel_raw_buf
|
||||
for i = 0, n - 1 do _vel_raw_buf[i] = _out_buf[i] - _prev_out_buf[i] end
|
||||
-- EMA update or initialization
|
||||
if _has_velocity then
|
||||
for i = 0, n - 1 do
|
||||
_vel_old_buf[i] = 0.8 * _vel_old_buf[i] + 0.2 * _vel_raw_buf[i]
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do _vel_old_buf[i] = _vel_raw_buf[i] end
|
||||
_has_velocity = true
|
||||
end
|
||||
-- Alpha modulated by entropy: low entropy (structured) → less inertia
|
||||
local alpha = inertia_a * clamp(_last_entropy / 7.5, 0.0, 1.5)
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] + alpha * _vel_old_buf[i] end
|
||||
end
|
||||
|
||||
-- ── 7. Concept Lock ───────────────────────────────────────────────────────
|
||||
-- Stability mask: elements with small step-to-step delta get pulled back
|
||||
-- toward their previous state. Sigmoid-shaped lock weight per element.
|
||||
-- Sigma-adaptive: lock_w scaled by (sigma_ratio ^ concept_sigma_power)
|
||||
-- → full effect at high sigma, fades to zero at sigma=0.
|
||||
if f_concept and past_warmup and _has_prev then
|
||||
local sigma_mod = sigma_ratio ^ concept_power
|
||||
if sigma_mod > 1e-4 then
|
||||
for i = 0, n - 1 do
|
||||
local delta = math.abs(_out_buf[i] - _prev_out_buf[i])
|
||||
-- Sigmoid: regions with delta < ~0.05 get near-full lock
|
||||
local lock_w = (1.0 / (1.0 + math.exp(delta * 40.0 - 2.0))) * sigma_mod
|
||||
_out_buf[i] = (1.0 - lock_w) * _out_buf[i] + lock_w * _prev_out_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 8. Identity Anchor ────────────────────────────────────────────────────
|
||||
-- Captures _out_buf snapshot when sigma_ratio crosses anchor_sigma threshold.
|
||||
-- On subsequent steps: gentle pull back toward the captured snapshot.
|
||||
-- Anchor sigma is a ratio of sigma_max (same as OmniRelational pattern).
|
||||
if f_anchor and past_warmup then
|
||||
if not _has_anchor and sigma_ratio <= anchor_sigma then
|
||||
-- Capture snapshot
|
||||
for i = 0, n - 1 do _anchor_buf[i] = _out_buf[i] end
|
||||
_has_anchor = true
|
||||
elseif _has_anchor then
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - anchor_blend) * _out_buf[i] + anchor_blend * _anchor_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 9. Tonal Anchor ───────────────────────────────────────────────────────
|
||||
-- Captures spectral centroid and 4-band energy ratios at anchor_sigma.
|
||||
-- Correction: per-element tilt for centroid drift + per-band ratio correction.
|
||||
-- Each per-element correction hard-capped at ±0.1% regardless of tonal_str.
|
||||
-- Sigma-adaptive: effective_str = tonal_str * sigma_ratio
|
||||
-- → full correction just after capture, fades to zero at sigma=0.
|
||||
if f_tonal and past_warmup then
|
||||
if _tonal_ref_centroid == nil and sigma_ratio <= anchor_sigma then
|
||||
-- Capture reference (fires same step as identity anchor)
|
||||
_tonal_ref_centroid = spectral_centroid(_out_buf, n)
|
||||
_tonal_ref_bands = band_energy(_out_buf, n)
|
||||
elseif _tonal_ref_centroid ~= nil then
|
||||
-- Sigma-adaptive correction scale
|
||||
local eff_str = tonal_str * sigma_ratio
|
||||
if eff_str > 1e-6 then
|
||||
local curr_centroid = spectral_centroid(_out_buf, n)
|
||||
local curr_bands = band_energy(_out_buf, n)
|
||||
|
||||
-- Centroid drift: linear tilt across elements, capped at 0.1%
|
||||
local drift_norm = (curr_centroid - _tonal_ref_centroid) /
|
||||
(math.abs(_tonal_ref_centroid) + EPSILON)
|
||||
local tilt = clamp(-drift_norm * eff_str, -1e-3, 1e-3)
|
||||
local center = (n - 1) / 2.0
|
||||
for i = 0, n - 1 do
|
||||
local dist_w = (i - center) / (center + EPSILON)
|
||||
_out_buf[i] = _out_buf[i] + tilt * dist_w * math.abs(_out_buf[i])
|
||||
end
|
||||
|
||||
-- Band energy ratio correction, capped at 0.1% per band
|
||||
local ref_total, curr_total = 0.0, 0.0
|
||||
for b = 1, 4 do
|
||||
ref_total = ref_total + _tonal_ref_bands[b]
|
||||
curr_total = curr_total + curr_bands[b]
|
||||
end
|
||||
if ref_total > EPSILON and curr_total > EPSILON then
|
||||
local bsize = math.floor(n / 4)
|
||||
for b = 0, 3 do
|
||||
local ref_ratio = _tonal_ref_bands[b + 1] / ref_total
|
||||
local curr_ratio = curr_bands[b + 1] / curr_total
|
||||
local band_corr = clamp((ref_ratio - curr_ratio) * eff_str, -1e-3, 1e-3)
|
||||
local lo = b * bsize
|
||||
local hi = (b == 3) and (n - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do
|
||||
_out_buf[i] = _out_buf[i] + band_corr * math.abs(_out_buf[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 10. Look-Back Smoother ────────────────────────────────────────────────
|
||||
-- SNR-adaptive EMA: lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
-- Blends current output toward previous step output.
|
||||
-- Heavy at high sigma (structure coherence), zero at sigma=0 (preserve detail).
|
||||
-- Pattern from MD PingPong. arXiv:2602.09449.
|
||||
if f_lookback and past_warmup and _has_prev then
|
||||
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
if lb_w > 1e-6 then
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - lb_w) * _out_buf[i] + lb_w * _prev_out_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 11. RMS Servo ─────────────────────────────────────────────────────────
|
||||
-- Downward-only RMS ceiling: fires only when cur_rms > rms_target.
|
||||
-- Target descends from rms_target_max (high sigma) to rms_target_min (low sigma).
|
||||
-- Curve: target = min + sigma_ratio^0.6 * (max - min) (from PingPong).
|
||||
-- Pattern from MD PingPong.
|
||||
if f_rms then
|
||||
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
|
||||
local cur_rms = rms(_out_buf, n)
|
||||
if cur_rms > rms_target then
|
||||
local servo_rms = cur_rms + rms_gain * (rms_target - cur_rms)
|
||||
local scale = servo_rms / cur_rms
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] * scale end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 12. Safety Clamp + NaN Guard ──────────────────────────────────────────
|
||||
-- NaN/Inf in output: roll back to raw Euler result before clamping.
|
||||
-- Abs ceiling applied regardless.
|
||||
if not is_safe(_out_buf, n) then
|
||||
for i = 0, n - 1 do _out_buf[i] = _fallback_buf[i] end
|
||||
end
|
||||
for i = 0, n - 1 do _out_buf[i] = clamp(_out_buf[i], -sclamp, sclamp) end
|
||||
|
||||
-- ── 13. Update state ──────────────────────────────────────────────────────
|
||||
-- Store this step's output as prev_out_buf for next step.
|
||||
-- Also push to memory ring buffer.
|
||||
if past_warmup then
|
||||
for i = 0, n - 1 do _prev_out_buf[i] = _out_buf[i] end
|
||||
_has_prev = true
|
||||
-- Ring buffer push
|
||||
if f_memory then
|
||||
for i = 0, n - 1 do _history[_hist_head][i] = _out_buf[i] end
|
||||
_hist_head = _hist_head + 1
|
||||
if _hist_head > 3 then _hist_head = 1 end
|
||||
if _hist_count < 3 then _hist_count = _hist_count + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 14. Write output ──────────────────────────────────────────────────────
|
||||
for i = 0, n - 1 do xt[i] = _out_buf[i] end
|
||||
|
||||
-- ── 15. SDE Noise Injection ───────────────────────────────────────────────
|
||||
-- Applied after write-back, outside the safety clamp, matching OmniRelational
|
||||
-- convention. scale = t_prev * eta — noise magnitude tracks current sigma level,
|
||||
-- naturally fades to zero as generation converges.
|
||||
if eta > 0.0 and t_prev > EPSILON then
|
||||
local rng = make_rng(seed + step_idx * 7919)
|
||||
local scale = t_prev * eta
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
xt[i] = xt[i] + normal(u1, u2) * scale
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,776 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- MD Trajectory Anchor V3 — Latent Path Stabilizer
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- A stateful trajectory stabilization solver for HOT-Step-CPP.
|
||||
-- Runs via step() — NOT owns_loop. The guider pipeline (APG, ADG, PMG, etc.)
|
||||
-- remains fully active. Receives the pre-guided vt from the engine and applies
|
||||
-- stateful corrections on top of the advancing latent.
|
||||
--
|
||||
-- WHY step() INSTEAD OF owns_loop:
|
||||
-- All features here (inertia, concept lock, anchors, look-back) only need
|
||||
-- xt — the latent tensor — which step() provides directly. owns_loop was
|
||||
-- used in V1/V2 because guidance features needed cond/uncond, but those
|
||||
-- have been removed. step() is the correct, minimal contract for this work.
|
||||
-- Guiders run normally alongside this solver.
|
||||
--
|
||||
-- PIPELINE PER STEP:
|
||||
-- xt → Euler advance (xt + dt * vt) → _out_buf
|
||||
-- → [entropy measurement] — Shannon H from xt (step 0+)
|
||||
-- → [latent pressure] — entropy×RMS correction (toggle, off)
|
||||
-- → [memory buffer] — 3-step ring buffer smoothing
|
||||
-- → [inertia engine] — EMA velocity carry-over
|
||||
-- → [concept lock] — stability mask, sigma-adaptive
|
||||
-- → [identity anchor] — mid-sigma snapshot pull-back
|
||||
-- → [tonal anchor] — spectral centroid correction, sigma-adaptive
|
||||
-- → [look-back smoother] — SNR-adaptive EMA (arXiv:2602.09449)
|
||||
-- → [RMS servo] — descending RMS ceiling (toggle, off)
|
||||
-- → [safety clamp + NaN guard] — abs ceiling + Euler rollback on NaN
|
||||
-- → write _out_buf to xt
|
||||
--
|
||||
-- STATE RESET:
|
||||
-- All module-level state resets on step_index == 0 OR n change.
|
||||
-- Same-length consecutive generations do not bleed state.
|
||||
--
|
||||
-- INERTIA EMA FIX (V2 regression):
|
||||
-- V2 computed: vel = 0.8 * vel + 0.2 * vel (no-op, same buffer).
|
||||
-- V1.0 uses two separate buffers: _vel_old_buf (EMA) and _vel_raw_buf (delta).
|
||||
-- EMA: _vel_old_buf[i] = 0.8 * _vel_old_buf[i] + 0.2 * _vel_raw_buf[i]
|
||||
--
|
||||
-- SIGMA-ADAPTIVE FEATURES (from OmniRelational V3 pattern):
|
||||
-- concept lock strength = full * (sigma_ratio ^ concept_sigma_power)
|
||||
-- tonal correction scale = tonal_strength * sigma_ratio
|
||||
-- look-back weight = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
-- All three are heavy at high sigma (structure phase), fade to zero at sigma=0.
|
||||
--
|
||||
-- PARAMS:
|
||||
-- warmup_steps — skip stateful features for first N steps
|
||||
-- inertia_engine — EMA latent velocity carry-over
|
||||
-- inertia_alpha — base velocity coefficient, entropy-modulated
|
||||
-- memory_buffer — 3-step ring buffer output smoothing
|
||||
-- memory_blend — history blend fraction
|
||||
-- concept_lock — stability mask on settled regions
|
||||
-- concept_sigma_power — how fast lock fades with sigma
|
||||
-- identity_anchor — captures xt snapshot at anchor_sigma, pulls back
|
||||
-- anchor_sigma — sigma fraction at which anchors are captured
|
||||
-- anchor_blend — pull strength toward identity anchor
|
||||
-- tonal_anchor — spectral centroid drift correction
|
||||
-- tonal_strength — correction scale (per-element hard cap 0.1%)
|
||||
-- look_back_enabled — SNR-adaptive latent EMA smoother
|
||||
-- look_back_lambda — max smoothing weight at high sigma
|
||||
-- look_back_snr_power — falloff exponent
|
||||
-- rms_servo — descending RMS ceiling (off by default)
|
||||
-- rms_target_min — RMS ceiling at low sigma
|
||||
-- rms_target_max — RMS ceiling at high sigma
|
||||
-- rms_servo_gain — servo correction aggressiveness
|
||||
-- latent_pressure — entropy×RMS target correction (off by default)
|
||||
-- pressure_target_rms — RMS target for pressure correction
|
||||
-- pressure_target_entropy — entropy target for pressure weighting
|
||||
-- safety_clamp — max absolute latent value
|
||||
-- ============================================================================
|
||||
|
||||
solver = {
|
||||
name = "md_trajectory_anchor_V3",
|
||||
display = "MD Trajectory Anchor V3",
|
||||
description = "Latent path stabilizer. step() solver — guiders stay active. Inertia engine, concept lock, identity anchor, tonal anchor, memory buffer, look-back smoother, RMS servo. All stateful. State resets cleanly between generations.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
params = {
|
||||
-- ── Warmup ────────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "warmup_steps",
|
||||
type = "slider",
|
||||
label = "Warmup Steps",
|
||||
default = 2,
|
||||
min = 0,
|
||||
max = 6,
|
||||
step = 1,
|
||||
hint = "Skip stateful features (inertia, concept lock, anchors) for first N steps. Latent is mostly noise at high sigma — anchoring into chaos makes things worse. 2=recommended. 0=always active.",
|
||||
},
|
||||
-- ── Inertia Engine ────────────────────────────────────────────────────
|
||||
{
|
||||
key = "inertia_engine",
|
||||
type = "toggle",
|
||||
label = "Inertia Engine",
|
||||
default = true,
|
||||
hint = "EMA-smoothed latent velocity carry-over. Adds step-to-step momentum — reduces abrupt trajectory direction changes. Alpha is entropy-modulated: less inertia when latent is already structured (low entropy).",
|
||||
},
|
||||
{
|
||||
key = "inertia_alpha",
|
||||
type = "slider",
|
||||
label = "Inertia Alpha",
|
||||
default = 0.15,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Base velocity carry-over coefficient. 0.10=subtle. 0.20=noticeable. 0.30+=strong. Scaled down at runtime when entropy is low (structured latent needs less push).",
|
||||
},
|
||||
-- ── Memory Buffer ─────────────────────────────────────────────────────
|
||||
{
|
||||
key = "memory_buffer",
|
||||
type = "toggle",
|
||||
label = "Memory Buffer",
|
||||
default = false,
|
||||
hint = "Blends last 3 step outputs into the current step output. Suppresses step-to-step jitter without redirecting the trajectory. Ring buffer, zero-alloc.",
|
||||
},
|
||||
{
|
||||
key = "memory_blend",
|
||||
type = "slider",
|
||||
label = "Memory Blend",
|
||||
default = 0.12,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Fraction of 3-step history mean blended into each step output. 0.12=subtle. 0.25+=heavy smoothing (may soften transients in audio).",
|
||||
},
|
||||
-- ── Concept Lock ──────────────────────────────────────────────────────
|
||||
{
|
||||
key = "concept_lock",
|
||||
type = "toggle",
|
||||
label = "Concept Lock",
|
||||
default = true,
|
||||
hint = "Stability mask: elements with small step-to-step delta are pulled back toward their previous state. Protects settled structure from noise. Sigma-adaptive — full strength at high sigma, fades at low sigma (detail phase).",
|
||||
},
|
||||
{
|
||||
key = "concept_sigma_power",
|
||||
type = "slider",
|
||||
label = "Concept Lock Sigma Power",
|
||||
default = 1.0,
|
||||
min = 0.25,
|
||||
max = 3.0,
|
||||
step = 0.25,
|
||||
hint = "Controls how fast concept lock fades as sigma decreases. 1.0=linear decay. 2.0=quadratic (lock concentrated on early structure steps only). 0.5=slow fade (lock persists into detail steps).",
|
||||
},
|
||||
-- ── Identity Anchor ───────────────────────────────────────────────────
|
||||
{
|
||||
key = "identity_anchor",
|
||||
type = "toggle",
|
||||
label = "Identity Anchor",
|
||||
default = false,
|
||||
hint = "Captures a snapshot of xt at anchor_sigma, then gently pulls toward it on all subsequent steps. Prevents late-stage structural drift. Tonal anchor fires at the same sigma.",
|
||||
},
|
||||
{
|
||||
key = "anchor_sigma",
|
||||
type = "slider",
|
||||
label = "Anchor Sigma",
|
||||
default = 0.5,
|
||||
min = 0.1,
|
||||
max = 0.9,
|
||||
step = 0.05,
|
||||
hint = "Sigma level (as fraction of sigma_max) at which the identity and tonal anchors are captured. 0.5=mid-generation. Lower=locks in more detail. Higher=locks coarser structure only.",
|
||||
},
|
||||
{
|
||||
key = "anchor_blend",
|
||||
type = "slider",
|
||||
label = "Anchor Blend",
|
||||
default = 0.08,
|
||||
min = 0.01,
|
||||
max = 0.30,
|
||||
step = 0.01,
|
||||
hint = "Pull strength toward identity anchor per step. 0.08=gentle (recommended). 0.15=noticeable. Setting too high constrains creative refinement after anchor capture.",
|
||||
},
|
||||
-- ── Tonal Anchor ──────────────────────────────────────────────────────
|
||||
{
|
||||
key = "tonal_anchor",
|
||||
type = "toggle",
|
||||
label = "Tonal Anchor",
|
||||
default = true,
|
||||
hint = "Captures spectral centroid and band energy ratios at anchor_sigma. Applies centroid drift correction and band ratio correction on subsequent steps. Sigma-adaptive — correction strength fades proportionally with sigma.",
|
||||
},
|
||||
{
|
||||
key = "tonal_strength",
|
||||
type = "slider",
|
||||
label = "Tonal Strength",
|
||||
default = 0.15,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Tonal correction scale. Each per-element correction is hard-capped at 0.1% per step regardless of this value. 0.10-0.20=recommended for audio. Higher values widen the correction window but the cap still applies.",
|
||||
},
|
||||
-- ── Look-Back Smoother ────────────────────────────────────────────────
|
||||
{
|
||||
key = "look_back_enabled",
|
||||
type = "toggle",
|
||||
label = "Look-Back Smoother",
|
||||
default = false,
|
||||
hint = "SNR-adaptive latent EMA. Blends current output toward previous step output, weighted heavily at high sigma (structure), fading to zero at low sigma (detail). Suppresses ODE manifold shearing and harmonic hum. arXiv:2602.09449.",
|
||||
},
|
||||
{
|
||||
key = "look_back_lambda",
|
||||
type = "slider",
|
||||
label = "Look-Back Lambda",
|
||||
default = 0.15,
|
||||
min = 0.05,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Max smoothing weight at sigma=sigma_max. 0.55=25-step DDIM (default). 0.35=35-step simple. Always fades to zero at sigma=0 regardless of this value.",
|
||||
},
|
||||
{
|
||||
key = "look_back_snr_power",
|
||||
type = "slider",
|
||||
label = "Look-Back SNR Power",
|
||||
default = 1.3,
|
||||
min = 0.5,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Falloff exponent for look-back weight. 1.3=25-step DDIM. 1.5=35-step simple. Higher = smoothing concentrated on early structure steps only.",
|
||||
},
|
||||
-- ── RMS Servo ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "rms_servo",
|
||||
type = "toggle",
|
||||
label = "RMS Servo",
|
||||
default = false,
|
||||
hint = "Downward-only RMS ceiling. Prevents latent energy runaway without hard clipping. Off by default — calibrate target_min and target_max for your domain before enabling. ACE-Step latents run ~2.0 RMS.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_min",
|
||||
type = "slider",
|
||||
label = "RMS Target Min",
|
||||
default = 1.2,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at low sigma (late/detail steps). ACE-Step latents ~2.0 RMS at x0. Start at 1.2-1.8 and observe results.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_max",
|
||||
type = "slider",
|
||||
label = "RMS Target Max",
|
||||
default = 2.5,
|
||||
min = 0.5,
|
||||
max = 5.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at high sigma (early/structure steps). Should be >= target_min. ACE-Step early sigma ~2.5-3.5. Servo only fires downward.",
|
||||
},
|
||||
{
|
||||
key = "rms_servo_gain",
|
||||
type = "slider",
|
||||
label = "RMS Servo Gain",
|
||||
default = 0.6,
|
||||
min = 0.1,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Servo correction aggressiveness. 0.6=gradual correction. 1.0=hard snap to target each step. Lower is smoother but slower to converge.",
|
||||
},
|
||||
-- ── Latent Pressure ───────────────────────────────────────────────────
|
||||
{
|
||||
key = "latent_pressure",
|
||||
type = "toggle",
|
||||
label = "Latent Pressure",
|
||||
default = false,
|
||||
hint = "Applies a small per-step RMS correction weighted by Shannon entropy. Nudges latent toward a healthy entropy×RMS product. Off by default — tune target params before enabling. Correction capped at 0.05% per step.",
|
||||
},
|
||||
{
|
||||
key = "pressure_target_rms",
|
||||
type = "slider",
|
||||
label = "Pressure Target RMS",
|
||||
default = 2.0,
|
||||
min = 0.5,
|
||||
max = 4.0,
|
||||
step = 0.1,
|
||||
hint = "RMS component of pressure target. ACE-Step ~2.0. Correction direction flips if current entropy×RMS is above target.",
|
||||
},
|
||||
{
|
||||
key = "pressure_target_entropy",
|
||||
type = "slider",
|
||||
label = "Pressure Target Entropy",
|
||||
default = 7.5,
|
||||
min = 1.0,
|
||||
max = 15.0,
|
||||
step = 0.5,
|
||||
hint = "Shannon entropy component of pressure target. 7.5=image-domain default. Audio domain may differ — run with verbose output and measure entropy distribution before setting this.",
|
||||
},
|
||||
-- ── SDE Noise ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "relational_weight",
|
||||
type = "slider",
|
||||
label = "Relational Weight",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Barbour Best Matching velocity decomposition. 0 = off. 0.3-0.5 = balanced.",
|
||||
},
|
||||
{
|
||||
key = "relational_sigma_power",
|
||||
type = "slider",
|
||||
label = "Relational Sigma Decay",
|
||||
default = 1.0,
|
||||
min = 0.25,
|
||||
max = 4.0,
|
||||
step = 0.25,
|
||||
hint = "How fast relational weight fades. 1.0 = linear.",
|
||||
},
|
||||
{
|
||||
key = "eta",
|
||||
type = "slider",
|
||||
label = "Eta (SDE Noise)",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Ancestral noise injection. 0=deterministic ODE (default). Scales with t_prev each step. Low values (0.05-0.15) add subtle stochasticity without overwhelming the stabilization features.",
|
||||
},
|
||||
{
|
||||
key = "seed",
|
||||
type = "slider",
|
||||
label = "Seed",
|
||||
default = 42,
|
||||
min = 0,
|
||||
max = 999999,
|
||||
step = 1,
|
||||
hint = "RNG seed for SDE noise. Deterministic per-step via seed + step_index * 7919.",
|
||||
},
|
||||
-- ── Safety ────────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "safety_clamp",
|
||||
type = "slider",
|
||||
label = "Safety Clamp",
|
||||
default = 2.5,
|
||||
min = 1.0,
|
||||
max = 5.0,
|
||||
step = 0.1,
|
||||
hint = "Max absolute latent value after all corrections. NaN/Inf triggers a full rollback to raw Euler output before clamping. 2.5=standard. Raise to 4.0+ if clamping is audible.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- ── Constants ─────────────────────────────────────────────────────────────────
|
||||
local EPSILON = 1e-8
|
||||
local PRESSURE_CAP = 5e-4 -- max pressure correction per step (0.05%)
|
||||
|
||||
local function make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
local function normal(u1, u2)
|
||||
return math.sqrt(-2.0 * math.log(math.max(u1, EPSILON))) * math.cos(2.0 * math.pi * u2)
|
||||
end
|
||||
|
||||
-- ── Hoisted Buffers (Zero Allocation Hot Loop) ────────────────────────────────
|
||||
-- Sized on first run or n-change. Reused every step — no GC pressure.
|
||||
local _last_n = 0
|
||||
local _out_buf = {} -- working output for this step
|
||||
local _fallback_buf = {} -- raw Euler output (NaN rollback)
|
||||
local _vel_old_buf = {} -- EMA velocity (carries across steps)
|
||||
local _vel_raw_buf = {} -- raw velocity delta (computed this step)
|
||||
local _anchor_buf = {} -- identity anchor snapshot (frozen at anchor_sigma)
|
||||
local _prev_out_buf = {} -- previous step final output (inertia + concept lock + look-back)
|
||||
local _hist_mean_buf = {} -- history mean scratch
|
||||
local _history = { {}, {}, {} } -- ring buffer (3 slots, 0-indexed elements)
|
||||
|
||||
-- ── Module State (reset on n change or step_index == 0) ───────────────────────
|
||||
local _sigma_max = nil
|
||||
local _has_prev = false -- true after first step output is stored
|
||||
local _has_velocity = false -- true after first EMA velocity is initialized
|
||||
local _has_anchor = false -- true after identity anchor is captured
|
||||
local _tonal_ref_centroid = nil
|
||||
local _tonal_ref_bands = nil
|
||||
local _last_entropy = 7.5
|
||||
local _hist_head = 1
|
||||
local _hist_count = 0
|
||||
|
||||
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
local function bool_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return p[key]
|
||||
end
|
||||
|
||||
local function num_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return tonumber(p[key]) or default
|
||||
end
|
||||
|
||||
local function rms(a, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + a[i] * a[i] end
|
||||
return math.sqrt(s / n + EPSILON)
|
||||
end
|
||||
|
||||
local function shannon_entropy(a, n)
|
||||
local sum = 0.0
|
||||
for i = 0, n - 1 do sum = sum + math.abs(a[i]) + 1e-7 end
|
||||
local inv_sum = 1.0 / (sum + 1e-8)
|
||||
local H = 0.0
|
||||
for i = 0, n - 1 do
|
||||
local p = (math.abs(a[i]) + 1e-7) * inv_sum
|
||||
H = H - p * math.log(p + EPSILON) / math.log(2.0)
|
||||
end
|
||||
H = math.max(0.05, H)
|
||||
if H ~= H or H == math.huge or H == -math.huge then H = 5.0 end
|
||||
return H
|
||||
end
|
||||
|
||||
local function spectral_centroid(a, n)
|
||||
local sum_mag, sum_w = 0.0, 0.0
|
||||
for i = 0, n - 1 do
|
||||
local m = math.abs(a[i])
|
||||
sum_mag = sum_mag + m
|
||||
sum_w = sum_w + m * i
|
||||
end
|
||||
if sum_mag < EPSILON then return 0.0 end
|
||||
return sum_w / sum_mag
|
||||
end
|
||||
|
||||
local function band_energy(a, n)
|
||||
local bands = {0.0, 0.0, 0.0, 0.0}
|
||||
local bsize = math.floor(n / 4)
|
||||
for b = 0, 3 do
|
||||
local s = 0.0
|
||||
local lo = b * bsize
|
||||
local hi = (b == 3) and (n - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do s = s + math.abs(a[i]) end
|
||||
bands[b + 1] = s / math.max(hi - lo + 1, 1)
|
||||
end
|
||||
return bands
|
||||
end
|
||||
|
||||
local function is_safe(a, n)
|
||||
for i = 0, n - 1 do
|
||||
local v = a[i]
|
||||
if v ~= v or v == math.huge or v == -math.huge then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- ── step() ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function step(xt, vt, t_curr, t_prev, n)
|
||||
|
||||
-- ── 0. Read params ────────────────────────────────────────────────────────
|
||||
local warmup = math.floor(num_param(params, "warmup_steps", 2))
|
||||
local f_inertia = bool_param(params, "inertia_engine", true)
|
||||
local inertia_a = num_param(params, "inertia_alpha", 0.15)
|
||||
local f_memory = bool_param(params, "memory_buffer", false)
|
||||
local mem_blend = num_param(params, "memory_blend", 0.12)
|
||||
local f_concept = bool_param(params, "concept_lock", true)
|
||||
local concept_power = num_param(params, "concept_sigma_power", 1.0)
|
||||
local f_anchor = bool_param(params, "identity_anchor", false)
|
||||
local anchor_sigma = num_param(params, "anchor_sigma", 0.5)
|
||||
local anchor_blend = num_param(params, "anchor_blend", 0.08)
|
||||
local f_tonal = bool_param(params, "tonal_anchor", true)
|
||||
local tonal_str = num_param(params, "tonal_strength", 0.15)
|
||||
local f_lookback = bool_param(params, "look_back_enabled", false)
|
||||
local lb_lambda = num_param(params, "look_back_lambda", 0.15)
|
||||
local lb_snr_power = num_param(params, "look_back_snr_power", 1.3)
|
||||
local f_rms = bool_param(params, "rms_servo", false)
|
||||
local rms_tgt_min = num_param(params, "rms_target_min", 1.2)
|
||||
local rms_tgt_max = num_param(params, "rms_target_max", 2.5)
|
||||
local rms_gain = num_param(params, "rms_servo_gain", 0.6)
|
||||
local f_pressure = bool_param(params, "latent_pressure", false)
|
||||
local p_tgt_rms = num_param(params, "pressure_target_rms", 2.0)
|
||||
local p_tgt_entropy = num_param(params, "pressure_target_entropy", 7.5)
|
||||
local eta = num_param(params, "eta", 0.0)
|
||||
local seed = math.floor(num_param(params, "seed", 42))
|
||||
local sclamp = num_param(params, "safety_clamp", 2.5)
|
||||
local rw = num_param(params, "relational_weight", 0.0)
|
||||
local rw_sig_pow = num_param(params, "relational_sigma_power", 1.0)
|
||||
|
||||
local step_idx = step_index or 0
|
||||
|
||||
-- ── 1. State reset (generation start or n change) ─────────────────────────
|
||||
-- n change: new latent shape (different duration/channels)
|
||||
-- step_idx == 0: new generation with same shape — must reset or prev
|
||||
-- generation's final state bleeds into next run's step 1
|
||||
if n ~= _last_n or step_idx == 0 then
|
||||
_sigma_max = nil
|
||||
_has_prev = false
|
||||
_has_velocity = false
|
||||
_has_anchor = false
|
||||
_tonal_ref_centroid = nil
|
||||
_tonal_ref_bands = nil
|
||||
_last_entropy = 7.5
|
||||
_hist_head = 1
|
||||
_hist_count = 0
|
||||
-- Resize hoisted buffers
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = 0.0
|
||||
_fallback_buf[i] = 0.0
|
||||
_vel_old_buf[i] = 0.0
|
||||
_vel_raw_buf[i] = 0.0
|
||||
_anchor_buf[i] = 0.0
|
||||
_prev_out_buf[i] = 0.0
|
||||
_hist_mean_buf[i] = 0.0
|
||||
_history[1][i] = 0.0
|
||||
_history[2][i] = 0.0
|
||||
_history[3][i] = 0.0
|
||||
end
|
||||
_last_n = n
|
||||
end
|
||||
|
||||
-- Capture sigma_max on first step of this generation
|
||||
if _sigma_max == nil then _sigma_max = t_curr end
|
||||
|
||||
-- sigma_ratio: 1.0 at high sigma (early), 0.0 at sigma=0 (final step)
|
||||
local sigma_ratio = clamp(t_curr / math.max(_sigma_max, EPSILON), 0.0, 1.0)
|
||||
|
||||
-- Warmup gate: stateful features are skipped for first `warmup` steps
|
||||
local past_warmup = (step_idx >= warmup)
|
||||
|
||||
-- ── 2. Entropy measurement (always, from step 0) ──────────────────────────
|
||||
-- Measured from xt (input), not the output. Represents current latent state.
|
||||
_last_entropy = shannon_entropy(xt, n)
|
||||
|
||||
-- ── 2b. Relational velocity decomposition ──────────────────────────────
|
||||
-- vt is read-only FloatArray, so we create a local velocity reference
|
||||
local vel = vt -- default: use vt directly (no copy overhead when rw=0)
|
||||
if rw > 0 and _sigma_max ~= nil then
|
||||
local v_tbl = {}
|
||||
for i = 0, n - 1 do v_tbl[i] = vt[i] end
|
||||
local x_tbl = {}
|
||||
for i = 0, n - 1 do x_tbl[i] = xt[i] end
|
||||
C.apply_relational(v_tbl, n, 1, n, sigma_ratio, _sigma_max,
|
||||
rw, rw_sig_pow, false, 0.85, x_tbl)
|
||||
vel = v_tbl
|
||||
end
|
||||
|
||||
-- ── 3. Euler advance ──────────────────────────────────────────────────────
|
||||
-- dt = t_prev - t_curr. t decrements each step, so dt < 0 (standard).
|
||||
-- x_next = xt + dt * vel
|
||||
local dt = t_prev - t_curr
|
||||
for i = 0, n - 1 do
|
||||
local v = xt[i] + dt * vel[i]
|
||||
_out_buf[i] = v
|
||||
_fallback_buf[i] = v -- save raw Euler for NaN rollback
|
||||
end
|
||||
|
||||
-- ── 4. Latent Pressure (always if enabled, from step 0) ───────────────────
|
||||
-- Nudges latent RMS toward pressure_target_rms, weighted by entropy proximity
|
||||
-- to pressure_target_entropy. Correction hard-capped at PRESSURE_CAP per step.
|
||||
if f_pressure then
|
||||
local cur_rms = rms(_out_buf, n)
|
||||
local target_product = p_tgt_entropy * p_tgt_rms
|
||||
local cur_product = _last_entropy * cur_rms
|
||||
local correction = clamp(
|
||||
(target_product - cur_product) / (target_product + EPSILON),
|
||||
-PRESSURE_CAP, PRESSURE_CAP
|
||||
)
|
||||
if math.abs(correction) > 1e-6 then
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] * (1.0 + correction) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Stateful features below: all gated on past_warmup AND _has_prev ────────
|
||||
|
||||
-- ── 5. Memory Buffer ──────────────────────────────────────────────────────
|
||||
-- Blends mean of last 3 step outputs into current output.
|
||||
-- Ring buffer: _hist_head cycles 1→2→3→1. _hist_count tracks fill level.
|
||||
if f_memory and past_warmup and _hist_count > 0 then
|
||||
for i = 0, n - 1 do _hist_mean_buf[i] = 0.0 end
|
||||
local hw = 1.0 / _hist_count
|
||||
for h = 1, _hist_count do
|
||||
for i = 0, n - 1 do _hist_mean_buf[i] = _hist_mean_buf[i] + _history[h][i] end
|
||||
end
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - mem_blend) * _out_buf[i] + mem_blend * (_hist_mean_buf[i] * hw)
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 6. Inertia Engine ─────────────────────────────────────────────────────
|
||||
-- EMA velocity = smoothed step-to-step output delta.
|
||||
-- Velocity raw this step: _out_buf - _prev_out_buf (output delta).
|
||||
-- EMA update: vel_old = 0.8 * vel_old + 0.2 * vel_raw (two separate buffers)
|
||||
-- Alpha entropy-modulated: less inertia when latent is structured (low H).
|
||||
if f_inertia and past_warmup and _has_prev then
|
||||
-- Compute raw velocity delta into _vel_raw_buf
|
||||
for i = 0, n - 1 do _vel_raw_buf[i] = _out_buf[i] - _prev_out_buf[i] end
|
||||
-- EMA update or initialization
|
||||
if _has_velocity then
|
||||
for i = 0, n - 1 do
|
||||
_vel_old_buf[i] = 0.8 * _vel_old_buf[i] + 0.2 * _vel_raw_buf[i]
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do _vel_old_buf[i] = _vel_raw_buf[i] end
|
||||
_has_velocity = true
|
||||
end
|
||||
-- Alpha modulated by entropy: low entropy (structured) → less inertia
|
||||
local alpha = inertia_a * clamp(_last_entropy / 7.5, 0.0, 1.5)
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] + alpha * _vel_old_buf[i] end
|
||||
end
|
||||
|
||||
-- ── 7. Concept Lock ───────────────────────────────────────────────────────
|
||||
-- Stability mask: elements with small step-to-step delta get pulled back
|
||||
-- toward their previous state. Sigmoid-shaped lock weight per element.
|
||||
-- Sigma-adaptive: lock_w scaled by (sigma_ratio ^ concept_sigma_power)
|
||||
-- → full effect at high sigma, fades to zero at sigma=0.
|
||||
if f_concept and past_warmup and _has_prev then
|
||||
local sigma_mod = sigma_ratio ^ concept_power
|
||||
if sigma_mod > 1e-4 then
|
||||
for i = 0, n - 1 do
|
||||
local delta = math.abs(_out_buf[i] - _prev_out_buf[i])
|
||||
-- Sigmoid: regions with delta < ~0.05 get near-full lock
|
||||
local lock_w = (1.0 / (1.0 + math.exp(delta * 40.0 - 2.0))) * sigma_mod
|
||||
_out_buf[i] = (1.0 - lock_w) * _out_buf[i] + lock_w * _prev_out_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 8. Identity Anchor ────────────────────────────────────────────────────
|
||||
-- Captures _out_buf snapshot when sigma_ratio crosses anchor_sigma threshold.
|
||||
-- On subsequent steps: gentle pull back toward the captured snapshot.
|
||||
-- Anchor sigma is a ratio of sigma_max (same as OmniRelational pattern).
|
||||
if f_anchor and past_warmup then
|
||||
if not _has_anchor and sigma_ratio <= anchor_sigma then
|
||||
-- Capture snapshot
|
||||
for i = 0, n - 1 do _anchor_buf[i] = _out_buf[i] end
|
||||
_has_anchor = true
|
||||
elseif _has_anchor then
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - anchor_blend) * _out_buf[i] + anchor_blend * _anchor_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 9. Tonal Anchor ───────────────────────────────────────────────────────
|
||||
-- Captures spectral centroid and 4-band energy ratios at anchor_sigma.
|
||||
-- Correction: per-element tilt for centroid drift + per-band ratio correction.
|
||||
-- Each per-element correction hard-capped at ±0.1% regardless of tonal_str.
|
||||
-- Sigma-adaptive: effective_str = tonal_str * sigma_ratio
|
||||
-- → full correction just after capture, fades to zero at sigma=0.
|
||||
if f_tonal and past_warmup then
|
||||
if _tonal_ref_centroid == nil and sigma_ratio <= anchor_sigma then
|
||||
-- Capture reference (fires same step as identity anchor)
|
||||
_tonal_ref_centroid = spectral_centroid(_out_buf, n)
|
||||
_tonal_ref_bands = band_energy(_out_buf, n)
|
||||
elseif _tonal_ref_centroid ~= nil then
|
||||
-- Sigma-adaptive correction scale
|
||||
local eff_str = tonal_str * sigma_ratio
|
||||
if eff_str > 1e-6 then
|
||||
local curr_centroid = spectral_centroid(_out_buf, n)
|
||||
local curr_bands = band_energy(_out_buf, n)
|
||||
|
||||
-- Centroid drift: linear tilt across elements, capped at 0.1%
|
||||
local drift_norm = (curr_centroid - _tonal_ref_centroid) /
|
||||
(math.abs(_tonal_ref_centroid) + EPSILON)
|
||||
local tilt = clamp(-drift_norm * eff_str, -1e-3, 1e-3)
|
||||
local center = (n - 1) / 2.0
|
||||
for i = 0, n - 1 do
|
||||
local dist_w = (i - center) / (center + EPSILON)
|
||||
_out_buf[i] = _out_buf[i] + tilt * dist_w * math.abs(_out_buf[i])
|
||||
end
|
||||
|
||||
-- Band energy ratio correction, capped at 0.1% per band
|
||||
local ref_total, curr_total = 0.0, 0.0
|
||||
for b = 1, 4 do
|
||||
ref_total = ref_total + _tonal_ref_bands[b]
|
||||
curr_total = curr_total + curr_bands[b]
|
||||
end
|
||||
if ref_total > EPSILON and curr_total > EPSILON then
|
||||
local bsize = math.floor(n / 4)
|
||||
for b = 0, 3 do
|
||||
local ref_ratio = _tonal_ref_bands[b + 1] / ref_total
|
||||
local curr_ratio = curr_bands[b + 1] / curr_total
|
||||
local band_corr = clamp((ref_ratio - curr_ratio) * eff_str, -1e-3, 1e-3)
|
||||
local lo = b * bsize
|
||||
local hi = (b == 3) and (n - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do
|
||||
_out_buf[i] = _out_buf[i] + band_corr * math.abs(_out_buf[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 10. Look-Back Smoother ────────────────────────────────────────────────
|
||||
-- SNR-adaptive EMA: lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
-- Blends current output toward previous step output.
|
||||
-- Heavy at high sigma (structure coherence), zero at sigma=0 (preserve detail).
|
||||
-- Pattern from MD PingPong. arXiv:2602.09449.
|
||||
if f_lookback and past_warmup and _has_prev then
|
||||
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
if lb_w > 1e-6 then
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - lb_w) * _out_buf[i] + lb_w * _prev_out_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 11. RMS Servo ─────────────────────────────────────────────────────────
|
||||
-- Downward-only RMS ceiling: fires only when cur_rms > rms_target.
|
||||
-- Target descends from rms_target_max (high sigma) to rms_target_min (low sigma).
|
||||
-- Curve: target = min + sigma_ratio^0.6 * (max - min) (from PingPong).
|
||||
-- Pattern from MD PingPong.
|
||||
if f_rms then
|
||||
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
|
||||
local cur_rms = rms(_out_buf, n)
|
||||
if cur_rms > rms_target then
|
||||
local servo_rms = cur_rms + rms_gain * (rms_target - cur_rms)
|
||||
local scale = servo_rms / cur_rms
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] * scale end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 12. Safety Clamp + NaN Guard ──────────────────────────────────────────
|
||||
-- NaN/Inf in output: roll back to raw Euler result before clamping.
|
||||
-- Abs ceiling applied regardless.
|
||||
if not is_safe(_out_buf, n) then
|
||||
for i = 0, n - 1 do _out_buf[i] = _fallback_buf[i] end
|
||||
end
|
||||
for i = 0, n - 1 do _out_buf[i] = clamp(_out_buf[i], -sclamp, sclamp) end
|
||||
|
||||
-- ── 13. Update state ──────────────────────────────────────────────────────
|
||||
-- Store this step's output as prev_out_buf for next step.
|
||||
-- Also push to memory ring buffer.
|
||||
if past_warmup then
|
||||
for i = 0, n - 1 do _prev_out_buf[i] = _out_buf[i] end
|
||||
_has_prev = true
|
||||
-- Ring buffer push
|
||||
if f_memory then
|
||||
for i = 0, n - 1 do _history[_hist_head][i] = _out_buf[i] end
|
||||
_hist_head = _hist_head + 1
|
||||
if _hist_head > 3 then _hist_head = 1 end
|
||||
if _hist_count < 3 then _hist_count = _hist_count + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 14. Write output ──────────────────────────────────────────────────────
|
||||
for i = 0, n - 1 do xt[i] = _out_buf[i] end
|
||||
|
||||
-- ── 15. SDE Noise Injection ───────────────────────────────────────────────
|
||||
-- Applied after write-back, outside the safety clamp, matching OmniRelational
|
||||
-- convention. scale = t_prev * eta — noise magnitude tracks current sigma level,
|
||||
-- naturally fades to zero as generation converges.
|
||||
if eta > 0.0 and t_prev > EPSILON then
|
||||
local rng = make_rng(seed + step_idx * 7919)
|
||||
local scale = t_prev * eta
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
xt[i] = xt[i] + normal(u1, u2) * scale
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,856 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU General Public License as published by
|
||||
-- the Free Software Foundation, either version 3 of the License, or
|
||||
-- (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU General Public License for more details: https://www.gnu.org/licenses/
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- MD Trajectory Anchor V5 — Latent Path Stabilizer
|
||||
-- MDMAchine | A&E Concepts © 2026
|
||||
--
|
||||
-- Plugin version: V5 (HOT-Step UI / filename — what users see)
|
||||
-- Internal version: v5.0 (math/changelog — what developers track)
|
||||
--
|
||||
-- A stateful trajectory stabilization solver for HOT-Step-CPP.
|
||||
-- Runs via step() — NOT owns_loop. The guider pipeline (APG, ADG, PMG, etc.)
|
||||
-- remains fully active. Receives the pre-guided vt from the engine and applies
|
||||
-- stateful corrections on top of the advancing latent.
|
||||
--
|
||||
-- WHY step() INSTEAD OF owns_loop:
|
||||
-- All features here (inertia, concept lock, anchors, look-back) only need
|
||||
-- xt — the latent tensor — which step() provides directly. owns_loop was
|
||||
-- used in V1/V2 because guidance features needed cond/uncond, but those
|
||||
-- have been removed. step() is the correct, minimal contract for this work.
|
||||
--
|
||||
-- CHANGELOG:
|
||||
-- V5 (v5.0): Step-budget auto-scaling — all system strengths adapt to step
|
||||
-- count via sqrt(35/num_steps). 12-step turbo pushes harder per
|
||||
-- step, 150-step runs back off automatically. Zero new params.
|
||||
-- Anti-ringing on identity anchor — when velocity is already
|
||||
-- moving toward the anchor, blend is reduced to prevent overshoot
|
||||
-- oscillation. Zero new params.
|
||||
-- V4 (v4.0): Version bump for batch consistency. Functionally identical to V3.
|
||||
-- V3 (v3.0): Terminal branch fix era. step()-based, unaffected.
|
||||
--
|
||||
-- FUTURE IDEAS (not yet implemented):
|
||||
-- - Velocity-aware memory buffer: weight ring buffer entries by cosine
|
||||
-- similarity to current velocity. Steps going the same direction get
|
||||
-- full weight, steps from before a direction change get discounted.
|
||||
-- Prevents smoothing across trajectory corners.
|
||||
-- - Sigma-adaptive concept lock threshold: scale lock_threshold with sigma
|
||||
-- so "settled" means different things at different noise levels.
|
||||
-- effective_threshold = lock_threshold * (1 + sigma_ratio * scale_factor)
|
||||
-- - Trajectory confidence metric: per-step quality signal (velocity
|
||||
-- stability EMA, magnitude variance, direction consistency) that
|
||||
-- modulates all system strengths globally. Borrow from STORM V5 LTE.
|
||||
-- Guiders run normally alongside this solver.
|
||||
--
|
||||
-- PIPELINE PER STEP:
|
||||
-- xt → Euler advance (xt + dt * vt) → _out_buf
|
||||
-- → [entropy measurement] — Shannon H from xt (step 0+)
|
||||
-- → [latent pressure] — entropy×RMS correction (toggle, off)
|
||||
-- → [memory buffer] — 3-step ring buffer smoothing
|
||||
-- → [inertia engine] — EMA velocity carry-over
|
||||
-- → [concept lock] — stability mask, sigma-adaptive
|
||||
-- → [identity anchor] — mid-sigma snapshot pull-back
|
||||
-- → [tonal anchor] — spectral centroid correction, sigma-adaptive
|
||||
-- → [look-back smoother] — SNR-adaptive EMA (arXiv:2602.09449)
|
||||
-- → [RMS servo] — descending RMS ceiling (toggle, off)
|
||||
-- → [safety clamp + NaN guard] — abs ceiling + Euler rollback on NaN
|
||||
-- → write _out_buf to xt
|
||||
--
|
||||
-- STATE RESET:
|
||||
-- All module-level state resets on step_index == 0 OR n change.
|
||||
-- Same-length consecutive generations do not bleed state.
|
||||
--
|
||||
-- INERTIA EMA FIX (V2 regression):
|
||||
-- V2 computed: vel = 0.8 * vel + 0.2 * vel (no-op, same buffer).
|
||||
-- V1.0 uses two separate buffers: _vel_old_buf (EMA) and _vel_raw_buf (delta).
|
||||
-- EMA: _vel_old_buf[i] = 0.8 * _vel_old_buf[i] + 0.2 * _vel_raw_buf[i]
|
||||
--
|
||||
-- SIGMA-ADAPTIVE FEATURES (from OmniRelational V3 pattern):
|
||||
-- concept lock strength = full * (sigma_ratio ^ concept_sigma_power)
|
||||
-- tonal correction scale = tonal_strength * sigma_ratio
|
||||
-- look-back weight = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
-- All three are heavy at high sigma (structure phase), fade to zero at sigma=0.
|
||||
--
|
||||
-- PARAMS:
|
||||
-- warmup_steps — skip stateful features for first N steps
|
||||
-- inertia_engine — EMA latent velocity carry-over
|
||||
-- inertia_alpha — base velocity coefficient, entropy-modulated
|
||||
-- memory_buffer — 3-step ring buffer output smoothing
|
||||
-- memory_blend — history blend fraction
|
||||
-- concept_lock — stability mask on settled regions
|
||||
-- concept_sigma_power — how fast lock fades with sigma
|
||||
-- identity_anchor — captures xt snapshot at anchor_sigma, pulls back
|
||||
-- anchor_sigma — sigma fraction at which anchors are captured
|
||||
-- anchor_blend — pull strength toward identity anchor
|
||||
-- tonal_anchor — spectral centroid drift correction
|
||||
-- tonal_strength — correction scale (per-element hard cap 0.1%)
|
||||
-- look_back_enabled — SNR-adaptive latent EMA smoother
|
||||
-- look_back_lambda — max smoothing weight at high sigma
|
||||
-- look_back_snr_power — falloff exponent
|
||||
-- rms_servo — descending RMS ceiling (off by default)
|
||||
-- rms_target_min — RMS ceiling at low sigma
|
||||
-- rms_target_max — RMS ceiling at high sigma
|
||||
-- rms_servo_gain — servo correction aggressiveness
|
||||
-- latent_pressure — entropy×RMS target correction (off by default)
|
||||
-- pressure_target_rms — RMS target for pressure correction
|
||||
-- pressure_target_entropy — entropy target for pressure weighting
|
||||
-- safety_clamp — max absolute latent value
|
||||
-- ============================================================================
|
||||
|
||||
solver = {
|
||||
name = "md_trajectory_anchor_V5",
|
||||
display = "MD Trajectory Anchor V5",
|
||||
description = "Latent path stabilizer. step() solver — guiders stay active. Inertia engine, concept lock, identity anchor, tonal anchor, memory buffer, look-back smoother, RMS servo. All stateful. State resets cleanly between generations.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
params = {
|
||||
-- ── Warmup ────────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "warmup_steps",
|
||||
type = "slider",
|
||||
label = "Warmup Steps",
|
||||
default = 2,
|
||||
min = 0,
|
||||
max = 6,
|
||||
step = 1,
|
||||
hint = "Skip stateful features (inertia, concept lock, anchors) for first N steps. Latent is mostly noise at high sigma — anchoring into chaos makes things worse. 2=recommended. 0=always active.",
|
||||
},
|
||||
-- ── Inertia Engine ────────────────────────────────────────────────────
|
||||
{
|
||||
key = "inertia_engine",
|
||||
type = "toggle",
|
||||
label = "Inertia Engine",
|
||||
default = true,
|
||||
hint = "EMA-smoothed latent velocity carry-over. Adds step-to-step momentum — reduces abrupt trajectory direction changes. Alpha is entropy-modulated: less inertia when latent is already structured (low entropy).",
|
||||
},
|
||||
{
|
||||
key = "inertia_alpha",
|
||||
type = "slider",
|
||||
label = "Inertia Alpha",
|
||||
default = 0.15,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Base velocity carry-over coefficient. 0.10=subtle. 0.20=noticeable. 0.30+=strong. Scaled down at runtime when entropy is low (structured latent needs less push).",
|
||||
},
|
||||
-- ── Memory Buffer ─────────────────────────────────────────────────────
|
||||
{
|
||||
key = "memory_buffer",
|
||||
type = "toggle",
|
||||
label = "Memory Buffer",
|
||||
default = false,
|
||||
hint = "Blends last 3 step outputs into the current step output. Suppresses step-to-step jitter without redirecting the trajectory. Ring buffer, zero-alloc.",
|
||||
},
|
||||
{
|
||||
key = "memory_blend",
|
||||
type = "slider",
|
||||
label = "Memory Blend",
|
||||
default = 0.12,
|
||||
min = 0.0,
|
||||
max = 0.5,
|
||||
step = 0.01,
|
||||
hint = "Fraction of 3-step history mean blended into each step output. 0.12=subtle. 0.25+=heavy smoothing (may soften transients in audio).",
|
||||
},
|
||||
-- ── Concept Lock ──────────────────────────────────────────────────────
|
||||
{
|
||||
key = "concept_lock",
|
||||
type = "toggle",
|
||||
label = "Concept Lock",
|
||||
default = true,
|
||||
hint = "Stability mask: elements with small step-to-step delta are pulled back toward their previous state. Protects settled structure from noise. Sigma-adaptive — full strength at high sigma, fades at low sigma (detail phase).",
|
||||
},
|
||||
{
|
||||
key = "concept_sigma_power",
|
||||
type = "slider",
|
||||
label = "Concept Lock Sigma Power",
|
||||
default = 1.0,
|
||||
min = 0.25,
|
||||
max = 3.0,
|
||||
step = 0.25,
|
||||
hint = "Controls how fast concept lock fades as sigma decreases. 1.0=linear decay. 2.0=quadratic (lock concentrated on early structure steps only). 0.5=slow fade (lock persists into detail steps).",
|
||||
},
|
||||
-- ── Identity Anchor ───────────────────────────────────────────────────
|
||||
{
|
||||
key = "identity_anchor",
|
||||
type = "toggle",
|
||||
label = "Identity Anchor",
|
||||
default = false,
|
||||
hint = "Captures a snapshot of xt at anchor_sigma, then gently pulls toward it on all subsequent steps. Prevents late-stage structural drift. Tonal anchor fires at the same sigma.",
|
||||
},
|
||||
{
|
||||
key = "anchor_sigma",
|
||||
type = "slider",
|
||||
label = "Anchor Sigma",
|
||||
default = 0.5,
|
||||
min = 0.1,
|
||||
max = 0.9,
|
||||
step = 0.05,
|
||||
hint = "Sigma level (as fraction of sigma_max) at which the identity and tonal anchors are captured. 0.5=mid-generation. Lower=locks in more detail. Higher=locks coarser structure only.",
|
||||
},
|
||||
{
|
||||
key = "anchor_blend",
|
||||
type = "slider",
|
||||
label = "Anchor Blend",
|
||||
default = 0.08,
|
||||
min = 0.01,
|
||||
max = 0.30,
|
||||
step = 0.01,
|
||||
hint = "Pull strength toward identity anchor per step. 0.08=gentle (recommended). 0.15=noticeable. Setting too high constrains creative refinement after anchor capture.",
|
||||
},
|
||||
-- ── Tonal Anchor ──────────────────────────────────────────────────────
|
||||
{
|
||||
key = "tonal_anchor",
|
||||
type = "toggle",
|
||||
label = "Tonal Anchor",
|
||||
default = true,
|
||||
hint = "Captures spectral centroid and band energy ratios at anchor_sigma. Applies centroid drift correction and band ratio correction on subsequent steps. Sigma-adaptive — correction strength fades proportionally with sigma.",
|
||||
},
|
||||
{
|
||||
key = "tonal_strength",
|
||||
type = "slider",
|
||||
label = "Tonal Strength",
|
||||
default = 0.15,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Tonal correction scale. Each per-element correction is hard-capped at 0.1% per step regardless of this value. 0.10-0.20=recommended for audio. Higher values widen the correction window but the cap still applies.",
|
||||
},
|
||||
-- ── Look-Back Smoother ────────────────────────────────────────────────
|
||||
{
|
||||
key = "look_back_enabled",
|
||||
type = "toggle",
|
||||
label = "Look-Back Smoother",
|
||||
default = false,
|
||||
hint = "SNR-adaptive latent EMA. Blends current output toward previous step output, weighted heavily at high sigma (structure), fading to zero at low sigma (detail). Suppresses ODE manifold shearing and harmonic hum. arXiv:2602.09449.",
|
||||
},
|
||||
{
|
||||
key = "look_back_lambda",
|
||||
type = "slider",
|
||||
label = "Look-Back Lambda",
|
||||
default = 0.15,
|
||||
min = 0.05,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Max smoothing weight at sigma=sigma_max. 0.55=25-step DDIM (default). 0.35=35-step simple. Always fades to zero at sigma=0 regardless of this value.",
|
||||
},
|
||||
{
|
||||
key = "look_back_snr_power",
|
||||
type = "slider",
|
||||
label = "Look-Back SNR Power",
|
||||
default = 1.3,
|
||||
min = 0.5,
|
||||
max = 3.0,
|
||||
step = 0.1,
|
||||
hint = "Falloff exponent for look-back weight. 1.3=25-step DDIM. 1.5=35-step simple. Higher = smoothing concentrated on early structure steps only.",
|
||||
},
|
||||
-- ── RMS Servo ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "rms_servo",
|
||||
type = "toggle",
|
||||
label = "RMS Servo",
|
||||
default = false,
|
||||
hint = "Downward-only RMS ceiling. Prevents latent energy runaway without hard clipping. Off by default — calibrate target_min and target_max for your domain before enabling. ACE-Step latents run ~2.0 RMS.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_min",
|
||||
type = "slider",
|
||||
label = "RMS Target Min",
|
||||
default = 1.2,
|
||||
min = 0.1,
|
||||
max = 3.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at low sigma (late/detail steps). ACE-Step latents ~2.0 RMS at x0. Start at 1.2-1.8 and observe results.",
|
||||
},
|
||||
{
|
||||
key = "rms_target_max",
|
||||
type = "slider",
|
||||
label = "RMS Target Max",
|
||||
default = 2.5,
|
||||
min = 0.5,
|
||||
max = 5.0,
|
||||
step = 0.05,
|
||||
hint = "RMS ceiling at high sigma (early/structure steps). Should be >= target_min. ACE-Step early sigma ~2.5-3.5. Servo only fires downward.",
|
||||
},
|
||||
{
|
||||
key = "rms_servo_gain",
|
||||
type = "slider",
|
||||
label = "RMS Servo Gain",
|
||||
default = 0.6,
|
||||
min = 0.1,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Servo correction aggressiveness. 0.6=gradual correction. 1.0=hard snap to target each step. Lower is smoother but slower to converge.",
|
||||
},
|
||||
-- ── Latent Pressure ───────────────────────────────────────────────────
|
||||
{
|
||||
key = "latent_pressure",
|
||||
type = "toggle",
|
||||
label = "Latent Pressure",
|
||||
default = false,
|
||||
hint = "Applies a small per-step RMS correction weighted by Shannon entropy. Nudges latent toward a healthy entropy×RMS product. Off by default — tune target params before enabling. Correction capped at 0.05% per step.",
|
||||
},
|
||||
{
|
||||
key = "pressure_target_rms",
|
||||
type = "slider",
|
||||
label = "Pressure Target RMS",
|
||||
default = 2.0,
|
||||
min = 0.5,
|
||||
max = 4.0,
|
||||
step = 0.1,
|
||||
hint = "RMS component of pressure target. ACE-Step ~2.0. Correction direction flips if current entropy×RMS is above target.",
|
||||
},
|
||||
{
|
||||
key = "pressure_target_entropy",
|
||||
type = "slider",
|
||||
label = "Pressure Target Entropy",
|
||||
default = 7.5,
|
||||
min = 1.0,
|
||||
max = 15.0,
|
||||
step = 0.5,
|
||||
hint = "Shannon entropy component of pressure target. 7.5=image-domain default. Audio domain may differ — run with verbose output and measure entropy distribution before setting this.",
|
||||
},
|
||||
-- ── SDE Noise ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "relational_weight",
|
||||
type = "slider",
|
||||
label = "Relational Weight",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Barbour Best Matching velocity decomposition. 0 = off. 0.3-0.5 = balanced.",
|
||||
},
|
||||
{
|
||||
key = "relational_sigma_power",
|
||||
type = "slider",
|
||||
label = "Relational Sigma Decay",
|
||||
default = 1.0,
|
||||
min = 0.25,
|
||||
max = 4.0,
|
||||
step = 0.25,
|
||||
hint = "How fast relational weight fades. 1.0 = linear.",
|
||||
},
|
||||
{
|
||||
key = "eta",
|
||||
type = "slider",
|
||||
label = "Eta (SDE Noise)",
|
||||
default = 0.0,
|
||||
min = 0.0,
|
||||
max = 1.0,
|
||||
step = 0.05,
|
||||
hint = "Ancestral noise injection. 0=deterministic ODE (default). Scales with t_prev each step. Low values (0.05-0.15) add subtle stochasticity without overwhelming the stabilization features.",
|
||||
},
|
||||
{
|
||||
key = "seed",
|
||||
type = "slider",
|
||||
label = "Seed",
|
||||
default = 42,
|
||||
min = 0,
|
||||
max = 999999,
|
||||
step = 1,
|
||||
hint = "RNG seed for SDE noise. Deterministic per-step via seed + step_index * 7919.",
|
||||
},
|
||||
-- ── Safety ────────────────────────────────────────────────────────────
|
||||
{
|
||||
key = "safety_clamp",
|
||||
type = "slider",
|
||||
label = "Safety Clamp",
|
||||
default = 2.5,
|
||||
min = 1.0,
|
||||
max = 5.0,
|
||||
step = 0.1,
|
||||
hint = "Max absolute latent value after all corrections. NaN/Inf triggers a full rollback to raw Euler output before clamping. 2.5=standard. Raise to 4.0+ if clamping is audible.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- ── Constants ─────────────────────────────────────────────────────────────────
|
||||
local EPSILON = 1e-8
|
||||
local PRESSURE_CAP = 5e-4 -- max pressure correction per step (0.05%)
|
||||
|
||||
local function make_rng(seed)
|
||||
local state = math.floor(seed) % 2147483647
|
||||
if state <= 0 then state = state + 2147483646 end
|
||||
return function()
|
||||
state = (state * 1664525 + 1013904223) % 2147483648
|
||||
return state / 2147483648.0
|
||||
end
|
||||
end
|
||||
|
||||
local function normal(u1, u2)
|
||||
return math.sqrt(-2.0 * math.log(math.max(u1, EPSILON))) * math.cos(2.0 * math.pi * u2)
|
||||
end
|
||||
|
||||
-- ── Hoisted Buffers (Zero Allocation Hot Loop) ────────────────────────────────
|
||||
-- Sized on first run or n-change. Reused every step — no GC pressure.
|
||||
local _last_n = 0
|
||||
local _out_buf = {} -- working output for this step
|
||||
local _fallback_buf = {} -- raw Euler output (NaN rollback)
|
||||
local _vel_old_buf = {} -- EMA velocity (carries across steps)
|
||||
local _vel_raw_buf = {} -- raw velocity delta (computed this step)
|
||||
local _anchor_buf = {} -- identity anchor snapshot (frozen at anchor_sigma)
|
||||
local _prev_out_buf = {} -- previous step final output (inertia + concept lock + look-back)
|
||||
local _hist_mean_buf = {} -- history mean scratch
|
||||
local _history = { {}, {}, {} } -- ring buffer (3 slots, 0-indexed elements)
|
||||
|
||||
-- ── Module State (reset on n change or step_index == 0) ───────────────────────
|
||||
local _sigma_max = nil
|
||||
local _has_prev = false -- true after first step output is stored
|
||||
local _has_velocity = false -- true after first EMA velocity is initialized
|
||||
local _has_anchor = false -- true after identity anchor is captured
|
||||
local _tonal_ref_centroid = nil
|
||||
local _tonal_ref_bands = nil
|
||||
local _last_entropy = 7.5
|
||||
local _hist_head = 1
|
||||
local _hist_count = 0
|
||||
|
||||
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
local function bool_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return p[key]
|
||||
end
|
||||
|
||||
local function num_param(p, key, default)
|
||||
if p == nil or p[key] == nil then return default end
|
||||
return tonumber(p[key]) or default
|
||||
end
|
||||
|
||||
local function rms(a, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + a[i] * a[i] end
|
||||
return math.sqrt(s / n + EPSILON)
|
||||
end
|
||||
|
||||
local function shannon_entropy(a, n)
|
||||
local sum = 0.0
|
||||
for i = 0, n - 1 do sum = sum + math.abs(a[i]) + 1e-7 end
|
||||
local inv_sum = 1.0 / (sum + 1e-8)
|
||||
local H = 0.0
|
||||
for i = 0, n - 1 do
|
||||
local p = (math.abs(a[i]) + 1e-7) * inv_sum
|
||||
H = H - p * math.log(p + EPSILON) / math.log(2.0)
|
||||
end
|
||||
H = math.max(0.05, H)
|
||||
if H ~= H or H == math.huge or H == -math.huge then H = 5.0 end
|
||||
return H
|
||||
end
|
||||
|
||||
local function spectral_centroid(a, n)
|
||||
local sum_mag, sum_w = 0.0, 0.0
|
||||
for i = 0, n - 1 do
|
||||
local m = math.abs(a[i])
|
||||
sum_mag = sum_mag + m
|
||||
sum_w = sum_w + m * i
|
||||
end
|
||||
if sum_mag < EPSILON then return 0.0 end
|
||||
return sum_w / sum_mag
|
||||
end
|
||||
|
||||
local function band_energy(a, n)
|
||||
local bands = {0.0, 0.0, 0.0, 0.0}
|
||||
local bsize = math.floor(n / 4)
|
||||
for b = 0, 3 do
|
||||
local s = 0.0
|
||||
local lo = b * bsize
|
||||
local hi = (b == 3) and (n - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do s = s + math.abs(a[i]) end
|
||||
bands[b + 1] = s / math.max(hi - lo + 1, 1)
|
||||
end
|
||||
return bands
|
||||
end
|
||||
|
||||
local function is_safe(a, n)
|
||||
for i = 0, n - 1 do
|
||||
local v = a[i]
|
||||
if v ~= v or v == math.huge or v == -math.huge then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function dot_product(a, b, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + a[i] * b[i] end
|
||||
return s
|
||||
end
|
||||
|
||||
local function vec_norm(a, n)
|
||||
return math.sqrt(dot_product(a, a, n) + EPSILON)
|
||||
end
|
||||
|
||||
-- ── step() ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function step(xt, vt, t_curr, t_prev, n)
|
||||
|
||||
-- ── 0. Read params ────────────────────────────────────────────────────────
|
||||
local warmup = math.floor(num_param(params, "warmup_steps", 2))
|
||||
local f_inertia = bool_param(params, "inertia_engine", true)
|
||||
local inertia_a = num_param(params, "inertia_alpha", 0.15)
|
||||
local f_memory = bool_param(params, "memory_buffer", false)
|
||||
local mem_blend = num_param(params, "memory_blend", 0.12)
|
||||
local f_concept = bool_param(params, "concept_lock", true)
|
||||
local concept_power = num_param(params, "concept_sigma_power", 1.0)
|
||||
local f_anchor = bool_param(params, "identity_anchor", false)
|
||||
local anchor_sigma = num_param(params, "anchor_sigma", 0.5)
|
||||
local anchor_blend = num_param(params, "anchor_blend", 0.08)
|
||||
local f_tonal = bool_param(params, "tonal_anchor", true)
|
||||
local tonal_str = num_param(params, "tonal_strength", 0.15)
|
||||
local f_lookback = bool_param(params, "look_back_enabled", false)
|
||||
local lb_lambda = num_param(params, "look_back_lambda", 0.15)
|
||||
local lb_snr_power = num_param(params, "look_back_snr_power", 1.3)
|
||||
local f_rms = bool_param(params, "rms_servo", false)
|
||||
local rms_tgt_min = num_param(params, "rms_target_min", 1.2)
|
||||
local rms_tgt_max = num_param(params, "rms_target_max", 2.5)
|
||||
local rms_gain = num_param(params, "rms_servo_gain", 0.6)
|
||||
local f_pressure = bool_param(params, "latent_pressure", false)
|
||||
local p_tgt_rms = num_param(params, "pressure_target_rms", 2.0)
|
||||
local p_tgt_entropy = num_param(params, "pressure_target_entropy", 7.5)
|
||||
local eta = num_param(params, "eta", 0.0)
|
||||
local seed = math.floor(num_param(params, "seed", 42))
|
||||
local sclamp = num_param(params, "safety_clamp", 2.5)
|
||||
local rw = num_param(params, "relational_weight", 0.0)
|
||||
local rw_sig_pow = num_param(params, "relational_sigma_power", 1.0)
|
||||
|
||||
local step_idx = step_index or 0
|
||||
|
||||
-- ── 0b. Step-budget auto-scaling ──────────────────────────────────────────
|
||||
-- All system strengths adapt to step count. At 12 steps each step matters
|
||||
-- more, so systems push harder. At 150 steps each step matters less, so
|
||||
-- systems back off. Reference = 35 steps (standard schedule).
|
||||
-- sqrt gives diminishing returns — doubling steps halves to 0.71, not 0.5.
|
||||
local total_steps = num_steps or 35
|
||||
if total_steps < 1 then total_steps = 35 end
|
||||
local budget_scale = math.sqrt(35.0 / total_steps)
|
||||
|
||||
-- Apply to all system strengths (modifies local copies, not params)
|
||||
inertia_a = inertia_a * budget_scale
|
||||
mem_blend = mem_blend * budget_scale
|
||||
anchor_blend = anchor_blend * budget_scale
|
||||
tonal_str = tonal_str * budget_scale
|
||||
lb_lambda = lb_lambda * budget_scale
|
||||
|
||||
-- ── 1. State reset (generation start or n change) ─────────────────────────
|
||||
-- n change: new latent shape (different duration/channels)
|
||||
-- step_idx == 0: new generation with same shape — must reset or prev
|
||||
-- generation's final state bleeds into next run's step 1
|
||||
if n ~= _last_n or step_idx == 0 then
|
||||
_sigma_max = nil
|
||||
_has_prev = false
|
||||
_has_velocity = false
|
||||
_has_anchor = false
|
||||
_tonal_ref_centroid = nil
|
||||
_tonal_ref_bands = nil
|
||||
_last_entropy = 7.5
|
||||
_hist_head = 1
|
||||
_hist_count = 0
|
||||
-- Resize hoisted buffers
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = 0.0
|
||||
_fallback_buf[i] = 0.0
|
||||
_vel_old_buf[i] = 0.0
|
||||
_vel_raw_buf[i] = 0.0
|
||||
_anchor_buf[i] = 0.0
|
||||
_prev_out_buf[i] = 0.0
|
||||
_hist_mean_buf[i] = 0.0
|
||||
_history[1][i] = 0.0
|
||||
_history[2][i] = 0.0
|
||||
_history[3][i] = 0.0
|
||||
end
|
||||
_last_n = n
|
||||
end
|
||||
|
||||
-- Capture sigma_max on first step of this generation
|
||||
if _sigma_max == nil then _sigma_max = t_curr end
|
||||
|
||||
-- sigma_ratio: 1.0 at high sigma (early), 0.0 at sigma=0 (final step)
|
||||
local sigma_ratio = clamp(t_curr / math.max(_sigma_max, EPSILON), 0.0, 1.0)
|
||||
|
||||
-- Warmup gate: stateful features are skipped for first `warmup` steps
|
||||
local past_warmup = (step_idx >= warmup)
|
||||
|
||||
-- ── 2. Entropy measurement (always, from step 0) ──────────────────────────
|
||||
-- Measured from xt (input), not the output. Represents current latent state.
|
||||
_last_entropy = shannon_entropy(xt, n)
|
||||
|
||||
-- ── 2b. Relational velocity decomposition ──────────────────────────────
|
||||
-- vt is read-only FloatArray, so we create a local velocity reference
|
||||
local vel = vt -- default: use vt directly (no copy overhead when rw=0)
|
||||
if rw > 0 and _sigma_max ~= nil then
|
||||
local v_tbl = {}
|
||||
for i = 0, n - 1 do v_tbl[i] = vt[i] end
|
||||
local x_tbl = {}
|
||||
for i = 0, n - 1 do x_tbl[i] = xt[i] end
|
||||
C.apply_relational(v_tbl, n, 1, n, sigma_ratio, _sigma_max,
|
||||
rw, rw_sig_pow, false, 0.85, x_tbl)
|
||||
vel = v_tbl
|
||||
end
|
||||
|
||||
-- ── 3. Euler advance ──────────────────────────────────────────────────────
|
||||
-- dt = t_prev - t_curr. t decrements each step, so dt < 0 (standard).
|
||||
-- x_next = xt + dt * vel
|
||||
local dt = t_prev - t_curr
|
||||
for i = 0, n - 1 do
|
||||
local v = xt[i] + dt * vel[i]
|
||||
_out_buf[i] = v
|
||||
_fallback_buf[i] = v -- save raw Euler for NaN rollback
|
||||
end
|
||||
|
||||
-- ── 4. Latent Pressure (always if enabled, from step 0) ───────────────────
|
||||
-- Nudges latent RMS toward pressure_target_rms, weighted by entropy proximity
|
||||
-- to pressure_target_entropy. Correction hard-capped at PRESSURE_CAP per step.
|
||||
if f_pressure then
|
||||
local cur_rms = rms(_out_buf, n)
|
||||
local target_product = p_tgt_entropy * p_tgt_rms
|
||||
local cur_product = _last_entropy * cur_rms
|
||||
local correction = clamp(
|
||||
(target_product - cur_product) / (target_product + EPSILON),
|
||||
-PRESSURE_CAP, PRESSURE_CAP
|
||||
)
|
||||
if math.abs(correction) > 1e-6 then
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] * (1.0 + correction) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Stateful features below: all gated on past_warmup AND _has_prev ────────
|
||||
|
||||
-- ── 5. Memory Buffer ──────────────────────────────────────────────────────
|
||||
-- Blends mean of last 3 step outputs into current output.
|
||||
-- Ring buffer: _hist_head cycles 1→2→3→1. _hist_count tracks fill level.
|
||||
if f_memory and past_warmup and _hist_count > 0 then
|
||||
for i = 0, n - 1 do _hist_mean_buf[i] = 0.0 end
|
||||
local hw = 1.0 / _hist_count
|
||||
for h = 1, _hist_count do
|
||||
for i = 0, n - 1 do _hist_mean_buf[i] = _hist_mean_buf[i] + _history[h][i] end
|
||||
end
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - mem_blend) * _out_buf[i] + mem_blend * (_hist_mean_buf[i] * hw)
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 6. Inertia Engine ─────────────────────────────────────────────────────
|
||||
-- EMA velocity = smoothed step-to-step output delta.
|
||||
-- Velocity raw this step: _out_buf - _prev_out_buf (output delta).
|
||||
-- EMA update: vel_old = 0.8 * vel_old + 0.2 * vel_raw (two separate buffers)
|
||||
-- Alpha entropy-modulated: less inertia when latent is structured (low H).
|
||||
if f_inertia and past_warmup and _has_prev then
|
||||
-- Compute raw velocity delta into _vel_raw_buf
|
||||
for i = 0, n - 1 do _vel_raw_buf[i] = _out_buf[i] - _prev_out_buf[i] end
|
||||
-- EMA update or initialization
|
||||
if _has_velocity then
|
||||
for i = 0, n - 1 do
|
||||
_vel_old_buf[i] = 0.8 * _vel_old_buf[i] + 0.2 * _vel_raw_buf[i]
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do _vel_old_buf[i] = _vel_raw_buf[i] end
|
||||
_has_velocity = true
|
||||
end
|
||||
-- Alpha modulated by entropy: low entropy (structured) → less inertia
|
||||
local alpha = inertia_a * clamp(_last_entropy / 7.5, 0.0, 1.5)
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] + alpha * _vel_old_buf[i] end
|
||||
end
|
||||
|
||||
-- ── 7. Concept Lock ───────────────────────────────────────────────────────
|
||||
-- Stability mask: elements with small step-to-step delta get pulled back
|
||||
-- toward their previous state. Sigmoid-shaped lock weight per element.
|
||||
-- Sigma-adaptive: lock_w scaled by (sigma_ratio ^ concept_sigma_power)
|
||||
-- → full effect at high sigma, fades to zero at sigma=0.
|
||||
if f_concept and past_warmup and _has_prev then
|
||||
local sigma_mod = sigma_ratio ^ concept_power
|
||||
if sigma_mod > 1e-4 then
|
||||
for i = 0, n - 1 do
|
||||
local delta = math.abs(_out_buf[i] - _prev_out_buf[i])
|
||||
-- Sigmoid: regions with delta < ~0.05 get near-full lock
|
||||
local lock_w = (1.0 / (1.0 + math.exp(delta * 40.0 - 2.0))) * sigma_mod
|
||||
_out_buf[i] = (1.0 - lock_w) * _out_buf[i] + lock_w * _prev_out_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 8. Identity Anchor ────────────────────────────────────────────────────
|
||||
-- Captures _out_buf snapshot when sigma_ratio crosses anchor_sigma threshold.
|
||||
-- On subsequent steps: gentle pull back toward the captured snapshot.
|
||||
-- Anti-ringing (V5): when velocity is already pointing toward the anchor,
|
||||
-- reduce the pull to prevent overshoot oscillation. If moving away from
|
||||
-- anchor, keep full pull. Zero new params — purely automatic.
|
||||
if f_anchor and past_warmup then
|
||||
if not _has_anchor and sigma_ratio <= anchor_sigma then
|
||||
-- Capture snapshot
|
||||
for i = 0, n - 1 do _anchor_buf[i] = _out_buf[i] end
|
||||
_has_anchor = true
|
||||
elseif _has_anchor then
|
||||
local eff_blend = anchor_blend
|
||||
|
||||
-- Anti-ringing: check if velocity aligns with anchor direction
|
||||
if _has_prev then
|
||||
-- direction_to_anchor = anchor - current
|
||||
-- velocity = current - prev
|
||||
local dot_va = 0.0
|
||||
local norm_v_sq = 0.0
|
||||
local norm_a_sq = 0.0
|
||||
for i = 0, n - 1 do
|
||||
local v_i = _out_buf[i] - _prev_out_buf[i]
|
||||
local a_i = _anchor_buf[i] - _out_buf[i]
|
||||
dot_va = dot_va + v_i * a_i
|
||||
norm_v_sq = norm_v_sq + v_i * v_i
|
||||
norm_a_sq = norm_a_sq + a_i * a_i
|
||||
end
|
||||
local norm_v = math.sqrt(norm_v_sq + EPSILON)
|
||||
local norm_a = math.sqrt(norm_a_sq + EPSILON)
|
||||
local cos_sim = dot_va / (norm_v * norm_a)
|
||||
|
||||
-- cos_sim > 0: already moving toward anchor → reduce pull
|
||||
-- cos_sim < 0: moving away from anchor → keep full pull
|
||||
if cos_sim > 0.0 then
|
||||
eff_blend = anchor_blend * (1.0 - clamp(cos_sim, 0.0, 0.8))
|
||||
end
|
||||
end
|
||||
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - eff_blend) * _out_buf[i] + eff_blend * _anchor_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 9. Tonal Anchor ───────────────────────────────────────────────────────
|
||||
-- Captures spectral centroid and 4-band energy ratios at anchor_sigma.
|
||||
-- Correction: per-element tilt for centroid drift + per-band ratio correction.
|
||||
-- Each per-element correction hard-capped at ±0.1% regardless of tonal_str.
|
||||
-- Sigma-adaptive: effective_str = tonal_str * sigma_ratio
|
||||
-- → full correction just after capture, fades to zero at sigma=0.
|
||||
if f_tonal and past_warmup then
|
||||
if _tonal_ref_centroid == nil and sigma_ratio <= anchor_sigma then
|
||||
-- Capture reference (fires same step as identity anchor)
|
||||
_tonal_ref_centroid = spectral_centroid(_out_buf, n)
|
||||
_tonal_ref_bands = band_energy(_out_buf, n)
|
||||
elseif _tonal_ref_centroid ~= nil then
|
||||
-- Sigma-adaptive correction scale
|
||||
local eff_str = tonal_str * sigma_ratio
|
||||
if eff_str > 1e-6 then
|
||||
local curr_centroid = spectral_centroid(_out_buf, n)
|
||||
local curr_bands = band_energy(_out_buf, n)
|
||||
|
||||
-- Centroid drift: linear tilt across elements, capped at 0.1%
|
||||
local drift_norm = (curr_centroid - _tonal_ref_centroid) /
|
||||
(math.abs(_tonal_ref_centroid) + EPSILON)
|
||||
local tilt = clamp(-drift_norm * eff_str, -1e-3, 1e-3)
|
||||
local center = (n - 1) / 2.0
|
||||
for i = 0, n - 1 do
|
||||
local dist_w = (i - center) / (center + EPSILON)
|
||||
_out_buf[i] = _out_buf[i] + tilt * dist_w * math.abs(_out_buf[i])
|
||||
end
|
||||
|
||||
-- Band energy ratio correction, capped at 0.1% per band
|
||||
local ref_total, curr_total = 0.0, 0.0
|
||||
for b = 1, 4 do
|
||||
ref_total = ref_total + _tonal_ref_bands[b]
|
||||
curr_total = curr_total + curr_bands[b]
|
||||
end
|
||||
if ref_total > EPSILON and curr_total > EPSILON then
|
||||
local bsize = math.floor(n / 4)
|
||||
for b = 0, 3 do
|
||||
local ref_ratio = _tonal_ref_bands[b + 1] / ref_total
|
||||
local curr_ratio = curr_bands[b + 1] / curr_total
|
||||
local band_corr = clamp((ref_ratio - curr_ratio) * eff_str, -1e-3, 1e-3)
|
||||
local lo = b * bsize
|
||||
local hi = (b == 3) and (n - 1) or (lo + bsize - 1)
|
||||
for i = lo, hi do
|
||||
_out_buf[i] = _out_buf[i] + band_corr * math.abs(_out_buf[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 10. Look-Back Smoother ────────────────────────────────────────────────
|
||||
-- SNR-adaptive EMA: lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
-- Blends current output toward previous step output.
|
||||
-- Heavy at high sigma (structure coherence), zero at sigma=0 (preserve detail).
|
||||
-- Pattern from MD PingPong. arXiv:2602.09449.
|
||||
if f_lookback and past_warmup and _has_prev then
|
||||
local lb_w = lb_lambda * (sigma_ratio ^ lb_snr_power)
|
||||
if lb_w > 1e-6 then
|
||||
for i = 0, n - 1 do
|
||||
_out_buf[i] = (1.0 - lb_w) * _out_buf[i] + lb_w * _prev_out_buf[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 11. RMS Servo ─────────────────────────────────────────────────────────
|
||||
-- Downward-only RMS ceiling: fires only when cur_rms > rms_target.
|
||||
-- Target descends from rms_target_max (high sigma) to rms_target_min (low sigma).
|
||||
-- Curve: target = min + sigma_ratio^0.6 * (max - min) (from PingPong).
|
||||
-- Pattern from MD PingPong.
|
||||
if f_rms then
|
||||
local rms_target = rms_tgt_min + (sigma_ratio ^ 0.6) * (rms_tgt_max - rms_tgt_min)
|
||||
local cur_rms = rms(_out_buf, n)
|
||||
if cur_rms > rms_target then
|
||||
local servo_rms = cur_rms + rms_gain * (rms_target - cur_rms)
|
||||
local scale = servo_rms / cur_rms
|
||||
for i = 0, n - 1 do _out_buf[i] = _out_buf[i] * scale end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 12. Safety Clamp + NaN Guard ──────────────────────────────────────────
|
||||
-- NaN/Inf in output: roll back to raw Euler result before clamping.
|
||||
-- Abs ceiling applied regardless.
|
||||
if not is_safe(_out_buf, n) then
|
||||
for i = 0, n - 1 do _out_buf[i] = _fallback_buf[i] end
|
||||
end
|
||||
for i = 0, n - 1 do _out_buf[i] = clamp(_out_buf[i], -sclamp, sclamp) end
|
||||
|
||||
-- ── 13. Update state ──────────────────────────────────────────────────────
|
||||
-- Store this step's output as prev_out_buf for next step.
|
||||
-- Also push to memory ring buffer.
|
||||
if past_warmup then
|
||||
for i = 0, n - 1 do _prev_out_buf[i] = _out_buf[i] end
|
||||
_has_prev = true
|
||||
-- Ring buffer push
|
||||
if f_memory then
|
||||
for i = 0, n - 1 do _history[_hist_head][i] = _out_buf[i] end
|
||||
_hist_head = _hist_head + 1
|
||||
if _hist_head > 3 then _hist_head = 1 end
|
||||
if _hist_count < 3 then _hist_count = _hist_count + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── 14. Write output ──────────────────────────────────────────────────────
|
||||
for i = 0, n - 1 do xt[i] = _out_buf[i] end
|
||||
|
||||
-- ── 15. SDE Noise Injection ───────────────────────────────────────────────
|
||||
-- Applied after write-back, outside the safety clamp, matching OmniRelational
|
||||
-- convention. scale = t_prev * eta — noise magnitude tracks current sigma level,
|
||||
-- naturally fades to zero as generation converges.
|
||||
if eta > 0.0 and t_prev > EPSILON then
|
||||
local rng = make_rng(seed + step_idx * 7919)
|
||||
local scale = t_prev * eta
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(rng(), EPSILON)
|
||||
local u2 = rng()
|
||||
xt[i] = xt[i] + normal(u1, u2) * scale
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,218 @@
|
||||
-- ============================================================================
|
||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-- Copyright (C) 2026 Alexander Allan (MDMAchine) -- A&E Concepts
|
||||
-- ============================================================================
|
||||
|
||||
-- MD Vortex V1 -- Vorticity Damping Sampler
|
||||
-- MDMAchine | A&E Concepts (c) 2026
|
||||
--
|
||||
-- Multi-scale curl proxy detects rotational velocity energy. Enstrophy EMA
|
||||
-- triggers targeted vortex shedding (direct damping of vortical component).
|
||||
-- Euler advance with cleaned velocity. owns_loop = true. Single NFE.
|
||||
-- ============================================================================
|
||||
|
||||
local C = require("md_solver_commons")
|
||||
|
||||
-- ── CURL PROXY (per-batch) ──────────────────────────────────────────────────
|
||||
|
||||
local function compute_curl_proxy(v_curr, v_prev, off, cnt, multi_scale)
|
||||
local curl = {}
|
||||
for i = 0, cnt - 1 do curl[i] = 0.0 end
|
||||
|
||||
for i = 1, cnt - 2 do
|
||||
local grad_curr = (v_curr[off + i + 1] - v_curr[off + i - 1]) * 0.5
|
||||
local grad_prev = (v_prev[off + i + 1] - v_prev[off + i - 1]) * 0.5
|
||||
curl[i] = curl[i] + (grad_curr - grad_prev)
|
||||
end
|
||||
|
||||
if multi_scale then
|
||||
for i = 2, cnt - 3 do
|
||||
local grad_curr = (v_curr[off + i + 2] - v_curr[off + i - 2]) * 0.25
|
||||
local grad_prev = (v_prev[off + i + 2] - v_prev[off + i - 2]) * 0.25
|
||||
curl[i] = curl[i] + 0.5 * (grad_curr - grad_prev)
|
||||
end
|
||||
for i = 4, cnt - 5 do
|
||||
local grad_curr = (v_curr[off + i + 4] - v_curr[off + i - 4]) * 0.125
|
||||
local grad_prev = (v_prev[off + i + 4] - v_prev[off + i - 4]) * 0.125
|
||||
curl[i] = curl[i] + 0.25 * (grad_curr - grad_prev)
|
||||
end
|
||||
end
|
||||
|
||||
local enstrophy, curl_max = 0.0, 0.0
|
||||
for i = 0, cnt - 1 do
|
||||
enstrophy = enstrophy + curl[i] * curl[i]
|
||||
local ac = math.abs(curl[i])
|
||||
if ac > curl_max then curl_max = ac end
|
||||
end
|
||||
return curl, enstrophy / math.max(cnt, 1), curl_max
|
||||
end
|
||||
|
||||
local function apply_shedding(v_out, off, cnt, curl, enstrophy_ema,
|
||||
threshold, strength, progressive)
|
||||
if strength < 1e-6 or enstrophy_ema <= threshold then return false end
|
||||
|
||||
local eff_strength = strength
|
||||
if progressive then
|
||||
local overshoot = C.clamp((enstrophy_ema - threshold) / (threshold + C.EPSILON), 0.0, 2.0)
|
||||
eff_strength = strength * (overshoot / 2.0)
|
||||
end
|
||||
if eff_strength < 1e-6 then return false end
|
||||
|
||||
local correction = {}
|
||||
correction[0] = 0.0
|
||||
for i = 1, cnt - 1 do correction[i] = correction[i - 1] + curl[i] end
|
||||
|
||||
local corr_energy = 0.0
|
||||
for i = 0, cnt - 1 do corr_energy = corr_energy + correction[i] * correction[i] end
|
||||
corr_energy = math.sqrt(corr_energy / math.max(cnt, 1) + C.EPSILON)
|
||||
|
||||
local scale = eff_strength / (corr_energy + C.EPSILON)
|
||||
local v_rms = 0.0
|
||||
for i = 0, cnt - 1 do v_rms = v_rms + v_out[off + i] * v_out[off + i] end
|
||||
v_rms = math.sqrt(v_rms / math.max(cnt, 1) + C.EPSILON)
|
||||
local max_corr = 0.05 * v_rms
|
||||
|
||||
for i = 0, cnt - 1 do
|
||||
v_out[off + i] = v_out[off + i] - C.clamp(correction[i] * scale, -max_corr, max_corr)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- ── SOLVER DEFINITION ───────────────────────────────────────────────────────
|
||||
|
||||
solver = {
|
||||
name = "md_vortex_v1",
|
||||
display = "MD Vortex V1",
|
||||
description = "Vorticity damping sampler. Multi-scale curl proxy, enstrophy tracking, targeted shedding. Batch-aware, shared anchor stack.",
|
||||
nfe = 1,
|
||||
order = 1,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = true,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
{ key = "shedding_strength", type = "slider", label = "Shedding Strength",
|
||||
default = 0.30, min = 0.0, max = 1.0, step = 0.05,
|
||||
hint = "Vortical energy damping. 0 = monitor only." },
|
||||
{ key = "enstrophy_threshold", type = "slider", label = "Enstrophy Threshold",
|
||||
default = 0.02, min = 0.001, max = 0.2, step = 0.001,
|
||||
hint = "EMA level triggering shedding. Calibrate with verbose=true." },
|
||||
{ key = "enstrophy_ema_alpha", type = "slider", label = "Enstrophy EMA Alpha",
|
||||
default = 0.1, min = 0.02, max = 0.5, step = 0.02,
|
||||
hint = "Tracker responsiveness." },
|
||||
{ key = "multi_scale", type = "toggle", label = "Multi-Scale Curl",
|
||||
default = true, hint = "Adjacent + skip-2 + skip-4 gradient changes." },
|
||||
{ key = "sigma_gate", type = "slider", label = "Sigma Gate",
|
||||
default = 0.9, min = 0.5, max = 1.0, step = 0.05,
|
||||
hint = "Sigma fraction above which shedding is disabled." },
|
||||
{ key = "progressive_shedding", type = "toggle", label = "Progressive Shedding",
|
||||
default = true, hint = "Strength scales with enstrophy overshoot." },
|
||||
},
|
||||
}
|
||||
|
||||
C.append_common_params(solver.params)
|
||||
|
||||
-- ── SAMPLE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
local p = params or {}
|
||||
local B, NPB = C.get_batch_routing(n)
|
||||
|
||||
local shed_str = C.num_param(p, "shedding_strength", 0.30)
|
||||
local enst_thresh = C.num_param(p, "enstrophy_threshold", 0.02)
|
||||
local enst_alpha = C.num_param(p, "enstrophy_ema_alpha", 0.1)
|
||||
local f_multi = C.bool_param(p, "multi_scale", true)
|
||||
local sigma_gate = C.num_param(p, "sigma_gate", 0.9)
|
||||
local f_prog = C.bool_param(p, "progressive_shedding", true)
|
||||
local opts = C.read_common_opts(p)
|
||||
local state = C.new_state()
|
||||
|
||||
-- Engine schedule has NO trailing 0 (fix ported from 46c081e): iterate all ns
|
||||
-- entries so the last iteration gets sigma_next = 0.0 and the terminal branch
|
||||
-- performs the final x0 projection. With ns - 1 that branch is dead code and
|
||||
-- the output keeps ~final-sigma noise.
|
||||
local ns, n_steps = #schedule, #schedule
|
||||
if n_steps < 1 then return end
|
||||
|
||||
local sigma_max = schedule[1]
|
||||
local v_prev = nil
|
||||
local enst_ema = {}
|
||||
for b = 0, B - 1 do enst_ema[b] = 0.0 end
|
||||
|
||||
local x = C.fa_to_tbl(xt, n)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[VORTEX V1] Schedule: %d steps | B=%d NPB=%d | shed=%.2f thresh=%.4f",
|
||||
n_steps, B, NPB, shed_str, enst_thresh))
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
local step_idx = i - 1
|
||||
local sigma_ratio = C.clamp(sigma_curr / math.max(sigma_max, C.EPSILON), 0.0, 1.0)
|
||||
|
||||
if sigma_next == 0.0 then
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_final = C.fa_to_tbl(vt_buf, n)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
break
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
model_fn(xt, sigma_curr)
|
||||
local v_curr = C.fa_to_tbl(vt_buf, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
|
||||
-- Relational decomposition
|
||||
if opts.rw > 0 then
|
||||
C.apply_relational(v_curr, n, B, NPB, sigma_ratio, sigma_max,
|
||||
opts.rw, opts.rw_sigma_pow, opts.drift_on, opts.drift_thr, x)
|
||||
end
|
||||
|
||||
local shedding_active, max_enst, max_curl = false, 0.0, 0.0
|
||||
|
||||
if v_prev ~= nil and sigma_ratio < sigma_gate then
|
||||
local v_shed = C.vec_clone(v_curr, n)
|
||||
for b = 0, B - 1 do
|
||||
local off = b * NPB
|
||||
local curl, enstrophy, curl_max = compute_curl_proxy(v_curr, v_prev, off, NPB, f_multi)
|
||||
enst_ema[b] = (1.0 - enst_alpha) * enst_ema[b] + enst_alpha * enstrophy
|
||||
if enst_ema[b] > max_enst then max_enst = enst_ema[b] end
|
||||
if curl_max > max_curl then max_curl = curl_max end
|
||||
if apply_shedding(v_shed, off, NPB, curl, enst_ema[b], enst_thresh, shed_str, f_prog) then
|
||||
shedding_active = true
|
||||
end
|
||||
end
|
||||
if not C.has_nan_inf(v_shed, n) then v_curr = v_shed end
|
||||
end
|
||||
|
||||
v_prev = C.vec_clone(v_curr, n)
|
||||
|
||||
local x_new = {}
|
||||
for j = 0, n - 1 do x_new[j] = x[j] + dt * v_curr[j] end
|
||||
if C.has_nan_inf(x_new, n) then
|
||||
local v_raw = C.fa_to_tbl(vt_buf, n)
|
||||
for j = 0, n - 1 do x_new[j] = x[j] + dt * v_raw[j] end
|
||||
end
|
||||
|
||||
opts.sigma_next = sigma_next
|
||||
opts.step_idx = step_idx
|
||||
C.post_advance(x_new, n, B, NPB, sigma_ratio, opts, state)
|
||||
|
||||
if opts.verbose then
|
||||
print(string.format("[VORTEX V1] step %02d | enst_ema=%.5f %s | curl_max=%.4f | rms=%.3f",
|
||||
step_idx, enst_ema[0] or 0, shedding_active and "SHEDDING" or "quiet",
|
||||
max_curl, C.rms(x_new, n)))
|
||||
end
|
||||
|
||||
x = x_new
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
C.tbl_to_fa(v_curr, vt_buf, n)
|
||||
if on_step(step_idx, sigma_curr, sigma_next) then return end
|
||||
x = C.fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
C.tbl_to_fa(x, xt, n)
|
||||
end
|
||||
@@ -0,0 +1,518 @@
|
||||
--[[
|
||||
storm_sampler_core.lua
|
||||
STORM -- Stabilized Taylor Oscillation with Runge-Kutta Memory
|
||||
Adaptive hybrid solver: STORK (stiff) + DPM++3M (stable), per-step dispatch
|
||||
|
||||
© 2026 Alexander Allan (MDMAchine) | A&E Concepts
|
||||
GPL v3 -- Public version. Gradient norm stiffness detection only.
|
||||
|
||||
Adapted for HOT-Step full-loop plugin API (owns_loop = true).
|
||||
All data uses 0-indexed FloatArray or 0-indexed Lua tables.
|
||||
|
||||
Version: 3.0.0 (HOT-Step plugin port from v2.1.0)
|
||||
--]]
|
||||
|
||||
solver = {
|
||||
name = "storm",
|
||||
display = "STORM",
|
||||
description = "Adaptive STORK/DPM++3M hybrid with stiffness detection",
|
||||
accent = "cyan",
|
||||
nfe = 0,
|
||||
order = 5,
|
||||
needs_model = false,
|
||||
stateful = true,
|
||||
stochastic = false,
|
||||
owns_loop = true,
|
||||
params = {
|
||||
{ key = "stiffness_threshold", type = "slider", label = "Detail Sensitivity",
|
||||
default = 0.15, min = 0.05, max = 0.50, step = 0.01,
|
||||
hint = "How aggressively complex passages get extra precision. Lower = more careful on transients and busy sections, higher = faster but looser" },
|
||||
{ key = "look_back_lambda", type = "slider", label = "Coherence Smoothing",
|
||||
default = 0.15, min = 0, max = 1, step = 0.01,
|
||||
hint = "Blends each step with previous ones for smoother output. 0 = off (raw), higher = more coherent but softer detail" },
|
||||
{ key = "look_back_snr_power", type = "slider", label = "Early-Step Focus",
|
||||
default = 1.5, min = 0.5, max = 3, step = 0.1,
|
||||
hint = "Concentrates smoothing on early noisy steps (structure). Higher = smooths structure more, leaves fine detail alone" },
|
||||
{ key = "rk_order", type = "select", label = "Precision Level",
|
||||
default = "auto",
|
||||
options = {
|
||||
{ value = "auto", label = "Auto (Recommended)" },
|
||||
{ value = "2", label = "Low (RK2)" },
|
||||
{ value = "3", label = "Medium (RK3)" },
|
||||
{ value = "4", label = "High (RK4)" },
|
||||
{ value = "5", label = "Maximum (RK5)" },
|
||||
},
|
||||
hint = "Solver accuracy per step. Auto ramps up gradually. Higher = cleaner but more compute per step" },
|
||||
{ key = "cache_depth", type = "slider", label = "History Memory",
|
||||
default = 5, min = 2, max = 10, step = 1,
|
||||
hint = "How many previous steps the solver remembers. More = smoother multi-step blending, but diminishing returns past 5" },
|
||||
{ key = "verbose", type = "toggle", label = "Verbose Logging",
|
||||
default = false,
|
||||
hint = "Print per-step solver decisions to the console (debug)" },
|
||||
},
|
||||
}
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- HELPERS: FloatArray ↔ Lua table (0-indexed)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function fa_to_tbl(fa, n)
|
||||
local t = {}
|
||||
for i = 0, n - 1 do t[i] = fa[i] end
|
||||
return t
|
||||
end
|
||||
|
||||
local function tbl_to_fa(t, fa, n)
|
||||
for i = 0, n - 1 do fa[i] = t[i] end
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- MATH HELPERS (0-indexed Lua tables)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function vec_norm(v, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + v[i] * v[i] end
|
||||
return math.sqrt(s)
|
||||
end
|
||||
|
||||
local function vec_sub_norm(a, b, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do local d = a[i] - b[i]; s = s + d * d end
|
||||
return math.sqrt(s)
|
||||
end
|
||||
|
||||
local function vec_dot(a, b, n)
|
||||
local s = 0.0
|
||||
for i = 0, n - 1 do s = s + a[i] * b[i] end
|
||||
return s
|
||||
end
|
||||
|
||||
local function vec_clone(v, n)
|
||||
local c = {}
|
||||
for i = 0, n - 1 do c[i] = v[i] end
|
||||
return c
|
||||
end
|
||||
|
||||
local function has_nan_inf_tbl(v, n)
|
||||
for i = 0, n - 1 do
|
||||
if v[i] ~= v[i] or math.abs(v[i]) == math.huge then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function clamp(x, lo, hi) return math.max(lo, math.min(hi, x)) end
|
||||
|
||||
local function randn_iso(n, scale)
|
||||
local out = {}
|
||||
for i = 0, n - 1, 2 do
|
||||
local u1 = math.max(1e-12, math.random())
|
||||
local u2 = math.random()
|
||||
local r = scale * math.sqrt(-2.0 * math.log(u1))
|
||||
out[i] = r * math.cos(2 * math.pi * u2)
|
||||
if i + 1 < n then
|
||||
out[i + 1] = r * math.sin(2 * math.pi * u2)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- LOOK-BACK SMOOTHER (arXiv:2602.09449)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function look_back_smooth(x_curr, x_prev, sigma_curr, sigma_max, lambda_base, snr_power, n)
|
||||
if x_prev == nil then return x_curr, 0.0 end
|
||||
local ratio = math.min(sigma_curr / math.max(sigma_max, 1e-8), 1.0)
|
||||
local lam = lambda_base * (ratio ^ snr_power)
|
||||
local out = {}
|
||||
for i = 0, n - 1 do out[i] = (1.0 - lam) * x_curr[i] + lam * x_prev[i] end
|
||||
return out, lam
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- STIFFNESS DETECTION
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function compute_stiffness(v_curr, v_cache, step_idx, baseline, threshold, ema_alpha, n_calib, n)
|
||||
threshold = threshold or 0.15
|
||||
ema_alpha = ema_alpha or 0.3
|
||||
n_calib = n_calib or 4
|
||||
|
||||
if #v_cache < 1 then return true, baseline, nil end
|
||||
|
||||
local v_prev = v_cache[#v_cache].v
|
||||
local norm_delta = vec_sub_norm(v_curr, v_prev, n)
|
||||
local norm_curr = vec_norm(v_curr, n) + 1e-8
|
||||
local raw_ratio = norm_delta / norm_curr
|
||||
|
||||
local prev_ema = baseline.ema or raw_ratio
|
||||
local smoothed = ema_alpha * raw_ratio + (1.0 - ema_alpha) * prev_ema
|
||||
baseline.ema = smoothed
|
||||
|
||||
local dot = vec_dot(v_curr, v_prev, n)
|
||||
local nc = vec_norm(v_curr, n)
|
||||
local np_ = vec_norm(v_prev, n)
|
||||
local cos_sim = dot / (nc * np_ + 1e-8)
|
||||
|
||||
if step_idx < n_calib then
|
||||
baseline.sum = (baseline.sum or 0.0) + smoothed
|
||||
baseline.count = (baseline.count or 0) + 1
|
||||
baseline.last_ratio = smoothed
|
||||
return true, baseline, cos_sim
|
||||
end
|
||||
|
||||
local bmean = baseline.sum / math.max(baseline.count, 1)
|
||||
local adap_thr = threshold * (bmean / 0.15)
|
||||
adap_thr = clamp(adap_thr, 0.05, 0.50)
|
||||
|
||||
local stiff = smoothed > adap_thr
|
||||
baseline.last_ratio = smoothed
|
||||
baseline.last_threshold = adap_thr
|
||||
return stiff, baseline, cos_sim
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- STORK MULTI-ORDER (AB2-AB5, single NFE, cached derivatives)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function stork_step(v_cache, x, sigma_curr, sigma_next, v_curr, rk_order, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
local n_cache = #v_cache
|
||||
|
||||
local actual_order
|
||||
if rk_order == "auto" then
|
||||
actual_order = (n_cache >= 1) and math.min(n_cache + 1, 5) or 1
|
||||
else
|
||||
actual_order = (n_cache >= 1) and math.min(tonumber(rk_order), n_cache + 1) or 1
|
||||
end
|
||||
actual_order = math.max(actual_order, 1)
|
||||
|
||||
if n_cache < 1 or actual_order <= 1 then
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
return x_next, 1
|
||||
end
|
||||
|
||||
local e0 = v_cache[#v_cache]
|
||||
local v_prev_0 = e0.v
|
||||
local sigma_prev = e0.sigma
|
||||
|
||||
-- Curvature damping
|
||||
local dot = vec_dot(v_curr, v_prev_0, n)
|
||||
local nc = vec_norm(v_curr, n)
|
||||
local np_ = vec_norm(v_prev_0, n)
|
||||
local cos_sim = dot / (nc * np_ + 1e-8)
|
||||
local damping = clamp(cos_sim, 0.0, 1.0)
|
||||
|
||||
local denom = sigma_curr - sigma_prev
|
||||
if math.abs(denom) < 1e-8 then
|
||||
local x_next = {}
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
return x_next, 2
|
||||
end
|
||||
local alpha = (sigma_next - sigma_curr) / denom
|
||||
|
||||
local x_next = {}
|
||||
|
||||
if actual_order == 2 then
|
||||
for i = 0, n - 1 do
|
||||
local v_extrap = v_curr[i] + (alpha * damping) * (v_curr[i] - v_prev_0[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * v_extrap)
|
||||
end
|
||||
|
||||
elseif actual_order == 3 and n_cache >= 2 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 then
|
||||
for i = 0, n - 1 do
|
||||
local ve = v_curr[i] + (alpha * damping) * (v_curr[i] - v1[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * ve)
|
||||
end
|
||||
actual_order = 2
|
||||
else
|
||||
local c0 = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do
|
||||
local v_pred = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (v_pred - v_curr[i]))
|
||||
end
|
||||
end
|
||||
|
||||
elseif actual_order == 4 and n_cache >= 3 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local v3, s3 = v_cache[#v_cache - 2].v, v_cache[#v_cache - 2].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
local h2 = s2 - s3
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 or math.abs(h2) < 1e-8 then
|
||||
local c0 = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 3
|
||||
else
|
||||
local c0 = 1.0 + dt / (2.0 * h) + dt ^ 2 / (3.0 * h * h1) + dt ^ 3 / (4.0 * h * h1 * h2)
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1 + dt ^ 2 / (2.0 * h1 * h2))
|
||||
local c2 = (dt ^ 2 / (3.0 * h * h1)) * (1.0 + dt / (2.0 * h2))
|
||||
local c3 = -(dt ^ 3) / (4.0 * h * h1 * h2)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i] + c3 * v3[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
end
|
||||
|
||||
elseif actual_order >= 5 and n_cache >= 4 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local v3, s3 = v_cache[#v_cache - 2].v, v_cache[#v_cache - 2].sigma
|
||||
local v4, s4 = v_cache[#v_cache - 3].v, v_cache[#v_cache - 3].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
local h2 = s2 - s3
|
||||
local h3 = s3 - s4
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 or math.abs(h2) < 1e-8 or math.abs(h3) < 1e-8 then
|
||||
local c0 = 1.0 + dt / (2.0 * h) + dt ^ 2 / (3.0 * h * h1) + dt ^ 3 / (4.0 * h * h1 * h2)
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1 + dt ^ 2 / (2.0 * h1 * h2))
|
||||
local c2 = (dt ^ 2 / (3.0 * h * h1)) * (1.0 + dt / (2.0 * h2))
|
||||
local c3 = -(dt ^ 3) / (4.0 * h * h1 * h2)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i] + c3 * v3[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 4
|
||||
else
|
||||
local c0 = 1.0 + dt / (2.0 * h) + dt ^ 2 / (3.0 * h * h1) + dt ^ 3 / (4.0 * h * h1 * h2) + dt ^ 4 / (5.0 * h * h1 * h2 * h3)
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1 + dt ^ 2 / (2.0 * h1 * h2) + dt ^ 3 / (3.0 * h1 * h2 * h3))
|
||||
local c2 = (dt ^ 2 / (3.0 * h * h1)) * (1.0 + dt / (2.0 * h2) + dt ^ 2 / (3.0 * h2 * h3))
|
||||
local c3 = -(dt ^ 3 / (4.0 * h * h1 * h2)) * (1.0 + dt / (2.0 * h3))
|
||||
local c4 = dt ^ 4 / (5.0 * h * h1 * h2 * h3)
|
||||
for i = 0, n - 1 do
|
||||
local vp = c0 * v_curr[i] + c1 * v1[i] + c2 * v2[i] + c3 * v3[i] + c4 * v4[i]
|
||||
x_next[i] = x[i] + dt * (v_curr[i] + damping * (vp - v_curr[i]))
|
||||
end
|
||||
actual_order = 5
|
||||
end
|
||||
|
||||
else
|
||||
-- Fallback AB2
|
||||
for i = 0, n - 1 do
|
||||
local ve = v_curr[i] + (alpha * damping) * (v_curr[i] - v_prev_0[i])
|
||||
x_next[i] = x[i] + dt * (0.5 * v_curr[i] + 0.5 * ve)
|
||||
end
|
||||
actual_order = 2
|
||||
end
|
||||
|
||||
return x_next, actual_order
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- DPM++3M -- smooth schedule path
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local function dpmpp3m_step(v_cache, x, sigma_curr, sigma_next, v_curr, n)
|
||||
local dt = sigma_next - sigma_curr
|
||||
local x_next = {}
|
||||
|
||||
if #v_cache >= 2 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local v2, s2 = v_cache[#v_cache - 1].v, v_cache[#v_cache - 1].sigma
|
||||
local h = sigma_curr - s1
|
||||
local h1 = s1 - s2
|
||||
if math.abs(h) < 1e-8 or math.abs(h1) < 1e-8 then
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
else
|
||||
local cc = 1.0 + (dt / (2.0 * h)) + (dt ^ 2 / (3.0 * h * h1))
|
||||
local c1 = -(dt / (2.0 * h)) * (1.0 + dt / h1)
|
||||
local c2 = (dt ^ 2) / (3.0 * h * h1)
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * (cc * v_curr[i] + c1 * v1[i] + c2 * v2[i]) end
|
||||
end
|
||||
elseif #v_cache >= 1 then
|
||||
local v1, s1 = v_cache[#v_cache].v, v_cache[#v_cache].sigma
|
||||
local h = sigma_curr - s1
|
||||
if math.abs(h) < 1e-8 then
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
else
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * (v_curr[i] + (dt / (2.0 * h)) * (v_curr[i] - v1[i])) end
|
||||
end
|
||||
else
|
||||
for i = 0, n - 1 do x_next[i] = x[i] + dt * v_curr[i] end
|
||||
end
|
||||
|
||||
return x_next
|
||||
end
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- SAMPLE — Full-loop entry point
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function sample(xt, vt_buf, schedule, n, model_fn)
|
||||
-- Read params
|
||||
local p = params or {}
|
||||
local thr = p.stiffness_threshold or 0.15
|
||||
local lb_lambda = p.look_back_lambda or 0.35
|
||||
local lb_snr_pow = p.look_back_snr_power or 1.5
|
||||
local rk_order = p.rk_order or "auto"
|
||||
local depth_max = p.cache_depth or 5
|
||||
local verbose = p.verbose or false
|
||||
|
||||
local hyst = 0.05
|
||||
local ema_a = 0.3
|
||||
local calib_frac = 0.12
|
||||
|
||||
local ns = #schedule
|
||||
-- The engine's schedule table has NO trailing 0 (unlike ComfyUI sigmas):
|
||||
-- iterate all ns entries so the last iteration gets sigma_next = 0.0 and
|
||||
-- the terminal branch performs the final x0 projection. With ns - 1 the
|
||||
-- terminal branch is dead code and the output keeps ~final-sigma noise.
|
||||
local n_steps = ns
|
||||
if n_steps < 1 then return end
|
||||
|
||||
local v_cache = {}
|
||||
local baseline = { sum = 0.0, count = 0 }
|
||||
local sigma_max = schedule[1]
|
||||
local n_calib = math.max(2, math.min(5, math.floor(n_steps * calib_frac)))
|
||||
local lb_enabled = (lb_lambda > 0)
|
||||
|
||||
if verbose then
|
||||
print(string.format("[STORM] Schedule: %d steps | Calib: %d | RK: %s | Cache: %d",
|
||||
n_steps, n_calib, tostring(rk_order), depth_max))
|
||||
end
|
||||
|
||||
-- Working copy of xt as a Lua table (we write back to FloatArray at each step)
|
||||
local x = fa_to_tbl(xt, n)
|
||||
|
||||
-- Seed x_prev for look-back
|
||||
local x_prev_lb = nil
|
||||
if lb_enabled then
|
||||
x_prev_lb = {}
|
||||
for i = 0, n - 1 do
|
||||
local u1 = math.max(1e-12, math.random())
|
||||
local u2 = math.random()
|
||||
local r = (sigma_max * 0.1) * math.sqrt(-2.0 * math.log(u1))
|
||||
x_prev_lb[i] = x[i] + r * math.cos(2 * math.pi * u2)
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper: evaluate model and return velocity as Lua table
|
||||
local function eval_model(x_tbl)
|
||||
tbl_to_fa(x_tbl, xt, n)
|
||||
model_fn(xt, 0) -- dummy t, we'll set it properly below
|
||||
return fa_to_tbl(vt_buf, n)
|
||||
end
|
||||
|
||||
-- Proper eval: writes x_tbl to xt, calls model at t_val, returns velocity table
|
||||
local function eval_at(x_tbl, t_val)
|
||||
tbl_to_fa(x_tbl, xt, n)
|
||||
model_fn(xt, t_val)
|
||||
return fa_to_tbl(vt_buf, n)
|
||||
end
|
||||
|
||||
for i = 1, n_steps do
|
||||
local sigma_curr = schedule[i]
|
||||
local sigma_next = (i < ns) and schedule[i + 1] or 0.0
|
||||
|
||||
-- Terminal step
|
||||
if sigma_next == 0.0 then
|
||||
local v_final = eval_at(x, sigma_curr)
|
||||
for j = 0, n - 1 do x[j] = x[j] - v_final[j] * sigma_curr end
|
||||
if verbose then
|
||||
print(string.format("[STORM] Step %02d: FINAL (Euler terminal)", i - 1))
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
local x_prev_lb_before = nil
|
||||
if lb_enabled then x_prev_lb_before = vec_clone(x, n) end
|
||||
|
||||
-- Evaluate velocity
|
||||
local v_curr = eval_at(x, sigma_curr)
|
||||
|
||||
-- Stiffness detection
|
||||
local stiff, cos_sim_out
|
||||
if #v_cache >= 1 then
|
||||
stiff, baseline, cos_sim_out = compute_stiffness(
|
||||
v_curr, v_cache, i - 1, baseline, thr, ema_a, n_calib, n)
|
||||
else
|
||||
stiff, cos_sim_out = true, nil
|
||||
end
|
||||
|
||||
-- Hysteresis
|
||||
local prev_mode = baseline.prev_mode or "STORK"
|
||||
if prev_mode == "DPM++" and not stiff then
|
||||
if (baseline.last_ratio or 0) > (baseline.last_threshold or thr) + hyst then
|
||||
stiff = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Dispatch
|
||||
local x_next, actual_order, mode
|
||||
if stiff then
|
||||
x_next, actual_order = stork_step(v_cache, x, sigma_curr, sigma_next, v_curr, rk_order, n)
|
||||
mode = "STORK"
|
||||
else
|
||||
x_next = dpmpp3m_step(v_cache, x, sigma_curr, sigma_next, v_curr, n)
|
||||
mode = "DPM++"
|
||||
actual_order = 3
|
||||
end
|
||||
|
||||
-- Verbose
|
||||
if verbose then
|
||||
local lr = baseline.last_ratio or 0.0
|
||||
local lt = baseline.last_threshold or thr
|
||||
local cs = cos_sim_out and string.format("%.4f", cos_sim_out) or "N/A"
|
||||
local tag = (stiff and prev_mode == "DPM++") and " -> CURVATURE SPIKE" or ""
|
||||
print(string.format("[STORM] Step %02d: %-5s RK%d | Ratio: %.3f | Thr: %.3f | cos: %s%s",
|
||||
i - 1, mode, actual_order, lr, lt, cs, tag))
|
||||
end
|
||||
|
||||
-- NaN guard
|
||||
if has_nan_inf_tbl(x_next, n) then
|
||||
print(string.format("[STORM] NaN/Inf at step %d. Flushing cache.", i - 1))
|
||||
local dt = sigma_next - sigma_curr
|
||||
x_next = {}
|
||||
for j = 0, n - 1 do x_next[j] = x[j] + dt * v_curr[j] end
|
||||
v_cache = {}
|
||||
baseline.prev_mode = "STORK"
|
||||
actual_order = 1
|
||||
end
|
||||
|
||||
-- Update cache
|
||||
table.insert(v_cache, { v = v_curr, sigma = sigma_curr })
|
||||
while #v_cache > depth_max do table.remove(v_cache, 1) end
|
||||
baseline.prev_mode = mode
|
||||
|
||||
x = x_next
|
||||
|
||||
-- Look-Back smoothing
|
||||
if lb_enabled and x_prev_lb ~= nil then
|
||||
local lam
|
||||
x, lam = look_back_smooth(x, x_prev_lb, sigma_curr, sigma_max, lb_lambda, lb_snr_pow, n)
|
||||
if verbose then
|
||||
print(string.format("[STORM] LookBack λ=%.4f @ σ=%.3f", lam, sigma_curr))
|
||||
end
|
||||
end
|
||||
x_prev_lb = x_prev_lb_before
|
||||
|
||||
-- Write x back to xt FloatArray for on_step hooks (DCW, repaint)
|
||||
tbl_to_fa(x, xt, n)
|
||||
-- Write velocity to vt_buf for DCW
|
||||
tbl_to_fa(v_curr, vt_buf, n)
|
||||
|
||||
-- Report step (engine hooks: DCW, repaint, progress)
|
||||
if on_step(i - 1, sigma_curr, sigma_next) then return end
|
||||
|
||||
-- Re-read xt in case hooks modified it (DCW, repaint)
|
||||
x = fa_to_tbl(xt, n)
|
||||
end
|
||||
|
||||
-- Write final x0 to xt
|
||||
tbl_to_fa(x, xt, n)
|
||||
end
|
||||
Reference in New Issue
Block a user