use std::sync::Arc;
use rayon::prelude::*;
use crate::compute::error::ComputeError;
use crate::compute::simd::{simd_dot_product, simd_sum};
const PARALLEL_THRESHOLD: usize = 8_192;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ComputeConfig {
#[serde(default)]
pub cpu_threads: usize,
}
pub trait ComputeEngine: Send + Sync {
fn batch_dot_product(&self, a: &[f64], b: &[f64]) -> Result<f64, ComputeError>;
fn batch_matrix_solve(
&self,
a_flat: &[f64],
b: &[f64],
n: usize,
) -> Result<Vec<f64>, ComputeError>;
fn batch_exponential_smooth(
&self,
values: &[f64],
alpha: f64,
) -> Result<Vec<f64>, ComputeError>;
fn batch_difference(&self, values: &[f64], d: usize) -> Result<Vec<f64>, ComputeError>;
fn batch_autocorrelation(
&self,
values: &[f64],
max_lag: usize,
) -> Result<Vec<f64>, ComputeError>;
fn batch_least_squares(
&self,
a_flat: &[f64],
b: &[f64],
m: usize,
n: usize,
) -> Result<Vec<f64>, ComputeError>;
}
pub struct CpuEngine {
config: ComputeConfig,
pool: Arc<rayon::ThreadPool>,
}
impl CpuEngine {
pub fn new(config: ComputeConfig) -> Result<Self, ComputeError> {
let num_threads = if config.cpu_threads > 0 {
config.cpu_threads
} else {
std::thread::available_parallelism()
.map(std::num::NonZero::get)
.unwrap_or(4)
};
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.thread_name(|i| format!("chronix-cpu-{i}"))
.build()
.map_err(|e| {
ComputeError::Internal(format!("failed to build rayon thread pool: {e}"))
})?;
metrics::gauge!("chronix_compute_thread_pool_size").set(num_threads as f64);
Ok(Self {
config,
pool: Arc::new(pool),
})
}
#[must_use]
pub fn thread_pool(&self) -> &rayon::ThreadPool {
&self.pool
}
#[must_use]
pub fn num_threads(&self) -> usize {
self.pool.current_num_threads()
}
pub fn batch_z_score(&self, values: &[f64]) -> Result<Vec<f64>, ComputeError> {
if values.len() < 2 {
return Err(ComputeError::InsufficientData {
min: 2,
got: values.len(),
});
}
let n = values.len() as f64;
let mean: f64 = values.iter().sum::<f64>() / n;
let variance: f64 = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
let std_dev = variance.sqrt();
if std_dev == 0.0 {
return Ok(vec![0.0; values.len()]);
}
Ok(values.iter().map(|&v| (v - mean) / std_dev).collect())
}
}
impl std::fmt::Debug for CpuEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CpuEngine")
.field("config", &self.config)
.field("num_threads", &self.pool.current_num_threads())
.finish()
}
}
impl Default for CpuEngine {
fn default() -> Self {
Self::new(ComputeConfig::default()).expect("default CpuEngine config should always succeed")
}
}
impl ComputeEngine for CpuEngine {
fn batch_dot_product(&self, a: &[f64], b: &[f64]) -> Result<f64, ComputeError> {
metrics::counter!("chronix_compute_batch_operations_total", "operation" => "dot_product", "backend" => "cpu").increment(1);
if a.len() >= PARALLEL_THRESHOLD {
if a.len() != b.len() {
return Err(ComputeError::DimensionMismatch {
expected: a.len(),
got: b.len(),
});
}
let chunk_size = (a.len() / self.pool.current_num_threads()).max(1024);
let result: Result<f64, ComputeError> = self.pool.install(|| {
a.par_chunks(chunk_size)
.zip(b.par_chunks(chunk_size))
.map(|(ca, cb)| simd_dot_product(ca, cb))
.try_reduce(|| 0.0, |acc, v| Ok(acc + v))
});
return result;
}
simd_dot_product(a, b)
}
fn batch_matrix_solve(
&self,
a_flat: &[f64],
b: &[f64],
n: usize,
) -> Result<Vec<f64>, ComputeError> {
if a_flat.len() != n * n {
return Err(ComputeError::DimensionMismatch {
expected: n * n,
got: a_flat.len(),
});
}
if b.len() != n {
return Err(ComputeError::DimensionMismatch {
expected: n,
got: b.len(),
});
}
metrics::counter!("chronix_compute_batch_operations_total", "operation" => "matrix_solve", "backend" => "cpu").increment(1);
lu_solve(a_flat, b, n)
}
fn batch_exponential_smooth(
&self,
values: &[f64],
alpha: f64,
) -> Result<Vec<f64>, ComputeError> {
if !(0.0..=1.0).contains(&alpha) {
return Err(ComputeError::InvalidParameter {
name: "alpha",
value: alpha.to_string(),
reason: "must be in [0.0, 1.0]",
});
}
if values.is_empty() {
return Ok(Vec::new());
}
let mut result = Vec::with_capacity(values.len());
result.push(values[0]);
let one_minus_alpha = 1.0 - alpha;
for i in 1..values.len() {
let smoothed = alpha * values[i] + one_minus_alpha * result[i - 1];
result.push(smoothed);
}
Ok(result)
}
fn batch_difference(&self, values: &[f64], d: usize) -> Result<Vec<f64>, ComputeError> {
if d == 0 {
return Ok(values.to_vec());
}
if values.len() <= d {
return Err(ComputeError::InsufficientData {
min: d + 1,
got: values.len(),
});
}
let mut current = values.to_vec();
for _ in 0..d {
let mut next = Vec::with_capacity(current.len() - 1);
for i in 1..current.len() {
next.push(current[i] - current[i - 1]);
}
current = next;
}
Ok(current)
}
fn batch_autocorrelation(
&self,
values: &[f64],
max_lag: usize,
) -> Result<Vec<f64>, ComputeError> {
if values.len() < 2 {
return Err(ComputeError::InsufficientData {
min: 2,
got: values.len(),
});
}
metrics::counter!("chronix_compute_batch_operations_total", "operation" => "autocorrelation", "backend" => "cpu").increment(1);
let n = values.len();
let mean = simd_sum(values) / n as f64;
let var: f64 = values.iter().map(|&v| (v - mean).powi(2)).sum();
if var.abs() < 1e-15 {
let mut result = vec![0.0; max_lag + 1];
result[0] = 1.0;
return Ok(result);
}
let effective_max = max_lag.min(n - 1);
let acf = if n * effective_max >= PARALLEL_THRESHOLD {
let pool = &self.pool;
pool.install(|| {
(0..=effective_max)
.into_par_iter()
.map(|lag| {
let mut cov = 0.0;
for i in 0..(n - lag) {
cov += (values[i] - mean) * (values[i + lag] - mean);
}
cov / var
})
.collect::<Vec<_>>()
})
} else {
let mut acf = Vec::with_capacity(effective_max + 1);
for lag in 0..=effective_max {
let mut cov = 0.0;
for i in 0..(n - lag) {
cov += (values[i] - mean) * (values[i + lag] - mean);
}
acf.push(cov / var);
}
acf
};
if acf.len() <= max_lag {
let mut padded = acf;
padded.resize(max_lag + 1, 0.0);
Ok(padded)
} else {
Ok(acf)
}
}
fn batch_least_squares(
&self,
a_flat: &[f64],
b: &[f64],
m: usize,
n: usize,
) -> Result<Vec<f64>, ComputeError> {
if a_flat.len() != m * n {
return Err(ComputeError::DimensionMismatch {
expected: m * n,
got: a_flat.len(),
});
}
if b.len() != m {
return Err(ComputeError::DimensionMismatch {
expected: m,
got: b.len(),
});
}
if m < n {
return Err(ComputeError::DimensionMismatch {
expected: n,
got: m,
});
}
qr_least_squares(a_flat, b, m, n)
}
}
fn qr_least_squares(
a_flat: &[f64],
b: &[f64],
m: usize,
n: usize,
) -> Result<Vec<f64>, ComputeError> {
let mut a_col = vec![0.0; m * n];
for i in 0..m {
for j in 0..n {
a_col[j * m + i] = a_flat[i * n + j];
}
}
let mut rhs = b.to_vec();
for j in 0..n {
let col_start = j * m + j;
let col_end = j * m + m;
let sub_col = &a_col[col_start..col_end];
let sub_len = m - j;
let sigma: f64 = sub_col.iter().map(|&v| v * v).sum::<f64>().sqrt();
if sigma < 1e-15 {
return Err(ComputeError::SingularMatrix);
}
let sign = if sub_col[0] >= 0.0 { 1.0 } else { -1.0 };
let mut v = sub_col.to_vec();
v[0] += sign * sigma;
let v_dot_v: f64 = v.iter().map(|&x| x * x).sum();
if v_dot_v < 1e-30 {
continue;
}
let tau = 2.0 / v_dot_v;
for k in j..n {
let ck = k * m;
let mut dot = 0.0;
for i in 0..sub_len {
dot += v[i] * a_col[ck + j + i];
}
let factor = tau * dot;
for i in 0..sub_len {
a_col[ck + j + i] -= factor * v[i];
}
}
let mut dot_rhs = 0.0;
for i in 0..sub_len {
dot_rhs += v[i] * rhs[j + i];
}
let factor_rhs = tau * dot_rhs;
for i in 0..sub_len {
rhs[j + i] -= factor_rhs * v[i];
}
}
let mut max_diag = 0.0f64;
for i in 0..n {
max_diag = max_diag.max(a_col[i * m + i].abs());
}
let rank_tol = max_diag * 1e-12 * (m.max(n) as f64);
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut sum = rhs[i];
for k in (i + 1)..n {
sum -= a_col[k * m + i] * x[k];
}
let diag = a_col[i * m + i];
if diag.abs() < rank_tol {
return Err(ComputeError::SingularMatrix);
}
x[i] = sum / diag;
}
Ok(x)
}
fn lu_solve(a_flat: &[f64], b: &[f64], n: usize) -> Result<Vec<f64>, ComputeError> {
let mut a = a_flat.to_vec();
let rhs = b.to_vec();
let mut pivot = (0..n).collect::<Vec<usize>>();
for col in 0..n {
let mut max_val = a[pivot[col] * n + col].abs();
let mut max_row = col;
for row in (col + 1)..n {
let v = a[pivot[row] * n + col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-14 {
return Err(ComputeError::SingularMatrix);
}
pivot.swap(col, max_row);
let piv_row = pivot[col];
for row in (col + 1)..n {
let cur_row = pivot[row];
let factor = a[cur_row * n + col] / a[piv_row * n + col];
a[cur_row * n + col] = factor; for k in (col + 1)..n {
a[cur_row * n + k] -= factor * a[piv_row * n + k];
}
}
}
let mut y = vec![0.0; n];
for i in 0..n {
let mut sum = rhs[pivot[i]];
for j in 0..i {
sum -= a[pivot[i] * n + j] * y[j];
}
y[i] = sum;
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut sum = y[i];
for j in (i + 1)..n {
sum -= a[pivot[i] * n + j] * x[j];
}
let diag = a[pivot[i] * n + i];
if diag.abs() < 1e-14 {
return Err(ComputeError::SingularMatrix);
}
x[i] = sum / diag;
}
Ok(x)
}
#[cfg(test)]
mod tests {
use super::*;
fn engine() -> CpuEngine {
CpuEngine::default()
}
#[test]
fn dot_product_correct() {
let e = engine();
let r = e
.batch_dot_product(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0])
.unwrap();
assert!((r - 32.0).abs() < 1e-10);
}
#[test]
fn dot_product_dimension_mismatch() {
let e = engine();
assert!(e.batch_dot_product(&[1.0], &[1.0, 2.0]).is_err());
}
#[test]
fn matrix_solve_identity() {
let e = engine();
let a = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
let b = vec![5.0, 3.0, 7.0];
let x = e.batch_matrix_solve(&a, &b, 3).unwrap();
for i in 0..3 {
assert!((x[i] - b[i]).abs() < 1e-10);
}
}
#[test]
fn matrix_solve_2x2() {
let e = engine();
let a = vec![2.0, 1.0, 5.0, 3.0];
let b = vec![4.0, 7.0];
let x = e.batch_matrix_solve(&a, &b, 2).unwrap();
assert!((x[0] - 5.0).abs() < 1e-10);
assert!((x[1] - (-6.0)).abs() < 1e-10);
}
#[test]
fn matrix_solve_singular() {
let e = engine();
let a = vec![1.0, 2.0, 2.0, 4.0]; let b = vec![3.0, 6.0];
assert!(e.batch_matrix_solve(&a, &b, 2).is_err());
}
#[test]
fn exponential_smooth_alpha_1() {
let e = engine();
let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = e.batch_exponential_smooth(&vals, 1.0).unwrap();
assert_eq!(result.len(), vals.len());
for (a, b) in result.iter().zip(vals.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn exponential_smooth_alpha_0() {
let e = engine();
let vals = vec![10.0, 20.0, 30.0];
let result = e.batch_exponential_smooth(&vals, 0.0).unwrap();
for &v in &result {
assert!((v - 10.0).abs() < 1e-10);
}
}
#[test]
fn exponential_smooth_invalid_alpha() {
let e = engine();
assert!(e.batch_exponential_smooth(&[1.0], 1.5).is_err());
assert!(e.batch_exponential_smooth(&[1.0], -0.1).is_err());
}
#[test]
fn difference_order_1() {
let e = engine();
let vals = vec![1.0, 3.0, 6.0, 10.0, 15.0];
let diff = e.batch_difference(&vals, 1).unwrap();
assert_eq!(diff, vec![2.0, 3.0, 4.0, 5.0]);
}
#[test]
fn difference_order_2() {
let e = engine();
let vals = vec![1.0, 3.0, 6.0, 10.0, 15.0];
let diff = e.batch_difference(&vals, 2).unwrap();
assert_eq!(diff, vec![1.0, 1.0, 1.0]);
}
#[test]
fn difference_order_0() {
let e = engine();
let vals = vec![1.0, 2.0, 3.0];
let diff = e.batch_difference(&vals, 0).unwrap();
assert_eq!(diff, vals);
}
#[test]
fn autocorrelation_lag0_is_one() {
let e = engine();
let vals = vec![1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0, 2.0];
let acf = e.batch_autocorrelation(&vals, 3).unwrap();
assert!((acf[0] - 1.0).abs() < 1e-10);
}
#[test]
fn autocorrelation_constant_series() {
let e = engine();
let vals = vec![5.0; 10];
let acf = e.batch_autocorrelation(&vals, 3).unwrap();
assert!((acf[0] - 1.0).abs() < 1e-10);
for i in 1..=3 {
assert!((acf[i]).abs() < 1e-10);
}
}
#[test]
fn least_squares_line() {
let e = engine();
let a = vec![1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 3.0];
let b = vec![1.0, 3.0, 5.0, 7.0];
let x = e.batch_least_squares(&a, &b, 4, 2).unwrap();
assert!((x[0] - 1.0).abs() < 1e-10); assert!((x[1] - 2.0).abs() < 1e-10); }
#[test]
fn least_squares_noisy() {
let e = engine();
let a = vec![1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 3.0, 1.0, 4.0];
let b = vec![2.1, 4.9, 8.0, 11.1, 14.0];
let x = e.batch_least_squares(&a, &b, 5, 2).unwrap();
assert!((x[0] - 2.0).abs() < 0.5); assert!((x[1] - 3.0).abs() < 0.5); }
#[test]
fn least_squares_ill_conditioned() {
let e = engine();
let m = 4;
let n = 3;
let mut a = Vec::with_capacity(m * n);
for i in 0..m {
for j in 0..n {
a.push(1.0 / (i + j + 1) as f64);
}
}
let x_true = [1.0, -1.0, 1.0];
let mut b = vec![0.0; m];
for i in 0..m {
for j in 0..n {
b[i] += a[i * n + j] * x_true[j];
}
}
let x = e.batch_least_squares(&a, &b, m, n).unwrap();
for j in 0..n {
assert!(
(x[j] - x_true[j]).abs() < 1e-8,
"x[{j}] = {} but expected {} (diff = {})",
x[j],
x_true[j],
(x[j] - x_true[j]).abs(),
);
}
}
#[test]
fn least_squares_underdetermined_fails() {
let e = engine();
let a = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
let b = vec![1.0, 2.0];
assert!(e.batch_least_squares(&a, &b, 2, 3).is_err());
}
#[test]
fn cpu_engine_has_dedicated_thread_pool() {
let config = ComputeConfig { cpu_threads: 2 };
let e = CpuEngine::new(config).unwrap();
assert_eq!(e.num_threads(), 2);
}
#[test]
fn cpu_engine_default_auto_detects_threads() {
let e = CpuEngine::default();
assert!(e.num_threads() >= 1);
}
#[test]
fn parallel_dot_product_matches_serial() {
let n = super::PARALLEL_THRESHOLD + 1000;
let a: Vec<f64> = (0..n).map(|i| (i as f64) * 0.001).collect();
let b: Vec<f64> = (0..n).map(|i| 1.0 - (i as f64) * 0.0005).collect();
let e = engine();
let result = e.batch_dot_product(&a, &b).unwrap();
let expected: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
assert!(
(result - expected).abs() < expected.abs() * 1e-10,
"parallel dot product diverged: got {result}, expected {expected}"
);
}
#[test]
fn parallel_autocorrelation_matches_serial() {
let n = 1000;
let max_lag = 20;
let config = ComputeConfig { cpu_threads: 2 };
let e = CpuEngine::new(config).unwrap();
let vals: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin()).collect();
let acf = e.batch_autocorrelation(&vals, max_lag).unwrap();
assert_eq!(acf.len(), max_lag + 1);
assert!(
(acf[0] - 1.0).abs() < 1e-10,
"lag-0 autocorrelation must be 1.0"
);
for (lag, &v) in acf.iter().enumerate() {
assert!(
(-1.0 - 1e-10..=1.0 + 1e-10).contains(&v),
"acf[{lag}] = {v} out of range [-1, 1]"
);
}
}
#[test]
fn cpu_engine_debug_shows_threads() {
let e = CpuEngine::new(ComputeConfig { cpu_threads: 3 }).unwrap();
let dbg = format!("{e:?}");
assert!(dbg.contains("num_threads: 3"), "debug output: {dbg}");
}
}