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 analyze_with(tokens, bytes, signal, &FlowConfig::default())
184}
185
186#[must_use]
189pub fn analyze_with(tokens: &[Token], bytes: &[u8], signal: Signal, cfg: &FlowConfig) -> FlowField {
190 let sig = signal_values(tokens, bytes, signal);
191 analyze_signal(tokens, &sig, cfg)
192}
193
194#[must_use]
196pub fn analyze_bytes(bytes: &[u8], signal: Signal) -> FlowField {
197 let toks = crate::tokutil::lex_sig(bytes);
198 analyze(&toks, bytes, signal)
199}
200
201#[must_use]
205pub fn analyze_signal(tokens: &[Token], signal: &[f32], cfg: &FlowConfig) -> FlowField {
206 let spans: Vec<(usize, usize)> = tokens.iter().map(|t| (t.start(), t.end())).collect();
207 analyze_spans(&spans, signal, cfg)
208}
209
210#[must_use]
218pub fn analyze_spans(spans: &[(usize, usize)], signal: &[f32], cfg: &FlowConfig) -> FlowField {
219 let n = spans.len();
220 let mut field = FlowField {
221 n_tokens: n,
222 spans: Vec::with_capacity(n),
223 frames: Vec::with_capacity(n),
224 reversals: Vec::new(),
225 };
226 if n == 0 {
227 return field;
228 }
229 field.spans.extend_from_slice(spans);
230
231 let mut frames: Vec<FlowFrame> = Vec::with_capacity(n);
232 let mut momentum: u16 = 0;
233 let mut prev_dir: i8 = 0;
234 for i in 0..n.min(signal.len()) {
235 let lo = i.saturating_sub(cfg.window);
237 let span = (i - lo).max(1) as f32;
238 let slope = (signal[i] - signal[lo]) / span;
239
240 let direction = if slope > cfg.steady_band {
241 1
242 } else if slope < -cfg.steady_band {
243 -1
244 } else {
245 0
246 };
247
248 if direction != 0 && direction == prev_dir {
250 momentum = momentum.saturating_add(1);
251 } else {
252 momentum = u16::from(direction != 0);
253 }
254
255 if direction != 0 && prev_dir != 0 && direction != prev_dir {
257 field.reversals.push(spans[i].0);
258 }
259 if direction != 0 {
260 prev_dir = direction;
261 }
262
263 frames.push(FlowFrame {
264 slope,
265 direction,
266 momentum,
267 });
268 }
269 while frames.len() < n {
271 frames.push(FlowFrame::default());
272 }
273 field.frames = frames;
274 field
275}
276
277#[derive(Clone, Copy, Debug)]
279pub struct AnalyticConfig {
280 pub taps: usize,
282 pub detrend: usize,
284}
285
286impl Default for AnalyticConfig {
287 fn default() -> Self {
288 Self { taps: 31, detrend: 16 }
289 }
290}
291
292#[derive(Clone, Debug, Default)]
300pub struct AnalyticField {
301 pub amplitude: Vec<f32>,
303 pub phase: Vec<f32>,
305 pub frequency: Vec<f32>,
311 pub latency: usize,
315}
316
317impl AnalyticField {
318 #[must_use]
320 pub fn amplitude_at(&self, i: usize) -> f32 {
321 self.amplitude.get(i).copied().unwrap_or(0.0)
322 }
323
324 #[must_use]
326 pub fn frequency_at(&self, i: usize) -> f32 {
327 self.frequency.get(i).copied().unwrap_or(0.0)
328 }
329}
330
331fn hilbert_kernel(taps: usize) -> Vec<f32> {
340 let n = taps | 1;
341 let mid = (n / 2) as isize;
342 (0..n)
343 .map(|k| {
344 let d = k as isize - mid;
345 if d == 0 || d % 2 == 0 {
346 return 0.0;
347 }
348 let ideal = 2.0 / (std::f32::consts::PI * d as f32);
349 let w = 0.54
350 - 0.46
351 * (std::f32::consts::TAU * k as f32 / (n - 1) as f32).cos();
352 ideal * w
353 })
354 .collect()
355}
356
357#[must_use]
374pub fn analytic_signal(signal: &[f32], cfg: &AnalyticConfig) -> AnalyticField {
375 let n = signal.len();
376 let taps = cfg.taps.max(3) | 1;
377 let latency = taps / 2;
378 let mut out = AnalyticField {
379 amplitude: vec![0.0; n],
380 phase: vec![0.0; n],
381 frequency: vec![0.0; n],
382 latency,
383 };
384 if n == 0 {
385 return out;
386 }
387
388 let win = cfg.detrend.max(1);
389 let detrended: Vec<f32> = {
390 let mut acc = 0.0f32;
391 signal
392 .iter()
393 .enumerate()
394 .map(|(i, &x)| {
395 acc += x;
396 if i >= win {
397 acc -= signal[i - win];
398 }
399 x - acc / (i + 1).min(win) as f32
400 })
401 .collect()
402 };
403
404 let h = hilbert_kernel(taps);
405 for i in 0..n {
406 let Some(at) = i.checked_sub(latency) else { continue };
409 let mut im = 0.0f32;
410 for (k, &hk) in h.iter().enumerate() {
411 if hk == 0.0 {
412 continue;
413 }
414 let Some(j) = i.checked_sub(k) else { break };
415 im += hk * detrended[j];
416 }
417 let re = detrended[at];
418 out.amplitude[at] = (re * re + im * im).sqrt();
419 out.phase[at] = im.atan2(re);
420 }
421
422 for i in 1..n {
423 let mut d = out.phase[i] - out.phase[i - 1];
424 while d > std::f32::consts::PI {
425 d -= std::f32::consts::TAU;
426 }
427 while d < -std::f32::consts::PI {
428 d += std::f32::consts::TAU;
429 }
430 out.frequency[i] = d;
431 }
432 out
433}
434
435#[must_use]
437pub fn analytic(tokens: &[Token], bytes: &[u8], signal: Signal) -> AnalyticField {
438 let sig = signal_values(tokens, bytes, signal);
439 analytic_signal(&sig, &AnalyticConfig::default())
440}
441
442fn supertoken_signal(
450 toks: &[Token],
451 bytes: &[u8],
452 signal: Signal,
453) -> (Vec<(usize, usize)>, Vec<f32>) {
454 let stress = crate::stress::analyze(toks, bytes);
455 let units = crate::supertoken::supertokens_from(toks, bytes);
456
457 let mut spans = Vec::with_capacity(units.len());
458 let mut values = Vec::with_capacity(units.len());
459 let mut cursor = 0usize;
460 for u in &units {
461 while cursor < toks.len() && toks[cursor].start() < u.start {
466 cursor += 1;
467 }
468 let lo = cursor;
469 let hi = lo + toks[lo..].partition_point(|t| t.end() <= u.end);
470 spans.push((u.start, u.end));
471 values.push(match signal {
483 Signal::Magnitude => {
484 if hi > lo {
485 let sum: f32 = toks[lo..hi]
486 .iter()
487 .map(|t| crate::magnitude::token_magnitude(t.kind, &bytes[t.span()]))
488 .sum();
489 sum / (hi - lo) as f32
490 } else {
491 0.0
492 }
493 }
494 Signal::StressDepth => {
495 stress.frames[lo..hi].iter().map(|f| f.depth).max().map_or(0.0, f32::from)
496 }
497 Signal::Length => (u.end - u.start) as f32,
498 });
499 }
500 (spans, values)
501}
502
503#[must_use]
509pub fn analyze_supertokens(toks: &[Token], bytes: &[u8], signal: Signal) -> FlowField {
510 analyze_supertokens_with(toks, bytes, signal, &FlowConfig::default())
511}
512
513#[must_use]
516pub fn analyze_supertokens_with(toks: &[Token], bytes: &[u8], signal: Signal, cfg: &FlowConfig) -> FlowField {
517 let (spans, values) = supertoken_signal(toks, bytes, signal);
518 analyze_spans(&spans, &values, cfg)
519}
520
521#[must_use]
523pub fn analytic_supertokens(toks: &[Token], bytes: &[u8], signal: Signal) -> AnalyticField {
524 let (_, values) = supertoken_signal(toks, bytes, signal);
525 analytic_signal(&values, &AnalyticConfig::default())
526}
527
528#[cfg(test)]
529mod grain_tests {
530 use super::*;
531
532 #[test]
533 fn the_supertoken_signal_reads_what_the_folded_profile_reads() {
534 use crate::profile::{AxisCtx, Profile, fold_tokens};
539 for src in [
540 &b"{module, [{a, [1, 2, {b, [3, 4]}]}, {c, [5, {d, [6, 7]}]}]}. {x, [{y, 8}]}."[..],
541 &b"alpha beta gamma delta"[..],
542 &b"f(x) g(yy) h(zzz) i(wwww)"[..],
543 ] {
544 let toks = crate::lexer::lex(src);
545 let stress = crate::stress::analyze(&toks, src);
546 let ctx = AxisCtx { stress: Some(&stress), ..AxisCtx::new(src) };
547 let units = crate::supertoken::supertokens_from(&toks, src);
548
549 for signal in [Signal::Magnitude, Signal::StressDepth, Signal::Length] {
550 let (spans, values) = supertoken_signal(&toks, src, signal);
551 assert_eq!(spans.len(), units.len(), "one span per unit, {signal:?} {src:?}");
552
553 let mut cursor = 0usize;
554 for (k, u) in units.iter().enumerate() {
555 while cursor < toks.len() && toks[cursor].start() < u.start {
556 cursor += 1;
557 }
558 let lo = cursor;
559 let mut hi = cursor;
560 while hi < toks.len() && toks[hi].end() <= u.end {
561 hi += 1;
562 }
563 let p: Profile = fold_tokens(lo, &toks[lo..hi], &ctx);
564 let want = match signal {
565 Signal::Magnitude => p.magnitude.mean(),
566 Signal::StressDepth => f32::from(p.stress.max_depth),
567 Signal::Length => (u.end - u.start) as f32,
568 };
569 assert_eq!(
570 values[k].to_bits(),
571 want.to_bits(),
572 "unit {k} under {signal:?} reads {} against the fold's {want}, {src:?}",
573 values[k]
574 );
575 }
576 }
577 }
578 }
579
580 #[test]
581 fn the_supertoken_reading_is_not_the_token_reading_coarsened() {
582 let src = b"a = 1;\nb = 22;\nc = 333;\nd = 4444;\ne = 55555;\nf = 666666;\n";
587 let toks = crate::lexer::lex(src);
588 let by_token = analyze(&toks, src, Signal::Magnitude);
589 let by_unit = analyze_supertokens(&toks, src, Signal::Magnitude);
590 assert!(!by_unit.frames.is_empty(), "there are constructs to read");
591 assert!(by_unit.frames.len() < by_token.frames.len(), "fewer units than tokens");
592
593 let rising = by_unit.frames.iter().filter(|f| f.direction > 0).count();
596 let falling = by_unit.frames.iter().filter(|f| f.direction < 0).count();
597 assert!(rising > falling, "the construct sequence trends up: {rising} vs {falling}");
598 }
599
600 #[test]
601 fn the_analytic_reading_lifts_the_same_way() {
602 let src = b"a = 1;\nb = 22;\nc = 333;\nd = 4444;\ne = 55555;\nf = 666666;\n";
603 let toks = crate::lexer::lex(src);
604 let a = analytic_supertokens(&toks, src, Signal::Length);
605 assert!(!a.amplitude.is_empty(), "an envelope over constructs");
606 }
607
608 #[test]
609 fn spans_are_all_the_reading_needs_of_its_layer() {
610 let src = b"a 1 bb 22 ccc 333";
613 let toks = crate::tokutil::lex_sig(src);
614 let sig: Vec<f32> = toks.iter().map(|t| t.len() as f32).collect();
615 let spans: Vec<(usize, usize)> = toks.iter().map(|t| (t.start(), t.end())).collect();
616 let cfg = FlowConfig::default();
617 assert_eq!(
618 analyze_signal(&toks, &sig, &cfg).frames.len(),
619 analyze_spans(&spans, &sig, &cfg).frames.len()
620 );
621 }
622}
623
624#[cfg(test)]
625mod analytic_tests {
626 use super::*;
627
628 fn cfg() -> AnalyticConfig {
629 AnalyticConfig::default()
630 }
631
632 fn mid_mean(v: &[f32], lat: usize) -> f32 {
634 let lo = lat * 2;
635 let hi = v.len().saturating_sub(lat);
636 if hi <= lo {
637 return 0.0;
638 }
639 v[lo..hi].iter().sum::<f32>() / (hi - lo) as f32
640 }
641
642 #[test]
643 fn a_constant_amplitude_sinusoid_reads_its_amplitude_and_frequency() {
644 let w = std::f32::consts::TAU / 8.0;
647 let s: Vec<f32> = (0..400).map(|i| 3.0 * (w * i as f32).sin()).collect();
648 let a = analytic_signal(&s, &cfg());
649 let env = mid_mean(&a.amplitude, a.latency);
650 assert!((env - 3.0).abs() < 0.3, "envelope reads the amplitude: {env}");
651 let f = mid_mean(&a.frequency, a.latency);
652 assert!((f - w).abs() < 0.05, "frequency reads the rate: {f} vs {w}");
653 }
654
655 #[test]
656 fn the_envelope_rises_where_the_slope_only_keeps_changing_sign() {
657 let w = std::f32::consts::TAU / 8.0;
663 let s: Vec<f32> =
664 (0..400).map(|i| (0.5 + i as f32 * 0.02) * (w * i as f32).sin()).collect();
665 let a = analytic_signal(&s, &cfg());
666 let lat = a.latency;
667 let early = a.amplitude[lat * 2..100].iter().sum::<f32>() / (100 - lat * 2) as f32;
668 let late = a.amplitude[300..380].iter().sum::<f32>() / 80.0;
669 assert!(late > early * 3.0, "the envelope tracks the growth: {early} -> {late}");
670
671 let env_flips = (lat * 2 + 1..380)
674 .filter(|&i| {
675 (a.amplitude[i] - a.amplitude[i - 1]) * (a.amplitude[i - 1] - a.amplitude[i - 2])
676 < 0.0
677 })
678 .count();
679
680 let src: Vec<u8> = std::iter::repeat_n(b"a ", s.len()).flatten().copied().collect();
687 let toks = crate::tokutil::lex_sig(&src);
688 assert_eq!(toks.len(), s.len(), "one token per sample");
689 let f = analyze_signal(&toks, &s, &FlowConfig::default());
690 let slope_flips = (lat * 2 + 1..380)
691 .filter(|&i| f.frames[i].slope * f.frames[i - 1].slope < 0.0)
692 .count();
693
694 assert!(
695 slope_flips > env_flips * 4,
696 "the slope keeps reversing where the envelope does not: {slope_flips} vs {env_flips}"
697 );
698 }
699
700 #[test]
701 fn a_chirp_is_read_as_a_rising_frequency() {
702 let s: Vec<f32> = (0..600)
707 .map(|i| {
708 let t = i as f32;
709 let phase = 0.15 * t + 0.0004 * t * t;
710 phase.sin()
711 })
712 .collect();
713 let a = analytic_signal(&s, &cfg());
714 let lat = a.latency;
715 let early = a.frequency[lat * 2..200].iter().sum::<f32>() / (200 - lat * 2) as f32;
716 let late = a.frequency[400..560].iter().sum::<f32>() / 160.0;
717 assert!(late > early + 0.05, "the frequency rises with the chirp: {early} -> {late}");
718 }
719
720 #[test]
721 fn the_reading_is_causal_with_latency() {
722 let w = std::f32::consts::TAU / 7.0;
726 let long: Vec<f32> = (0..300).map(|i| (w * i as f32).sin()).collect();
727 let short = &long[..200];
728 let a = analytic_signal(&long, &cfg());
729 let b = analytic_signal(short, &cfg());
730 let lat = a.latency;
731 for i in lat..(200 - lat) {
733 assert!(
734 (a.amplitude[i] - b.amplitude[i]).abs() < 1e-4,
735 "index {i} moved when later input arrived: {} vs {}",
736 b.amplitude[i],
737 a.amplitude[i]
738 );
739 }
740 }
741
742 #[test]
743 fn a_constant_signal_has_no_envelope() {
744 let s = vec![7.5f32; 200];
747 let a = analytic_signal(&s, &cfg());
748 assert!(mid_mean(&a.amplitude, a.latency) < 0.05, "a flat signal does not oscillate");
749 }
750
751 #[test]
752 fn degenerate_inputs_are_safe() {
753 assert!(analytic_signal(&[], &cfg()).amplitude.is_empty());
754 let one = analytic_signal(&[1.0], &cfg());
755 assert_eq!(one.amplitude.len(), 1);
756 assert_eq!(one.frequency_at(99), 0.0);
757 assert_eq!(one.amplitude_at(99), 0.0);
758 }
759
760 #[test]
761 fn it_composes_over_a_named_axis_the_way_flow_does() {
762 let src = b"a 1 bb 22 ccc 333 dddd 4444 eeeee 55555 f 6 gg 77 hhh 888";
763 let toks = crate::tokutil::lex_sig(src);
764 let a = analytic(&toks, src, Signal::Length);
765 assert_eq!(a.amplitude.len(), toks.len());
766 assert!(a.amplitude.iter().any(|&x| x > 0.0), "the length signal swings");
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 #[test]
775 fn ramp_flows_upward_with_momentum() {
776 let f = analyze_bytes(b"1 10 100 1000 10000 100000", Signal::Magnitude);
778 let last = f.frames.last().copied().unwrap_or_default();
779 assert_eq!(last.direction, 1, "a rising ramp flows upward");
780 assert!(last.momentum >= 3, "sustained trend builds momentum, got {}", last.momentum);
781 assert!(f.reversals.is_empty(), "a monotone ramp has no reversal");
782 }
783
784 #[test]
785 fn valley_has_a_reversal_at_the_bottom() {
786 let f = analyze_bytes(b"100000 1000 10 1 10 1000 100000", Signal::Magnitude);
788 assert!(
789 !f.reversals.is_empty(),
790 "a fall-then-rise should register a reversal"
791 );
792 }
793
794 #[test]
795 fn flow_composes_over_stress_depth() {
796 let f = analyze_bytes(b"a(b(c(d(e))))", Signal::StressDepth);
798 assert!(f.frames.iter().any(|fr| fr.direction == 1), "deepening rises");
799 assert!(
800 f.frames.iter().any(|fr| fr.direction == -1),
801 "the release falls"
802 );
803 assert!(!f.reversals.is_empty(), "deepen-then-release reverses");
804 }
805
806 #[test]
807 fn flat_signal_is_steady() {
808 let f = analyze_bytes(b"a b c d e f g", Signal::Length);
809 assert!(f.frames.iter().all(|fr| fr.direction == 0), "equal lengths are steady");
810 assert!(f.reversals.is_empty());
811 }
812
813 #[test]
814 fn empty_is_safe() {
815 let f = analyze_bytes(b"", Signal::Magnitude);
816 assert_eq!(f.n_tokens, 0);
817 assert!(f.frames.is_empty());
818 assert!(f.reversals.is_empty());
819 assert_eq!(f.direction_at(0), 0);
820 }
821}