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(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    /// Create a SpinGlass problem from a graph with no on-site fields.
242    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    /// Get a reference to the underlying graph.
253    pub fn graph(&self) -> &G {
254        &self.graph
255    }
256
257    /// Get the number of spins.
258    pub fn num_spins(&self) -> usize {
259        self.graph.num_vertices()
260    }
261
262    /// Get the number of interactions (edges in the interaction graph).
263    pub fn num_interactions(&self) -> usize {
264        self.graph.num_edges()
265    }
266
267    /// Get the interactions as ((i, j), weight) pairs.
268    ///
269    /// Reconstructs from graph.edges() and couplings.
270    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    /// Get the couplings (J_ij values).
280    pub fn couplings(&self) -> &[W] {
281        &self.couplings
282    }
283
284    /// Get the on-site fields.
285    pub fn fields(&self) -> &[W] {
286        &self.fields
287    }
288
289    /// Convert a binary configuration to implementation-local spin signs.
290    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    /// Compute the Hamiltonian energy for a spin configuration.
310    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        // Interaction terms: sum J_ij * s_i * s_j
330        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        // On-site terms: sum h_i * s_i
343        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;