Coverage for src / qutip_sampler / samplers.py: 100%
83 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-16 19:01 +0900
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-16 19:01 +0900
1"""
2Quantum annealing sampler using QuTiP sesolve.
4Maps a dimod BinaryQuadraticModel onto a transverse-field Ising model,
5evolves the ground state of the transverse-field Hamiltonian to the Ising
6Hamiltonian via a linear annealing schedule, then samples bitstrings from
7the final quantum state probability distribution.
8"""
10from __future__ import annotations
12from typing import Any
14import numpy as np
15import dimod
16import qutip
18__all__ = ["QuTipSampler"]
21def _operator_on_qubit(op: qutip.Qobj, qubit_idx: int, n_qubits: int) -> qutip.Qobj:
22 """Embed a single-qubit operator into the N-qubit Hilbert space.
24 Builds: I x ... x op x ... x I where op sits at position qubit_idx.
25 """
26 ops = [qutip.identity(2)] * n_qubits
27 ops[qubit_idx] = op
28 return qutip.tensor(ops)
31def _build_ising_hamiltonian(
32 h: dict[Any, float],
33 J: dict[tuple[Any, Any], float],
34 variables: list[Any],
35) -> qutip.Qobj:
36 """Build H_ising = sum_i h[i]*sigma_z^(i) + sum_(i,j) J[i,j]*sigma_z^(i)*sigma_z^(j)."""
37 n = len(variables)
38 var_to_idx = {v: i for i, v in enumerate(variables)}
40 H = qutip.qzero([2] * n)
42 for var, bias in h.items():
43 if bias == 0.0:
44 continue
45 H += bias * _operator_on_qubit(qutip.sigmaz(), var_to_idx[var], n)
47 for (var_i, var_j), coupling in J.items():
48 if coupling == 0.0:
49 continue
50 ops = [qutip.identity(2)] * n
51 ops[var_to_idx[var_i]] = qutip.sigmaz()
52 ops[var_to_idx[var_j]] = qutip.sigmaz()
53 H += coupling * qutip.tensor(ops)
55 return H
58def _build_transverse_hamiltonian(n_qubits: int) -> qutip.Qobj:
59 """Build H_transverse = -sum_i sigma_x^(i).
61 The negative sign ensures |+>^N is the ground state.
62 """
63 H = qutip.qzero([2] * n_qubits)
64 for i in range(n_qubits):
65 H -= _operator_on_qubit(qutip.sigmax(), i, n_qubits)
66 return H
69def _initial_state(n_qubits: int) -> qutip.Qobj:
70 """Ground state of H_transverse: tensor product of |+> = (|0>+|1>)/sqrt(2)."""
71 plus = (qutip.basis(2, 0) + qutip.basis(2, 1)).unit()
72 return qutip.tensor([plus] * n_qubits)
75def _sample_from_state(
76 final_state: qutip.Qobj,
77 variables: list[Any],
78 num_reads: int,
79 rng: np.random.Generator,
80) -> list[dict[Any, int]]:
81 """Draw bitstring samples from the final quantum state.
83 Basis index i -> binary string -> qubit 0 is MSB.
84 |0> (bit '0') -> sigma_z = +1 -> spin +1.
85 |1> (bit '1') -> sigma_z = -1 -> spin -1.
86 """
87 n = len(variables)
88 amplitudes = final_state.full().flatten()
89 probs = np.abs(amplitudes) ** 2
90 probs /= probs.sum()
92 indices = rng.choice(2**n, size=num_reads, p=probs)
94 return [
95 {var: (+1 if bits[i] == "0" else -1) for i, var in enumerate(variables)}
96 for idx in indices
97 for bits in [format(idx, f"0{n}b")]
98 ]
101def _anneal(
102 h: dict[Any, float],
103 J: dict[tuple[Any, Any], float],
104 variables: list[Any],
105 T: float,
106 steps: int,
107) -> qutip.Qobj:
108 """Run the quantum annealing schedule and return the final state."""
109 n = len(variables)
110 H_ising = _build_ising_hamiltonian(h, J, variables)
111 H_transverse = _build_transverse_hamiltonian(n)
112 psi0 = _initial_state(n)
113 tlist = np.linspace(0.0, T, steps + 1)
114 H_td = [
115 [H_transverse, lambda t, _=None: 1.0 - t / T],
116 [H_ising, lambda t, _=None: t / T],
117 ]
118 return qutip.sesolve(H_td, psi0, tlist, e_ops=[]).states[-1]
121class QuTipSampler(dimod.Sampler):
122 """dimod Sampler that simulates quantum annealing via QuTiP sesolve.
124 Takes a dimod.BinaryQuadraticModel (via sample/sample_ising/sample_qubo),
125 maps it to a transverse-field Ising model, evolves the state with a linear
126 annealing schedule using sesolve, and returns a dimod.SampleSet.
128 Parameters
129 ----------
130 anneal_time : float
131 Total annealing time T (default 10.0).
132 n_steps : int
133 Number of time steps for sesolve (default 200).
135 Example
136 -------
137 >>> import dimod
138 >>> from qutip_sampler import QuTipSampler
139 >>> bqm = dimod.BinaryQuadraticModel({'a': -1, 'b': -1}, {'ab': 0.5}, 0.0, 'SPIN')
140 >>> result = QuTipSampler().sample(bqm, num_reads=100, seed=42)
141 >>> print(result.first.sample)
142 """
144 def __init__(self, anneal_time: float = 10.0, n_steps: int = 200) -> None:
145 self._anneal_time = anneal_time
146 self._n_steps = n_steps
147 self._parameters: dict[str, list] = {
148 "num_reads": [],
149 "seed": [],
150 "anneal_time": [],
151 "n_steps": [],
152 }
153 self._properties: dict[str, Any] = {
154 "description": "Quantum annealing sampler backed by QuTiP sesolve",
155 "annealing_schedule": "linear",
156 }
158 @property
159 def parameters(self) -> dict[str, list]:
160 return self._parameters
162 @property
163 def properties(self) -> dict[str, Any]:
164 return self._properties
166 def _resolve(
167 self, anneal_time: float | None, n_steps: int | None
168 ) -> tuple[float, int]:
169 """Return effective (T, steps), falling back to instance defaults."""
170 return (
171 anneal_time if anneal_time is not None else self._anneal_time,
172 n_steps if n_steps is not None else self._n_steps,
173 )
175 def sample_ising(
176 self,
177 h: dict[Any, float],
178 J: dict[tuple[Any, Any], float],
179 *,
180 num_reads: int = 100,
181 seed: int | None = None,
182 anneal_time: float | None = None,
183 n_steps: int | None = None,
184 **kwargs,
185 ) -> dimod.SampleSet:
186 """Sample from an Ising problem via quantum annealing simulation.
188 Parameters
189 ----------
190 h : dict
191 Linear biases {variable: bias}.
192 J : dict
193 Quadratic couplings {(var_i, var_j): coupling}.
194 num_reads : int
195 Number of bitstring samples to draw (default 100).
196 seed : int or None
197 RNG seed for reproducible sampling.
198 anneal_time : float or None
199 Override the instance anneal_time.
200 n_steps : int or None
201 Override the instance n_steps.
203 Returns
204 -------
205 dimod.SampleSet
206 Samples with SPIN vartype and corresponding Ising energies.
207 """
208 all_vars: set[Any] = set(h.keys())
209 for vi, vj in J.keys():
210 all_vars.update([vi, vj])
211 variables = sorted(all_vars)
213 if not variables:
214 return dimod.SampleSet.from_samples([], dimod.SPIN, energy=[])
216 T, steps = self._resolve(anneal_time, n_steps)
217 final_state = _anneal(h, J, variables, T, steps)
219 rng = np.random.default_rng(seed)
220 raw_samples = _sample_from_state(final_state, variables, num_reads, rng)
221 energies = [dimod.ising_energy(s, h, J) for s in raw_samples]
223 return dimod.SampleSet.from_samples(raw_samples, vartype=dimod.SPIN, energy=energies)
225 def sample_qubo(
226 self,
227 Q: dict[tuple[Any, Any], float],
228 *,
229 num_reads: int = 100,
230 seed: int | None = None,
231 anneal_time: float | None = None,
232 n_steps: int | None = None,
233 **kwargs,
234 ) -> dimod.SampleSet:
235 """Sample from a QUBO problem via quantum annealing simulation.
237 Parameters
238 ----------
239 Q : dict
240 QUBO coefficients {(var_i, var_j): value}. Diagonal entries
241 (var, var) are linear biases; off-diagonal are quadratic.
242 num_reads : int
243 Number of bitstring samples to draw (default 100).
244 seed : int or None
245 RNG seed for reproducible sampling.
246 anneal_time : float or None
247 Override the instance anneal_time.
248 n_steps : int or None
249 Override the instance n_steps.
251 Returns
252 -------
253 dimod.SampleSet
254 Samples with BINARY vartype and corresponding QUBO energies.
255 """
256 bqm = dimod.BinaryQuadraticModel.from_qubo(Q)
257 return self.sample(bqm, num_reads=num_reads, seed=seed, anneal_time=anneal_time, n_steps=n_steps, **kwargs)
259 def sample(
260 self,
261 bqm: dimod.BinaryQuadraticModel,
262 *,
263 num_reads: int = 100,
264 seed: int | None = None,
265 anneal_time: float | None = None,
266 n_steps: int | None = None,
267 **kwargs,
268 ) -> dimod.SampleSet:
269 """Sample from a BinaryQuadraticModel via quantum annealing simulation.
271 Parameters
272 ----------
273 bqm : dimod.BinaryQuadraticModel
274 The problem to sample. Supports both SPIN and BINARY vartypes.
275 num_reads : int
276 Number of bitstring samples to draw (default 100).
277 seed : int or None
278 RNG seed for reproducible sampling.
279 anneal_time : float or None
280 Override the instance anneal_time.
281 n_steps : int or None
282 Override the instance n_steps.
284 Returns
285 -------
286 dimod.SampleSet
287 Samples with the same vartype as the input BQM.
288 """
289 h, J, offset = bqm.to_ising()
290 sampleset = self.sample_ising(
291 h, J,
292 num_reads=num_reads,
293 seed=seed,
294 anneal_time=anneal_time,
295 n_steps=n_steps,
296 **kwargs,
297 )
298 return sampleset.change_vartype(bqm.vartype, energy_offset=offset)