1use scirs2_core::ndarray::{Array1, Array2};
34use scirs2_core::random::rngs::StdRng;
35use scirs2_core::random::{Rng, SeedableRng};
36use scirs2_core::RngExt;
37
38use crate::error::{OptimizeError, OptimizeResult};
39
40#[derive(Debug, Clone)]
46enum TreeNode {
47 Split {
49 feature: usize,
50 threshold: f64,
51 left: Box<TreeNode>,
52 right: Box<TreeNode>,
53 },
54 Leaf { value: f64 },
56}
57
58impl TreeNode {
59 fn predict(&self, x: &[f64]) -> f64 {
61 match self {
62 TreeNode::Leaf { value } => *value,
63 TreeNode::Split {
64 feature,
65 threshold,
66 left,
67 right,
68 } => {
69 let v = if *feature < x.len() { x[*feature] } else { 0.0 };
70 if v <= *threshold {
71 left.predict(x)
72 } else {
73 right.predict(x)
74 }
75 }
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
86struct TreeParams {
87 max_depth: usize,
88 min_samples_split: usize,
89 max_features: usize,
91}
92
93impl Default for TreeParams {
94 fn default() -> Self {
95 Self {
96 max_depth: 10,
97 min_samples_split: 2,
98 max_features: 0,
99 }
100 }
101}
102
103fn build_tree(
105 x: &[Vec<f64>],
106 y: &[f64],
107 params: &TreeParams,
108 depth: usize,
109 rng: &mut StdRng,
110) -> TreeNode {
111 let n = x.len();
112 if n == 0 {
113 return TreeNode::Leaf { value: 0.0 };
114 }
115
116 let mean = y.iter().sum::<f64>() / n as f64;
118
119 if depth >= params.max_depth || n < params.min_samples_split {
121 return TreeNode::Leaf { value: mean };
122 }
123
124 let var = y.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / n as f64;
126 if var < 1e-12 {
127 return TreeNode::Leaf { value: mean };
128 }
129
130 let n_features = if x.is_empty() { 0 } else { x[0].len() };
131 let n_try = if params.max_features == 0 {
132 ((n_features as f64).sqrt() as usize).max(1)
133 } else {
134 params.max_features.min(n_features)
135 };
136
137 let mut feature_indices: Vec<usize> = (0..n_features).collect();
139 for i in 0..n_try {
141 let j = i + rng.random_range(0..(n_features - i));
142 feature_indices.swap(i, j);
143 }
144 let features_to_try = &feature_indices[..n_try];
145
146 let mut best_gain = 0.0;
147 let mut best_feature = 0;
148 let mut best_threshold = 0.0;
149
150 for &feat in features_to_try {
151 let mut vals: Vec<f64> = x.iter().map(|xi| xi[feat]).collect();
153 vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
154 vals.dedup();
155
156 for i in 0..(vals.len().saturating_sub(1)) {
157 let threshold = 0.5 * (vals[i] + vals[i + 1]);
158 let gain = variance_reduction(x, y, feat, threshold, mean, var, n);
159 if gain > best_gain {
160 best_gain = gain;
161 best_feature = feat;
162 best_threshold = threshold;
163 }
164 }
165 }
166
167 if best_gain <= 0.0 {
168 return TreeNode::Leaf { value: mean };
169 }
170
171 let (x_left, y_left, x_right, y_right) = partition(x, y, best_feature, best_threshold);
173
174 if x_left.is_empty() || x_right.is_empty() {
175 return TreeNode::Leaf { value: mean };
176 }
177
178 let left = build_tree(&x_left, &y_left, params, depth + 1, rng);
179 let right = build_tree(&x_right, &y_right, params, depth + 1, rng);
180
181 TreeNode::Split {
182 feature: best_feature,
183 threshold: best_threshold,
184 left: Box::new(left),
185 right: Box::new(right),
186 }
187}
188
189fn variance_reduction(
191 x: &[Vec<f64>],
192 y: &[f64],
193 feature: usize,
194 threshold: f64,
195 parent_mean: f64,
196 parent_var: f64,
197 n: usize,
198) -> f64 {
199 let (n_l, sum_l, sq_l, n_r, sum_r, sq_r) = x.iter().zip(y.iter()).fold(
200 (0usize, 0.0_f64, 0.0_f64, 0usize, 0.0_f64, 0.0_f64),
201 |mut acc, (xi, &yi)| {
202 if xi[feature] <= threshold {
203 acc.0 += 1;
204 acc.1 += yi;
205 acc.2 += yi * yi;
206 } else {
207 acc.3 += 1;
208 acc.4 += yi;
209 acc.5 += yi * yi;
210 }
211 acc
212 },
213 );
214
215 if n_l == 0 || n_r == 0 {
216 return 0.0;
217 }
218
219 let var_l = (sq_l - sum_l * sum_l / n_l as f64) / n_l as f64;
220 let var_r = (sq_r - sum_r * sum_r / n_r as f64) / n_r as f64;
221
222 let weighted_var = (n_l as f64 * var_l + n_r as f64 * var_r) / n as f64;
223
224 let _ = parent_mean; parent_var - weighted_var
226}
227
228fn partition(
230 x: &[Vec<f64>],
231 y: &[f64],
232 feature: usize,
233 threshold: f64,
234) -> (Vec<Vec<f64>>, Vec<f64>, Vec<Vec<f64>>, Vec<f64>) {
235 let mut x_l = Vec::new();
236 let mut y_l = Vec::new();
237 let mut x_r = Vec::new();
238 let mut y_r = Vec::new();
239
240 for (xi, &yi) in x.iter().zip(y.iter()) {
241 if xi[feature] <= threshold {
242 x_l.push(xi.clone());
243 y_l.push(yi);
244 } else {
245 x_r.push(xi.clone());
246 y_r.push(yi);
247 }
248 }
249
250 (x_l, y_l, x_r, y_r)
251}
252
253#[derive(Debug, Clone)]
263pub struct RandomForestSurrogate {
264 trees: Vec<TreeNode>,
265 n_estimators: usize,
266 max_depth: usize,
267 min_samples_split: usize,
268 max_features: usize,
269 seed: u64,
270}
271
272impl RandomForestSurrogate {
273 pub fn new(
275 n_estimators: usize,
276 max_depth: usize,
277 min_samples_split: usize,
278 max_features: usize,
279 seed: u64,
280 ) -> Self {
281 Self {
282 trees: Vec::new(),
283 n_estimators: n_estimators.max(1),
284 max_depth: max_depth.max(1),
285 min_samples_split: min_samples_split.max(2),
286 max_features,
287 seed,
288 }
289 }
290
291 pub fn default_config(seed: u64) -> Self {
293 Self::new(100, 10, 2, 0, seed)
294 }
295
296 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> OptimizeResult<()> {
300 if x.nrows() != y.len() {
301 return Err(OptimizeError::InvalidInput(format!(
302 "x has {} rows but y has {} elements",
303 x.nrows(),
304 y.len()
305 )));
306 }
307 if x.nrows() == 0 {
308 return Err(OptimizeError::InvalidInput(
309 "Cannot fit RF with zero samples".to_string(),
310 ));
311 }
312
313 let n = x.nrows();
314 let n_features = x.ncols();
315
316 let x_vec: Vec<Vec<f64>> = (0..n)
318 .map(|i| (0..n_features).map(|j| x[[i, j]]).collect())
319 .collect();
320 let y_vec: Vec<f64> = y.iter().copied().collect();
321
322 let params = TreeParams {
323 max_depth: self.max_depth,
324 min_samples_split: self.min_samples_split,
325 max_features: self.max_features,
326 };
327
328 let mut rng = StdRng::seed_from_u64(self.seed);
329 self.trees.clear();
330
331 for t in 0..self.n_estimators {
332 let mut x_boot = Vec::with_capacity(n);
334 let mut y_boot = Vec::with_capacity(n);
335
336 let mut tree_rng = StdRng::seed_from_u64(
338 self.seed
339 .wrapping_add((t as u64).wrapping_mul(6364136223846793005)),
340 );
341
342 for _ in 0..n {
343 let idx = rng.random_range(0..n);
344 x_boot.push(x_vec[idx].clone());
345 y_boot.push(y_vec[idx]);
346 }
347
348 let tree = build_tree(&x_boot, &y_boot, ¶ms, 0, &mut tree_rng);
349 self.trees.push(tree);
350 }
351
352 Ok(())
353 }
354
355 pub fn predict(&self, x: &Array2<f64>) -> OptimizeResult<(Array1<f64>, Array1<f64>)> {
359 if self.trees.is_empty() {
360 return Err(OptimizeError::ComputationError(
361 "RandomForestSurrogate has not been fitted".to_string(),
362 ));
363 }
364
365 let n = x.nrows();
366 let n_features = x.ncols();
367 let n_trees = self.trees.len() as f64;
368
369 let mut means = Array1::zeros(n);
370 let mut variances = Array1::zeros(n);
371
372 for i in 0..n {
373 let x_row: Vec<f64> = (0..n_features).map(|j| x[[i, j]]).collect();
374
375 let preds: Vec<f64> = self.trees.iter().map(|t| t.predict(&x_row)).collect();
377 let mean = preds.iter().sum::<f64>() / n_trees;
378 let variance = preds.iter().map(|&p| (p - mean) * (p - mean)).sum::<f64>()
379 / n_trees.max(1.0 - 1.0 / n_trees.max(1.0)); means[i] = mean;
382 variances[i] = variance.max(0.0);
383 }
384
385 Ok((means, variances))
386 }
387
388 pub fn predict_single(&self, x: &[f64]) -> OptimizeResult<(f64, f64)> {
390 if self.trees.is_empty() {
391 return Err(OptimizeError::ComputationError(
392 "RandomForestSurrogate has not been fitted".to_string(),
393 ));
394 }
395 let n_trees = self.trees.len() as f64;
396 let preds: Vec<f64> = self.trees.iter().map(|t| t.predict(x)).collect();
397 let mean = preds.iter().sum::<f64>() / n_trees;
398 let variance =
399 preds.iter().map(|&p| (p - mean) * (p - mean)).sum::<f64>() / n_trees.max(1.0);
400 Ok((mean, variance.max(0.0).sqrt()))
401 }
402
403 pub fn n_estimators(&self) -> usize {
405 self.trees.len()
406 }
407
408 pub fn is_fitted(&self) -> bool {
410 !self.trees.is_empty()
411 }
412}
413
414pub fn ei_random_forest(
427 x: &[f64],
428 surrogate: &RandomForestSurrogate,
429 best_y: f64,
430 xi: f64,
431) -> OptimizeResult<f64> {
432 let (mu, sigma) = surrogate.predict_single(x)?;
433 if sigma < 1e-12 {
434 return Ok(0.0);
435 }
436 let z = (best_y - mu - xi) / sigma;
437 let ei = (best_y - mu - xi) * norm_cdf(z) + sigma * norm_pdf(z);
438 Ok(ei.max(0.0))
439}
440
441#[derive(Debug, Clone)]
447pub struct SmacConfig {
448 pub n_initial: usize,
450 pub n_iterations: usize,
452 pub n_candidates: usize,
454 pub n_local_search: usize,
456 pub xi: f64,
458 pub n_estimators: usize,
460 pub rf_max_depth: usize,
462 pub seed: Option<u64>,
464 pub verbose: usize,
466}
467
468impl Default for SmacConfig {
469 fn default() -> Self {
470 Self {
471 n_initial: 10,
472 n_iterations: 50,
473 n_candidates: 300,
474 n_local_search: 5,
475 xi: 0.01,
476 n_estimators: 50,
477 rf_max_depth: 10,
478 seed: None,
479 verbose: 0,
480 }
481 }
482}
483
484#[derive(Debug, Clone)]
486pub struct SmacResult {
487 pub x_best: Array1<f64>,
489 pub f_best: f64,
491 pub n_evals: usize,
493 pub history: Vec<(usize, f64)>,
495}
496
497pub struct SmacOptimizer {
502 bounds: Vec<(f64, f64)>,
503 config: SmacConfig,
504}
505
506impl SmacOptimizer {
507 pub fn new(bounds: Vec<(f64, f64)>, config: SmacConfig) -> OptimizeResult<Self> {
509 if bounds.is_empty() {
510 return Err(OptimizeError::InvalidInput(
511 "Search space bounds must not be empty".to_string(),
512 ));
513 }
514 Ok(Self { bounds, config })
515 }
516
517 pub fn optimize<F>(&mut self, objective: F, n_evaluations: usize) -> OptimizeResult<SmacResult>
522 where
523 F: Fn(&[f64]) -> f64,
524 {
525 let n_iter = if n_evaluations > 0 {
526 n_evaluations
527 } else {
528 self.config.n_iterations
529 };
530
531 if n_iter == 0 {
532 return Err(OptimizeError::InvalidInput(
533 "n_iterations must be > 0".to_string(),
534 ));
535 }
536
537 let n_dims = self.bounds.len();
538 let seed = self.config.seed.unwrap_or(0);
539 let mut rng = StdRng::seed_from_u64(seed);
540
541 let mut x_history: Vec<Vec<f64>> = Vec::new();
542 let mut y_history: Vec<f64> = Vec::new();
543 let mut history: Vec<(usize, f64)> = Vec::new();
544 let mut best_y = f64::INFINITY;
545 let mut best_x: Option<Array1<f64>> = None;
546
547 let n_init = self.config.n_initial.min(n_iter).max(3);
551 for i in 0..n_init {
552 let x_rand: Vec<f64> = (0..n_dims)
553 .map(|d| {
554 let lo = self.bounds[d].0;
555 let hi = self.bounds[d].1;
556 lo + rng.random::<f64>() * (hi - lo)
557 })
558 .collect();
559
560 let y = objective(&x_rand);
561 history.push((i + 1, y));
562
563 if y < best_y {
564 best_y = y;
565 let arr = Array1::from_vec(x_rand.clone());
566 best_x = Some(arr);
567 }
568
569 x_history.push(x_rand);
570 y_history.push(y);
571 }
572
573 if self.config.verbose >= 1 {
574 println!(
575 "[SMAC] Initial phase complete: {} evals, best_y={:.6}",
576 n_init, best_y
577 );
578 }
579
580 let mut surrogate = RandomForestSurrogate::new(
584 self.config.n_estimators,
585 self.config.rf_max_depth,
586 2,
587 0,
588 seed,
589 );
590
591 for iter in n_init..n_iter {
592 let n_obs = x_history.len();
594 let mut x_train = Array2::zeros((n_obs, n_dims));
595 let mut y_train = Array1::zeros(n_obs);
596 for (i, (xi, &yi)) in x_history.iter().zip(y_history.iter()).enumerate() {
597 for d in 0..n_dims {
598 x_train[[i, d]] = xi[d];
599 }
600 y_train[i] = yi;
601 }
602
603 if let Err(e) = surrogate.fit(&x_train, &y_train) {
605 if self.config.verbose >= 1 {
606 println!("[SMAC] surrogate fit error at iter {}: {}", iter, e);
607 }
608 let x_rand: Vec<f64> = (0..n_dims)
610 .map(|d| {
611 let lo = self.bounds[d].0;
612 let hi = self.bounds[d].1;
613 lo + rng.random::<f64>() * (hi - lo)
614 })
615 .collect();
616 let y = objective(&x_rand);
617 history.push((iter + 1, y));
618 if y < best_y {
619 best_y = y;
620 best_x = Some(Array1::from_vec(x_rand.clone()));
621 }
622 x_history.push(x_rand);
623 y_history.push(y);
624 continue;
625 }
626
627 let n_cands = self.config.n_candidates;
631 let mut best_acq = f64::NEG_INFINITY;
632 let mut best_candidate = vec![0.0f64; n_dims];
633
634 for _ in 0..n_cands {
636 let cand: Vec<f64> = (0..n_dims)
637 .map(|d| {
638 let lo = self.bounds[d].0;
639 let hi = self.bounds[d].1;
640 lo + rng.random::<f64>() * (hi - lo)
641 })
642 .collect();
643
644 let acq =
645 ei_random_forest(&cand, &surrogate, best_y, self.config.xi).unwrap_or(0.0);
646 if acq > best_acq {
647 best_acq = acq;
648 best_candidate = cand;
649 }
650 }
651
652 if let Some(ref inc) = best_x {
654 let n_local = self.config.n_local_search;
655 let inc_vec: Vec<f64> = inc.iter().copied().collect();
656
657 for step_size in [0.1, 0.05, 0.01] {
658 for _ in 0..n_local {
659 let neighbor: Vec<f64> = inc_vec
660 .iter()
661 .zip(self.bounds.iter())
662 .map(|(&xi, &(lo, hi))| {
663 let range = hi - lo;
664 let perturb = (rng.random::<f64>() - 0.5) * 2.0 * step_size * range;
665 (xi + perturb).clamp(lo, hi)
666 })
667 .collect();
668
669 let acq = ei_random_forest(&neighbor, &surrogate, best_y, self.config.xi)
670 .unwrap_or(0.0);
671 if acq > best_acq {
672 best_acq = acq;
673 best_candidate = neighbor;
674 }
675 }
676 }
677 }
678
679 let y_next = objective(&best_candidate);
683 history.push((iter + 1, y_next));
684
685 if y_next < best_y {
686 best_y = y_next;
687 best_x = Some(Array1::from_vec(best_candidate.clone()));
688 }
689
690 x_history.push(best_candidate);
691 y_history.push(y_next);
692
693 if self.config.verbose >= 2 {
694 println!(
695 "[SMAC] iter={} acq={:.4} f={:.6} best={:.6}",
696 iter + 1,
697 best_acq,
698 y_next,
699 best_y
700 );
701 }
702 }
703
704 if self.config.verbose >= 1 {
705 println!(
706 "[SMAC] Done. n_evals={} best_f={:.6}",
707 history.len(),
708 best_y
709 );
710 }
711
712 let x_best = best_x.unwrap_or_else(|| Array1::zeros(n_dims));
713
714 Ok(SmacResult {
715 x_best,
716 f_best: best_y,
717 n_evals: history.len(),
718 history,
719 })
720 }
721}
722
723fn erf_approx(x: f64) -> f64 {
728 let p = 0.3275911_f64;
729 let (a1, a2, a3, a4, a5) = (
730 0.254829592_f64,
731 -0.284496736_f64,
732 1.421413741_f64,
733 -1.453152027_f64,
734 1.061405429_f64,
735 );
736 let sign = if x < 0.0 { -1.0 } else { 1.0 };
737 let xa = x.abs();
738 let t = 1.0 / (1.0 + p * xa);
739 let poly = (((a5 * t + a4) * t + a3) * t + a2) * t + a1;
740 sign * (1.0 - poly * t * (-xa * xa).exp())
741}
742
743fn norm_cdf(z: f64) -> f64 {
744 0.5 * (1.0 + erf_approx(z / std::f64::consts::SQRT_2))
745}
746
747fn norm_pdf(z: f64) -> f64 {
748 (-0.5 * z * z).exp() / (2.0 * std::f64::consts::PI).sqrt()
749}
750
751#[cfg(test)]
756mod tests {
757 use super::*;
758 use scirs2_core::ndarray::{Array1, Array2};
759
760 fn make_simple_data() -> (Array2<f64>, Array1<f64>) {
761 let xs: Vec<f64> = vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5];
763 let ys: Vec<f64> = xs.iter().map(|&x| x * x).collect();
764 let x = Array2::from_shape_vec((8, 1), xs).expect("shape");
765 let y = Array1::from_vec(ys);
766 (x, y)
767 }
768
769 #[test]
770 fn test_rf_surrogate_fit_predict() {
771 let (x, y) = make_simple_data();
772 let mut rf = RandomForestSurrogate::new(20, 8, 2, 0, 42);
773 rf.fit(&x, &y).expect("fit");
774
775 assert!(rf.is_fitted());
776 assert_eq!(rf.n_estimators(), 20);
777
778 let x_test = Array2::from_shape_vec((3, 1), vec![0.25, 1.25, 2.75]).expect("shape");
779 let (mean, var) = rf.predict(&x_test).expect("predict");
780
781 for i in 0..3 {
783 assert!(mean[i].is_finite(), "mean[{}] must be finite", i);
784 assert!(var[i] >= 0.0, "var[{}] must be non-negative", i);
785 }
786
787 assert!(
789 (mean[1] - 1.5625).abs() < 2.0,
790 "mean[1]={:.4} expected ~1.56",
791 mean[1]
792 );
793 }
794
795 #[test]
796 fn test_rf_predict_monotone_variance() {
797 let (x, y) = make_simple_data();
798 let mut rf = RandomForestSurrogate::new(30, 8, 2, 0, 99);
799 rf.fit(&x, &y).expect("fit");
800
801 let x_near = Array2::from_shape_vec((1, 1), vec![1.0]).expect("shape");
803 let x_far = Array2::from_shape_vec((1, 1), vec![10.0]).expect("shape");
804 let (_, var_near) = rf.predict(&x_near).expect("predict near");
805 let (_, var_far) = rf.predict(&x_far).expect("predict far");
806
807 assert!(var_far[0] >= 0.0);
810 assert!(var_near[0] >= 0.0);
811 }
812
813 #[test]
814 fn test_ei_rf_non_negative() {
815 let (x, y) = make_simple_data();
816 let mut rf = RandomForestSurrogate::new(20, 8, 2, 0, 7);
817 rf.fit(&x, &y).expect("fit");
818
819 let cands = vec![0.3, 1.7, 2.8];
820 for c in cands {
821 let ei = ei_random_forest(&[c], &rf, 2.0, 0.01).expect("ei");
822 assert!(ei >= 0.0, "EI must be non-negative at x={}", c);
823 }
824 }
825
826 #[test]
827 fn test_smac_minimizes_quadratic() {
828 let bounds = vec![(-3.0_f64, 3.0_f64)];
829 let config = SmacConfig {
830 n_initial: 5,
831 n_iterations: 25,
832 n_candidates: 50,
833 n_local_search: 3,
834 n_estimators: 20,
835 seed: Some(42),
836 verbose: 0,
837 ..Default::default()
838 };
839 let mut smac = SmacOptimizer::new(bounds, config).expect("build smac");
840 let result = smac.optimize(|x: &[f64]| x[0].powi(2), 25).expect("opt");
841
842 assert!(result.f_best.is_finite());
843 assert!(
844 result.f_best < 0.5,
845 "Expected f_best < 0.5, got {}",
846 result.f_best
847 );
848 assert_eq!(result.n_evals, 25);
849 }
850
851 #[test]
852 fn test_smac_2d_rosenbrock_trend() {
853 let bounds = vec![(-2.0_f64, 2.0_f64), (-2.0, 2.0)];
854 let config = SmacConfig {
855 n_initial: 8,
856 n_iterations: 30,
857 n_candidates: 80,
858 n_estimators: 25,
859 seed: Some(123),
860 verbose: 0,
861 ..Default::default()
862 };
863 let mut smac = SmacOptimizer::new(bounds, config).expect("build smac");
864 let result = smac
866 .optimize(
867 |x: &[f64]| (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2),
868 30,
869 )
870 .expect("opt");
871
872 assert!(result.f_best.is_finite());
873 assert!(result.f_best < 1000.0, "f_best={}", result.f_best);
875 }
876
877 #[test]
878 fn test_rf_unfitted_predict_error() {
879 let rf = RandomForestSurrogate::new(10, 5, 2, 0, 0);
880 let x = Array2::zeros((2, 1));
881 assert!(rf.predict(&x).is_err());
882 }
883}