use crate::approximation::paa::{Paa, PaaConfig};
use crate::core::config::GafMethod;
use crate::core::traits::Transformer;
#[derive(Debug, Clone)]
pub struct GafConfig {
pub image_size: Option<usize>,
pub method: GafMethod,
pub sample_range: (f64, f64),
}
impl GafConfig {
pub fn new() -> Self {
Self {
image_size: None,
method: GafMethod::Summation,
sample_range: (-1.0, 1.0),
}
}
}
impl Default for GafConfig {
fn default() -> Self {
Self::new()
}
}
pub struct Gaf;
impl Gaf {
pub fn transform(config: &GafConfig, x: &[Vec<f64>]) -> Vec<Vec<Vec<f64>>> {
assert!(!x.is_empty(), "Input must have at least one sample");
let n_timestamps = x[0].len();
let image_size = config.image_size.unwrap_or(n_timestamps);
let (range_min, range_max) = config.sample_range;
let method = config.method;
if image_size < n_timestamps {
let scaled = scale_to_range(x, config.sample_range);
let paa_config = PaaConfig::new(image_size);
let reduced = Paa::transform(&paa_config, &scaled);
let build = |sample: &Vec<f64>| {
let sin_phi: Vec<f64> = sample
.iter()
.map(|&c| (1.0 - c * c).max(0.0).sqrt())
.collect();
gaf_image(sample, &sin_phi, method)
};
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
return reduced.par_iter().map(build).collect();
}
#[cfg(not(feature = "parallel"))]
reduced.iter().map(build).collect()
} else {
let build = |raw_sample: &Vec<f64>| {
let x_min = raw_sample.iter().copied().fold(f64::INFINITY, f64::min);
let x_max = raw_sample.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let data_range = x_max - x_min;
let (cos_phi, sin_phi): (Vec<f64>, Vec<f64>) = if data_range == 0.0 {
let c = range_min.clamp(-1.0, 1.0);
let s = (1.0 - c * c).max(0.0).sqrt();
(vec![c; raw_sample.len()], vec![s; raw_sample.len()])
} else {
let scale = (range_max - range_min) / data_range;
raw_sample
.iter()
.map(|&v| {
let c = ((v - x_min) * scale + range_min).clamp(-1.0, 1.0);
let s = (1.0 - c * c).max(0.0).sqrt();
(c, s)
})
.unzip()
};
gaf_image(&cos_phi, &sin_phi, method)
};
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
return x.par_iter().map(build).collect();
}
#[cfg(not(feature = "parallel"))]
x.iter().map(build).collect()
}
}
}
#[inline]
fn gaf_image(cos_phi: &[f64], sin_phi: &[f64], method: GafMethod) -> Vec<Vec<f64>> {
let n = cos_phi.len();
let mut buf = vec![0.0f64; n * n];
match method {
GafMethod::Summation => {
for i in 0..n {
let ci = cos_phi[i];
let si = sin_phi[i];
let row = i * n;
for j in 0..n {
buf[row + j] = ci * cos_phi[j] - si * sin_phi[j];
}
}
}
GafMethod::Difference => {
for i in 0..n {
let ci = cos_phi[i];
let si = sin_phi[i];
let row = i * n;
for j in 0..n {
buf[row + j] = si * cos_phi[j] - ci * sin_phi[j];
}
}
}
}
buf.chunks_exact(n).map(|row| row.to_vec()).collect()
}
fn scale_to_range(x: &[Vec<f64>], range: (f64, f64)) -> Vec<Vec<f64>> {
let (range_min, range_max) = range;
x.iter()
.map(|sample| {
let x_min = sample.iter().copied().fold(f64::INFINITY, f64::min);
let x_max = sample.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let data_range = x_max - x_min;
if data_range == 0.0 {
vec![range_min; sample.len()]
} else {
let scale = (range_max - range_min) / data_range;
sample
.iter()
.map(|&v| ((v - x_min) * scale + range_min).clamp(-1.0, 1.0))
.collect()
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gasf_shape() {
let config = GafConfig::new();
let x = vec![vec![1.0, 2.0, 3.0, 4.0]];
let result = Gaf::transform(&config, &x);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 4);
assert_eq!(result[0][0].len(), 4);
}
#[test]
fn test_gasf_symmetric() {
let config = GafConfig::new();
let x = vec![vec![0.0, 0.5, 1.0]];
let result = Gaf::transform(&config, &x);
for i in 0..3 {
for j in 0..3 {
assert!(
(result[0][i][j] - result[0][j][i]).abs() < 1e-10,
"GASF should be symmetric"
);
}
}
}
#[test]
fn test_gadf_antisymmetric() {
let config = GafConfig {
method: GafMethod::Difference,
..GafConfig::new()
};
let x = vec![vec![0.0, 0.5, 1.0]];
let result = Gaf::transform(&config, &x);
for i in 0..3 {
assert!(result[0][i][i].abs() < 1e-10, "GADF diagonal should be 0");
for j in (i + 1)..3 {
assert!(
(result[0][i][j] + result[0][j][i]).abs() < 1e-10,
"GADF should be antisymmetric"
);
}
}
}
#[test]
fn test_gaf_with_paa() {
let config = GafConfig {
image_size: Some(3),
..GafConfig::new()
};
let x = vec![vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]];
let result = Gaf::transform(&config, &x);
assert_eq!(result[0].len(), 3);
assert_eq!(result[0][0].len(), 3);
}
#[test]
fn test_gaf_values_in_range() {
let config = GafConfig::new();
let x = vec![vec![0.0, 1.0, 2.0, 3.0]];
let result = Gaf::transform(&config, &x);
for row in &result[0] {
for &v in row {
assert!((-1.0 - 1e-10..=1.0 + 1e-10).contains(&v));
}
}
}
}