1use crate::optimization::constraints::{ConstraintType, Expression};
7use scirs2_core::ndarray::{Array1, Array2};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[cfg(feature = "scirs")]
12use crate::scirs_stub::{
13 scirs2_linalg::norm::Norm,
14 scirs2_optimization::{OptimizationProblem, Optimizer},
15};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PenaltyConfig {
20 pub initial_weight: f64,
22 pub min_weight: f64,
24 pub max_weight: f64,
26 pub adjustment_factor: f64,
28 pub violation_tolerance: f64,
30 pub max_iterations: usize,
32 pub adaptive_scaling: bool,
34 pub penalty_type: PenaltyType,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum PenaltyType {
41 Quadratic,
43 Linear,
45 LogBarrier,
47 Exponential,
49 AugmentedLagrangian,
51}
52
53impl Default for PenaltyConfig {
54 fn default() -> Self {
55 Self {
56 initial_weight: 1.0,
57 min_weight: 0.001,
58 max_weight: 1000.0,
59 adjustment_factor: 2.0,
60 violation_tolerance: 1e-6,
61 max_iterations: 100,
62 adaptive_scaling: true,
63 penalty_type: PenaltyType::Quadratic,
64 }
65 }
66}
67
68pub struct PenaltyOptimizer {
70 config: PenaltyConfig,
71 constraint_weights: HashMap<String, f64>,
72 violation_history: Vec<ConstraintViolation>,
73 #[cfg(feature = "scirs")]
74 optimizer: Option<Box<dyn Optimizer>>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ConstraintViolation {
80 pub constraint_name: String,
81 pub violation_amount: f64,
82 pub penalty_weight: f64,
83 pub iteration: usize,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct PenaltyOptimizationResult {
89 pub optimal_weights: HashMap<String, f64>,
90 pub final_violations: HashMap<String, f64>,
91 pub converged: bool,
92 pub iterations: usize,
93 pub objective_value: f64,
94 pub constraint_satisfaction: f64,
95}
96
97impl PenaltyOptimizer {
98 pub fn new(config: PenaltyConfig) -> Self {
100 Self {
101 config,
102 constraint_weights: HashMap::new(),
103 violation_history: Vec::new(),
104 #[cfg(feature = "scirs")]
105 optimizer: None,
106 }
107 }
108
109 pub fn initialize_weights(&mut self, constraints: &[String]) {
111 for constraint in constraints {
112 self.constraint_weights
113 .insert(constraint.clone(), self.config.initial_weight);
114 }
115
116 #[cfg(feature = "scirs")]
117 {
118 use crate::scirs_stub::scirs2_optimization::gradient::LBFGS;
120 self.optimizer = Some(Box::new(LBFGS::new(constraints.len())));
121 }
122 }
123
124 pub fn optimize_penalties(
126 &mut self,
127 model: &CompiledModel,
128 sample_results: &[(Vec<bool>, f64)],
129 ) -> Result<PenaltyOptimizationResult, Box<dyn std::error::Error>> {
130 let mut iteration = 0;
131 let mut converged = false;
132
133 while iteration < self.config.max_iterations && !converged {
134 let violations = self.evaluate_violations(model, sample_results)?;
136
137 let max_violation = violations.values().map(|v| v.abs()).fold(0.0, f64::max);
139
140 if max_violation < self.config.violation_tolerance {
141 converged = true;
142 break;
143 }
144
145 self.update_weights(&violations, iteration)?;
147
148 for (name, &violation) in &violations {
150 self.violation_history.push(ConstraintViolation {
151 constraint_name: name.clone(),
152 violation_amount: violation,
153 penalty_weight: self.constraint_weights[name],
154 iteration,
155 });
156 }
157
158 iteration += 1;
159 }
160
161 let final_violations = self.evaluate_violations(model, sample_results)?;
163 let objective_value = self.calculate_objective(model, sample_results)?;
164 let constraint_satisfaction = self.calculate_satisfaction_rate(&final_violations);
165
166 Ok(PenaltyOptimizationResult {
167 optimal_weights: self.constraint_weights.clone(),
168 final_violations,
169 converged,
170 iterations: iteration,
171 objective_value,
172 constraint_satisfaction,
173 })
174 }
175
176 fn evaluate_violations(
178 &self,
179 model: &CompiledModel,
180 sample_results: &[(Vec<bool>, f64)],
181 ) -> Result<HashMap<String, f64>, Box<dyn std::error::Error>> {
182 let mut violations = HashMap::new();
183
184 for (constraint_name, constraint_expr) in model.get_constraints() {
186 let mut total_violation = 0.0;
187 let mut count = 0;
188
189 for (assignment, _energy) in sample_results {
191 let violation = self.evaluate_constraint_violation(
192 constraint_expr,
193 assignment,
194 model.get_variable_map(),
195 )?;
196
197 total_violation += violation;
198 count += 1;
199 }
200
201 violations.insert(
203 constraint_name.clone(),
204 if count > 0 {
205 total_violation / count as f64
206 } else {
207 0.0
208 },
209 );
210 }
211
212 Ok(violations)
213 }
214
215 fn evaluate_constraint_violation(
221 &self,
222 constraint: &ConstraintExpr,
223 assignment: &[bool],
224 var_map: &HashMap<String, usize>,
225 ) -> Result<f64, Box<dyn std::error::Error>> {
226 let named_assignment: HashMap<String, bool> = var_map
227 .iter()
228 .filter_map(|(name, &index)| assignment.get(index).map(|&value| (name.clone(), value)))
229 .collect();
230
231 let value = constraint.violation(&named_assignment);
232
233 Ok(match self.config.penalty_type {
235 PenaltyType::Quadratic => value.powi(2),
236 PenaltyType::Linear => value.abs(),
237 PenaltyType::LogBarrier => {
238 if value > 0.0 {
239 -value.ln()
240 } else {
241 f64::INFINITY
242 }
243 }
244 PenaltyType::Exponential => value.exp_m1(),
245 PenaltyType::AugmentedLagrangian => {
246 value.mul_add(value, value.abs())
248 }
249 })
250 }
251
252 fn update_weights(
254 &mut self,
255 violations: &HashMap<String, f64>,
256 iteration: usize,
257 ) -> Result<(), Box<dyn std::error::Error>> {
258 #[cfg(feature = "scirs")]
259 {
260 if self.config.adaptive_scaling && self.optimizer.is_some() {
261 self.update_weights_optimized(violations, iteration)?;
263 return Ok(());
264 }
265 }
266
267 for (constraint_name, &violation) in violations {
269 if let Some(weight) = self.constraint_weights.get_mut(constraint_name) {
270 if violation.abs() > self.config.violation_tolerance {
271 *weight = (*weight * self.config.adjustment_factor).min(self.config.max_weight);
273 } else if violation.abs() < self.config.violation_tolerance * 0.1 {
274 *weight = (*weight / self.config.adjustment_factor.sqrt())
276 .max(self.config.min_weight);
277 }
278 }
279 }
280
281 Ok(())
282 }
283
284 #[cfg(feature = "scirs")]
285 fn update_weights_optimized(
287 &mut self,
288 violations: &HashMap<String, f64>,
289 iteration: usize,
290 ) -> Result<(), Box<dyn std::error::Error>> {
291 use crate::scirs_stub::scirs2_optimization::{Bounds, ObjectiveFunction};
292
293 let constraint_names: Vec<_> = violations.keys().cloned().collect();
295 let current_weights: Array1<f64> = constraint_names
296 .iter()
297 .map(|name| self.constraint_weights[name])
298 .collect();
299
300 let violations_vec: Array1<f64> = constraint_names
302 .iter()
303 .map(|name| violations[name].abs())
304 .collect();
305
306 let mut objective = WeightOptimizationObjective {
307 violations: violations_vec,
308 penalty_type: self.config.penalty_type,
309 regularization: 0.01, };
311
312 let lower_bounds = Array1::from_elem(constraint_names.len(), self.config.min_weight);
314 let upper_bounds = Array1::from_elem(constraint_names.len(), self.config.max_weight);
315 let bounds = Bounds::new(lower_bounds, upper_bounds);
316
317 let inner_max_iterations = self.config.max_iterations.max(1);
321 let _ = iteration;
322 if let Some(ref mut optimizer) = self.optimizer {
323 let mut result =
324 optimizer.minimize(&objective, ¤t_weights, &bounds, inner_max_iterations)?;
325
326 for (i, name) in constraint_names.iter().enumerate() {
328 self.constraint_weights.insert(name.clone(), result.x[i]);
329 }
330 }
331
332 Ok(())
333 }
334
335 fn calculate_objective(
337 &self,
338 model: &CompiledModel,
339 sample_results: &[(Vec<bool>, f64)],
340 ) -> Result<f64, Box<dyn std::error::Error>> {
341 let mut total_objective = 0.0;
342
343 for (assignment, energy) in sample_results {
344 let mut penalized_objective = *energy;
346
347 for (constraint_name, constraint_expr) in model.get_constraints() {
349 let violation = self.evaluate_constraint_violation(
350 constraint_expr,
351 assignment,
352 model.get_variable_map(),
353 )?;
354
355 let weight = self
356 .constraint_weights
357 .get(constraint_name)
358 .copied()
359 .unwrap_or(1.0);
360
361 penalized_objective += weight * violation;
362 }
363
364 total_objective += penalized_objective;
365 }
366
367 Ok(total_objective / sample_results.len() as f64)
368 }
369
370 fn calculate_satisfaction_rate(&self, violations: &HashMap<String, f64>) -> f64 {
372 let satisfied = violations
373 .values()
374 .filter(|&&v| v.abs() < self.config.violation_tolerance)
375 .count();
376
377 if violations.is_empty() {
378 1.0
379 } else {
380 satisfied as f64 / violations.len() as f64
381 }
382 }
383
384 pub fn get_weight(&self, constraint_name: &str) -> Option<f64> {
386 self.constraint_weights.get(constraint_name).copied()
387 }
388
389 pub fn get_violation_history(&self) -> &[ConstraintViolation] {
391 &self.violation_history
392 }
393
394 pub fn export_config(&self) -> PenaltyExport {
396 PenaltyExport {
397 config: self.config.clone(),
398 weights: self.constraint_weights.clone(),
399 final_violations: self
400 .violation_history
401 .iter()
402 .filter(|v| {
403 v.iteration
404 == self
405 .violation_history
406 .iter()
407 .map(|h| h.iteration)
408 .max()
409 .unwrap_or(0)
410 })
411 .map(|v| (v.constraint_name.clone(), v.violation_amount))
412 .collect(),
413 }
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct PenaltyExport {
420 pub config: PenaltyConfig,
421 pub weights: HashMap<String, f64>,
422 pub final_violations: HashMap<String, f64>,
423}
424
425#[cfg(feature = "scirs")]
426struct WeightOptimizationObjective {
428 violations: Array1<f64>,
429 penalty_type: PenaltyType,
430 regularization: f64,
431}
432
433#[cfg(feature = "scirs")]
434impl WeightOptimizationObjective {
435 const MIN_SAFE_WEIGHT: f64 = 1e-12;
439}
440
441#[cfg(feature = "scirs")]
442impl crate::scirs_stub::scirs2_optimization::ObjectiveFunction for WeightOptimizationObjective {
443 fn evaluate(&self, weights: &Array1<f64>) -> f64 {
444 let residual: f64 = self
454 .violations
455 .iter()
456 .zip(weights.iter())
457 .map(|(&violation, &weight)| violation / weight.max(Self::MIN_SAFE_WEIGHT))
458 .sum();
459
460 residual + self.regularization * weights.dot(weights)
461 }
462
463 fn gradient(&self, weights: &Array1<f64>) -> Array1<f64> {
464 let mut gradient = Array1::zeros(weights.len());
465 for (index, (&violation, &weight)) in self.violations.iter().zip(weights.iter()).enumerate()
466 {
467 let safe_weight = weight.max(Self::MIN_SAFE_WEIGHT);
468 gradient[index] =
469 2.0 * self.regularization * weight - violation / (safe_weight * safe_weight);
470 }
471 gradient
472 }
473}
474
475#[derive(Debug, Clone)]
479pub struct CompiledModel {
480 constraints: HashMap<String, ConstraintExpr>,
481 variable_map: HashMap<String, usize>,
482}
483
484impl Default for CompiledModel {
485 fn default() -> Self {
486 Self::new()
487 }
488}
489
490impl CompiledModel {
491 pub fn new() -> Self {
492 Self {
493 constraints: HashMap::new(),
494 variable_map: HashMap::new(),
495 }
496 }
497
498 #[must_use]
500 pub fn with_variables(variables: &[String]) -> Self {
501 let variable_map = variables
502 .iter()
503 .enumerate()
504 .map(|(index, name)| (name.clone(), index))
505 .collect();
506 Self {
507 constraints: HashMap::new(),
508 variable_map,
509 }
510 }
511
512 pub fn add_constraint(&mut self, name: impl Into<String>, constraint: ConstraintExpr) {
514 self.constraints.insert(name.into(), constraint);
515 }
516
517 pub fn set_variable_index(&mut self, name: impl Into<String>, index: usize) {
519 self.variable_map.insert(name.into(), index);
520 }
521
522 pub const fn get_constraints(&self) -> &HashMap<String, ConstraintExpr> {
523 &self.constraints
524 }
525
526 pub const fn get_variable_map(&self) -> &HashMap<String, usize> {
527 &self.variable_map
528 }
529
530 pub fn to_qubo(&self) -> (Array2<f64>, HashMap<String, usize>) {
531 let size = self.variable_map.len();
532 (Array2::zeros((size, size)), self.variable_map.clone())
533 }
534}
535
536#[derive(Debug, Clone)]
541pub struct ConstraintExpr {
542 pub expression: Expression,
543 pub constraint_type: ConstraintType,
544}
545
546impl ConstraintExpr {
547 #[must_use]
548 pub const fn new(expression: Expression, constraint_type: ConstraintType) -> Self {
549 Self {
550 expression,
551 constraint_type,
552 }
553 }
554
555 #[must_use]
558 pub fn violation(&self, assignment: &HashMap<String, bool>) -> f64 {
559 let value = self.expression.evaluate(assignment);
560 self.constraint_type.violation(value)
561 }
562}
563
564pub fn analyze_penalty_landscape(config: &PenaltyConfig, violations: &[f64]) -> PenaltyAnalysis {
566 let weights = Array1::linspace(config.min_weight, config.max_weight, 100);
567 let mut penalties = Vec::new();
568
569 for &weight in &weights {
570 let penalty_values: Vec<f64> = violations
571 .iter()
572 .map(|&v| calculate_penalty(v, weight, config.penalty_type))
573 .collect();
574
575 penalties.push(PenaltyPoint {
576 weight,
577 avg_penalty: penalty_values.iter().sum::<f64>() / penalty_values.len() as f64,
578 max_penalty: penalty_values.iter().fold(0.0, |a, &b| a.max(b)),
579 min_penalty: penalty_values.iter().fold(f64::INFINITY, |a, &b| a.min(b)),
580 });
581 }
582
583 PenaltyAnalysis {
584 penalty_points: penalties,
585 optimal_weight: find_optimal_weight(&weights, violations, config),
586 sensitivity: calculate_sensitivity(violations, config),
587 }
588}
589
590fn calculate_penalty(violation: f64, weight: f64, penalty_type: PenaltyType) -> f64 {
592 weight
593 * match penalty_type {
594 PenaltyType::Quadratic => violation.powi(2),
595 PenaltyType::Linear => violation.abs(),
596 PenaltyType::LogBarrier => {
597 if violation > 0.0 {
598 -violation.ln()
599 } else {
600 1e10 }
602 }
603 PenaltyType::Exponential => violation.exp_m1(),
604 PenaltyType::AugmentedLagrangian => violation.mul_add(violation, violation.abs()),
605 }
606}
607
608fn find_optimal_weight(weights: &Array1<f64>, violations: &[f64], config: &PenaltyConfig) -> f64 {
610 let target_penalty = violations.len() as f64 * config.violation_tolerance;
613
614 let mut best_weight = config.initial_weight;
615 let mut best_diff = f64::INFINITY;
616
617 for &weight in weights {
618 let total_penalty: f64 = violations
619 .iter()
620 .map(|&v| calculate_penalty(v, weight, config.penalty_type))
621 .sum();
622
623 let diff = (total_penalty - target_penalty).abs();
624 if diff < best_diff {
625 best_diff = diff;
626 best_weight = weight;
627 }
628 }
629
630 best_weight
631}
632
633fn calculate_sensitivity(violations: &[f64], config: &PenaltyConfig) -> f64 {
635 if violations.is_empty() {
636 return 0.0;
637 }
638
639 let weight = config.initial_weight;
641 let penalties: Vec<f64> = violations
642 .iter()
643 .map(|&v| calculate_penalty(v, weight, config.penalty_type))
644 .collect();
645
646 let delta = 0.01 * weight;
647 let penalties_delta: Vec<f64> = violations
648 .iter()
649 .map(|&v| calculate_penalty(v, weight + delta, config.penalty_type))
650 .collect();
651
652 let derivatives: Vec<f64> = penalties
653 .iter()
654 .zip(penalties_delta.iter())
655 .map(|(&p1, &p2)| (p2 - p1) / delta)
656 .collect();
657
658 derivatives.iter().sum::<f64>() / derivatives.len() as f64
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct PenaltyAnalysis {
665 pub penalty_points: Vec<PenaltyPoint>,
666 pub optimal_weight: f64,
667 pub sensitivity: f64,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct PenaltyPoint {
673 pub weight: f64,
674 pub avg_penalty: f64,
675 pub max_penalty: f64,
676 pub min_penalty: f64,
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use crate::optimization::constraints::Variable;
683
684 fn equality_model() -> CompiledModel {
687 let mut model = CompiledModel::with_variables(&["x0".to_string(), "x1".to_string()]);
688 let expr: Expression = Variable::new("x0".to_string()).into();
689 model.add_constraint(
690 "x0_must_be_true",
691 ConstraintExpr::new(expr, ConstraintType::Equality { target: 1.0 }),
692 );
693 model
694 }
695
696 #[test]
697 fn test_evaluate_constraint_violation_is_real() {
698 let optimizer = PenaltyOptimizer::new(PenaltyConfig::default());
699 let model = equality_model();
700 let constraint = &model.get_constraints()["x0_must_be_true"];
701
702 let violated = optimizer
704 .evaluate_constraint_violation(constraint, &[false, true], model.get_variable_map())
705 .expect("evaluation should succeed");
706 assert_eq!(violated, 1.0);
707
708 let satisfied = optimizer
710 .evaluate_constraint_violation(constraint, &[true, false], model.get_variable_map())
711 .expect("evaluation should succeed");
712 assert_eq!(satisfied, 0.0);
713 }
714
715 #[test]
716 fn test_optimize_penalties_detects_real_violations() {
717 let mut optimizer = PenaltyOptimizer::new(PenaltyConfig {
718 max_iterations: 5,
719 ..PenaltyConfig::default()
720 });
721 let model = equality_model();
722 optimizer.initialize_weights(&["x0_must_be_true".to_string()]);
723
724 let sample_results = vec![
726 (vec![false, false], 0.0),
727 (vec![false, true], 0.0),
728 (vec![false, true], 0.0),
729 ];
730
731 let result = optimizer
732 .optimize_penalties(&model, &sample_results)
733 .expect("optimization should succeed");
734
735 assert!(result.final_violations["x0_must_be_true"].abs() > 0.0);
739 assert!(result.constraint_satisfaction < 1.0);
740 assert!(
741 result.optimal_weights["x0_must_be_true"] > PenaltyConfig::default().initial_weight
742 );
743 }
744
745 #[test]
746 fn test_optimize_penalties_recognizes_feasible_samples() {
747 let mut optimizer = PenaltyOptimizer::new(PenaltyConfig::default());
748 let model = equality_model();
749 optimizer.initialize_weights(&["x0_must_be_true".to_string()]);
750
751 let sample_results = vec![(vec![true, false], -1.0), (vec![true, true], -0.5)];
753
754 let result = optimizer
755 .optimize_penalties(&model, &sample_results)
756 .expect("optimization should succeed");
757
758 assert!(result.converged);
759 assert_eq!(result.constraint_satisfaction, 1.0);
760 assert_eq!(result.final_violations["x0_must_be_true"], 0.0);
761 }
762}