1use 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#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum SamplingStrategy {
55 UncertaintyBased,
57 ErrorBased,
59 GradientBased,
61 ExpectedImprovement,
63 ExplorationExploitation,
65 Combined,
67}
68
69#[derive(Debug, Clone)]
71pub struct ActiveLearningConfig<T> {
72 pub maxsamples_per_iteration: usize,
74 pub total_budget: usize,
76 pub exploration_weight: T,
78 pub min_sample_distance: T,
80 pub convergence_threshold: T,
82 pub uncertainty_weight: T,
84 pub error_weight: T,
86 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#[derive(Debug, Clone)]
107pub struct SamplingCandidate<T> {
108 pub location: T,
110 pub utility: T,
112 pub information_type: String,
114 pub confidence: T,
116}
117
118#[derive(Debug, Clone, Default)]
120pub struct LearningStats {
121 pub samples_suggested: usize,
123 pub iterations_completed: usize,
125 pub best_error: f64,
127 pub error_history: Vec<f64>,
129 pub total_computation_time_ms: u64,
131 pub strategy_usage: HashMap<String, usize>,
133}
134
135#[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 interpolator: MultiscaleBSpline<T>,
152 config: ActiveLearningConfig<T>,
154 strategy: SamplingStrategy,
156 sample_points: Array1<T>,
158 sample_values: Array1<T>,
159 domain_min: T,
161 domain_max: T,
162 stats: LearningStats,
164 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 pub fn new(interpolator: MultiscaleBSpline<T>, strategy: SamplingStrategy) -> Self {
192 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, }
208 }
209
210 pub fn with_budget(mut self, budget: usize) -> Self {
212 self.config.total_budget = budget;
213 self
214 }
215
216 pub fn with_exploration_weight(mut self, weight: T) -> Self {
218 self.config.exploration_weight = weight;
219 self
220 }
221
222 pub fn with_min_sample_distance(mut self, distance: T) -> Self {
224 self.config.min_sample_distance = distance;
225 self
226 }
227
228 pub fn with_maxsamples_per_iteration(mut self, maxsamples: usize) -> Self {
230 self.config.maxsamples_per_iteration = maxsamples;
231 self
232 }
233
234 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 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 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 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 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 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 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 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 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 let degree = 3;
331 let min_knots = 2 * (degree + 1); 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), degree, 10, T::from(0.01).expect("Operation failed"),
339 crate::ExtrapolateMode::Extrapolate,
340 )?;
341
342 self.interpolator = new_interpolator;
343
344 self.sample_points = x_array;
346 self.sample_values = y_array;
347
348 Ok(true)
349 }
350
351 pub fn get_stats(&self) -> &LearningStats {
353 &self.stats
354 }
355
356 pub fn get_interpolator(&self) -> &MultiscaleBSpline<T> {
358 &self.interpolator
359 }
360
361 pub fn should_stop(&self) -> bool {
363 self.stats.samples_suggested >= self.config.total_budget || self.has_converged()
364 }
365
366 fn has_converged(&self) -> bool {
368 if self.stats.error_history.len() < 3 {
369 return false;
370 }
371
372 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 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 let num_candidates = num_points * 10; 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 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 utilities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
410
411 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"), });
421 selected_points.push(location);
422
423 if candidates.len() >= num_points {
424 break;
425 }
426 }
427 }
428
429 Ok(candidates)
430 }
431
432 fn error_based_sampling(
434 &mut self,
435 num_points: usize,
436 ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
437 let mut candidates = Vec::new();
438
439 let current_spline = &self.interpolator;
441 let x_data = current_spline.x_data();
442 let y_data = current_spline.y_data();
443
444 let y_approx = current_spline.evaluate(&x_data.view())?;
446 let errors = y_data - &y_approx;
447
448 let mut error_locations = Vec::new();
450 for (i, &error) in errors.iter().enumerate() {
451 if i > 0 && i < errors.len() - 1 {
452 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 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 error_locations.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
478
479 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"), });
489 selected_points.push(location);
490
491 if candidates.len() >= num_points {
492 break;
493 }
494 }
495 }
496
497 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 fn gradient_based_sampling(
516 &mut self,
517 num_points: usize,
518 ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
519 let mut candidates = Vec::new();
520
521 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 for &x_val in candidate_points.iter() {
533 let x_t = T::from(x_val).expect("Operation failed");
534
535 let gradient = self.interpolator.derivative(
537 1, &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 gradient_utilities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
547
548 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"), });
558 selected_points.push(location);
559
560 if candidates.len() >= num_points {
561 break;
562 }
563 }
564 }
565
566 Ok(candidates)
567 }
568
569 fn expected_improvement_sampling(
571 &mut self,
572 num_points: usize,
573 ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
574 let mut candidates = Vec::new();
577
578 let uncertainty_candidates = self.uncertainty_based_sampling(num_points)?;
580 let error_candidates = self.error_based_sampling(num_points)?;
581
582 let mut combined_utilities = Vec::new();
584
585 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 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 combined_utilities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
607
608 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"), });
618 selected_points.push(location);
619
620 if candidates.len() >= num_points {
621 break;
622 }
623 }
624 }
625
626 Ok(candidates)
627 }
628
629 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 let exploitation_candidates = self.error_based_sampling(num_exploitation)?;
647 candidates.extend(exploitation_candidates);
648
649 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 fn combined_sampling(
665 &mut self,
666 num_points: usize,
667 ) -> InterpolateResult<Vec<SamplingCandidate<T>>> {
668 let mut candidates = Vec::new();
669
670 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 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 all_candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("Operation failed"));
704
705 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"), });
715 selected_points.push(location);
716
717 if candidates.len() >= num_points {
718 break;
719 }
720 }
721 }
722
723 Ok(candidates)
724 }
725
726 fn estimate_uncertainty(&self, x: T) -> InterpolateResult<T> {
728 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 let uncertainty = T::one() / (T::one() + min_distance);
741 Ok(uncertainty)
742 }
743
744 fn is_valid_location(&self, location: T, existingpoints: &[T]) -> bool {
746 if location < self.domain_min || location > self.domain_max {
748 return false;
749 }
750
751 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 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 fn generate_random_point(&mut self) -> T {
771 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 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 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 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#[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 let degree = 3;
853 let min_knots = 2 * (degree + 1); let spline = MultiscaleBSpline::new(
855 x,
856 y,
857 std::cmp::max(std::cmp::min(x.len() / 2, 10), min_knots), degree, 10, T::from(0.01).expect("Operation failed"), 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 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 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 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 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 assert_eq!(learner.sample_points.len(), 10); }
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 let info_types: std::collections::HashSet<_> = candidates
964 .iter()
965 .map(|c| c.information_type.as_str())
966 .collect();
967
968 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]); 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 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 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}