scirs2_integrate/visualization/
advanced.rs

1//! Advanced visualization features for scientific computing
2//!
3//! This module provides advanced visualization capabilities including multi-dimensional
4//! data visualization, dimension reduction techniques, clustering algorithms, and animated
5//! visualizations for time-series and dynamic systems.
6
7use super::types::*;
8use crate::error::{IntegrateError, IntegrateResult};
9use crate::ode::ODEResult;
10use scirs2_core::ndarray::{Array1, Array2, Axis};
11use scirs2_core::random::Rng;
12use std::collections::HashMap;
13
14/// Multi-dimensional data visualization engine
15#[derive(Debug, Clone)]
16pub struct MultiDimensionalVisualizer {
17    /// Dimension reduction method
18    pub reduction_method: DimensionReductionMethod,
19    /// Target dimensions for visualization
20    pub target_dimensions: usize,
21    /// Clustering method for data grouping
22    pub clustering_method: ClusteringMethod,
23}
24
25impl MultiDimensionalVisualizer {
26    /// Create new multi-dimensional visualizer
27    pub fn new() -> Self {
28        Self {
29            reduction_method: DimensionReductionMethod::PCA,
30            target_dimensions: 2,
31            clustering_method: ClusteringMethod::None,
32        }
33    }
34
35    /// Visualize high-dimensional data
36    pub fn visualize_high_dimensional_data(
37        &self,
38        data: &Array2<f64>,
39        labels: Option<&Array1<usize>>,
40    ) -> IntegrateResult<HighDimensionalPlot> {
41        // Apply dimension reduction
42        let reduced_data = self.apply_dimension_reduction(data)?;
43
44        // Apply clustering if specified
45        let cluster_labels = self.apply_clustering(&reduced_data)?;
46
47        // Create plot data
48        let x: Vec<f64> = reduced_data.column(0).to_vec();
49        let y: Vec<f64> = if reduced_data.ncols() > 1 {
50            reduced_data.column(1).to_vec()
51        } else {
52            vec![0.0; x.len()]
53        };
54
55        let z: Option<Vec<f64>> = if self.target_dimensions > 2 && reduced_data.ncols() > 2 {
56            Some(reduced_data.column(2).to_vec())
57        } else {
58            None
59        };
60
61        let colors = if let Some(labels) = labels {
62            labels.to_vec().into_iter().map(|l| l as f64).collect()
63        } else if let Some(clusters) = &cluster_labels {
64            clusters.iter().map(|&c| c as f64).collect()
65        } else {
66            (0..x.len()).map(|i| i as f64).collect()
67        };
68
69        let mut metadata = PlotMetadata::default();
70        metadata.title = "High-Dimensional Data Visualization".to_string();
71        metadata.xlabel = format!("{:?} Component 1", self.reduction_method);
72        metadata.ylabel = format!("{:?} Component 2", self.reduction_method);
73
74        Ok(HighDimensionalPlot {
75            x,
76            y,
77            z,
78            colors,
79            cluster_labels,
80            original_dimensions: data.ncols(),
81            reduced_dimensions: self.target_dimensions,
82            reduction_method: self.reduction_method,
83            metadata,
84        })
85    }
86
87    /// Apply dimension reduction to data
88    fn apply_dimension_reduction(&self, data: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
89        match self.reduction_method {
90            DimensionReductionMethod::PCA => self.apply_pca(data),
91            DimensionReductionMethod::TSNE => self.apply_tsne(data),
92            DimensionReductionMethod::UMAP => self.apply_umap(data),
93            DimensionReductionMethod::LDA => self.apply_lda(data),
94            DimensionReductionMethod::MDS => self.apply_mds(data),
95        }
96    }
97
98    /// Apply Principal Component Analysis
99    fn apply_pca(&self, data: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
100        let (n_samples, n_features) = data.dim();
101
102        // Center the data
103        let mean = data.mean_axis(Axis(0)).unwrap();
104        let centered_data = data - &mean.insert_axis(Axis(0));
105
106        // Compute covariance matrix
107        let cov_matrix = centered_data.t().dot(&centered_data) / (n_samples - 1) as f64;
108
109        // Simplified eigenvalue decomposition (for small matrices)
110        let (eigenvalues, eigenvectors) = self.compute_eigendecomposition(&cov_matrix)?;
111
112        // Sort by eigenvalue magnitude (descending)
113        let mut eigenvalue_indices: Vec<usize> = (0..eigenvalues.len()).collect();
114        eigenvalue_indices.sort_by(|&i, &j| eigenvalues[j].partial_cmp(&eigenvalues[i]).unwrap());
115
116        // Project data onto principal components
117        let n_components = self.target_dimensions.min(n_features);
118        let mut projected_data = Array2::zeros((n_samples, n_components));
119
120        for (i, &idx) in eigenvalue_indices.iter().take(n_components).enumerate() {
121            let component = eigenvectors.column(idx);
122            let projection = centered_data.dot(&component);
123            projected_data.column_mut(i).assign(&projection);
124        }
125
126        Ok(projected_data)
127    }
128
129    /// Apply t-SNE (simplified implementation)
130    fn apply_tsne(&self, data: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
131        // Simplified t-SNE implementation - in practice would use proper t-SNE algorithm
132        let (n_samples, _) = data.dim();
133        let mut rng = scirs2_core::random::rng();
134
135        // Random initialization
136        let mut embedding = Array2::zeros((n_samples, self.target_dimensions));
137        for i in 0..n_samples {
138            for j in 0..self.target_dimensions {
139                embedding[[i, j]] = rng.random::<f64>() * 2.0 - 1.0;
140            }
141        }
142
143        // For this simplified version, return random initialization
144        // In practice, would implement full t-SNE gradient descent
145        Ok(embedding)
146    }
147
148    /// Apply UMAP (simplified implementation)
149    fn apply_umap(&self, data: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
150        // Simplified UMAP - in practice would use proper UMAP algorithm
151        // For now, fall back to PCA
152        self.apply_pca(data)
153    }
154
155    /// Apply Linear Discriminant Analysis
156    fn apply_lda(&self, data: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
157        // Simplified LDA - would need class labels for proper implementation
158        // For now, fall back to PCA
159        self.apply_pca(data)
160    }
161
162    /// Apply Multidimensional Scaling
163    fn apply_mds(&self, data: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
164        let n_samples = data.nrows();
165
166        // Compute distance matrix
167        let mut distance_matrix = Array2::zeros((n_samples, n_samples));
168        for i in 0..n_samples {
169            for j in i..n_samples {
170                let dist = self.euclidean_distance(&data.row(i), &data.row(j));
171                distance_matrix[[i, j]] = dist;
172                distance_matrix[[j, i]] = dist;
173            }
174        }
175
176        // Classical MDS using eigendecomposition
177        let squared_distances = distance_matrix.mapv(|d| d * d);
178
179        // Double centering
180        let row_means = squared_distances.mean_axis(Axis(1)).unwrap();
181        let col_means = squared_distances.mean_axis(Axis(0)).unwrap();
182        let grand_mean = squared_distances.iter().sum::<f64>() / squared_distances.len() as f64;
183
184        let mut b_matrix = Array2::zeros((n_samples, n_samples));
185        for i in 0..n_samples {
186            for j in 0..n_samples {
187                b_matrix[[i, j]] =
188                    -0.5 * (squared_distances[[i, j]] - row_means[i] - col_means[j] + grand_mean);
189            }
190        }
191
192        // Eigendecomposition of B matrix
193        let (eigenvalues, eigenvectors) = self.compute_eigendecomposition(&b_matrix)?;
194
195        // Sort eigenvalues in descending order
196        let mut eigenvalue_indices: Vec<usize> = (0..eigenvalues.len()).collect();
197        eigenvalue_indices.sort_by(|&i, &j| eigenvalues[j].partial_cmp(&eigenvalues[i]).unwrap());
198
199        // Construct embedding
200        let n_components = self.target_dimensions.min(n_samples);
201        let mut embedding = Array2::zeros((n_samples, n_components));
202
203        for (i, &idx) in eigenvalue_indices.iter().take(n_components).enumerate() {
204            if eigenvalues[idx] > 0.0 {
205                let scale = eigenvalues[idx].sqrt();
206                for j in 0..n_samples {
207                    embedding[[j, i]] = eigenvectors[[j, idx]] * scale;
208                }
209            }
210        }
211
212        Ok(embedding)
213    }
214
215    /// Apply clustering to reduced data
216    fn apply_clustering(&self, data: &Array2<f64>) -> IntegrateResult<Option<Vec<usize>>> {
217        match self.clustering_method {
218            ClusteringMethod::KMeans { k } => Ok(Some(self.kmeans_clustering(data, k)?)),
219            ClusteringMethod::DBSCAN { eps, min_samples } => {
220                Ok(Some(self.dbscan_clustering(data, eps, min_samples)?))
221            }
222            ClusteringMethod::Hierarchical { n_clusters } => {
223                Ok(Some(self.hierarchical_clustering(data, n_clusters)?))
224            }
225            ClusteringMethod::None => Ok(None),
226        }
227    }
228
229    /// K-means clustering implementation
230    fn kmeans_clustering(&self, data: &Array2<f64>, k: usize) -> IntegrateResult<Vec<usize>> {
231        let mut rng = scirs2_core::random::rng();
232        let (n_samples, n_features) = data.dim();
233
234        // Initialize centroids randomly
235        let mut centroids = Array2::zeros((k, n_features));
236        for i in 0..k {
237            for j in 0..n_features {
238                let min_val = data.column(j).iter().fold(f64::INFINITY, |a, &b| a.min(b));
239                let max_val = data
240                    .column(j)
241                    .iter()
242                    .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
243                centroids[[i, j]] = min_val + rng.random::<f64>() * (max_val - min_val);
244            }
245        }
246
247        let mut labels = vec![0; n_samples];
248        let max_iterations = 100;
249
250        for _iteration in 0..max_iterations {
251            let mut changed = false;
252
253            // Assign points to nearest centroids
254            for i in 0..n_samples {
255                let mut min_distance = f64::INFINITY;
256                let mut best_cluster = 0;
257
258                for j in 0..k {
259                    let distance = self.euclidean_distance(&data.row(i), &centroids.row(j));
260                    if distance < min_distance {
261                        min_distance = distance;
262                        best_cluster = j;
263                    }
264                }
265
266                if labels[i] != best_cluster {
267                    labels[i] = best_cluster;
268                    changed = true;
269                }
270            }
271
272            if !changed {
273                break;
274            }
275
276            // Update centroids
277            let mut cluster_counts = vec![0; k];
278            centroids.fill(0.0);
279
280            for i in 0..n_samples {
281                let cluster = labels[i];
282                cluster_counts[cluster] += 1;
283                for j in 0..n_features {
284                    centroids[[cluster, j]] += data[[i, j]];
285                }
286            }
287
288            for i in 0..k {
289                if cluster_counts[i] > 0 {
290                    for j in 0..n_features {
291                        centroids[[i, j]] /= cluster_counts[i] as f64;
292                    }
293                }
294            }
295        }
296
297        Ok(labels)
298    }
299
300    /// DBSCAN clustering implementation (simplified)
301    fn dbscan_clustering(
302        &self,
303        data: &Array2<f64>,
304        eps: f64,
305        min_samples: usize,
306    ) -> IntegrateResult<Vec<usize>> {
307        let n_samples = data.nrows();
308        let mut labels = vec![usize::MAX; n_samples]; // MAX means unclassified
309        let mut cluster_id = 0;
310
311        for i in 0..n_samples {
312            if labels[i] != usize::MAX {
313                continue; // Already processed
314            }
315
316            let neighbors = self.find_neighbors(data, i, eps);
317
318            if neighbors.len() < min_samples {
319                labels[i] = usize::MAX - 1; // Mark as noise
320            } else {
321                self.expand_cluster(
322                    data,
323                    i,
324                    &neighbors,
325                    cluster_id,
326                    eps,
327                    min_samples,
328                    &mut labels,
329                );
330                cluster_id += 1;
331            }
332        }
333
334        // Convert noise points to cluster 0 and increment others
335        for label in &mut labels {
336            if *label == usize::MAX - 1 {
337                *label = 0; // Noise cluster
338            } else if *label != usize::MAX {
339                *label += 1; // Shift cluster IDs
340            }
341        }
342
343        Ok(labels)
344    }
345
346    /// Hierarchical clustering implementation (simplified)
347    fn hierarchical_clustering(
348        &self,
349        data: &Array2<f64>,
350        n_clusters: usize,
351    ) -> IntegrateResult<Vec<usize>> {
352        let n_samples = data.nrows();
353
354        // Initialize each point as its own cluster
355        let mut clusters: Vec<Vec<usize>> = (0..n_samples).map(|i| vec![i]).collect();
356
357        // Compute initial distance matrix
358        let mut distance_matrix = Array2::zeros((n_samples, n_samples));
359        for i in 0..n_samples {
360            for j in i + 1..n_samples {
361                let dist = self.euclidean_distance(&data.row(i), &data.row(j));
362                distance_matrix[[i, j]] = dist;
363                distance_matrix[[j, i]] = dist;
364            }
365        }
366
367        // Agglomerative clustering
368        while clusters.len() > n_clusters {
369            let mut min_distance = f64::INFINITY;
370            let mut merge_i = 0;
371            let mut merge_j = 1;
372
373            // Find closest clusters
374            for i in 0..clusters.len() {
375                for j in i + 1..clusters.len() {
376                    let dist = self.cluster_distance(&clusters[i], &clusters[j], &distance_matrix);
377                    if dist < min_distance {
378                        min_distance = dist;
379                        merge_i = i;
380                        merge_j = j;
381                    }
382                }
383            }
384
385            // Merge clusters
386            let mut merged_cluster = clusters[merge_i].clone();
387            merged_cluster.extend(&clusters[merge_j]);
388
389            // Remove old clusters and add merged cluster
390            clusters.remove(merge_j); // Remove j first (higher index)
391            clusters.remove(merge_i);
392            clusters.push(merged_cluster);
393        }
394
395        // Assign labels
396        let mut labels = vec![0; n_samples];
397        for (cluster_id, cluster) in clusters.iter().enumerate() {
398            for &point_id in cluster {
399                labels[point_id] = cluster_id;
400            }
401        }
402
403        Ok(labels)
404    }
405
406    /// Helper functions
407    fn euclidean_distance(
408        &self,
409        a: &scirs2_core::ndarray::ArrayView1<f64>,
410        b: &scirs2_core::ndarray::ArrayView1<f64>,
411    ) -> f64 {
412        a.iter()
413            .zip(b.iter())
414            .map(|(&x, &y)| (x - y).powi(2))
415            .sum::<f64>()
416            .sqrt()
417    }
418
419    fn compute_eigendecomposition(
420        &self,
421        matrix: &Array2<f64>,
422    ) -> IntegrateResult<(Array1<f64>, Array2<f64>)> {
423        let n = matrix.nrows();
424
425        // Simplified eigendecomposition using power iteration
426        let mut eigenvalues = Array1::zeros(n.min(self.target_dimensions));
427        let mut eigenvectors = Array2::zeros((n, n.min(self.target_dimensions)));
428        let mut remaining_matrix = matrix.clone();
429
430        for k in 0..n.min(self.target_dimensions) {
431            // Power iteration for largest eigenvalue
432            let mut v = Array1::from_elem(n, 1.0 / (n as f64).sqrt());
433            let mut eigenvalue = 0.0;
434
435            for _ in 0..100 {
436                let v_new = remaining_matrix.dot(&v);
437                eigenvalue = v.dot(&v_new);
438                let norm = v_new.iter().map(|&x| x * x).sum::<f64>().sqrt();
439                if norm > 1e-10 {
440                    v = v_new / norm;
441                }
442            }
443
444            eigenvalues[k] = eigenvalue;
445            eigenvectors.column_mut(k).assign(&v);
446
447            // Deflate matrix
448            if eigenvalue.abs() > 1e-10 {
449                for i in 0..n {
450                    for j in 0..n {
451                        remaining_matrix[[i, j]] -= eigenvalue * v[i] * v[j];
452                    }
453                }
454            }
455        }
456
457        // Extend to full matrices
458        let mut full_eigenvalues = Array1::zeros(n);
459        let mut full_eigenvectors = Array2::zeros((n, n));
460
461        for i in 0..n.min(self.target_dimensions) {
462            full_eigenvalues[i] = eigenvalues[i];
463            full_eigenvectors
464                .column_mut(i)
465                .assign(&eigenvectors.column(i));
466        }
467
468        // Fill remaining with identity
469        for i in n.min(self.target_dimensions)..n {
470            let mut v = Array1::zeros(n);
471            v[i] = 1.0;
472            full_eigenvectors.column_mut(i).assign(&v);
473        }
474
475        Ok((full_eigenvalues, full_eigenvectors))
476    }
477
478    fn find_neighbors(&self, data: &Array2<f64>, point: usize, eps: f64) -> Vec<usize> {
479        let mut neighbors = Vec::new();
480        for i in 0..data.nrows() {
481            if i != point {
482                let dist = self.euclidean_distance(&data.row(point), &data.row(i));
483                if dist <= eps {
484                    neighbors.push(i);
485                }
486            }
487        }
488        neighbors
489    }
490
491    fn expand_cluster(
492        &self,
493        data: &Array2<f64>,
494        point: usize,
495        neighbors: &[usize],
496        cluster_id: usize,
497        eps: f64,
498        min_samples: usize,
499        labels: &mut [usize],
500    ) {
501        labels[point] = cluster_id;
502        let mut seed_set = neighbors.to_vec();
503        let mut i = 0;
504
505        while i < seed_set.len() {
506            let q = seed_set[i];
507
508            if labels[q] == usize::MAX - 1 {
509                // Change noise to border point
510                labels[q] = cluster_id;
511            } else if labels[q] == usize::MAX {
512                // Unclassified
513                labels[q] = cluster_id;
514                let q_neighbors = self.find_neighbors(data, q, eps);
515
516                if q_neighbors.len() >= min_samples {
517                    for &neighbor in &q_neighbors {
518                        if !seed_set.contains(&neighbor) {
519                            seed_set.push(neighbor);
520                        }
521                    }
522                }
523            }
524
525            i += 1;
526        }
527    }
528
529    fn cluster_distance(
530        &self,
531        cluster1: &[usize],
532        cluster2: &[usize],
533        distance_matrix: &Array2<f64>,
534    ) -> f64 {
535        // Single linkage (minimum distance)
536        let mut min_distance = f64::INFINITY;
537
538        for &i in cluster1 {
539            for &j in cluster2 {
540                let dist = distance_matrix[[i, j]];
541                if dist < min_distance {
542                    min_distance = dist;
543                }
544            }
545        }
546
547        min_distance
548    }
549}
550
551impl Default for MultiDimensionalVisualizer {
552    fn default() -> Self {
553        Self::new()
554    }
555}
556
557/// Animated visualization for time-series or dynamic systems
558#[derive(Debug, Clone)]
559pub struct AnimatedVisualizer {
560    /// Frame data
561    pub frames: Vec<PhaseSpacePlot>,
562    /// Animation settings
563    pub animation_settings: AnimationSettings,
564    /// Current frame index
565    pub current_frame: usize,
566}
567
568impl AnimatedVisualizer {
569    /// Create new animated visualizer
570    pub fn new() -> Self {
571        Self {
572            frames: Vec::new(),
573            animation_settings: AnimationSettings::default(),
574            current_frame: 0,
575        }
576    }
577
578    /// Create animation from ODE solution
579    pub fn create_animation_from_ode<F: crate::common::IntegrateFloat>(
580        &mut self,
581        ode_result: &ODEResult<F>,
582        x_index: usize,
583        y_index: usize,
584        frames_per_time_unit: usize,
585    ) -> IntegrateResult<()> {
586        let n_points = ode_result.t.len();
587        if n_points == 0 {
588            return Err(IntegrateError::ValueError("Empty ODE result".to_string()));
589        }
590
591        let n_vars = ode_result.y[0].len();
592        if x_index >= n_vars || y_index >= n_vars {
593            return Err(IntegrateError::ValueError(
594                "Variable index out of bounds".to_string(),
595            ));
596        }
597
598        // Create frames with progressive trajectory buildup
599        self.frames.clear();
600        let frame_step =
601            (n_points as f64 / (frames_per_time_unit * n_points) as f64).max(1.0) as usize;
602
603        for frame_end in (frame_step..=n_points).step_by(frame_step) {
604            let x: Vec<f64> = (0..frame_end)
605                .map(|i| ode_result.y[i][x_index].to_f64().unwrap_or(0.0))
606                .collect();
607
608            let y: Vec<f64> = (0..frame_end)
609                .map(|i| ode_result.y[i][y_index].to_f64().unwrap_or(0.0))
610                .collect();
611
612            let colors: Vec<f64> = (0..frame_end)
613                .map(|i| ode_result.t[i].to_f64().unwrap_or(0.0))
614                .collect();
615
616            let mut metadata = PlotMetadata::default();
617            metadata.title = format!("Animation Frame {}", self.frames.len() + 1);
618            metadata.xlabel = format!("Variable {x_index}");
619            metadata.ylabel = format!("Variable {y_index}");
620
621            self.frames.push(PhaseSpacePlot {
622                x,
623                y,
624                colors: Some(colors),
625                metadata,
626            });
627        }
628
629        Ok(())
630    }
631
632    /// Get current frame
633    pub fn current_frame(&self) -> Option<&PhaseSpacePlot> {
634        self.frames.get(self.current_frame)
635    }
636
637    /// Advance to next frame
638    pub fn next_frame(&mut self) -> bool {
639        if self.current_frame + 1 < self.frames.len() {
640            self.current_frame += 1;
641            true
642        } else if self.animation_settings.loop_animation {
643            self.current_frame = 0;
644            true
645        } else {
646            false
647        }
648    }
649
650    /// Go to previous frame
651    pub fn previous_frame(&mut self) -> bool {
652        if self.current_frame > 0 {
653            self.current_frame -= 1;
654            true
655        } else if self.animation_settings.loop_animation {
656            self.current_frame = self.frames.len().saturating_sub(1);
657            true
658        } else {
659            false
660        }
661    }
662
663    /// Reset to first frame
664    pub fn reset(&mut self) {
665        self.current_frame = 0;
666    }
667}
668
669impl Default for AnimatedVisualizer {
670    fn default() -> Self {
671        Self::new()
672    }
673}
674
675/// Create multi-dimensional visualization
676pub fn advanced_visualization(
677    data: &Array2<f64>,
678    method: DimensionReductionMethod,
679    target_dims: usize,
680) -> IntegrateResult<HighDimensionalPlot> {
681    let visualizer = MultiDimensionalVisualizer {
682        reduction_method: method,
683        target_dimensions: target_dims,
684        clustering_method: ClusteringMethod::None,
685    };
686
687    visualizer.visualize_high_dimensional_data(data, None)
688}
689
690/// Create interactive 3D visualization
691pub fn advanced_interactive_3d(
692    data: &Array2<f64>,
693    labels: Option<&Array1<usize>>,
694) -> IntegrateResult<HighDimensionalPlot> {
695    let visualizer = MultiDimensionalVisualizer {
696        reduction_method: DimensionReductionMethod::PCA,
697        target_dimensions: 3,
698        clustering_method: ClusteringMethod::KMeans { k: 3 },
699    };
700
701    visualizer.visualize_high_dimensional_data(data, labels)
702}