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
| 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
|