Skip to content
Oakfield Operator Calculus Function Reference Site

Flux Lens

The Flux Lens is a context-level diagnostic that computes a spectral energy-transfer spectrum from a 1D field and uses it to maintain a tapered bandpass reconstruction in both spectral and physical space.

It is designed to answer:

  • Where is the dominant spectral energy transfer? (kc)
  • How concentrated is that transfer? (k50, k90)
  • What does the corresponding band-limited signal look like? (band_phys, band_spec)

The Flux Lens uses a skew-Burgers spectral flux to estimate inter-scale energy transfer.

Given a real or complex field uu with spectral coefficients u^(k)\hat{u}(k):

Step 1 — Nonlinear term. Form the squared field u2u^2 and FFT it to get u2^(k)\widehat{u^2}(k). Compute the skew-Burgers nonlinear term:

N^(k)=i2ku2^(k)\hat{N}(k) = -\frac{i}{2}\, k\, \widehat{u^2}(k)

Step 2 — Mode-by-mode energy exchange rate. The work done by mode kk:

S(k)=Re ⁣[u^(k)N^(k)]S(k) = \operatorname{Re}\!\left[\overline{\hat{u}(k)}\cdot\hat{N}(k)\right]

where ()\overline{(\cdot)} denotes the complex conjugate.

Step 3 — Cumulative flux. Sort modes by k|k| and accumulate:

Π(K)=kKS(k)\Pi(K) = \sum_{|k| \leq K} S(k)

Π(K)\Pi(K) is the net energy flux from scales larger than KK to scales smaller than KK. Peaks and sign changes identify dominant energy cascades.

Marks derived from Π\Pi:

MarkDefinition
kcWavenumber of peak flux magnitude: kc=argmaxKΠ(K)k_c = \arg\max_K \lvert\Pi(K)\rvert
k50Smallest KK such that Π(K)0.5Π(kc)\Pi(K) \geq 0.5\,\Pi(k_c)
k90Smallest KK such that Π(K)0.9Π(kc)\Pi(K) \geq 0.9\,\Pi(k_c)

Dealiasing. When use_dealias = true, all modes above the 2/3 rule cutoff are zeroed before the nonlinear product:

kcut=23kmaxk_{\text{cut}} = \frac{2}{3}\, k_{\max}
  1. Enable the lens with flux_lens_set_enabled(ctx, true).
  2. Optionally select a field with flux_lens_set_field_index.
  3. Read scalars from ooc.flux_lens(ctx) and arrays from lens:buckets() / lens:band().

local lens = ooc.flux_lens(ctx)

Returns a step-cached snapshot proxy. Fields are read lazily; the proxy refreshes automatically at step boundaries. Call lens:refresh() to force an immediate update.

FieldTypeDescription
enabledboolean Lens is enabled; disabled lenses free their FFT buffers.
lockedboolean true when band_lo/band_hi are frozen and will not track marks automatically.
force_updateboolean A refresh was explicitly requested for the next recompute.
band_readyboolean band() arrays are populated and valid.
validboolean Convenience mirror of marks.valid; true when the field is 1D and the computation succeeded.
field_indexinteger 0-based index of the field driving the lens.
use_dealiasboolean Whether 2/3 dealiasing is applied before the nonlinear product.
smoothingdouble Current EMA smoothing factor for band tracking.
min_bandwidthdouble Minimum enforced band_hi - band_lo.
update_periodinteger Minimum accepted steps between recomputes (0 = every step).
last_update_stepinteger Step index of the most recent recompute.
band_lo, band_hidouble Current bandpass bounds in $
target_band_lo, target_band_hidouble Target bounds derived from marks (before EMA smoothing).
band_max_componentdouble Largest component value in the current band snapshot.
band_max_magnitudedouble Largest complex magnitude in the current band snapshot.
kcdouble Flux-peak wavenumber.
k50double 50 % cumulative-flux wavenumber.
k90double 90 % cumulative-flux wavenumber.
pi_at_kcdouble Π\Pi value at kck_c.
pi_min, pi_maxdouble Flux extremes across all $
absS_totaldouble Total absolute work: $\sum_k
max_kdouble Nyquist wavenumber (N/2N/2 for a length-NN field).
bucket_capacityinteger Allocated capacity of the bucket buffers.
bucket_countinteger Number of populated $
band_capacityinteger Allocated length of the band() arrays.
markstableNested mark snapshot {valid, kc, k50, k90, pi_at_kc, total_abs_work, pi_min, pi_max, kmax}.
MethodReturnsDescription
lens:refresh()boolean Force an immediate snapshot update; returns true when a fresh snapshot could be read.
lens:buckets()tablePer-bucket arrays {k, S, absS, pi} (1-based, length = bucket_count).
lens:band()table{spec, phys} complex-pair arrays of the band-limited signal (length = band_capacity).

local b = lens:buckets()
-- b.k[i] : wavenumber |k| of bucket i
-- b.S[i] : energy exchange rate S(k)
-- b.absS[i] : |S(k)|
-- b.pi[i] : cumulative flux Π(k)
-- All arrays have the same length: lens.bucket_count
local band = lens:band()
-- band.spec[i] : spectral-domain complex sample {re, im}
-- band.phys[i] : physical-domain complex sample {re, im}
-- Both arrays have length: lens.band_capacity

All control functions take ctx as the first argument and return true on success, false if the lens is not initialised.

ooc.flux_lens_set_enabled(ctx, true) -- enable (allocates FFT buffers on first use)
ooc.flux_lens_set_enabled(ctx, false) -- disable (frees internal buffers)
ooc.flux_lens_force_refresh(ctx) -- trigger immediate recompute regardless of update_period
ooc.flux_lens_set_field_index(ctx, index) -- 0-based integer index
FunctionParameterDescription
flux_lens_set_update_period(ctx, steps)integer ≥ 0Minimum accepted steps between recomputes; 0 = every step.
flux_lens_set_smoothing(ctx, alpha)double ∈ [0, 1]EMA factor for band motion: 0 = instant snap, 1 = frozen.
flux_lens_set_min_bandwidth(ctx, width)double Enforce a minimum band_hi − band_lo in $
flux_lens_set_use_dealias(ctx, enabled)boolean Toggle 2/3 dealiasing on the nonlinear product.
ooc.flux_lens_set_locked(ctx, true) -- freeze band_lo/hi; marks still update
ooc.flux_lens_set_locked(ctx, false) -- allow band to track marks automatically
ooc.flux_lens_scale_width(ctx, scale) -- multiply (band_hi − band_lo) by scale (>1 wider, <1 narrower)
ooc.flux_lens_shift_center(ctx, shift) -- translate band center by shift in |k| units

scale_width and shift_center operate on the current band_lo/hi when the lens has a valid snapshot, otherwise on target_band_lo/hi.

ooc.flux_lens_set_band_low(ctx, k_low) -- set band_lo explicitly; auto-locks the lens
ooc.flux_lens_set_band_high(ctx, k_high) -- set band_hi explicitly; auto-locks the lens

ooc.flux_lens_set_enabled(ctx, true)
ooc.flux_lens_set_update_period(ctx, 30)
local lens = ooc.flux_lens(ctx)
ooc.step(ctx)
lens:refresh()
if lens.valid then
ooc.log("kc=%.3f k50=%.3f k90=%.3f pi_max=%.4f",
lens.kc, lens.k50, lens.k90, lens.pi_max)
end
ooc.flux_lens_set_enabled(ctx, true)
ooc.flux_lens_set_update_period(ctx, 200)
for i = 1, 500 do ooc.step(ctx) end
ooc.flux_lens_force_refresh(ctx)
local lens = ooc.flux_lens(ctx)
lens:refresh()
ooc.flux_lens_set_locked(ctx, true)
ooc.log("band locked at [%.2f, %.2f]", lens.band_lo, lens.band_hi)
ooc.flux_lens_set_enabled(ctx, true)
ooc.flux_lens_set_band_low(ctx, 2.0) -- auto-locks
ooc.flux_lens_set_band_high(ctx, 8.0) -- already locked; updates hi bound only
local lens = ooc.flux_lens(ctx)
lens:refresh()
ooc.log("band=[%.1f, %.1f] locked=%s", lens.band_lo, lens.band_hi, tostring(lens.locked))
local a = ooc.add_field(ctx, {512}, { type = "complex_double", fill = {0, 0} })
local b = ooc.add_field(ctx, {512}, { type = "complex_double", fill = {0, 0} })
ooc.flux_lens_set_enabled(ctx, true)
ooc.flux_lens_set_field_index(ctx, 1) -- 0-based field index
ooc.flux_lens_force_refresh(ctx)
local lens = ooc.flux_lens(ctx)
lens:refresh()
local b = lens:buckets()
for i = 1, #b.k do
ooc.log("k=%.3f S=%+.4f Pi=%.4f", b.k[i], b.S[i], b.pi[i])
end
ooc.flux_lens_set_enabled(ctx, true)
ooc.flux_lens_set_smoothing(ctx, 0.05)
ooc.flux_lens_set_min_bandwidth(ctx, 1.0)
for i = 1, 100 do ooc.step(ctx) end
local lens = ooc.flux_lens(ctx)
lens:refresh()
if lens.band_ready then
local band = lens:band()
local mid = math.floor(lens.band_capacity / 2) + 1
ooc.log("band phys centre: %.4f + %.4fi", band.phys[mid][1], band.phys[mid][2])
end