quantrs2_anneal/
dsl.rs

1//! Domain-Specific Language (DSL) for Optimization Problems
2//!
3//! This module provides a high-level DSL for expressing optimization problems that can be
4//! automatically compiled to Ising/QUBO formulations. The DSL supports various variable types,
5//! constraints, and objective functions with a natural syntax.
6
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9use thiserror::Error;
10
11use crate::ising::{IsingError, IsingModel, QuboModel};
12
13/// Errors that can occur in DSL operations
14#[derive(Error, Debug)]
15pub enum DslError {
16    /// Variable not found
17    #[error("Variable not found: {0}")]
18    VariableNotFound(String),
19
20    /// Invalid constraint
21    #[error("Invalid constraint: {0}")]
22    InvalidConstraint(String),
23
24    /// Invalid objective
25    #[error("Invalid objective: {0}")]
26    InvalidObjective(String),
27
28    /// Compilation error
29    #[error("Compilation error: {0}")]
30    CompilationError(String),
31
32    /// Type mismatch
33    #[error("Type mismatch: {0}")]
34    TypeMismatch(String),
35
36    /// Ising model error
37    #[error("Ising error: {0}")]
38    IsingError(#[from] IsingError),
39
40    /// Dimension mismatch
41    #[error("Dimension mismatch: expected {expected}, got {actual}")]
42    DimensionMismatch { expected: usize, actual: usize },
43
44    /// Invalid range
45    #[error("Invalid range: {0}")]
46    InvalidRange(String),
47}
48
49/// Result type for DSL operations
50pub type DslResult<T> = Result<T, DslError>;
51
52/// Variable types in the DSL
53#[derive(Debug, Clone, PartialEq)]
54pub enum VariableType {
55    /// Binary variable (0 or 1)
56    Binary,
57
58    /// Integer variable with bounds
59    Integer { min: i32, max: i32 },
60
61    /// Spin variable (-1 or +1)
62    Spin,
63
64    /// Categorical variable (one-hot encoded)
65    Categorical { categories: Vec<String> },
66
67    /// Continuous variable (discretized)
68    Continuous { min: f64, max: f64, steps: usize },
69}
70
71/// Variable representation in the DSL
72#[derive(Debug, Clone)]
73pub struct Variable {
74    /// Unique identifier
75    pub id: String,
76
77    /// Variable type
78    pub var_type: VariableType,
79
80    /// Qubit indices for this variable
81    pub qubit_indices: Vec<usize>,
82
83    /// Description
84    pub description: Option<String>,
85}
86
87/// Variable vector for array operations
88#[derive(Debug, Clone)]
89pub struct VariableVector {
90    /// Variables in the vector
91    pub variables: Vec<Variable>,
92
93    /// Vector name
94    pub name: String,
95}
96
97/// Expression tree for building complex expressions
98#[derive(Debug, Clone)]
99pub enum Expression {
100    /// Constant value
101    Constant(f64),
102
103    /// Variable reference
104    Variable(Variable),
105
106    /// Sum of expressions
107    Sum(Vec<Self>),
108
109    /// Product of expressions
110    Product(Box<Self>, Box<Self>),
111
112    /// Linear combination
113    LinearCombination { weights: Vec<f64>, terms: Vec<Self> },
114
115    /// Quadratic term
116    Quadratic {
117        var1: Variable,
118        var2: Variable,
119        coefficient: f64,
120    },
121
122    /// Power of expression
123    Power(Box<Self>, i32),
124
125    /// Negation
126    Negate(Box<Self>),
127
128    /// Absolute value
129    Abs(Box<Self>),
130
131    /// Conditional expression
132    Conditional {
133        condition: Box<BooleanExpression>,
134        if_true: Box<Self>,
135        if_false: Box<Self>,
136    },
137}
138
139/// Boolean expressions for constraints
140#[derive(Debug, Clone)]
141pub enum BooleanExpression {
142    /// Always true
143    True,
144
145    /// Always false
146    False,
147
148    /// Equality comparison
149    Equal(Expression, Expression),
150
151    /// Less than comparison
152    LessThan(Expression, Expression),
153
154    /// Less than or equal comparison
155    LessThanOrEqual(Expression, Expression),
156
157    /// Greater than comparison
158    GreaterThan(Expression, Expression),
159
160    /// Greater than or equal comparison
161    GreaterThanOrEqual(Expression, Expression),
162
163    /// Logical AND
164    And(Box<Self>, Box<Self>),
165
166    /// Logical OR
167    Or(Box<Self>, Box<Self>),
168
169    /// Logical NOT
170    Not(Box<Self>),
171
172    /// Logical XOR
173    Xor(Box<Self>, Box<Self>),
174
175    /// Implication (if-then)
176    Implies(Box<Self>, Box<Self>),
177
178    /// All different constraint
179    AllDifferent(Vec<Variable>),
180
181    /// At most one constraint
182    AtMostOne(Vec<Variable>),
183
184    /// Exactly one constraint
185    ExactlyOne(Vec<Variable>),
186}
187
188/// Constraint in the optimization model
189#[derive(Debug, Clone)]
190pub struct Constraint {
191    /// Constraint expression
192    pub expression: BooleanExpression,
193
194    /// Constraint name
195    pub name: Option<String>,
196
197    /// Penalty weight (for soft constraints)
198    pub penalty_weight: Option<f64>,
199
200    /// Whether this is a hard constraint
201    pub is_hard: bool,
202}
203
204/// Objective function direction
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum ObjectiveDirection {
207    Minimize,
208    Maximize,
209}
210
211/// Objective function
212#[derive(Debug, Clone)]
213pub struct Objective {
214    /// Expression to optimize
215    pub expression: Expression,
216
217    /// Direction (minimize or maximize)
218    pub direction: ObjectiveDirection,
219
220    /// Objective name
221    pub name: Option<String>,
222}
223
224/// Optimization model builder
225pub struct OptimizationModel {
226    /// Model name
227    pub name: String,
228
229    /// Variables in the model
230    variables: HashMap<String, Variable>,
231
232    /// Variable vectors
233    variable_vectors: HashMap<String, VariableVector>,
234
235    /// Constraints
236    constraints: Vec<Constraint>,
237
238    /// Objectives (for multi-objective optimization)
239    objectives: Vec<Objective>,
240
241    /// Next available qubit index
242    next_qubit: usize,
243
244    /// Model metadata
245    metadata: HashMap<String, String>,
246}
247
248impl OptimizationModel {
249    /// Create a new optimization model
250    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    /// Add metadata to the model
263    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    /// Add a binary variable
268    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    /// Add a binary variable vector
291    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    /// Add an integer variable
315    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        // Calculate number of bits needed
336        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    /// Add a spin variable
359    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    /// Add a constraint to the model
382    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    /// Add an objective function (minimize)
389    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    /// Add an objective function (maximize)
401    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    /// Compile the model to QUBO formulation
413    pub fn compile_to_qubo(&self) -> DslResult<QuboModel> {
414        // Create QUBO model with the right size
415        let mut model = QuboModel::new(self.next_qubit);
416
417        // Process objectives and add to QUBO
418        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        // Process constraints and add as penalties
428        for constraint in &self.constraints {
429            // Hard constraints use a large penalty, soft constraints use specified weight
430            let penalty_weight = if constraint.is_hard {
431                1000.0 // Large penalty for hard constraints
432            } 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    /// Compile the model to Ising formulation
443    pub fn compile_to_ising(&self) -> DslResult<IsingModel> {
444        let qubo = self.compile_to_qubo()?;
445
446        // Convert QUBO to Ising using the standard transformation
447        // QUBO: x_i ∈ {0,1}, Ising: s_i ∈ {-1,+1}
448        // Transformation: x_i = (1 + s_i) / 2
449        let mut ising = IsingModel::new(self.next_qubit);
450
451        // Transform QUBO coefficients to Ising coefficients
452        // For each QUBO term Q_ij x_i x_j, we get:
453        // Q_ij * (1+s_i)/2 * (1+s_j)/2 = Q_ij/4 * (1 + s_i + s_j + s_i*s_j)
454
455        let mut offset = 0.0;
456
457        // Process linear terms (diagonal of QUBO)
458        for i in 0..self.next_qubit {
459            let q_val = qubo.get_linear(i)?;
460
461            if q_val.abs() > 1e-10 {
462                // Diagonal term: Q_ii x_i = Q_ii/4 * (1 + 2*s_i + s_i^2)
463                // = Q_ii/4 * (2 + 2*s_i) since s_i^2 = 1
464                let h_i = q_val / 2.0; // Linear term coefficient
465                let current_bias = ising.get_bias(i)?;
466                ising.set_bias(i, current_bias + h_i)?;
467                offset += q_val / 4.0; // Constant offset
468            }
469        }
470
471        // Process quadratic terms (off-diagonal of QUBO)
472        for (i, j, q_val) in qubo.quadratic_terms() {
473            if q_val.abs() > 1e-10 {
474                // Off-diagonal term: Q_ij x_i x_j
475                // = Q_ij/4 * (1 + s_i + s_j + s_i*s_j)
476                let j_ij = q_val / 4.0; // Coupling coefficient
477                ising.set_coupling(i, j, j_ij)?;
478
479                // Add linear field contributions
480                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        // Store the constant offset in metadata if needed
491        // (The offset doesn't affect optimization but is needed for absolute energy)
492
493        Ok(ising)
494    }
495
496    /// Helper: Add an expression to QUBO model
497    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                // Constants don't affect optimization, only the absolute energy value
506                Ok(())
507            }
508            Expression::Variable(var) => {
509                // Linear term
510                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                // Recursively add all terms
517                for term in terms {
518                    self.add_expression_to_qubo(model, term, coefficient)?;
519                }
520                Ok(())
521            }
522            Expression::Product(e1, e2) => {
523                // Handle quadratic terms
524                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                            // Same variable - add to linear term
532                            model.add_linear(q1, coefficient)?;
533                        } else {
534                            // Different variables - add to quadratic term
535                            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                // Direct quadratic term
548                if let (Some(&q1), Some(&q2)) =
549                    (var1.qubit_indices.first(), var2.qubit_indices.first())
550                {
551                    if q1 == q2 {
552                        // Same variable - add to linear term
553                        model.add_linear(q1, coefficient * coef)?;
554                    } else {
555                        // Different variables - add to quadratic term
556                        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                // Add weighted sum of terms
564                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                // For other expression types, return an error or handle appropriately
575                Err(DslError::CompilationError(
576                    "Unsupported expression type in QUBO compilation".to_string(),
577                ))
578            }
579        }
580    }
581
582    /// Helper: Add a constraint to QUBO as a penalty term
583    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                // Unsatisfiable constraint
593                Err(DslError::InvalidConstraint(
594                    "Unsatisfiable constraint (False)".to_string(),
595                ))
596            }
597            BooleanExpression::Equal(e1, e2) => {
598                // (e1 - e2)^2 penalty
599                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                // Sum of variables should equal 1: (sum - 1)^2
606                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                // Penalize pairs: sum_{i<j} x_i * x_j
618                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                // Both constraints must be satisfied
632                self.add_constraint_to_qubo(model, b1, penalty)?;
633                self.add_constraint_to_qubo(model, b2, penalty)?;
634                Ok(())
635            }
636            _ => {
637                // For other constraint types, return error or implement as needed
638                Err(DslError::CompilationError(
639                    "Unsupported constraint type in QUBO compilation".to_string(),
640                ))
641            }
642        }
643    }
644
645    /// Get model summary
646    #[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    /// Count variables by type
659    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/// Model summary information
679#[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
704/// Expression builder helper methods
705impl Expression {
706    /// Create a constant expression
707    #[must_use]
708    pub const fn constant(value: f64) -> Self {
709        Self::Constant(value)
710    }
711
712    /// Create a sum expression
713    #[must_use]
714    pub const fn sum(terms: Vec<Self>) -> Self {
715        Self::Sum(terms)
716    }
717
718    /// Add two expressions
719    #[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    /// Multiply expression by a constant
739    #[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    /// Negate expression
755    #[must_use]
756    pub fn negate(self) -> Self {
757        Self::Negate(Box::new(self))
758    }
759}
760
761/// Variable vector helper methods
762impl VariableVector {
763    /// Sum all variables in the vector
764    #[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    /// Weighted sum of variables
775    #[must_use]
776    pub fn weighted_sum(&self, weights: &[f64]) -> Expression {
777        if weights.len() != self.variables.len() {
778            // Return zero expression if dimensions don't match
779            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    /// Get a specific variable by index
793    #[must_use]
794    pub fn get(&self, index: usize) -> Option<&Variable> {
795        self.variables.get(index)
796    }
797
798    /// Number of variables in the vector
799    #[must_use]
800    pub fn len(&self) -> usize {
801        self.variables.len()
802    }
803
804    /// Check if vector is empty
805    #[must_use]
806    pub fn is_empty(&self) -> bool {
807        self.variables.is_empty()
808    }
809}
810
811/// Constraint builder methods for expressions
812impl Expression {
813    /// Create equality constraint
814    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    /// Create less-than constraint
824    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    /// Create less-than-or-equal constraint
834    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    /// Create greater-than constraint
844    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    /// Create greater-than-or-equal constraint
854    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
864/// Implement `Into<Expression>` for numeric types
865impl 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
883/// Common optimization patterns
884pub mod patterns {
885    use super::{
886        BooleanExpression, Constraint, DslError, DslResult, Expression, OptimizationModel, Variable,
887    };
888
889    /// Create a knapsack problem
890    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        // Binary variables for item selection
908        let selection = model.add_binary_vector("select", n)?;
909
910        // Constraint: total weight <= capacity
911        model.add_constraint(selection.weighted_sum(weights).less_than_or_equal(capacity))?;
912
913        // Objective: maximize total value
914        model.maximize(selection.weighted_sum(values))?;
915
916        Ok(model)
917    }
918
919    /// Create a graph coloring problem
920    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        // Binary variables x[v][c] = 1 if vertex v has color c
930        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        // Constraint: each vertex has exactly one color
937        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        // Constraint: adjacent vertices have different colors
951        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                    // Both vertices cannot have the same color
955                    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        // Objective: minimize number of colors used (optional)
969        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            // If any vertex uses color c, then color_used[c] = 1
975            for v in 0..n {
976                if let Some(var_vc) = x[v].get(c) {
977                    // This is a simplified constraint - full implementation would be more complex
978                    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); // 2^3 = 8 > 7
1032        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}