1use cubecl::prelude::*;
2use crate::core::DspFloat;
3use super::covariance::covariance_of_host;
4use super::eigen::{symmetric_eigen, EigenOptions};
5use super::projection::DeviceProjection;
6
7pub const MIN_NOISE_VARIANCE: f64 = 1e-8;
9
10pub const FULL_RANK_NOISE_VARIANCE: f64 = 1e-6;
12
13#[derive(Debug, Clone)]
18pub struct PpcaModel {
19 pub num_channels: usize,
20 pub num_components: usize,
21 pub mean: Vec<f32>,
22 pub weights: Vec<f32>,
24 pub posterior_projection: Vec<f32>,
27 pub noise_variance: f32,
29 pub eigenvalues: Vec<f32>,
31}
32
33impl PpcaModel {
34 pub fn fit<F: DspFloat>(client: &Client, data: &[f32], channels: usize, samples: usize, num_components: usize) -> Self {
37 assert!(channels > 0 && samples > 0, "Channels and samples must be > 0");
38 let num_components = num_components.clamp(1, channels);
39 let (cov, mean) = covariance_of_host::<F>(client, data, channels, samples);
40 let eig = symmetric_eigen::<F>(client, &cov, channels, EigenOptions::default());
41
42 let noise_variance = if num_components < channels {
44 let tail: f64 = eig.values[num_components..].iter().map(|&v| v.max(0.0)).sum();
45 (tail / (channels - num_components) as f64).max(MIN_NOISE_VARIANCE)
46 } else {
47 FULL_RANK_NOISE_VARIANCE
48 };
49
50 let mut weights = vec![0.0f32; channels * num_components];
51 let mut posterior_projection = vec![0.0f32; channels * num_components];
52 let mut eigenvalues = Vec::with_capacity(num_components);
53 for k in 0..num_components {
54 let lam = eig.values[k].max(noise_variance + MIN_NOISE_VARIANCE);
55 eigenvalues.push(lam as f32);
56 let scale_w = (lam - noise_variance).max(0.0).sqrt();
57 let scale_p = scale_w / lam;
59 for c in 0..channels {
60 let u_ck = eig.vectors[c * channels + k];
61 weights[c * num_components + k] = (u_ck * scale_w) as f32;
62 posterior_projection[c * num_components + k] = (u_ck * scale_p) as f32;
63 }
64 }
65
66 Self {
67 num_channels: channels,
68 num_components,
69 mean: mean.iter().map(|&m| m as f32).collect(),
70 weights,
71 posterior_projection,
72 noise_variance: noise_variance as f32,
73 eigenvalues,
74 }
75 }
76
77 pub fn fit_em_masked<F: DspFloat>(
80 client: &Client,
81 data: &[f32],
82 observed_mask: &[bool],
83 channels: usize,
84 samples: usize,
85 mut num_components: usize,
86 max_iters: usize,
87 ) -> Self {
88 assert_eq!(data.len(), channels * samples);
89 assert_eq!(observed_mask.len(), channels * samples);
90 num_components = num_components.clamp(1, channels);
91
92 let mut mean = vec![0.0f32; channels];
94 for c in 0..channels {
95 let mut sum = 0.0f32;
96 let mut count = 0usize;
97 for t in 0..samples {
98 if observed_mask[c * samples + t] {
99 sum += data[c * samples + t];
100 count += 1;
101 }
102 }
103 mean[c] = if count > 0 { sum / (count as f32) } else { 0.0 };
104 }
105
106 let mut imputed = data.to_vec();
107 for c in 0..channels {
108 for t in 0..samples {
109 if !observed_mask[c * samples + t] {
110 imputed[c * samples + t] = mean[c];
111 }
112 }
113 }
114
115 let mut model = Self::fit::<F>(client, &imputed, channels, samples, num_components);
116 for _iter in 0..max_iters.max(1) {
117 let z = model.project_cpu(&imputed, channels, samples);
119 let recon = model.reconstruct_cpu(&z, samples);
121 for c in 0..channels {
122 for t in 0..samples {
123 let idx = c * samples + t;
124 if !observed_mask[idx] {
125 imputed[idx] = recon[idx];
126 }
127 }
128 }
129 model = Self::fit::<F>(client, &imputed, channels, samples, num_components);
131 }
132 model
133 }
134
135 pub fn project_cpu(&self, data: &[f32], channels: usize, samples: usize) -> Vec<f32> {
138 assert_eq!(channels, self.num_channels);
139 assert_eq!(data.len(), channels * samples);
140 let mut out = vec![0.0f32; self.num_components * samples];
141
142 for k in 0..self.num_components {
143 let out_row = &mut out[k * samples..(k + 1) * samples];
144 for c in 0..channels {
145 let p_ck = self.posterior_projection[c * self.num_components + k];
146 let mean_c = self.mean[c];
147 let in_row = &data[c * samples..(c + 1) * samples];
148 for t in 0..samples {
149 out_row[t] += (in_row[t] - mean_c) * p_ck;
150 }
151 }
152 }
153 out
154 }
155
156 pub fn reconstruct_cpu(&self, z: &[f32], samples: usize) -> Vec<f32> {
158 assert_eq!(z.len(), self.num_components * samples);
159 let mut recon = vec![0.0f32; self.num_channels * samples];
160
161 for c in 0..self.num_channels {
162 let out_row = &mut recon[c * samples..(c + 1) * samples];
163 out_row.fill(self.mean[c]);
164 for k in 0..self.num_components {
165 let w_ck = self.weights[c * self.num_components + k];
166 let z_row = &z[k * samples..(k + 1) * samples];
167 for t in 0..samples {
168 out_row[t] += w_ck * z_row[t];
169 }
170 }
171 }
172 recon
173 }
174
175 pub fn to_device<F: DspFloat>(&self, client: &Client) -> DeviceProjection {
177 DeviceProjection::upload::<F>(client, &self.posterior_projection, &self.mean, self.num_channels, self.num_components)
178 }
179
180 pub fn project_gpu<F: DspFloat>(
183 &self,
184 client: &Client,
185 input_handle: &cubecl::server::Handle,
186 output_handle: &cubecl::server::Handle,
187 channels: usize,
188 samples: usize,
189 ) {
190 assert_eq!(channels, self.num_channels);
191 self.to_device::<F>(client).project::<F>(client, input_handle, output_handle, samples);
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn recovers(client: &Client) {
200 let channels = 5;
201 let samples = 3000;
202 let true_sigma2 = 0.25f32; let true_sigma = true_sigma2.sqrt();
204
205 let mut state = 0xCAFE_BABEu64;
206 let mut next_norm = || -> f32 {
207 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
208 let u1 = ((state >> 32) as f32 / (u32::MAX as f32)).clamp(1e-6, 1.0 - 1e-6);
209 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
210 let u2 = (state >> 32) as f32 / (u32::MAX as f32);
211 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos()
212 };
213
214 let w_true = [3.0f32, -2.0, 1.0, 0.0, 0.0];
216 let mut data = vec![0.0f32; channels * samples];
217 for t in 0..samples {
218 let z = next_norm();
219 for c in 0..channels {
220 data[c * samples + t] = w_true[c] * z + true_sigma * next_norm();
221 }
222 }
223
224 let ppca = PpcaModel::fit::<f32>(client, &data, channels, samples, 1);
225 assert!((ppca.noise_variance - true_sigma2).abs() < 0.05, "noise_var={}", ppca.noise_variance);
226
227 let w_norm_sq: f32 = ppca.weights.iter().map(|&v| v * v).sum();
229 assert!((w_norm_sq - 14.0).abs() < 1.0, "w_norm_sq={w_norm_sq}");
230 }
231 runtime_test!(test_ppca_recovers_subspace_and_isotropic_noise_variance, recovers);
232}