From f9e6f1e63619d0f0bc0f7d3932859ae2db2302fb Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:56:12 +0000 Subject: [PATCH 01/11] docs: add github --- docs/source/github.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/source/github.md diff --git a/docs/source/github.md b/docs/source/github.md new file mode 100644 index 0000000..eef9381 --- /dev/null +++ b/docs/source/github.md @@ -0,0 +1,3 @@ +# Github + +The source code for QeMCMC can be found at [https://github.com/Stuartferguson00/QeMCMC](https://github.com/Stuartferguson00/QeMCMC). Contributions are welcome, and you can submit issues or pull requests to help improve the project. Please refer to the repository's README and contribution guidelines for more information on how to get involved. From 010b8581b33dfc5babeae4bd1e894f5126ed8954 Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:57:50 +0000 Subject: [PATCH 02/11] docs: add section --- docs/source/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.md b/docs/source/index.md index 5411733..e438eb0 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -32,6 +32,7 @@ api/index :maxdepth: 2 :caption: Development +github license authors ``` \ No newline at end of file From 88286aead28cdb4f0bd854826d45e9482201b94f Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:22:23 +0000 Subject: [PATCH 03/11] feat: dynamic versioning --- .github/workflows/publish.yaml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/publish.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..e82d4bd --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,34 @@ +name: Publish to PyPI + +on: + push: +  tags: +   - "v*" + +jobs: + publish: +  runs-on: ubuntu-latest + +  environment: +   name: pypi + +  permissions: +   id-token: write  # for trusted publishing + +  steps: +   - uses: actions/checkout@v4 + with: + fetch-depth: 0 + +   - uses: actions/setup-python@v5 +    with: +     python-version: "3.13" + +   - name: Install build tool +    run: pip install build + +   - name: Build +    run: python -m build + +   - name: Publish +    uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file From e334b90ad97a899ea49288223c18461e655206f4 Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:22:52 +0000 Subject: [PATCH 04/11] feat: dynamic versioning --- pyproject.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 98d0b11..9c913d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,11 @@ [project] name = "qemcmc" -version = "0.0.0" +# version = "0.0.0" +dynamic = ["version"] description = "Quantum enhanced Markov Chain Monte Carlo sampler simulated using PennyLane" readme = "README.md" authors = [{ name = "Stuart Ferguson" }, { name = "Feroz Hassan" }] +license = { text = "MIT" } requires-python = ">=3.13" dependencies = [ "joblib>=1.5.0", @@ -25,5 +27,10 @@ dependencies = [ qemcmc = "qemcmc:main" [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=61.0", "wheel", "setuptools-scm"] build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] + +[tool.setuptools.packages.find] +where = ["src"] From a12c974c43c4ef6fcbbb8ad42e98e3334ec95d20 Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:27:59 +0000 Subject: [PATCH 05/11] feat: publish to PyPi --- .github/workflows/publish.yaml | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e82d4bd..3d2e3c4 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,34 +1,34 @@ name: Publish to PyPI on: - push: -  tags: -   - "v*" + push: + tags: + - "v*" jobs: - publish: -  runs-on: ubuntu-latest + publish: + runs-on: ubuntu-latest -  environment: -   name: pypi + environment: + name: pypi -  permissions: -   id-token: write  # for trusted publishing + permissions: + id-token: write # for trusted publishing -  steps: -   - uses: actions/checkout@v4 - with: - fetch-depth: 0 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 -   - uses: actions/setup-python@v5 -    with: -     python-version: "3.13" + - uses: actions/setup-python@v5 + with: + python-version: "3.13" -   - name: Install build tool -    run: pip install build + - name: Install build tool + run: pip install build -   - name: Build -    run: python -m build + - name: Build + run: python -m build -   - name: Publish -    uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file + - name: Publish + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file From 8880e694db6eb3ae39b19de5a22cb7216f51df1c Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:29:05 +0000 Subject: [PATCH 06/11] feat: publish qemcmc to PyPi --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 3d2e3c4..b9b7397 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,4 +1,4 @@ -name: Publish to PyPI +name: Publish qemcmc to PyPI on: push: From 4d4ed1a597dddc6682cbd0e69ebdb2b3cc64500a Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:55:16 +0000 Subject: [PATCH 07/11] fix: optimize pennylane device initialisation --- src/qemcmc/circuits.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/qemcmc/circuits.py b/src/qemcmc/circuits.py index 6995bbc..00c8bec 100644 --- a/src/qemcmc/circuits.py +++ b/src/qemcmc/circuits.py @@ -48,8 +48,15 @@ def __init__(self, model, gamma, time, delta_time=0.8): t_val = self.time[0] if isinstance(self.time, tuple) else self.time self.num_trotter_steps = int(np.floor((t_val / self.delta_time))) + self._devices = {} self.dev = qml.device("lightning.qubit", wires=self.n_qubits) + def _get_device(self, num_wires: int): + """Gets a PennyLane device of the exact required size, or creates it if it doesn't exist.""" + if num_wires not in self._devices: + self._devices[num_wires] = qml.device("lightning.qubit", wires=num_wires) + return self._devices[num_wires] + def get_problem_hamiltonian(self, couplings, sign=-1): """ Construct Problem Hamiltonian from symmetric coupling tensors. @@ -63,6 +70,7 @@ def get_problem_hamiltonian(self, couplings, sign=-1): if order == 0: continue spin_sign = (-1) ** order + non_zero_indices = np.transpose(np.nonzero(coupling_tensor)) for index_tuple in non_zero_indices: index_tuple = tuple(int(i) for i in index_tuple) @@ -85,20 +93,23 @@ def get_problem_hamiltonian(self, couplings, sign=-1): return qml.Hamiltonian(coeffs, obs) - def get_mixer_hamiltonian(self): + def get_mixer_hamiltonian(self, num_wires: int): """Constructs the Mixer Hamiltonian: Σ X_i.""" - return qml.Hamiltonian([1.0] * self.n_qubits, [qml.PauliX(i) for i in range(self.n_qubits)]) + return qml.Hamiltonian([1.0] * num_wires, [qml.PauliX(i) for i in range(num_wires)]) def get_state_vector(self, s: str) -> str: """Return the state vector.""" + num_wires = len(s) + dev = self._get_device(num_wires) + # Coefficients alpha = self.model.calculate_alpha(couplings=self.local_couplings) coeff_mixer = self.gamma coeff_problem = -(1 - self.gamma) * alpha - H_total = qml.Hamiltonian([coeff_mixer] + [1.0], [self.get_mixer_hamiltonian(), self.get_problem_hamiltonian(couplings=self.local_couplings, sign=coeff_problem)]) + H_total = qml.Hamiltonian([coeff_mixer] + [1.0], [self.get_mixer_hamiltonian(num_wires), self.get_problem_hamiltonian(couplings=self.local_couplings, sign=coeff_problem)]) - @qml.qnode(self.dev) + @qml.qnode(dev) def quantum_evolution(input_string): for i, bit in enumerate(input_string): if bit == "1": @@ -125,14 +136,17 @@ def get_sample_from_state_vector(self, s: str) -> str: def get_sample(self, s: str): """Returns a measured sample after time evolution""" + num_wires = len(s) + dev = self._get_device(num_wires) + # Coefficients alpha = self.model.calculate_alpha(n=self.spin_length, couplings=self.local_couplings) coeff_mixer = self.gamma coeff_problem = -(1 - self.gamma) * alpha - H_total = qml.Hamiltonian([coeff_mixer] + [1.0], [self.get_mixer_hamiltonian(), self.get_problem_hamiltonian(couplings=self.local_couplings, sign=coeff_problem)]) + H_total = qml.Hamiltonian([coeff_mixer] + [1.0], [self.get_mixer_hamiltonian(num_wires), self.get_problem_hamiltonian(couplings=self.local_couplings, sign=coeff_problem)]) - @qml.qnode(self.dev, shots=1) + @qml.qnode(dev, shots=1) def quantum_evolution(input_string): for i, bit in enumerate(input_string): if bit == "1": From 9948c7891ba9f04ac9776eff717cd499f14a8871 Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:12:57 +0000 Subject: [PATCH 08/11] chore: add dimod dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 9c913d5..c620b1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "iprogress", "spyder-kernels==2.4", "ipywidgets>=8.1.7", + "dimod=0.12.21", ] [project.scripts] From 089c03bfb292ef28dafa5014bcd51d683e38de1f Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:13:33 +0000 Subject: [PATCH 09/11] feat: get ground state energy method --- src/qemcmc/model/energy_model.py | 45 +++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/qemcmc/model/energy_model.py b/src/qemcmc/model/energy_model.py index a9e37a9..fa6b85d 100644 --- a/src/qemcmc/model/energy_model.py +++ b/src/qemcmc/model/energy_model.py @@ -2,6 +2,9 @@ import typing from typing import List import numpy as np +import dimod +import math +from tqdm import tqdm class EnergyModel: @@ -47,6 +50,34 @@ def __init__( for i in range(100): self.initial_state.append("".join(str(i) for i in np.random.randint(0, 2, self.n, dtype=int))) + def get_ground_state(self, num_reads=100, num_batches=10): + """ + Finds an approximate ground state using Simulated Annealing. + """ + h, J = self.couplings + + h_dict = {i: h[i] for i in range(self.n_spins)} + J_dict = {(i, j): J[i, j] for i in range(self.n_spins) for j in range(i + 1, self.n_spins)} + + bqm = dimod.BinaryQuadraticModel.from_ising(h_dict, J_dict) + + sampler = dimod.SimulatedAnnealingSampler() + best_energy = float("inf") + + reads_per_batch = max(1, num_reads // num_batches) + + for _ in tqdm(range(num_batches), desc=f"Annealing ({num_reads} total reads)"): + response = sampler.sample(bqm, num_reads=reads_per_batch) + + current_best = response.first + if current_best.energy < best_energy: + best_energy = current_best.energy + + print("\n--- Simulated Annealing Results ---") + print(f"Lowest Energy Found: {best_energy:.4f}") + + return best_energy + def calculate_energy(self, state, couplings, spin_type="binary", sign=1): """ Calculate the energy of a given state for an arbitrary-order Ising/QUBO model. @@ -89,12 +120,13 @@ def calculate_energy(self, state, couplings, spin_type="binary", sign=1): if order == 1: total_energy += np.dot(coupling, state) elif order == 2: - total_energy += np.einsum("ij, i, j->", coupling, state, state) + total_energy += 0.5 * np.einsum("ij, i, j->", coupling, state, state) else: # General case for any order >=3 (cubic, quartic etc.) indices = "".join(chr(97 + i) for i in range(order)) # 'abc...', 'ijkl...' einsum_str = f"{indices}," + ",".join([indices[i] for i in range(order)]) + "->" - total_energy += np.einsum(einsum_str, coupling, *([state] * order)) + coefficient = 1.0 / math.factorial(order) + total_energy += coefficient * np.einsum(einsum_str, coupling, *([state] * order)) return sign * total_energy @@ -113,9 +145,14 @@ def get_subgroup_couplings(self, subgroup: List[int], current_state: str): new_couplings = [np.zeros((n_sub,) * d) for d in range(1, max_order + 1)] for coupling in self.couplings: - for indices in np.ndindex(coupling.shape): + # only loop over elements that actually exist (non-zero) + non_zero_indices = np.transpose(np.nonzero(coupling)) + + for indices in non_zero_indices: + indices = tuple(indices) coeff = coupling[indices] - if coeff == 0 or len(set(indices)) != len(indices): + + if len(set(indices)) != len(indices): continue in_group = [i for i in indices if i in subgroup_set] From 729db6df1f5a0ad2e7138276ab7cc20117717074 Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:13:53 +0000 Subject: [PATCH 10/11] refactor: tweak spins --- src/qemcmc/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qemcmc/main.py b/src/qemcmc/main.py index a92c441..d2164cc 100644 --- a/src/qemcmc/main.py +++ b/src/qemcmc/main.py @@ -58,7 +58,7 @@ def plot_chains(chains: list[MCMCChain], color: str, label: str): plot_chains(loc_chains, "lightgreen", "classical local MCMC") def run_qemcmc(rep): - qemcmc = QeMCMC(model, gamma=(0.3, 0.6), time=(2, 20), temp=temp, coarse_graining=cg) + qemcmc = QeMCMC(model, gamma=(0.3, 0.6), time=(2, 20), temp=temp, coarse_graining=None) return qemcmc.run( steps, initial_state=initial_states[rep], From 9633083da634bb07f20a595c7a9d5c024fcf7947 Mon Sep 17 00:00:00 2001 From: Feroz Hassan <75996466+ferozmay@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:15:36 +0000 Subject: [PATCH 11/11] chore: add dimod dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c620b1b..eee849d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "iprogress", "spyder-kernels==2.4", "ipywidgets>=8.1.7", - "dimod=0.12.21", + "dimod==0.12.21", ] [project.scripts]