Skip to main content

scirs2_interpolate/
adaptive_learning.rs

1//! Active learning approaches for adaptive sampling in interpolation
2//!
3//! This module provides algorithms for intelligently selecting new sampling points
4//! to improve interpolation accuracy. Active learning techniques can significantly
5//! reduce the number of expensive function evaluations needed to achieve good
6//! interpolation accuracy by focusing sampling on the most informative regions.
7//!
8//! # Active Learning Strategies
9//!
10//! - **Uncertainty-based sampling**: Sample where interpolation uncertainty is highest
11//! - **Error-based refinement**: Add points where current approximation error is large  
12//! - **Variance-based selection**: Sample where prediction variance is high
13//! - **Gradient-based sampling**: Sample in regions with high function gradients
14//! - **Expected improvement**: Sample where expected improvement in approximation is highest
15//! - **Exploration vs exploitation**: Balance between exploring unknown regions and refining known ones
16//!
17//! # Examples
18//!
19//! ```rust
20//! use scirs2_core::ndarray::Array1;
21//! use scirs2_interpolate::adaptive_learning::{ActiveLearner, SamplingStrategy};
22//! use scirs2_interpolate::multiscale::MultiscaleBSpline;
23//!
24//! // Create sample data
25//! let x = Array1::linspace(0.0_f64, 10.0_f64, 20);
26//! let y = x.mapv(|x| x.sin() + 0.1_f64 * (5.0_f64 * x).sin());
27//!
28//! // Create an initial interpolation model
29//! let spline = MultiscaleBSpline::new(
30//!     &x.view(), &y.view(), 10, 3, 5, 0.01_f64,
31//!     scirs2_interpolate::ExtrapolateMode::Extrapolate
32//! ).expect("Operation failed");
33//!
34//! // Set up active learning
35//! let mut learner = ActiveLearner::new(spline, SamplingStrategy::UncertaintyBased)
36//!     .with_budget(50)
37//!     .with_exploration_weight(0.2);
38//!
39//! // Suggest next sampling points
40//! let candidates = learner.suggest_samples(10).expect("Operation failed");
41//! println!("Suggested sampling points: {:?}", candidates);
42//! ```
43
44use crate::error::{InterpolateError, InterpolateResult};
45use crate::multiscale::MultiscaleBSpline;
46use scirs2_core::ndarray::{Array1, ArrayView1};
47use scirs2_core::numeric::{Float, FromPrimitive};
48use std::collections::HashMap;
49use std::fmt::{Debug, Display};
50use std::ops::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign};
51
52/// Strategies for active sampling in interpolation problems
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum SamplingStrategy {
55    /// Sample points where interpolation uncertainty is highest
56    UncertaintyBased,
57    /// Sample based on current approximation error estimates
58    ErrorBased,
59    /// Sample where function gradients are estimated to be large
60    GradientBased,
61    /// Sample to maximize expected improvement in global approximation
62    ExpectedImprovement,
63    /// Balance exploration of unknown regions with exploitation of known errors
64    ExplorationExploitation,
65    /// Combine multiple strategies with weighted importance
66    Combined,
67}
68
69/// Parameters controlling active learning behavior
70#[derive(Debug, Clone)]
71pub struct ActiveLearningConfig<T> {
72    /// Maximum number of new samples to suggest in one iteration
73    pub maxsamples_per_iteration: usize,
74    /// Total sampling budget (maximum number of samples)
75    pub total_budget: usize,
76    /// Weight for exploration vs exploitation (0.0 = pure exploitation, 1.0 = pure exploration)
77    pub exploration_weight: T,
78    /// Minimum distance between new sampling points (to avoid clustering)
79    pub min_sample_distance: T,
80    /// Threshold for stopping active learning (when improvement is below this)
81    pub convergence_threshold: T,
82    /// Weight for uncertainty in combined strategies
83    pub uncertainty_weight: T,
84    /// Weight for error estimates in combined strategies
85    pub error_weight: T,
86    /// Weight for gradient estimates in combined strategies
87    pub gradient_weight: T,
88}
89
90impl<T: Float + FromPrimitive> Default for ActiveLearningConfig<T> {
91    fn default() -> Self {
92        Self {
93            maxsamples_per_iteration: 10,
94            total_budget: 100,
95            exploration_weight: T::from(0.1).expect("Operation failed"),
96            min_sample_distance: T::from(0.01).expect("Operation failed"),
97            convergence_threshold: T::from(1e-6).expect("Operation failed"),
98            uncertainty_weight: T::from(0.4).expect("Operation failed"),
99            error_weight: T::from(0.4).expect("Operation failed"),
100            gradient_weight: T::from(0.2).expect("Operation failed"),
101        }
102    }
103}
104
105/// Information about a candidate sampling point
106#[derive(Debug, Clone)]
107pub struct SamplingCandidate<T> {
108    /// Location of the candidate point
109    pub location: T,
110    /// Estimated utility/importance of sampling at this point
111    pub utility: T,
112    /// Type of information this sample would provide
113    pub information_type: String,
114    /// Confidence in the utility estimate
115    pub confidence: T,
116}
117
118/// Statistics tracking active learning progress
119#[derive(Debug, Clone, Default)]
120pub struct LearningStats {
121    /// Number of samples suggested so far
122    pub samples_suggested: usize,
123    /// Number of iterations completed
124    pub iterations_completed: usize,
125    /// Best approximation error achieved
126    pub best_error: f64,
127    /// History of approximation errors over iterations
128    pub error_history: Vec<f64>,
129    /// Computational cost statistics
130    pub total_computation_time_ms: u64,
131    /// Distribution of sampling strategies used
132    pub strategy_usage: HashMap<String, usize>,
133}
134
135/// Active learning system for intelligent sampling in interpolation
136#[derive(Debug)]
137pub struct ActiveLearner<T>
138where
139    T: Float
140        + FromPrimitive
141        + Debug
142        + Display
143        + AddAssign
144        + SubAssign
145        + MulAssign
146        + DivAssign
147        + RemAssign
148        + Copy,
149{
150    /// The current interpolation model
151    interpolator: MultiscaleBSpline<T>,
152    /// Active learning configuration
153    config: ActiveLearningConfig<T>,
154    /// Primary sampling strategy
155    strategy: SamplingStrategy,
156    /// Current sample points and values
157    sample_points: Array1<T>,
158    sample_values: Array1<T>,
159    /// Domain bounds for sampling
160    domain_min: T,
161    domain_max: T,
162    /// Statistics tracking learning progress
163    stats: LearningStats,
164    /// Random number generator state (for reproducible sampling)
165    rng_state: u64,
166}
167
168impl<T> ActiveLearner<T>
169where
170    T: Float
171        + FromPrimitive
172        + Debug
173        + Display
174        + AddAssign
175        + SubAssign
176        + MulAssign
177        + DivAssign
178        + RemAssign
179        + Copy,
180{
181    /// Create a new active learner with an initial interpolation model
182    ///
183    /// # Arguments
184    ///
185    /// * `interpolator` - Initial interpolation model (typically trained on a small dataset)
186    /// * `strategy` - Primary sampling strategy to use
187    ///
188    /// # Returns
189    ///
190    /// A new `ActiveLearner` instance ready for adaptive sampling
191    pub fn new(interpolator: MultiscaleBSpline<T>, strategy: SamplingStrategy) -> Self {
192        // Extract domain bounds from the _interpolator
193        let x_data = interpolator.x_data();
194        let domain_min = x_data[0];
195        let domain_max = x_data[x_data.len() - 1];
196
197        Self {
198            interpolator,
199            config: ActiveLearningConfig::default(),
200            strategy,
201            sample_points: Array1::zeros(0),
202            sample_values: Array1::zeros(0),
203            domain_min,
204            domain_max,
205            stats: LearningStats::default(),
206            rng_state: 12345, // Fixed seed for reproducibility
207        }
208    }
209
210    /// Set the total sampling budget
211    pub fn with_budget(mut self, budget: usize) -> Self {
212        self.config.total_budget = budget;
213        self
214    }
215
216    /// Set the exploration weight (0.0 = pure exploitation, 1.0 = pure exploration)
217    pub fn with_exploration_weight(mut self, weight: T) -> Self {
218        self.config.exploration_weight = weight;
219        self
220    }
221
222    /// Set the minimum distance between new sampling points
223    pub fn with_min_sample_distance(mut self, distance: T) -> Self {
224        self.config.min_sample_distance = distance;
225        self
226    }
227
228    /// Set the maximum number of samples per iteration
229    pub fn with_maxsamples_per_iteration(mut self, maxsamples: usize) -> Self {
230        self.config.maxsamples_per_iteration = maxsamples;
231        self
232    }
233
234    /// Suggest new sampling points based on the current strategy
235    ///
236    /// # Arguments
237    ///
238    /// * `num_points` - Number of new points to suggest (limited by maxsamples_per_iteration)
239    ///
240    /// # Returns
241    ///
242    /// A vector of candidate sampling points with their utility scores
243    pub fn suggest_samples(
244        &mut self,
245        num_points: usize,
246    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
247        let num_to_suggest = std::cmp::min(num_points, self.config.maxsamples_per_iteration);
248
249        // Check if we've exceeded the budget
250        if self.stats.samples_suggested + num_to_suggest > self.config.total_budget {
251            return Ok(Vec::new());
252        }
253
254        let start_time = std::time::Instant::now();
255
256        let candidates = match self.strategy {
257            SamplingStrategy::UncertaintyBased => {
258                self.uncertainty_based_sampling(num_to_suggest)?
259            }
260            SamplingStrategy::ErrorBased => self.error_based_sampling(num_to_suggest)?,
261            SamplingStrategy::GradientBased => self.gradient_based_sampling(num_to_suggest)?,
262            SamplingStrategy::ExpectedImprovement => {
263                self.expected_improvement_sampling(num_to_suggest)?
264            }
265            SamplingStrategy::ExplorationExploitation => {
266                self.exploration_exploitation_sampling(num_to_suggest)?
267            }
268            SamplingStrategy::Combined => self.combined_sampling(num_to_suggest)?,
269        };
270
271        // Update statistics
272        self.stats.samples_suggested += candidates.len();
273        self.stats.iterations_completed += 1;
274        self.stats.total_computation_time_ms += start_time.elapsed().as_millis() as u64;
275
276        // Update strategy usage statistics
277        let strategy_name = format!("{:?}", self.strategy);
278        *self.stats.strategy_usage.entry(strategy_name).or_insert(0) += 1;
279
280        Ok(candidates)
281    }
282
283    /// Update the interpolation model with new data points
284    ///
285    /// # Arguments
286    ///
287    /// * `new_x` - x-coordinates of new sample points
288    /// * `new_y` - y-coordinates of new sample points
289    ///
290    /// # Returns
291    ///
292    /// `true` if the model was successfully updated
293    pub fn update_model(
294        &mut self,
295        new_x: &ArrayView1<T>,
296        new_y: &ArrayView1<T>,
297    ) -> InterpolateResult<bool> {
298        if new_x.len() != new_y.len() {
299            return Err(InterpolateError::DimensionMismatch(format!(
300                "_x and _y arrays must have same length, got {} and {}",
301                new_x.len(),
302                new_y.len()
303            )));
304        }
305
306        // Combine existing data with new data
307        let current_x = self.interpolator.x_data();
308        let current_y = self.interpolator.y_data();
309
310        let mut combined_x = Vec::with_capacity(current_x.len() + new_x.len());
311        let mut combined_y = Vec::with_capacity(current_y.len() + new_y.len());
312
313        // Add existing data
314        combined_x.extend_from_slice(current_x.as_slice().expect("Operation failed"));
315        combined_y.extend_from_slice(current_y.as_slice().expect("Operation failed"));
316
317        // Add new data
318        combined_x.extend_from_slice(new_x.as_slice().expect("Operation failed"));
319        combined_y.extend_from_slice(new_y.as_slice().expect("Operation failed"));
320
321        // Create combined arrays and sort by _x values
322        let mut data_pairs: Vec<_> = combined_x.into_iter().zip(combined_y).collect();
323        data_pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("Operation failed"));
324
325        let (sorted_x, sorted_y): (Vec<_>, Vec<_>) = data_pairs.into_iter().unzip();
326        let x_array = Array1::from_vec(sorted_x);
327        let y_array = Array1::from_vec(sorted_y);
328
329        // Create a new interpolation model
330        let degree = 3;
331        let min_knots = 2 * (degree + 1); // Minimum knots required for the spline degree
332        let new_interpolator = MultiscaleBSpline::new(
333            &x_array.view(),
334            &y_array.view(),
335            std::cmp::max(std::cmp::min(x_array.len() / 2, 20), min_knots), // Ensure enough knots
336            degree,                                                         // Cubic degree
337            10,                                                             // Max levels
338            T::from(0.01).expect("Operation failed"),
339            crate::ExtrapolateMode::Extrapolate,
340        )?;
341
342        self.interpolator = new_interpolator;
343
344        // Update stored sample data
345        self.sample_points = x_array;
346        self.sample_values = y_array;
347
348        Ok(true)
349    }
350
351    /// Get current learning statistics
352    pub fn get_stats(&self) -> &LearningStats {
353        &self.stats
354    }
355
356    /// Get the current interpolation model
357    pub fn get_interpolator(&self) -> &MultiscaleBSpline<T> {
358        &self.interpolator
359    }
360
361    /// Check if learning should stop based on convergence criteria
362    pub fn should_stop(&self) -> bool {
363        self.stats.samples_suggested >= self.config.total_budget || self.has_converged()
364    }
365
366    /// Check if the learning process has converged
367    fn has_converged(&self) -> bool {
368        if self.stats.error_history.len() < 3 {
369            return false;
370        }
371
372        // Check if improvement in recent iterations is below threshold
373        let recent_errors = &self.stats.error_history[self.stats.error_history.len() - 3..];
374        let improvement = recent_errors[0] - recent_errors[2];
375        improvement
376            < self
377                .config
378                .convergence_threshold
379                .to_f64()
380                .expect("Operation failed")
381    }
382
383    /// Uncertainty-based sampling strategy
384    fn uncertainty_based_sampling(
385        &mut self,
386        num_points: usize,
387    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
388        let mut candidates = Vec::new();
389        let _domain_range = self.domain_max - self.domain_min;
390
391        // Generate candidate _points across the domain
392        let num_candidates = num_points * 10; // Oversample to allow selection
393        let candidate_points = Array1::linspace(
394            self.domain_min.to_f64().expect("Operation failed"),
395            self.domain_max.to_f64().expect("Operation failed"),
396            num_candidates,
397        );
398
399        let mut utilities = Vec::with_capacity(num_candidates);
400
401        // Estimate uncertainty at each candidate point
402        for &x_val in candidate_points.iter() {
403            let x_t = T::from(x_val).expect("Operation failed");
404            let uncertainty = self.estimate_uncertainty(x_t)?;
405            utilities.push((x_t, uncertainty, "uncertainty".to_string()));
406        }
407
408        // Sort by utility (uncertainty) in descending order
409        utilities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
410
411        // Select _points ensuring minimum distance constraint
412        let mut selected_points = Vec::new();
413        for (location, utility, info_type) in utilities {
414            if self.is_valid_location(location, &selected_points) {
415                candidates.push(SamplingCandidate {
416                    location,
417                    utility,
418                    information_type: info_type,
419                    confidence: T::from(0.8).expect("Operation failed"), // Moderate confidence in uncertainty estimates
420                });
421                selected_points.push(location);
422
423                if candidates.len() >= num_points {
424                    break;
425                }
426            }
427        }
428
429        Ok(candidates)
430    }
431
432    /// Error-based sampling strategy
433    fn error_based_sampling(
434        &mut self,
435        num_points: usize,
436    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
437        let mut candidates = Vec::new();
438
439        // Use the multiscale spline's refinement mechanism to identify high-error regions
440        let current_spline = &self.interpolator;
441        let x_data = current_spline.x_data();
442        let y_data = current_spline.y_data();
443
444        // Evaluate current approximation and compute errors
445        let y_approx = current_spline.evaluate(&x_data.view())?;
446        let errors = y_data - &y_approx;
447
448        // Find regions with highest errors
449        let mut error_locations = Vec::new();
450        for (i, &error) in errors.iter().enumerate() {
451            if i > 0 && i < errors.len() - 1 {
452                // Look for local error maxima
453                let local_error = error.abs();
454                let left_error = errors[i - 1].abs();
455                let right_error = errors[i + 1].abs();
456
457                if local_error > left_error && local_error > right_error {
458                    error_locations.push((x_data[i], local_error, "error_peak".to_string()));
459                }
460            }
461        }
462
463        // Also add _points between high-error regions
464        for i in 0..error_locations.len().saturating_sub(1) {
465            let mid_point = (error_locations[i].0 + error_locations[i + 1].0)
466                / T::from(2.0).expect("Operation failed");
467            let estimated_error = (error_locations[i].1 + error_locations[i + 1].1)
468                / T::from(2.0).expect("Operation failed");
469            error_locations.push((
470                mid_point,
471                estimated_error,
472                "error_interpolation".to_string(),
473            ));
474        }
475
476        // Sort by error magnitude
477        error_locations.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
478
479        // Select _points ensuring minimum distance constraint
480        let mut selected_points = Vec::new();
481        for (location, utility, info_type) in error_locations {
482            if self.is_valid_location(location, &selected_points) {
483                candidates.push(SamplingCandidate {
484                    location,
485                    utility,
486                    information_type: info_type,
487                    confidence: T::from(0.9).expect("Operation failed"), // High confidence in error-based sampling
488                });
489                selected_points.push(location);
490
491                if candidates.len() >= num_points {
492                    break;
493                }
494            }
495        }
496
497        // If we don't have enough candidates, fill in with uniform sampling
498        while candidates.len() < num_points {
499            let random_point = self.generate_random_point();
500            if self.is_valid_location(random_point, &selected_points) {
501                candidates.push(SamplingCandidate {
502                    location: random_point,
503                    utility: T::from(0.1).expect("Operation failed"),
504                    information_type: "exploration".to_string(),
505                    confidence: T::from(0.3).expect("Operation failed"),
506                });
507                selected_points.push(random_point);
508            }
509        }
510
511        Ok(candidates)
512    }
513
514    /// Gradient-based sampling strategy
515    fn gradient_based_sampling(
516        &mut self,
517        num_points: usize,
518    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
519        let mut candidates = Vec::new();
520
521        // Generate candidate _points
522        let num_candidates = num_points * 10;
523        let candidate_points = Array1::linspace(
524            self.domain_min.to_f64().expect("Operation failed"),
525            self.domain_max.to_f64().expect("Operation failed"),
526            num_candidates,
527        );
528
529        let mut gradient_utilities = Vec::new();
530
531        // Estimate gradients at candidate _points
532        for &x_val in candidate_points.iter() {
533            let x_t = T::from(x_val).expect("Operation failed");
534
535            // Compute first derivative (gradient magnitude)
536            let gradient = self.interpolator.derivative(
537                1, // First derivative
538                &Array1::from_vec(vec![x_t]).view(),
539            )?;
540
541            let gradient_magnitude = gradient[0].abs();
542            gradient_utilities.push((x_t, gradient_magnitude, "gradient".to_string()));
543        }
544
545        // Sort by gradient magnitude in descending order
546        gradient_utilities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
547
548        // Select _points ensuring minimum distance constraint
549        let mut selected_points = Vec::new();
550        for (location, utility, info_type) in gradient_utilities {
551            if self.is_valid_location(location, &selected_points) {
552                candidates.push(SamplingCandidate {
553                    location,
554                    utility,
555                    information_type: info_type,
556                    confidence: T::from(0.7).expect("Operation failed"), // Moderate confidence in gradient estimates
557                });
558                selected_points.push(location);
559
560                if candidates.len() >= num_points {
561                    break;
562                }
563            }
564        }
565
566        Ok(candidates)
567    }
568
569    /// Expected improvement sampling strategy
570    fn expected_improvement_sampling(
571        &mut self,
572        num_points: usize,
573    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
574        // This is a simplified version of expected improvement
575        // In practice, this would involve more sophisticated uncertainty quantification
576        let mut candidates = Vec::new();
577
578        // Combine uncertainty and error information
579        let uncertainty_candidates = self.uncertainty_based_sampling(num_points)?;
580        let error_candidates = self.error_based_sampling(num_points)?;
581
582        // Create a combined utility that represents expected improvement
583        let mut combined_utilities = Vec::new();
584
585        // Add uncertainty-based candidates with weights
586        for candidate in uncertainty_candidates {
587            let expected_improvement = candidate.utility * self.config.uncertainty_weight;
588            combined_utilities.push((
589                candidate.location,
590                expected_improvement,
591                "expected_improvement_uncertainty".to_string(),
592            ));
593        }
594
595        // Add error-based candidates with weights
596        for candidate in error_candidates {
597            let expected_improvement = candidate.utility * self.config.error_weight;
598            combined_utilities.push((
599                candidate.location,
600                expected_improvement,
601                "expected_improvement_error".to_string(),
602            ));
603        }
604
605        // Sort by expected improvement
606        combined_utilities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
607
608        // Select _points ensuring minimum distance constraint
609        let mut selected_points = Vec::new();
610        for (location, utility, info_type) in combined_utilities {
611            if self.is_valid_location(location, &selected_points) {
612                candidates.push(SamplingCandidate {
613                    location,
614                    utility,
615                    information_type: info_type,
616                    confidence: T::from(0.6).expect("Operation failed"), // Lower confidence for complex heuristic
617                });
618                selected_points.push(location);
619
620                if candidates.len() >= num_points {
621                    break;
622                }
623            }
624        }
625
626        Ok(candidates)
627    }
628
629    /// Exploration-exploitation sampling strategy
630    fn exploration_exploitation_sampling(
631        &mut self,
632        num_points: usize,
633    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
634        let num_exploitation = (num_points as f64
635            * (1.0
636                - self
637                    .config
638                    .exploration_weight
639                    .to_f64()
640                    .expect("Operation failed"))) as usize;
641        let num_exploration = num_points - num_exploitation;
642
643        let mut candidates = Vec::new();
644
645        // Exploitation: sample in high-error regions
646        let exploitation_candidates = self.error_based_sampling(num_exploitation)?;
647        candidates.extend(exploitation_candidates);
648
649        // Exploration: sample in under-sampled regions
650        for _ in 0..num_exploration {
651            let exploration_point = self.find_under_sampled_region()?;
652            candidates.push(SamplingCandidate {
653                location: exploration_point,
654                utility: T::from(0.5).expect("Operation failed"),
655                information_type: "exploration".to_string(),
656                confidence: T::from(0.4).expect("Operation failed"),
657            });
658        }
659
660        Ok(candidates)
661    }
662
663    /// Combined sampling strategy using multiple criteria
664    fn combined_sampling(
665        &mut self,
666        num_points: usize,
667    ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
668        let mut candidates = Vec::new();
669
670        // Get candidates from each strategy
671        let uncertainty_candidates = self.uncertainty_based_sampling(num_points * 2)?;
672        let error_candidates = self.error_based_sampling(num_points * 2)?;
673        let gradient_candidates = self.gradient_based_sampling(num_points * 2)?;
674
675        // Combine all candidates with weighted utilities
676        let mut all_candidates = Vec::new();
677
678        for candidate in uncertainty_candidates {
679            all_candidates.push((
680                candidate.location,
681                candidate.utility * self.config.uncertainty_weight,
682                "combined_uncertainty".to_string(),
683            ));
684        }
685
686        for candidate in error_candidates {
687            all_candidates.push((
688                candidate.location,
689                candidate.utility * self.config.error_weight,
690                "combined_error".to_string(),
691            ));
692        }
693
694        for candidate in gradient_candidates {
695            all_candidates.push((
696                candidate.location,
697                candidate.utility * self.config.gradient_weight,
698                "combined_gradient".to_string(),
699            ));
700        }
701
702        // Sort by combined utility
703        all_candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
704
705        // Select _points ensuring minimum distance constraint
706        let mut selected_points = Vec::new();
707        for (location, utility, info_type) in all_candidates {
708            if self.is_valid_location(location, &selected_points) {
709                candidates.push(SamplingCandidate {
710                    location,
711                    utility,
712                    information_type: info_type,
713                    confidence: T::from(0.75).expect("Operation failed"), // Good confidence in combined approach
714                });
715                selected_points.push(location);
716
717                if candidates.len() >= num_points {
718                    break;
719                }
720            }
721        }
722
723        Ok(candidates)
724    }
725
726    /// Estimate interpolation uncertainty at a point
727    fn estimate_uncertainty(&self, x: T) -> InterpolateResult<T> {
728        // Simple uncertainty estimate based on distance to nearest sample points
729        let x_data = self.interpolator.x_data();
730
731        let mut min_distance = T::infinity();
732        for &sample_x in x_data.iter() {
733            let distance = (x - sample_x).abs();
734            if distance < min_distance {
735                min_distance = distance;
736            }
737        }
738
739        // Uncertainty inversely related to distance to nearest samples
740        let uncertainty = T::one() / (T::one() + min_distance);
741        Ok(uncertainty)
742    }
743
744    /// Check if a location is valid (respects minimum distance constraint)
745    fn is_valid_location(&self, location: T, existingpoints: &[T]) -> bool {
746        // Check domain bounds
747        if location < self.domain_min || location > self.domain_max {
748            return false;
749        }
750
751        // Check minimum distance to existing sample _points
752        let x_data = self.interpolator.x_data();
753        for &sample_x in x_data.iter() {
754            if (location - sample_x).abs() < self.config.min_sample_distance {
755                return false;
756            }
757        }
758
759        // Check minimum distance to other selected _points
760        for &point in existingpoints {
761            if (location - point).abs() < self.config.min_sample_distance {
762                return false;
763            }
764        }
765
766        true
767    }
768
769    /// Generate a random point within the domain (simple LCG for reproducibility)
770    fn generate_random_point(&mut self) -> T {
771        // Simple linear congruential generator for reproducible randomness
772        self.rng_state = (self
773            .rng_state
774            .wrapping_mul(1664525)
775            .wrapping_add(1013904223))
776            % (1 << 32);
777        let normalized = (self.rng_state as f64) / ((1u64 << 32) as f64);
778
779        let range = self.domain_max - self.domain_min;
780        self.domain_min + range * T::from(normalized).expect("Operation failed")
781    }
782
783    /// Find an under-sampled region for exploration
784    fn find_under_sampled_region(&mut self) -> InterpolateResult<T> {
785        let x_data = self.interpolator.x_data();
786
787        if x_data.len() < 2 {
788            return Ok(self.generate_random_point());
789        }
790
791        // Find the largest gap between consecutive sample points
792        let mut max_gap = T::zero();
793        let mut best_location = self.domain_min;
794
795        for i in 0..x_data.len() - 1 {
796            let gap = x_data[i + 1] - x_data[i];
797            if gap > max_gap {
798                max_gap = gap;
799                best_location = x_data[i] + gap / T::from(2.0).expect("Operation failed");
800            }
801        }
802
803        // Also check gaps at domain boundaries
804        let left_gap = x_data[0] - self.domain_min;
805        if left_gap > max_gap {
806            max_gap = left_gap;
807            best_location = self.domain_min + left_gap / T::from(2.0).expect("Operation failed");
808        }
809
810        let right_gap = self.domain_max - x_data[x_data.len() - 1];
811        if right_gap > max_gap {
812            best_location =
813                x_data[x_data.len() - 1] + right_gap / T::from(2.0).expect("Operation failed");
814        }
815
816        Ok(best_location)
817    }
818}
819
820/// Convenience function to create an active learner with default settings
821///
822/// # Arguments
823///
824/// * `x` - Initial x-coordinates
825/// * `y` - Initial y-coordinates  
826/// * `strategy` - Sampling strategy to use
827/// * `budget` - Total sampling budget
828///
829/// # Returns
830///
831/// A configured `ActiveLearner` ready for adaptive sampling
832#[allow(dead_code)]
833pub fn make_active_learner<T>(
834    x: &ArrayView1<T>,
835    y: &ArrayView1<T>,
836    strategy: SamplingStrategy,
837    budget: usize,
838) -> InterpolateResult<ActiveLearner<T>>
839where
840    T: Float
841        + FromPrimitive
842        + Debug
843        + Display
844        + AddAssign
845        + SubAssign
846        + MulAssign
847        + DivAssign
848        + RemAssign
849        + Copy,
850{
851    // Create initial interpolation model
852    let degree = 3;
853    let min_knots = 2 * (degree + 1); // Minimum knots required for the spline degree
854    let spline = MultiscaleBSpline::new(
855        x,
856        y,
857        std::cmp::max(std::cmp::min(x.len() / 2, 10), min_knots), // Ensure enough knots
858        degree,                                                   // Cubic degree
859        10,                                                       // Max levels
860        T::from(0.01).expect("Operation failed"),                 // Error threshold
861        crate::ExtrapolateMode::Extrapolate,
862    )?;
863
864    let learner = ActiveLearner::new(spline, strategy).with_budget(budget);
865
866    Ok(learner)
867}
868
869#[cfg(test)]
870mod tests {
871    use super::*;
872    use scirs2_core::ndarray::Array1;
873
874    #[test]
875    fn test_active_learner_creation() {
876        let x = Array1::linspace(0.0, 10.0, 21);
877        let y = x.mapv(|x| x.sin());
878
879        let learner =
880            make_active_learner(&x.view(), &y.view(), SamplingStrategy::UncertaintyBased, 50)
881                .expect("Operation failed");
882
883        assert_eq!(learner.config.total_budget, 50);
884        assert_eq!(learner.strategy, SamplingStrategy::UncertaintyBased);
885    }
886
887    #[test]
888    fn test_uncertainty_based_sampling() {
889        let x = Array1::from_vec(vec![0.0, 1.4, 2.9, 4.3, 5.7, 7.1, 8.6, 10.0]);
890        let y = Array1::from_vec(vec![0.0, 0.7, 1.0, 0.8, 0.5, 0.3, 0.1, 0.0]);
891
892        let mut learner =
893            make_active_learner(&x.view(), &y.view(), SamplingStrategy::UncertaintyBased, 20)
894                .expect("Operation failed");
895
896        let candidates = learner.suggest_samples(5).expect("Operation failed");
897
898        assert!(!candidates.is_empty());
899        assert!(candidates.len() <= 5);
900
901        // Check that candidates are within domain
902        for candidate in &candidates {
903            assert!(candidate.location >= 0.0);
904            assert!(candidate.location <= 10.0);
905        }
906    }
907
908    #[test]
909    fn test_error_based_sampling() {
910        // Create data with a sharp feature that will have high error
911        let x = Array1::linspace(0.0, 10.0, 11);
912        let y = x.mapv(|x| if (x - 5.0).abs() < 1.0 { x * x } else { x });
913
914        let mut learner =
915            make_active_learner(&x.view(), &y.view(), SamplingStrategy::ErrorBased, 20)
916                .expect("Operation failed");
917
918        let candidates = learner.suggest_samples(3).expect("Operation failed");
919
920        assert!(!candidates.is_empty());
921
922        // At least one candidate should be near the sharp feature (around x=5)
923        let near_feature = candidates.iter().any(|c| (c.location - 5.0).abs() < 2.0);
924        assert!(near_feature);
925    }
926
927    #[test]
928    fn test_model_update() {
929        let x = Array1::from_vec(vec![0.0, 1.4, 2.9, 4.3, 5.7, 7.1, 8.6, 10.0]);
930        let y = Array1::from_vec(vec![0.0, 0.7, 1.0, 0.8, 0.5, 0.3, 0.1, 0.0]);
931
932        let mut learner =
933            make_active_learner(&x.view(), &y.view(), SamplingStrategy::UncertaintyBased, 20)
934                .expect("Operation failed");
935
936        // Add new data points
937        let new_x = Array1::from_vec(vec![2.5, 7.5]);
938        let new_y = Array1::from_vec(vec![0.5, 0.5]);
939
940        let success = learner
941            .update_model(&new_x.view(), &new_y.view())
942            .expect("Operation failed");
943        assert!(success);
944
945        // Check that the model has been updated
946        assert_eq!(learner.sample_points.len(), 10); // Original 8 + new 2
947    }
948
949    #[test]
950    fn test_combined_sampling() {
951        let x = Array1::linspace(0.0, 10.0, 21);
952        let y = x.mapv(|x| x.sin() + 0.1 * (5.0 * x).sin());
953
954        let mut learner = make_active_learner(&x.view(), &y.view(), SamplingStrategy::Combined, 30)
955            .expect("Operation failed");
956
957        let candidates = learner.suggest_samples(5).expect("Operation failed");
958
959        assert!(!candidates.is_empty());
960        assert!(candidates.len() <= 5);
961
962        // Check that we get different types of information
963        let info_types: std::collections::HashSet<_> = candidates
964            .iter()
965            .map(|c| c.information_type.as_str())
966            .collect();
967
968        // Should have multiple types when using combined strategy
969        assert!(!info_types.is_empty());
970    }
971
972    #[test]
973    fn test_exploration_exploitation_balance() {
974        let x = Array1::from_vec(vec![0.0, 1.0, 2.0, 2.5, 3.0, 3.5, 9.0, 10.0]); // Sparse sampling with gap
975        let y = Array1::from_vec(vec![0.0, 1.0, 1.5, 1.8, 2.0, 2.2, 9.0, 10.0]);
976
977        let mut learner = make_active_learner(
978            &x.view(),
979            &y.view(),
980            SamplingStrategy::ExplorationExploitation,
981            20,
982        )
983        .expect("Operation failed");
984
985        let candidates = learner.suggest_samples(4).expect("Operation failed");
986
987        assert!(!candidates.is_empty());
988
989        // Should suggest points in the large gap (exploration)
990        let in_gap = candidates
991            .iter()
992            .any(|c| c.location > 3.0 && c.location < 8.0);
993        assert!(in_gap);
994    }
995
996    #[test]
997    fn test_minimum_distance_constraint() {
998        let x = Array1::from_vec(vec![0.0, 1.4, 2.9, 4.3, 5.7, 7.1, 8.6, 10.0]);
999        let y = Array1::from_vec(vec![0.0, 1.4, 2.9, 4.3, 5.7, 7.1, 8.6, 10.0]);
1000
1001        let mut learner =
1002            make_active_learner(&x.view(), &y.view(), SamplingStrategy::UncertaintyBased, 20)
1003                .expect("Operation failed")
1004                .with_min_sample_distance(1.0);
1005
1006        let candidates = learner.suggest_samples(5).expect("Operation failed");
1007
1008        // Check that all candidates respect minimum distance constraint
1009        for i in 0..candidates.len() {
1010            for j in i + 1..candidates.len() {
1011                let distance = (candidates[i].location - candidates[j].location).abs();
1012                assert!(distance >= 1.0);
1013            }
1014        }
1015    }
1016}