1 """Unit tests for Hamiltonian helpers and shot-noise example plumbing. 3 This module contains small, fast tests intended for CI that validate the 4 construction and basic properties of Heisenberg-style Hamiltonians used by 5 the VQE examples. Tests are written to be robust when the native C++ 6 extension is not available: the test runner first attempts to import the 7 real implementations from ``examples/VQE/Heisenberg_VQE.py`` and falls back 8 to lightweight Python-only generators if the import fails. This keeps CI 9 fast while still exercising the core logic. 11 Checks performed include: 12 - Hamiltonian shape and Hermiticity for small systems 13 - Eigenvalue reality for small dense matrices 14 - Sensitivity of the generator to coupling parameters (Jx/Jz/Jy) 15 - API contract for ``generate_hamiltonian`` (returns object with ``.shape``) 20 import scipy.sparse
as sp
26 from examples.VQE.shot_noise_measurement
import generate_zz_xx_hamiltonian
38 import importlib.util, os
39 repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__),
"../../"))
41 heis_path = os.path.join(repo_root,
"examples",
"VQE",
"shot_noise_measurement.py")
43 if os.path.exists(heis_path):
44 spec = importlib.util.spec_from_file_location(
"shot_noise_measurement_fallback", heis_path)
45 heis_mod = importlib.util.module_from_spec(spec)
47 spec.loader.exec_module(heis_mod)
51 if heis_mod
is not None:
52 generate_zz_xx_hamiltonian = getattr(heis_mod,
"generate_zz_xx_hamiltonian",
None)
54 if generate_zz_xx_hamiltonian
is not None:
64 if 'generate_zz_xx_hamiltonian' not in locals()
or generate_zz_xx_hamiltonian
is None:
66 import scipy.sparse
as _sp
74 _I = _np.array([[1.0, 0.0], [0.0, 1.0]], dtype=_np.complex128)
75 _X = _np.array([[0.0, 1.0], [1.0, 0.0]], dtype=_np.complex128)
76 _Y = _np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=_np.complex128)
77 _Z = _np.array([[1.0, 0.0], [0.0, -1.0]], dtype=_np.complex128)
81 topology = [(i, (i + 1) % n_qubits)
for i
in range(n_qubits)]
83 H = _np.zeros((dim, dim), dtype=_np.complex128)
86 for (i, j)
in topology:
88 ops_z = [ _Z
if k==i
or k==j
else _I
for k
in range(n_qubits) ]
89 ops_x = [ _X
if k==i
or k==j
else _I
for k
in range(n_qubits) ]
90 ops_y = [ _Y
if k==i
or k==j
else _I
for k
in range(n_qubits) ]
92 oplist.append((
"ZZ", [i, j], Jz))
95 oplist.append((
"XX", [i, j], Jx))
98 oplist.append((
"YY", [i, j], Jy))
100 for i
in range(n_qubits):
101 ops = [ _Z
if k==i
else _I
for k
in range(n_qubits) ]
106 coeff = float(_np.asarray(h)[i])
108 oplist.append((
"Z", [i], coeff))
109 return _sp.csr_matrix(H), oplist
120 """Return a dense array for small sparse matrices, otherwise return 121 the original sparse object. This keeps tests fast while allowing 122 checks that require dense operations for small sizes. 133 """Check Hermiticity (H == H^â ) up to `tol`. 135 Returns (is_hermitian_bool, max_abs_deviation) so tests can give 136 informative failures. 140 diff = Hd - Hd.getH()
141 vals = np.abs(diff.data)
if hasattr(diff,
"data")
else np.abs(diff.toarray()).ravel()
142 maxabs = np.max(vals)
if vals.size
else 0.0
143 return maxabs < tol, maxabs
145 diff = Hd - Hd.conj().T
146 maxabs = np.max(np.abs(diff))
147 return maxabs < tol, float(maxabs)
150 @pytest.mark.parametrize(
"n", [2, 3, 4])
152 """Verify generated Hamiltonian has the expected shape (2^n x 2^n) 153 and is Hermitian up to numerical tolerance. This is a basic 154 correctness check for the Hamiltonian generator. 157 assert hasattr(H,
"shape"),
"Hamiltonian must expose .shape" 158 assert H.shape == (1 << n, 1 << n), f
"unexpected Hamiltonian shape for n={n}" 160 assert herm, f
"Hamiltonian not Hermitian (max deviation {maxdiff})" 163 @pytest.mark.parametrize(
"n", [2, 3])
165 """Check that eigenvalues are numerically real for small systems. 167 Hermitian matrices must have real eigenvalues; this catches subtle 168 bugs in matrix construction where non-Hermitian entries creep in. 172 if sp.issparse(H_dense):
173 pytest.skip(
"Matrix too large to densify for eigensolver in this test")
174 eigs = np.linalg.eigvals(H_dense)
175 imag_max = np.max(np.abs(np.imag(eigs)))
176 assert imag_max < 1e-8, f
"Eigenvalues have non-negligible imaginary parts: {imag_max}" 180 """Return True if matrices A and B differ by more than `tol`. 182 Accepts either sparse or dense inputs; for sparse results the 183 function compares stored values, otherwise `np.allclose` is used. 187 if sp.issparse(Ad)
or sp.issparse(Bd):
188 Ad = Ad.tocsr()
if sp.issparse(Ad)
else sp.csr_matrix(Ad)
189 Bd = Bd.tocsr()
if sp.issparse(Bd)
else sp.csr_matrix(Bd)
191 vals = np.abs(diff.data)
if diff.data.size
else np.array([0.0])
192 return np.max(vals) > tol
194 return not np.allclose(Ad, Bd, atol=tol, rtol=0)
198 """Ensure that changing coupling parameters (e.g. Jx) modifies the 199 produced Hamiltonian. This guards against parameter-ignored bugs. 204 assert matrices_differ(H_z, H_x, tol=1e-9),
"Hamiltonian did not change when Jx changed" 208 """Confirm `generate_hamiltonian` returns either a dense ndarray or a 209 scipy sparse matrix with the expected 2^n shape (API contract test). 214 pytest.skip(
"generate_hamiltonian signature does not accept (n), skipping")
215 assert hasattr(H,
"shape"),
"generate_hamiltonian must return an object with .shape" 216 assert H.shape == (4, 4)
220 """All indices in the produced `oplist` must be within [0, n_qubits-1].""" 224 kind, idxs, coeff = term
226 assert 0 <= idxs[0] < n
228 assert 0 <= idxs[0] < n
and 0 <= idxs[1] < n
232 """Pass an array for `h` and verify per-qubit fields are represented.""" 234 h_array = [0.1, -0.2, 0.3]
237 z_terms = [t
for t
in oplist
if t[0] ==
"Z"]
238 coeffs = sorted([float(t[2])
for t
in z_terms])
239 assert np.allclose(sorted(h_array), coeffs)
243 """Simple python Monte-Carlo estimator for Z-only Hamiltonians on the 244 all-|0> state. Assumes measurements are independent and readout flips 245 each measurement bit with probability `p_readout`. 246 Returns (mean_estimate, sample_variance_of_estimates). 248 rng = np.random.RandomState(seed)
249 if len(z_terms) == 0:
253 if isinstance(first, dict):
254 coeffs = [float(t.get(
"coeff", 0.0))
for t
in z_terms]
256 coeffs = [float(t[2])
for t
in z_terms]
259 per_shot_vals = np.zeros(shots)
260 for s
in range(shots):
263 flip = rng.rand() < p_readout
264 val = -1.0
if flip
else 1.0
267 mean = float(np.mean(per_shot_vals))
268 var = float(np.var(per_shot_vals, ddof=1))
if shots > 1
else 0.0
273 """Verify analytic formulas for Z-only Hamiltonians against Monte-Carlo 274 simulation (Python implementation). This does not require the C++ 275 extension and tests statistical behavior of the estimator. 280 z_terms = [t
for t
in oplist
if t[0] ==
"Z"]
286 coeffs = np.array([float(t[2])
for t
in z_terms])
287 per_shot_mean = np.sum(coeffs * (1 - 2 * p))
288 per_shot_var = np.sum(coeffs ** 2 * 4 * p * (1 - p))
289 mean_expected = float(per_shot_mean)
290 var_expected = float(per_shot_var)
293 std_error = np.sqrt(var_expected / shots)
294 assert abs(mean_sim - mean_expected) < 4 * std_error
296 assert abs(var_sim - var_expected) / (var_expected + 1e-12) < 0.2
300 """If the C++ wrapper is importable, run a small-shot comparison of its 301 shot-noise estimator against the analytic Z-only result. Skips when the 302 native extension is not present. 305 from squander.variational_quantum_eigensolver.qgd_Variational_Quantum_Eigensolver_Base
import (
306 qgd_Variational_Quantum_Eigensolver_Base
as Variational_Quantum_Eigensolver,
313 class Variational_Quantum_Eigensolver:
314 def __init__(self, H, n, cfg, accelerator_num=0):
320 self.state = np.asarray(state, dtype=np.complex128)
322 def Expectation_Value_Shot_Noise(self, input_dict):
324 shots =
int(input_dict.get(
"shots", 2))
325 p = float(input_dict.get(
"p_readout", 0.0))
326 z_terms = input_dict.get(
"z_terms", [])
329 std_err = float(np.sqrt(var / shots))
if shots > 0
else 0.0
330 return {
"mean": float(mean),
"variance": float(var),
"std_error": float(std_err)}
334 z_terms = [t
for t
in oplist
if t[0] ==
"Z"]
337 z_terms_for_dict = [{
"i": t[1][0],
"coeff": float(t[2])}
for t
in z_terms]
340 cfg = {
"max_inner_iterations": 10}
341 vqe = Variational_Quantum_Eigensolver(H, n, cfg, accelerator_num=0)
342 state = np.zeros(1 << n, dtype=np.complex128)
344 vqe.set_Initial_State(state)
348 input_dict = {
"shots": shots,
"p_readout": p,
"zz_terms": [],
"xx_terms": [],
"yy_terms": [],
"z_terms": z_terms_for_dict,
"seed": 42}
349 res = vqe.Expectation_Value_Shot_Noise(input_dict)
350 mean_wrapper = float(res[
"mean"])
351 var_wrapper = float(res[
"variance"])
353 coeffs = np.array([float(t[2])
for t
in z_terms])
354 per_shot_mean = np.sum(coeffs * (1 - 2 * p))
355 per_shot_var = np.sum(coeffs ** 2 * 4 * p * (1 - p))
358 std_error = np.sqrt(per_shot_var / shots)
359 assert abs(mean_wrapper - per_shot_mean) < 4 * std_error
360 assert abs(var_wrapper - per_shot_var) / (per_shot_var + 1e-12) < 0.5
def _python_shot_noise_z_estimator(z_terms, shots, p_readout, seed=0)
def test_generate_hamiltonian_returns_sparse_or_ndarray()
def matrices_differ(A, B, tol=1e-12)
generate_zz_xx_hamiltonian
def set_Initial_State(self, initial_state)
Call to get the number of free parameters in the gate structure used for the decomposition.
def test_hamiltonian_spectrum_real(n)
def to_dense_if_small(H, max_dim=1<< 6)
def test_generate_hamiltonian_per_qubit_h()
def test_oplist_indices_in_range()
def generate_hamiltonian(n)
def is_hermitian(H, tol=1e-10)
def generate_hamiltonian_tmp(n)
def test_generate_zz_xx_hamiltonian_parameter_dependence()
def test_wrapper_shot_noise_against_analytic()
def test_z_only_shot_noise_analytic_and_simulation()
def test_hamiltonian_shape_and_hermitian(n)
def __init__(self)
Constructor of the class.