wickra_core/indicators/
kendall_tau.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8fn sign(a: f64, b: f64) -> i32 {
10 if a > b {
11 1
12 } else if a < b {
13 -1
14 } else {
15 0
16 }
17}
18
19#[derive(Debug, Clone)]
58pub struct KendallTau {
59 period: usize,
60 window: VecDeque<(f64, f64)>,
61 scratch: Vec<(f64, f64)>,
63 last: Option<f64>,
64}
65
66impl KendallTau {
67 pub fn new(period: usize) -> Result<Self> {
74 if period < 2 {
75 return Err(Error::InvalidPeriod {
76 message: "Kendall tau needs period >= 2",
77 });
78 }
79 if period > crate::error::MAX_PERIOD {
80 return Err(Error::InvalidPeriod {
81 message: crate::error::PERIOD_ABOVE_MAX,
82 });
83 }
84 Ok(Self {
85 period,
86 window: VecDeque::with_capacity(period),
87 scratch: Vec::with_capacity(period),
88 last: None,
89 })
90 }
91
92 pub const fn period(&self) -> usize {
94 self.period
95 }
96
97 pub const fn value(&self) -> Option<f64> {
99 self.last
100 }
101
102 fn compute(&mut self) -> f64 {
103 self.scratch.clear();
104 self.scratch.extend(self.window.iter().copied());
105 let pairs = &self.scratch;
106 let len = pairs.len();
107 let mut concordant: i64 = 0;
108 let mut discordant: i64 = 0;
109 let mut tie_x: i64 = 0;
110 let mut tie_y: i64 = 0;
111 for i in 0..len {
112 for j in (i + 1)..len {
113 let sx = sign(pairs[j].0, pairs[i].0);
114 let sy = sign(pairs[j].1, pairs[i].1);
115 if sx == 0 {
116 tie_x += 1;
117 }
118 if sy == 0 {
119 tie_y += 1;
120 }
121 let prod = sx * sy;
122 if prod > 0 {
123 concordant += 1;
124 } else if prod < 0 {
125 discordant += 1;
126 }
127 }
128 }
129 let n0 = (len * (len - 1) / 2) as f64;
130 let denom = ((n0 - tie_x as f64) * (n0 - tie_y as f64)).sqrt();
131 if denom == 0.0 {
132 return 0.0;
133 }
134 ((concordant - discordant) as f64 / denom).clamp(-1.0, 1.0)
135 }
136}
137
138impl Indicator for KendallTau {
139 type Input = (f64, f64);
140 type Output = f64;
141
142 #[inline]
143 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
144 if !input.0.is_finite() || !input.1.is_finite() {
145 return None;
146 }
147 if self.window.len() == self.period {
148 self.window.pop_front();
149 }
150 self.window.push_back(input);
151 if self.window.len() < self.period {
152 return None;
153 }
154 let out = self.compute();
155 self.last = Some(out);
156 Some(out)
157 }
158
159 fn reset(&mut self) {
160 self.window.clear();
161 self.scratch.clear();
162 self.last = None;
163 }
164
165 #[inline]
166 fn warmup_period(&self) -> usize {
167 self.period
168 }
169
170 #[inline]
171 fn is_ready(&self) -> bool {
172 self.last.is_some()
173 }
174
175 #[inline]
176 fn name(&self) -> &'static str {
177 "KendallTau"
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::traits::BatchExt;
185 use approx::assert_relative_eq;
186
187 #[test]
188 fn rejects_period_below_two() {
189 assert!(matches!(
190 KendallTau::new(1),
191 Err(Error::InvalidPeriod { .. })
192 ));
193 assert!(KendallTau::new(2).is_ok());
194 }
195
196 #[test]
197 fn accessors_and_metadata() {
198 let k = KendallTau::new(20).unwrap();
199 assert_eq!(k.period(), 20);
200 assert_eq!(k.warmup_period(), 20);
201 assert_eq!(k.name(), "KendallTau");
202 assert!(!k.is_ready());
203 assert_eq!(k.value(), None);
204 }
205
206 #[test]
207 fn first_emission_at_warmup_period() {
208 let mut k = KendallTau::new(4).unwrap();
209 let out = k.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0), (5.0, 5.0)]);
210 for v in out.iter().take(3) {
211 assert!(v.is_none());
212 }
213 assert!(out[3].is_some());
214 }
215
216 #[test]
217 fn monotone_increasing_is_one() {
218 let pairs: Vec<(f64, f64)> = (0..20)
219 .map(|i| (f64::from(i), 2.0 * f64::from(i) + 1.0))
220 .collect();
221 let last = KendallTau::new(10)
222 .unwrap()
223 .batch(&pairs)
224 .into_iter()
225 .flatten()
226 .last()
227 .unwrap();
228 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
229 }
230
231 #[test]
232 fn monotone_decreasing_is_minus_one() {
233 let pairs: Vec<(f64, f64)> = (0..20)
234 .map(|i| (f64::from(i), -3.0 * f64::from(i)))
235 .collect();
236 let last = KendallTau::new(10)
237 .unwrap()
238 .batch(&pairs)
239 .into_iter()
240 .flatten()
241 .last()
242 .unwrap();
243 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
244 }
245
246 #[test]
247 fn constant_channel_yields_zero() {
248 let pairs: Vec<(f64, f64)> = (0..20).map(|i| (f64::from(i), 7.0)).collect();
250 let last = KendallTau::new(8)
251 .unwrap()
252 .batch(&pairs)
253 .into_iter()
254 .flatten()
255 .last()
256 .unwrap();
257 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
258 }
259
260 #[test]
261 fn output_in_range() {
262 let pairs: Vec<(f64, f64)> = (0..80)
263 .map(|i| {
264 let t = f64::from(i);
265 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.3).cos() * 3.0)
266 })
267 .collect();
268 for v in KendallTau::new(20)
269 .unwrap()
270 .batch(&pairs)
271 .into_iter()
272 .flatten()
273 {
274 assert!((-1.0..=1.0).contains(&v));
275 }
276 }
277
278 #[test]
279 fn reset_clears_state() {
280 let mut k = KendallTau::new(4).unwrap();
281 k.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)]);
282 assert!(k.is_ready());
283 k.reset();
284 assert!(!k.is_ready());
285 assert_eq!(k.value(), None);
286 assert_eq!(k.update((1.0, 1.0)), None);
287 }
288
289 #[test]
290 fn batch_equals_streaming() {
291 let pairs: Vec<(f64, f64)> = (0..60)
292 .map(|i| {
293 let t = f64::from(i);
294 (t.sin(), (t * 0.5).cos())
295 })
296 .collect();
297 let batch = KendallTau::new(14).unwrap().batch(&pairs);
298 let mut b = KendallTau::new(14).unwrap();
299 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
300 assert_eq!(batch, streamed);
301 }
302
303 #[test]
304 fn ties_are_corrected() {
305 let mut k = KendallTau::new(4).unwrap();
308 assert_eq!(k.update((1.0, 1.0)), None);
309 assert_eq!(k.update((1.0, 2.0)), None);
310 assert_eq!(k.update((2.0, 2.0)), None);
311 let v = k.update((3.0, 3.0)).unwrap();
312 assert!((-1.0..=1.0).contains(&v), "got {v}");
313 }
314
315 #[test]
316 fn non_finite_input_returns_none() {
317 let mut k = KendallTau::new(2).unwrap();
318 assert_eq!(k.update((f64::NAN, 1.0)), None);
319 assert_eq!(k.update((1.0, f64::INFINITY)), None);
320 assert_eq!(k.update((1.0, 2.0)), None);
322 assert!(k.update((2.0, 5.0)).is_some());
323 }
324}