1use 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#[derive(Debug, Clone, Deserialize)]
59#[serde(try_from = "QuboInputData<W>")]
60#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>"))]
61pub struct QUBO<W = i64> {
62 num_vars: usize,
64 entries: Vec<(usize, usize, W)>,
67 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#[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 #[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 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 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 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 for (i, val) in linear.into_iter().enumerate() {
234 matrix[i][i] = val;
235 }
236
237 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 pub fn num_vars(&self) -> usize {
258 self.num_vars
259 }
260
261 pub fn num_quadratic_terms(&self) -> usize {
263 self.entries.iter().filter(|(i, j, _)| i != j).count()
264 }
265
266 pub fn entries(&self) -> &[(usize, usize, W)] {
268 &self.entries
269 }
270
271 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 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}