surface_lib/calibration/
pipeline.rs

1use crate::calibration::config::OptimizationConfig;
2use crate::calibration::types::{MarketDataRow, ModelCalibrator};
3// Note: HashMap removed as param_map is no longer used
4use cmaes_lbfgsb::cmaes::{canonical_cmaes_optimize, CmaesCanonicalConfig};
5use cmaes_lbfgsb::lbfgsb_optimize::lbfgsb_optimize;
6
7/// A simplified calibration process for surface models
8pub struct CalibrationProcess {
9    model: Box<dyn ModelCalibrator>,
10    config: OptimizationConfig,
11    market_data: Vec<MarketDataRow>,
12    initial_guess: Option<Vec<f64>>,
13}
14
15impl CalibrationProcess {
16    pub fn new(
17        model: Box<dyn ModelCalibrator>,
18        config: OptimizationConfig,
19        market_data: Vec<MarketDataRow>,
20    ) -> Self {
21        Self {
22            model,
23            config,
24            market_data,
25            initial_guess: None,
26        }
27    }
28
29    /// Set initial guess for optimization
30    pub fn with_initial_guess(mut self, guess: Vec<f64>) -> Self {
31        self.initial_guess = Some(guess);
32        self
33    }
34
35    /// Run the calibration process and return the best parameters
36    pub fn run(&self) -> (f64, Vec<f64>) {
37        let (best_obj, best_params) = calibrate_model(
38            &*self.model,
39            &self.market_data,
40            &self.config,
41            self.initial_guess.clone(),
42        );
43        (best_obj, best_params)
44    }
45}
46
47/// Advanced optimization function combining CMA-ES for global search and L-BFGS-B for local refinement.
48/// Uses relaxed bounds and objective function for global search, then standard bounds for refinement.
49pub fn calibrate_model(
50    model: &dyn ModelCalibrator,
51    market_data: &[MarketDataRow],
52    config: &OptimizationConfig,
53    initial_guess: Option<Vec<f64>>,
54) -> (f64, Vec<f64>) {
55    // Standard bounds and objective used for L-BFGS-B
56    let bounds = model.param_bounds();
57    let obj_fn = |x: &[f64]| model.evaluate_objective(x, market_data);
58
59    // 1) CMA-ES approach, either a "mini CMA-ES" around the initial guess or full CMA-ES if none provided.
60    // Use relaxed bounds and objective function for the global search
61    let (best_obj, best_sol) = {
62        // Use the same bounds and objective for CMA-ES
63        let relaxed_bounds = model.param_bounds();
64        let relaxed_obj_fn = |x: &[f64]| model.evaluate_objective(x, market_data);
65
66        // Prepare CMA-ES config with all the sophisticated settings
67        let cmaes_config = CmaesCanonicalConfig {
68            population_size: config.pop_size,
69            max_generations: config.max_gen,
70            seed: config.cmaes.seed.unwrap_or(123456),
71            c1: None, // Use defaults for now - could be added to config later
72            c_mu: None,
73            c_sigma: None,
74            d_sigma: None,
75            parallel_eval: config.cmaes.parallel_eval,
76            verbosity: config.cmaes.verbosity,
77            ipop_restarts: config.cmaes.ipop_restarts,
78            ipop_increase_factor: config.cmaes.ipop_increase_factor,
79            bipop_restarts: config.cmaes.bipop_restarts,
80            total_evals_budget: config.cmaes.total_evals_budget,
81            use_subrun_budgeting: config.cmaes.use_subrun_budgeting,
82            alpha_mu: None,
83            hsig_threshold_factor: None,
84            bipop_small_population_factor: None,
85            bipop_small_budget_factor: None,
86            bipop_large_budget_factor: None,
87            bipop_large_pop_increase_factor: None,
88            max_bound_iterations: None,
89            eig_precision_threshold: None,
90            min_eig_value: None,
91            matrix_op_threshold: None,
92            stagnation_limit: None,
93            min_sigma: None,
94        };
95
96        // If we have an initial guess, check if we should run mini CMA-ES or go straight to L-BFGS-B
97        if let Some(ref guess) = initial_guess {
98            // Check if mini_cmaes_on_refinement is enabled
99            let use_mini_cmaes = config.cmaes.mini_cmaes_on_refinement;
100
101            if use_mini_cmaes {
102                if config.cmaes.verbosity > 0 {
103                    println!(
104                        "Using provided initial guess => launching mini CMA-ES around it. \
105                         Then local L-BFGS refinement."
106                    );
107                }
108
109                // Evaluate the guess if you want to log it
110                let guess_obj = relaxed_obj_fn(guess);
111                if config.cmaes.verbosity > 0 {
112                    println!("  Initial guess objective = {:.6}", guess_obj);
113                }
114
115                // Run mini-CMA-ES
116                let cmaes_result = canonical_cmaes_optimize(
117                    relaxed_obj_fn,
118                    relaxed_bounds,
119                    cmaes_config,
120                    // We pass the guess as the initial distribution center
121                    Some(guess.clone()),
122                );
123
124                // Get best solution from relaxed objective, evaluate with standard objective
125                let (_, relaxed_params) = cmaes_result.best_solution;
126                let standard_obj = obj_fn(&relaxed_params);
127                (standard_obj, relaxed_params)
128            } else {
129                // Skip mini CMA-ES and use the initial guess directly for L-BFGS-B
130                if config.cmaes.verbosity > 0 {
131                    println!(
132                        "Using provided initial guess => skipping mini CMA-ES and proceeding directly to L-BFGS-B."
133                    );
134                }
135
136                // Evaluate the guess to get initial objective value
137                let guess_obj = obj_fn(guess);
138                if config.cmaes.verbosity > 0 {
139                    println!("  Initial guess objective = {:.6}", guess_obj);
140                }
141
142                (guess_obj, guess.clone())
143            }
144        } else {
145            // Otherwise do the full CMA-ES as before
146            if config.cmaes.verbosity > 0 {
147                println!("No initial guess provided => running full CMA-ES with BIPOP restarts");
148            }
149
150            let cmaes_result =
151                canonical_cmaes_optimize(relaxed_obj_fn, relaxed_bounds, cmaes_config, None);
152
153            // Get best solution from relaxed objective, evaluate with standard objective
154            let (_, relaxed_params) = cmaes_result.best_solution;
155            let standard_obj = obj_fn(&relaxed_params);
156            (standard_obj, relaxed_params)
157        }
158    };
159
160    // 2) Local refinement of the best solution with L-BFGS-B (if enabled)
161    if config.cmaes.lbfgsb_enabled {
162        if config.cmaes.verbosity > 0 {
163            println!("Running L-BFGS-B refinement on best CMA-ES solution...");
164        }
165
166        let mut refined_solution = best_sol.clone();
167        let refine_res = lbfgsb_optimize(
168            &mut refined_solution,
169            bounds,
170            &obj_fn,
171            config.cmaes.lbfgsb_max_iterations,
172            config.tolerance,
173            if config.cmaes.verbosity >= 1 {
174                Some(|_current_x: &[f64], current_obj: f64| {
175                    println!("L-BFGS-B iteration => objective = {:.6}", current_obj);
176                })
177            } else {
178                None
179            },
180            None, // Use default config
181        );
182
183        match refine_res {
184            Ok((loc_obj, loc_sol)) => {
185                if loc_obj < best_obj {
186                    if config.cmaes.verbosity > 0 {
187                        println!(
188                            "L-BFGS-B improved objective: {:.6} -> {:.6}",
189                            best_obj, loc_obj
190                        );
191                    }
192                    (loc_obj, loc_sol)
193                } else {
194                    if config.cmaes.verbosity > 0 {
195                        println!("L-BFGS-B did not improve objective, keeping CMA-ES solution");
196                    }
197                    (best_obj, best_sol)
198                }
199            }
200            Err(e) => {
201                if config.cmaes.verbosity > 0 {
202                    println!("L-BFGS-B failed: {:?}, keeping CMA-ES solution", e);
203                }
204                (best_obj, best_sol)
205            }
206        }
207    } else {
208        if config.cmaes.verbosity > 0 {
209            println!("L-BFGS-B refinement disabled, using CMA-ES solution directly");
210        }
211        (best_obj, best_sol)
212    }
213}
214
215/// Generic adaptive calibration wrapper
216pub fn calibrate_model_adaptive(
217    mut model: Box<dyn ModelCalibrator>,
218    market_data: &[MarketDataRow],
219    config: &OptimizationConfig,
220    initial_guess: Option<Vec<f64>>,
221) -> (f64, Vec<f64>, Vec<(f64, f64)>) {
222    if !config.adaptive_bounds.enabled {
223        let (obj, params) = calibrate_model(&*model, market_data, config, initial_guess);
224        let bounds = model.param_bounds().to_vec();
225        return (obj, params, bounds);
226    }
227
228    let mut best_obj = f64::MAX;
229    let mut best_params = Vec::new();
230
231    for iter in 0..config.adaptive_bounds.max_iterations {
232        let (obj, params) = calibrate_model(&*model, market_data, config, initial_guess.clone());
233        if obj < best_obj {
234            best_obj = obj;
235            best_params = params.clone();
236        }
237        let adjusted = model.expand_bounds_if_needed(
238            &params,
239            config.adaptive_bounds.proximity_threshold,
240            config.adaptive_bounds.expansion_factor,
241        );
242
243        if config.cmaes.verbosity > 0 {
244            if adjusted {
245                println!(
246                    "Adaptive iteration {}: Expanded bounds for next iteration",
247                    iter + 1
248                );
249            } else {
250                println!(
251                    "Adaptive iteration {}: No expansion needed, stopping early",
252                    iter + 1
253                );
254            }
255        }
256
257        if !adjusted {
258            break;
259        }
260    }
261
262    let bounds = model.param_bounds().to_vec();
263    (best_obj, best_params, bounds)
264}