Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
shot_noise_measurement.py
Go to the documentation of this file.
1 """Heisenberg VQE example and shot-noise energy estimator.
2 
3 This example constructs a Heisenberg-style Hamiltonian (ZZ/XX/YY couplings
4 plus local Z fields), builds an instance of the C++-backed
5 `qgd_Variational_Quantum_Eigensolver_Base` wrapper, applies a parameterized
6 ansatz, and demonstrates both exact (ideal) and shot-noise energy estimation.
7 
8 The file shows how to format the term lists passed to the C++ wrapper:
9 `zz_terms`, `xx_terms`, `yy_terms` each contain items of the form
10 `{"i": int, "j": int, "coeff": float}` and `z_terms` contains
11 `{"i": int, "coeff": float}`. The shot-noise API expects a dictionary
12 containing these lists together with `shots`, `p_readout`, and `seed`.
13 
14 This script is intended for interactive runs and manual inspection. For
15 lightweight automated checks that do not require compiling the native
16 extension, see `tests/VQE/test_shot_noise_measurement.py` which provides
17 fallback implementations and fast unit tests.
18 """
19 
20 import sys
21 import os
22 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
23 
24 import numpy as np
25 import scipy as sp
26 from qiskit.quantum_info import SparsePauliOp
27 from networkx.generators.random_graphs import random_regular_graph
28 
29 np.set_printoptions(linewidth=200)
30 
31 # =======================================
32 # Generate ZZ Hamiltonian + field terms
33 # =======================================
34 
35 # Import the C++-backed VQE class inside main() to avoid loading the
36 # native extension at import time (keeps pytest collection safe).
37 
38 def generate_zz_xx_hamiltonian(n_qubits, h=0.5, topology=None, Jz=1.0, Jx=1.0, Jy=1.0):
39  """Create a Heisenberg-like Hamiltonian and a Pauli-format oplist.
40 
41  The Hamiltonian built is:
42  H = sum_{(i,j) in topology} [ Jz Z_i Z_j + Jx X_i X_j + Jy Y_i Y_j ]
43  + sum_i h_i Z_i
44 
45  Parameters
46  ----------
47  n_qubits : int
48  Number of qubits in the system.
49  h : float or array-like, optional
50  Local Z field magnitude (scalar for uniform field or array-like for
51  per-qubit fields). Default 0.5.
52  topology : sequence of (i, j) pairs, optional
53  Edge list describing which qubit pairs are coupled. If ``None``, a
54  nearest-neighbour ring topology is used.
55  Jz, Jx, Jy : float, optional
56  Coupling strengths for the ZZ, XX and YY terms respectively.
57 
58  Returns
59  -------
60  H_sparse : scipy.sparse.csr_matrix
61  The full Hamiltonian as a sparse matrix in the computational (Z)
62  basis. Suitable for small-system exact calculations.
63  oplist : list
64  A list of tuples describing Pauli terms in the form expected by the
65  wrapper and by :class:`qiskit.quantum_info.SparsePauliOp`:
66  ("ZZ", [i, j], coeff), ("XX", [i, j], coeff), ("YY", [i, j], coeff),
67  and ("Z", [i], coeff).
68 
69  Notes
70  -----
71  The returned ``oplist`` is used to build dictionaries passed to the C++
72  wrapper shot-noise routine (see ``zz_terms_for_dict`` etc. in
73  :func:`main`). The sparse Hamiltonian is convenient for exact energy
74  evaluation using ``scipy.sparse.linalg.eigsh`` for small systems.
75  """
76 
77  if topology is None:
78  topology = [[i, (i + 1) % n_qubits] for i in range(n_qubits)]
79 
80  # normalize h into a vector
81  if np.isscalar(h):
82  h_vec = np.full(n_qubits, float(h))
83  else:
84  h_vec = np.asarray(h, dtype=float)
85 
86  oplist = []
87 
88  # --- ZZ, XX, and YY couplings ---
89  for i, j in topology:
90  # ZZ term
91  oplist.append(("ZZ", [i, j], float(Jz)))
92 
93  # XX term
94  oplist.append(("XX", [i, j], float(Jx)))
95 
96  # YY term
97  oplist.append(("YY", [i, j], float(Jy)))
98 
99  # --- Magnetic field (Z terms) ---
100  for i in range(n_qubits):
101  oplist.append(("Z", [i], float(h_vec[i])))
102 
103  # Convert to SparsePauliOp
104  H_sparse = SparsePauliOp.from_sparse_list(oplist, num_qubits=n_qubits)
105  H_sparse = H_sparse.to_matrix(sparse=True).tocsr()
106 
107  return H_sparse, oplist
108 
109 
110 def main():
111  """Run a full VQE example and shot-noise energy estimation.
112 
113  Workflow:
114  - Build a Heisenberg Hamiltonian and decompose it to term lists
115  consumable by the C++ wrapper.
116  - Instantiate the wrapped VQE object and configure optimizer/ansatz.
117  - Generate and apply the circuit to obtain the final state |psi>.
118  - Compute exact (ideal) energies and per-term contributions.
119  - Run the shot-noise estimator via the wrapper and print a summary.
120 
121  This function prints human-readable diagnostics. For automated tests
122  prefer using the unit-tests under ``tests/VQE`` which include
123  lightweight fallbacks when the native extension is unavailable.
124  """
125 
126  from squander.variational_quantum_eigensolver.qgd_Variational_Quantum_Eigensolver_Base import (
127  qgd_Variational_Quantum_Eigensolver_Base as Variational_Quantum_Eigensolver,
128  )
129 
130  # ----------------------
131  # Simulation config
132  # ----------------------
133  n_qubits = 2
134  h = 0.5
135  topology = None # ring if None
136 
137  Hamiltonian, oplist = generate_zz_xx_hamiltonian(n_qubits, h=h, topology=topology)
138 
139  # ----------------------
140  # Split term lists
141  # ----------------------
142  zz_terms_for_dict = [
143  {"i": t[1][0], "j": t[1][1], "coeff": float(t[2])}
144  for t in oplist if t[0] == "ZZ"
145  ]
146 
147  xx_terms_for_dict = [
148  {"i": t[1][0], "j": t[1][1], "coeff": float(t[2])}
149  for t in oplist if t[0] == "XX"
150  ]
151 
152  yy_terms_for_dict = [
153  {"i": t[1][0], "j": t[1][1], "coeff": float(t[2])}
154  for t in oplist if t[0] == "YY"
155  ]
156 
157  z_terms_for_dict = [
158  {"i": t[1][0], "coeff": float(t[2])}
159  for t in oplist if t[0] == "Z"
160  ]
161 
162  # --- Build separate Hamiltonians for ZZ, XX, YY, Z parts (for analysis) ---
163  zz_ops = [(t[0], t[1], t[2]) for t in oplist if t[0] == "ZZ"]
164  xx_ops = [(t[0], t[1], t[2]) for t in oplist if t[0] == "XX"]
165  yy_ops = [(t[0], t[1], t[2]) for t in oplist if t[0] == "YY"]
166  z_ops = [(t[0], t[1], t[2]) for t in oplist if t[0] == "Z"]
167 
168  H_zz = (
169  SparsePauliOp.from_sparse_list(zz_ops, num_qubits=n_qubits)
170  .to_matrix(sparse=True)
171  .tocsr()
172  if zz_ops else None
173  )
174  H_xx = (
175  SparsePauliOp.from_sparse_list(xx_ops, num_qubits=n_qubits)
176  .to_matrix(sparse=True)
177  .tocsr()
178  if xx_ops else None
179  )
180  H_yy = (
181  SparsePauliOp.from_sparse_list(yy_ops, num_qubits=n_qubits)
182  .to_matrix(sparse=True)
183  .tocsr()
184  if yy_ops else None
185  )
186  H_z = (
187  SparsePauliOp.from_sparse_list(z_ops, num_qubits=n_qubits)
188  .to_matrix(sparse=True)
189  .tocsr()
190  if z_ops else None
191  )
192 
193  # Reference ground energy (small n). Use eigsh since H is Hermitian.
194  # 'SA' = smallest algebraic
195  eigvals, _ = sp.sparse.linalg.eigsh(Hamiltonian, k=1, which="SA")
196  target_e = float(np.real(eigvals[0]))
197 
198  # ----------------------
199  # Build & apply VQE
200  # ----------------------
201  config = {"max_inner_iterations": 200, "batch_size": 64, "convergence_length": 10}
202  VQE = Variational_Quantum_Eigensolver(Hamiltonian, n_qubits, config, accelerator_num=0)
203  VQE.set_Optimizer("COSINE")
204  VQE.set_Ansatz("HEA_ZYZ")
205  VQE.Generate_Circuit(layers=2, inner_blocks=1)
206 
207  param_num = VQE.get_Parameter_Num()
208  parameters = np.random.randn(param_num) * 2 * np.pi
209  VQE.set_Optimized_Parameters(parameters)
210 
211  # Initial |0...0> state
212  state = np.zeros(1 << n_qubits, dtype=np.complex128)
213  state[0] = 1.0
214 
215  # set the internal initial state used by the shot-noise path
216  VQE.set_Initial_State(state)
217 
218  # Apply circuit in-place (C++ mutates `state`) to get |ψ>
219  VQE.apply_to(parameters, state)
220 
221  # Sanity checks
222  prob_sum = float(np.sum(np.abs(state) ** 2))
223  if not np.isclose(prob_sum, 1.0, atol=1e-9):
224  raise RuntimeError(f"State not normalized after apply_to: sum|amp|^2 = {prob_sum}")
225 
226  if Hamiltonian.shape != (len(state), len(state)):
227  raise RuntimeError(
228  f"Dimension mismatch: H is {Hamiltonian.shape}, state length {len(state)} "
229  f"(expected {(1<<n_qubits)})."
230  )
231 
232  # ---------------------------------------------------
233  # Show the final VQE state |ψ> in the Z basis
234  # ---------------------------------------------------
235  print("\n================== Final VQE state |ψ⟩ (Z basis) ==================")
236  print(f"(Only amplitudes with |amp| > 1e-3 are shown; norm^2 = {prob_sum:.12f})\n")
237 
238  threshold = 1e-3
239  for idx, amp in enumerate(state):
240  if np.abs(amp) > threshold:
241  bitstring = format(idx, f"0{n_qubits}b")
242  print(f"|{bitstring}> : {amp.real:+.6f} + {amp.imag:+.6f}i")
243  print("==================================================================\n")
244 
245  # ===================================================
246  # Ideal (exact) energy with this |ψ>
247  # ===================================================
248  ideal_energy = VQE.Expectation_value_of_energy_real(state, state)
249  energy_ref = float(np.vdot(state, Hamiltonian.dot(state)).real)
250  diff = ideal_energy - energy_ref
251 
252  # --- Per-part ideal energies: ZZ, XX, YY, Z ---
253  def exp_val(part_H):
254  if part_H is None:
255  return 0.0
256  return float(np.vdot(state, part_H.dot(state)).real)
257 
258  E_zz = exp_val(H_zz)
259  E_xx = exp_val(H_xx)
260  E_yy = exp_val(H_yy)
261  E_z = exp_val(H_z)
262  E_sum_parts = E_zz + E_xx + E_yy + E_z
263 
264  print("\n============================================================")
265  print("🧮 Quantum Heisenberg VQE Simulation Results")
266  print("============================================================")
267  print(f"Target ground state eigenvalue: {target_e:.8f}")
268  print(f"Ideal energy ⟨ψ|H|ψ⟩ (wrapper): {ideal_energy:.8f} | direct: {energy_ref:.8f} | Δ = {diff:.2e}")
269 
270  print("\n--- Decomposition of ⟨ψ|H|ψ⟩ into ZZ / XX / YY / Z parts (ideal) ---")
271  print(f"E_ZZ = {E_zz: .8f}")
272  print(f"E_XX = {E_xx: .8f}")
273  print(f"E_YY = {E_yy: .8f}")
274  print(f"E_Z = {E_z: .8f}")
275  print(f"E_ZZ + E_XX + E_YY + E_Z = {E_sum_parts: .8f}")
276 
277  # ===================================================
278  # Shot-noise run setup
279  # ===================================================
280  shots = 1
281  p_readout = 0.0
282  seed = 42
283 
284  input_dict = {
285  "shots": shots,
286  "p_readout": p_readout,
287  "zz_terms": zz_terms_for_dict,
288  "xx_terms": xx_terms_for_dict,
289  "yy_terms": yy_terms_for_dict,
290  "z_terms": z_terms_for_dict,
291  "seed": seed,
292  }
293 
294  # ===================================================
295  # Debug info
296  # ===================================================
297  max_zz_idx = max(max(t["i"], t["j"]) for t in zz_terms_for_dict) if zz_terms_for_dict else -1
298  max_xx_idx = max(max(t["i"], t["j"]) for t in xx_terms_for_dict) if xx_terms_for_dict else -1
299  max_yy_idx = max(max(t["i"], t["j"]) for t in yy_terms_for_dict) if yy_terms_for_dict else -1
300  max_z_idx = max(t["i"] for t in z_terms_for_dict) if z_terms_for_dict else -1
301 
302  print("------------------------------------------------------------")
303  print(f"Qubits: {n_qubits}")
304  print(f"ZZ terms: {len(zz_terms_for_dict)} (max index: {max_zz_idx})")
305  print(f"XX terms: {len(xx_terms_for_dict)} (max index: {max_xx_idx})")
306  print(f"YY terms: {len(yy_terms_for_dict)} (max index: {max_yy_idx})")
307  print(f"Z terms: {len(z_terms_for_dict)} (max index: {max_z_idx})")
308  print(f"State shape: {state.shape} | Hamiltonian shape: {Hamiltonian.shape}")
309  print(f"Shots per measurement basis (Z, X, and Y): {shots}")
310 
311 
312  # ===================================================
313  # Run shot-noise energy estimation
314  # ===================================================
315  result = VQE.Expectation_Value_Shot_Noise(input_dict)
316 
317  mean_energy = result["mean"]
318  variance = result["variance"]
319  std_error = result["std_error"]
320  delta = mean_energy - ideal_energy
321 
322  print("------------------------------------------------------------")
323  print(f"Shot-noise energy ({shots} shots, {p_readout*100:.2f}% readout error): {mean_energy:.6f}")
324  print(f"Variance: {variance:.6f} | Std. error: {std_error:.6f}")
325  print(f"Difference (noisy − ideal): {delta:+.6f}")
326 
327  # ===================================================
328  # Summary interpretation
329  # ===================================================
330  rel_change = (delta / ideal_energy) * 100 if abs(ideal_energy) > 1e-10 else 0.0
331  direction = "decrease" if delta < 0 else "increase"
332  print("------------------------------------------------------------")
333  print(f"Summary: Noise introduced a {direction} of {abs(delta):.6f} "
334  f"in energy ({abs(rel_change):.2f}% relative change).")
335  print("============================================================\n")
336 
337 
338 if __name__ == "__main__":
339  main()
340 
def generate_zz_xx_hamiltonian(n_qubits, h=0.5, topology=None, Jz=1.0, Jx=1.0, Jy=1.0)