use scirs2_core::ndarray::{s, Array1, Array2, ArrayStatCompat};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::Debug;
use crate::error::{Result, TimeSeriesError};
use statrs::statistics::Statistics;
#[derive(Debug, Clone)]
pub struct SymbolicApproximationConfig {
pub method: SymbolicMethod,
pub alphabet_size: usize,
pub window_size: usize,
pub nsegments: usize,
pub normalize_data: bool,
pub breakpoints: Option<Array1<f64>>,
pub distance_metric: SymbolicDistance,
}
impl Default for SymbolicApproximationConfig {
fn default() -> Self {
Self {
method: SymbolicMethod::SAX,
alphabet_size: 8,
window_size: 16,
nsegments: 10,
normalize_data: true,
breakpoints: None,
distance_metric: SymbolicDistance::MINDIST,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SymbolicMethod {
SAX,
APCA,
PLA,
Persist,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SymbolicDistance {
MINDIST,
Hamming,
Edit,
}
#[derive(Debug, Clone)]
pub struct SymbolicApproximationResult {
pub symbolic_sequence: Vec<char>,
pub breakpoints: Array1<f64>,
pub _paavalues: Array1<f64>,
pub reconstruction_error: f64,
pub compression_ratio: f64,
pub distance_matrix: Option<Array2<f64>>,
}
#[allow(dead_code)]
pub fn apply_symbolic_approximation(
_timeseries: &Array1<f64>,
config: &SymbolicApproximationConfig,
) -> Result<SymbolicApproximationResult> {
if _timeseries.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Time _series cannot be empty".to_string(),
));
}
match config.method {
SymbolicMethod::SAX => apply_sax(_timeseries, config),
SymbolicMethod::APCA => apply_apca(_timeseries, config),
SymbolicMethod::PLA => apply_pla(_timeseries, config),
SymbolicMethod::Persist => apply_persist(_timeseries, config),
}
}
#[allow(dead_code)]
fn apply_sax(
_timeseries: &Array1<f64>,
config: &SymbolicApproximationConfig,
) -> Result<SymbolicApproximationResult> {
let normalized_data = if config.normalize_data {
normalize_timeseries(_timeseries)?
} else {
_timeseries.clone()
};
let _paavalues = compute_paa(&normalized_data, config.nsegments)?;
let breakpoints = config
.breakpoints
.clone()
.unwrap_or_else(|| compute_gaussian_breakpoints(config.alphabet_size));
let symbolic_sequence = paa_to_symbols(&_paavalues, &breakpoints)?;
let reconstruction_error = 0.0;
let compression_ratio = _timeseries.len() as f64 / symbolic_sequence.len() as f64;
Ok(SymbolicApproximationResult {
symbolic_sequence,
breakpoints,
_paavalues,
reconstruction_error,
compression_ratio,
distance_matrix: None,
})
}
#[allow(dead_code)]
fn apply_apca(
timeseries: &Array1<f64>,
config: &SymbolicApproximationConfig,
) -> Result<SymbolicApproximationResult> {
let n = timeseries.len();
if n == 0 {
return Err(TimeSeriesError::InvalidInput(
"Time series must not be empty".to_string(),
));
}
let nseg = config.nsegments.min(n);
let data = if config.normalize_data {
normalize_timeseries(timeseries)?
} else {
timeseries.clone()
};
let mut segments: Vec<(usize, usize)> = (0..n).map(|i| (i, i)).collect();
let segment_sse = |a: usize, b: usize| -> f64 {
let len = (b - a + 1) as f64;
let mut sum = 0.0_f64;
let mut sum_sq = 0.0_f64;
for k in a..=b {
sum += data[k];
sum_sq += data[k] * data[k];
}
sum_sq - sum * sum / len
};
while segments.len() > nseg {
let mut best_cost = f64::INFINITY;
let mut best_idx = 0;
for i in 0..segments.len() - 1 {
let a = segments[i].0;
let b = segments[i + 1].1;
let cost = segment_sse(a, b);
if cost < best_cost {
best_cost = cost;
best_idx = i;
}
}
let merged_end = segments[best_idx + 1].1;
segments[best_idx].1 = merged_end;
segments.remove(best_idx + 1);
}
let mut paa_values = Array1::zeros(segments.len());
for (j, &(a, b)) in segments.iter().enumerate() {
let len = (b - a + 1) as f64;
let mean: f64 = data.slice(scirs2_core::ndarray::s![a..=b]).sum() / len;
paa_values[j] = mean;
}
let breakpoints = config
.breakpoints
.clone()
.unwrap_or_else(|| compute_gaussian_breakpoints(config.alphabet_size));
let symbolic_sequence = paa_to_symbols(&paa_values, &breakpoints)?;
let mut total_sse = 0.0_f64;
for (j, &(a, b)) in segments.iter().enumerate() {
let mean = paa_values[j];
for k in a..=b {
let e = data[k] - mean;
total_sse += e * e;
}
}
let reconstruction_error = (total_sse / n as f64).sqrt();
let compression_ratio = n as f64 / symbolic_sequence.len() as f64;
Ok(SymbolicApproximationResult {
symbolic_sequence,
breakpoints,
_paavalues: paa_values,
reconstruction_error,
compression_ratio,
distance_matrix: None,
})
}
#[allow(dead_code)]
fn apply_pla(
timeseries: &Array1<f64>,
config: &SymbolicApproximationConfig,
) -> Result<SymbolicApproximationResult> {
let n = timeseries.len();
if n == 0 {
return Err(TimeSeriesError::InvalidInput(
"Time series must not be empty".to_string(),
));
}
let nseg = config.nsegments.min(n);
let data = if config.normalize_data {
normalize_timeseries(timeseries)?
} else {
timeseries.clone()
};
let seg_size = n as f64 / nseg as f64;
let mut paa_values = Array1::zeros(nseg);
let mut total_sse = 0.0_f64;
for j in 0..nseg {
let start = (j as f64 * seg_size).round() as usize;
let end = ((j + 1) as f64 * seg_size).round() as usize;
let end = end.min(n);
let seg_len = end - start;
if seg_len == 0 {
continue;
}
if seg_len == 1 {
paa_values[j] = data[start];
continue;
}
let len_f = seg_len as f64;
let t_mean = (seg_len - 1) as f64 / 2.0;
let mut y_mean = 0.0_f64;
let mut sxx = 0.0_f64;
let mut sxy = 0.0_f64;
for (k, idx) in (start..end).enumerate() {
let t = k as f64;
let y = data[idx];
y_mean += y;
sxx += (t - t_mean) * (t - t_mean);
sxy += (t - t_mean) * y;
}
y_mean /= len_f;
let slope = if sxx.abs() > 1e-12 { sxy / sxx } else { 0.0 };
let intercept = y_mean - slope * t_mean;
paa_values[j] = y_mean;
for (k, idx) in (start..end).enumerate() {
let fitted = slope * k as f64 + intercept;
let e = data[idx] - fitted;
total_sse += e * e;
}
}
let breakpoints = config
.breakpoints
.clone()
.unwrap_or_else(|| compute_gaussian_breakpoints(config.alphabet_size));
let symbolic_sequence = paa_to_symbols(&paa_values, &breakpoints)?;
let reconstruction_error = (total_sse / n as f64).sqrt();
let compression_ratio = n as f64 / symbolic_sequence.len() as f64;
Ok(SymbolicApproximationResult {
symbolic_sequence,
breakpoints,
_paavalues: paa_values,
reconstruction_error,
compression_ratio,
distance_matrix: None,
})
}
#[allow(dead_code)]
fn apply_persist(
timeseries: &Array1<f64>,
config: &SymbolicApproximationConfig,
) -> Result<SymbolicApproximationResult> {
let n = timeseries.len();
if n == 0 {
return Err(TimeSeriesError::InvalidInput(
"Time series must not be empty".to_string(),
));
}
let data = if config.normalize_data {
normalize_timeseries(timeseries)?
} else {
timeseries.clone()
};
let nseg = config.nsegments.min(n);
let paa_values = compute_paa(&data, nseg)?;
let mut symbolic_sequence = Vec::with_capacity(nseg);
for j in 0..nseg {
let symbol = if j == 0 {
'b'
} else {
match paa_values[j].partial_cmp(&paa_values[j - 1]) {
Some(std::cmp::Ordering::Greater) => 'c', Some(std::cmp::Ordering::Less) => 'a', _ => 'b', }
};
symbolic_sequence.push(symbol);
}
let breakpoints = config
.breakpoints
.clone()
.unwrap_or_else(|| compute_gaussian_breakpoints(config.alphabet_size));
let seg_size = n as f64 / nseg as f64;
let mut total_sse = 0.0_f64;
for j in 0..nseg {
let start = (j as f64 * seg_size).round() as usize;
let end = (((j + 1) as f64 * seg_size).round() as usize).min(n);
let mean = paa_values[j];
for idx in start..end {
let e = data[idx] - mean;
total_sse += e * e;
}
}
let reconstruction_error = (total_sse / n as f64).sqrt();
let compression_ratio = n as f64 / symbolic_sequence.len() as f64;
Ok(SymbolicApproximationResult {
symbolic_sequence,
breakpoints,
_paavalues: paa_values,
reconstruction_error,
compression_ratio,
distance_matrix: None,
})
}
#[allow(dead_code)]
fn normalize_timeseries(_timeseries: &Array1<f64>) -> Result<Array1<f64>> {
let mean = _timeseries.mean_or(0.0);
let std = _timeseries.std(0.0);
if std == 0.0 {
return Ok(Array1::zeros(_timeseries.len()));
}
let normalized = _timeseries.mapv(|x| (x - mean) / std);
Ok(normalized)
}
#[allow(dead_code)]
fn compute_paa(_timeseries: &Array1<f64>, nsegments: usize) -> Result<Array1<f64>> {
let n = _timeseries.len();
let segment_size = n as f64 / nsegments as f64;
let mut _paavalues = Array1::zeros(nsegments);
for i in 0..nsegments {
let start = (i as f64 * segment_size) as usize;
let end = ((i + 1) as f64 * segment_size) as usize;
let end = std::cmp::min(end, n);
if start < end {
let segment_mean = _timeseries.slice(s![start..end]).mean();
_paavalues[i] = segment_mean;
}
}
Ok(_paavalues)
}
#[allow(dead_code)]
fn compute_gaussian_breakpoints(_alphabetsize: usize) -> Array1<f64> {
let mut breakpoints = Array1::zeros(_alphabetsize - 1);
for i in 0.._alphabetsize - 1 {
let quantile = (i + 1) as f64 / _alphabetsize as f64;
let breakpoint = if quantile < 0.5 {
-(1.0 - 2.0 * quantile).sqrt()
} else {
(2.0 * quantile - 1.0).sqrt()
};
breakpoints[i] = breakpoint;
}
breakpoints
}
#[allow(dead_code)]
fn paa_to_symbols(_paavalues: &Array1<f64>, breakpoints: &Array1<f64>) -> Result<Vec<char>> {
let alphabet_chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz".chars().collect();
let mut symbols = Vec::new();
for &value in _paavalues.iter() {
let mut symbol_idx = 0;
for &breakpoint in breakpoints.iter() {
if value > breakpoint {
symbol_idx += 1;
} else {
break;
}
}
let symbol = alphabet_chars.get(symbol_idx).copied().unwrap_or('z');
symbols.push(symbol);
}
Ok(symbols)
}
#[allow(dead_code)]
pub fn reconstruct_from_sax(
symbolic_sequence: &[char],
breakpoints: &Array1<f64>,
original_length: usize,
nsegments: usize,
) -> Result<Array1<f64>> {
if nsegments == 0 || original_length == 0 {
return Err(TimeSeriesError::InvalidInput(
"nsegments and original_length must be positive".to_string(),
));
}
let bp = breakpoints;
let nb = bp.len();
let mut midpoints: Vec<f64> = Vec::with_capacity(nb + 1);
if nb == 0 {
midpoints.push(0.0);
} else {
let lower_ext = if nb > 1 {
bp[0] - (bp[1] - bp[0]) / 2.0
} else {
bp[0] - 1.0
};
midpoints.push(lower_ext);
for i in 0..nb.saturating_sub(1) {
midpoints.push((bp[i] + bp[i + 1]) / 2.0);
}
let upper_ext = if nb > 1 {
bp[nb - 1] + (bp[nb - 1] - bp[nb - 2]) / 2.0
} else {
bp[0] + 1.0
};
midpoints.push(upper_ext);
}
let alphabet_start = 'a' as u8;
let mut paa_values = Array1::zeros(symbolic_sequence.len());
for (j, &sym) in symbolic_sequence.iter().enumerate() {
let level = (sym as u8).saturating_sub(alphabet_start) as usize;
let level = level.min(midpoints.len() - 1);
paa_values[j] = midpoints[level];
}
let nseg = nsegments.min(symbolic_sequence.len());
let seg_size = original_length as f64 / nseg as f64;
let mut reconstructed = Array1::zeros(original_length);
for j in 0..nseg {
let start = (j as f64 * seg_size).round() as usize;
let end = (((j + 1) as f64 * seg_size).round() as usize).min(original_length);
let val = if j < paa_values.len() {
paa_values[j]
} else {
0.0
};
for idx in start..end {
reconstructed[idx] = val;
}
}
Ok(reconstructed)
}
#[allow(dead_code)]
pub fn compute_reconstruction_error(original: &Array1<f64>, reconstructed: &Array1<f64>) -> f64 {
let n = original.len().min(reconstructed.len());
if n == 0 {
return 0.0;
}
let mut sse = 0.0_f64;
for i in 0..n {
let e = original[i] - reconstructed[i];
sse += e * e;
}
(sse / n as f64).sqrt()
}