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

1""" 

2Quantum annealing sampler using QuTiP sesolve. 

3 

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

9 

10from __future__ import annotations 

11 

12from typing import Any 

13 

14import numpy as np 

15import dimod 

16import qutip 

17 

18__all__ = ["QuTipSampler"] 

19 

20 

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. 

23 

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) 

29 

30 

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

39 

40 H = qutip.qzero([2] * n) 

41 

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) 

46 

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) 

54 

55 return H 

56 

57 

58def _build_transverse_hamiltonian(n_qubits: int) -> qutip.Qobj: 

59 """Build H_transverse = -sum_i sigma_x^(i). 

60 

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 

67 

68 

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) 

73 

74 

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. 

82 

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

91 

92 indices = rng.choice(2**n, size=num_reads, p=probs) 

93 

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 ] 

99 

100 

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] 

119 

120 

121class QuTipSampler(dimod.Sampler): 

122 """dimod Sampler that simulates quantum annealing via QuTiP sesolve. 

123 

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. 

127 

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

134 

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

143 

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 } 

157 

158 @property 

159 def parameters(self) -> dict[str, list]: 

160 return self._parameters 

161 

162 @property 

163 def properties(self) -> dict[str, Any]: 

164 return self._properties 

165 

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 ) 

174 

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. 

187 

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. 

202 

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) 

212 

213 if not variables: 

214 return dimod.SampleSet.from_samples([], dimod.SPIN, energy=[]) 

215 

216 T, steps = self._resolve(anneal_time, n_steps) 

217 final_state = _anneal(h, J, variables, T, steps) 

218 

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] 

222 

223 return dimod.SampleSet.from_samples(raw_samples, vartype=dimod.SPIN, energy=energies) 

224 

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. 

236 

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. 

250 

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) 

258 

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. 

270 

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. 

283 

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)