Skip to content

Spectral

The spectral axis

A reading of the input at every byte position: the byte-class mix at five timescales, the entropy, the byte period and the novelty of the neighbourhood, and the offsets where that reading changes.

Source: src/spectral.rs.

The reader is one causal forward pass: the reading at byte t depends only on the bytes up to t, so a change in the input shows in the reading a few bytes after the bytes that cause it.

Reading the axis

$ trex spectral --text 'the quick brown fox jumps over the lazy dog'
trex spectral: 43 bytes, 3 frames (hop 16), 0 change-points
  entropy   min 0.62  mean 0.68  max 0.73  (normalized bits/byte)
  period    none detected (no strong byte-periodicity)
  texture timeline (merged regions, cut at change-points):
    [       0..43      ] prose  H=0.68 per=0 nov=1.00
FlagEffect
--segmentthe change-point byte offsets
--bandsone row a frame: the medium-clock class mix, entropy, period, novelty and texture
--classifythe texture timeline alongside --segment or --bands; it is printed by default otherwise
--code-classifythe regions read by texture and token shape together: table, code, prose, blob, numeric or mixed
--jsonthe field’s frames and boundaries as JSON
--limit Nat most N rows of --segment and --bands

In PowerShell -Classify adds the regions --code-classify prints, and -Detail adds every frame.

In a pattern

\F{pred} is a token atom: a token matches when the predicate holds of the pooled reading over its span.

AtomHolds when
\F{entropy>0.8} / \F{entropy<0.3}the pooled entropy is above or below the threshold, read to the hundredth
\F{period=4}the dominant byte period is 4
\F{period} / \F{period:any} / \F{period:line}any strong period is detected; the three are one predicate
\F{texture:prose} / :code / :math / :datathe pooled texture is that class; texture= is the same
\F{onset}a change-point lies inside the token’s span

A pattern naming the axis runs on the set engine, which builds the field once a scan with only the readings its atoms take: entropy the entropy, period the period, texture the class mix and the entropy, and onset those two and the change-points.

$ trex scan '\F{texture:code}+' --text 'the quick brown fox jumps; fn add(a,b){let c=a+b;return c*2;}'
[33..61] "(a,b){let c=a+b;return c*2;}"

fn add is read as prose: the medium clock still holds the sentence before it.

In the lexer

The lexer collapses each whitespace-free run whose rolling entropy holds at or above 0.85 for at least 48 bytes (BLOB_ENTROPY_PCT, BLOB_MIN_LEN) into one opaque token, so base64, hex and packed data are not split into word and number tokens:

$ trex scan . --text 'key = aGVsbG8gd29ybGQgdGhpcyBpcyBiYXNlNjQgZGF0YSB0aGF0IGtlZXBzIGdvaW5nIGZvciBhIHdoaWxl end'
[0..3] "key"
[4..5] "="
[6..86] "aGVsbG8gd29ybGQgdGhpcyBpcyBiYXNlNjQgZGF0YSB0aGF0IGtlZXBzIGdvaW5nIGZvciBhIHdoaWxl"
[87..90] "end"

Prose and code stay below the gate. spectral::high_entropy_runs returns the runs.

Data model

SpectralFrame, one position’s reading:

FieldTypeMeaning
bands[f32; 25]the filterbank: bands[d*5 + c] is decay d by byte class c, each in [0,1]
entropyf32rolling Shannon entropy of the local window, normalized to [0,1]
periodu16the dominant byte period of the neighbourhood, 0 for none
period_strengthf32the normalized autocorrelation peak, [0,1]
noveltyf32k-gram surprise, [0,1], 1 for a first sighting

SpectralField, keyed by byte offset:

FieldTypeMeaning
lenusizeinput length in bytes
hopusizesampling stride: frames[j] reads the bytes up to (j+1)*hop - 1
framesVec<SpectralFrame>the sampled frames
boundariesVec<usize>change-point byte offsets, ascending
needsNeedsthe readings the field carries; one it does not carry reads as zero
MethodReturns
frame_at(byte)the sampled frame covering byte
signature(start, end)the mean of the frames covering [start, end), with the period of the strongest one
boundary_near(byte, tol)whether a change-point lies within tol bytes
boundary_in(start, end)whether a change-point lies inside the span

analyze(input) computes every reading with the default configuration, analyze_with(input, cfg) with a SpectralConfig, and analyze_needing(input, cfg, needs) only the readings needs names. texture_of(&frame) classifies a frame as Prose, Code, Math, Data or Mixed, regions(&field) cuts the input at its change-points and merges adjacent spans of one texture, and code_regions(input) labels each span Code, Blob, Prose, Numeric or Mixed from a fresh pass over that span’s bytes.

IndexByte classBytes
0digit0-9
1alpha_, A-Z, a-z
2spaceASCII whitespace
3punctother printable ASCII
4high>= 0x80 and non-space control bytes
SpectralConfig fieldDefaultMeaning
hop16frame sampling stride in bytes
entropy_window64rolling entropy window in bytes
period_window512autocorrelation window in bytes
max_lag128the largest period searched
period_hop64baseline stride between autocorrelation evaluations
ngram4k-gram size for novelty
novelty_window4096novelty window in k-grams
cp_threshold4.0change-point sensitivity k
cp_floor0.05change-point floor f, a fraction of the mean
cp_min_gap4minimum bytes between two change-points

Algorithms

Filterbank

Five leaky integrators per byte class, x[t] = a*x[t-1] + (1-a)*u[t], with u[t] one for the byte’s class and zero for the others. The decays a_d = exp(-1/tau_d) follow a power-of-two ladder of time constants:

dtau_d (bytes)a_dreach
020.6065byte-local
180.8825sub-word
2320.9692word and line
31280.9922line and record
45120.9980block and section

Each band is a leaky average in [0,1]. The texture is read from the medium clock, d = 2.

Entropy, period and novelty

  • Entropy: a 256-bin histogram over the sliding window, keeping s = sum c_i*log2(c_i) incrementally, so each byte is O(1); H = log2(n) - s/n, normalized by log2(min(W, 256)).
  • Period: centered autocorrelation over the trailing window at lags 2..max_lag, the dominant period the arg-max with prominence, recomputed every period_hop bytes; the hop rises with the input so the total work stays under a fixed operation budget.
  • Novelty: a rolling hash of the last k bytes with a count map over the window and a ring buffer for eviction; novelty = 1/(1 + count_before_increment).

Change-points

Per byte, d[t] is the distance between the two fastest clocks’ class mix and entropy at t and at t-1. An EWMA mean and mean absolute deviation of d give the threshold: a boundary is recorded where d[t] > mean + max(k*mad, f*mean) and at least cp_min_gap bytes have passed since the last.

The floor f acts where d is constant rather than noisy: a run of one byte class drives mad to zero and puts the threshold on the signal. Over six files of prose and source in both line-ending conventions (69,118 boundaries as CRLF, 74,841 as LF) and three padded binaries with constant runs of 5,140, 4,169 and 9,812 bytes, a floor anywhere from 0.03 to 0.10 moves no boundary. In those three binaries the run lies 26 kB, 49 kB and 380 kB in, and the reader marks its two ends and nothing between.

The floor acts where the input opens with the constant run. mean then converges onto the run’s own residual divergence, the spread collapses, and without the floor one ULP clears the threshold at byte 2218, for letters and zeros alike and at run lengths from 3,735 to 14,864; the hysteresis then holds through the run’s real end, so that boundary is lost too. In a corpus of 14,017 binaries three files open this way, all sparse instruction-set decode tables:

FileBytesLeading runFloor 0.00Floor 0.04 and up
arm32.idx_t32_sub.bin16,38414,864[2218][14864]
ppc.form_idx.bin6,6546,654 (all)[2218]none
ppc.prefix_bits.bin13,3123,735[2218, ...][3735, ...]

Each boundary the floor gains is the leading run’s length, where the zeros stop. The knee is 0.04 on all three files and on a synthetic constant run; the default is 0.05.

Cost

KernelPer-byte costBounded by
filterbank25 multiply-addsfixed
entropyO(1) incrementalfixed
noveltyO(1) amortizednovelty_window
periodO(1) amortizedthe adaptive period_hop under a fixed operation budget
change-pointO(1)fixed