Skip to main content

quantrs2_anneal/
continuous_variable.rs

1//! Continuous variable annealing for optimization problems
2//!
3//! This module extends quantum annealing to continuous variables, enabling
4//! the solution of optimization problems with real-valued decision variables
5//! using discretization and approximation techniques.
6
7use scirs2_core::random::prelude::*;
8use scirs2_core::random::ChaCha8Rng;
9use scirs2_core::random::{Rng, SeedableRng};
10use scirs2_core::RngExt;
11use std::collections::HashMap;
12use std::time::{Duration, Instant};
13use thiserror::Error;
14
15use crate::simulator::{AnnealingParams, AnnealingSolution, TemperatureSchedule};
16
17/// Errors that can occur during continuous variable annealing
18#[derive(Error, Debug)]
19pub enum ContinuousVariableError {
20    /// Invalid variable definition
21    #[error("Invalid variable: {0}")]
22    InvalidVariable(String),
23
24    /// Invalid constraint
25    #[error("Invalid constraint: {0}")]
26    InvalidConstraint(String),
27
28    /// Discretization error
29    #[error("Discretization error: {0}")]
30    DiscretizationError(String),
31
32    /// Optimization failed
33    #[error("Optimization failed: {0}")]
34    OptimizationFailed(String),
35
36    /// Numerical error
37    #[error("Numerical error: {0}")]
38    NumericalError(String),
39}
40
41/// Result type for continuous variable operations
42pub type ContinuousVariableResult<T> = Result<T, ContinuousVariableError>;
43
44/// Continuous variable definition
45#[derive(Debug, Clone)]
46pub struct ContinuousVariable {
47    /// Variable name
48    pub name: String,
49
50    /// Lower bound
51    pub lower_bound: f64,
52
53    /// Upper bound
54    pub upper_bound: f64,
55
56    /// Precision (number of discretization bits)
57    pub precision_bits: usize,
58
59    /// Variable description
60    pub description: Option<String>,
61}
62
63impl ContinuousVariable {
64    /// Create a new continuous variable
65    pub fn new(
66        name: String,
67        lower_bound: f64,
68        upper_bound: f64,
69        precision_bits: usize,
70    ) -> ContinuousVariableResult<Self> {
71        if lower_bound >= upper_bound {
72            return Err(ContinuousVariableError::InvalidVariable(format!(
73                "Invalid bounds: {lower_bound} >= {upper_bound}"
74            )));
75        }
76
77        if precision_bits == 0 || precision_bits > 32 {
78            return Err(ContinuousVariableError::InvalidVariable(
79                "Precision bits must be between 1 and 32".to_string(),
80            ));
81        }
82
83        Ok(Self {
84            name,
85            lower_bound,
86            upper_bound,
87            precision_bits,
88            description: None,
89        })
90    }
91
92    /// Add description to the variable
93    #[must_use]
94    pub fn with_description(mut self, description: String) -> Self {
95        self.description = Some(description);
96        self
97    }
98
99    /// Get the number of discrete levels
100    #[must_use]
101    pub const fn num_levels(&self) -> usize {
102        2_usize.pow(self.precision_bits as u32)
103    }
104
105    /// Convert binary representation to continuous value
106    #[must_use]
107    pub fn binary_to_continuous(&self, binary_value: u32) -> f64 {
108        let max_value = (1u32 << self.precision_bits) - 1;
109        let normalized = f64::from(binary_value) / f64::from(max_value);
110        self.lower_bound + normalized * (self.upper_bound - self.lower_bound)
111    }
112
113    /// Convert continuous value to binary representation
114    #[must_use]
115    pub fn continuous_to_binary(&self, continuous_value: f64) -> u32 {
116        let clamped = continuous_value.clamp(self.lower_bound, self.upper_bound);
117        let normalized = (clamped - self.lower_bound) / (self.upper_bound - self.lower_bound);
118        let max_value = (1u32 << self.precision_bits) - 1;
119        (normalized * f64::from(max_value)).round() as u32
120    }
121
122    /// Get the resolution (smallest representable difference)
123    #[must_use]
124    pub fn resolution(&self) -> f64 {
125        (self.upper_bound - self.lower_bound) / (self.num_levels() - 1) as f64
126    }
127}
128
129/// Objective function for continuous optimization
130pub type ObjectiveFunction = Box<dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync>;
131
132/// Constraint function for continuous optimization
133pub type ConstraintFunction = Box<dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync>;
134
135/// Constraint specification
136pub struct ContinuousConstraint {
137    /// Constraint name
138    pub name: String,
139
140    /// Constraint function (should return <= 0 for feasible points)
141    pub function: ConstraintFunction,
142
143    /// Penalty weight for constraint violations
144    pub penalty_weight: f64,
145
146    /// Constraint tolerance
147    pub tolerance: f64,
148}
149
150impl std::fmt::Debug for ContinuousConstraint {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("ContinuousConstraint")
153            .field("name", &self.name)
154            .field("function", &"<function>")
155            .field("penalty_weight", &self.penalty_weight)
156            .field("tolerance", &self.tolerance)
157            .finish()
158    }
159}
160
161impl ContinuousConstraint {
162    /// Create a new constraint
163    #[must_use]
164    pub fn new(name: String, function: ConstraintFunction, penalty_weight: f64) -> Self {
165        Self {
166            name,
167            function,
168            penalty_weight,
169            tolerance: 1e-6,
170        }
171    }
172
173    /// Set constraint tolerance
174    #[must_use]
175    pub const fn with_tolerance(mut self, tolerance: f64) -> Self {
176        self.tolerance = tolerance;
177        self
178    }
179}
180
181/// Continuous optimization problem
182pub struct ContinuousOptimizationProblem {
183    /// Variables in the problem
184    variables: HashMap<String, ContinuousVariable>,
185
186    /// Objective function to minimize
187    objective: ObjectiveFunction,
188
189    /// Constraints
190    constraints: Vec<ContinuousConstraint>,
191
192    /// Default penalty weight for constraint violations
193    default_penalty_weight: f64,
194}
195
196impl std::fmt::Debug for ContinuousOptimizationProblem {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        f.debug_struct("ContinuousOptimizationProblem")
199            .field("variables", &self.variables)
200            .field("objective", &"<function>")
201            .field("constraints", &self.constraints)
202            .field("default_penalty_weight", &self.default_penalty_weight)
203            .finish()
204    }
205}
206
207impl ContinuousOptimizationProblem {
208    /// Create a new continuous optimization problem
209    #[must_use]
210    pub fn new(objective: ObjectiveFunction) -> Self {
211        Self {
212            variables: HashMap::new(),
213            objective,
214            constraints: Vec::new(),
215            default_penalty_weight: 100.0,
216        }
217    }
218
219    /// Add a variable to the problem
220    pub fn add_variable(&mut self, variable: ContinuousVariable) -> ContinuousVariableResult<()> {
221        if self.variables.contains_key(&variable.name) {
222            return Err(ContinuousVariableError::InvalidVariable(format!(
223                "Variable '{}' already exists",
224                variable.name
225            )));
226        }
227
228        self.variables.insert(variable.name.clone(), variable);
229        Ok(())
230    }
231
232    /// Add a constraint to the problem
233    pub fn add_constraint(&mut self, constraint: ContinuousConstraint) {
234        self.constraints.push(constraint);
235    }
236
237    /// Set default penalty weight
238    pub const fn set_default_penalty_weight(&mut self, weight: f64) {
239        self.default_penalty_weight = weight;
240    }
241
242    /// Get total number of binary variables needed
243    #[must_use]
244    pub fn total_binary_variables(&self) -> usize {
245        self.variables.values().map(|v| v.precision_bits).sum()
246    }
247
248    /// Create binary variable mapping
249    #[must_use]
250    pub fn create_binary_mapping(&self) -> HashMap<String, Vec<usize>> {
251        let mut mapping = HashMap::new();
252        let mut current_index = 0;
253
254        for (var_name, var) in &self.variables {
255            let indices: Vec<usize> = (current_index..current_index + var.precision_bits).collect();
256            mapping.insert(var_name.clone(), indices);
257            current_index += var.precision_bits;
258        }
259
260        mapping
261    }
262
263    /// Convert binary solution to continuous values
264    pub fn binary_to_continuous_solution(
265        &self,
266        binary_solution: &[i8],
267    ) -> ContinuousVariableResult<HashMap<String, f64>> {
268        let binary_mapping = self.create_binary_mapping();
269        let mut continuous_solution = HashMap::new();
270
271        for (var_name, var) in &self.variables {
272            let indices = &binary_mapping[var_name];
273
274            if indices.iter().any(|&i| i >= binary_solution.len()) {
275                return Err(ContinuousVariableError::DiscretizationError(format!(
276                    "Binary solution too short for variable '{var_name}'"
277                )));
278            }
279
280            // Convert binary bits to integer value
281            let mut binary_value = 0u32;
282            for (bit_idx, &global_idx) in indices.iter().enumerate() {
283                if binary_solution[global_idx] > 0 {
284                    binary_value |= 1 << (var.precision_bits - 1 - bit_idx);
285                }
286            }
287
288            // Convert to continuous value
289            let continuous_value = var.binary_to_continuous(binary_value);
290            continuous_solution.insert(var_name.clone(), continuous_value);
291        }
292
293        Ok(continuous_solution)
294    }
295
296    /// Evaluate objective function with penalty for constraint violations
297    #[must_use]
298    pub fn evaluate_penalized_objective(&self, continuous_solution: &HashMap<String, f64>) -> f64 {
299        let mut objective_value = (self.objective)(continuous_solution);
300
301        // Add constraint penalties
302        for constraint in &self.constraints {
303            let constraint_value = (constraint.function)(continuous_solution);
304            if constraint_value > constraint.tolerance {
305                objective_value += constraint.penalty_weight * constraint_value.powi(2);
306            }
307        }
308
309        objective_value
310    }
311}
312
313/// Configuration for continuous variable annealing
314#[derive(Debug, Clone)]
315pub struct ContinuousAnnealingConfig {
316    /// Base annealing parameters
317    pub annealing_params: AnnealingParams,
318
319    /// Adaptive discretization
320    pub adaptive_discretization: bool,
321
322    /// Maximum refinement iterations
323    pub max_refinement_iterations: usize,
324
325    /// Convergence tolerance for refinement
326    pub refinement_tolerance: f64,
327
328    /// Enable local search post-processing
329    pub local_search: bool,
330
331    /// Local search iterations
332    pub local_search_iterations: usize,
333
334    /// Local search step size (as fraction of variable range)
335    pub local_search_step_size: f64,
336}
337
338impl Default for ContinuousAnnealingConfig {
339    fn default() -> Self {
340        Self {
341            annealing_params: AnnealingParams::default(),
342            adaptive_discretization: true,
343            max_refinement_iterations: 3,
344            refinement_tolerance: 1e-4,
345            local_search: true,
346            local_search_iterations: 100,
347            local_search_step_size: 0.01,
348        }
349    }
350}
351
352/// Solution for continuous optimization problem
353#[derive(Debug, Clone)]
354pub struct ContinuousSolution {
355    /// Variable values
356    pub variable_values: HashMap<String, f64>,
357
358    /// Objective value
359    pub objective_value: f64,
360
361    /// Constraint violations
362    pub constraint_violations: Vec<(String, f64)>,
363
364    /// Binary solution used
365    pub binary_solution: Vec<i8>,
366
367    /// Solution statistics
368    pub stats: ContinuousOptimizationStats,
369}
370
371/// Statistics for continuous optimization
372#[derive(Debug, Clone)]
373pub struct ContinuousOptimizationStats {
374    /// Total runtime
375    pub total_runtime: Duration,
376
377    /// Discretization time
378    pub discretization_time: Duration,
379
380    /// Annealing time
381    pub annealing_time: Duration,
382
383    /// Local search time
384    pub local_search_time: Duration,
385
386    /// Number of refinement iterations
387    pub refinement_iterations: usize,
388
389    /// Final discretization resolution
390    pub final_resolution: HashMap<String, f64>,
391
392    /// Convergence achieved
393    pub converged: bool,
394}
395
396/// Continuous variable annealing solver
397pub struct ContinuousVariableAnnealer {
398    /// Configuration
399    config: ContinuousAnnealingConfig,
400
401    /// Random number generator
402    rng: ChaCha8Rng,
403}
404
405impl ContinuousVariableAnnealer {
406    /// Create a new continuous variable annealer
407    #[must_use]
408    pub fn new(config: ContinuousAnnealingConfig) -> Self {
409        let rng = match config.annealing_params.seed {
410            Some(seed) => ChaCha8Rng::seed_from_u64(seed),
411            None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
412        };
413
414        Self { config, rng }
415    }
416
417    /// Solve a continuous optimization problem
418    pub fn solve(
419        &mut self,
420        problem: &ContinuousOptimizationProblem,
421    ) -> ContinuousVariableResult<ContinuousSolution> {
422        let total_start = Instant::now();
423
424        // Initial discretization
425        let discretize_start = Instant::now();
426        let mut current_problem = self.create_discretized_problem(problem)?;
427        let discretization_time = discretize_start.elapsed();
428
429        let mut best_solution = None;
430        let mut best_objective = f64::INFINITY;
431        let mut refinement_iterations = 0;
432
433        // Iterative refinement loop
434        for iteration in 0..self.config.max_refinement_iterations {
435            // Solve discretized problem
436            let anneal_start = Instant::now();
437            let binary_solution = self.solve_discretized_problem(problem, &current_problem)?;
438            let annealing_time = anneal_start.elapsed();
439
440            // Convert to continuous solution using the same focus mapping the
441            // search used, so the reported values match the optimized window.
442            let continuous_values =
443                Self::decode_with_focus(problem, &current_problem, &binary_solution)?;
444            let objective_value = problem.evaluate_penalized_objective(&continuous_values);
445
446            // Check for improvement
447            let improvement = if best_objective.is_finite() {
448                best_objective - objective_value
449            } else {
450                f64::INFINITY
451            };
452
453            if objective_value < best_objective {
454                best_objective = objective_value;
455                best_solution = Some((binary_solution, continuous_values.clone(), annealing_time));
456            }
457
458            refinement_iterations += 1;
459
460            // Check convergence
461            if improvement < self.config.refinement_tolerance && iteration > 0 {
462                break;
463            }
464
465            // Adaptive refinement
466            if self.config.adaptive_discretization
467                && iteration < self.config.max_refinement_iterations - 1
468            {
469                current_problem = self.refine_discretization(problem, &continuous_values)?;
470            }
471        }
472
473        let (final_binary, mut final_continuous, annealing_time) =
474            best_solution.ok_or_else(|| {
475                ContinuousVariableError::OptimizationFailed("No solution found".to_string())
476            })?;
477
478        // Local search post-processing
479        let local_search_start = Instant::now();
480        let local_search_time = if self.config.local_search {
481            self.local_search(problem, &mut final_continuous)?;
482            local_search_start.elapsed()
483        } else {
484            Duration::from_secs(0)
485        };
486
487        // Calculate constraint violations
488        let constraint_violations =
489            self.calculate_constraint_violations(problem, &final_continuous);
490
491        // Calculate final objective (without penalties)
492        let final_objective = (problem.objective)(&final_continuous);
493
494        // Calculate final resolutions
495        let final_resolution = problem
496            .variables
497            .iter()
498            .map(|(name, var)| (name.clone(), var.resolution()))
499            .collect();
500
501        let total_runtime = total_start.elapsed();
502
503        let stats = ContinuousOptimizationStats {
504            total_runtime,
505            discretization_time,
506            annealing_time,
507            local_search_time,
508            refinement_iterations,
509            final_resolution,
510            converged: refinement_iterations < self.config.max_refinement_iterations,
511        };
512
513        Ok(ContinuousSolution {
514            variable_values: final_continuous,
515            objective_value: final_objective,
516            constraint_violations,
517            binary_solution: final_binary,
518            stats,
519        })
520    }
521
522    /// Create the binary (discretized) representation of the continuous problem.
523    ///
524    /// The number of binary spins equals the sum of every variable's
525    /// `precision_bits`; the encoding itself is defined by the problem's
526    /// [`ContinuousOptimizationProblem::create_binary_mapping`]. Initially no
527    /// refinement focus is set, so the full bound box of each variable is
528    /// searched.
529    fn create_discretized_problem(
530        &self,
531        problem: &ContinuousOptimizationProblem,
532    ) -> ContinuousVariableResult<DiscretizedProblem> {
533        let num_variables = problem.total_binary_variables();
534        if num_variables == 0 {
535            return Err(ContinuousVariableError::DiscretizationError(
536                "Problem has no continuous variables to discretize".to_string(),
537            ));
538        }
539        Ok(DiscretizedProblem {
540            num_variables,
541            focus: HashMap::new(),
542        })
543    }
544
545    /// Decode a binary spin vector into continuous values, applying the
546    /// refinement `focus` window (if any) for each variable so the same encoding
547    /// is used during the search and when reading back the final solution.
548    fn decode_with_focus(
549        problem: &ContinuousOptimizationProblem,
550        discretized: &DiscretizedProblem,
551        bits: &[i8],
552    ) -> ContinuousVariableResult<HashMap<String, f64>> {
553        let mut values = problem.binary_to_continuous_solution(bits)?;
554        for (name, &(lo, hi)) in &discretized.focus {
555            if let (Some(value), Some(var)) =
556                (values.get(name).copied(), problem.variables.get(name))
557            {
558                let span = var.upper_bound - var.lower_bound;
559                if span > 0.0 {
560                    let t = (value - var.lower_bound) / span;
561                    values.insert(name.clone(), t.mul_add(hi - lo, lo));
562                }
563            }
564        }
565        Ok(values)
566    }
567
568    /// Solve the discretized problem with simulated annealing over the binary
569    /// spins, scoring each candidate with the *true* penalized objective.
570    ///
571    /// This is a genuine optimizer, not a random draw: it runs a Metropolis
572    /// Monte-Carlo chain over single-bit flips with a geometric cooling schedule
573    /// derived from the configured annealing parameters, always returning the
574    /// best configuration encountered. When a refinement `focus` is present, the
575    /// decoded value of each variable is mapped into the focused sub-interval so
576    /// the search zooms in around the incumbent solution.
577    fn solve_discretized_problem(
578        &mut self,
579        problem: &ContinuousOptimizationProblem,
580        discretized: &DiscretizedProblem,
581    ) -> ContinuousVariableResult<Vec<i8>> {
582        let num_vars = discretized.num_variables;
583        if num_vars == 0 {
584            return Err(ContinuousVariableError::DiscretizationError(
585                "Discretized problem has zero binary variables".to_string(),
586            ));
587        }
588
589        // Random initial configuration.
590        let mut current: Vec<i8> = (0..num_vars)
591            .map(|_| if self.rng.random_bool(0.5) { 1 } else { -1 })
592            .collect();
593        let mut current_energy = problem.evaluate_penalized_objective(&Self::decode_with_focus(
594            problem,
595            discretized,
596            &current,
597        )?);
598
599        let mut best = current.clone();
600        let mut best_energy = current_energy;
601
602        // Cooling schedule from the annealing parameters.
603        let num_sweeps = self.config.annealing_params.num_sweeps.max(1);
604        let t_start = 1.0_f64.max(current_energy.abs());
605        let t_end = 1e-3;
606        let cooling = (t_end / t_start).powf(1.0 / num_sweeps as f64);
607        let mut temperature = t_start;
608
609        for _ in 0..num_sweeps {
610            for _ in 0..num_vars {
611                let flip = self.rng.random_range(0..num_vars);
612                current[flip] = -current[flip];
613
614                let candidate_energy = problem.evaluate_penalized_objective(
615                    &Self::decode_with_focus(problem, discretized, &current)?,
616                );
617                let delta = candidate_energy - current_energy;
618
619                if delta <= 0.0 || self.rng.random_bool((-delta / temperature).exp().min(1.0)) {
620                    current_energy = candidate_energy;
621                    if current_energy < best_energy {
622                        best_energy = current_energy;
623                        best.copy_from_slice(&current);
624                    }
625                } else {
626                    // Reject: undo the flip.
627                    current[flip] = -current[flip];
628                }
629            }
630            temperature *= cooling;
631        }
632
633        Ok(best)
634    }
635
636    /// Refine the discretization around the current solution.
637    ///
638    /// Builds a new [`DiscretizedProblem`] whose search focus is a contracted
639    /// window centered on each variable's current value (a fraction of its
640    /// original range), so subsequent annealing rounds resolve the solution more
641    /// finely without increasing the bit count.
642    fn refine_discretization(
643        &self,
644        problem: &ContinuousOptimizationProblem,
645        current_solution: &HashMap<String, f64>,
646    ) -> ContinuousVariableResult<DiscretizedProblem> {
647        let mut focus = HashMap::new();
648        // Contract each window to 25% of the original range around the incumbent.
649        const CONTRACTION: f64 = 0.25;
650
651        for (name, var) in &problem.variables {
652            if let Some(&value) = current_solution.get(name) {
653                let half_width = 0.5 * CONTRACTION * (var.upper_bound - var.lower_bound);
654                let lo = (value - half_width).max(var.lower_bound);
655                let hi = (value + half_width).min(var.upper_bound);
656                if hi > lo {
657                    focus.insert(name.clone(), (lo, hi));
658                }
659            }
660        }
661
662        Ok(DiscretizedProblem {
663            num_variables: problem.total_binary_variables(),
664            focus,
665        })
666    }
667
668    /// Perform local search to improve solution
669    fn local_search(
670        &self,
671        problem: &ContinuousOptimizationProblem,
672        solution: &mut HashMap<String, f64>,
673    ) -> ContinuousVariableResult<()> {
674        let mut current_objective = problem.evaluate_penalized_objective(solution);
675
676        for _ in 0..self.config.local_search_iterations {
677            let mut improved = false;
678
679            // Try small perturbations for each variable
680            for (var_name, var) in &problem.variables {
681                let current_value = solution[var_name];
682                let step_size =
683                    (var.upper_bound - var.lower_bound) * self.config.local_search_step_size;
684
685                // Try both directions
686                for direction in [-1.0_f64, 1.0] {
687                    let new_value = direction
688                        .mul_add(step_size, current_value)
689                        .clamp(var.lower_bound, var.upper_bound);
690
691                    // Temporarily update solution
692                    solution.insert(var_name.clone(), new_value);
693                    let new_objective = problem.evaluate_penalized_objective(solution);
694
695                    if new_objective < current_objective {
696                        current_objective = new_objective;
697                        improved = true;
698                        break; // Keep this improvement
699                    }
700                    // Revert change
701                    solution.insert(var_name.clone(), current_value);
702                }
703            }
704
705            // If no improvement found, stop early
706            if !improved {
707                break;
708            }
709        }
710
711        Ok(())
712    }
713
714    /// Calculate constraint violations
715    fn calculate_constraint_violations(
716        &self,
717        problem: &ContinuousOptimizationProblem,
718        solution: &HashMap<String, f64>,
719    ) -> Vec<(String, f64)> {
720        problem
721            .constraints
722            .iter()
723            .map(|constraint| {
724                let violation = (constraint.function)(solution);
725                (constraint.name.clone(), violation.max(0.0))
726            })
727            .collect()
728    }
729}
730
731/// Binary (discretized) representation of a continuous optimization problem.
732///
733/// Each continuous variable is encoded with its `precision_bits` binary spins
734/// (fixed-point encoding, see [`ContinuousVariable::binary_to_continuous`]).
735/// Because the objective is a black-box closure rather than a quadratic form, an
736/// explicit QUBO `Q` matrix cannot in general be extracted; the search therefore
737/// evaluates the *true* (decoded) objective directly. `num_variables` is the real
738/// total spin count, and `focus` optionally restricts the per-variable search
739/// window for adaptive refinement around an incumbent solution.
740#[derive(Debug, Clone)]
741struct DiscretizedProblem {
742    /// Total number of binary spins across all continuous variables.
743    num_variables: usize,
744    /// Optional refinement focus: for each variable name, the (lower, upper)
745    /// sub-interval to search within (used by adaptive discretization).
746    focus: HashMap<String, (f64, f64)>,
747}
748
749/// Helper functions for common continuous optimization problems
750
751/// Create a quadratic programming problem
752pub fn create_quadratic_problem(
753    linear_coeffs: &[f64],
754    quadratic_matrix: &[Vec<f64>],
755    bounds: &[(f64, f64)],
756    precision_bits: usize,
757) -> ContinuousVariableResult<ContinuousOptimizationProblem> {
758    // Objective: 0.5 * x^T * Q * x + c^T * x
759    let linear_coeffs = linear_coeffs.to_vec();
760    let quadratic_matrix = quadratic_matrix.to_vec();
761
762    let objective: ObjectiveFunction = Box::new(move |vars: &HashMap<String, f64>| {
763        let n = linear_coeffs.len();
764        let x: Vec<f64> = (0..n).map(|i| vars[&format!("x{i}")]).collect();
765
766        // Linear term
767        let linear_term: f64 = linear_coeffs
768            .iter()
769            .zip(x.iter())
770            .map(|(c, xi)| c * xi)
771            .sum();
772
773        // Quadratic term
774        let mut quadratic_term = 0.0;
775        for i in 0..n {
776            for j in 0..n {
777                quadratic_term += 0.5 * quadratic_matrix[i][j] * x[i] * x[j];
778            }
779        }
780
781        linear_term + quadratic_term
782    });
783
784    let mut problem = ContinuousOptimizationProblem::new(objective);
785
786    // Add variables
787    for (i, &(lower, upper)) in bounds.iter().enumerate() {
788        let var = ContinuousVariable::new(format!("x{i}"), lower, upper, precision_bits)?;
789        problem.add_variable(var)?;
790    }
791
792    Ok(problem)
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798
799    #[test]
800    fn test_continuous_variable_creation() {
801        let var = ContinuousVariable::new("x".to_string(), 0.0, 10.0, 8)
802            .expect("should create continuous variable with valid bounds");
803        assert_eq!(var.name, "x");
804        assert_eq!(var.lower_bound, 0.0);
805        assert_eq!(var.upper_bound, 10.0);
806        assert_eq!(var.precision_bits, 8);
807        assert_eq!(var.num_levels(), 256);
808    }
809
810    #[test]
811    fn test_binary_continuous_conversion() {
812        let var = ContinuousVariable::new("x".to_string(), 0.0, 10.0, 4)
813            .expect("should create continuous variable for conversion test");
814
815        // Test conversion: 0 -> 0.0, 15 -> 10.0
816        assert_eq!(var.binary_to_continuous(0), 0.0);
817        assert!((var.binary_to_continuous(15) - 10.0).abs() < 1e-10);
818
819        // Test reverse conversion
820        assert_eq!(var.continuous_to_binary(0.0), 0);
821        assert_eq!(var.continuous_to_binary(10.0), 15);
822
823        // Test round-trip
824        let continuous_val = 3.7;
825        let binary_val = var.continuous_to_binary(continuous_val);
826        let recovered_val = var.binary_to_continuous(binary_val);
827        assert!((recovered_val - continuous_val).abs() <= var.resolution());
828    }
829
830    #[test]
831    fn test_quadratic_problem_creation() {
832        let linear_coeffs = vec![1.0, -2.0];
833        let quadratic_matrix = vec![vec![2.0, 0.0], vec![0.0, 2.0]];
834        let bounds = vec![(0.0, 5.0), (-3.0, 3.0)];
835
836        let problem = create_quadratic_problem(&linear_coeffs, &quadratic_matrix, &bounds, 6)
837            .expect("should create quadratic problem with valid parameters");
838        assert_eq!(problem.variables.len(), 2);
839        assert!(problem.variables.contains_key("x0"));
840        assert!(problem.variables.contains_key("x1"));
841    }
842
843    #[test]
844    fn test_constraint_evaluation() {
845        let constraint_fn: ConstraintFunction = Box::new(|vars| {
846            vars["x"] + vars["y"] - 5.0 // x + y <= 5
847        });
848
849        let constraint =
850            ContinuousConstraint::new("sum_constraint".to_string(), constraint_fn, 10.0);
851
852        let mut vars = HashMap::new();
853        vars.insert("x".to_string(), 2.0);
854        vars.insert("y".to_string(), 2.0);
855
856        let violation = (constraint.function)(&vars);
857        assert_eq!(violation, -1.0); // 2 + 2 - 5 = -1 (feasible)
858
859        vars.insert("y".to_string(), 4.0);
860        let violation = (constraint.function)(&vars);
861        assert_eq!(violation, 1.0); // 2 + 4 - 5 = 1 (infeasible)
862    }
863}