use crate::matrix::Matrix;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DistanceMetric {
Euclidean,
Cosine,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RecurrenceMode {
Connectivity,
Affinity,
Distance,
}
pub struct RecurrenceParams {
pub k: Option<usize>,
pub width: usize,
pub metric: DistanceMetric,
pub mode: RecurrenceMode,
pub bandwidth: Option<f64>,
pub sym: bool,
pub self_links: bool,
}
impl Default for RecurrenceParams {
fn default() -> Self {
Self {
k: None,
width: 1,
metric: DistanceMetric::Euclidean,
mode: RecurrenceMode::Connectivity,
bandwidth: None,
sym: false,
self_links: false,
}
}
}
pub fn recurrence_matrix(data: &Matrix, params: &RecurrenceParams) -> Matrix {
let t = data.cols();
let width = params.width;
let k = params.k.unwrap_or_else(|| {
if t > 2 * width + 1 {
let k = 2 * (((t - 2 * width + 1) as f64).sqrt().ceil() as usize);
k.max(2)
} else {
2
}
});
let k = k.min(t.saturating_sub(1));
let k_query = (k + 2 * width).min(t.saturating_sub(1));
let dist = compute_distance_matrix(data, params.metric);
let mut rec = vec![0.0f64; t * t];
for i in 0..t {
let mut candidates: Vec<(f64, usize)> = (0..t)
.filter(|&j| j != i)
.map(|j| (dist[i * t + j], j))
.collect();
candidates
.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let take = k_query.min(candidates.len());
for &(d, j) in candidates.iter().take(take) {
rec[i * t + j] = if params.mode == RecurrenceMode::Connectivity {
1.0
} else {
d
};
}
}
for i in 0..t {
for offset in 0..width {
if i + offset < t {
rec[i * t + (i + offset)] = 0.0;
}
if offset > 0 && i >= offset {
rec[i * t + (i - offset)] = 0.0;
}
}
}
for i in 0..t {
let mut links: Vec<usize> = (0..t).filter(|&j| rec[i * t + j] != 0.0).collect();
if links.len() > k {
if params.mode == RecurrenceMode::Connectivity {
} else {
links.sort_unstable_by(|&a, &b| {
rec[i * t + a]
.partial_cmp(&rec[i * t + b])
.unwrap_or(std::cmp::Ordering::Equal)
});
}
for &j in links[k..].iter() {
rec[i * t + j] = 0.0;
}
}
}
if !params.self_links {
for i in 0..t {
rec[i * t + i] = 0.0;
}
}
if params.sym {
let mut sym_rec = vec![0.0f64; t * t];
for i in 0..t {
for j in i..t {
let rij = rec[i * t + j];
let rji = rec[j * t + i];
let m = rij.min(rji);
sym_rec[i * t + j] = m;
sym_rec[j * t + i] = m;
}
}
rec = sym_rec;
}
if matches!(params.mode, RecurrenceMode::Affinity) {
let bandwidth = params.bandwidth.unwrap_or_else(|| {
let mut kth_dists: Vec<f64> = Vec::with_capacity(t);
for i in 0..t {
let mut row_dists: Vec<f64> = (0..t)
.filter(|&j| rec[i * t + j] > 0.0)
.map(|j| rec[i * t + j])
.collect();
row_dists
.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let take = k.min(row_dists.len());
if take > 0 {
kth_dists.push(row_dists[take - 1]);
}
}
kth_dists
.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = kth_dists.len();
if n == 0 {
1.0
} else if n.is_multiple_of(2) {
(kth_dists[n / 2 - 1] + kth_dists[n / 2]) / 2.0
} else {
kth_dists[n / 2]
}
});
let bandwidth = if bandwidth < 1e-15 { 1.0 } else { bandwidth };
for v in rec.iter_mut() {
if *v != 0.0 {
*v = (-(*v) / bandwidth).exp();
}
}
}
let mut transposed = vec![0.0f64; t * t];
for i in 0..t {
for j in 0..t {
transposed[j * t + i] = rec[i * t + j];
}
}
if params.mode == RecurrenceMode::Connectivity {
for v in transposed.iter_mut() {
*v = if *v != 0.0 { 1.0 } else { 0.0 };
}
}
Matrix::from_vec(transposed, t, t)
}
fn compute_distance_matrix(data: &Matrix, metric: DistanceMetric) -> Vec<f64> {
let t = data.cols();
let d = data.rows();
let norms: Vec<f64> = if matches!(metric, DistanceMetric::Cosine) {
(0..t)
.map(|j| (0..d).map(|r| data.get(r, j).powi(2)).sum::<f64>().sqrt())
.collect()
} else {
vec![]
};
let mut dist = vec![0.0f64; t * t];
for i in 0..t {
for j in (i + 1)..t {
let d_val = match metric {
DistanceMetric::Euclidean => {
let mut sum = 0.0f64;
for r in 0..d {
let diff = data.get(r, i) - data.get(r, j);
sum += diff * diff;
}
sum.sqrt()
}
DistanceMetric::Cosine => {
let ni = norms[i];
let nj = norms[j];
if ni < 1e-15 || nj < 1e-15 {
1.0
} else {
let dot: f64 = (0..d).map(|r| data.get(r, i) * data.get(r, j)).sum();
let cos = (dot / (ni * nj)).clamp(-1.0, 1.0);
1.0 - cos
}
}
};
dist[i * t + j] = d_val;
dist[j * t + i] = d_val;
}
}
dist
}