scirs2_integrate/visualization/
engine.rs

1//! Core visualization engine for numerical integration
2//!
3//! This module provides the main VisualizationEngine struct and its implementation
4//! for creating various types of plots from numerical integration results.
5
6use super::types::*;
7use crate::analysis::{BasinAnalysis, BifurcationPoint};
8use crate::error::{IntegrateError, IntegrateResult};
9use crate::ode::ODEResult;
10use scirs2_core::ndarray::{Array1, Array2};
11
12/// Main visualization engine for creating plots from numerical data
13#[derive(Debug, Clone)]
14pub struct VisualizationEngine {
15    /// Output format preference
16    pub output_format: OutputFormat,
17    /// Color scheme
18    pub color_scheme: ColorScheme,
19    /// Figure size
20    pub figure_size: (f64, f64),
21}
22
23impl Default for VisualizationEngine {
24    fn default() -> Self {
25        Self {
26            output_format: OutputFormat::ASCII,
27            color_scheme: ColorScheme::Viridis,
28            figure_size: (800.0, 600.0),
29        }
30    }
31}
32
33impl VisualizationEngine {
34    /// Create a new visualization engine
35    pub fn new() -> Self {
36        Default::default()
37    }
38
39    /// Create phase space plot from ODE result
40    pub fn create_phase_spaceplot<F: crate::common::IntegrateFloat>(
41        &self,
42        ode_result: &ODEResult<F>,
43        x_index: usize,
44        y_index: usize,
45    ) -> IntegrateResult<PhaseSpacePlot> {
46        let n_points = ode_result.t.len();
47        let n_vars = if !ode_result.y.is_empty() {
48            ode_result.y[0].len()
49        } else {
50            0
51        };
52
53        if x_index >= n_vars || y_index >= n_vars {
54            return Err(IntegrateError::ValueError(
55                "Variable _index out of bounds".to_string(),
56            ));
57        }
58
59        let x: Vec<f64> = (0..n_points)
60            .map(|i| ode_result.y[i][x_index].to_f64().unwrap_or(0.0))
61            .collect();
62
63        let y: Vec<f64> = (0..n_points)
64            .map(|i| ode_result.y[i][y_index].to_f64().unwrap_or(0.0))
65            .collect();
66
67        // Color by time for trajectory visualization
68        let colors: Vec<f64> = ode_result
69            .t
70            .iter()
71            .map(|t| t.to_f64().unwrap_or(0.0))
72            .collect();
73
74        let mut metadata = PlotMetadata::default();
75        metadata.title = "Phase Space Plot".to_string();
76        metadata.xlabel = format!("Variable {x_index}");
77        metadata.ylabel = format!("Variable {y_index}");
78
79        Ok(PhaseSpacePlot {
80            x,
81            y,
82            colors: Some(colors),
83            metadata,
84        })
85    }
86
87    /// Create bifurcation diagram from analysis results
88    pub fn create_bifurcation_diagram(
89        &self,
90        bifurcation_points: &[BifurcationPoint],
91        parameter_range: (f64, f64),
92        n_points: usize,
93    ) -> IntegrateResult<BifurcationDiagram> {
94        let mut parameters = Vec::new();
95        let mut states = Vec::new();
96        let mut stability = Vec::new();
97
98        // Create parameter grid
99        let param_step = (parameter_range.1 - parameter_range.0) / (n_points - 1) as f64;
100        for i in 0..n_points {
101            let param = parameter_range.0 + i as f64 * param_step;
102            parameters.push(param);
103
104            // Find corresponding state (simplified)
105            let mut found = false;
106            for bif_point in bifurcation_points {
107                if (bif_point.parameter_value - param).abs() < param_step {
108                    states.push(bif_point.state.to_vec());
109                    // Simplified stability check based on eigenvalues
110                    let is_stable = bif_point.eigenvalues.iter().all(|eig| eig.re < 0.0);
111                    stability.push(is_stable);
112                    found = true;
113                    break;
114                }
115            }
116
117            if !found {
118                states.push(vec![0.0]); // Default value
119                stability.push(true);
120            }
121        }
122
123        Ok(BifurcationDiagram {
124            parameters,
125            states,
126            stability,
127            bifurcation_points: bifurcation_points.to_vec(),
128        })
129    }
130
131    /// Create vector field plot for 2D dynamical systems
132    pub fn create_vector_fieldplot<F>(
133        &self,
134        system: F,
135        x_range: (f64, f64),
136        y_range: (f64, f64),
137        grid_size: (usize, usize),
138    ) -> IntegrateResult<VectorFieldPlot>
139    where
140        F: Fn(&Array1<f64>) -> Array1<f64>,
141    {
142        let (nx, ny) = grid_size;
143        let mut x_grid = Array2::zeros((ny, nx));
144        let mut y_grid = Array2::zeros((ny, nx));
145        let mut u = Array2::zeros((ny, nx));
146        let mut v = Array2::zeros((ny, nx));
147        let mut magnitude = Array2::zeros((ny, nx));
148
149        let dx = (x_range.1 - x_range.0) / (nx - 1) as f64;
150        let dy = (y_range.1 - y_range.0) / (ny - 1) as f64;
151
152        for i in 0..ny {
153            for j in 0..nx {
154                let x = x_range.0 + j as f64 * dx;
155                let y = y_range.0 + i as f64 * dy;
156
157                x_grid[[i, j]] = x;
158                y_grid[[i, j]] = y;
159
160                let state = Array1::from_vec(vec![x, y]);
161                let derivative = system(&state);
162
163                if derivative.len() >= 2 {
164                    u[[i, j]] = derivative[0];
165                    v[[i, j]] = derivative[1];
166                    magnitude[[i, j]] = (derivative[0].powi(2) + derivative[1].powi(2)).sqrt();
167                }
168            }
169        }
170
171        let mut metadata = PlotMetadata::default();
172        metadata.title = "Vector Field Plot".to_string();
173        metadata.xlabel = "X".to_string();
174        metadata.ylabel = "Y".to_string();
175
176        Ok(VectorFieldPlot {
177            x_grid,
178            y_grid,
179            u,
180            v,
181            magnitude,
182            metadata,
183        })
184    }
185
186    /// Create basin of attraction visualization
187    pub fn create_basinplot(_basinanalysis: &BasinAnalysis) -> IntegrateResult<HeatMapPlot> {
188        let grid_size = _basinanalysis.attractor_indices.nrows();
189        let x = Array1::linspace(0.0, 1.0, grid_size);
190        let y = Array1::linspace(0.0, 1.0, grid_size);
191
192        // Convert attractor indices to f64 for plotting
193        let z = _basinanalysis.attractor_indices.mapv(|x| x as f64);
194
195        let mut metadata = PlotMetadata::default();
196        metadata.title = "Basin of Attraction".to_string();
197        metadata.xlabel = "X".to_string();
198        metadata.ylabel = "Y".to_string();
199
200        Ok(HeatMapPlot { x, y, z, metadata })
201    }
202
203    /// Generate ASCII art representation of a 2D plot
204    pub fn render_asciiplot(data: &[(f64, f64)], width: usize, height: usize) -> String {
205        if data.is_empty() {
206            return "No data to plot".to_string();
207        }
208
209        // Find data bounds
210        let x_min = data.iter().map(|(x_, _)| *x_).fold(f64::INFINITY, f64::min);
211        let x_max = data
212            .iter()
213            .map(|(x_, _)| *x_)
214            .fold(f64::NEG_INFINITY, f64::max);
215        let y_min = data.iter().map(|(_, y)| *y).fold(f64::INFINITY, f64::min);
216        let y_max = data
217            .iter()
218            .map(|(_, y)| *y)
219            .fold(f64::NEG_INFINITY, f64::max);
220
221        // Create character grid
222        let mut grid = vec![vec![' '; width]; height];
223
224        // Map data points to grid
225        for (x, y) in data {
226            let i = ((y - y_min) / (y_max - y_min) * (height - 1) as f64) as usize;
227            let j = ((x - x_min) / (x_max - x_min) * (width - 1) as f64) as usize;
228
229            if i < height && j < width {
230                grid[height - 1 - i][j] = '*'; // Flip y-axis for proper orientation
231            }
232        }
233
234        // Convert grid to string
235        let mut result = String::new();
236        for row in grid {
237            result.push_str(&row.iter().collect::<String>());
238            result.push('\n');
239        }
240
241        // Add axis labels
242        result.push_str(&format!("\nX range: [{x_min:.3}, {x_max:.3}]\n"));
243        result.push_str(&format!("Y range: [{y_min:.3}, {y_max:.3}]\n"));
244
245        result
246    }
247
248    /// Export plot data to CSV format
249    pub fn export_csv(plot: &PhaseSpacePlot) -> IntegrateResult<String> {
250        let mut csv = String::new();
251
252        // Header
253        csv.push_str("x,y");
254        if plot.colors.is_some() {
255            csv.push_str(",color");
256        }
257        csv.push('\n');
258
259        // Data
260        for i in 0..plot.x.len() {
261            csv.push_str(&format!("{},{}", plot.x[i], plot.y[i]));
262            if let Some(ref colors) = plot.colors {
263                csv.push_str(&format!(",{}", colors[i]));
264            }
265            csv.push('\n');
266        }
267
268        Ok(csv)
269    }
270
271    /// Create learning curve plot for optimization algorithms
272    pub fn create_learning_curve(
273        &self,
274        iterations: &[usize],
275        values: &[f64],
276        title: &str,
277    ) -> IntegrateResult<PhaseSpacePlot> {
278        let x: Vec<f64> = iterations.iter().map(|&i| i as f64).collect();
279        let y = values.to_vec();
280
281        let mut metadata = PlotMetadata::default();
282        metadata.title = title.to_string();
283        metadata.xlabel = "Iteration".to_string();
284        metadata.ylabel = "Value".to_string();
285
286        Ok(PhaseSpacePlot {
287            x,
288            y,
289            colors: None,
290            metadata,
291        })
292    }
293
294    /// Create convergence analysis plot
295    pub fn create_convergenceplot(
296        &self,
297        step_sizes: &[f64],
298        errors: &[f64],
299        theoretical_order: f64,
300    ) -> IntegrateResult<PhaseSpacePlot> {
301        let x: Vec<f64> = step_sizes.iter().map(|h| h.log10()).collect();
302        let y: Vec<f64> = errors.iter().map(|e| e.log10()).collect();
303
304        let mut metadata = PlotMetadata::default();
305        metadata.title = "Convergence Analysis".to_string();
306        metadata.xlabel = "log10(step size)".to_string();
307        metadata.ylabel = "log10(error)".to_string();
308        metadata.annotations.insert(
309            "theoretical_slope".to_string(),
310            theoretical_order.to_string(),
311        );
312
313        Ok(PhaseSpacePlot {
314            x,
315            y,
316            colors: None,
317            metadata,
318        })
319    }
320
321    /// Create interactive parameter space exploration plot
322    pub fn create_parameter_exploration(
323        &self,
324        param_ranges: &[(f64, f64)], // [(min1, max1), (min2, max2), ...]
325        param_names: &[String],
326        evaluation_function: &dyn Fn(&[f64]) -> f64,
327        resolution: usize,
328    ) -> IntegrateResult<ParameterExplorationPlot> {
329        if param_ranges.len() != 2 {
330            return Err(IntegrateError::ValueError(
331                "Parameter exploration currently supports only 2D parameter spaces".to_string(),
332            ));
333        }
334
335        let (x_min, x_max) = param_ranges[0];
336        let (y_min, y_max) = param_ranges[1];
337
338        let dx = (x_max - x_min) / (resolution - 1) as f64;
339        let dy = (y_max - y_min) / (resolution - 1) as f64;
340
341        let mut x_grid = Array2::zeros((resolution, resolution));
342        let mut y_grid = Array2::zeros((resolution, resolution));
343        let mut z_values = Array2::zeros((resolution, resolution));
344
345        for i in 0..resolution {
346            for j in 0..resolution {
347                let x = x_min + i as f64 * dx;
348                let y = y_min + j as f64 * dy;
349
350                x_grid[[i, j]] = x;
351                y_grid[[i, j]] = y;
352                z_values[[i, j]] = evaluation_function(&[x, y]);
353            }
354        }
355
356        let mut metadata = PlotMetadata::default();
357        metadata.title = "Parameter Space Exploration".to_string();
358        metadata.xlabel = param_names
359            .get(0)
360            .cloned()
361            .unwrap_or_else(|| "Parameter 1".to_string());
362        metadata.ylabel = param_names
363            .get(1)
364            .cloned()
365            .unwrap_or_else(|| "Parameter 2".to_string());
366
367        Ok(ParameterExplorationPlot {
368            x_grid,
369            y_grid,
370            z_values,
371            param_ranges: param_ranges.to_vec(),
372            param_names: param_names.to_vec(),
373            metadata,
374        })
375    }
376
377    /// Create real-time bifurcation diagram
378    pub fn create_real_time_bifurcation(
379        &self,
380        system: &dyn Fn(&Array1<f64>, f64) -> Array1<f64>,
381        parameter_range: (f64, f64),
382        initial_conditions: &[Array1<f64>],
383        transient_steps: usize,
384        record_steps: usize,
385    ) -> IntegrateResult<RealTimeBifurcationPlot> {
386        let n_params = 200;
387        let param_step = (parameter_range.1 - parameter_range.0) / (n_params - 1) as f64;
388
389        let mut parameter_values = Vec::new();
390        let mut attractordata = Vec::new();
391        let mut stabilitydata = Vec::new();
392
393        for i in 0..n_params {
394            let param = parameter_range.0 + i as f64 * param_step;
395            parameter_values.push(param);
396
397            let mut param_attractors = Vec::new();
398            let mut param_stability = Vec::new();
399
400            for initial in initial_conditions {
401                // Evolve system to let transients die out
402                let mut state = initial.clone();
403                for _ in 0..transient_steps {
404                    let derivative = system(&state, param);
405                    state += &(&derivative * 0.01); // Small time step
406                }
407
408                // Record attractor points
409                let mut attractor_points = Vec::new();
410                let mut local_maxima = Vec::new();
411
412                for step in 0..record_steps {
413                    let derivative = system(&state, param);
414                    let derivative_scaled = &derivative * 0.01;
415                    let new_state = &state + &derivative_scaled;
416
417                    // Simple local maxima detection for period identification
418                    if step > 2
419                        && new_state[0] > state[0]
420                        && state[0] > (state.clone() - &derivative_scaled)[0]
421                    {
422                        local_maxima.push(state[0]);
423                    }
424
425                    attractor_points.push(state[0]);
426                    state = new_state;
427                }
428
429                // Determine stability based on attractor behavior
430                let stability = if local_maxima.len() == 1 {
431                    AttractorStability::FixedPoint
432                } else if local_maxima.len() == 2 {
433                    AttractorStability::PeriodTwo
434                } else if local_maxima.len() > 2 && local_maxima.len() < 10 {
435                    AttractorStability::Periodic(local_maxima.len())
436                } else {
437                    AttractorStability::Chaotic
438                };
439
440                param_attractors.push(attractor_points);
441                param_stability.push(stability);
442            }
443
444            attractordata.push(param_attractors);
445            stabilitydata.push(param_stability);
446        }
447
448        let mut metadata = PlotMetadata::default();
449        metadata.title = "Real-time Bifurcation Diagram".to_string();
450        metadata.xlabel = "Parameter".to_string();
451        metadata.ylabel = "Attractor Values".to_string();
452
453        Ok(RealTimeBifurcationPlot {
454            parameter_values,
455            attractor_data: attractordata,
456            stability_data: stabilitydata,
457            parameter_range,
458            metadata,
459        })
460    }
461
462    /// Create 3D phase space trajectory
463    pub fn create_3d_phase_space<F: crate::common::IntegrateFloat>(
464        &self,
465        ode_result: &ODEResult<F>,
466        x_index: usize,
467        y_index: usize,
468        z_index: usize,
469    ) -> IntegrateResult<PhaseSpace3D> {
470        let n_points = ode_result.t.len();
471        let n_vars = if !ode_result.y.is_empty() {
472            ode_result.y[0].len()
473        } else {
474            0
475        };
476
477        if x_index >= n_vars || y_index >= n_vars || z_index >= n_vars {
478            return Err(IntegrateError::ValueError(
479                "Variable _index out of bounds".to_string(),
480            ));
481        }
482
483        let x: Vec<f64> = (0..n_points)
484            .map(|i| ode_result.y[i][x_index].to_f64().unwrap_or(0.0))
485            .collect();
486
487        let y: Vec<f64> = (0..n_points)
488            .map(|i| ode_result.y[i][y_index].to_f64().unwrap_or(0.0))
489            .collect();
490
491        let z: Vec<f64> = (0..n_points)
492            .map(|i| ode_result.y[i][z_index].to_f64().unwrap_or(0.0))
493            .collect();
494
495        // Color by time or by distance from initial point
496        let colors: Vec<f64> = ode_result
497            .t
498            .iter()
499            .map(|t| t.to_f64().unwrap_or(0.0))
500            .collect();
501
502        let mut metadata = PlotMetadata::default();
503        metadata.title = "3D Phase Space Trajectory".to_string();
504        metadata.xlabel = format!("Variable {x_index}");
505        metadata.ylabel = format!("Variable {y_index}");
506        metadata
507            .annotations
508            .insert("zlabel".to_string(), format!("Variable {z_index}"));
509
510        Ok(PhaseSpace3D {
511            x,
512            y,
513            z,
514            colors: Some(colors),
515            metadata,
516        })
517    }
518
519    /// Create interactive sensitivity analysis plot
520    pub fn create_sensitivity_analysis(
521        &self,
522        base_parameters: &[f64],
523        parameter_names: &[String],
524        sensitivity_function: &dyn Fn(&[f64]) -> f64,
525        perturbation_percent: f64,
526    ) -> IntegrateResult<SensitivityPlot> {
527        let n_params = base_parameters.len();
528        let mut sensitivities = Vec::with_capacity(n_params);
529        let base_value = sensitivity_function(base_parameters);
530
531        for i in 0..n_params {
532            let mut perturbed_params = base_parameters.to_vec();
533            let perturbation = base_parameters[i] * perturbation_percent / 100.0;
534
535            // Forward difference
536            perturbed_params[i] += perturbation;
537            let perturbed_value = sensitivity_function(&perturbed_params);
538
539            // Calculate normalized sensitivity
540            let sensitivity = if perturbation.abs() > 1e-12 {
541                (perturbed_value - base_value) / perturbation * base_parameters[i] / base_value
542            } else {
543                0.0
544            };
545
546            sensitivities.push(sensitivity);
547        }
548
549        let mut metadata = PlotMetadata::default();
550        metadata.title = "Parameter Sensitivity Analysis".to_string();
551        metadata.xlabel = "Parameters".to_string();
552        metadata.ylabel = "Normalized Sensitivity".to_string();
553
554        Ok(SensitivityPlot {
555            parameter_names: parameter_names.to_vec(),
556            sensitivities,
557            base_parameters: base_parameters.to_vec(),
558            base_value,
559            metadata,
560        })
561    }
562}