quantrs2_tytan/solution_debugger/
constraint_analyzer.rs1use super::types::{ConstraintInfo, ConstraintType};
4use serde::Serialize;
5use std::collections::HashMap;
6
7pub struct ConstraintAnalyzer {
9 tolerance: f64,
11 violation_cache: HashMap<String, Vec<ConstraintViolation>>,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct ConstraintViolation {
17 pub constraint: ConstraintInfo,
19 pub violation_amount: f64,
21 pub violating_variables: Vec<String>,
23 pub suggested_fixes: Vec<SuggestedFix>,
25}
26
27#[derive(Debug, Clone, Serialize)]
28pub struct SuggestedFix {
29 pub description: String,
31 pub variable_changes: HashMap<String, bool>,
33 pub expected_improvement: f64,
35 pub complexity: FixComplexity,
37}
38
39#[derive(Debug, Clone, Serialize)]
40pub enum FixComplexity {
41 Simple,
43 Moderate,
45 Complex,
47}
48
49impl ConstraintAnalyzer {
50 pub fn new(tolerance: f64) -> Self {
52 Self {
53 tolerance,
54 violation_cache: HashMap::new(),
55 }
56 }
57
58 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 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 None
98 }
99 }
100 }
101
102 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 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 fn check_all_different_constraint(
191 &self,
192 constraint: &ConstraintInfo,
193 assignments: &HashMap<String, bool>,
194 ) -> Option<ConstraintViolation> {
195 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 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 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 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 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 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 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 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 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 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 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 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 fn generate_at_most_one_fixes(
521 &self,
522 constraint: &ConstraintInfo,
523 assignments: &HashMap<String, bool>,
524 ) -> Vec<SuggestedFix> {
525 self.generate_all_different_fixes(constraint, assignments)
527 }
528}