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(ConstructionError::Conversion(
220 "couplings length must match num_edges".into(),
221 ));
222 }
223 if fields.len() != graph.num_vertices() {
224 return Err(ConstructionError::Conversion(
225 "fields length must match num_vertices".into(),
226 ));
227 }
228 for (index, coupling) in couplings.iter().enumerate() {
229 coupling.validate_element(&format!("coupling at index {index}"))?;
230 }
231 for (index, field) in fields.iter().enumerate() {
232 field.validate_element(&format!("field at index {index}"))?;
233 }
234 Ok(Self {
235 graph,
236 couplings,
237 fields,
238 })
239 }
240
241 pub fn from_graph_without_fields(graph: G, couplings: Vec<W>) -> Result<Self, ConstructionError>
243 where
244 W: num_traits::Zero,
245 {
246 let fields = vec![W::zero(); graph.num_vertices()];
247 Self::from_graph(graph, couplings, fields)
248 }
249}
250
251impl<G: Graph, W: Clone + Default> SpinGlass<G, W> {
252 pub fn graph(&self) -> &G {
254 &self.graph
255 }
256
257 pub fn num_spins(&self) -> usize {
259 self.graph.num_vertices()
260 }
261
262 pub fn num_interactions(&self) -> usize {
264 self.graph.num_edges()
265 }
266
267 pub fn interactions(&self) -> Vec<((usize, usize), W)> {
271 self.graph
272 .edges()
273 .into_iter()
274 .zip(self.couplings.iter())
275 .map(|((i, j), w)| ((i, j), w.clone()))
276 .collect()
277 }
278
279 pub fn couplings(&self) -> &[W] {
281 &self.couplings
282 }
283
284 pub fn fields(&self) -> &[W] {
286 &self.fields
287 }
288
289 pub fn config_to_spins(config: &[usize]) -> Result<Vec<i8>, crate::traits::EvaluationError> {
291 config
292 .iter()
293 .map(|&value| match value {
294 0 => Ok(-1),
295 1 => Ok(1),
296 _ => Err(crate::traits::EvaluationError::InvalidConfiguration(
297 format!("binary spin configuration value must be 0 or 1, got {value}"),
298 )),
299 })
300 .collect()
301 }
302}
303
304impl<G, W> SpinGlass<G, W>
305where
306 G: Graph,
307 W: WeightElement,
308{
309 pub fn compute_energy(&self, spins: &[i8]) -> Result<W::Sum, crate::traits::EvaluationError> {
311 if spins.len() != self.graph.num_vertices() {
312 return Err(crate::traits::EvaluationError::InvalidConfiguration(
313 format!(
314 "expected {} spin values, got {}",
315 self.graph.num_vertices(),
316 spins.len()
317 ),
318 ));
319 }
320 let spin_sign = |spin| match spin {
321 1 => Ok(W::Sum::one()),
322 -1 => Ok(W::Sum::zero() - W::Sum::one()),
323 value => Err(crate::traits::EvaluationError::InvalidConfiguration(
324 format!("spin value must be -1 or 1, got {value}"),
325 )),
326 };
327 let mut energy = W::Sum::zero();
328
329 for ((i, j), j_val) in self.graph.edges().iter().zip(self.couplings.iter()) {
331 let s_i = spins[*i];
332 let s_j = spins[*j];
333 let product = s_i * s_j;
334 let term = W::checked_mul_sum(
335 j_val.to_sum(),
336 spin_sign(product)?,
337 "multiplying a SpinGlass coupling by its spin sign",
338 )?;
339 energy = W::checked_add_to_sum(energy, term, "summing SpinGlass interaction energy")?;
340 }
341
342 for (i, h_val) in self.fields.iter().enumerate() {
344 let term = W::checked_mul_sum(
345 h_val.to_sum(),
346 spin_sign(spins[i])?,
347 "multiplying a SpinGlass field by its spin sign",
348 )?;
349 energy = W::checked_add_to_sum(energy, term, "summing SpinGlass field energy")?;
350 }
351
352 Ok(energy)
353 }
354}
355
356impl<G, W> Problem for SpinGlass<G, W>
357where
358 G: Graph + crate::variant::VariantParam,
359 W: WeightElement
360 + crate::variant::VariantParam
361 + PartialOrd
362 + num_traits::Zero
363 + num_traits::Bounded,
364{
365 const NAME: &'static str = "SpinGlass";
366 type Solution = Vec<i8>;
367 type Value = Min<W::Sum>;
368
369 crate::problem_parameters![
370 ("num_interactions", num_interactions),
371 ("num_spins", num_spins),
372 ];
373
374 fn evaluate(
375 &self,
376 spins: &Self::Solution,
377 ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
378 Ok(Min(Some(self.compute_energy(spins)?)))
379 }
380
381 fn variant() -> Vec<(&'static str, &'static str)> {
382 crate::variant_params![G, W]
383 }
384}
385
386impl<G, W> crate::solvers::BruteForceProblem for SpinGlass<G, W>
387where
388 G: Graph + crate::variant::VariantParam,
389 W: WeightElement
390 + crate::variant::VariantParam
391 + PartialOrd
392 + num_traits::Zero
393 + num_traits::Bounded,
394{
395 fn dimensions(&self) -> Vec<usize> {
396 vec![2; self.graph.num_vertices()]
397 }
398}
399
400crate::impl_random_generate!(SpinGlass<SimpleGraph, i64>, crate::random::SimpleGraphRandomSpec, |spec| {
401 let graph = spec.graph()?;
402 let num_edges = graph.num_edges();
403 SpinGlass::from_graph(
404 graph,
405 vec![1; num_edges],
406 vec![0; spec.num_vertices],
407 )
408});
409
410crate::declare_variants! {
411 default SpinGlass<SimpleGraph, i64> => "2^num_spins" create SpinGlassI64CreateSpec random,
412 SpinGlass<SimpleGraph, f64> => "2^num_spins" create SpinGlassF64CreateSpec,
413}
414
415crate::register_brute_force! {
416 SpinGlass<SimpleGraph, i64> decode |_, indices: Vec<usize>| SpinGlass::<SimpleGraph, i64>::config_to_spins(&indices).expect("enumerated spin bits are valid"),
417 SpinGlass<SimpleGraph, f64> decode |_, indices: Vec<usize>| SpinGlass::<SimpleGraph, f64>::config_to_spins(&indices).expect("enumerated spin bits are valid"),
418}
419
420#[cfg(feature = "example-db")]
421pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
422 vec![crate::example_db::specs::ModelExampleSpec {
423 id: "spin_glass_simplegraph",
424 instance: Box::new(
425 SpinGlass::<SimpleGraph, i64>::without_fields(
426 5,
427 vec![
428 ((0, 1), 1),
429 ((1, 2), 1),
430 ((3, 4), 1),
431 ((0, 3), 1),
432 ((1, 3), 1),
433 ((1, 4), 1),
434 ((2, 4), 1),
435 ],
436 )
437 .unwrap(),
438 ),
439 optimal_config: serde_json::json!(vec![1, -1, 1, 1, -1]),
440 optimal_value: serde_json::json!(-3),
441 }]
442}
443
444#[cfg(test)]
445#[path = "../../unit_tests/models/graph/spin_glass.rs"]
446mod tests;