qemcmc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qemcmc/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ from .sampler import MCMC, ClassicalMCMC, QeMCMC
2
+ from .model import EnergyModel
3
+ from .circuits import CircuitMaker
4
+ from .utils import MCMCState, MCMCChain, plot_chains, get_random_state, get_all_possible_states, ModelMaker
5
+ from .spectralgap import SpectralGap
6
+
7
+ __all__ = [
8
+ "EnergyModel",
9
+ "MCMC",
10
+ "ClassicalMCMC",
11
+ "QeMCMC",
12
+ "MCMCState",
13
+ "MCMCChain",
14
+ "ModelMaker",
15
+ "plot_chains",
16
+ "get_random_state",
17
+ "get_all_possible_states",
18
+ "SpectralGap",
19
+ "CircuitMaker",
20
+ "CoarseGraining",
21
+ ]
22
+
23
+
24
+ def main() -> None:
25
+ print("Hello from QeMCMC!")
qemcmc/circuits.py ADDED
@@ -0,0 +1,180 @@
1
+ import pennylane as qml
2
+ import numpy as np
3
+
4
+
5
+ class CircuitMaker:
6
+ """
7
+ Constructs and simulates quantum circuits used to generate QeMCMC proposals.
8
+
9
+ This class builds the Hamiltonian corresponding to a given energy model and
10
+ simulates its time evolution using PennyLane. Starting from a classical
11
+ bitstring configuration, the circuit performs Trotterised quantum evolution
12
+ and samples a new configuration from the resulting quantum state.
13
+
14
+ The generated sample serves as the proposal state in the quantum-enhanced
15
+ MCMC algorithm.
16
+
17
+ Parameters
18
+ ----------
19
+ model : EnergyModel
20
+ Energy model defining the problem Hamiltonian.
21
+ gamma : float
22
+ Strength of the transverse-field (mixer) term in the Hamiltonian.
23
+ time : int or tuple[int, int]
24
+ Total evolution time or range of evolution times used in the simulation.
25
+ delta_time : float, optional
26
+ Duration of each Trotter step used in the approximate time evolution.
27
+ Default is 0.8.
28
+
29
+ Notes
30
+ -----
31
+ The total Hamiltonian simulated by the circuit is
32
+
33
+ H = γ H_mixer + (1 - γ) α H_problem
34
+
35
+ where ``H_problem`` encodes the classical energy model and ``H_mixer``
36
+ corresponds to a transverse-field term. The evolution is approximated
37
+ using Trotterisation via ``qml.ApproxTimeEvolution``.
38
+ """
39
+
40
+ def __init__(self, model, gamma, time, delta_time=0.8):
41
+ self.model = model
42
+ self.gamma = gamma
43
+ self.time = time
44
+ self.delta_time = delta_time
45
+ self.n_qubits = model.n
46
+
47
+ # Calculate number of Trotter steps
48
+ t_val = self.time[0] if isinstance(self.time, tuple) else self.time
49
+ self.num_trotter_steps = int(np.floor((t_val / self.delta_time)))
50
+
51
+ self.dev = qml.device("lightning.qubit", wires=self.n_qubits)
52
+
53
+ def get_problem_hamiltonian(self, couplings, sign=-1):
54
+ """
55
+ Construct Problem Hamiltonian from symmetric coupling tensors.
56
+ """
57
+ coeffs = []
58
+ obs = []
59
+
60
+ for coupling_tensor in couplings:
61
+ coupling_tensor = np.asarray(coupling_tensor)
62
+ order = coupling_tensor.ndim
63
+ if order == 0:
64
+ continue
65
+ spin_sign = (-1) ** order
66
+ non_zero_indices = np.transpose(np.nonzero(coupling_tensor))
67
+ for index_tuple in non_zero_indices:
68
+ index_tuple = tuple(int(i) for i in index_tuple)
69
+
70
+ # skip repeated indices
71
+ if len(set(index_tuple)) != len(index_tuple):
72
+ continue
73
+ # keep only strictly increasing tuples i1 < i2 < ... < ik
74
+ if index_tuple != tuple(sorted(index_tuple)):
75
+ continue
76
+ coeff = coupling_tensor[index_tuple]
77
+ if coeff == 0:
78
+ continue
79
+
80
+ term = qml.PauliZ(index_tuple[0])
81
+ for q in index_tuple[1:]:
82
+ term = term @ qml.PauliZ(q)
83
+ coeffs.append(sign * spin_sign * float(coeff))
84
+ obs.append(term)
85
+
86
+ return qml.Hamiltonian(coeffs, obs)
87
+
88
+ def get_mixer_hamiltonian(self):
89
+ """Constructs the Mixer Hamiltonian: Σ X_i."""
90
+ return qml.Hamiltonian([1.0] * self.n_qubits, [qml.PauliX(i) for i in range(self.n_qubits)])
91
+
92
+ def get_state_vector(self, s: str) -> str:
93
+ """Return the state vector."""
94
+ # Coefficients
95
+ alpha = self.model.calculate_alpha(couplings=self.local_couplings)
96
+ coeff_mixer = self.gamma
97
+ coeff_problem = -(1 - self.gamma) * alpha
98
+
99
+ H_total = qml.Hamiltonian([coeff_mixer] + [1.0], [self.get_mixer_hamiltonian(), self.get_problem_hamiltonian(couplings=self.local_couplings, sign=coeff_problem)])
100
+
101
+ @qml.qnode(self.dev)
102
+ def quantum_evolution(input_string):
103
+ for i, bit in enumerate(input_string):
104
+ if bit == "1":
105
+ qml.PauliX(i)
106
+ qml.ApproxTimeEvolution(H_total, self.time, self.num_trotter_steps)
107
+ return qml.state()
108
+
109
+ state_vector = quantum_evolution(s)
110
+ return state_vector
111
+
112
+ def get_sample_from_state_vector(self, s: str) -> str:
113
+ """Returns a single sampled bitstring s' using the quantum distribution."""
114
+ # Get the full state vector probabilities
115
+ state_vector = self.get_state_vector(s) # This returns the complex amplitudes
116
+ probs = np.abs(state_vector) ** 2
117
+
118
+ # Sample one index based on the probabilities
119
+ n_states = len(probs)
120
+ idx = np.random.choice(n_states, p=probs)
121
+
122
+ # Convert that index back to a bitstring (e.g., 3 -> "011")
123
+ s_prime = np.binary_repr(idx, width=self.model.n)
124
+ return s_prime
125
+
126
+ def get_sample(self, s: str):
127
+ """Returns a measured sample after time evolution"""
128
+ # Coefficients
129
+ alpha = self.model.calculate_alpha(n=self.spin_length, couplings=self.local_couplings)
130
+ coeff_mixer = self.gamma
131
+ coeff_problem = -(1 - self.gamma) * alpha
132
+
133
+ H_total = qml.Hamiltonian([coeff_mixer] + [1.0], [self.get_mixer_hamiltonian(), self.get_problem_hamiltonian(couplings=self.local_couplings, sign=coeff_problem)])
134
+
135
+ @qml.qnode(self.dev, shots=1)
136
+ def quantum_evolution(input_string):
137
+ for i, bit in enumerate(input_string):
138
+ if bit == "1":
139
+ qml.PauliX(i)
140
+ qml.ApproxTimeEvolution(H_total, self.time, self.num_trotter_steps)
141
+ return qml.sample()
142
+
143
+ # Get the first shot from the sample
144
+ sample = quantum_evolution(s)[0] # pennylane update doesnt squeeze singletons anymore
145
+ bitstring = "".join(str(int(b)) for b in sample)
146
+ return bitstring
147
+
148
+ def update(self, s, subgroup_choice, local_couplings, gamma, time) -> str:
149
+ """
150
+ Performs time evolution on coarse grained hamiltonian update to get s' from s
151
+ """
152
+ self._assert_bitstring(s)
153
+ self.gamma = gamma
154
+ self.time = time
155
+ self.local_couplings = local_couplings
156
+ self.spin_length = len(subgroup_choice)
157
+
158
+ # Get s_cg' for the subgroup and reconstruct full s' using s and s_cg'
159
+ s_cg = "".join([s[i] for i in subgroup_choice])
160
+ s_cg_prime = self.get_sample(s_cg)
161
+
162
+ s_list = list(s)
163
+ for i, global_index in enumerate(subgroup_choice):
164
+ s_list[global_index] = s_cg_prime[i]
165
+
166
+ return "".join(s_list)
167
+
168
+ def _assert_bitstring(self, s, *, length=None):
169
+ # Accept numpy strings etc.
170
+ if not isinstance(s, str):
171
+ raise TypeError(f"bitstring must be of type str, got {type(s)}: {s!r}")
172
+
173
+ if length is not None and len(s) != length:
174
+ raise ValueError(f"bitstring must have length {length}, got {len(s)}: {s!r}")
175
+
176
+ bad = set(s) - {"0", "1"}
177
+ if bad:
178
+ raise ValueError(f"bitstring must contain only '0'/'1'. Bad chars: {bad}. Value: {s!r}")
179
+
180
+ return s
qemcmc/coarse_grain.py ADDED
@@ -0,0 +1,28 @@
1
+ # Internal
2
+ from qemcmc.utils.helpers import validate_subgroups
3
+
4
+ # External
5
+ import numpy as np
6
+
7
+
8
+ class CoarseGraining:
9
+ def __init__(self, n, subgroups=None, subgroup_probs=None):
10
+ if subgroups is None:
11
+ used = False
12
+ subgroups = [list(range(n))]
13
+ subgroup_probs = [1.0]
14
+ else:
15
+ used = True
16
+ if subgroup_probs is None:
17
+ subgroup_probs = [1.0 / len(subgroups)] * len(subgroups)
18
+
19
+ validate_subgroups(subgroups=subgroups, subgroup_probs=subgroup_probs, n_spins=n)
20
+
21
+ self.n = n
22
+ self.subgroups = subgroups
23
+ self.subgroup_probs = subgroup_probs
24
+ self.used = used
25
+
26
+ def sample(self, rng=np.random):
27
+ idx = rng.choice(len(self.subgroups), p=self.subgroup_probs)
28
+ return self.subgroups[idx]
qemcmc/main.py ADDED
@@ -0,0 +1,78 @@
1
+ # Internal
2
+ from qemcmc.sampler import ClassicalMCMC, QeMCMC
3
+ from qemcmc.utils import ModelMaker, MCMCChain
4
+
5
+ # External
6
+ from matplotlib import pyplot as plt
7
+ from joblib import Parallel, delayed
8
+
9
+
10
+ if __name__ == "__main__":
11
+
12
+ def plot_chains(chains: list[MCMCChain], color: str, label: str):
13
+ for chain in chains:
14
+ energies = chain.get_current_energy_array()
15
+ pos = chain.get_pos_array()
16
+ plt.plot(pos, energies, color=color, alpha=0.3)
17
+ avg_energy = sum(chain.get_current_energy_array() for chain in chains) / len(chains)
18
+ plt.plot(pos, avg_energy, color=color, label=f"Average {label}")
19
+
20
+ n = 15
21
+ steps = 300
22
+ reps = 10
23
+ model_type = "Fully Connected Ising"
24
+ model_type2 = "Coarse Grained Ising"
25
+ name = "Test Ising model"
26
+
27
+ model_maker = ModelMaker(n, model_type2, name)
28
+ model = model_maker.model
29
+ cg = model_maker.cg
30
+
31
+ temp = 0.1
32
+ initial_states = model.initial_state
33
+
34
+ uni_chains = []
35
+ for rep in range(reps):
36
+ classical_uniform_MCMC = ClassicalMCMC(model, temp, method="uniform")
37
+ uni_chain = classical_uniform_MCMC.run(
38
+ steps,
39
+ initial_state=initial_states[rep],
40
+ name="classical uniform MCMC",
41
+ verbose=True,
42
+ sample_frequency=1,
43
+ )
44
+ uni_chains.append(uni_chain)
45
+ plot_chains(uni_chains, "orange", "classical uniform MCMC")
46
+
47
+ loc_chains = []
48
+ for rep in range(reps):
49
+ classical_local_MCMC = ClassicalMCMC(model, temp, method="local")
50
+ loc_chain = classical_local_MCMC.run(
51
+ steps,
52
+ initial_state=initial_states[rep],
53
+ name="classical local MCMC",
54
+ verbose=True,
55
+ sample_frequency=1,
56
+ )
57
+ loc_chains.append(loc_chain)
58
+ plot_chains(loc_chains, "lightgreen", "classical local MCMC")
59
+
60
+ def run_qemcmc(rep):
61
+ qemcmc = QeMCMC(model, gamma=(0.3, 0.6), time=(2, 20), temp=temp, coarse_graining=cg)
62
+ return qemcmc.run(
63
+ steps,
64
+ initial_state=initial_states[rep],
65
+ name="QeMCMC",
66
+ verbose=True,
67
+ sample_frequency=1,
68
+ )
69
+
70
+ Qe_chains = Parallel(n_jobs=-1)(delayed(run_qemcmc)(rep) for rep in range(reps))
71
+ plot_chains(Qe_chains, "lightblue", "QeMCMC")
72
+
73
+ plt.xlabel("MCMC step")
74
+ plt.ylabel("Energy")
75
+ plt.title("MCMC chains")
76
+ plt.legend()
77
+ plt.show()
78
+ # plt.savefig("plots/MCMC_chains.png")
@@ -0,0 +1,4 @@
1
+ from .energy_model import EnergyModel
2
+
3
+
4
+ __all__ = ["EnergyModel"]
@@ -0,0 +1,328 @@
1
+ import itertools
2
+ import typing
3
+ from typing import List
4
+ import numpy as np
5
+
6
+
7
+ class EnergyModel:
8
+ """
9
+ A base class for energy models for a classical energy function over n spins,
10
+ defined by arbitrary-order coupling tensors.
11
+
12
+ Parameters
13
+ ----------
14
+ n:
15
+ Number of spins
16
+ couplings:
17
+ List of numpy arrays representing interaction tensors. A 1D array encodes linear terms,
18
+ a 2D array encodes pairwise terms (expected symmetric), and higher-rank tensors encode
19
+ higher-order interactions.
20
+ name:
21
+ Optional label for the model (used in plotting / logging).
22
+ cost_function_signs:
23
+ Sign convention(s) used by downstream components (e.g. proposal/acceptance conventions).
24
+
25
+ Notes
26
+ -----
27
+ - Energies are computed by mapping binary states ``{0,1}`` to spin values ``{-1,+1}`` internally.
28
+ - Brute-force methods such as ``get_all_energies`` scale as O(2^n) and are intended only for
29
+ small systems.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ n: int,
35
+ couplings: List[np.ndarray] = [],
36
+ name: str = None,
37
+ cost_function_signs: list = [-1, -1],
38
+ ):
39
+ self.n = n
40
+ self.n_spins = n
41
+ self.couplings = couplings
42
+ self.name = name
43
+ self.alpha = self.calculate_alpha(n, couplings)
44
+ self.cost_function_signs = cost_function_signs
45
+ self.initial_state = []
46
+
47
+ for i in range(100):
48
+ self.initial_state.append("".join(str(i) for i in np.random.randint(0, 2, self.n, dtype=int)))
49
+
50
+ def calculate_energy(self, state, couplings, spin_type="binary", sign=1):
51
+ """
52
+ Calculate the energy of a given state for an arbitrary-order Ising/QUBO model.
53
+
54
+ Parameters:
55
+ -----------
56
+ state : array-like (str, list, tuple, np.array)
57
+ State configuration. Can be:
58
+ - Binary: "011", [0,1,1], (0,1,1), etc. (uses values 0 and 1 directly)
59
+ - Spin: [-1,1,1], (-1,1,1), etc. (uses values -1 and +1 directly)
60
+
61
+ couplings : list of numpy arrays
62
+ List of coupling tensors where:
63
+ - 1D arrays represent linear terms (h_i)
64
+ - 2D arrays represent quadratic terms (J_ij)
65
+ - 3D arrays represent cubic terms, etc.
66
+
67
+ spin_type : str, optional (default='binary')
68
+ - 'binary': state uses 0/1 values
69
+ - 'spin': state uses -1/+1 values
70
+ Note: The actual values in 'state' are used directly in the calculations
71
+
72
+ Returns:
73
+ --------
74
+ float : Total energy of the state
75
+ """
76
+ if isinstance(state, str):
77
+ state = np.array([int(bit) for bit in state])
78
+ else:
79
+ state = np.array(state)
80
+
81
+ if spin_type == "binary":
82
+ state = 2 * state - 1
83
+
84
+ total_energy = 0.0
85
+ for coupling in couplings:
86
+ coupling = np.array(coupling)
87
+ order = coupling.ndim
88
+
89
+ if order == 1:
90
+ total_energy += np.dot(coupling, state)
91
+ elif order == 2:
92
+ total_energy += np.einsum("ij, i, j->", coupling, state, state)
93
+ else:
94
+ # General case for any order >=3 (cubic, quartic etc.)
95
+ indices = "".join(chr(97 + i) for i in range(order)) # 'abc...', 'ijkl...'
96
+ einsum_str = f"{indices}," + ",".join([indices[i] for i in range(order)]) + "->"
97
+ total_energy += np.einsum(einsum_str, coupling, *([state] * order))
98
+
99
+ return sign * total_energy
100
+
101
+ def get_subgroup_couplings(self, subgroup: List[int], current_state: str):
102
+ """
103
+ Calculates local couplings for a subgroup.
104
+ Spins outside the group are treated as frozen constants.
105
+ """
106
+ n_sub = len(subgroup)
107
+ subgroup_set = set(subgroup)
108
+ g_to_l = {g_idx: l_idx for l_idx, g_idx in enumerate(subgroup)}
109
+
110
+ # Map bitstring '0'/'1' to spin values -1/+1
111
+ state_vals = np.array([1 if b == "1" else -1 for b in current_state])
112
+ max_order = max(c.ndim for c in self.couplings)
113
+ new_couplings = [np.zeros((n_sub,) * d) for d in range(1, max_order + 1)]
114
+
115
+ for coupling in self.couplings:
116
+ for indices in np.ndindex(coupling.shape):
117
+ coeff = coupling[indices]
118
+ if coeff == 0 or len(set(indices)) != len(indices):
119
+ continue
120
+
121
+ in_group = [i for i in indices if i in subgroup_set]
122
+ out_group = [i for i in indices if i not in subgroup_set]
123
+
124
+ # Multiply coefficient by values of fixed spins outside the subgroup
125
+ multiplier = np.prod(state_vals[out_group])
126
+ effective_coeff = coeff * multiplier
127
+
128
+ if in_group:
129
+ new_order = len(in_group)
130
+ local_indices = tuple(g_to_l[i] for i in in_group)
131
+ new_couplings[new_order - 1][local_indices] += effective_coeff
132
+
133
+ return new_couplings
134
+
135
+ def calculate_alpha(self, n, couplings, eps: float = 1e-15) -> float:
136
+ """
137
+ Compute alpha = sqrt(n) / sqrt(sum of squares of UNIQUE coupling coefficients),
138
+ assuming coupling tensors are symmetric representations.
139
+
140
+ Any non-symmetric 2-body input raises ValueError.
141
+
142
+ Parameters
143
+ ----------
144
+ couplings : list[np.ndarray] | None
145
+ Couplings to use. Defaults to self.couplings.
146
+ eps : float
147
+ Small threshold to avoid division by zero.
148
+
149
+ Returns
150
+ -------
151
+ float : alpha
152
+ """
153
+ if couplings is None:
154
+ couplings = self.couplings
155
+
156
+ norm_sq = 0.0
157
+
158
+ for T in couplings:
159
+ T = np.asarray(T)
160
+ order = T.ndim
161
+
162
+ if order == 0:
163
+ continue
164
+
165
+ # 1-body: h_i
166
+ if order == 1:
167
+ if T.shape != (n,):
168
+ raise ValueError(f"1-body tensor has shape {T.shape}, expected ({n},)")
169
+ for i in range(n):
170
+ c = float(T[i])
171
+ norm_sq += c * c
172
+ continue
173
+
174
+ # 2-body: symmetric J
175
+ if order == 2:
176
+ if T.shape != (n, n):
177
+ raise ValueError(f"2-body tensor has shape {T.shape}, expected ({n},{n})")
178
+
179
+ # Enforce symmetry (rejects pure upper/lower triangular)
180
+ if not np.allclose(T, T.T):
181
+ raise ValueError("Non-symmetric J provided. This alpha function only accepts symmetric J.")
182
+
183
+ # Count each interaction once: i<j
184
+ for i in range(n):
185
+ for j in range(i + 1, n):
186
+ c = float(T[i, j])
187
+ if c != 0.0:
188
+ norm_sq += c * c
189
+ continue
190
+
191
+ # Order >= 3: count each unordered interaction once using i1<i2<...<ik
192
+ if T.shape != (n,) * order:
193
+ raise ValueError(f"{order}-body tensor has shape {T.shape}, expected {(n,) * order}")
194
+
195
+ for comb in itertools.combinations(range(n), order):
196
+ c = float(T[comb])
197
+ if c != 0.0:
198
+ norm_sq += c * c
199
+
200
+ if norm_sq < eps:
201
+ raise ValueError("Cannot compute alpha: no nonzero (non-constant) couplings found.")
202
+
203
+ return np.sqrt(n / norm_sq)
204
+
205
+ def get_energy(self, state: str) -> float:
206
+ """
207
+ Returns the energy of a given state
208
+ """
209
+ if not isinstance(state, str):
210
+ raise TypeError(f"State must be a string, but got {type(state)}")
211
+ energy = self.calculate_energy(state, self.couplings)
212
+ return energy
213
+
214
+ def get_all_energies(self) -> np.ndarray:
215
+ """
216
+ Calculate the energies for all possible spin states.
217
+ This method generates all possible spin states for the
218
+ system and calculates the energy for each state.
219
+
220
+ Returns
221
+ -------
222
+ np.ndarray: An array containing the energies of all possible spin states.
223
+ """
224
+ self.S = ["".join(i) for i in itertools.product("01", repeat=self.n)]
225
+ all_energies = np.zeros(len(self.S))
226
+ for state in self.S:
227
+ all_energies[int(state, 2)] = self.calculate_energy(state, self.couplings)
228
+ return all_energies
229
+
230
+ def get_lowest_energies(self, num_states: int) -> typing.Tuple[np.ndarray, np.ndarray]:
231
+ """
232
+ Retrieve the lowest energy states and their degeneracies.
233
+ This method computes all possible energies and then finds the specified number
234
+ of lowest energy states along with their degeneracies. Note that this method
235
+ is intended for small instances due to its brute-force nature, which is extremely
236
+ memory intensive and slow.
237
+ Args:
238
+ num_states (int): The number of lowest energy states to retrieve.
239
+ Returns:
240
+ Two numpy arrays:
241
+ - The first array contains the lowest energy values.
242
+ - The second array contains the degeneracies of the corresponding energy values.
243
+ """
244
+ # only to be used for small instances, it is just brute force so extremely memory intensive and slow
245
+ all_energies = self.get_all_energies()
246
+
247
+ # very slow (sorts whole array)
248
+ self.lowest_energies, self.lowest_energy_degeneracy = self.find_lowest_values(all_energies, num_values=num_states)
249
+
250
+ return self.lowest_energies, self.lowest_energy_degeneracy
251
+
252
+ def find_lowest_values(self, arr: np.ndarray, num_values: int = 5):
253
+ """
254
+ Find the lowest unique values in an array and their degeneracies.
255
+
256
+ Args:
257
+ arr (np.ndarray): The input array from which to find the lowest values.
258
+ num_values (int, optional): The number of lowest unique values to find. Defaults to 5.
259
+
260
+ Returns:
261
+ tuple: A tuple containing two numpy arrays:
262
+ - lowest_values (np.ndarray): The lowest unique values in the array.
263
+ - degeneracy (np.ndarray): The counts of each of the lowest unique values.
264
+ """
265
+ # Count the occurrences of each value
266
+ unique_values, counts = np.unique(arr, return_counts=True)
267
+ # Sort the unique values and counts by value
268
+ sorted_indices = np.argsort(unique_values)
269
+ unique_values_sorted = unique_values[sorted_indices]
270
+ counts_sorted = counts[sorted_indices]
271
+ # Find the first num_values
272
+ lowest_values = unique_values_sorted[:num_values]
273
+ degeneracy = counts_sorted[:num_values]
274
+ return lowest_values, degeneracy
275
+
276
+ def get_lowest_energy(self):
277
+ """
278
+ Calculate and return the lowest energy from all possible energies.
279
+ This method uses a brute force approach to find the lowest energy,
280
+ making it extremely memory intensive and slow. It is recommended
281
+ to use this method only for small instances.
282
+ Returns:
283
+ float: The lowest energy value.
284
+ Notes:
285
+ If the lowest energy has already been calculated and stored
286
+ in `self.lowest_energy`, it will return that value directly
287
+ to save computation time.
288
+ """
289
+
290
+ # Only to be used for small instances, it is just brute force so extremely memory intensive and slow
291
+ if self.lowest_energy is not None:
292
+ return self.lowest_energy
293
+ else:
294
+ all_energies = self.get_all_energies()
295
+
296
+ lowest_energy = np.min(all_energies)
297
+
298
+ return lowest_energy
299
+
300
+ def get_boltzmann_factor(self, state: str, beta: float = 1.0) -> float:
301
+ """
302
+ Get un-normalised boltzmann probability of a given state
303
+
304
+ Args:
305
+ state (str): configuration of spins for which probability is to be calculated
306
+ beta (float): inverse temperature (1/T) at which the probability is to be calculated.
307
+
308
+ Returns:
309
+ float corresponding to the un-normalised boltzmann probability of the given state.
310
+ """
311
+ E = self.get_energy(state)
312
+ r = np.exp(-1 * beta * E, dtype=np.longdouble)
313
+
314
+ return r
315
+
316
+ def get_boltzmann_factor_from_energy(self, E, beta: float = 1.0) -> float:
317
+ """
318
+ Get un-normalized Boltzmann probability for a given energy.
319
+
320
+ Args:
321
+ E (float): Energy for which the Boltzmann factor is to be calculated.
322
+ beta (float): Inverse temperature (1/T) at which the probability is to be calculated.
323
+
324
+ Returns:
325
+ float: The un-normalized Boltzmann probability for the given energy.
326
+ """
327
+
328
+ return np.exp(-1 * beta * E, dtype=np.longdouble)
@@ -0,0 +1,6 @@
1
+ from .mcmc import MCMC
2
+ from .classical_mcmc import ClassicalMCMC
3
+ from .qe_mcmc import QeMCMC
4
+
5
+
6
+ __all__ = ["MCMC", "ClassicalMCMC", "QeMCMC"]