scirs2_integrate/visualization/
interactive.rs

1//! Interactive parameter exploration tools
2//!
3//! This module provides interactive tools for exploring parameter spaces
4//! and generating bifurcation diagrams for dynamical systems.
5
6use super::types::*;
7use crate::analysis::BifurcationPoint;
8use crate::error::{IntegrateError, IntegrateResult};
9use scirs2_core::ndarray::Array1;
10use scirs2_core::random::Rng;
11
12/// Interactive parameter space explorer
13#[derive(Debug, Clone)]
14pub struct InteractiveParameterExplorer {
15    /// Parameter space dimensions
16    pub param_dimensions: usize,
17    /// Parameter bounds
18    pub param_bounds: Vec<(f64, f64)>,
19    /// Number of samples per dimension
20    pub samples_per_dim: usize,
21    /// Current parameter values
22    pub currentparams: Array1<f64>,
23    /// Parameter history
24    pub param_history: Vec<Array1<f64>>,
25    /// System response cache
26    pub response_cache: std::collections::HashMap<String, Array1<f64>>,
27}
28
29impl InteractiveParameterExplorer {
30    /// Create new parameter explorer
31    pub fn new(
32        param_dimensions: usize,
33        param_bounds: Vec<(f64, f64)>,
34        samples_per_dim: usize,
35    ) -> Self {
36        let currentparams = Array1::from_vec(
37            param_bounds
38                .iter()
39                .map(|(min, max)| (min + max) / 2.0)
40                .collect(),
41        );
42
43        Self {
44            param_dimensions,
45            param_bounds,
46            samples_per_dim,
47            currentparams,
48            param_history: Vec::new(),
49            response_cache: std::collections::HashMap::new(),
50        }
51    }
52
53    /// Explore parameter space with interactive visualization
54    pub fn explore_parameter_space<F>(
55        &mut self,
56        system_function: F,
57        exploration_method: ExplorationMethod,
58    ) -> IntegrateResult<ParameterExplorationResult>
59    where
60        F: Fn(&Array1<f64>) -> IntegrateResult<Array1<f64>>,
61    {
62        match exploration_method {
63            ExplorationMethod::GridScan => self.grid_scan_exploration(&system_function),
64            ExplorationMethod::RandomSampling => self.random_sampling_exploration(&system_function),
65            ExplorationMethod::AdaptiveSampling => {
66                self.adaptive_sampling_exploration(&system_function)
67            }
68            ExplorationMethod::GradientGuided => self.gradient_guided_exploration(&system_function),
69        }
70    }
71
72    /// Grid-based parameter exploration
73    fn grid_scan_exploration<F>(
74        &mut self,
75        system_function: &F,
76    ) -> IntegrateResult<ParameterExplorationResult>
77    where
78        F: Fn(&Array1<f64>) -> IntegrateResult<Array1<f64>>,
79    {
80        let mut exploration_points = Vec::new();
81        let mut response_values = Vec::new();
82        let mut parameter_grid = Vec::new();
83
84        // Generate grid points
85        let grid_indices = self.generate_grid_indices();
86
87        for indices in grid_indices {
88            let params = self.indices_to_parameters(&indices);
89            parameter_grid.push(params.clone());
90
91            // Evaluate system at this parameter point
92            match system_function(&params) {
93                Ok(response) => {
94                    exploration_points.push(params.clone());
95                    response_values.push(response.clone());
96
97                    // Cache result
98                    let key = self.params_to_cache_key(&params);
99                    self.response_cache.insert(key, response);
100                }
101                Err(_) => {
102                    // Skip invalid parameter combinations
103                    continue;
104                }
105            }
106        }
107
108        Ok(ParameterExplorationResult {
109            exploration_points,
110            response_values: response_values.clone(),
111            parameter_grid,
112            convergence_history: self.param_history.clone(),
113            exploration_method: ExplorationMethod::GridScan,
114            optimization_metrics: self.compute_exploration_metrics(&response_values)?,
115        })
116    }
117
118    /// Random sampling exploration
119    fn random_sampling_exploration<F>(
120        &mut self,
121        system_function: &F,
122    ) -> IntegrateResult<ParameterExplorationResult>
123    where
124        F: Fn(&Array1<f64>) -> IntegrateResult<Array1<f64>>,
125    {
126        use scirs2_core::random::Rng;
127        let mut rng = scirs2_core::random::rng();
128
129        let mut exploration_points = Vec::new();
130        let mut response_values = Vec::new();
131        let mut parameter_grid = Vec::new();
132
133        let total_samples = self.samples_per_dim.pow(self.param_dimensions as u32);
134
135        for _ in 0..total_samples {
136            let mut params = Array1::zeros(self.param_dimensions);
137
138            for i in 0..self.param_dimensions {
139                let (min, max) = self.param_bounds[i];
140                params[i] = rng.random::<f64>() * (max - min) + min;
141            }
142
143            parameter_grid.push(params.clone());
144
145            match system_function(&params) {
146                Ok(response) => {
147                    exploration_points.push(params.clone());
148                    response_values.push(response.clone());
149
150                    let key = self.params_to_cache_key(&params);
151                    self.response_cache.insert(key, response);
152                }
153                Err(_) => continue,
154            }
155        }
156
157        Ok(ParameterExplorationResult {
158            exploration_points,
159            response_values: response_values.clone(),
160            parameter_grid,
161            convergence_history: self.param_history.clone(),
162            exploration_method: ExplorationMethod::RandomSampling,
163            optimization_metrics: self.compute_exploration_metrics(&response_values)?,
164        })
165    }
166
167    /// Adaptive sampling based on response characteristics
168    fn adaptive_sampling_exploration<F>(
169        &mut self,
170        system_function: &F,
171    ) -> IntegrateResult<ParameterExplorationResult>
172    where
173        F: Fn(&Array1<f64>) -> IntegrateResult<Array1<f64>>,
174    {
175        let mut exploration_points = Vec::new();
176        let mut response_values = Vec::new();
177        let mut parameter_grid = Vec::new();
178
179        // Start with a coarse grid
180        let coarse_samples = (self.samples_per_dim as f64).sqrt() as usize;
181        let mut currentresolution = coarse_samples;
182
183        // Initial coarse sampling
184        let initial_grid = self.generate_coarse_grid(coarse_samples);
185
186        for params in &initial_grid {
187            parameter_grid.push(params.clone());
188
189            match system_function(params) {
190                Ok(response) => {
191                    exploration_points.push(params.clone());
192                    response_values.push(response);
193                }
194                Err(_) => continue,
195            }
196        }
197
198        // Adaptive refinement
199        while currentresolution < self.samples_per_dim {
200            let refinement_candidates =
201                self.identify_refinement_regions(&exploration_points, &response_values)?;
202
203            for region in refinement_candidates {
204                let refined_points = self.refine_region(&region, currentresolution * 2);
205
206                for params in refined_points {
207                    parameter_grid.push(params.clone());
208
209                    match system_function(&params) {
210                        Ok(response) => {
211                            exploration_points.push(params.clone());
212                            response_values.push(response);
213                        }
214                        Err(_) => continue,
215                    }
216                }
217            }
218
219            currentresolution *= 2;
220        }
221
222        Ok(ParameterExplorationResult {
223            exploration_points,
224            response_values: response_values.clone(),
225            parameter_grid,
226            convergence_history: self.param_history.clone(),
227            exploration_method: ExplorationMethod::AdaptiveSampling,
228            optimization_metrics: self.compute_exploration_metrics(&response_values)?,
229        })
230    }
231
232    /// Gradient-guided exploration
233    fn gradient_guided_exploration<F>(
234        &mut self,
235        system_function: &F,
236    ) -> IntegrateResult<ParameterExplorationResult>
237    where
238        F: Fn(&Array1<f64>) -> IntegrateResult<Array1<f64>>,
239    {
240        let mut exploration_points = Vec::new();
241        let mut response_values = Vec::new();
242        let mut parameter_grid = Vec::new();
243
244        // Start from current parameters
245        let mut currentparams = self.currentparams.clone();
246        exploration_points.push(currentparams.clone());
247        parameter_grid.push(currentparams.clone());
248
249        let initial_response = system_function(&currentparams)?;
250        response_values.push(initial_response.clone());
251
252        let learning_rate = 0.01;
253        let max_iterations = 100;
254        let tolerance = 1e-6;
255
256        for iteration in 0..max_iterations {
257            // Compute numerical gradient
258            let gradient = self.compute_numerical_gradient(system_function, &currentparams)?;
259
260            // Update parameters along gradient
261            let gradient_norm = gradient.iter().map(|&g| g * g).sum::<f64>().sqrt();
262
263            if gradient_norm < tolerance {
264                break;
265            }
266
267            // Gradient ascent step (maximize response)
268            for i in 0..self.param_dimensions {
269                currentparams[i] += learning_rate * gradient[i] / gradient_norm;
270
271                // Clamp to bounds
272                let (min, max) = self.param_bounds[i];
273                currentparams[i] = currentparams[i].max(min).min(max);
274            }
275
276            // Evaluate at new point
277            match system_function(&currentparams) {
278                Ok(response) => {
279                    exploration_points.push(currentparams.clone());
280                    parameter_grid.push(currentparams.clone());
281                    response_values.push(response);
282                    self.param_history.push(currentparams.clone());
283                }
284                Err(_) => {
285                    // If evaluation fails, take smaller step
286                    currentparams = exploration_points.last().unwrap().clone();
287                    break;
288                }
289            }
290
291            // Convergence check
292            if iteration > 0 {
293                let current_response = response_values.last().unwrap();
294                let prev_response = &response_values[response_values.len() - 2];
295
296                let response_change = current_response
297                    .iter()
298                    .zip(prev_response.iter())
299                    .map(|(a, b)| (a - b).abs())
300                    .fold(0.0, f64::max);
301
302                if response_change < tolerance {
303                    break;
304                }
305            }
306        }
307
308        self.currentparams = currentparams;
309
310        Ok(ParameterExplorationResult {
311            exploration_points,
312            response_values: response_values.clone(),
313            parameter_grid,
314            convergence_history: self.param_history.clone(),
315            exploration_method: ExplorationMethod::GradientGuided,
316            optimization_metrics: self.compute_exploration_metrics(&response_values)?,
317        })
318    }
319
320    // Helper methods
321    fn generate_grid_indices(&self) -> Vec<Vec<usize>> {
322        let mut indices = Vec::new();
323        let mut current_index = vec![0; self.param_dimensions];
324
325        loop {
326            indices.push(current_index.clone());
327
328            // Increment counter
329            let mut carry = 1;
330            for i in (0..self.param_dimensions).rev() {
331                current_index[i] += carry;
332                if current_index[i] < self.samples_per_dim {
333                    carry = 0;
334                    break;
335                } else {
336                    current_index[i] = 0;
337                }
338            }
339
340            if carry == 1 {
341                break;
342            }
343        }
344
345        indices
346    }
347
348    fn indices_to_parameters(&self, indices: &[usize]) -> Array1<f64> {
349        let mut params = Array1::zeros(self.param_dimensions);
350
351        for i in 0..self.param_dimensions {
352            let (min, max) = self.param_bounds[i];
353            let fraction = indices[i] as f64 / (self.samples_per_dim - 1) as f64;
354            params[i] = min + fraction * (max - min);
355        }
356
357        params
358    }
359
360    fn params_to_cache_key(&self, params: &Array1<f64>) -> String {
361        params
362            .iter()
363            .map(|&p| format!("{p:.6}"))
364            .collect::<Vec<_>>()
365            .join(",")
366    }
367
368    fn generate_coarse_grid(&self, resolution: usize) -> Vec<Array1<f64>> {
369        let mut grid = Vec::new();
370        let mut indices = vec![0; self.param_dimensions];
371
372        loop {
373            let mut params = Array1::zeros(self.param_dimensions);
374
375            for i in 0..self.param_dimensions {
376                let (min, max) = self.param_bounds[i];
377                let fraction = indices[i] as f64 / (resolution - 1) as f64;
378                params[i] = min + fraction * (max - min);
379            }
380
381            grid.push(params);
382
383            // Increment
384            let mut carry = 1;
385            for i in (0..self.param_dimensions).rev() {
386                indices[i] += carry;
387                if indices[i] < resolution {
388                    carry = 0;
389                    break;
390                } else {
391                    indices[i] = 0;
392                }
393            }
394
395            if carry == 1 {
396                break;
397            }
398        }
399
400        grid
401    }
402
403    fn identify_refinement_regions(
404        &self,
405        points: &[Array1<f64>],
406        responses: &[Array1<f64>],
407    ) -> IntegrateResult<Vec<ParameterRegion>> {
408        let mut regions = Vec::new();
409
410        // Simple heuristic: identify regions with high response variance
411        if responses.len() < 2 {
412            return Ok(regions);
413        }
414
415        // Find best response point for refinement
416        let best_idx = responses
417            .iter()
418            .enumerate()
419            .max_by(|(_, a), (_, b)| {
420                let a_norm = a.iter().map(|&x| x * x).sum::<f64>().sqrt();
421                let b_norm = b.iter().map(|&x| x * x).sum::<f64>().sqrt();
422                a_norm
423                    .partial_cmp(&b_norm)
424                    .unwrap_or(std::cmp::Ordering::Equal)
425            })
426            .map(|(i, _)| i)
427            .unwrap_or(0);
428
429        let center = points[best_idx].clone();
430        let radius = 0.1; // 10% of parameter range
431
432        regions.push(ParameterRegion { center, radius });
433
434        Ok(regions)
435    }
436
437    fn refine_region(&self, region: &ParameterRegion, resolution: usize) -> Vec<Array1<f64>> {
438        let mut refined_points = Vec::new();
439
440        // Generate points within the _region
441        for _ in 0..resolution {
442            let mut rng = scirs2_core::random::rng();
443            let mut point = region.center.clone();
444
445            for i in 0..self.param_dimensions {
446                let (min, max) = self.param_bounds[i];
447                let range = (max - min) * region.radius;
448                let offset = (rng.random::<f64>() - 0.5) * 2.0 * range;
449                point[i] = (point[i] + offset).max(min).min(max);
450            }
451
452            refined_points.push(point);
453        }
454
455        refined_points
456    }
457
458    fn compute_numerical_gradient<F>(
459        &self,
460        system_function: &F,
461        params: &Array1<f64>,
462    ) -> IntegrateResult<Array1<f64>>
463    where
464        F: Fn(&Array1<f64>) -> IntegrateResult<Array1<f64>>,
465    {
466        let epsilon = 1e-6;
467        let mut gradient = Array1::zeros(self.param_dimensions);
468
469        let base_response = system_function(params)?;
470        let _base_norm = base_response.iter().map(|&x| x * x).sum::<f64>().sqrt();
471
472        for i in 0..self.param_dimensions {
473            let mut params_plus = params.clone();
474            let mut params_minus = params.clone();
475
476            params_plus[i] += epsilon;
477            params_minus[i] -= epsilon;
478
479            let response_plus = system_function(&params_plus)?;
480            let response_minus = system_function(&params_minus)?;
481
482            let norm_plus = response_plus.iter().map(|&x| x * x).sum::<f64>().sqrt();
483            let norm_minus = response_minus.iter().map(|&x| x * x).sum::<f64>().sqrt();
484
485            gradient[i] = (norm_plus - norm_minus) / (2.0 * epsilon);
486        }
487
488        Ok(gradient)
489    }
490
491    fn compute_exploration_metrics(
492        &self,
493        responses: &[Array1<f64>],
494    ) -> IntegrateResult<ExplorationMetrics> {
495        if responses.is_empty() {
496            return Ok(ExplorationMetrics {
497                max_response_norm: 0.0,
498                min_response_norm: 0.0,
499                mean_response_norm: 0.0,
500                response_variance: 0.0,
501                coverage_efficiency: 0.0,
502            });
503        }
504
505        let norms: Vec<f64> = responses
506            .iter()
507            .map(|r| r.iter().map(|&x| x * x).sum::<f64>().sqrt())
508            .collect();
509
510        let max_norm = norms.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
511        let min_norm = norms.iter().fold(f64::INFINITY, |a, &b| a.min(b));
512        let mean_norm = norms.iter().sum::<f64>() / norms.len() as f64;
513
514        let variance =
515            norms.iter().map(|&x| (x - mean_norm).powi(2)).sum::<f64>() / norms.len() as f64;
516
517        // Simple coverage efficiency based on response diversity
518        let coverage_efficiency = if max_norm > min_norm {
519            variance / (max_norm - min_norm).powi(2)
520        } else {
521            1.0
522        };
523
524        Ok(ExplorationMetrics {
525            max_response_norm: max_norm,
526            min_response_norm: min_norm,
527            mean_response_norm: mean_norm,
528            response_variance: variance,
529            coverage_efficiency,
530        })
531    }
532}
533
534/// Advanced Bifurcation Diagram Generator
535#[derive(Debug, Clone)]
536pub struct BifurcationDiagramGenerator {
537    /// Parameter range for bifurcation analysis
538    pub parameter_range: (f64, f64),
539    /// Number of parameter samples
540    pub n_parameter_samples: usize,
541    /// Number of initial transient steps to skip
542    pub transient_steps: usize,
543    /// Number of sampling steps after transients
544    pub sampling_steps: usize,
545    /// Tolerance for detecting fixed points
546    pub fixed_point_tolerance: f64,
547    /// Tolerance for detecting periodic orbits
548    pub period_tolerance: f64,
549}
550
551impl BifurcationDiagramGenerator {
552    /// Create new bifurcation diagram generator
553    pub fn new(_parameterrange: (f64, f64), n_parameter_samples: usize) -> Self {
554        Self {
555            parameter_range: _parameterrange,
556            n_parameter_samples,
557            transient_steps: 1000,
558            sampling_steps: 500,
559            fixed_point_tolerance: 1e-8,
560            period_tolerance: 1e-6,
561        }
562    }
563
564    /// Generate bifurcation diagram for 1D map
565    pub fn generate_1d_map_diagram<F>(
566        &self,
567        map_function: F,
568        initial_condition: f64,
569    ) -> IntegrateResult<BifurcationDiagram>
570    where
571        F: Fn(f64, f64) -> f64, // (x, parameter) -> x_next
572    {
573        let mut parameter_values = Vec::new();
574        let mut state_values = Vec::new();
575        let mut stability_flags = Vec::new();
576        let mut bifurcation_points = Vec::new();
577
578        let param_step = (self.parameter_range.1 - self.parameter_range.0)
579            / (self.n_parameter_samples - 1) as f64;
580
581        for i in 0..self.n_parameter_samples {
582            let param = self.parameter_range.0 + i as f64 * param_step;
583
584            // Run transients
585            let mut x = initial_condition;
586            for _ in 0..self.transient_steps {
587                x = map_function(x, param);
588            }
589
590            // Sample attractor
591            let mut attractorstates = Vec::new();
592            for _ in 0..self.sampling_steps {
593                x = map_function(x, param);
594                attractorstates.push(x);
595            }
596
597            // Analyze attractor structure
598            let attractor_info = self.analyze_1d_attractor(&attractorstates)?;
599
600            // Store results
601            for &state in &attractor_info.representative_states {
602                parameter_values.push(param);
603                state_values.push(state);
604                stability_flags.push(attractor_info.is_stable);
605            }
606
607            // Detect bifurcations
608            if i > 0 {
609                let prev_param = parameter_values
610                    [parameter_values.len() - attractor_info.representative_states.len() - 1];
611                if let Some(bif_point) =
612                    self.detect_bifurcation_1d(prev_param, param, &map_function, initial_condition)?
613                {
614                    bifurcation_points.push(bif_point);
615                }
616            }
617        }
618
619        Ok(BifurcationDiagram {
620            parameters: parameter_values,
621            states: vec![state_values],
622            stability: stability_flags,
623            bifurcation_points,
624        })
625    }
626
627    /// Analyze 1D attractor structure
628    fn analyze_1d_attractor(&self, states: &[f64]) -> IntegrateResult<AttractorInfo> {
629        // Detect fixed points
630        let mut representative_states = Vec::new();
631        let mut uniquestates = states.to_vec();
632        uniquestates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
633        uniquestates.dedup_by(|a, b| (*a - *b).abs() < self.fixed_point_tolerance);
634
635        if uniquestates.len() == 1 {
636            // Fixed point
637            representative_states.push(uniquestates[0]);
638        } else {
639            // Periodic orbit or chaos
640            let period = self.detect_period(states)?;
641
642            if period > 0 && period <= self.sampling_steps / 10 {
643                // Periodic orbit - sample one period
644                for i in 0..period {
645                    representative_states.push(states[states.len() - period + i]);
646                }
647            } else {
648                // Chaotic - sample representative points
649                let sample_rate = states.len() / 20;
650                for i in (0..states.len()).step_by(sample_rate) {
651                    representative_states.push(states[i]);
652                }
653            }
654        }
655
656        // Simple stability analysis
657        let is_stable = uniquestates.len() <= 2; // Fixed point or period-2
658
659        Ok(AttractorInfo {
660            representative_states,
661            is_stable,
662            period: if uniquestates.len() <= 2 {
663                uniquestates.len()
664            } else {
665                0
666            },
667        })
668    }
669
670    /// Detect period of oscillation
671    fn detect_period(&self, states: &[f64]) -> IntegrateResult<usize> {
672        let max_period = (self.sampling_steps / 10).min(50);
673
674        for period in 1..=max_period {
675            let mut is_periodic = true;
676
677            for i in 0..(states.len() - period) {
678                if (states[i] - states[i + period]).abs() > self.period_tolerance {
679                    is_periodic = false;
680                    break;
681                }
682            }
683
684            if is_periodic {
685                return Ok(period);
686            }
687        }
688
689        Ok(0) // Not periodic within tolerance
690    }
691
692    /// Detect bifurcation between two parameter values
693    fn detect_bifurcation_1d<F>(
694        &self,
695        param1: f64,
696        param2: f64,
697        map_function: &F,
698        initial_condition: f64,
699    ) -> IntegrateResult<Option<BifurcationPoint>>
700    where
701        F: Fn(f64, f64) -> f64,
702    {
703        // Simple bifurcation detection based on attractor dimension change
704        let attractor1 = self.sample_attractor_1d(map_function, param1, initial_condition)?;
705        let attractor2 = self.sample_attractor_1d(map_function, param2, initial_condition)?;
706
707        let info1 = self.analyze_1d_attractor(&attractor1)?;
708        let info2 = self.analyze_1d_attractor(&attractor2)?;
709
710        if info1.representative_states.len() != info2.representative_states.len() {
711            // Detected a change in attractor dimension
712            let bif_param = (param1 + param2) / 2.0;
713            let bif_state = Array1::from_vec(vec![info1.representative_states[0]]);
714
715            let bif_type = match (
716                info1.representative_states.len(),
717                info2.representative_states.len(),
718            ) {
719                (1, 2) => crate::analysis::BifurcationType::PeriodDoubling,
720                (1, _) => crate::analysis::BifurcationType::PeriodDoubling,
721                (_, 1) => crate::analysis::BifurcationType::Fold,
722                _ => crate::analysis::BifurcationType::Unknown,
723            };
724
725            return Ok(Some(BifurcationPoint {
726                parameter_value: bif_param,
727                state: bif_state,
728                bifurcation_type: bif_type,
729                eigenvalues: vec![], // Not computed for maps
730            }));
731        }
732
733        Ok(None)
734    }
735
736    /// Sample attractor for given parameter
737    fn sample_attractor_1d<F>(
738        &self,
739        map_function: &F,
740        param: f64,
741        initial_condition: f64,
742    ) -> IntegrateResult<Vec<f64>>
743    where
744        F: Fn(f64, f64) -> f64,
745    {
746        let mut x = initial_condition;
747
748        // Skip transients
749        for _ in 0..self.transient_steps {
750            x = map_function(x, param);
751        }
752
753        // Sample attractor
754        let mut states = Vec::new();
755        for _ in 0..self.sampling_steps {
756            x = map_function(x, param);
757            states.push(x);
758        }
759
760        Ok(states)
761    }
762}