quantrs2_tytan/solution_debugger/
constraint_analyzer.rs

1//! Constraint analysis functionality for the solution debugger.
2
3use super::types::{ConstraintInfo, ConstraintType};
4use serde::Serialize;
5use std::collections::HashMap;
6
7/// Constraint analyzer
8pub struct ConstraintAnalyzer {
9    /// Tolerance for constraint satisfaction
10    tolerance: f64,
11    /// Violation cache
12    violation_cache: HashMap<String, Vec<ConstraintViolation>>,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct ConstraintViolation {
17    /// Constraint violated
18    pub constraint: ConstraintInfo,
19    /// Violation amount
20    pub violation_amount: f64,
21    /// Variables causing violation
22    pub violating_variables: Vec<String>,
23    /// Suggested fixes
24    pub suggested_fixes: Vec<SuggestedFix>,
25}
26
27#[derive(Debug, Clone, Serialize)]
28pub struct SuggestedFix {
29    /// Fix description
30    pub description: String,
31    /// Variables to change
32    pub variable_changes: HashMap<String, bool>,
33    /// Expected improvement
34    pub expected_improvement: f64,
35    /// Fix complexity
36    pub complexity: FixComplexity,
37}
38
39#[derive(Debug, Clone, Serialize)]
40pub enum FixComplexity {
41    /// Single variable flip
42    Simple,
43    /// Multiple variable changes
44    Moderate,
45    /// Complex changes required
46    Complex,
47}
48
49impl ConstraintAnalyzer {
50    /// Create new constraint analyzer
51    pub fn new(tolerance: f64) -> Self {
52        Self {
53            tolerance,
54            violation_cache: HashMap::new(),
55        }
56    }
57
58    /// Analyze constraints for a solution
59    pub fn analyze(
60        &mut self,
61        constraints: &[ConstraintInfo],
62        assignments: &HashMap<String, bool>,
63    ) -> Vec<ConstraintViolation> {
64        let mut violations = Vec::new();
65
66        for constraint in constraints {
67            if let Some(violation) = self.check_constraint(constraint, assignments) {
68                violations.push(violation);
69            }
70        }
71
72        violations
73    }
74
75    /// Check individual constraint
76    fn check_constraint(
77        &self,
78        constraint: &ConstraintInfo,
79        assignments: &HashMap<String, bool>,
80    ) -> Option<ConstraintViolation> {
81        match &constraint.constraint_type {
82            ConstraintType::Equality { target } => {
83                self.check_equality_constraint(constraint, assignments, *target)
84            }
85            ConstraintType::Inequality { bound, direction } => {
86                self.check_inequality_constraint(constraint, assignments, *bound, direction)
87            }
88            ConstraintType::AllDifferent => {
89                self.check_all_different_constraint(constraint, assignments)
90            }
91            ConstraintType::ExactlyOne => {
92                self.check_exactly_one_constraint(constraint, assignments)
93            }
94            ConstraintType::AtMostOne => self.check_at_most_one_constraint(constraint, assignments),
95            ConstraintType::Custom { .. } => {
96                // Placeholder for custom constraint evaluation
97                None
98            }
99        }
100    }
101
102    /// Check equality constraint
103    fn check_equality_constraint(
104        &self,
105        constraint: &ConstraintInfo,
106        assignments: &HashMap<String, bool>,
107        target: f64,
108    ) -> Option<ConstraintViolation> {
109        let sum: f64 = constraint
110            .variables
111            .iter()
112            .map(|var| {
113                if assignments.get(var).copied().unwrap_or(false) {
114                    1.0
115                } else {
116                    0.0
117                }
118            })
119            .sum();
120
121        let violation_amount = (sum - target).abs();
122        if violation_amount > self.tolerance {
123            Some(ConstraintViolation {
124                constraint: constraint.clone(),
125                violation_amount,
126                violating_variables: constraint.variables.clone(),
127                suggested_fixes: self.generate_equality_fixes(constraint, assignments, target, sum),
128            })
129        } else {
130            None
131        }
132    }
133
134    /// Check inequality constraint
135    fn check_inequality_constraint(
136        &self,
137        constraint: &ConstraintInfo,
138        assignments: &HashMap<String, bool>,
139        bound: f64,
140        direction: &super::types::InequalityDirection,
141    ) -> Option<ConstraintViolation> {
142        let sum: f64 = constraint
143            .variables
144            .iter()
145            .map(|var| {
146                if assignments.get(var).copied().unwrap_or(false) {
147                    1.0
148                } else {
149                    0.0
150                }
151            })
152            .sum();
153
154        let violation_amount = match direction {
155            super::types::InequalityDirection::LessEqual => {
156                if sum > bound {
157                    sum - bound
158                } else {
159                    0.0
160                }
161            }
162            super::types::InequalityDirection::GreaterEqual => {
163                if sum < bound {
164                    bound - sum
165                } else {
166                    0.0
167                }
168            }
169        };
170
171        if violation_amount > self.tolerance {
172            Some(ConstraintViolation {
173                constraint: constraint.clone(),
174                violation_amount,
175                violating_variables: constraint.variables.clone(),
176                suggested_fixes: self.generate_inequality_fixes(
177                    constraint,
178                    assignments,
179                    bound,
180                    direction,
181                    sum,
182                ),
183            })
184        } else {
185            None
186        }
187    }
188
189    /// Check all different constraint
190    fn check_all_different_constraint(
191        &self,
192        constraint: &ConstraintInfo,
193        assignments: &HashMap<String, bool>,
194    ) -> Option<ConstraintViolation> {
195        // For binary variables, all different means at most one can be true
196        let true_count = constraint
197            .variables
198            .iter()
199            .filter(|var| assignments.get(*var).copied().unwrap_or(false))
200            .count();
201
202        if true_count > 1 {
203            let violation_amount = (true_count - 1) as f64;
204            Some(ConstraintViolation {
205                constraint: constraint.clone(),
206                violation_amount,
207                violating_variables: constraint
208                    .variables
209                    .iter()
210                    .filter(|var| assignments.get(*var).copied().unwrap_or(false))
211                    .cloned()
212                    .collect(),
213                suggested_fixes: self.generate_all_different_fixes(constraint, assignments),
214            })
215        } else {
216            None
217        }
218    }
219
220    /// Check exactly one constraint
221    fn check_exactly_one_constraint(
222        &self,
223        constraint: &ConstraintInfo,
224        assignments: &HashMap<String, bool>,
225    ) -> Option<ConstraintViolation> {
226        let true_count = constraint
227            .variables
228            .iter()
229            .filter(|var| assignments.get(*var).copied().unwrap_or(false))
230            .count();
231
232        if true_count == 1 {
233            None
234        } else {
235            let violation_amount = (true_count as i32 - 1).abs() as f64;
236            Some(ConstraintViolation {
237                constraint: constraint.clone(),
238                violation_amount,
239                violating_variables: constraint.variables.clone(),
240                suggested_fixes: self.generate_exactly_one_fixes(constraint, assignments),
241            })
242        }
243    }
244
245    /// Check at most one constraint
246    fn check_at_most_one_constraint(
247        &self,
248        constraint: &ConstraintInfo,
249        assignments: &HashMap<String, bool>,
250    ) -> Option<ConstraintViolation> {
251        let true_count = constraint
252            .variables
253            .iter()
254            .filter(|var| assignments.get(*var).copied().unwrap_or(false))
255            .count();
256
257        if true_count > 1 {
258            let violation_amount = (true_count - 1) as f64;
259            Some(ConstraintViolation {
260                constraint: constraint.clone(),
261                violation_amount,
262                violating_variables: constraint
263                    .variables
264                    .iter()
265                    .filter(|var| assignments.get(*var).copied().unwrap_or(false))
266                    .cloned()
267                    .collect(),
268                suggested_fixes: self.generate_at_most_one_fixes(constraint, assignments),
269            })
270        } else {
271            None
272        }
273    }
274
275    /// Generate fixes for equality constraints
276    fn generate_equality_fixes(
277        &self,
278        constraint: &ConstraintInfo,
279        assignments: &HashMap<String, bool>,
280        target: f64,
281        current_sum: f64,
282    ) -> Vec<SuggestedFix> {
283        let mut fixes = Vec::new();
284
285        let difference = target - current_sum;
286        if difference > 0.0 {
287            // Need to set more variables to true
288            let false_vars: Vec<_> = constraint
289                .variables
290                .iter()
291                .filter(|var| !assignments.get(*var).copied().unwrap_or(false))
292                .collect();
293
294            if false_vars.len() >= difference as usize {
295                let mut changes = HashMap::new();
296                for var in false_vars.iter().take(difference as usize) {
297                    changes.insert((*var).clone(), true);
298                }
299
300                fixes.push(SuggestedFix {
301                    description: format!("Set {} variables to true", difference as usize),
302                    variable_changes: changes,
303                    expected_improvement: difference * constraint.penalty,
304                    complexity: if difference == 1.0 {
305                        FixComplexity::Simple
306                    } else {
307                        FixComplexity::Moderate
308                    },
309                });
310            }
311        } else if difference < 0.0 {
312            // Need to set some variables to false
313            let true_vars: Vec<_> = constraint
314                .variables
315                .iter()
316                .filter(|var| assignments.get(*var).copied().unwrap_or(false))
317                .collect();
318
319            if true_vars.len() >= (-difference) as usize {
320                let mut changes = HashMap::new();
321                for var in true_vars.iter().take((-difference) as usize) {
322                    changes.insert((*var).clone(), false);
323                }
324
325                fixes.push(SuggestedFix {
326                    description: format!("Set {} variables to false", (-difference) as usize),
327                    variable_changes: changes,
328                    expected_improvement: (-difference) * constraint.penalty,
329                    complexity: if difference == -1.0 {
330                        FixComplexity::Simple
331                    } else {
332                        FixComplexity::Moderate
333                    },
334                });
335            }
336        }
337
338        fixes
339    }
340
341    /// Generate fixes for inequality constraints
342    fn generate_inequality_fixes(
343        &self,
344        constraint: &ConstraintInfo,
345        assignments: &HashMap<String, bool>,
346        bound: f64,
347        direction: &super::types::InequalityDirection,
348        current_sum: f64,
349    ) -> Vec<SuggestedFix> {
350        let mut fixes = Vec::new();
351
352        match direction {
353            super::types::InequalityDirection::LessEqual => {
354                if current_sum > bound {
355                    let excess = current_sum - bound;
356                    let true_vars: Vec<_> = constraint
357                        .variables
358                        .iter()
359                        .filter(|var| assignments.get(*var).copied().unwrap_or(false))
360                        .collect();
361
362                    if true_vars.len() >= excess as usize {
363                        let mut changes = HashMap::new();
364                        for var in true_vars.iter().take(excess as usize) {
365                            changes.insert((*var).clone(), false);
366                        }
367
368                        fixes.push(SuggestedFix {
369                            description: format!(
370                                "Set {} variables to false to satisfy bound",
371                                excess as usize
372                            ),
373                            variable_changes: changes,
374                            expected_improvement: excess * constraint.penalty,
375                            complexity: if excess == 1.0 {
376                                FixComplexity::Simple
377                            } else {
378                                FixComplexity::Moderate
379                            },
380                        });
381                    }
382                }
383            }
384            super::types::InequalityDirection::GreaterEqual => {
385                if current_sum < bound {
386                    let deficit = bound - current_sum;
387                    let false_vars: Vec<_> = constraint
388                        .variables
389                        .iter()
390                        .filter(|var| !assignments.get(*var).copied().unwrap_or(false))
391                        .collect();
392
393                    if false_vars.len() >= deficit as usize {
394                        let mut changes = HashMap::new();
395                        for var in false_vars.iter().take(deficit as usize) {
396                            changes.insert((*var).clone(), true);
397                        }
398
399                        fixes.push(SuggestedFix {
400                            description: format!(
401                                "Set {} variables to true to satisfy bound",
402                                deficit as usize
403                            ),
404                            variable_changes: changes,
405                            expected_improvement: deficit * constraint.penalty,
406                            complexity: if deficit == 1.0 {
407                                FixComplexity::Simple
408                            } else {
409                                FixComplexity::Moderate
410                            },
411                        });
412                    }
413                }
414            }
415        }
416
417        fixes
418    }
419
420    /// Generate fixes for all different constraints
421    fn generate_all_different_fixes(
422        &self,
423        constraint: &ConstraintInfo,
424        assignments: &HashMap<String, bool>,
425    ) -> Vec<SuggestedFix> {
426        let mut fixes = Vec::new();
427
428        let true_vars: Vec<_> = constraint
429            .variables
430            .iter()
431            .filter(|var| assignments.get(*var).copied().unwrap_or(false))
432            .collect();
433
434        if true_vars.len() > 1 {
435            // Set all but one to false
436            for keep_var in &true_vars {
437                let mut changes = HashMap::new();
438                for var in &true_vars {
439                    if *var != *keep_var {
440                        changes.insert((*var).clone(), false);
441                    }
442                }
443
444                fixes.push(SuggestedFix {
445                    description: format!("Keep only {keep_var} set to true"),
446                    variable_changes: changes,
447                    expected_improvement: (true_vars.len() - 1) as f64 * constraint.penalty,
448                    complexity: if true_vars.len() == 2 {
449                        FixComplexity::Simple
450                    } else {
451                        FixComplexity::Moderate
452                    },
453                });
454            }
455        }
456
457        fixes
458    }
459
460    /// Generate fixes for exactly one constraints
461    fn generate_exactly_one_fixes(
462        &self,
463        constraint: &ConstraintInfo,
464        assignments: &HashMap<String, bool>,
465    ) -> Vec<SuggestedFix> {
466        let mut fixes = Vec::new();
467
468        let true_count = constraint
469            .variables
470            .iter()
471            .filter(|var| assignments.get(*var).copied().unwrap_or(false))
472            .count();
473
474        if true_count == 0 {
475            // Set one variable to true
476            for var in &constraint.variables {
477                let mut changes = HashMap::new();
478                changes.insert(var.clone(), true);
479
480                fixes.push(SuggestedFix {
481                    description: format!("Set {var} to true"),
482                    variable_changes: changes,
483                    expected_improvement: constraint.penalty,
484                    complexity: FixComplexity::Simple,
485                });
486            }
487        } else if true_count > 1 {
488            // Keep only one true, set others to false
489            let true_vars: Vec<_> = constraint
490                .variables
491                .iter()
492                .filter(|var| assignments.get(*var).copied().unwrap_or(false))
493                .collect();
494
495            for keep_var in &true_vars {
496                let mut changes = HashMap::new();
497                for var in &true_vars {
498                    if *var != *keep_var {
499                        changes.insert((*var).clone(), false);
500                    }
501                }
502
503                fixes.push(SuggestedFix {
504                    description: format!("Keep only {keep_var} set to true"),
505                    variable_changes: changes,
506                    expected_improvement: (true_vars.len() - 1) as f64 * constraint.penalty,
507                    complexity: if true_vars.len() == 2 {
508                        FixComplexity::Simple
509                    } else {
510                        FixComplexity::Moderate
511                    },
512                });
513            }
514        }
515
516        fixes
517    }
518
519    /// Generate fixes for at most one constraints
520    fn generate_at_most_one_fixes(
521        &self,
522        constraint: &ConstraintInfo,
523        assignments: &HashMap<String, bool>,
524    ) -> Vec<SuggestedFix> {
525        // Same as all different for binary variables
526        self.generate_all_different_fixes(constraint, assignments)
527    }
528}