1use std::collections::HashMap;
7use thiserror::Error;
8
9use crate::ising::{IsingError, QuboModel};
10
11#[derive(Error, Debug)]
13pub enum QuboError {
14 #[error("Ising error: {0}")]
16 IsingError(#[from] IsingError),
17
18 #[error("Constraint error: {0}")]
20 ConstraintError(String),
21
22 #[error("Variable {0} is already defined")]
24 DuplicateVariable(String),
25
26 #[error("Variable {0} not found")]
28 VariableNotFound(String),
29}
30
31pub type QuboResult<T> = Result<T, QuboError>;
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct Variable {
37 pub name: String,
39 pub index: usize,
41}
42
43impl Variable {
44 pub fn new(name: impl Into<String>, index: usize) -> Self {
46 Self {
47 name: name.into(),
48 index,
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
58pub struct QuboBuilder {
59 num_vars: usize,
61
62 var_map: HashMap<String, usize>,
64
65 model: QuboModel,
67
68 constraint_weight: f64,
70}
71
72impl QuboBuilder {
73 #[must_use]
75 pub fn new() -> Self {
76 Self {
77 num_vars: 0,
78 var_map: HashMap::new(),
79 model: QuboModel::new(0),
80 constraint_weight: 10.0,
81 }
82 }
83
84 pub fn set_constraint_weight(&mut self, weight: f64) -> QuboResult<()> {
86 if !weight.is_finite() || weight <= 0.0 {
87 return Err(QuboError::ConstraintError(format!(
88 "Constraint weight must be positive and finite, got {weight}"
89 )));
90 }
91
92 self.constraint_weight = weight;
93 Ok(())
94 }
95
96 pub fn add_variable(&mut self, name: impl Into<String>) -> QuboResult<Variable> {
98 let name = name.into();
99
100 if self.var_map.contains_key(&name) {
102 return Err(QuboError::DuplicateVariable(name));
103 }
104
105 let index = self.num_vars;
107 self.var_map.insert(name.clone(), index);
108 self.num_vars += 1;
109
110 self.model = QuboModel::new(self.num_vars);
112
113 Ok(Variable::new(name, index))
114 }
115
116 pub fn add_variables(
118 &mut self,
119 names: impl IntoIterator<Item = impl Into<String>>,
120 ) -> QuboResult<Vec<Variable>> {
121 let mut variables = Vec::new();
122 for name in names {
123 variables.push(self.add_variable(name)?);
124 }
125 Ok(variables)
126 }
127
128 pub fn get_variable(&self, name: &str) -> QuboResult<Variable> {
130 match self.var_map.get(name) {
131 Some(&index) => Ok(Variable::new(name, index)),
132 None => Err(QuboError::VariableNotFound(name.to_string())),
133 }
134 }
135
136 pub fn set_linear_term(&mut self, var: &Variable, value: f64) -> QuboResult<()> {
138 if var.index >= self.num_vars {
140 return Err(QuboError::VariableNotFound(var.name.clone()));
141 }
142
143 Ok(self.model.set_linear(var.index, value)?)
144 }
145
146 pub fn set_quadratic_term(
148 &mut self,
149 var1: &Variable,
150 var2: &Variable,
151 value: f64,
152 ) -> QuboResult<()> {
153 if var1.index >= self.num_vars {
155 return Err(QuboError::VariableNotFound(var1.name.clone()));
156 }
157 if var2.index >= self.num_vars {
158 return Err(QuboError::VariableNotFound(var2.name.clone()));
159 }
160
161 if var1.index == var2.index {
163 return Err(QuboError::ConstraintError(format!(
164 "Cannot set quadratic term for the same variable: {}",
165 var1.name
166 )));
167 }
168
169 Ok(self.model.set_quadratic(var1.index, var2.index, value)?)
170 }
171
172 pub fn set_offset(&mut self, offset: f64) -> QuboResult<()> {
174 if !offset.is_finite() {
175 return Err(QuboError::ConstraintError(format!(
176 "Offset must be finite, got {offset}"
177 )));
178 }
179
180 self.model.offset = offset;
181 Ok(())
182 }
183
184 pub fn add_bias(&mut self, var_index: usize, bias: f64) -> QuboResult<()> {
186 if var_index >= self.num_vars {
187 return Err(QuboError::VariableNotFound(format!(
188 "Variable index {var_index}"
189 )));
190 }
191 let current = self.model.get_linear(var_index)?;
192 self.model.set_linear(var_index, current + bias)?;
193 Ok(())
194 }
195
196 pub fn add_coupling(
198 &mut self,
199 var1_index: usize,
200 var2_index: usize,
201 coupling: f64,
202 ) -> QuboResult<()> {
203 if var1_index >= self.num_vars {
204 return Err(QuboError::VariableNotFound(format!(
205 "Variable index {var1_index}"
206 )));
207 }
208 if var2_index >= self.num_vars {
209 return Err(QuboError::VariableNotFound(format!(
210 "Variable index {var2_index}"
211 )));
212 }
213 let current = self.model.get_quadratic(var1_index, var2_index)?;
214 self.model
215 .set_quadratic(var1_index, var2_index, current + coupling)?;
216 Ok(())
217 }
218
219 pub fn minimize_linear(&mut self, var: &Variable, coeff: f64) -> QuboResult<()> {
221 self.set_linear_term(var, self.model.get_linear(var.index)? + coeff)
222 }
223
224 pub fn minimize_quadratic(
226 &mut self,
227 var1: &Variable,
228 var2: &Variable,
229 coeff: f64,
230 ) -> QuboResult<()> {
231 let current = self.model.get_quadratic(var1.index, var2.index)?;
232 self.set_quadratic_term(var1, var2, current + coeff)
233 }
234
235 pub fn constrain_equal(&mut self, var1: &Variable, var2: &Variable) -> QuboResult<()> {
239 let weight = self.constraint_weight;
241
242 self.set_linear_term(var1, self.model.get_linear(var1.index)? + weight)?;
244
245 self.set_linear_term(var2, self.model.get_linear(var2.index)? + weight)?;
247
248 let current = self.model.get_quadratic(var1.index, var2.index)?;
250 self.set_quadratic_term(var1, var2, 2.0f64.mul_add(-weight, current))
251 }
252
253 pub fn constrain_different(&mut self, var1: &Variable, var2: &Variable) -> QuboResult<()> {
257 let weight = self.constraint_weight;
259
260 self.set_linear_term(var1, self.model.get_linear(var1.index)? - weight)?;
262
263 self.set_linear_term(var2, self.model.get_linear(var2.index)? - weight)?;
265
266 let current = self.model.get_quadratic(var1.index, var2.index)?;
268 self.set_quadratic_term(var1, var2, 2.0f64.mul_add(weight, current))?;
269
270 self.model.offset += weight;
272
273 Ok(())
274 }
275
276 pub fn constrain_one_hot(&mut self, vars: &[Variable]) -> QuboResult<()> {
280 if vars.is_empty() {
281 return Err(QuboError::ConstraintError(
282 "Empty one-hot constraint".to_string(),
283 ));
284 }
285
286 let weight = self.constraint_weight;
291
292 for var in vars {
294 self.set_linear_term(var, self.model.get_linear(var.index)? - weight)?;
295 }
296
297 for i in 0..vars.len() {
299 for j in (i + 1)..vars.len() {
300 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
301 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
302 }
303 }
304
305 self.model.offset += weight;
307
308 Ok(())
309 }
310
311 pub fn constrain_at_most_one(&mut self, vars: &[Variable]) -> QuboResult<()> {
315 if vars.is_empty() {
316 return Err(QuboError::ConstraintError(
317 "Empty at-most-one constraint".to_string(),
318 ));
319 }
320
321 let weight = self.constraint_weight;
325
326 for i in 0..vars.len() {
328 for j in (i + 1)..vars.len() {
329 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
330 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
331 }
332 }
333
334 Ok(())
335 }
336
337 pub fn constrain_at_least_one(&mut self, vars: &[Variable]) -> QuboResult<()> {
341 if vars.is_empty() {
342 return Err(QuboError::ConstraintError(
343 "Empty at-least-one constraint".to_string(),
344 ));
345 }
346
347 let weight = self.constraint_weight;
352
353 for var in vars {
355 self.set_linear_term(
356 var,
357 2.0f64.mul_add(-weight, self.model.get_linear(var.index)?),
358 )?;
359 }
360
361 for i in 0..vars.len() {
363 for j in (i + 1)..vars.len() {
364 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
365 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
366 }
367 }
368
369 self.model.offset += weight;
371
372 Ok(())
373 }
374
375 pub fn constrain_sum_equal(&mut self, vars: &[Variable], target: f64) -> QuboResult<()> {
379 if vars.is_empty() {
380 return Err(QuboError::ConstraintError(
381 "Empty sum constraint".to_string(),
382 ));
383 }
384
385 let weight = self.constraint_weight;
387
388 for var in vars {
390 let current = self.model.get_linear(var.index)?;
391 self.set_linear_term(var, weight.mul_add(2.0f64.mul_add(-target, 1.0), current))?;
392 }
393
394 for i in 0..vars.len() {
396 for j in (i + 1)..vars.len() {
397 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
398 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
399 }
400 }
401
402 self.model.offset += weight * target * target;
404
405 Ok(())
406 }
407
408 #[must_use]
410 pub fn build(&self) -> QuboModel {
411 self.model.clone()
412 }
413
414 #[must_use]
416 pub fn variable_map(&self) -> HashMap<String, usize> {
417 self.var_map.clone()
418 }
419
420 #[must_use]
422 pub const fn num_variables(&self) -> usize {
423 self.num_vars
424 }
425
426 #[must_use]
428 pub fn variables(&self) -> Vec<Variable> {
429 self.var_map
430 .iter()
431 .map(|(name, &index)| Variable::new(name, index))
432 .collect()
433 }
434}
435
436impl Default for QuboBuilder {
438 fn default() -> Self {
439 Self::new()
440 }
441}
442
443pub trait QuboFormulation {
445 fn to_qubo(&self) -> QuboResult<(QuboModel, HashMap<String, usize>)>;
447
448 fn interpret_solution(&self, binary_vars: &[bool]) -> QuboResult<Vec<(String, bool)>>;
450}
451
452impl QuboFormulation for QuboModel {
454 fn to_qubo(&self) -> QuboResult<(QuboModel, HashMap<String, usize>)> {
455 let mut var_map = HashMap::new();
457 for i in 0..self.num_variables {
458 var_map.insert(format!("x_{i}"), i);
459 }
460 Ok((self.clone(), var_map))
461 }
462
463 fn interpret_solution(&self, binary_vars: &[bool]) -> QuboResult<Vec<(String, bool)>> {
464 if binary_vars.len() != self.num_variables {
465 return Err(QuboError::ConstraintError(format!(
466 "Solution length {} does not match number of variables {}",
467 binary_vars.len(),
468 self.num_variables
469 )));
470 }
471
472 let mut result = Vec::new();
473 for (i, &value) in binary_vars.iter().enumerate() {
474 result.push((format!("x_{i}"), value));
475 }
476 Ok(result)
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_qubo_builder_basic() {
486 let mut builder = QuboBuilder::new();
487
488 let x1 = builder
490 .add_variable("x1")
491 .expect("failed to add variable x1");
492 let x2 = builder
493 .add_variable("x2")
494 .expect("failed to add variable x2");
495 let x3 = builder
496 .add_variable("x3")
497 .expect("failed to add variable x3");
498
499 builder
501 .set_linear_term(&x1, 2.0)
502 .expect("failed to set linear term for x1");
503 builder
504 .set_linear_term(&x2, -1.0)
505 .expect("failed to set linear term for x2");
506 builder
507 .set_quadratic_term(&x1, &x2, -4.0)
508 .expect("failed to set quadratic term for x1-x2");
509 builder
510 .set_quadratic_term(&x2, &x3, 2.0)
511 .expect("failed to set quadratic term for x2-x3");
512 builder.set_offset(1.5).expect("failed to set offset");
513
514 let model = builder.build();
516
517 assert_eq!(
519 model.get_linear(0).expect("failed to get linear term 0"),
520 2.0
521 );
522 assert_eq!(
523 model.get_linear(1).expect("failed to get linear term 1"),
524 -1.0
525 );
526 assert_eq!(
527 model.get_linear(2).expect("failed to get linear term 2"),
528 0.0
529 );
530
531 assert_eq!(
533 model
534 .get_quadratic(0, 1)
535 .expect("failed to get quadratic term 0-1"),
536 -4.0
537 );
538 assert_eq!(
539 model
540 .get_quadratic(1, 2)
541 .expect("failed to get quadratic term 1-2"),
542 2.0
543 );
544
545 assert_eq!(model.offset, 1.5);
547 }
548
549 #[test]
550 fn test_qubo_builder_objective() {
551 let mut builder = QuboBuilder::new();
552
553 let x1 = builder
555 .add_variable("x1")
556 .expect("failed to add variable x1");
557 let x2 = builder
558 .add_variable("x2")
559 .expect("failed to add variable x2");
560
561 builder
563 .minimize_linear(&x1, 2.0)
564 .expect("failed to minimize linear x1");
565 builder
566 .minimize_linear(&x2, -1.0)
567 .expect("failed to minimize linear x2");
568 builder
569 .minimize_quadratic(&x1, &x2, -4.0)
570 .expect("failed to minimize quadratic x1-x2");
571
572 let model = builder.build();
574
575 assert_eq!(
577 model.get_linear(0).expect("failed to get linear term 0"),
578 2.0
579 );
580 assert_eq!(
581 model.get_linear(1).expect("failed to get linear term 1"),
582 -1.0
583 );
584
585 assert_eq!(
587 model
588 .get_quadratic(0, 1)
589 .expect("failed to get quadratic term 0-1"),
590 -4.0
591 );
592 }
593
594 #[test]
595 fn test_qubo_builder_constraints() {
596 let mut builder = QuboBuilder::new();
597
598 let x1 = builder
600 .add_variable("x1")
601 .expect("failed to add variable x1");
602 let x2 = builder
603 .add_variable("x2")
604 .expect("failed to add variable x2");
605 let x3 = builder
606 .add_variable("x3")
607 .expect("failed to add variable x3");
608
609 builder
611 .set_constraint_weight(5.0)
612 .expect("failed to set constraint weight");
613
614 builder
616 .constrain_equal(&x1, &x2)
617 .expect("failed to add equality constraint");
618
619 builder
621 .constrain_different(&x2, &x3)
622 .expect("failed to add inequality constraint");
623
624 let model = builder.build();
626
627 assert_eq!(
633 model.get_linear(0).expect("failed to get linear term 0"),
634 5.0
635 ); assert_eq!(
637 model.get_linear(1).expect("failed to get linear term 1"),
638 5.0 - 5.0
639 ); assert_eq!(
641 model.get_linear(2).expect("failed to get linear term 2"),
642 -5.0
643 ); assert_eq!(
647 model
648 .get_quadratic(0, 1)
649 .expect("failed to get quadratic term 0-1"),
650 -10.0
651 ); assert_eq!(
653 model
654 .get_quadratic(1, 2)
655 .expect("failed to get quadratic term 1-2"),
656 10.0
657 ); assert_eq!(model.offset, 5.0); }
662
663 #[test]
664 fn test_qubo_builder_one_hot() {
665 let mut builder = QuboBuilder::new();
666
667 let x1 = builder
669 .add_variable("x1")
670 .expect("failed to add variable x1");
671 let x2 = builder
672 .add_variable("x2")
673 .expect("failed to add variable x2");
674 let x3 = builder
675 .add_variable("x3")
676 .expect("failed to add variable x3");
677
678 builder
680 .set_constraint_weight(5.0)
681 .expect("failed to set constraint weight");
682
683 builder
685 .constrain_one_hot(&[x1.clone(), x2.clone(), x3.clone()])
686 .expect("failed to add one-hot constraint");
687
688 let model = builder.build();
690
691 assert_eq!(
698 model.get_linear(0).expect("failed to get linear term 0"),
699 -5.0
700 ); assert_eq!(
702 model.get_linear(1).expect("failed to get linear term 1"),
703 -5.0
704 ); assert_eq!(
706 model.get_linear(2).expect("failed to get linear term 2"),
707 -5.0
708 ); assert_eq!(
712 model
713 .get_quadratic(0, 1)
714 .expect("failed to get quadratic term 0-1"),
715 10.0
716 ); assert_eq!(
718 model
719 .get_quadratic(0, 2)
720 .expect("failed to get quadratic term 0-2"),
721 10.0
722 ); assert_eq!(
724 model
725 .get_quadratic(1, 2)
726 .expect("failed to get quadratic term 1-2"),
727 10.0
728 ); assert_eq!(model.offset, 5.0); }
733}