use cubecl::prelude::*;
use crate::core::DspFloat;
use super::covariance::covariance_of_host;
use super::eigen::{symmetric_eigen, EigenOptions};
use super::projection::DeviceProjection;
pub const MIN_NOISE_VARIANCE: f64 = 1e-8;
pub const FULL_RANK_NOISE_VARIANCE: f64 = 1e-6;
#[derive(Debug, Clone)]
pub struct PpcaModel {
pub num_channels: usize,
pub num_components: usize,
pub mean: Vec<f32>,
pub weights: Vec<f32>,
pub posterior_projection: Vec<f32>,
pub noise_variance: f32,
pub eigenvalues: Vec<f32>,
}
impl PpcaModel {
pub fn fit<F: DspFloat>(client: &Client, data: &[f32], channels: usize, samples: usize, num_components: usize) -> Self {
assert!(channels > 0 && samples > 0, "Channels and samples must be > 0");
let num_components = num_components.clamp(1, channels);
let (cov, mean) = covariance_of_host::<F>(client, data, channels, samples);
let eig = symmetric_eigen::<F>(client, &cov, channels, EigenOptions::default());
let noise_variance = if num_components < channels {
let tail: f64 = eig.values[num_components..].iter().map(|&v| v.max(0.0)).sum();
(tail / (channels - num_components) as f64).max(MIN_NOISE_VARIANCE)
} else {
FULL_RANK_NOISE_VARIANCE
};
let mut weights = vec![0.0f32; channels * num_components];
let mut posterior_projection = vec![0.0f32; channels * num_components];
let mut eigenvalues = Vec::with_capacity(num_components);
for k in 0..num_components {
let lam = eig.values[k].max(noise_variance + MIN_NOISE_VARIANCE);
eigenvalues.push(lam as f32);
let scale_w = (lam - noise_variance).max(0.0).sqrt();
let scale_p = scale_w / lam;
for c in 0..channels {
let u_ck = eig.vectors[c * channels + k];
weights[c * num_components + k] = (u_ck * scale_w) as f32;
posterior_projection[c * num_components + k] = (u_ck * scale_p) as f32;
}
}
Self {
num_channels: channels,
num_components,
mean: mean.iter().map(|&m| m as f32).collect(),
weights,
posterior_projection,
noise_variance: noise_variance as f32,
eigenvalues,
}
}
pub fn fit_em_masked<F: DspFloat>(
client: &Client,
data: &[f32],
observed_mask: &[bool],
channels: usize,
samples: usize,
mut num_components: usize,
max_iters: usize,
) -> Self {
assert_eq!(data.len(), channels * samples);
assert_eq!(observed_mask.len(), channels * samples);
num_components = num_components.clamp(1, channels);
let mut mean = vec![0.0f32; channels];
for c in 0..channels {
let mut sum = 0.0f32;
let mut count = 0usize;
for t in 0..samples {
if observed_mask[c * samples + t] {
sum += data[c * samples + t];
count += 1;
}
}
mean[c] = if count > 0 { sum / (count as f32) } else { 0.0 };
}
let mut imputed = data.to_vec();
for c in 0..channels {
for t in 0..samples {
if !observed_mask[c * samples + t] {
imputed[c * samples + t] = mean[c];
}
}
}
let mut model = Self::fit::<F>(client, &imputed, channels, samples, num_components);
for _iter in 0..max_iters.max(1) {
let z = model.project_cpu(&imputed, channels, samples);
let recon = model.reconstruct_cpu(&z, samples);
for c in 0..channels {
for t in 0..samples {
let idx = c * samples + t;
if !observed_mask[idx] {
imputed[idx] = recon[idx];
}
}
}
model = Self::fit::<F>(client, &imputed, channels, samples, num_components);
}
model
}
pub fn project_cpu(&self, data: &[f32], channels: usize, samples: usize) -> Vec<f32> {
assert_eq!(channels, self.num_channels);
assert_eq!(data.len(), channels * samples);
let mut out = vec![0.0f32; self.num_components * samples];
for k in 0..self.num_components {
let out_row = &mut out[k * samples..(k + 1) * samples];
for c in 0..channels {
let p_ck = self.posterior_projection[c * self.num_components + k];
let mean_c = self.mean[c];
let in_row = &data[c * samples..(c + 1) * samples];
for t in 0..samples {
out_row[t] += (in_row[t] - mean_c) * p_ck;
}
}
}
out
}
pub fn reconstruct_cpu(&self, z: &[f32], samples: usize) -> Vec<f32> {
assert_eq!(z.len(), self.num_components * samples);
let mut recon = vec![0.0f32; self.num_channels * samples];
for c in 0..self.num_channels {
let out_row = &mut recon[c * samples..(c + 1) * samples];
out_row.fill(self.mean[c]);
for k in 0..self.num_components {
let w_ck = self.weights[c * self.num_components + k];
let z_row = &z[k * samples..(k + 1) * samples];
for t in 0..samples {
out_row[t] += w_ck * z_row[t];
}
}
}
recon
}
pub fn to_device<F: DspFloat>(&self, client: &Client) -> DeviceProjection {
DeviceProjection::upload::<F>(client, &self.posterior_projection, &self.mean, self.num_channels, self.num_components)
}
pub fn project_gpu<F: DspFloat>(
&self,
client: &Client,
input_handle: &cubecl::server::Handle,
output_handle: &cubecl::server::Handle,
channels: usize,
samples: usize,
) {
assert_eq!(channels, self.num_channels);
self.to_device::<F>(client).project::<F>(client, input_handle, output_handle, samples);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn recovers(client: &Client) {
let channels = 5;
let samples = 3000;
let true_sigma2 = 0.25f32; let true_sigma = true_sigma2.sqrt();
let mut state = 0xCAFE_BABEu64;
let mut next_norm = || -> f32 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let u1 = ((state >> 32) as f32 / (u32::MAX as f32)).clamp(1e-6, 1.0 - 1e-6);
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let u2 = (state >> 32) as f32 / (u32::MAX as f32);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos()
};
let w_true = [3.0f32, -2.0, 1.0, 0.0, 0.0];
let mut data = vec![0.0f32; channels * samples];
for t in 0..samples {
let z = next_norm();
for c in 0..channels {
data[c * samples + t] = w_true[c] * z + true_sigma * next_norm();
}
}
let ppca = PpcaModel::fit::<f32>(client, &data, channels, samples, 1);
assert!((ppca.noise_variance - true_sigma2).abs() < 0.05, "noise_var={}", ppca.noise_variance);
let w_norm_sq: f32 = ppca.weights.iter().map(|&v| v * v).sum();
assert!((w_norm_sq - 14.0).abs() < 1.0, "w_norm_sq={w_norm_sq}");
}
runtime_test!(test_ppca_recovers_subspace_and_isotropic_noise_variance, recovers);
}