1use std::collections::{HashMap, HashSet};
8use std::fmt;
9use thiserror::Error;
10
11use crate::ising::{IsingError, IsingModel, QuboModel};
12
13#[derive(Error, Debug)]
15pub enum DslError {
16 #[error("Variable not found: {0}")]
18 VariableNotFound(String),
19
20 #[error("Invalid constraint: {0}")]
22 InvalidConstraint(String),
23
24 #[error("Invalid objective: {0}")]
26 InvalidObjective(String),
27
28 #[error("Compilation error: {0}")]
30 CompilationError(String),
31
32 #[error("Type mismatch: {0}")]
34 TypeMismatch(String),
35
36 #[error("Ising error: {0}")]
38 IsingError(#[from] IsingError),
39
40 #[error("Dimension mismatch: expected {expected}, got {actual}")]
42 DimensionMismatch { expected: usize, actual: usize },
43
44 #[error("Invalid range: {0}")]
46 InvalidRange(String),
47}
48
49pub type DslResult<T> = Result<T, DslError>;
51
52#[derive(Debug, Clone, PartialEq)]
54pub enum VariableType {
55 Binary,
57
58 Integer { min: i32, max: i32 },
60
61 Spin,
63
64 Categorical { categories: Vec<String> },
66
67 Continuous { min: f64, max: f64, steps: usize },
69}
70
71#[derive(Debug, Clone)]
73pub struct Variable {
74 pub id: String,
76
77 pub var_type: VariableType,
79
80 pub qubit_indices: Vec<usize>,
82
83 pub description: Option<String>,
85}
86
87#[derive(Debug, Clone)]
89pub struct VariableVector {
90 pub variables: Vec<Variable>,
92
93 pub name: String,
95}
96
97#[derive(Debug, Clone)]
99pub enum Expression {
100 Constant(f64),
102
103 Variable(Variable),
105
106 Sum(Vec<Self>),
108
109 Product(Box<Self>, Box<Self>),
111
112 LinearCombination { weights: Vec<f64>, terms: Vec<Self> },
114
115 Quadratic {
117 var1: Variable,
118 var2: Variable,
119 coefficient: f64,
120 },
121
122 Power(Box<Self>, i32),
124
125 Negate(Box<Self>),
127
128 Abs(Box<Self>),
130
131 Conditional {
133 condition: Box<BooleanExpression>,
134 if_true: Box<Self>,
135 if_false: Box<Self>,
136 },
137}
138
139#[derive(Debug, Clone)]
141pub enum BooleanExpression {
142 True,
144
145 False,
147
148 Equal(Expression, Expression),
150
151 LessThan(Expression, Expression),
153
154 LessThanOrEqual(Expression, Expression),
156
157 GreaterThan(Expression, Expression),
159
160 GreaterThanOrEqual(Expression, Expression),
162
163 And(Box<Self>, Box<Self>),
165
166 Or(Box<Self>, Box<Self>),
168
169 Not(Box<Self>),
171
172 Xor(Box<Self>, Box<Self>),
174
175 Implies(Box<Self>, Box<Self>),
177
178 AllDifferent(Vec<Variable>),
180
181 AtMostOne(Vec<Variable>),
183
184 ExactlyOne(Vec<Variable>),
186}
187
188#[derive(Debug, Clone)]
190pub struct Constraint {
191 pub expression: BooleanExpression,
193
194 pub name: Option<String>,
196
197 pub penalty_weight: Option<f64>,
199
200 pub is_hard: bool,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum ObjectiveDirection {
207 Minimize,
208 Maximize,
209}
210
211#[derive(Debug, Clone)]
213pub struct Objective {
214 pub expression: Expression,
216
217 pub direction: ObjectiveDirection,
219
220 pub name: Option<String>,
222}
223
224pub struct OptimizationModel {
226 pub name: String,
228
229 variables: HashMap<String, Variable>,
231
232 variable_vectors: HashMap<String, VariableVector>,
234
235 constraints: Vec<Constraint>,
237
238 objectives: Vec<Objective>,
240
241 next_qubit: usize,
243
244 metadata: HashMap<String, String>,
246}
247
248impl OptimizationModel {
249 pub fn new(name: impl Into<String>) -> Self {
251 Self {
252 name: name.into(),
253 variables: HashMap::new(),
254 variable_vectors: HashMap::new(),
255 constraints: Vec::new(),
256 objectives: Vec::new(),
257 next_qubit: 0,
258 metadata: HashMap::new(),
259 }
260 }
261
262 pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
264 self.metadata.insert(key.into(), value.into());
265 }
266
267 pub fn add_binary(&mut self, name: impl Into<String>) -> DslResult<Variable> {
269 let var_name = name.into();
270
271 if self.variables.contains_key(&var_name) {
272 return Err(DslError::InvalidConstraint(format!(
273 "Variable {var_name} already exists"
274 )));
275 }
276
277 let var = Variable {
278 id: var_name.clone(),
279 var_type: VariableType::Binary,
280 qubit_indices: vec![self.next_qubit],
281 description: None,
282 };
283
284 self.next_qubit += 1;
285 self.variables.insert(var_name, var.clone());
286
287 Ok(var)
288 }
289
290 pub fn add_binary_vector(
292 &mut self,
293 name: impl Into<String>,
294 size: usize,
295 ) -> DslResult<VariableVector> {
296 let vec_name = name.into();
297 let mut variables = Vec::new();
298
299 for i in 0..size {
300 let var_name = format!("{vec_name}[{i}]");
301 let var = self.add_binary(var_name)?;
302 variables.push(var);
303 }
304
305 let var_vec = VariableVector {
306 variables,
307 name: vec_name.clone(),
308 };
309
310 self.variable_vectors.insert(vec_name, var_vec.clone());
311 Ok(var_vec)
312 }
313
314 pub fn add_integer(
316 &mut self,
317 name: impl Into<String>,
318 min: i32,
319 max: i32,
320 ) -> DslResult<Variable> {
321 let var_name = name.into();
322
323 if self.variables.contains_key(&var_name) {
324 return Err(DslError::InvalidConstraint(format!(
325 "Variable {var_name} already exists"
326 )));
327 }
328
329 if min > max {
330 return Err(DslError::InvalidRange(format!(
331 "Invalid range [{min}, {max}]"
332 )));
333 }
334
335 let range = (max - min) as u32;
337 let num_bits = (range + 1).next_power_of_two().trailing_zeros() as usize;
338
339 let qubit_indices: Vec<usize> = (0..num_bits)
340 .map(|_| {
341 let idx = self.next_qubit;
342 self.next_qubit += 1;
343 idx
344 })
345 .collect();
346
347 let var = Variable {
348 id: var_name.clone(),
349 var_type: VariableType::Integer { min, max },
350 qubit_indices,
351 description: None,
352 };
353
354 self.variables.insert(var_name, var.clone());
355 Ok(var)
356 }
357
358 pub fn add_spin(&mut self, name: impl Into<String>) -> DslResult<Variable> {
360 let var_name = name.into();
361
362 if self.variables.contains_key(&var_name) {
363 return Err(DslError::InvalidConstraint(format!(
364 "Variable {var_name} already exists"
365 )));
366 }
367
368 let var = Variable {
369 id: var_name.clone(),
370 var_type: VariableType::Spin,
371 qubit_indices: vec![self.next_qubit],
372 description: None,
373 };
374
375 self.next_qubit += 1;
376 self.variables.insert(var_name, var.clone());
377
378 Ok(var)
379 }
380
381 pub fn add_constraint(&mut self, constraint: impl Into<Constraint>) -> DslResult<()> {
383 let constraint = constraint.into();
384 self.constraints.push(constraint);
385 Ok(())
386 }
387
388 pub fn minimize(&mut self, expression: impl Into<Expression>) -> DslResult<()> {
390 let objective = Objective {
391 expression: expression.into(),
392 direction: ObjectiveDirection::Minimize,
393 name: None,
394 };
395
396 self.objectives.push(objective);
397 Ok(())
398 }
399
400 pub fn maximize(&mut self, expression: impl Into<Expression>) -> DslResult<()> {
402 let objective = Objective {
403 expression: expression.into(),
404 direction: ObjectiveDirection::Maximize,
405 name: None,
406 };
407
408 self.objectives.push(objective);
409 Ok(())
410 }
411
412 pub fn compile_to_qubo(&self) -> DslResult<QuboModel> {
414 let mut model = QuboModel::new(self.next_qubit);
416
417 for objective in &self.objectives {
419 let sign = match objective.direction {
420 ObjectiveDirection::Minimize => 1.0,
421 ObjectiveDirection::Maximize => -1.0,
422 };
423
424 self.add_expression_to_qubo(&mut model, &objective.expression, sign)?;
425 }
426
427 for constraint in &self.constraints {
429 let penalty_weight = if constraint.is_hard {
431 1000.0 } else {
433 constraint.penalty_weight.unwrap_or(1.0)
434 };
435
436 self.add_constraint_to_qubo(&mut model, &constraint.expression, penalty_weight)?;
437 }
438
439 Ok(model)
440 }
441
442 pub fn compile_to_ising(&self) -> DslResult<IsingModel> {
444 let qubo = self.compile_to_qubo()?;
445
446 let mut ising = IsingModel::new(self.next_qubit);
450
451 let mut offset = 0.0;
456
457 for i in 0..self.next_qubit {
459 let q_val = qubo.get_linear(i)?;
460
461 if q_val.abs() > 1e-10 {
462 let h_i = q_val / 2.0; let current_bias = ising.get_bias(i)?;
466 ising.set_bias(i, current_bias + h_i)?;
467 offset += q_val / 4.0; }
469 }
470
471 for (i, j, q_val) in qubo.quadratic_terms() {
473 if q_val.abs() > 1e-10 {
474 let j_ij = q_val / 4.0; ising.set_coupling(i, j, j_ij)?;
478
479 let bias_i = ising.get_bias(i)?;
481 ising.set_bias(i, bias_i + q_val / 4.0)?;
482
483 let bias_j = ising.get_bias(j)?;
484 ising.set_bias(j, bias_j + q_val / 4.0)?;
485
486 offset += q_val / 4.0;
487 }
488 }
489
490 Ok(ising)
494 }
495
496 fn add_expression_to_qubo(
498 &self,
499 model: &mut QuboModel,
500 expr: &Expression,
501 coefficient: f64,
502 ) -> DslResult<()> {
503 match expr {
504 Expression::Constant(_c) => {
505 Ok(())
507 }
508 Expression::Variable(var) => {
509 if let Some(&qubit_idx) = var.qubit_indices.first() {
511 model.add_linear(qubit_idx, coefficient)?;
512 }
513 Ok(())
514 }
515 Expression::Sum(terms) => {
516 for term in terms {
518 self.add_expression_to_qubo(model, term, coefficient)?;
519 }
520 Ok(())
521 }
522 Expression::Product(e1, e2) => {
523 if let (Expression::Variable(v1), Expression::Variable(v2)) =
525 (e1.as_ref(), e2.as_ref())
526 {
527 if let (Some(&q1), Some(&q2)) =
528 (v1.qubit_indices.first(), v2.qubit_indices.first())
529 {
530 if q1 == q2 {
531 model.add_linear(q1, coefficient)?;
533 } else {
534 let current = model.get_quadratic(q1, q2)?;
536 model.set_quadratic(q1, q2, current + coefficient)?;
537 }
538 }
539 }
540 Ok(())
541 }
542 Expression::Quadratic {
543 var1,
544 var2,
545 coefficient: coef,
546 } => {
547 if let (Some(&q1), Some(&q2)) =
549 (var1.qubit_indices.first(), var2.qubit_indices.first())
550 {
551 if q1 == q2 {
552 model.add_linear(q1, coefficient * coef)?;
554 } else {
555 let current = model.get_quadratic(q1, q2)?;
557 model.set_quadratic(q1, q2, current + coefficient * coef)?;
558 }
559 }
560 Ok(())
561 }
562 Expression::LinearCombination { weights, terms } => {
563 for (weight, term) in weights.iter().zip(terms.iter()) {
565 self.add_expression_to_qubo(model, term, coefficient * weight)?;
566 }
567 Ok(())
568 }
569 Expression::Negate(e) => {
570 self.add_expression_to_qubo(model, e, -coefficient)?;
571 Ok(())
572 }
573 _ => {
574 Err(DslError::CompilationError(
576 "Unsupported expression type in QUBO compilation".to_string(),
577 ))
578 }
579 }
580 }
581
582 fn add_constraint_to_qubo(
584 &self,
585 model: &mut QuboModel,
586 constraint: &BooleanExpression,
587 penalty: f64,
588 ) -> DslResult<()> {
589 match constraint {
590 BooleanExpression::True => Ok(()),
591 BooleanExpression::False => {
592 Err(DslError::InvalidConstraint(
594 "Unsatisfiable constraint (False)".to_string(),
595 ))
596 }
597 BooleanExpression::Equal(e1, e2) => {
598 let diff =
600 Expression::Sum(vec![e1.clone(), Expression::Negate(Box::new(e2.clone()))]);
601 let penalty_expr = Expression::Product(Box::new(diff.clone()), Box::new(diff));
602 self.add_expression_to_qubo(model, &penalty_expr, penalty)
603 }
604 BooleanExpression::ExactlyOne(vars) => {
605 let sum_expr = Expression::Sum(
607 vars.iter()
608 .map(|v| Expression::Variable(v.clone()))
609 .collect(),
610 );
611 let one = Expression::Constant(1.0);
612 let diff = Expression::Sum(vec![sum_expr, Expression::Negate(Box::new(one))]);
613 let penalty_expr = Expression::Product(Box::new(diff.clone()), Box::new(diff));
614 self.add_expression_to_qubo(model, &penalty_expr, penalty)
615 }
616 BooleanExpression::AtMostOne(vars) => {
617 for i in 0..vars.len() {
619 for j in (i + 1)..vars.len() {
620 if let (Some(&q1), Some(&q2)) =
621 (vars[i].qubit_indices.first(), vars[j].qubit_indices.first())
622 {
623 let current = model.get_quadratic(q1, q2)?;
624 model.set_quadratic(q1, q2, current + penalty)?;
625 }
626 }
627 }
628 Ok(())
629 }
630 BooleanExpression::And(b1, b2) => {
631 self.add_constraint_to_qubo(model, b1, penalty)?;
633 self.add_constraint_to_qubo(model, b2, penalty)?;
634 Ok(())
635 }
636 _ => {
637 Err(DslError::CompilationError(
639 "Unsupported constraint type in QUBO compilation".to_string(),
640 ))
641 }
642 }
643 }
644
645 #[must_use]
647 pub fn summary(&self) -> ModelSummary {
648 ModelSummary {
649 name: self.name.clone(),
650 num_variables: self.variables.len(),
651 num_qubits: self.next_qubit,
652 num_constraints: self.constraints.len(),
653 num_objectives: self.objectives.len(),
654 variable_types: self.count_variable_types(),
655 }
656 }
657
658 fn count_variable_types(&self) -> HashMap<String, usize> {
660 let mut counts = HashMap::new();
661
662 for var in self.variables.values() {
663 let type_name = match &var.var_type {
664 VariableType::Binary => "binary",
665 VariableType::Integer { .. } => "integer",
666 VariableType::Spin => "spin",
667 VariableType::Categorical { .. } => "categorical",
668 VariableType::Continuous { .. } => "continuous",
669 };
670
671 *counts.entry(type_name.to_string()).or_insert(0) += 1;
672 }
673
674 counts
675 }
676}
677
678#[derive(Debug)]
680pub struct ModelSummary {
681 pub name: String,
682 pub num_variables: usize,
683 pub num_qubits: usize,
684 pub num_constraints: usize,
685 pub num_objectives: usize,
686 pub variable_types: HashMap<String, usize>,
687}
688
689impl fmt::Display for ModelSummary {
690 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691 writeln!(f, "Model: {}", self.name)?;
692 writeln!(f, " Variables: {}", self.num_variables)?;
693 writeln!(f, " Qubits: {}", self.num_qubits)?;
694 writeln!(f, " Constraints: {}", self.num_constraints)?;
695 writeln!(f, " Objectives: {}", self.num_objectives)?;
696 writeln!(f, " Variable types:")?;
697 for (var_type, count) in &self.variable_types {
698 writeln!(f, " {var_type}: {count}")?;
699 }
700 Ok(())
701 }
702}
703
704impl Expression {
706 #[must_use]
708 pub const fn constant(value: f64) -> Self {
709 Self::Constant(value)
710 }
711
712 #[must_use]
714 pub const fn sum(terms: Vec<Self>) -> Self {
715 Self::Sum(terms)
716 }
717
718 #[must_use]
720 pub fn add(self, other: Self) -> Self {
721 match (self, other) {
722 (Self::Sum(mut terms), Self::Sum(other_terms)) => {
723 terms.extend(other_terms);
724 Self::Sum(terms)
725 }
726 (Self::Sum(mut terms), other) => {
727 terms.push(other);
728 Self::Sum(terms)
729 }
730 (expr, Self::Sum(mut terms)) => {
731 terms.insert(0, expr);
732 Self::Sum(terms)
733 }
734 (expr1, expr2) => Self::Sum(vec![expr1, expr2]),
735 }
736 }
737
738 #[must_use]
740 pub fn scale(self, factor: f64) -> Self {
741 match self {
742 Self::Constant(value) => Self::Constant(value * factor),
743 Self::LinearCombination { weights, terms } => Self::LinearCombination {
744 weights: weights.into_iter().map(|w| w * factor).collect(),
745 terms,
746 },
747 expr => Self::LinearCombination {
748 weights: vec![factor],
749 terms: vec![expr],
750 },
751 }
752 }
753
754 #[must_use]
756 pub fn negate(self) -> Self {
757 Self::Negate(Box::new(self))
758 }
759}
760
761impl VariableVector {
763 #[must_use]
765 pub fn sum(&self) -> Expression {
766 Expression::Sum(
767 self.variables
768 .iter()
769 .map(|v| Expression::Variable(v.clone()))
770 .collect(),
771 )
772 }
773
774 #[must_use]
776 pub fn weighted_sum(&self, weights: &[f64]) -> Expression {
777 if weights.len() != self.variables.len() {
778 return Expression::Constant(0.0);
780 }
781
782 Expression::LinearCombination {
783 weights: weights.to_vec(),
784 terms: self
785 .variables
786 .iter()
787 .map(|v| Expression::Variable(v.clone()))
788 .collect(),
789 }
790 }
791
792 #[must_use]
794 pub fn get(&self, index: usize) -> Option<&Variable> {
795 self.variables.get(index)
796 }
797
798 #[must_use]
800 pub fn len(&self) -> usize {
801 self.variables.len()
802 }
803
804 #[must_use]
806 pub fn is_empty(&self) -> bool {
807 self.variables.is_empty()
808 }
809}
810
811impl Expression {
813 pub fn equals(self, other: impl Into<Self>) -> Constraint {
815 Constraint {
816 expression: BooleanExpression::Equal(self, other.into()),
817 name: None,
818 penalty_weight: None,
819 is_hard: true,
820 }
821 }
822
823 pub fn less_than(self, other: impl Into<Self>) -> Constraint {
825 Constraint {
826 expression: BooleanExpression::LessThan(self, other.into()),
827 name: None,
828 penalty_weight: None,
829 is_hard: true,
830 }
831 }
832
833 pub fn less_than_or_equal(self, other: impl Into<Self>) -> Constraint {
835 Constraint {
836 expression: BooleanExpression::LessThanOrEqual(self, other.into()),
837 name: None,
838 penalty_weight: None,
839 is_hard: true,
840 }
841 }
842
843 pub fn greater_than(self, other: impl Into<Self>) -> Constraint {
845 Constraint {
846 expression: BooleanExpression::GreaterThan(self, other.into()),
847 name: None,
848 penalty_weight: None,
849 is_hard: true,
850 }
851 }
852
853 pub fn greater_than_or_equal(self, other: impl Into<Self>) -> Constraint {
855 Constraint {
856 expression: BooleanExpression::GreaterThanOrEqual(self, other.into()),
857 name: None,
858 penalty_weight: None,
859 is_hard: true,
860 }
861 }
862}
863
864impl From<f64> for Expression {
866 fn from(value: f64) -> Self {
867 Self::Constant(value)
868 }
869}
870
871impl From<i32> for Expression {
872 fn from(value: i32) -> Self {
873 Self::Constant(f64::from(value))
874 }
875}
876
877impl From<Variable> for Expression {
878 fn from(var: Variable) -> Self {
879 Self::Variable(var)
880 }
881}
882
883pub mod patterns {
885 use super::{
886 BooleanExpression, Constraint, DslError, DslResult, Expression, OptimizationModel, Variable,
887 };
888
889 pub fn knapsack(
891 items: &[String],
892 values: &[f64],
893 weights: &[f64],
894 capacity: f64,
895 ) -> DslResult<OptimizationModel> {
896 let n = items.len();
897
898 if values.len() != n || weights.len() != n {
899 return Err(DslError::DimensionMismatch {
900 expected: n,
901 actual: values.len().min(weights.len()),
902 });
903 }
904
905 let mut model = OptimizationModel::new("Knapsack Problem");
906
907 let selection = model.add_binary_vector("select", n)?;
909
910 model.add_constraint(selection.weighted_sum(weights).less_than_or_equal(capacity))?;
912
913 model.maximize(selection.weighted_sum(values))?;
915
916 Ok(model)
917 }
918
919 pub fn graph_coloring(
921 vertices: &[String],
922 edges: &[(usize, usize)],
923 num_colors: usize,
924 ) -> DslResult<OptimizationModel> {
925 let n = vertices.len();
926
927 let mut model = OptimizationModel::new("Graph Coloring");
928
929 let mut x = Vec::new();
931 for v in 0..n {
932 let colors = model.add_binary_vector(format!("vertex_{v}_color"), num_colors)?;
933 x.push(colors);
934 }
935
936 for v in 0..n {
938 let vertex_vars: Vec<Variable> = (0..num_colors)
939 .filter_map(|c| x[v].get(c).cloned())
940 .collect();
941
942 model.add_constraint(Constraint {
943 expression: BooleanExpression::ExactlyOne(vertex_vars),
944 name: Some(format!("vertex_{v}_one_color")),
945 penalty_weight: None,
946 is_hard: true,
947 })?;
948 }
949
950 for &(u, v) in edges {
952 for c in 0..num_colors {
953 if let (Some(var_u), Some(var_v)) = (x[u].get(c), x[v].get(c)) {
954 model.add_constraint(Constraint {
956 expression: BooleanExpression::AtMostOne(vec![
957 var_u.clone(),
958 var_v.clone(),
959 ]),
960 name: Some(format!("edge_{u}_{v}_color_{c}")),
961 penalty_weight: None,
962 is_hard: true,
963 })?;
964 }
965 }
966 }
967
968 let mut color_used = Vec::new();
970 for c in 0..num_colors {
971 let color_var = model.add_binary(format!("color_{c}_used"))?;
972 color_used.push(color_var.clone());
973
974 for v in 0..n {
976 if let Some(var_vc) = x[v].get(c) {
977 model.add_constraint(
979 Expression::Variable(var_vc.clone())
980 .less_than_or_equal(Expression::Variable(color_var.clone())),
981 )?;
982 }
983 }
984 }
985
986 model.minimize(Expression::Sum(
987 color_used.into_iter().map(Expression::Variable).collect(),
988 ))?;
989
990 Ok(model)
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997
998 #[test]
999 fn test_binary_variable_creation() {
1000 let mut model = OptimizationModel::new("Test Model");
1001 let var = model
1002 .add_binary("x")
1003 .expect("Failed to add binary variable");
1004
1005 assert_eq!(var.id, "x");
1006 assert_eq!(var.qubit_indices.len(), 1);
1007 assert!(matches!(var.var_type, VariableType::Binary));
1008 }
1009
1010 #[test]
1011 fn test_binary_vector_creation() {
1012 let mut model = OptimizationModel::new("Test Model");
1013 let vec = model
1014 .add_binary_vector("x", 5)
1015 .expect("Failed to add binary vector");
1016
1017 assert_eq!(vec.name, "x");
1018 assert_eq!(vec.len(), 5);
1019 assert_eq!(vec.variables[0].id, "x[0]");
1020 assert_eq!(vec.variables[4].id, "x[4]");
1021 }
1022
1023 #[test]
1024 fn test_integer_variable_creation() {
1025 let mut model = OptimizationModel::new("Test Model");
1026 let var = model
1027 .add_integer("i", 0, 7)
1028 .expect("Failed to add integer variable");
1029
1030 assert_eq!(var.id, "i");
1031 assert_eq!(var.qubit_indices.len(), 3); assert!(matches!(
1033 var.var_type,
1034 VariableType::Integer { min: 0, max: 7 }
1035 ));
1036 }
1037
1038 #[test]
1039 fn test_expression_building() {
1040 let expr1 = Expression::constant(5.0);
1041 let expr2 = Expression::constant(3.0);
1042
1043 let sum = expr1.add(expr2);
1044 assert!(matches!(sum, Expression::Sum(_)));
1045
1046 let scaled = Expression::constant(2.0).scale(3.0);
1047 if let Expression::Constant(value) = scaled {
1048 assert_eq!(value, 6.0);
1049 } else {
1050 panic!("Expected constant expression");
1051 }
1052 }
1053
1054 #[test]
1055 fn test_knapsack_pattern() {
1056 let items = vec![
1057 "Item1".to_string(),
1058 "Item2".to_string(),
1059 "Item3".to_string(),
1060 ];
1061 let values = vec![10.0, 20.0, 15.0];
1062 let weights = vec![5.0, 10.0, 7.0];
1063 let capacity = 15.0;
1064
1065 let model = patterns::knapsack(&items, &values, &weights, capacity)
1066 .expect("Failed to create knapsack model");
1067
1068 assert_eq!(model.name, "Knapsack Problem");
1069 assert_eq!(model.summary().num_variables, 3);
1070 assert_eq!(model.summary().num_constraints, 1);
1071 assert_eq!(model.summary().num_objectives, 1);
1072 }
1073}