use crate::token::Token;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Signal {
Magnitude,
StressDepth,
Length,
}
impl Signal {
#[must_use]
pub fn parse(name: &str) -> Option<Signal> {
match name {
"magnitude" => Some(Signal::Magnitude),
"stress" => Some(Signal::StressDepth),
"length" => Some(Signal::Length),
_ => None,
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Signal::Magnitude => "magnitude",
Signal::StressDepth => "stress",
Signal::Length => "length",
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FlowConfig {
pub window: usize,
pub steady_band: f32,
}
impl Default for FlowConfig {
fn default() -> Self {
Self {
window: 4,
steady_band: 0.05,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FlowFrame {
pub slope: f32,
pub direction: i8,
pub momentum: u16,
}
#[derive(Clone, Debug, Default)]
pub struct FlowField {
pub n_tokens: usize,
pub spans: Vec<(usize, usize)>,
pub frames: Vec<FlowFrame>,
pub reversals: Vec<usize>,
}
impl FlowField {
fn token_at(&self, byte: usize) -> Option<usize> {
if self.spans.is_empty() {
return None;
}
let i = self.spans.partition_point(|&(s, _)| s <= byte);
Some(i.saturating_sub(1))
}
#[must_use]
pub fn slope_at(&self, byte: usize) -> f32 {
self.token_at(byte)
.and_then(|i| self.frames.get(i))
.map_or(0.0, |f| f.slope)
}
#[must_use]
pub fn direction_at(&self, byte: usize) -> i8 {
self.token_at(byte)
.and_then(|i| self.frames.get(i))
.map_or(0, |f| f.direction)
}
#[must_use]
pub fn momentum_at(&self, byte: usize) -> u16 {
self.token_at(byte)
.and_then(|i| self.frames.get(i))
.map_or(0, |f| f.momentum)
}
}
#[must_use]
pub fn signal_values(tokens: &[Token], bytes: &[u8], signal: Signal) -> Vec<f32> {
match signal {
Signal::Magnitude => crate::magnitude::analyze(tokens, bytes)
.frames
.iter()
.map(|f| f.magnitude)
.collect(),
Signal::StressDepth => crate::stress::analyze(tokens, bytes)
.frames
.iter()
.map(|f| f32::from(f.depth))
.collect(),
Signal::Length => tokens
.iter()
.map(|t| (t.end - t.start) as f32)
.collect(),
}
}
#[must_use]
pub fn analyze(tokens: &[Token], bytes: &[u8], signal: Signal) -> FlowField {
let sig = signal_values(tokens, bytes, signal);
analyze_signal(tokens, &sig, &FlowConfig::default())
}
#[must_use]
pub fn analyze_bytes(bytes: &[u8], signal: Signal) -> FlowField {
let toks = crate::tokutil::lex_sig(bytes);
analyze(&toks, bytes, signal)
}
#[must_use]
pub fn analyze_signal(tokens: &[Token], signal: &[f32], cfg: &FlowConfig) -> FlowField {
let spans: Vec<(usize, usize)> = tokens.iter().map(|t| (t.start(), t.end())).collect();
analyze_spans(&spans, signal, cfg)
}
#[must_use]
pub fn analyze_spans(spans: &[(usize, usize)], signal: &[f32], cfg: &FlowConfig) -> FlowField {
let n = spans.len();
let mut field = FlowField {
n_tokens: n,
spans: Vec::with_capacity(n),
frames: Vec::with_capacity(n),
reversals: Vec::new(),
};
if n == 0 {
return field;
}
field.spans.extend_from_slice(spans);
let mut frames: Vec<FlowFrame> = Vec::with_capacity(n);
let mut momentum: u16 = 0;
let mut prev_dir: i8 = 0;
for i in 0..n.min(signal.len()) {
let lo = i.saturating_sub(cfg.window);
let span = (i - lo).max(1) as f32;
let slope = (signal[i] - signal[lo]) / span;
let direction = if slope > cfg.steady_band {
1
} else if slope < -cfg.steady_band {
-1
} else {
0
};
if direction != 0 && direction == prev_dir {
momentum = momentum.saturating_add(1);
} else {
momentum = u16::from(direction != 0);
}
if direction != 0 && prev_dir != 0 && direction != prev_dir {
field.reversals.push(spans[i].0);
}
if direction != 0 {
prev_dir = direction;
}
frames.push(FlowFrame {
slope,
direction,
momentum,
});
}
while frames.len() < n {
frames.push(FlowFrame::default());
}
field.frames = frames;
field
}
#[derive(Clone, Copy, Debug)]
pub struct AnalyticConfig {
pub taps: usize,
pub detrend: usize,
}
impl Default for AnalyticConfig {
fn default() -> Self {
Self { taps: 31, detrend: 16 }
}
}
#[derive(Clone, Debug, Default)]
pub struct AnalyticField {
pub amplitude: Vec<f32>,
pub phase: Vec<f32>,
pub frequency: Vec<f32>,
pub latency: usize,
}
impl AnalyticField {
#[must_use]
pub fn amplitude_at(&self, i: usize) -> f32 {
self.amplitude.get(i).copied().unwrap_or(0.0)
}
#[must_use]
pub fn frequency_at(&self, i: usize) -> f32 {
self.frequency.get(i).copied().unwrap_or(0.0)
}
}
fn hilbert_kernel(taps: usize) -> Vec<f32> {
let n = taps | 1;
let mid = (n / 2) as isize;
(0..n)
.map(|k| {
let d = k as isize - mid;
if d == 0 || d % 2 == 0 {
return 0.0;
}
let ideal = 2.0 / (std::f32::consts::PI * d as f32);
let w = 0.54
- 0.46
* (std::f32::consts::TAU * k as f32 / (n - 1) as f32).cos();
ideal * w
})
.collect()
}
#[must_use]
pub fn analytic_signal(signal: &[f32], cfg: &AnalyticConfig) -> AnalyticField {
let n = signal.len();
let taps = cfg.taps.max(3) | 1;
let latency = taps / 2;
let mut out = AnalyticField {
amplitude: vec![0.0; n],
phase: vec![0.0; n],
frequency: vec![0.0; n],
latency,
};
if n == 0 {
return out;
}
let win = cfg.detrend.max(1);
let detrended: Vec<f32> = {
let mut acc = 0.0f32;
signal
.iter()
.enumerate()
.map(|(i, &x)| {
acc += x;
if i >= win {
acc -= signal[i - win];
}
x - acc / (i + 1).min(win) as f32
})
.collect()
};
let h = hilbert_kernel(taps);
for i in 0..n {
let Some(at) = i.checked_sub(latency) else { continue };
let mut im = 0.0f32;
for (k, &hk) in h.iter().enumerate() {
if hk == 0.0 {
continue;
}
let Some(j) = i.checked_sub(k) else { break };
im += hk * detrended[j];
}
let re = detrended[at];
out.amplitude[at] = (re * re + im * im).sqrt();
out.phase[at] = im.atan2(re);
}
for i in 1..n {
let mut d = out.phase[i] - out.phase[i - 1];
while d > std::f32::consts::PI {
d -= std::f32::consts::TAU;
}
while d < -std::f32::consts::PI {
d += std::f32::consts::TAU;
}
out.frequency[i] = d;
}
out
}
#[must_use]
pub fn analytic(tokens: &[Token], bytes: &[u8], signal: Signal) -> AnalyticField {
let sig = signal_values(tokens, bytes, signal);
analytic_signal(&sig, &AnalyticConfig::default())
}
fn supertoken_signal(
toks: &[Token],
bytes: &[u8],
signal: Signal,
) -> (Vec<(usize, usize)>, Vec<f32>) {
let stress = crate::stress::analyze(toks, bytes);
let units = crate::supertoken::supertokens_from(toks, bytes);
let mut spans = Vec::with_capacity(units.len());
let mut values = Vec::with_capacity(units.len());
let mut cursor = 0usize;
for u in &units {
while cursor < toks.len() && toks[cursor].start() < u.start {
cursor += 1;
}
let lo = cursor;
let hi = lo + toks[lo..].partition_point(|t| t.end() <= u.end);
spans.push((u.start, u.end));
values.push(match signal {
Signal::Magnitude => {
if hi > lo {
let sum: f32 = toks[lo..hi]
.iter()
.map(|t| crate::magnitude::token_magnitude(t.kind, &bytes[t.span()]))
.sum();
sum / (hi - lo) as f32
} else {
0.0
}
}
Signal::StressDepth => {
stress.frames[lo..hi].iter().map(|f| f.depth).max().map_or(0.0, f32::from)
}
Signal::Length => (u.end - u.start) as f32,
});
}
(spans, values)
}
#[must_use]
pub fn analyze_supertokens(toks: &[Token], bytes: &[u8], signal: Signal) -> FlowField {
let (spans, values) = supertoken_signal(toks, bytes, signal);
analyze_spans(&spans, &values, &FlowConfig::default())
}
#[must_use]
pub fn analytic_supertokens(toks: &[Token], bytes: &[u8], signal: Signal) -> AnalyticField {
let (_, values) = supertoken_signal(toks, bytes, signal);
analytic_signal(&values, &AnalyticConfig::default())
}
#[cfg(test)]
mod grain_tests {
use super::*;
#[test]
fn the_supertoken_signal_reads_what_the_folded_profile_reads() {
use crate::profile::{AxisCtx, Profile, fold_tokens};
for src in [
&b"{module, [{a, [1, 2, {b, [3, 4]}]}, {c, [5, {d, [6, 7]}]}]}. {x, [{y, 8}]}."[..],
&b"alpha beta gamma delta"[..],
&b"f(x) g(yy) h(zzz) i(wwww)"[..],
] {
let toks = crate::lexer::lex(src);
let stress = crate::stress::analyze(&toks, src);
let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(src) };
let units = crate::supertoken::supertokens_from(&toks, src);
for signal in [Signal::Magnitude, Signal::StressDepth, Signal::Length] {
let (spans, values) = supertoken_signal(&toks, src, signal);
assert_eq!(spans.len(), units.len(), "one span per unit, {signal:?} {src:?}");
let mut cursor = 0usize;
for (k, u) in units.iter().enumerate() {
while cursor < toks.len() && toks[cursor].start() < u.start {
cursor += 1;
}
let lo = cursor;
let mut hi = cursor;
while hi < toks.len() && toks[hi].end() <= u.end {
hi += 1;
}
let p: Profile = fold_tokens(lo, &toks[lo..hi], &ctx);
let want = match signal {
Signal::Magnitude => p.magnitude.mean(),
Signal::StressDepth => f32::from(p.stress.max_depth),
Signal::Length => (u.end - u.start) as f32,
};
assert_eq!(
values[k].to_bits(),
want.to_bits(),
"unit {k} under {signal:?} reads {} against the fold's {want}, {src:?}",
values[k]
);
}
}
}
}
#[test]
fn the_supertoken_reading_is_not_the_token_reading_coarsened() {
let src = b"a = 1;\nb = 22;\nc = 333;\nd = 4444;\ne = 55555;\nf = 666666;\n";
let toks = crate::lexer::lex(src);
let by_token = analyze(&toks, src, Signal::Magnitude);
let by_unit = analyze_supertokens(&toks, src, Signal::Magnitude);
assert!(!by_unit.frames.is_empty(), "there are constructs to read");
assert!(by_unit.frames.len() < by_token.frames.len(), "fewer units than tokens");
let rising = by_unit.frames.iter().filter(|f| f.direction > 0).count();
let falling = by_unit.frames.iter().filter(|f| f.direction < 0).count();
assert!(rising > falling, "the construct sequence trends up: {rising} vs {falling}");
}
#[test]
fn the_analytic_reading_lifts_the_same_way() {
let src = b"a = 1;\nb = 22;\nc = 333;\nd = 4444;\ne = 55555;\nf = 666666;\n";
let toks = crate::lexer::lex(src);
let a = analytic_supertokens(&toks, src, Signal::Length);
assert!(!a.amplitude.is_empty(), "an envelope over constructs");
}
#[test]
fn spans_are_all_the_reading_needs_of_its_layer() {
let src = b"a 1 bb 22 ccc 333";
let toks = crate::tokutil::lex_sig(src);
let sig: Vec<f32> = toks.iter().map(|t| t.len() as f32).collect();
let spans: Vec<(usize, usize)> = toks.iter().map(|t| (t.start(), t.end())).collect();
let cfg = FlowConfig::default();
assert_eq!(
analyze_signal(&toks, &sig, &cfg).frames.len(),
analyze_spans(&spans, &sig, &cfg).frames.len()
);
}
}
#[cfg(test)]
mod analytic_tests {
use super::*;
fn cfg() -> AnalyticConfig {
AnalyticConfig::default()
}
fn mid_mean(v: &[f32], lat: usize) -> f32 {
let lo = lat * 2;
let hi = v.len().saturating_sub(lat);
if hi <= lo {
return 0.0;
}
v[lo..hi].iter().sum::<f32>() / (hi - lo) as f32
}
#[test]
fn a_constant_amplitude_sinusoid_reads_its_amplitude_and_frequency() {
let w = std::f32::consts::TAU / 8.0;
let s: Vec<f32> = (0..400).map(|i| 3.0 * (w * i as f32).sin()).collect();
let a = analytic_signal(&s, &cfg());
let env = mid_mean(&a.amplitude, a.latency);
assert!((env - 3.0).abs() < 0.3, "envelope reads the amplitude: {env}");
let f = mid_mean(&a.frequency, a.latency);
assert!((f - w).abs() < 0.05, "frequency reads the rate: {f} vs {w}");
}
#[test]
fn the_envelope_rises_where_the_slope_only_keeps_changing_sign() {
let w = std::f32::consts::TAU / 8.0;
let s: Vec<f32> =
(0..400).map(|i| (0.5 + i as f32 * 0.02) * (w * i as f32).sin()).collect();
let a = analytic_signal(&s, &cfg());
let lat = a.latency;
let early = a.amplitude[lat * 2..100].iter().sum::<f32>() / (100 - lat * 2) as f32;
let late = a.amplitude[300..380].iter().sum::<f32>() / 80.0;
assert!(late > early * 3.0, "the envelope tracks the growth: {early} -> {late}");
let env_flips = (lat * 2 + 1..380)
.filter(|&i| {
(a.amplitude[i] - a.amplitude[i - 1]) * (a.amplitude[i - 1] - a.amplitude[i - 2])
< 0.0
})
.count();
let src: Vec<u8> = std::iter::repeat_n(b"a ", s.len()).flatten().copied().collect();
let toks = crate::tokutil::lex_sig(&src);
assert_eq!(toks.len(), s.len(), "one token per sample");
let f = analyze_signal(&toks, &s, &FlowConfig::default());
let slope_flips = (lat * 2 + 1..380)
.filter(|&i| f.frames[i].slope * f.frames[i - 1].slope < 0.0)
.count();
assert!(
slope_flips > env_flips * 4,
"the slope keeps reversing where the envelope does not: {slope_flips} vs {env_flips}"
);
}
#[test]
fn a_chirp_is_read_as_a_rising_frequency() {
let s: Vec<f32> = (0..600)
.map(|i| {
let t = i as f32;
let phase = 0.15 * t + 0.0004 * t * t;
phase.sin()
})
.collect();
let a = analytic_signal(&s, &cfg());
let lat = a.latency;
let early = a.frequency[lat * 2..200].iter().sum::<f32>() / (200 - lat * 2) as f32;
let late = a.frequency[400..560].iter().sum::<f32>() / 160.0;
assert!(late > early + 0.05, "the frequency rises with the chirp: {early} -> {late}");
}
#[test]
fn the_reading_is_causal_with_latency() {
let w = std::f32::consts::TAU / 7.0;
let long: Vec<f32> = (0..300).map(|i| (w * i as f32).sin()).collect();
let short = &long[..200];
let a = analytic_signal(&long, &cfg());
let b = analytic_signal(short, &cfg());
let lat = a.latency;
for i in lat..(200 - lat) {
assert!(
(a.amplitude[i] - b.amplitude[i]).abs() < 1e-4,
"index {i} moved when later input arrived: {} vs {}",
b.amplitude[i],
a.amplitude[i]
);
}
}
#[test]
fn a_constant_signal_has_no_envelope() {
let s = vec![7.5f32; 200];
let a = analytic_signal(&s, &cfg());
assert!(mid_mean(&a.amplitude, a.latency) < 0.05, "a flat signal does not oscillate");
}
#[test]
fn degenerate_inputs_are_safe() {
assert!(analytic_signal(&[], &cfg()).amplitude.is_empty());
let one = analytic_signal(&[1.0], &cfg());
assert_eq!(one.amplitude.len(), 1);
assert_eq!(one.frequency_at(99), 0.0);
assert_eq!(one.amplitude_at(99), 0.0);
}
#[test]
fn it_composes_over_a_named_axis_the_way_flow_does() {
let src = b"a 1 bb 22 ccc 333 dddd 4444 eeeee 55555 f 6 gg 77 hhh 888";
let toks = crate::tokutil::lex_sig(src);
let a = analytic(&toks, src, Signal::Length);
assert_eq!(a.amplitude.len(), toks.len());
assert!(a.amplitude.iter().any(|&x| x > 0.0), "the length signal swings");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ramp_flows_upward_with_momentum() {
let f = analyze_bytes(b"1 10 100 1000 10000 100000", Signal::Magnitude);
let last = f.frames.last().copied().unwrap_or_default();
assert_eq!(last.direction, 1, "a rising ramp flows upward");
assert!(last.momentum >= 3, "sustained trend builds momentum, got {}", last.momentum);
assert!(f.reversals.is_empty(), "a monotone ramp has no reversal");
}
#[test]
fn valley_has_a_reversal_at_the_bottom() {
let f = analyze_bytes(b"100000 1000 10 1 10 1000 100000", Signal::Magnitude);
assert!(
!f.reversals.is_empty(),
"a fall-then-rise should register a reversal"
);
}
#[test]
fn flow_composes_over_stress_depth() {
let f = analyze_bytes(b"a(b(c(d(e))))", Signal::StressDepth);
assert!(f.frames.iter().any(|fr| fr.direction == 1), "deepening rises");
assert!(
f.frames.iter().any(|fr| fr.direction == -1),
"the release falls"
);
assert!(!f.reversals.is_empty(), "deepen-then-release reverses");
}
#[test]
fn flat_signal_is_steady() {
let f = analyze_bytes(b"a b c d e f g", Signal::Length);
assert!(f.frames.iter().all(|fr| fr.direction == 0), "equal lengths are steady");
assert!(f.reversals.is_empty());
}
#[test]
fn empty_is_safe() {
let f = analyze_bytes(b"", Signal::Magnitude);
assert_eq!(f.n_tokens, 0);
assert!(f.frames.is_empty());
assert!(f.reversals.is_empty());
assert_eq!(f.direction_at(0), 0);
}
}