Skip to main content

problemreductions/models/algebraic/
qubo.rs

1//! QUBO (Quadratic Unconstrained Binary Optimization) problem implementation.
2//!
3//! QUBO minimizes a quadratic function over binary variables.
4
5use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension};
6use crate::traits::Problem;
7use crate::types::{Min, WeightElement};
8use num_traits::Zero;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "QUBO",
14        display_name: "QUBO",
15        aliases: &[],
16        dimensions: &[VariantDimension::new("weight", "i64", &["i64", "f64"])],
17        category: crate::registry::ProblemCategory::Algebraic,
18        module_path: module_path!(),
19        description: "Minimize quadratic unconstrained binary objective",
20        fields: QuboCreateSpec::<i64>::FIELDS,
21    }
22}
23
24/// The QUBO (Quadratic Unconstrained Binary Optimization) problem.
25///
26/// Given n binary variables x_i ∈ {0, 1} and a matrix Q,
27/// minimize the quadratic form:
28///
29/// f(x) = Σ_i Σ_j Q_ij * x_i * x_j = x^T Q x
30///
31/// The matrix Q is typically upper triangular, with diagonal elements
32/// representing linear terms and off-diagonal elements representing
33/// quadratic interactions.
34///
35/// `QUBO<i64>` is the default exact-integer variant. `QUBO<f64>` stores
36/// finite floating-point coefficients. An explicit variant reduction converts
37/// exactly representable integer coefficients to `f64`.
38///
39/// # Example
40///
41/// ```
42/// use problemreductions::models::algebraic::QUBO;
43/// use problemreductions::{Problem, BruteForce};
44///
45/// // Q matrix: minimize x0 - 2*x1 + x0*x1
46/// // Q = [[1, 1], [0, -2]]
47/// let problem = QUBO::from_matrix(vec![
48///     vec![1, 1],
49///     vec![0, -2],
50/// ]).unwrap();
51///
52/// let solver = BruteForce::new();
53/// let solutions = solver.find_all_witnesses(&problem).unwrap();
54///
55/// // Optimal is x = [0, 1] with value -2
56/// assert!(solutions.contains(&vec![false, true]));
57/// ```
58#[derive(Debug, Clone, Deserialize)]
59#[serde(try_from = "QuboInputData<W>")]
60#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>"))]
61pub struct QUBO<W = i64> {
62    /// Number of variables.
63    num_vars: usize,
64    /// Nonzero upper-triangular coefficients `(i, j, Q[i][j])` with `i <= j`,
65    /// sorted by `(i, j)`; absent entries are zero.
66    entries: Vec<(usize, usize, W)>,
67    /// Zero coefficient returned by [`QUBO::get`] for absent entries.
68    zero: W,
69}
70
71#[derive(Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73struct QuboData<W> {
74    num_vars: usize,
75    entries: Vec<(usize, usize, W)>,
76}
77
78// Keep parse-error format guidance separate from constructor validation errors.
79#[derive(Deserialize)]
80#[serde(transparent)]
81struct QuboInputData<W> {
82    #[serde(
83        deserialize_with = "deserialize_qubo_data",
84        bound(deserialize = "W: Deserialize<'de>")
85    )]
86    data: QuboData<W>,
87}
88
89fn deserialize_qubo_data<'de, W: Deserialize<'de>, D: serde::Deserializer<'de>>(
90    deserializer: D,
91) -> Result<QuboData<W>, D::Error> {
92    QuboData::deserialize(deserializer).map_err(|error| {
93        serde::de::Error::custom(format!(
94            "{error}; expected QUBO format: num_vars and sparse entries [row, col, value] with row <= col"
95        ))
96    })
97}
98
99impl<W: WeightElement> TryFrom<QuboInputData<W>> for QUBO<W> {
100    type Error = ConstructionError;
101
102    fn try_from(input: QuboInputData<W>) -> Result<Self, Self::Error> {
103        Self::try_from(input.data)
104    }
105}
106
107impl<W: WeightElement + Serialize> Serialize for QUBO<W> {
108    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
109        QuboData {
110            num_vars: self.num_vars,
111            entries: self
112                .entries
113                .iter()
114                .map(|(i, j, value)| (*i, *j, value))
115                .collect(),
116        }
117        .serialize(serializer)
118    }
119}
120
121impl<W: WeightElement> TryFrom<QuboData<W>> for QUBO<W> {
122    type Error = ConstructionError;
123
124    fn try_from(data: QuboData<W>) -> Result<Self, Self::Error> {
125        Self::from_entries(data.num_vars, data.entries)
126    }
127}
128
129#[derive(Debug, Deserialize, crate::CreateSpec)]
130struct QuboCreateSpec<W> {
131    /// Q matrix; the number of variables is its row count.
132    #[create(codec = "semicolon-separated")]
133    matrix: Vec<Vec<W>>,
134}
135
136impl<W: WeightElement> TryFrom<QuboCreateSpec<W>> for QUBO<W> {
137    type Error = ConstructionError;
138
139    fn try_from(spec: QuboCreateSpec<W>) -> Result<Self, Self::Error> {
140        Self::from_matrix(spec.matrix)
141    }
142}
143
144impl<W: WeightElement> QUBO<W> {
145    /// Create a QUBO problem from a full matrix.
146    ///
147    /// The matrix should be square. Only the upper triangular part
148    /// (including diagonal) is used.
149    pub fn from_matrix(matrix: Vec<Vec<W>>) -> Result<Self, ConstructionError> {
150        let num_vars = matrix.len();
151        if let Some((row, actual)) = matrix
152            .iter()
153            .enumerate()
154            .find_map(|(row, values)| (values.len() != num_vars).then_some((row, values.len())))
155        {
156            return Err(ConstructionError::Conversion(format!(
157                "QUBO matrix row {row} has length {actual}, expected {num_vars}"
158            )));
159        }
160        for (row, values) in matrix.iter().enumerate() {
161            for (column, value) in values.iter().enumerate() {
162                value.validate_element(&format!("QUBO coefficient at ({row}, {column})"))?;
163            }
164        }
165        let entries = matrix
166            .into_iter()
167            .enumerate()
168            .flat_map(|(row, values)| {
169                values
170                    .into_iter()
171                    .enumerate()
172                    .skip(row)
173                    .map(move |(column, value)| (row, column, value))
174            })
175            .collect();
176        Self::from_entries(num_vars, entries)
177    }
178
179    /// Create a QUBO from sparse coefficients `(i, j, Q[i][j])`.
180    ///
181    /// Indices must be in `0..num_vars` with `i <= j`, and each pair may
182    /// appear at most once. Zero coefficients are dropped.
183    pub fn from_entries(
184        num_vars: usize,
185        mut entries: Vec<(usize, usize, W)>,
186    ) -> Result<Self, ConstructionError> {
187        for (row, column, value) in &entries {
188            if *row >= num_vars || *column >= num_vars {
189                return Err(ConstructionError::Conversion(format!(
190                    "QUBO index ({row}, {column}) is outside 0..{num_vars}"
191                )));
192            }
193            if row > column {
194                return Err(ConstructionError::Conversion(format!(
195                    "QUBO index ({row}, {column}) is below the diagonal; use ({column}, {row}) instead"
196                )));
197            }
198            value.validate_element(&format!("QUBO coefficient at ({row}, {column})"))?;
199        }
200        entries.sort_by_key(|&(row, column, _)| (row, column));
201        if let Some(pair) = entries
202            .windows(2)
203            .find(|pair| (pair[0].0, pair[0].1) == (pair[1].0, pair[1].1))
204        {
205            return Err(ConstructionError::Conversion(format!(
206                "duplicate QUBO index ({}, {})",
207                pair[0].0, pair[0].1
208            )));
209        }
210        Ok(Self {
211            num_vars,
212            entries: entries
213                .into_iter()
214                .filter(|(_, _, value)| !value.to_sum().is_zero())
215                .collect(),
216            zero: W::default(),
217        })
218    }
219
220    /// Create a QUBO from linear and quadratic terms.
221    ///
222    /// # Arguments
223    /// * `linear` - Linear coefficients (diagonal of Q)
224    /// * `quadratic` - Quadratic coefficients as ((i, j), value) for i < j
225    pub fn new(
226        linear: Vec<W>,
227        quadratic: Vec<((usize, usize), W)>,
228    ) -> Result<Self, ConstructionError> {
229        let num_vars = linear.len();
230        let mut matrix = vec![vec![W::default(); num_vars]; num_vars];
231
232        // Set diagonal (linear terms)
233        for (i, val) in linear.into_iter().enumerate() {
234            matrix[i][i] = val;
235        }
236
237        // Set off-diagonal (quadratic terms)
238        for ((i, j), val) in quadratic {
239            if i >= num_vars || j >= num_vars {
240                return Err(ConstructionError::Conversion(format!(
241                    "QUBO quadratic index ({i}, {j}) is outside 0..{num_vars}"
242                )));
243            }
244            if i < j {
245                matrix[i][j] = val;
246            } else {
247                matrix[j][i] = val;
248            }
249        }
250
251        Self::from_matrix(matrix)
252    }
253}
254
255impl<W: Clone> QUBO<W> {
256    /// Get the number of variables.
257    pub fn num_vars(&self) -> usize {
258        self.num_vars
259    }
260
261    /// Number of nonzero off-diagonal coefficients.
262    pub fn num_quadratic_terms(&self) -> usize {
263        self.entries.iter().filter(|(i, j, _)| i != j).count()
264    }
265
266    /// Nonzero upper-triangular coefficients `(i, j, Q[i][j])`, sorted by `(i, j)`.
267    pub fn entries(&self) -> &[(usize, usize, W)] {
268        &self.entries
269    }
270
271    /// Dense upper-triangular copy of Q. Allocates `num_vars^2` elements.
272    pub fn matrix(&self) -> Vec<Vec<W>> {
273        let mut matrix = vec![vec![self.zero.clone(); self.num_vars]; self.num_vars];
274        for (i, j, value) in &self.entries {
275            matrix[*i][*j] = value.clone();
276        }
277        matrix
278    }
279
280    /// Get a specific matrix element `Q[i][j]`; entries below the diagonal are zero.
281    pub fn get(&self, i: usize, j: usize) -> Option<&W> {
282        if i >= self.num_vars || j >= self.num_vars {
283            return None;
284        }
285        Some(
286            self.entries
287                .binary_search_by_key(&(i, j), |&(row, column, _)| (row, column))
288                .map_or(&self.zero, |index| &self.entries[index].2),
289        )
290    }
291}
292
293impl<W> Problem for QUBO<W>
294where
295    W: WeightElement + crate::variant::VariantParam,
296{
297    const NAME: &'static str = "QUBO";
298    type Solution = Vec<bool>;
299    type Value = Min<W::Sum>;
300
301    crate::problem_parameters![
302        ("num_vars", num_vars),
303        ("num_quadratic_terms", num_quadratic_terms),
304    ];
305
306    fn evaluate(
307        &self,
308        solution: &Self::Solution,
309    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
310        if solution.len() != self.num_vars {
311            return Err(crate::traits::EvaluationError::InvalidConfiguration(
312                format!(
313                    "solution has {} variables, expected {}",
314                    solution.len(),
315                    self.num_vars
316                ),
317            ));
318        }
319        let mut value = W::Sum::zero();
320        for (i, j, coefficient) in &self.entries {
321            if solution[*i] && solution[*j] {
322                value = W::checked_add_to_sum(
323                    value,
324                    coefficient.to_sum(),
325                    "summing selected QUBO coefficients",
326                )?;
327            }
328        }
329
330        Ok(Min(Some(value)))
331    }
332
333    fn variant() -> Vec<(&'static str, &'static str)> {
334        crate::variant_params![W]
335    }
336}
337
338impl<W> crate::solvers::BruteForceProblem for QUBO<W>
339where
340    W: WeightElement + crate::variant::VariantParam,
341{
342    fn dimensions(&self) -> Vec<usize> {
343        vec![2; self.num_vars]
344    }
345}
346
347crate::declare_variants! {
348    default QUBO<i64> => "2^num_vars" create QuboCreateSpec<i64>,
349    QUBO<f64> => "2^num_vars" create QuboCreateSpec<f64>,
350}
351
352crate::register_brute_force! {
353    QUBO<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
354    QUBO<f64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
355}
356
357#[cfg(feature = "example-db")]
358pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
359    vec![crate::example_db::specs::ModelExampleSpec {
360        id: "qubo",
361        instance: Box::new(
362            QUBO::from_matrix(vec![vec![-1, 2, 0], vec![0, -1, 2], vec![0, 0, -1]]).unwrap(),
363        ),
364        optimal_config: serde_json::json!(vec![true, false, true]),
365        optimal_value: serde_json::json!(-2),
366    }]
367}
368
369#[cfg(test)]
370#[path = "../../unit_tests/models/algebraic/qubo.rs"]
371mod tests;
372
373crate::decision_problem_meta!(QUBO<i64>, "DecisionQUBO");
374crate::register_decision_variant!(
375    QUBO<i64>, "DecisionQUBO", "2^num_vars", &[],
376    "Does a feasible solution have objective value <= the bound?",
377    category: crate::registry::ProblemCategory::Algebraic,
378    dims: [VariantDimension::new("weight", "i64", &["i64"])],
379    fields: [
380        crate::registry::FieldInfo { name: "matrix", type_name: "Vec<Vec<W>>", description: "Q matrix; the number of variables is its row count." },
381        crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Accept objective values <= this bound" },
382    ],
383    decode: |_, indices: Vec<usize>| crate::config::config_to_bits(&indices)
384);
385
386#[cfg(feature = "example-db")]
387pub(crate) fn decision_canonical_rule_example_specs(
388) -> Vec<crate::example_db::specs::RuleExampleSpec> {
389    vec![crate::example_db::specs::RuleExampleSpec {
390        id: "decision_qubo_to_qubo",
391        build: || {
392            let source = crate::models::decision::Decision::new(
393                QUBO::from_matrix(vec![vec![-1, 2, 0], vec![0, -1, 2], vec![0, 0, -1]]).unwrap(),
394                -2,
395            );
396            let witness = serde_json::json!(vec![true, false, true]);
397            crate::example_db::specs::rule_example_with_witness::<_, QUBO<i64>>(
398                source,
399                crate::export::SolutionPair {
400                    source_config: witness.clone(),
401                    target_config: witness,
402                },
403            )
404        },
405    }]
406}