1use crate::error::{Result, TransformError};
7use scirs2_core::gpu::{GpuBackend, GpuContext};
8use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
9use scirs2_core::validation::{check_not_empty, check_positive, checkarray_finite};
10
11#[cfg(feature = "gpu")]
13pub struct GpuPCA {
14 pub n_components: usize,
16 pub center: bool,
18 pub components: Option<Array2<f64>>,
20 pub explained_variance: Option<Array1<f64>>,
22 pub mean: Option<Array1<f64>>,
24 gpu_context: Option<GpuContext>,
26}
27
28#[cfg(feature = "gpu")]
29impl GpuPCA {
30 pub fn new(n_components: usize) -> Result<Self> {
54 check_positive(n_components, "n_components")?;
55
56 let gpu_context = GpuContext::new(GpuBackend::preferred()).map_err(|e| {
57 TransformError::ComputationError(format!("Failed to initialize GPU: {}", e))
58 })?;
59
60 Ok(GpuPCA {
61 n_components,
62 center: true,
63 components: None,
64 explained_variance: None,
65 mean: None,
66 gpu_context: Some(gpu_context),
67 })
68 }
69
70 pub fn fit(&mut self, x: &ArrayView2<f64>) -> Result<()> {
104 check_not_empty(x, "x")?;
105 checkarray_finite(x, "x")?;
106
107 let (n_samples, n_features) = x.dim();
108 if self.n_components > n_features.min(n_samples) {
109 return Err(TransformError::InvalidInput(
110 "n_components cannot be larger than min(n_samples, n_features)".to_string(),
111 ));
112 }
113
114 let mut cpu_pca = crate::reduction::PCA::new(self.n_components, self.center, false);
116 cpu_pca.fit(x)?;
117
118 self.components = cpu_pca.components().cloned();
119 self.mean = cpu_pca.mean().cloned();
120 self.explained_variance = cpu_pca.explained_variance_ratio().cloned();
123
124 Ok(())
125 }
126
127 pub fn transform(&self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
146 check_not_empty(x, "x")?;
147 checkarray_finite(x, "x")?;
148
149 let components = self.components.as_ref().ok_or_else(|| {
150 TransformError::NotFitted(
151 "GpuPCA model has not been fitted; call fit() first".to_string(),
152 )
153 })?;
154
155 let (n_samples, n_features) = x.dim();
156 let n_comp_features = components.shape()[1];
157
158 if n_features != n_comp_features {
159 return Err(TransformError::InvalidInput(format!(
160 "x has {} features, but GpuPCA was fitted with {} features",
161 n_features, n_comp_features,
162 )));
163 }
164
165 let x_centered: Array2<f64> = if self.center {
167 if let Some(mean) = &self.mean {
168 let mut centered = Array2::zeros((n_samples, n_features));
169 for i in 0..n_samples {
170 for j in 0..n_features {
171 centered[[i, j]] = x[[i, j]] - mean[j];
172 }
173 }
174 centered
175 } else {
176 x.to_owned()
178 }
179 } else {
180 x.to_owned()
181 };
182
183 let transformed = x_centered.dot(&components.t());
185 Ok(transformed)
186 }
187
188 pub fn fit_transform(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
205 self.fit(x)?;
206 self.transform(x)
207 }
208
209 pub fn explained_variance_ratio(&self) -> Result<Array1<f64>> {
221 self.explained_variance
222 .as_ref()
223 .cloned()
224 .ok_or_else(|| TransformError::NotFitted("GpuPCA model not fitted".to_string()))
225 }
226}
227
228#[cfg(feature = "gpu")]
230pub struct GpuMatrixOps {
231 #[allow(dead_code)]
232 gpu_context: GpuContext,
233}
234
235#[cfg(feature = "gpu")]
236impl GpuMatrixOps {
237 pub fn new() -> Result<Self> {
239 let gpu_context = GpuContext::new(GpuBackend::preferred()).map_err(|e| {
240 TransformError::ComputationError(format!("Failed to initialize GPU: {}", e))
241 })?;
242
243 Ok(GpuMatrixOps { gpu_context })
244 }
245
246 pub fn matmul(self_a: &ArrayView2<f64>, b: &ArrayView2<f64>) -> Result<Array2<f64>> {
248 Err(TransformError::NotImplemented(
249 "GPU matrix multiplication is not yet implemented. Use CPU operations instead."
250 .to_string(),
251 ))
252 }
253
254 pub fn svd(selfa: &ArrayView2<f64>) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
256 Err(TransformError::NotImplemented(
257 "GPU SVD is not yet implemented. Use CPU operations instead.".to_string(),
258 ))
259 }
260
261 pub fn eigh(selfa: &ArrayView2<f64>) -> Result<(Array1<f64>, Array2<f64>)> {
263 Err(TransformError::NotImplemented(
264 "GPU eigendecomposition is not yet implemented. Use CPU operations instead."
265 .to_string(),
266 ))
267 }
268}
269
270#[cfg(feature = "gpu")]
272pub struct GpuTSNE {
273 pub n_components: usize,
275 pub perplexity: f64,
277 pub learning_rate: f64,
279 pub max_iter: usize,
281 #[allow(dead_code)]
283 gpu_context: GpuContext,
284}
285
286#[cfg(feature = "gpu")]
287impl GpuTSNE {
288 pub fn new(n_components: usize) -> Result<Self> {
290 check_positive(n_components, "n_components")?;
291
292 let gpu_context = GpuContext::new(GpuBackend::preferred()).map_err(|e| {
293 TransformError::ComputationError(format!("Failed to initialize GPU: {}", e))
294 })?;
295
296 Ok(GpuTSNE {
297 n_components,
298 perplexity: 30.0,
299 learning_rate: 200.0,
300 max_iter: 1000,
301 gpu_context,
302 })
303 }
304
305 pub fn with_perplexity(mut self, perplexity: f64) -> Self {
307 self.perplexity = perplexity;
308 self
309 }
310
311 pub fn with_learning_rate(mut self, learning_rate: f64) -> Self {
313 self.learning_rate = learning_rate;
314 self
315 }
316
317 pub fn with_max_iter(mut self, max_iter: usize) -> Self {
319 self.max_iter = max_iter;
320 self
321 }
322
323 pub fn fit_transform(selfx: &ArrayView2<f64>) -> Result<Array2<f64>> {
325 Err(TransformError::NotImplemented(
326 "GPU t-SNE is not yet implemented. Use CPU t-SNE instead.".to_string(),
327 ))
328 }
329}
330
331#[cfg(not(feature = "gpu"))]
333pub struct GpuPCA;
334
335#[cfg(not(feature = "gpu"))]
336pub struct GpuMatrixOps;
337
338#[cfg(not(feature = "gpu"))]
339pub struct GpuTSNE;
340
341#[cfg(not(feature = "gpu"))]
342impl GpuPCA {
343 pub fn new(_ncomponents: usize) -> Result<Self> {
344 Err(TransformError::FeatureNotEnabled(
345 "GPU acceleration requires the 'gpu' feature to be enabled".to_string(),
346 ))
347 }
348}
349
350#[cfg(not(feature = "gpu"))]
351impl GpuMatrixOps {
352 pub fn new() -> Result<Self> {
353 Err(TransformError::FeatureNotEnabled(
354 "GPU acceleration requires the 'gpu' feature to be enabled".to_string(),
355 ))
356 }
357}
358
359#[cfg(not(feature = "gpu"))]
360impl GpuTSNE {
361 pub fn new(_ncomponents: usize) -> Result<Self> {
362 Err(TransformError::FeatureNotEnabled(
363 "GPU acceleration requires the 'gpu' feature to be enabled".to_string(),
364 ))
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use scirs2_core::ndarray::Array2;
372
373 #[cfg(feature = "gpu")]
380 fn sample_data_5x4() -> Array2<f64> {
381 Array2::from_shape_vec(
382 (5, 4),
383 vec![
384 1.0, 2.0, 3.0, 4.0, 2.0, 3.0, 4.0, 5.0, 3.0, 4.0, 5.0, 6.0, 4.0, 5.0, 6.0, 7.0,
385 5.0, 6.0, 7.0, 8.0,
386 ],
387 )
388 .expect("shape vec must be consistent")
389 }
390
391 #[test]
396 #[cfg(feature = "gpu")]
397 fn test_gpu_pca_creation() {
398 let pca = GpuPCA::new(3);
399 assert!(pca.is_ok());
400 let pca = pca.expect("GpuPCA::new should succeed");
401 assert_eq!(pca.n_components, 3);
402 assert!(pca.center);
403 assert!(pca.components.is_none());
404 assert!(pca.explained_variance.is_none());
405 assert!(pca.mean.is_none());
406 }
407
408 #[test]
409 #[cfg(feature = "gpu")]
410 fn test_gpu_pca_invalid_components() {
411 let result = GpuPCA::new(0);
412 assert!(result.is_err());
413 }
414
415 #[test]
420 #[cfg(feature = "gpu")]
421 fn test_gpu_pca_fit_populates_fields() {
422 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
423 let data = sample_data_5x4();
424
425 pca.fit(&data.view()).expect("GpuPCA::fit should succeed");
426
427 assert!(pca.components.is_some(), "components must be set after fit");
429 assert!(
430 pca.explained_variance.is_some(),
431 "explained_variance must be set after fit"
432 );
433 assert!(pca.mean.is_some(), "mean must be set after fit");
434 }
435
436 #[test]
437 #[cfg(feature = "gpu")]
438 fn test_gpu_pca_fit_components_shape() {
439 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
440 let data = sample_data_5x4();
441 pca.fit(&data.view()).expect("fit must succeed");
442
443 let comp = pca.components.as_ref().expect("components present");
445 assert_eq!(comp.shape(), &[2, 4]);
446 }
447
448 #[test]
449 #[cfg(feature = "gpu")]
450 fn test_gpu_pca_fit_mean_shape() {
451 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
452 let data = sample_data_5x4();
453 pca.fit(&data.view()).expect("fit must succeed");
454
455 let mean = pca.mean.as_ref().expect("mean present");
456 assert_eq!(mean.len(), 4, "mean must have one entry per feature");
457 }
458
459 #[test]
460 #[cfg(feature = "gpu")]
461 fn test_gpu_pca_fit_explained_variance_length() {
462 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
463 let data = sample_data_5x4();
464 pca.fit(&data.view()).expect("fit must succeed");
465
466 let ev = pca
467 .explained_variance
468 .as_ref()
469 .expect("explained_variance present");
470 assert_eq!(
471 ev.len(),
472 2,
473 "explained_variance must have n_components entries"
474 );
475 }
476
477 #[test]
478 #[cfg(feature = "gpu")]
479 fn test_gpu_pca_fit_explained_variance_non_increasing() {
480 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
481 let data = sample_data_5x4();
482 pca.fit(&data.view()).expect("fit must succeed");
483
484 let ev = pca
485 .explained_variance
486 .as_ref()
487 .expect("explained_variance present");
488 for i in 0..ev.len() - 1 {
489 assert!(
490 ev[i] >= ev[i + 1] - 1e-10,
491 "explained variance must be non-increasing: ev[{}]={} < ev[{}]={}",
492 i,
493 ev[i],
494 i + 1,
495 ev[i + 1]
496 );
497 }
498 }
499
500 #[test]
501 #[cfg(feature = "gpu")]
502 fn test_gpu_pca_fit_n_components_too_large() {
503 let mut pca = GpuPCA::new(10).expect("GpuPCA::new(10) should succeed");
505 let data = sample_data_5x4(); let result = pca.fit(&data.view());
507 assert!(
508 result.is_err(),
509 "should fail when n_components > min(n_samples, n_features)"
510 );
511 }
512
513 #[test]
518 #[cfg(feature = "gpu")]
519 fn test_gpu_pca_transform_shape() {
520 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
521 let data = sample_data_5x4();
522 pca.fit(&data.view()).expect("fit must succeed");
523
524 let transformed = pca
525 .transform(&data.view())
526 .expect("transform must succeed after fit");
527
528 assert_eq!(transformed.shape(), &[5, 2]);
530 }
531
532 #[test]
533 #[cfg(feature = "gpu")]
534 fn test_gpu_pca_transform_all_finite() {
535 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
536 let data = sample_data_5x4();
537 pca.fit(&data.view()).expect("fit must succeed");
538 let transformed = pca.transform(&data.view()).expect("transform must succeed");
539
540 assert!(
541 transformed.iter().all(|v| v.is_finite()),
542 "all transformed values must be finite"
543 );
544 }
545
546 #[test]
547 #[cfg(feature = "gpu")]
548 fn test_gpu_pca_transform_before_fit_returns_error() {
549 let pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
550 let data = sample_data_5x4();
551 let result = pca.transform(&data.view());
552 assert!(result.is_err(), "transform before fit must return an error");
553 }
554
555 #[test]
560 #[cfg(feature = "gpu")]
561 fn test_gpu_pca_fit_transform_shape() {
562 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
563 let data = sample_data_5x4();
564 let ft = pca
565 .fit_transform(&data.view())
566 .expect("fit_transform must succeed");
567 assert_eq!(ft.shape(), &[5, 2]);
568 }
569
570 #[test]
571 #[cfg(feature = "gpu")]
572 fn test_gpu_pca_fit_transform_matches_fit_then_transform() {
573 let data = sample_data_5x4();
574
575 let mut pca1 = GpuPCA::new(2).expect("GpuPCA::new should succeed");
576 let ft = pca1
577 .fit_transform(&data.view())
578 .expect("fit_transform must succeed");
579
580 let mut pca2 = GpuPCA::new(2).expect("GpuPCA::new should succeed");
581 pca2.fit(&data.view()).expect("fit must succeed");
582 let t = pca2
583 .transform(&data.view())
584 .expect("transform must succeed");
585
586 assert_eq!(ft.shape(), t.shape());
587 for (a, b) in ft.iter().zip(t.iter()) {
588 assert!(
589 (a - b).abs() < 1e-10,
590 "fit_transform and fit+transform must agree: {} vs {}",
591 a,
592 b
593 );
594 }
595 }
596
597 #[test]
602 #[cfg(feature = "gpu")]
603 fn test_gpu_pca_explained_variance_ratio_before_fit_returns_error() {
604 let pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
605 assert!(pca.explained_variance_ratio().is_err());
606 }
607
608 #[test]
609 #[cfg(feature = "gpu")]
610 fn test_gpu_pca_explained_variance_ratio_after_fit() {
611 let mut pca = GpuPCA::new(2).expect("GpuPCA::new should succeed");
612 let data = sample_data_5x4();
613 pca.fit(&data.view()).expect("fit must succeed");
614
615 let ratio = pca
616 .explained_variance_ratio()
617 .expect("explained_variance_ratio must succeed after fit");
618 assert_eq!(ratio.len(), 2);
619 assert!(ratio.iter().all(|&v| v >= 0.0));
621 }
622
623 #[test]
628 #[cfg(feature = "gpu")]
629 fn test_gpu_matrix_ops_creation() {
630 let ops = GpuMatrixOps::new();
631 assert!(ops.is_ok());
632 }
633
634 #[test]
635 #[cfg(feature = "gpu")]
636 fn test_gpu_tsne_creation() {
637 let tsne = GpuTSNE::new(2);
638 assert!(tsne.is_ok());
639 let tsne = tsne.expect("GpuTSNE::new should succeed");
640 assert_eq!(tsne.n_components, 2);
641 assert_eq!(tsne.perplexity, 30.0);
642 assert_eq!(tsne.learning_rate, 200.0);
643 assert_eq!(tsne.max_iter, 1000);
644 }
645
646 #[test]
647 #[cfg(feature = "gpu")]
648 fn test_gpu_tsne_with_params() {
649 let tsne = GpuTSNE::new(3)
650 .expect("GpuTSNE::new should succeed")
651 .with_perplexity(50.0)
652 .with_learning_rate(100.0)
653 .with_max_iter(500);
654
655 assert_eq!(tsne.n_components, 3);
656 assert_eq!(tsne.perplexity, 50.0);
657 assert_eq!(tsne.learning_rate, 100.0);
658 assert_eq!(tsne.max_iter, 500);
659 }
660
661 #[test]
662 #[cfg(not(feature = "gpu"))]
663 fn test_gpu_features_disabled() {
664 assert!(GpuPCA::new(2).is_err());
665 assert!(GpuMatrixOps::new().is_err());
666 assert!(GpuTSNE::new(2).is_err());
667 }
668}