Skip to content

Solvers

Combinatorial optimization via SC-native Ising machines.

  • StochasticIsingGraph — Quantum-inspired Ising solver. Spins S_i in {-1, +1} (mapped to 0/1 for SC). Energy: E = -Sum(J_ij * S_i * S_j) - Sum(h_i * S_i). Finds minimum-energy configuration via simulated annealing with SC arithmetic.

Maps to SC hardware: spin products = AND gates, energy accumulation = popcount.

import numpy as np
from sc_neurocore.solvers import StochasticIsingGraph

J = np.random.randn(10, 10)
J = (J + J.T) / 2
np.fill_diagonal(J, 0)
solver = StochasticIsingGraph(num_spins=10, J=J)
solution = solver.solve(n_steps=1000)

sc_neurocore.solvers.ising

StochasticIsingGraph dataclass

Quantum-Inspired Ising Machine Solver.

Spins S_i in {-1, 1} (mapped to 0, 1 for SC). Energy E = -Sum(J_ij * S_i * S_j) - Sum(h_i * S_i). Goal: Find configuration that minimizes E.

Source code in src/sc_neurocore/solvers/ising.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@dataclass
class StochasticIsingGraph:
    """
    Quantum-Inspired Ising Machine Solver.

    Spins S_i in {-1, 1} (mapped to 0, 1 for SC).
    Energy E = -Sum(J_ij * S_i * S_j) - Sum(h_i * S_i).
    Goal: Find configuration that minimizes E.
    """

    num_spins: int
    J: np.ndarray[Any, Any]  # Coupling matrix (symmetric, zero diagonal)
    h: np.ndarray[Any, Any]  # Bias vector
    temperature: float = 1.0
    anneal_rate: float = 0.99

    def __post_init__(self) -> None:
        # Initialize spins randomly
        self.spins = np.random.randint(0, 2, self.num_spins).astype(np.int8)
        # Convert 0/1 to -1/1 for physics calc
        self.bipolar_spins = 2 * self.spins - 1

    def step(self) -> None:
        """
        Perform one Metropolis-Hastings update step (parallel / cellular automaton style).
        """
        # Calculate local field H_i = Sum(J_ij * S_j) + h_i
        # Using matrix multiplication
        local_field = np.dot(self.J, self.bipolar_spins) + self.h

        # Calculate Energy Difference Delta_E if we flip S_i
        # Delta_E = 2 * S_i * H_i
        # (Physics convention)

        delta_E = 2 * self.bipolar_spins * local_field

        # Probability of flipping: P = min(1, exp(-Delta_E / T))
        # If Delta_E < 0 (flip reduces energy), P=1 (always flip, greedy)
        # If Delta_E > 0 (flip increases energy), P = exp(...)

        # Vectorized probability calculation
        flip_prob = np.exp(-delta_E / self.temperature)
        flip_prob = np.minimum(1.0, flip_prob)

        # Determine flips
        random_draws = np.random.random(self.num_spins)
        should_flip = random_draws < flip_prob

        # Apply flips
        # Flip -1 to 1 and 1 to -1: S_new = -S_old
        self.bipolar_spins[should_flip] *= -1

        # Update 0/1 representation
        self.spins = (self.bipolar_spins + 1) // 2

        # Anneal
        self.temperature *= self.anneal_rate

        return self.get_energy()

    def get_energy(self) -> float:
        """Calculate global energy."""
        # E = -0.5 * S^T * J * S - h^T * S
        # Factor 0.5 because J_ij is counted twice in full matrix sum
        interaction = -0.5 * np.dot(self.bipolar_spins, np.dot(self.J, self.bipolar_spins))
        bias = -np.dot(self.h, self.bipolar_spins)
        return interaction + bias

    def get_config(self) -> np.ndarray[Any, Any]:
        return self.spins

step()

Perform one Metropolis-Hastings update step (parallel / cellular automaton style).

Source code in src/sc_neurocore/solvers/ising.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def step(self) -> None:
    """
    Perform one Metropolis-Hastings update step (parallel / cellular automaton style).
    """
    # Calculate local field H_i = Sum(J_ij * S_j) + h_i
    # Using matrix multiplication
    local_field = np.dot(self.J, self.bipolar_spins) + self.h

    # Calculate Energy Difference Delta_E if we flip S_i
    # Delta_E = 2 * S_i * H_i
    # (Physics convention)

    delta_E = 2 * self.bipolar_spins * local_field

    # Probability of flipping: P = min(1, exp(-Delta_E / T))
    # If Delta_E < 0 (flip reduces energy), P=1 (always flip, greedy)
    # If Delta_E > 0 (flip increases energy), P = exp(...)

    # Vectorized probability calculation
    flip_prob = np.exp(-delta_E / self.temperature)
    flip_prob = np.minimum(1.0, flip_prob)

    # Determine flips
    random_draws = np.random.random(self.num_spins)
    should_flip = random_draws < flip_prob

    # Apply flips
    # Flip -1 to 1 and 1 to -1: S_new = -S_old
    self.bipolar_spins[should_flip] *= -1

    # Update 0/1 representation
    self.spins = (self.bipolar_spins + 1) // 2

    # Anneal
    self.temperature *= self.anneal_rate

    return self.get_energy()

get_energy()

Calculate global energy.

Source code in src/sc_neurocore/solvers/ising.py
74
75
76
77
78
79
80
def get_energy(self) -> float:
    """Calculate global energy."""
    # E = -0.5 * S^T * J * S - h^T * S
    # Factor 0.5 because J_ij is counted twice in full matrix sum
    interaction = -0.5 * np.dot(self.bipolar_spins, np.dot(self.J, self.bipolar_spins))
    bias = -np.dot(self.h, self.bipolar_spins)
    return interaction + bias