Skip to main content

problemreductions/models/graph/
spin_glass.rs

1//! Spin Glass (Ising model) problem implementation.
2//!
3//! The Spin Glass problem minimizes the Ising Hamiltonian energy.
4
5use 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/// The Spin Glass (Ising model) problem.
29///
30/// Given n spin variables s_i in {-1, +1}, interaction coefficients J_ij,
31/// and on-site fields h_i, minimize the Hamiltonian:
32///
33/// H(s) = sum_{i<j} J_ij * s_i * s_j + sum_i h_i * s_i
34///
35/// # Representation
36///
37/// Variables are binary (0 or 1), mapped to spins via: s = 2*x - 1
38/// - x = 0 -> s = -1
39/// - x = 1 -> s = +1
40///
41/// # Type Parameters
42///
43/// * `G` - The graph type (e.g., `SimpleGraph`, `KingsSubgraph`, `UnitDiskGraph`)
44/// * `W` - The weight type for couplings (e.g., `i64`, `f64`)
45///
46/// # Example
47///
48/// ```
49/// use problemreductions::models::graph::SpinGlass;
50/// use problemreductions::topology::SimpleGraph;
51/// use problemreductions::{Problem, BruteForce};
52///
53/// // Two spins with antiferromagnetic coupling J_01 = 1
54/// let problem = SpinGlass::<SimpleGraph, f64>::new(2, vec![((0, 1), 1.0)], vec![0.0, 0.0]).unwrap();
55///
56/// let solver = BruteForce::new();
57/// let solutions = solver.find_all_witnesses(&problem).unwrap();
58///
59/// // Ground state has opposite spins
60/// for sol in &solutions {
61///     assert!(sol[0] != sol[1]); // Antiferromagnetic: opposite spins
62/// }
63/// ```
64#[derive(Debug, Clone, Serialize)]
65pub struct SpinGlass<G, W> {
66    /// The underlying graph structure.
67    graph: G,
68    /// Coupling terms J_ij, one per edge in graph.edges() order.
69    couplings: Vec<W>,
70    /// On-site fields h_i.
71    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            /// Undirected interaction graph edges.
100            #[create(codec = "edge-list")]
101            graph: Vec<(usize, usize)>,
102            /// Vertex count, needed to preserve isolated spins.
103            num_vertices: Option<usize>,
104            /// Pairwise couplings; defaults to one per edge.
105            #[create(codec = "comma-separated")]
106            couplings: Option<Vec<$weight>>,
107            /// On-site fields; defaults to zero per vertex.
108            #[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    /// Create a new Spin Glass problem.
167    ///
168    /// # Arguments
169    /// * `num_spins` - Number of spin variables
170    /// * `interactions` - Coupling terms J_ij as ((i, j), value)
171    /// * `fields` - On-site fields h_i
172    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    /// Create a Spin Glass with no on-site fields.
194    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    /// Create a SpinGlass problem from a graph with specified couplings.
208    ///
209    /// # Arguments
210    /// * `graph` - The underlying graph
211    /// * `couplings` - Coupling terms (must match graph.num_edges())
212    /// * `fields` - On-site fields h_i
213    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    /// Create a SpinGlass problem from a graph with no on-site fields.
246    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    /// Get a reference to the underlying graph.
257    pub fn graph(&self) -> &G {
258        &self.graph
259    }
260
261    /// Get the number of spins.
262    pub fn num_spins(&self) -> usize {
263        self.graph.num_vertices()
264    }
265
266    /// Get the number of interactions (edges in the interaction graph).
267    pub fn num_interactions(&self) -> usize {
268        self.graph.num_edges()
269    }
270
271    /// Get the interactions as ((i, j), weight) pairs.
272    ///
273    /// Reconstructs from graph.edges() and couplings.
274    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    /// Get the couplings (J_ij values).
284    pub fn couplings(&self) -> &[W] {
285        &self.couplings
286    }
287
288    /// Get the on-site fields.
289    pub fn fields(&self) -> &[W] {
290        &self.fields
291    }
292
293    /// Convert a binary configuration to implementation-local spin signs.
294    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    /// Compute the Hamiltonian energy for a spin configuration.
314    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        // Interaction terms: sum J_ij * s_i * s_j
334        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        // On-site terms: sum h_i * s_i
347        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}