scirs2_integrate/visualization/
engine.rs1use super::types::*;
7use crate::analysis::{BasinAnalysis, BifurcationPoint};
8use crate::error::{IntegrateError, IntegrateResult};
9use crate::ode::ODEResult;
10use scirs2_core::ndarray::{Array1, Array2};
11
12#[derive(Debug, Clone)]
14pub struct VisualizationEngine {
15 pub output_format: OutputFormat,
17 pub color_scheme: ColorScheme,
19 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 pub fn new() -> Self {
36 Default::default()
37 }
38
39 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 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 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 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 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 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]); 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 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 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 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 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 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 let mut grid = vec![vec![' '; width]; height];
223
224 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] = '*'; }
232 }
233
234 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 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 pub fn export_csv(plot: &PhaseSpacePlot) -> IntegrateResult<String> {
250 let mut csv = String::new();
251
252 csv.push_str("x,y");
254 if plot.colors.is_some() {
255 csv.push_str(",color");
256 }
257 csv.push('\n');
258
259 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 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 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 pub fn create_parameter_exploration(
323 &self,
324 param_ranges: &[(f64, f64)], 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 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 let mut state = initial.clone();
403 for _ in 0..transient_steps {
404 let derivative = system(&state, param);
405 state += &(&derivative * 0.01); }
407
408 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 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 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 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 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 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 perturbed_params[i] += perturbation;
537 let perturbed_value = sensitivity_function(&perturbed_params);
538
539 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}