1use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension};
6use crate::topology::{Graph, SimpleGraph};
7use crate::traits::Problem;
8use crate::types::{Min, WeightElement};
9use num_traits::{One as _, Zero as _};
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13 ProblemSchemaEntry {
14 name: "SpinGlass",
15 display_name: "Spin Glass",
16 aliases: &[],
17 dimensions: &[
18 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
19 VariantDimension::new("weight", "i64", &["i64", "f64"]),
20 ],
21 category: crate::registry::ProblemCategory::Graph,
22 module_path: module_path!(),
23 description: "Minimize Ising Hamiltonian on a graph",
24 fields: SpinGlassI64CreateSpec::FIELDS,
25 }
26}
27
28#[derive(Debug, Clone, Serialize)]
65pub struct SpinGlass<G, W> {
66 graph: G,
68 couplings: Vec<W>,
70 fields: Vec<W>,
72}
73
74#[derive(Deserialize)]
75struct SpinGlassData<G, W> {
76 graph: G,
77 couplings: Vec<W>,
78 fields: Vec<W>,
79}
80
81impl<'de, G, W> Deserialize<'de> for SpinGlass<G, W>
82where
83 G: Graph + Deserialize<'de>,
84 W: WeightElement + Deserialize<'de>,
85{
86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87 where
88 D: serde::Deserializer<'de>,
89 {
90 let data = SpinGlassData::deserialize(deserializer)?;
91 Self::from_graph(data.graph, data.couplings, data.fields).map_err(serde::de::Error::custom)
92 }
93}
94
95macro_rules! spin_glass_create_spec {
96 ($name:ident, $weight:ty, $one:expr, $zero:expr) => {
97 #[derive(Debug, Deserialize, crate::CreateSpec)]
98 struct $name {
99 #[create(codec = "edge-list")]
101 graph: Vec<(usize, usize)>,
102 num_vertices: Option<usize>,
104 #[create(codec = "comma-separated")]
106 couplings: Option<Vec<$weight>>,
107 #[create(codec = "comma-separated")]
109 fields: Option<Vec<$weight>>,
110 }
111
112 impl TryFrom<$name> for SpinGlass<SimpleGraph, $weight> {
113 type Error = ConstructionError;
114
115 fn try_from(spec: $name) -> Result<Self, Self::Error> {
116 if spec.graph.is_empty() && spec.num_vertices.is_none() {
117 return Err(ConstructionError::Conversion(
118 "num_vertices is required for an empty graph".into(),
119 ));
120 }
121 for (index, &(u, v)) in spec.graph.iter().enumerate() {
122 if u == v {
123 return Err(ConstructionError::Conversion(format!(
124 "graph edge {index} is a self-loop at vertex {u}"
125 )));
126 }
127 }
128 let inferred = spec
129 .graph
130 .iter()
131 .flat_map(|&(u, v)| [u, v])
132 .max()
133 .map(|vertex| {
134 vertex.checked_add(1).ok_or_else(|| {
135 ConstructionError::IntegerOverflow(
136 "inferring the SpinGlass vertex count".into(),
137 )
138 })
139 })
140 .transpose()?
141 .unwrap_or(0);
142 let num_vertices = spec.num_vertices.unwrap_or(inferred);
143 if num_vertices < inferred {
144 return Err(ConstructionError::Conversion(format!(
145 "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
146 )));
147 }
148 let couplings = spec
149 .couplings
150 .unwrap_or_else(|| vec![$one; spec.graph.len()]);
151 let fields = spec.fields.unwrap_or_else(|| vec![$zero; num_vertices]);
152 SpinGlass::from_graph(
153 SimpleGraph::new(num_vertices, spec.graph),
154 couplings,
155 fields,
156 )
157 }
158 }
159 };
160}
161
162spin_glass_create_spec!(SpinGlassI64CreateSpec, i64, 1_i64, 0_i64);
163spin_glass_create_spec!(SpinGlassF64CreateSpec, f64, 1.0_f64, 0.0_f64);
164
165impl<W: WeightElement> SpinGlass<SimpleGraph, W> {
166 pub fn new(
173 num_spins: usize,
174 interactions: Vec<((usize, usize), W)>,
175 fields: Vec<W>,
176 ) -> Result<Self, ConstructionError> {
177 for (index, &((u, v), _)) in interactions.iter().enumerate() {
178 if u >= num_spins || v >= num_spins {
179 return Err(ConstructionError::Conversion(format!(
180 "interaction {index} endpoint exceeds num_spins"
181 )));
182 }
183 }
184 let edges = interactions.iter().map(|((u, v), _)| (*u, *v)).collect();
185 let couplings = interactions
186 .iter()
187 .map(|(_, coupling)| coupling.clone())
188 .collect();
189 let graph = SimpleGraph::new(num_spins, edges);
190 Self::from_graph(graph, couplings, fields)
191 }
192
193 pub fn without_fields(
195 num_spins: usize,
196 interactions: Vec<((usize, usize), W)>,
197 ) -> Result<Self, ConstructionError>
198 where
199 W: num_traits::Zero,
200 {
201 let fields = vec![W::zero(); num_spins];
202 Self::new(num_spins, interactions, fields)
203 }
204}
205
206impl<G: Graph, W: WeightElement> SpinGlass<G, W> {
207 pub fn from_graph(
214 graph: G,
215 couplings: Vec<W>,
216 fields: Vec<W>,
217 ) -> Result<Self, ConstructionError> {
218 if couplings.len() != graph.num_edges() {
219 return Err(crate::registry::ConstructionError::length_mismatch(
220 "couplings",
221 couplings.len(),
222 graph.num_edges(),
223 ));
224 }
225 if fields.len() != graph.num_vertices() {
226 return Err(crate::registry::ConstructionError::length_mismatch(
227 "fields",
228 fields.len(),
229 graph.num_vertices(),
230 ));
231 }
232 for (index, coupling) in couplings.iter().enumerate() {
233 coupling.validate_element(&format!("coupling at index {index}"))?;
234 }
235 for (index, field) in fields.iter().enumerate() {
236 field.validate_element(&format!("field at index {index}"))?;
237 }
238 Ok(Self {
239 graph,
240 couplings,
241 fields,
242 })
243 }
244
245 pub fn from_graph_without_fields(graph: G, couplings: Vec<W>) -> Result<Self, ConstructionError>
247 where
248 W: num_traits::Zero,
249 {
250 let fields = vec![W::zero(); graph.num_vertices()];
251 Self::from_graph(graph, couplings, fields)
252 }
253}
254
255impl<G: Graph, W: Clone + Default> SpinGlass<G, W> {
256 pub fn graph(&self) -> &G {
258 &self.graph
259 }
260
261 pub fn num_spins(&self) -> usize {
263 self.graph.num_vertices()
264 }
265
266 pub fn num_interactions(&self) -> usize {
268 self.graph.num_edges()
269 }
270
271 pub fn interactions(&self) -> Vec<((usize, usize), W)> {
275 self.graph
276 .edges()
277 .into_iter()
278 .zip(self.couplings.iter())
279 .map(|((i, j), w)| ((i, j), w.clone()))
280 .collect()
281 }
282
283 pub fn couplings(&self) -> &[W] {
285 &self.couplings
286 }
287
288 pub fn fields(&self) -> &[W] {
290 &self.fields
291 }
292
293 pub fn config_to_spins(config: &[usize]) -> Result<Vec<i8>, crate::traits::EvaluationError> {
295 config
296 .iter()
297 .map(|&value| match value {
298 0 => Ok(-1),
299 1 => Ok(1),
300 _ => Err(crate::traits::EvaluationError::InvalidConfiguration(
301 format!("binary spin configuration value must be 0 or 1, got {value}"),
302 )),
303 })
304 .collect()
305 }
306}
307
308impl<G, W> SpinGlass<G, W>
309where
310 G: Graph,
311 W: WeightElement,
312{
313 pub fn compute_energy(&self, spins: &[i8]) -> Result<W::Sum, crate::traits::EvaluationError> {
315 if spins.len() != self.graph.num_vertices() {
316 return Err(crate::traits::EvaluationError::InvalidConfiguration(
317 format!(
318 "expected {} spin values, got {}",
319 self.graph.num_vertices(),
320 spins.len()
321 ),
322 ));
323 }
324 let spin_sign = |spin| match spin {
325 1 => Ok(W::Sum::one()),
326 -1 => Ok(W::Sum::zero() - W::Sum::one()),
327 value => Err(crate::traits::EvaluationError::InvalidConfiguration(
328 format!("spin value must be -1 or 1, got {value}"),
329 )),
330 };
331 let mut energy = W::Sum::zero();
332
333 for ((i, j), j_val) in self.graph.edges().iter().zip(self.couplings.iter()) {
335 let s_i = spins[*i];
336 let s_j = spins[*j];
337 let product = s_i * s_j;
338 let term = W::checked_mul_sum(
339 j_val.to_sum(),
340 spin_sign(product)?,
341 "multiplying a SpinGlass coupling by its spin sign",
342 )?;
343 energy = W::checked_add_to_sum(energy, term, "summing SpinGlass interaction energy")?;
344 }
345
346 for (i, h_val) in self.fields.iter().enumerate() {
348 let term = W::checked_mul_sum(
349 h_val.to_sum(),
350 spin_sign(spins[i])?,
351 "multiplying a SpinGlass field by its spin sign",
352 )?;
353 energy = W::checked_add_to_sum(energy, term, "summing SpinGlass field energy")?;
354 }
355
356 Ok(energy)
357 }
358}
359
360impl<G, W> Problem for SpinGlass<G, W>
361where
362 G: Graph + crate::variant::VariantParam,
363 W: WeightElement
364 + crate::variant::VariantParam
365 + PartialOrd
366 + num_traits::Zero
367 + num_traits::Bounded,
368{
369 const NAME: &'static str = "SpinGlass";
370 type Solution = Vec<i8>;
371 type Value = Min<W::Sum>;
372
373 crate::problem_parameters![
374 ("num_interactions", num_interactions),
375 ("num_spins", num_spins),
376 ];
377
378 fn evaluate(
379 &self,
380 spins: &Self::Solution,
381 ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
382 Ok(Min(Some(self.compute_energy(spins)?)))
383 }
384
385 fn variant() -> Vec<(&'static str, &'static str)> {
386 crate::variant_params![G, W]
387 }
388}
389
390impl<G, W> crate::solvers::BruteForceProblem for SpinGlass<G, W>
391where
392 G: Graph + crate::variant::VariantParam,
393 W: WeightElement
394 + crate::variant::VariantParam
395 + PartialOrd
396 + num_traits::Zero
397 + num_traits::Bounded,
398{
399 fn dimensions(&self) -> Vec<usize> {
400 vec![2; self.graph.num_vertices()]
401 }
402}
403
404crate::impl_random_generate!(SpinGlass<SimpleGraph, i64>, crate::random::SimpleGraphRandomSpec, |spec| {
405 let graph = spec.graph()?;
406 let num_edges = graph.num_edges();
407 SpinGlass::from_graph(
408 graph,
409 vec![1; num_edges],
410 vec![0; spec.num_vertices],
411 )
412});
413
414crate::declare_variants! {
415 default SpinGlass<SimpleGraph, i64> => "2^num_spins" create SpinGlassI64CreateSpec random,
416 SpinGlass<SimpleGraph, f64> => "2^num_spins" create SpinGlassF64CreateSpec,
417}
418
419crate::register_brute_force! {
420 SpinGlass<SimpleGraph, i64> decode |_, indices: Vec<usize>| SpinGlass::<SimpleGraph, i64>::config_to_spins(&indices).expect("enumerated spin bits are valid"),
421 SpinGlass<SimpleGraph, f64> decode |_, indices: Vec<usize>| SpinGlass::<SimpleGraph, f64>::config_to_spins(&indices).expect("enumerated spin bits are valid"),
422}
423
424#[cfg(feature = "example-db")]
425pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
426 vec![crate::example_db::specs::ModelExampleSpec {
427 id: "spin_glass_simplegraph",
428 instance: Box::new(
429 SpinGlass::<SimpleGraph, i64>::without_fields(
430 5,
431 vec![
432 ((0, 1), 1),
433 ((1, 2), 1),
434 ((3, 4), 1),
435 ((0, 3), 1),
436 ((1, 3), 1),
437 ((1, 4), 1),
438 ((2, 4), 1),
439 ],
440 )
441 .unwrap(),
442 ),
443 optimal_config: serde_json::json!(vec![1, -1, 1, 1, -1]),
444 optimal_value: serde_json::json!(-3),
445 }]
446}
447
448#[cfg(test)]
449#[path = "../../unit_tests/models/graph/spin_glass.rs"]
450mod tests;
451
452crate::decision_problem_meta!(SpinGlass<SimpleGraph, i64>, "DecisionSpinGlass");
453crate::register_decision_variant!(
454 SpinGlass<SimpleGraph, i64>, "DecisionSpinGlass", "2^num_spins", &[],
455 "Does a feasible solution have objective value <= the bound?",
456 category: crate::registry::ProblemCategory::Graph,
457 dims: [
458 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
459 VariantDimension::new("weight", "i64", &["i64"]),
460 ],
461 fields: [
462 crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "Undirected interaction graph edges." },
463 crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "Vertex count, needed to preserve isolated spins." },
464 crate::registry::FieldInfo { name: "couplings", type_name: "Vec<i64>", description: "Pairwise couplings; defaults to one per edge." },
465 crate::registry::FieldInfo { name: "fields", type_name: "Vec<i64>", description: "On-site fields; defaults to zero per vertex." },
466 crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Accept objective values <= this bound" },
467 ],
468 decode: |_, indices: Vec<usize>| SpinGlass::<SimpleGraph, i64>::config_to_spins(&indices).expect("enumerated spin bits are valid")
469);
470
471#[cfg(feature = "example-db")]
472pub(crate) fn decision_canonical_rule_example_specs(
473) -> Vec<crate::example_db::specs::RuleExampleSpec> {
474 vec![crate::example_db::specs::RuleExampleSpec {
475 id: "decision_spin_glass_to_spin_glass",
476 build: || {
477 let source = crate::models::decision::Decision::new(
478 SpinGlass::<SimpleGraph, i64>::without_fields(
479 5,
480 vec![
481 ((0, 1), 1),
482 ((1, 2), 1),
483 ((3, 4), 1),
484 ((0, 3), 1),
485 ((1, 3), 1),
486 ((1, 4), 1),
487 ((2, 4), 1),
488 ],
489 )
490 .unwrap(),
491 -3,
492 );
493 let witness = serde_json::json!(vec![1, -1, 1, 1, -1]);
494 crate::example_db::specs::rule_example_with_witness::<_, SpinGlass<SimpleGraph, i64>>(
495 source,
496 crate::export::SolutionPair {
497 source_config: witness.clone(),
498 target_config: witness,
499 },
500 )
501 },
502 }]
503}