1use crate::token::Token;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum Signal {
48 Magnitude,
50 StressDepth,
52 Length,
54}
55
56impl Signal {
57 #[must_use]
59 pub fn parse(name: &str) -> Option<Signal> {
60 match name {
61 "magnitude" => Some(Signal::Magnitude),
62 "stress" => Some(Signal::StressDepth),
63 "length" => Some(Signal::Length),
64 _ => None,
65 }
66 }
67
68 #[must_use]
70 pub fn label(self) -> &'static str {
71 match self {
72 Signal::Magnitude => "magnitude",
73 Signal::StressDepth => "stress",
74 Signal::Length => "length",
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug)]
81pub struct FlowConfig {
82 pub window: usize,
84 pub steady_band: f32,
86}
87
88impl Default for FlowConfig {
89 fn default() -> Self {
90 Self {
91 window: 4,
92 steady_band: 0.05,
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, Default)]
99pub struct FlowFrame {
100 pub slope: f32,
102 pub direction: i8,
104 pub momentum: u16,
107}
108
109#[derive(Clone, Debug, Default)]
111pub struct FlowField {
112 pub n_tokens: usize,
114 pub spans: Vec<(usize, usize)>,
116 pub frames: Vec<FlowFrame>,
118 pub reversals: Vec<usize>,
120}
121
122impl FlowField {
123 fn token_at(&self, byte: usize) -> Option<usize> {
125 if self.spans.is_empty() {
126 return None;
127 }
128 let i = self.spans.partition_point(|&(s, _)| s <= byte);
129 Some(i.saturating_sub(1))
130 }
131
132 #[must_use]
134 pub fn slope_at(&self, byte: usize) -> f32 {
135 self.token_at(byte)
136 .and_then(|i| self.frames.get(i))
137 .map_or(0.0, |f| f.slope)
138 }
139
140 #[must_use]
142 pub fn direction_at(&self, byte: usize) -> i8 {
143 self.token_at(byte)
144 .and_then(|i| self.frames.get(i))
145 .map_or(0, |f| f.direction)
146 }
147
148 #[must_use]
152 pub fn momentum_at(&self, byte: usize) -> u16 {
153 self.token_at(byte)
154 .and_then(|i| self.frames.get(i))
155 .map_or(0, |f| f.momentum)
156 }
157}
158
159#[must_use]
161pub fn signal_values(tokens: &[Token], bytes: &[u8], signal: Signal) -> Vec<f32> {
162 match signal {
163 Signal::Magnitude => crate::magnitude::analyze(tokens, bytes)
164 .frames
165 .iter()
166 .map(|f| f.magnitude)
167 .collect(),
168 Signal::StressDepth => crate::stress::analyze(tokens, bytes)
169 .frames
170 .iter()
171 .map(|f| f32::from(f.depth))
172 .collect(),
173 Signal::Length => tokens
174 .iter()
175 .map(|t| (t.end - t.start) as f32)
176 .collect(),
177 }
178}
179
180#[must_use]
182pub fn analyze(tokens: &[Token], bytes: &[u8], signal: Signal) -> FlowField {
183 let sig = signal_values(tokens, bytes, signal);
184 analyze_signal(tokens, &sig, &FlowConfig::default())
185}
186
187#[must_use]
189pub fn analyze_bytes(bytes: &[u8], signal: Signal) -> FlowField {
190 let toks = crate::tokutil::lex_sig(bytes);
191 analyze(&toks, bytes, signal)
192}
193
194#[must_use]
198pub fn analyze_signal(tokens: &[Token], signal: &[f32], cfg: &FlowConfig) -> FlowField {
199 let spans: Vec<(usize, usize)> = tokens.iter().map(|t| (t.start(), t.end())).collect();
200 analyze_spans(&spans, signal, cfg)
201}
202
203#[must_use]
211pub fn analyze_spans(spans: &[(usize, usize)], signal: &[f32], cfg: &FlowConfig) -> FlowField {
212 let n = spans.len();
213 let mut field = FlowField {
214 n_tokens: n,
215 spans: Vec::with_capacity(n),
216 frames: Vec::with_capacity(n),
217 reversals: Vec::new(),
218 };
219 if n == 0 {
220 return field;
221 }
222 field.spans.extend_from_slice(spans);
223
224 let mut frames: Vec<FlowFrame> = Vec::with_capacity(n);
225 let mut momentum: u16 = 0;
226 let mut prev_dir: i8 = 0;
227 for i in 0..n.min(signal.len()) {
228 let lo = i.saturating_sub(cfg.window);
230 let span = (i - lo).max(1) as f32;
231 let slope = (signal[i] - signal[lo]) / span;
232
233 let direction = if slope > cfg.steady_band {
234 1
235 } else if slope < -cfg.steady_band {
236 -1
237 } else {
238 0
239 };
240
241 if direction != 0 && direction == prev_dir {
243 momentum = momentum.saturating_add(1);
244 } else {
245 momentum = u16::from(direction != 0);
246 }
247
248 if direction != 0 && prev_dir != 0 && direction != prev_dir {
250 field.reversals.push(spans[i].0);
251 }
252 if direction != 0 {
253 prev_dir = direction;
254 }
255
256 frames.push(FlowFrame {
257 slope,
258 direction,
259 momentum,
260 });
261 }
262 while frames.len() < n {
264 frames.push(FlowFrame::default());
265 }
266 field.frames = frames;
267 field
268}
269
270#[derive(Clone, Copy, Debug)]
272pub struct AnalyticConfig {
273 pub taps: usize,
275 pub detrend: usize,
277}
278
279impl Default for AnalyticConfig {
280 fn default() -> Self {
281 Self { taps: 31, detrend: 16 }
282 }
283}
284
285#[derive(Clone, Debug, Default)]
293pub struct AnalyticField {
294 pub amplitude: Vec<f32>,
296 pub phase: Vec<f32>,
298 pub frequency: Vec<f32>,
304 pub latency: usize,
308}
309
310impl AnalyticField {
311 #[must_use]
313 pub fn amplitude_at(&self, i: usize) -> f32 {
314 self.amplitude.get(i).copied().unwrap_or(0.0)
315 }
316
317 #[must_use]
319 pub fn frequency_at(&self, i: usize) -> f32 {
320 self.frequency.get(i).copied().unwrap_or(0.0)
321 }
322}
323
324fn hilbert_kernel(taps: usize) -> Vec<f32> {
333 let n = taps | 1;
334 let mid = (n / 2) as isize;
335 (0..n)
336 .map(|k| {
337 let d = k as isize - mid;
338 if d == 0 || d % 2 == 0 {
339 return 0.0;
340 }
341 let ideal = 2.0 / (std::f32::consts::PI * d as f32);
342 let w = 0.54
343 - 0.46
344 * (std::f32::consts::TAU * k as f32 / (n - 1) as f32).cos();
345 ideal * w
346 })
347 .collect()
348}
349
350#[must_use]
367pub fn analytic_signal(signal: &[f32], cfg: &AnalyticConfig) -> AnalyticField {
368 let n = signal.len();
369 let taps = cfg.taps.max(3) | 1;
370 let latency = taps / 2;
371 let mut out = AnalyticField {
372 amplitude: vec![0.0; n],
373 phase: vec![0.0; n],
374 frequency: vec![0.0; n],
375 latency,
376 };
377 if n == 0 {
378 return out;
379 }
380
381 let win = cfg.detrend.max(1);
382 let detrended: Vec<f32> = {
383 let mut acc = 0.0f32;
384 signal
385 .iter()
386 .enumerate()
387 .map(|(i, &x)| {
388 acc += x;
389 if i >= win {
390 acc -= signal[i - win];
391 }
392 x - acc / (i + 1).min(win) as f32
393 })
394 .collect()
395 };
396
397 let h = hilbert_kernel(taps);
398 for i in 0..n {
399 let Some(at) = i.checked_sub(latency) else { continue };
402 let mut im = 0.0f32;
403 for (k, &hk) in h.iter().enumerate() {
404 if hk == 0.0 {
405 continue;
406 }
407 let Some(j) = i.checked_sub(k) else { break };
408 im += hk * detrended[j];
409 }
410 let re = detrended[at];
411 out.amplitude[at] = (re * re + im * im).sqrt();
412 out.phase[at] = im.atan2(re);
413 }
414
415 for i in 1..n {
416 let mut d = out.phase[i] - out.phase[i - 1];
417 while d > std::f32::consts::PI {
418 d -= std::f32::consts::TAU;
419 }
420 while d < -std::f32::consts::PI {
421 d += std::f32::consts::TAU;
422 }
423 out.frequency[i] = d;
424 }
425 out
426}
427
428#[must_use]
430pub fn analytic(tokens: &[Token], bytes: &[u8], signal: Signal) -> AnalyticField {
431 let sig = signal_values(tokens, bytes, signal);
432 analytic_signal(&sig, &AnalyticConfig::default())
433}
434
435fn supertoken_signal(
443 toks: &[Token],
444 bytes: &[u8],
445 signal: Signal,
446) -> (Vec<(usize, usize)>, Vec<f32>) {
447 let stress = crate::stress::analyze(toks, bytes);
448 let units = crate::supertoken::supertokens_from(toks, bytes);
449
450 let mut spans = Vec::with_capacity(units.len());
451 let mut values = Vec::with_capacity(units.len());
452 let mut cursor = 0usize;
453 for u in &units {
454 while cursor < toks.len() && toks[cursor].start() < u.start {
459 cursor += 1;
460 }
461 let lo = cursor;
462 let hi = lo + toks[lo..].partition_point(|t| t.end() <= u.end);
463 spans.push((u.start, u.end));
464 values.push(match signal {
476 Signal::Magnitude => {
477 if hi > lo {
478 let sum: f32 = toks[lo..hi]
479 .iter()
480 .map(|t| crate::magnitude::token_magnitude(t.kind, &bytes[t.span()]))
481 .sum();
482 sum / (hi - lo) as f32
483 } else {
484 0.0
485 }
486 }
487 Signal::StressDepth => {
488 stress.frames[lo..hi].iter().map(|f| f.depth).max().map_or(0.0, f32::from)
489 }
490 Signal::Length => (u.end - u.start) as f32,
491 });
492 }
493 (spans, values)
494}
495
496#[must_use]
502pub fn analyze_supertokens(toks: &[Token], bytes: &[u8], signal: Signal) -> FlowField {
503 let (spans, values) = supertoken_signal(toks, bytes, signal);
504 analyze_spans(&spans, &values, &FlowConfig::default())
505}
506
507#[must_use]
509pub fn analytic_supertokens(toks: &[Token], bytes: &[u8], signal: Signal) -> AnalyticField {
510 let (_, values) = supertoken_signal(toks, bytes, signal);
511 analytic_signal(&values, &AnalyticConfig::default())
512}
513
514#[cfg(test)]
515mod grain_tests {
516 use super::*;
517
518 #[test]
519 fn the_supertoken_signal_reads_what_the_folded_profile_reads() {
520 use crate::profile::{AxisCtx, Profile, fold_tokens};
525 for src in [
526 &b"{module, [{a, [1, 2, {b, [3, 4]}]}, {c, [5, {d, [6, 7]}]}]}. {x, [{y, 8}]}."[..],
527 &b"alpha beta gamma delta"[..],
528 &b"f(x) g(yy) h(zzz) i(wwww)"[..],
529 ] {
530 let toks = crate::lexer::lex(src);
531 let stress = crate::stress::analyze(&toks, src);
532 let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(src) };
533 let units = crate::supertoken::supertokens_from(&toks, src);
534
535 for signal in [Signal::Magnitude, Signal::StressDepth, Signal::Length] {
536 let (spans, values) = supertoken_signal(&toks, src, signal);
537 assert_eq!(spans.len(), units.len(), "one span per unit, {signal:?} {src:?}");
538
539 let mut cursor = 0usize;
540 for (k, u) in units.iter().enumerate() {
541 while cursor < toks.len() && toks[cursor].start() < u.start {
542 cursor += 1;
543 }
544 let lo = cursor;
545 let mut hi = cursor;
546 while hi < toks.len() && toks[hi].end() <= u.end {
547 hi += 1;
548 }
549 let p: Profile = fold_tokens(lo, &toks[lo..hi], &ctx);
550 let want = match signal {
551 Signal::Magnitude => p.magnitude.mean(),
552 Signal::StressDepth => f32::from(p.stress.max_depth),
553 Signal::Length => (u.end - u.start) as f32,
554 };
555 assert_eq!(
556 values[k].to_bits(),
557 want.to_bits(),
558 "unit {k} under {signal:?} reads {} against the fold's {want}, {src:?}",
559 values[k]
560 );
561 }
562 }
563 }
564 }
565
566 #[test]
567 fn the_supertoken_reading_is_not_the_token_reading_coarsened() {
568 let src = b"a = 1;\nb = 22;\nc = 333;\nd = 4444;\ne = 55555;\nf = 666666;\n";
573 let toks = crate::lexer::lex(src);
574 let by_token = analyze(&toks, src, Signal::Magnitude);
575 let by_unit = analyze_supertokens(&toks, src, Signal::Magnitude);
576 assert!(!by_unit.frames.is_empty(), "there are constructs to read");
577 assert!(by_unit.frames.len() < by_token.frames.len(), "fewer units than tokens");
578
579 let rising = by_unit.frames.iter().filter(|f| f.direction > 0).count();
582 let falling = by_unit.frames.iter().filter(|f| f.direction < 0).count();
583 assert!(rising > falling, "the construct sequence trends up: {rising} vs {falling}");
584 }
585
586 #[test]
587 fn the_analytic_reading_lifts_the_same_way() {
588 let src = b"a = 1;\nb = 22;\nc = 333;\nd = 4444;\ne = 55555;\nf = 666666;\n";
589 let toks = crate::lexer::lex(src);
590 let a = analytic_supertokens(&toks, src, Signal::Length);
591 assert!(!a.amplitude.is_empty(), "an envelope over constructs");
592 }
593
594 #[test]
595 fn spans_are_all_the_reading_needs_of_its_layer() {
596 let src = b"a 1 bb 22 ccc 333";
599 let toks = crate::tokutil::lex_sig(src);
600 let sig: Vec<f32> = toks.iter().map(|t| t.len() as f32).collect();
601 let spans: Vec<(usize, usize)> = toks.iter().map(|t| (t.start(), t.end())).collect();
602 let cfg = FlowConfig::default();
603 assert_eq!(
604 analyze_signal(&toks, &sig, &cfg).frames.len(),
605 analyze_spans(&spans, &sig, &cfg).frames.len()
606 );
607 }
608}
609
610#[cfg(test)]
611mod analytic_tests {
612 use super::*;
613
614 fn cfg() -> AnalyticConfig {
615 AnalyticConfig::default()
616 }
617
618 fn mid_mean(v: &[f32], lat: usize) -> f32 {
620 let lo = lat * 2;
621 let hi = v.len().saturating_sub(lat);
622 if hi <= lo {
623 return 0.0;
624 }
625 v[lo..hi].iter().sum::<f32>() / (hi - lo) as f32
626 }
627
628 #[test]
629 fn a_constant_amplitude_sinusoid_reads_its_amplitude_and_frequency() {
630 let w = std::f32::consts::TAU / 8.0;
633 let s: Vec<f32> = (0..400).map(|i| 3.0 * (w * i as f32).sin()).collect();
634 let a = analytic_signal(&s, &cfg());
635 let env = mid_mean(&a.amplitude, a.latency);
636 assert!((env - 3.0).abs() < 0.3, "envelope reads the amplitude: {env}");
637 let f = mid_mean(&a.frequency, a.latency);
638 assert!((f - w).abs() < 0.05, "frequency reads the rate: {f} vs {w}");
639 }
640
641 #[test]
642 fn the_envelope_rises_where_the_slope_only_keeps_changing_sign() {
643 let w = std::f32::consts::TAU / 8.0;
649 let s: Vec<f32> =
650 (0..400).map(|i| (0.5 + i as f32 * 0.02) * (w * i as f32).sin()).collect();
651 let a = analytic_signal(&s, &cfg());
652 let lat = a.latency;
653 let early = a.amplitude[lat * 2..100].iter().sum::<f32>() / (100 - lat * 2) as f32;
654 let late = a.amplitude[300..380].iter().sum::<f32>() / 80.0;
655 assert!(late > early * 3.0, "the envelope tracks the growth: {early} -> {late}");
656
657 let env_flips = (lat * 2 + 1..380)
660 .filter(|&i| {
661 (a.amplitude[i] - a.amplitude[i - 1]) * (a.amplitude[i - 1] - a.amplitude[i - 2])
662 < 0.0
663 })
664 .count();
665
666 let src: Vec<u8> = std::iter::repeat_n(b"a ", s.len()).flatten().copied().collect();
673 let toks = crate::tokutil::lex_sig(&src);
674 assert_eq!(toks.len(), s.len(), "one token per sample");
675 let f = analyze_signal(&toks, &s, &FlowConfig::default());
676 let slope_flips = (lat * 2 + 1..380)
677 .filter(|&i| f.frames[i].slope * f.frames[i - 1].slope < 0.0)
678 .count();
679
680 assert!(
681 slope_flips > env_flips * 4,
682 "the slope keeps reversing where the envelope does not: {slope_flips} vs {env_flips}"
683 );
684 }
685
686 #[test]
687 fn a_chirp_is_read_as_a_rising_frequency() {
688 let s: Vec<f32> = (0..600)
693 .map(|i| {
694 let t = i as f32;
695 let phase = 0.15 * t + 0.0004 * t * t;
696 phase.sin()
697 })
698 .collect();
699 let a = analytic_signal(&s, &cfg());
700 let lat = a.latency;
701 let early = a.frequency[lat * 2..200].iter().sum::<f32>() / (200 - lat * 2) as f32;
702 let late = a.frequency[400..560].iter().sum::<f32>() / 160.0;
703 assert!(late > early + 0.05, "the frequency rises with the chirp: {early} -> {late}");
704 }
705
706 #[test]
707 fn the_reading_is_causal_with_latency() {
708 let w = std::f32::consts::TAU / 7.0;
712 let long: Vec<f32> = (0..300).map(|i| (w * i as f32).sin()).collect();
713 let short = &long[..200];
714 let a = analytic_signal(&long, &cfg());
715 let b = analytic_signal(short, &cfg());
716 let lat = a.latency;
717 for i in lat..(200 - lat) {
719 assert!(
720 (a.amplitude[i] - b.amplitude[i]).abs() < 1e-4,
721 "index {i} moved when later input arrived: {} vs {}",
722 b.amplitude[i],
723 a.amplitude[i]
724 );
725 }
726 }
727
728 #[test]
729 fn a_constant_signal_has_no_envelope() {
730 let s = vec![7.5f32; 200];
733 let a = analytic_signal(&s, &cfg());
734 assert!(mid_mean(&a.amplitude, a.latency) < 0.05, "a flat signal does not oscillate");
735 }
736
737 #[test]
738 fn degenerate_inputs_are_safe() {
739 assert!(analytic_signal(&[], &cfg()).amplitude.is_empty());
740 let one = analytic_signal(&[1.0], &cfg());
741 assert_eq!(one.amplitude.len(), 1);
742 assert_eq!(one.frequency_at(99), 0.0);
743 assert_eq!(one.amplitude_at(99), 0.0);
744 }
745
746 #[test]
747 fn it_composes_over_a_named_axis_the_way_flow_does() {
748 let src = b"a 1 bb 22 ccc 333 dddd 4444 eeeee 55555 f 6 gg 77 hhh 888";
749 let toks = crate::tokutil::lex_sig(src);
750 let a = analytic(&toks, src, Signal::Length);
751 assert_eq!(a.amplitude.len(), toks.len());
752 assert!(a.amplitude.iter().any(|&x| x > 0.0), "the length signal swings");
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn ramp_flows_upward_with_momentum() {
762 let f = analyze_bytes(b"1 10 100 1000 10000 100000", Signal::Magnitude);
764 let last = f.frames.last().copied().unwrap_or_default();
765 assert_eq!(last.direction, 1, "a rising ramp flows upward");
766 assert!(last.momentum >= 3, "sustained trend builds momentum, got {}", last.momentum);
767 assert!(f.reversals.is_empty(), "a monotone ramp has no reversal");
768 }
769
770 #[test]
771 fn valley_has_a_reversal_at_the_bottom() {
772 let f = analyze_bytes(b"100000 1000 10 1 10 1000 100000", Signal::Magnitude);
774 assert!(
775 !f.reversals.is_empty(),
776 "a fall-then-rise should register a reversal"
777 );
778 }
779
780 #[test]
781 fn flow_composes_over_stress_depth() {
782 let f = analyze_bytes(b"a(b(c(d(e))))", Signal::StressDepth);
784 assert!(f.frames.iter().any(|fr| fr.direction == 1), "deepening rises");
785 assert!(
786 f.frames.iter().any(|fr| fr.direction == -1),
787 "the release falls"
788 );
789 assert!(!f.reversals.is_empty(), "deepen-then-release reverses");
790 }
791
792 #[test]
793 fn flat_signal_is_steady() {
794 let f = analyze_bytes(b"a b c d e f g", Signal::Length);
795 assert!(f.frames.iter().all(|fr| fr.direction == 0), "equal lengths are steady");
796 assert!(f.reversals.is_empty());
797 }
798
799 #[test]
800 fn empty_is_safe() {
801 let f = analyze_bytes(b"", Signal::Magnitude);
802 assert_eq!(f.n_tokens, 0);
803 assert!(f.frames.is_empty());
804 assert!(f.reversals.is_empty());
805 assert_eq!(f.direction_at(0), 0);
806 }
807}