1use std::collections::HashMap;
7use thiserror::Error;
8
9use crate::ising::{IsingError, QuboModel};
10
11#[derive(Error, Debug)]
13#[non_exhaustive]
14pub enum QuboError {
15 #[error("Ising error: {0}")]
17 IsingError(#[from] IsingError),
18
19 #[error("Constraint error: {0}")]
21 ConstraintError(String),
22
23 #[error("Variable {0} is already defined")]
25 DuplicateVariable(String),
26
27 #[error("Variable {0} not found")]
29 VariableNotFound(String),
30}
31
32pub type QuboResult<T> = Result<T, QuboError>;
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct Variable {
38 pub name: String,
40 pub index: usize,
42}
43
44impl Variable {
45 pub fn new(name: impl Into<String>, index: usize) -> Self {
47 Self {
48 name: name.into(),
49 index,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
74pub struct QuboBuilder {
75 num_vars: usize,
77
78 var_map: HashMap<String, usize>,
80
81 model: QuboModel,
83
84 constraint_weight: f64,
86}
87
88impl QuboBuilder {
89 #[must_use]
91 pub fn new() -> Self {
92 Self {
93 num_vars: 0,
94 var_map: HashMap::new(),
95 model: QuboModel::new(0),
96 constraint_weight: 10.0,
97 }
98 }
99
100 pub fn set_constraint_weight(&mut self, weight: f64) -> QuboResult<()> {
102 if !weight.is_finite() || weight <= 0.0 {
103 return Err(QuboError::ConstraintError(format!(
104 "Constraint weight must be positive and finite, got {weight}"
105 )));
106 }
107
108 self.constraint_weight = weight;
109 Ok(())
110 }
111
112 pub fn add_variable(&mut self, name: impl Into<String>) -> QuboResult<Variable> {
114 let name = name.into();
115
116 if self.var_map.contains_key(&name) {
118 return Err(QuboError::DuplicateVariable(name));
119 }
120
121 let index = self.num_vars;
123 self.var_map.insert(name.clone(), index);
124 self.num_vars += 1;
125
126 self.model = QuboModel::new(self.num_vars);
128
129 Ok(Variable::new(name, index))
130 }
131
132 pub fn add_variables(
134 &mut self,
135 names: impl IntoIterator<Item = impl Into<String>>,
136 ) -> QuboResult<Vec<Variable>> {
137 let mut variables = Vec::new();
138 for name in names {
139 variables.push(self.add_variable(name)?);
140 }
141 Ok(variables)
142 }
143
144 pub fn get_variable(&self, name: &str) -> QuboResult<Variable> {
146 match self.var_map.get(name) {
147 Some(&index) => Ok(Variable::new(name, index)),
148 None => Err(QuboError::VariableNotFound(name.to_string())),
149 }
150 }
151
152 pub fn set_linear_term(&mut self, var: &Variable, value: f64) -> QuboResult<()> {
154 if var.index >= self.num_vars {
156 return Err(QuboError::VariableNotFound(var.name.clone()));
157 }
158
159 Ok(self.model.set_linear(var.index, value)?)
160 }
161
162 pub fn set_quadratic_term(
164 &mut self,
165 var1: &Variable,
166 var2: &Variable,
167 value: f64,
168 ) -> QuboResult<()> {
169 if var1.index >= self.num_vars {
171 return Err(QuboError::VariableNotFound(var1.name.clone()));
172 }
173 if var2.index >= self.num_vars {
174 return Err(QuboError::VariableNotFound(var2.name.clone()));
175 }
176
177 if var1.index == var2.index {
179 return Err(QuboError::ConstraintError(format!(
180 "Cannot set quadratic term for the same variable: {}",
181 var1.name
182 )));
183 }
184
185 Ok(self.model.set_quadratic(var1.index, var2.index, value)?)
186 }
187
188 pub fn set_offset(&mut self, offset: f64) -> QuboResult<()> {
190 if !offset.is_finite() {
191 return Err(QuboError::ConstraintError(format!(
192 "Offset must be finite, got {offset}"
193 )));
194 }
195
196 self.model.offset = offset;
197 Ok(())
198 }
199
200 pub fn add_bias(&mut self, var_index: usize, bias: f64) -> QuboResult<()> {
202 if var_index >= self.num_vars {
203 return Err(QuboError::VariableNotFound(format!(
204 "Variable index {var_index}"
205 )));
206 }
207 let current = self.model.get_linear(var_index)?;
208 self.model.set_linear(var_index, current + bias)?;
209 Ok(())
210 }
211
212 pub fn add_coupling(
214 &mut self,
215 var1_index: usize,
216 var2_index: usize,
217 coupling: f64,
218 ) -> QuboResult<()> {
219 if var1_index >= self.num_vars {
220 return Err(QuboError::VariableNotFound(format!(
221 "Variable index {var1_index}"
222 )));
223 }
224 if var2_index >= self.num_vars {
225 return Err(QuboError::VariableNotFound(format!(
226 "Variable index {var2_index}"
227 )));
228 }
229 let current = self.model.get_quadratic(var1_index, var2_index)?;
230 self.model
231 .set_quadratic(var1_index, var2_index, current + coupling)?;
232 Ok(())
233 }
234
235 pub fn minimize_linear(&mut self, var: &Variable, coeff: f64) -> QuboResult<()> {
237 self.set_linear_term(var, self.model.get_linear(var.index)? + coeff)
238 }
239
240 pub fn minimize_quadratic(
242 &mut self,
243 var1: &Variable,
244 var2: &Variable,
245 coeff: f64,
246 ) -> QuboResult<()> {
247 let current = self.model.get_quadratic(var1.index, var2.index)?;
248 self.set_quadratic_term(var1, var2, current + coeff)
249 }
250
251 pub fn constrain_equal(&mut self, var1: &Variable, var2: &Variable) -> QuboResult<()> {
255 let weight = self.constraint_weight;
257
258 self.set_linear_term(var1, self.model.get_linear(var1.index)? + weight)?;
260
261 self.set_linear_term(var2, self.model.get_linear(var2.index)? + weight)?;
263
264 let current = self.model.get_quadratic(var1.index, var2.index)?;
266 self.set_quadratic_term(var1, var2, 2.0f64.mul_add(-weight, current))
267 }
268
269 pub fn constrain_different(&mut self, var1: &Variable, var2: &Variable) -> QuboResult<()> {
273 let weight = self.constraint_weight;
275
276 self.set_linear_term(var1, self.model.get_linear(var1.index)? - weight)?;
278
279 self.set_linear_term(var2, self.model.get_linear(var2.index)? - weight)?;
281
282 let current = self.model.get_quadratic(var1.index, var2.index)?;
284 self.set_quadratic_term(var1, var2, 2.0f64.mul_add(weight, current))?;
285
286 self.model.offset += weight;
288
289 Ok(())
290 }
291
292 pub fn constrain_one_hot(&mut self, vars: &[Variable]) -> QuboResult<()> {
296 if vars.is_empty() {
297 return Err(QuboError::ConstraintError(
298 "Empty one-hot constraint".to_string(),
299 ));
300 }
301
302 let weight = self.constraint_weight;
307
308 for var in vars {
310 self.set_linear_term(var, self.model.get_linear(var.index)? - weight)?;
311 }
312
313 for i in 0..vars.len() {
315 for j in (i + 1)..vars.len() {
316 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
317 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
318 }
319 }
320
321 self.model.offset += weight;
323
324 Ok(())
325 }
326
327 pub fn constrain_at_most_one(&mut self, vars: &[Variable]) -> QuboResult<()> {
331 if vars.is_empty() {
332 return Err(QuboError::ConstraintError(
333 "Empty at-most-one constraint".to_string(),
334 ));
335 }
336
337 let weight = self.constraint_weight;
341
342 for i in 0..vars.len() {
344 for j in (i + 1)..vars.len() {
345 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
346 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
347 }
348 }
349
350 Ok(())
351 }
352
353 pub fn constrain_at_least_one(&mut self, vars: &[Variable]) -> QuboResult<()> {
357 if vars.is_empty() {
358 return Err(QuboError::ConstraintError(
359 "Empty at-least-one constraint".to_string(),
360 ));
361 }
362
363 let weight = self.constraint_weight;
368
369 for var in vars {
371 self.set_linear_term(
372 var,
373 2.0f64.mul_add(-weight, self.model.get_linear(var.index)?),
374 )?;
375 }
376
377 for i in 0..vars.len() {
379 for j in (i + 1)..vars.len() {
380 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
381 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
382 }
383 }
384
385 self.model.offset += weight;
387
388 Ok(())
389 }
390
391 pub fn constrain_sum_equal(&mut self, vars: &[Variable], target: f64) -> QuboResult<()> {
395 if vars.is_empty() {
396 return Err(QuboError::ConstraintError(
397 "Empty sum constraint".to_string(),
398 ));
399 }
400
401 let weight = self.constraint_weight;
403
404 for var in vars {
406 let current = self.model.get_linear(var.index)?;
407 self.set_linear_term(var, weight.mul_add(2.0f64.mul_add(-target, 1.0), current))?;
408 }
409
410 for i in 0..vars.len() {
412 for j in (i + 1)..vars.len() {
413 let current = self.model.get_quadratic(vars[i].index, vars[j].index)?;
414 self.set_quadratic_term(&vars[i], &vars[j], 2.0f64.mul_add(weight, current))?;
415 }
416 }
417
418 self.model.offset += weight * target * target;
420
421 Ok(())
422 }
423
424 #[must_use]
426 pub fn build(&self) -> QuboModel {
427 self.model.clone()
428 }
429
430 #[must_use]
432 pub fn variable_map(&self) -> HashMap<String, usize> {
433 self.var_map.clone()
434 }
435
436 #[must_use]
438 pub const fn num_variables(&self) -> usize {
439 self.num_vars
440 }
441
442 #[must_use]
444 pub fn variables(&self) -> Vec<Variable> {
445 self.var_map
446 .iter()
447 .map(|(name, &index)| Variable::new(name, index))
448 .collect()
449 }
450}
451
452impl Default for QuboBuilder {
454 fn default() -> Self {
455 Self::new()
456 }
457}
458
459pub trait QuboFormulation {
461 fn to_qubo(&self) -> QuboResult<(QuboModel, HashMap<String, usize>)>;
463
464 fn interpret_solution(&self, binary_vars: &[bool]) -> QuboResult<Vec<(String, bool)>>;
466}
467
468impl QuboFormulation for QuboModel {
470 fn to_qubo(&self) -> QuboResult<(QuboModel, HashMap<String, usize>)> {
471 let mut var_map = HashMap::new();
473 for i in 0..self.num_variables {
474 var_map.insert(format!("x_{i}"), i);
475 }
476 Ok((self.clone(), var_map))
477 }
478
479 fn interpret_solution(&self, binary_vars: &[bool]) -> QuboResult<Vec<(String, bool)>> {
480 if binary_vars.len() != self.num_variables {
481 return Err(QuboError::ConstraintError(format!(
482 "Solution length {} does not match number of variables {}",
483 binary_vars.len(),
484 self.num_variables
485 )));
486 }
487
488 let mut result = Vec::new();
489 for (i, &value) in binary_vars.iter().enumerate() {
490 result.push((format!("x_{i}"), value));
491 }
492 Ok(result)
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_qubo_builder_basic() {
502 let mut builder = QuboBuilder::new();
503
504 let x1 = builder
506 .add_variable("x1")
507 .expect("failed to add variable x1");
508 let x2 = builder
509 .add_variable("x2")
510 .expect("failed to add variable x2");
511 let x3 = builder
512 .add_variable("x3")
513 .expect("failed to add variable x3");
514
515 builder
517 .set_linear_term(&x1, 2.0)
518 .expect("failed to set linear term for x1");
519 builder
520 .set_linear_term(&x2, -1.0)
521 .expect("failed to set linear term for x2");
522 builder
523 .set_quadratic_term(&x1, &x2, -4.0)
524 .expect("failed to set quadratic term for x1-x2");
525 builder
526 .set_quadratic_term(&x2, &x3, 2.0)
527 .expect("failed to set quadratic term for x2-x3");
528 builder.set_offset(1.5).expect("failed to set offset");
529
530 let model = builder.build();
532
533 assert_eq!(
535 model.get_linear(0).expect("failed to get linear term 0"),
536 2.0
537 );
538 assert_eq!(
539 model.get_linear(1).expect("failed to get linear term 1"),
540 -1.0
541 );
542 assert_eq!(
543 model.get_linear(2).expect("failed to get linear term 2"),
544 0.0
545 );
546
547 assert_eq!(
549 model
550 .get_quadratic(0, 1)
551 .expect("failed to get quadratic term 0-1"),
552 -4.0
553 );
554 assert_eq!(
555 model
556 .get_quadratic(1, 2)
557 .expect("failed to get quadratic term 1-2"),
558 2.0
559 );
560
561 assert_eq!(model.offset, 1.5);
563 }
564
565 #[test]
566 fn test_qubo_builder_objective() {
567 let mut builder = QuboBuilder::new();
568
569 let x1 = builder
571 .add_variable("x1")
572 .expect("failed to add variable x1");
573 let x2 = builder
574 .add_variable("x2")
575 .expect("failed to add variable x2");
576
577 builder
579 .minimize_linear(&x1, 2.0)
580 .expect("failed to minimize linear x1");
581 builder
582 .minimize_linear(&x2, -1.0)
583 .expect("failed to minimize linear x2");
584 builder
585 .minimize_quadratic(&x1, &x2, -4.0)
586 .expect("failed to minimize quadratic x1-x2");
587
588 let model = builder.build();
590
591 assert_eq!(
593 model.get_linear(0).expect("failed to get linear term 0"),
594 2.0
595 );
596 assert_eq!(
597 model.get_linear(1).expect("failed to get linear term 1"),
598 -1.0
599 );
600
601 assert_eq!(
603 model
604 .get_quadratic(0, 1)
605 .expect("failed to get quadratic term 0-1"),
606 -4.0
607 );
608 }
609
610 #[test]
611 fn test_qubo_builder_constraints() {
612 let mut builder = QuboBuilder::new();
613
614 let x1 = builder
616 .add_variable("x1")
617 .expect("failed to add variable x1");
618 let x2 = builder
619 .add_variable("x2")
620 .expect("failed to add variable x2");
621 let x3 = builder
622 .add_variable("x3")
623 .expect("failed to add variable x3");
624
625 builder
627 .set_constraint_weight(5.0)
628 .expect("failed to set constraint weight");
629
630 builder
632 .constrain_equal(&x1, &x2)
633 .expect("failed to add equality constraint");
634
635 builder
637 .constrain_different(&x2, &x3)
638 .expect("failed to add inequality constraint");
639
640 let model = builder.build();
642
643 assert_eq!(
649 model.get_linear(0).expect("failed to get linear term 0"),
650 5.0
651 ); assert_eq!(
653 model.get_linear(1).expect("failed to get linear term 1"),
654 5.0 - 5.0
655 ); assert_eq!(
657 model.get_linear(2).expect("failed to get linear term 2"),
658 -5.0
659 ); assert_eq!(
663 model
664 .get_quadratic(0, 1)
665 .expect("failed to get quadratic term 0-1"),
666 -10.0
667 ); assert_eq!(
669 model
670 .get_quadratic(1, 2)
671 .expect("failed to get quadratic term 1-2"),
672 10.0
673 ); assert_eq!(model.offset, 5.0); }
678
679 #[test]
680 fn test_qubo_builder_one_hot() {
681 let mut builder = QuboBuilder::new();
682
683 let x1 = builder
685 .add_variable("x1")
686 .expect("failed to add variable x1");
687 let x2 = builder
688 .add_variable("x2")
689 .expect("failed to add variable x2");
690 let x3 = builder
691 .add_variable("x3")
692 .expect("failed to add variable x3");
693
694 builder
696 .set_constraint_weight(5.0)
697 .expect("failed to set constraint weight");
698
699 builder
701 .constrain_one_hot(&[x1.clone(), x2.clone(), x3.clone()])
702 .expect("failed to add one-hot constraint");
703
704 let model = builder.build();
706
707 assert_eq!(
714 model.get_linear(0).expect("failed to get linear term 0"),
715 -5.0
716 ); assert_eq!(
718 model.get_linear(1).expect("failed to get linear term 1"),
719 -5.0
720 ); assert_eq!(
722 model.get_linear(2).expect("failed to get linear term 2"),
723 -5.0
724 ); assert_eq!(
728 model
729 .get_quadratic(0, 1)
730 .expect("failed to get quadratic term 0-1"),
731 10.0
732 ); assert_eq!(
734 model
735 .get_quadratic(0, 2)
736 .expect("failed to get quadratic term 0-2"),
737 10.0
738 ); assert_eq!(
740 model
741 .get_quadratic(1, 2)
742 .expect("failed to get quadratic term 1-2"),
743 10.0
744 ); assert_eq!(model.offset, 5.0); }
749}