Operator Calculus, Co.
Browse Studio documentation
On this page

What It Is 🧠

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)

Note

Flux Lens is currently implemented for 1D fields only. If the selected field has rank ≠ 1, lens.valid is false.


Mathematical Formulation 📐

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)=−i2 k u2^(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)=∑∣k∣≤KS(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:

Mark Definition
kc Wavenumber of peak flux magnitude: kc=arg⁡max⁡K∣Π(K)∣k_c = \arg\max_K \lvert\Pi(K)\rvert
k50 Smallest KK such that Π(K)≥0.5 Π(kc)\Pi(K) \geq 0.5\,\Pi(k_c)
k90 Smallest 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=23 kmax⁡k_{\text{cut}} = \frac{2}{3}\, k_{\max}

Quick Start 🚀

  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().

API 📚

Create Proxy

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.

Snapshot Fields

Field Type Description
enabled boolean Lens is enabled; disabled lenses free their FFT buffers.
locked boolean true when band_lo/band_hi are frozen and will not track marks automatically.
force_update boolean A refresh was explicitly requested for the next recompute.
band_ready boolean band() arrays are populated and valid.
valid boolean Convenience mirror of marks.valid; true when the field is 1D and the computation succeeded.
field_index integer 0-based index of the field driving the lens.
use_dealias boolean Whether 2/3 dealiasing is applied before the nonlinear product.
smoothing double Current EMA smoothing factor for band tracking.
min_bandwidth double Minimum enforced band_hi - band_lo.
update_period integer Minimum accepted steps between recomputes (0 = every step).
last_update_step integer Step index of the most recent recompute.
band_lo, band_hi double Current bandpass bounds in ∣k∣\vert k\vert units.
target_band_lo, target_band_hi double Target bounds derived from marks (before EMA smoothing).
band_max_component double Largest component value in the current band snapshot.
band_max_magnitude double Largest complex magnitude in the current band snapshot.
kc double Flux-peak wavenumber.
k50 double 50 % cumulative-flux wavenumber.
k90 double 90 % cumulative-flux wavenumber.
pi_at_kc double Π\Pi value at kck_c.
pi_min, pi_max double Flux extremes across all ∣k∣\vert k\vert buckets.
absS_total double Total absolute work: ∑k∣S(k)∣\sum_k \vert S(k)\vert .
max_k double Nyquist wavenumber (N/2N/2 for a length-NN field).
bucket_capacity integer Allocated capacity of the bucket buffers.
bucket_count integer Number of populated ∣k∣\vert k\vert buckets.
band_capacity integer Allocated length of the band() arrays.
marks table Nested mark snapshot {valid, kc, k50, k90, pi_at_kc, total_abs_work, pi_min, pi_max, kmax}.

Proxy Methods

Method Returns Description
lens:refresh() boolean Force an immediate snapshot update; returns true when a fresh snapshot could be read.
lens:buckets() table Per-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).

Array Layouts 📊

Buckets

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

Band

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

Control Functions 🧰

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

Enable / Disable

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

Field Selection

ooc.flux_lens_set_field_index(ctx, index)   -- 0-based integer index

Update Rate & Smoothing

Function Parameter Description
flux_lens_set_update_period(ctx, steps) integer ≥ 0 Minimum 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 ∣k∣\vert k\vert units.
flux_lens_set_use_dealias(ctx, enabled) boolean Toggle 2/3 dealiasing on the nonlinear product.

Lock & Band Manipulation

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.

Direct Band Bounds

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

Tip

flux_lens_set_band_low and flux_lens_set_band_high automatically call flux_lens_set_locked(ctx, true) if the lens is not already locked — preventing mark-based updates from overriding your explicit bounds on the next recompute.


Examples 🧪

Minimal enable + marks readout

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

Slow refresh + lock band at current marks

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)

Explicit band bounds with direct setters

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))

Select which field drives the lens

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

Bucket export for analysis

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

Band-limited physical signal

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