vqe-pennylane 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qpe/__init__.py +77 -0
- qpe/__main__.py +233 -0
- qpe/core.py +317 -0
- qpe/hamiltonian.py +100 -0
- qpe/io_utils.py +132 -0
- qpe/noise.py +47 -0
- qpe/visualize.py +212 -0
- vqe/__init__.py +118 -0
- vqe/__main__.py +318 -0
- vqe/ansatz.py +420 -0
- vqe/core.py +907 -0
- vqe/engine.py +390 -0
- vqe/hamiltonian.py +260 -0
- vqe/io_utils.py +265 -0
- vqe/optimizer.py +58 -0
- vqe/ssvqe.py +271 -0
- vqe/visualize.py +308 -0
- vqe_pennylane-0.2.2.dist-info/METADATA +239 -0
- vqe_pennylane-0.2.2.dist-info/RECORD +28 -0
- vqe_pennylane-0.2.2.dist-info/WHEEL +5 -0
- vqe_pennylane-0.2.2.dist-info/entry_points.txt +3 -0
- vqe_pennylane-0.2.2.dist-info/licenses/LICENSE +21 -0
- vqe_pennylane-0.2.2.dist-info/top_level.txt +3 -0
- vqe_qpe_common/__init__.py +67 -0
- vqe_qpe_common/geometry.py +52 -0
- vqe_qpe_common/hamiltonian.py +58 -0
- vqe_qpe_common/molecules.py +107 -0
- vqe_qpe_common/plotting.py +167 -0
vqe/core.py
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vqe.core
|
|
3
|
+
--------
|
|
4
|
+
High-level orchestration of Variational Quantum Eigensolver (VQE) workflows.
|
|
5
|
+
|
|
6
|
+
Includes:
|
|
7
|
+
- Main VQE runner (`run_vqe`)
|
|
8
|
+
- Noise studies and multi-seed averaging
|
|
9
|
+
- Optimizer / ansatz comparisons
|
|
10
|
+
- Geometry scans (bond lengths, angles)
|
|
11
|
+
- Fermion-to-qubit mapping comparisons
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
import pennylane as qml
|
|
20
|
+
from pennylane import numpy as np
|
|
21
|
+
|
|
22
|
+
from .hamiltonian import build_hamiltonian, generate_geometry
|
|
23
|
+
from .visualize import (
|
|
24
|
+
plot_convergence,
|
|
25
|
+
plot_optimizer_comparison,
|
|
26
|
+
plot_ansatz_comparison,
|
|
27
|
+
plot_noise_statistics,
|
|
28
|
+
)
|
|
29
|
+
from .io_utils import (
|
|
30
|
+
IMG_DIR,
|
|
31
|
+
RESULTS_DIR,
|
|
32
|
+
make_run_config_dict,
|
|
33
|
+
make_filename_prefix,
|
|
34
|
+
run_signature,
|
|
35
|
+
save_run_record,
|
|
36
|
+
ensure_dirs,
|
|
37
|
+
)
|
|
38
|
+
from .engine import (
|
|
39
|
+
make_device,
|
|
40
|
+
build_ansatz as engine_build_ansatz,
|
|
41
|
+
build_optimizer as engine_build_optimizer,
|
|
42
|
+
make_energy_qnode,
|
|
43
|
+
make_state_qnode,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ================================================================
|
|
48
|
+
# SHARED HELPERS
|
|
49
|
+
# ================================================================
|
|
50
|
+
def compute_fidelity(pure_state, state_or_rho):
|
|
51
|
+
"""
|
|
52
|
+
Fidelity between a pure state |ψ⟩ and either:
|
|
53
|
+
- a statevector |φ⟩
|
|
54
|
+
- or a density matrix ρ
|
|
55
|
+
|
|
56
|
+
Returns |⟨ψ|φ⟩|² or ⟨ψ|ρ|ψ⟩ respectively.
|
|
57
|
+
"""
|
|
58
|
+
state_or_rho = np.array(state_or_rho)
|
|
59
|
+
pure_state = np.array(pure_state)
|
|
60
|
+
|
|
61
|
+
if state_or_rho.ndim == 1:
|
|
62
|
+
return float(abs(np.vdot(pure_state, state_or_rho)) ** 2)
|
|
63
|
+
elif state_or_rho.ndim == 2:
|
|
64
|
+
return float(np.real(np.vdot(pure_state, state_or_rho @ pure_state)))
|
|
65
|
+
|
|
66
|
+
raise ValueError("Invalid state shape for fidelity computation")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ================================================================
|
|
70
|
+
# MAIN VQE EXECUTION
|
|
71
|
+
# ================================================================
|
|
72
|
+
def run_vqe(
|
|
73
|
+
molecule: str = "H2",
|
|
74
|
+
seed: int = 0,
|
|
75
|
+
n_steps: int = 50,
|
|
76
|
+
stepsize: float = 0.2,
|
|
77
|
+
plot: bool = True,
|
|
78
|
+
ansatz_name: str = "UCCSD",
|
|
79
|
+
optimizer_name: str = "Adam",
|
|
80
|
+
noisy: bool = False,
|
|
81
|
+
depolarizing_prob: float = 0.0,
|
|
82
|
+
amplitude_damping_prob: float = 0.0,
|
|
83
|
+
force: bool = False,
|
|
84
|
+
symbols=None,
|
|
85
|
+
coordinates=None,
|
|
86
|
+
basis: str = "sto-3g",
|
|
87
|
+
mapping: str = "jordan_wigner",
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Run a Variational Quantum Eigensolver (VQE) workflow end-to-end.
|
|
91
|
+
|
|
92
|
+
The behaviour is designed to match the legacy notebooks, while using the new
|
|
93
|
+
engine/ansatz modules internally.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
molecule : str
|
|
98
|
+
Molecular label (used when symbols/coordinates are not explicitly provided).
|
|
99
|
+
seed : int
|
|
100
|
+
RNG seed for parameter initialisation and any stochastic components.
|
|
101
|
+
n_steps : int
|
|
102
|
+
Number of optimisation steps.
|
|
103
|
+
stepsize : float
|
|
104
|
+
Optimizer learning rate.
|
|
105
|
+
plot : bool
|
|
106
|
+
If True, plot the convergence curve.
|
|
107
|
+
ansatz_name : str
|
|
108
|
+
Name of the ansatz from vqe.ansatz.ANSATZES.
|
|
109
|
+
optimizer_name : str
|
|
110
|
+
Name of the classical optimizer.
|
|
111
|
+
noisy : bool
|
|
112
|
+
Whether to include depolarizing / amplitude-damping noise.
|
|
113
|
+
depolarizing_prob : float
|
|
114
|
+
Depolarizing channel probability (per qubit).
|
|
115
|
+
amplitude_damping_prob : float
|
|
116
|
+
Amplitude damping probability (per qubit).
|
|
117
|
+
force : bool
|
|
118
|
+
If True, ignore cached results and rerun optimisation.
|
|
119
|
+
symbols, coordinates :
|
|
120
|
+
Optional direct molecular specification; if provided, qchem is used directly.
|
|
121
|
+
basis : str
|
|
122
|
+
Basis set string (e.g. "sto-3g", "STO-3G").
|
|
123
|
+
mapping : str
|
|
124
|
+
Fermion-to-qubit mapping label ("jordan_wigner", etc.).
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
dict
|
|
129
|
+
{
|
|
130
|
+
"energy": float,
|
|
131
|
+
"energies": [float, ...],
|
|
132
|
+
"steps": int,
|
|
133
|
+
"final_state_real": [...],
|
|
134
|
+
"final_state_imag": [...],
|
|
135
|
+
"num_qubits": int,
|
|
136
|
+
}
|
|
137
|
+
"""
|
|
138
|
+
ensure_dirs()
|
|
139
|
+
np.random.seed(seed)
|
|
140
|
+
|
|
141
|
+
# --- Hamiltonian & molecular data ---
|
|
142
|
+
if symbols is not None and coordinates is not None:
|
|
143
|
+
# Direct molecular override (used in geometry scans, etc.)
|
|
144
|
+
# Normalise basis for qchem
|
|
145
|
+
basis = basis.lower()
|
|
146
|
+
H, qubits = qml.qchem.molecular_hamiltonian(
|
|
147
|
+
symbols, coordinates, charge=0, basis=basis, unit="angstrom"
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
# Use shared build_hamiltonian (which already embeds mapping logic)
|
|
151
|
+
H, qubits, symbols, coordinates, basis = build_hamiltonian(
|
|
152
|
+
molecule, mapping=mapping
|
|
153
|
+
)
|
|
154
|
+
# Normalise basis string consistently
|
|
155
|
+
basis = basis.lower()
|
|
156
|
+
|
|
157
|
+
# --- Configuration & caching ---
|
|
158
|
+
cfg = make_run_config_dict(
|
|
159
|
+
symbols=symbols,
|
|
160
|
+
coordinates=coordinates,
|
|
161
|
+
basis=basis,
|
|
162
|
+
ansatz_desc=ansatz_name,
|
|
163
|
+
optimizer_name=optimizer_name,
|
|
164
|
+
stepsize=stepsize,
|
|
165
|
+
max_iterations=n_steps,
|
|
166
|
+
seed=seed,
|
|
167
|
+
mapping=mapping,
|
|
168
|
+
noisy=noisy,
|
|
169
|
+
depolarizing_prob=depolarizing_prob,
|
|
170
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
171
|
+
molecule_label=molecule,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
sig = run_signature(cfg)
|
|
175
|
+
prefix = make_filename_prefix(
|
|
176
|
+
cfg,
|
|
177
|
+
noisy=noisy,
|
|
178
|
+
seed=seed,
|
|
179
|
+
hash_str=sig,
|
|
180
|
+
ssvqe=False,
|
|
181
|
+
)
|
|
182
|
+
result_path = os.path.join(RESULTS_DIR, f"{prefix}.json")
|
|
183
|
+
|
|
184
|
+
if not force and os.path.exists(result_path):
|
|
185
|
+
print(f"\n📂 Found cached result: {result_path}")
|
|
186
|
+
with open(result_path, "r") as f:
|
|
187
|
+
record = json.load(f)
|
|
188
|
+
return record["result"]
|
|
189
|
+
|
|
190
|
+
# --- Device, ansatz, optim, QNodes ---
|
|
191
|
+
dev = make_device(qubits, noisy=noisy)
|
|
192
|
+
ansatz_fn, params = engine_build_ansatz(
|
|
193
|
+
ansatz_name,
|
|
194
|
+
qubits,
|
|
195
|
+
seed=seed,
|
|
196
|
+
symbols=symbols,
|
|
197
|
+
coordinates=coordinates,
|
|
198
|
+
basis=basis,
|
|
199
|
+
)
|
|
200
|
+
energy_qnode = make_energy_qnode(
|
|
201
|
+
H,
|
|
202
|
+
dev,
|
|
203
|
+
ansatz_fn,
|
|
204
|
+
qubits,
|
|
205
|
+
noisy=noisy,
|
|
206
|
+
depolarizing_prob=depolarizing_prob,
|
|
207
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
208
|
+
symbols=symbols,
|
|
209
|
+
coordinates=coordinates,
|
|
210
|
+
basis=basis,
|
|
211
|
+
)
|
|
212
|
+
state_qnode = make_state_qnode(
|
|
213
|
+
dev,
|
|
214
|
+
ansatz_fn,
|
|
215
|
+
qubits,
|
|
216
|
+
noisy=noisy,
|
|
217
|
+
depolarizing_prob=depolarizing_prob,
|
|
218
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
219
|
+
symbols=symbols,
|
|
220
|
+
coordinates=coordinates,
|
|
221
|
+
basis=basis,
|
|
222
|
+
)
|
|
223
|
+
opt = engine_build_optimizer(optimizer_name, stepsize=stepsize)
|
|
224
|
+
|
|
225
|
+
# --- Optimization loop ---
|
|
226
|
+
params = np.array(params, requires_grad=True)
|
|
227
|
+
energies = [float(energy_qnode(params))]
|
|
228
|
+
|
|
229
|
+
for step in range(n_steps):
|
|
230
|
+
try:
|
|
231
|
+
# Use cost returned by step_and_cost to avoid extra QNode calls
|
|
232
|
+
params, cost = opt.step_and_cost(energy_qnode, params)
|
|
233
|
+
e = float(cost)
|
|
234
|
+
except AttributeError:
|
|
235
|
+
# Optimizers without step_and_cost
|
|
236
|
+
params = opt.step(energy_qnode, params)
|
|
237
|
+
e = float(energy_qnode(params))
|
|
238
|
+
|
|
239
|
+
energies.append(e)
|
|
240
|
+
print(f"Step {step + 1:02d}/{n_steps}: E = {e:.6f} Ha")
|
|
241
|
+
|
|
242
|
+
final_energy = float(energies[-1])
|
|
243
|
+
final_state = state_qnode(params)
|
|
244
|
+
|
|
245
|
+
# --- Optional plot ---
|
|
246
|
+
if plot:
|
|
247
|
+
plot_convergence(
|
|
248
|
+
energies,
|
|
249
|
+
molecule,
|
|
250
|
+
optimizer=optimizer_name,
|
|
251
|
+
ansatz=ansatz_name,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# --- Save ---
|
|
255
|
+
result = {
|
|
256
|
+
"energy": final_energy,
|
|
257
|
+
"energies": [float(e) for e in energies],
|
|
258
|
+
"steps": n_steps,
|
|
259
|
+
"final_state_real": np.real(final_state).tolist(),
|
|
260
|
+
"final_state_imag": np.imag(final_state).tolist(),
|
|
261
|
+
"num_qubits": qubits,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
record = {"config": cfg, "result": result}
|
|
265
|
+
save_run_record(prefix, record)
|
|
266
|
+
print(f"\n💾 Saved run record to {result_path}\n")
|
|
267
|
+
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ================================================================
|
|
272
|
+
# NOISE SWEEP (SINGLE-SEED)
|
|
273
|
+
# ================================================================
|
|
274
|
+
def run_vqe_noise_sweep(
|
|
275
|
+
molecule="H2",
|
|
276
|
+
ansatz_name="RY-CZ",
|
|
277
|
+
optimizer_name="Adam",
|
|
278
|
+
steps=30,
|
|
279
|
+
depolarizing_probs=None,
|
|
280
|
+
amplitude_damping_probs=None,
|
|
281
|
+
force=False,
|
|
282
|
+
mapping: str = "jordan_wigner",
|
|
283
|
+
show: bool = True,
|
|
284
|
+
):
|
|
285
|
+
"""
|
|
286
|
+
Simple single-seed noise sweep:
|
|
287
|
+
- Compute a noiseless reference
|
|
288
|
+
- Sweep over noise probabilities and record ΔE and fidelity
|
|
289
|
+
|
|
290
|
+
Parameters
|
|
291
|
+
----------
|
|
292
|
+
show : bool
|
|
293
|
+
Whether to display the generated plot (via matplotlib).
|
|
294
|
+
"""
|
|
295
|
+
depolarizing_probs = (
|
|
296
|
+
np.arange(0.0, 0.11, 0.02)
|
|
297
|
+
if depolarizing_probs is None
|
|
298
|
+
else np.asarray(depolarizing_probs)
|
|
299
|
+
)
|
|
300
|
+
amplitude_damping_probs = (
|
|
301
|
+
np.zeros_like(depolarizing_probs)
|
|
302
|
+
if amplitude_damping_probs is None
|
|
303
|
+
else np.asarray(amplitude_damping_probs)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# --- Reference run (noiseless) ---
|
|
307
|
+
ref = run_vqe(
|
|
308
|
+
molecule=molecule,
|
|
309
|
+
n_steps=steps,
|
|
310
|
+
stepsize=0.2,
|
|
311
|
+
plot=False,
|
|
312
|
+
ansatz_name=ansatz_name,
|
|
313
|
+
optimizer_name=optimizer_name,
|
|
314
|
+
noisy=False,
|
|
315
|
+
mapping=mapping,
|
|
316
|
+
force=force,
|
|
317
|
+
)
|
|
318
|
+
reference_energy = ref["energy"]
|
|
319
|
+
pure_state = np.array(ref["final_state_real"]) + 1j * np.array(
|
|
320
|
+
ref["final_state_imag"]
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
energy_means, energy_stds = [], []
|
|
324
|
+
fidelity_means, fidelity_stds = [], []
|
|
325
|
+
|
|
326
|
+
# --- Sweep noise ---
|
|
327
|
+
for p_dep, p_amp in zip(depolarizing_probs, amplitude_damping_probs):
|
|
328
|
+
res = run_vqe(
|
|
329
|
+
molecule=molecule,
|
|
330
|
+
n_steps=steps,
|
|
331
|
+
stepsize=0.2,
|
|
332
|
+
plot=False,
|
|
333
|
+
ansatz_name=ansatz_name,
|
|
334
|
+
optimizer_name=optimizer_name,
|
|
335
|
+
noisy=True,
|
|
336
|
+
depolarizing_prob=float(p_dep),
|
|
337
|
+
amplitude_damping_prob=float(p_amp),
|
|
338
|
+
mapping=mapping,
|
|
339
|
+
force=force,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
energy = res["energy"]
|
|
343
|
+
state = np.array(res["final_state_real"]) + 1j * np.array(
|
|
344
|
+
res["final_state_imag"]
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
dE = energy - reference_energy
|
|
348
|
+
F = compute_fidelity(pure_state, state)
|
|
349
|
+
|
|
350
|
+
energy_means.append(dE)
|
|
351
|
+
energy_stds.append(0.0) # single seed
|
|
352
|
+
fidelity_means.append(F)
|
|
353
|
+
fidelity_stds.append(0.0)
|
|
354
|
+
|
|
355
|
+
# Decide noise label for plots
|
|
356
|
+
if np.allclose(amplitude_damping_probs, 0.0):
|
|
357
|
+
noise_type = "Depolarizing"
|
|
358
|
+
noise_levels = depolarizing_probs
|
|
359
|
+
elif np.allclose(depolarizing_probs, 0.0):
|
|
360
|
+
noise_type = "Amplitude"
|
|
361
|
+
noise_levels = amplitude_damping_probs
|
|
362
|
+
else:
|
|
363
|
+
noise_type = "Combined"
|
|
364
|
+
noise_levels = depolarizing_probs # x-axis label; both are meaningful
|
|
365
|
+
|
|
366
|
+
plot_noise_statistics(
|
|
367
|
+
molecule,
|
|
368
|
+
noise_levels,
|
|
369
|
+
energy_means,
|
|
370
|
+
energy_stds,
|
|
371
|
+
fidelity_means,
|
|
372
|
+
fidelity_stds,
|
|
373
|
+
optimizer_name=optimizer_name,
|
|
374
|
+
ansatz_name=ansatz_name,
|
|
375
|
+
noise_type=noise_type,
|
|
376
|
+
show=show,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
print(f"\n✅ Noise sweep complete for {molecule} ({ansatz_name}, {optimizer_name})")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ================================================================
|
|
383
|
+
# OPTIMIZER COMPARISON
|
|
384
|
+
# ================================================================
|
|
385
|
+
def run_vqe_optimizer_comparison(
|
|
386
|
+
molecule="H2",
|
|
387
|
+
ansatz_name="RY-CZ",
|
|
388
|
+
optimizers=None,
|
|
389
|
+
steps=50,
|
|
390
|
+
stepsize=0.2,
|
|
391
|
+
noisy=True,
|
|
392
|
+
depolarizing_prob=0.05,
|
|
393
|
+
amplitude_damping_prob=0.05,
|
|
394
|
+
force=False,
|
|
395
|
+
mapping: str = "jordan_wigner",
|
|
396
|
+
show: bool = True,
|
|
397
|
+
seed=0,
|
|
398
|
+
):
|
|
399
|
+
"""
|
|
400
|
+
Compare different classical optimizers on the same VQE instance.
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
show : bool
|
|
405
|
+
Whether to display the generated plot.
|
|
406
|
+
|
|
407
|
+
Returns
|
|
408
|
+
-------
|
|
409
|
+
dict
|
|
410
|
+
{
|
|
411
|
+
"energies": {opt_name: [E0, E1, ...], ...},
|
|
412
|
+
"final_energies": {opt_name: E_final, ...}
|
|
413
|
+
}
|
|
414
|
+
"""
|
|
415
|
+
import matplotlib.pyplot as plt
|
|
416
|
+
from vqe_qpe_common.plotting import build_filename, save_plot
|
|
417
|
+
|
|
418
|
+
optimizers = optimizers or ["Adam", "GradientDescent", "Momentum"]
|
|
419
|
+
results = {}
|
|
420
|
+
final_vals = {}
|
|
421
|
+
|
|
422
|
+
# --- Run each optimizer ---
|
|
423
|
+
for opt_name in optimizers:
|
|
424
|
+
print(f"\n⚙️ Running optimizer: {opt_name}")
|
|
425
|
+
|
|
426
|
+
res = run_vqe(
|
|
427
|
+
molecule=molecule,
|
|
428
|
+
n_steps=steps,
|
|
429
|
+
stepsize=stepsize,
|
|
430
|
+
plot=False,
|
|
431
|
+
ansatz_name=ansatz_name,
|
|
432
|
+
optimizer_name=opt_name,
|
|
433
|
+
noisy=noisy,
|
|
434
|
+
depolarizing_prob=depolarizing_prob,
|
|
435
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
436
|
+
mapping=mapping,
|
|
437
|
+
force=force,
|
|
438
|
+
seed=seed,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
results[opt_name] = res["energies"]
|
|
442
|
+
final_vals[opt_name] = res["energy"]
|
|
443
|
+
|
|
444
|
+
# --- Plot comparison ---
|
|
445
|
+
plt.figure(figsize=(8, 5))
|
|
446
|
+
min_len = min(len(v) for v in results.values())
|
|
447
|
+
|
|
448
|
+
for opt, energies in results.items():
|
|
449
|
+
plt.plot(range(min_len), energies[:min_len], label=opt)
|
|
450
|
+
|
|
451
|
+
plt.title(f"{molecule} – Optimizer Comparison ({ansatz_name})")
|
|
452
|
+
plt.xlabel("Iteration")
|
|
453
|
+
plt.ylabel("Energy (Ha)")
|
|
454
|
+
plt.grid(True, alpha=0.4)
|
|
455
|
+
plt.legend()
|
|
456
|
+
plt.tight_layout()
|
|
457
|
+
|
|
458
|
+
# Show first (for notebooks), then save (save_plot will close the figure)
|
|
459
|
+
if show:
|
|
460
|
+
plt.show()
|
|
461
|
+
|
|
462
|
+
fname = build_filename(
|
|
463
|
+
molecule=molecule,
|
|
464
|
+
topic="optimizer_comparison",
|
|
465
|
+
extras={"ans": ansatz_name},
|
|
466
|
+
)
|
|
467
|
+
save_plot(fname)
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
"energies": results,
|
|
471
|
+
"final_energies": final_vals,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# ================================================================
|
|
476
|
+
# ANSATZ COMPARISON
|
|
477
|
+
# ================================================================
|
|
478
|
+
def run_vqe_ansatz_comparison(
|
|
479
|
+
molecule="H2",
|
|
480
|
+
optimizer_name="Adam",
|
|
481
|
+
ansatzes=None,
|
|
482
|
+
steps=50,
|
|
483
|
+
stepsize=0.2,
|
|
484
|
+
noisy=True,
|
|
485
|
+
depolarizing_prob=0.05,
|
|
486
|
+
amplitude_damping_prob=0.05,
|
|
487
|
+
force=False,
|
|
488
|
+
mapping: str = "jordan_wigner",
|
|
489
|
+
show: bool = True,
|
|
490
|
+
):
|
|
491
|
+
"""
|
|
492
|
+
Compare different ansatz families on the same molecule / optimizer.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
show : bool
|
|
497
|
+
Whether to display the generated plot.
|
|
498
|
+
|
|
499
|
+
Returns
|
|
500
|
+
-------
|
|
501
|
+
dict
|
|
502
|
+
{
|
|
503
|
+
"energies": {ansatz_name: [E0, E1, ...], ...},
|
|
504
|
+
"final_energies": {ansatz_name: E_final, ...}
|
|
505
|
+
}
|
|
506
|
+
"""
|
|
507
|
+
ansatzes = ansatzes or ["RY-CZ", "Minimal", "TwoQubit-RY-CNOT"]
|
|
508
|
+
results = {}
|
|
509
|
+
|
|
510
|
+
for ans_name in ansatzes:
|
|
511
|
+
print(f"\n🔹 Running ansatz: {ans_name}")
|
|
512
|
+
res = run_vqe(
|
|
513
|
+
molecule=molecule,
|
|
514
|
+
n_steps=steps,
|
|
515
|
+
stepsize=stepsize,
|
|
516
|
+
plot=False,
|
|
517
|
+
ansatz_name=ans_name,
|
|
518
|
+
optimizer_name=optimizer_name,
|
|
519
|
+
noisy=noisy,
|
|
520
|
+
depolarizing_prob=depolarizing_prob,
|
|
521
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
522
|
+
mapping=mapping,
|
|
523
|
+
force=force,
|
|
524
|
+
)
|
|
525
|
+
results[ans_name] = res["energies"]
|
|
526
|
+
|
|
527
|
+
plot_ansatz_comparison(molecule, results, optimizer=optimizer_name, show=show)
|
|
528
|
+
print(f"\n✅ Ansatz comparison complete for {molecule} ({optimizer_name})")
|
|
529
|
+
|
|
530
|
+
final_energies = {name: energies[-1] for name, energies in results.items()}
|
|
531
|
+
return {
|
|
532
|
+
"energies": results,
|
|
533
|
+
"final_energies": final_energies,
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# ================================================================
|
|
538
|
+
# MULTI-SEED NOISE STUDIES
|
|
539
|
+
# ================================================================
|
|
540
|
+
def run_vqe_multi_seed_noise(
|
|
541
|
+
molecule="H2",
|
|
542
|
+
ansatz_name="RY-CZ",
|
|
543
|
+
optimizer_name="Adam",
|
|
544
|
+
steps=30,
|
|
545
|
+
stepsize=0.2,
|
|
546
|
+
seeds=None,
|
|
547
|
+
noise_type="depolarizing",
|
|
548
|
+
depolarizing_probs=None,
|
|
549
|
+
amplitude_damping_probs=None,
|
|
550
|
+
force=False,
|
|
551
|
+
mapping: str = "jordan_wigner",
|
|
552
|
+
show: bool = True,
|
|
553
|
+
):
|
|
554
|
+
"""
|
|
555
|
+
Multi-seed noise statistics for a given molecule and ansatz.
|
|
556
|
+
"""
|
|
557
|
+
# ---------- FIXED HANDLING FOR NUMPY ARRAYS ----------
|
|
558
|
+
if seeds is None:
|
|
559
|
+
seeds = np.arange(0, 5)
|
|
560
|
+
|
|
561
|
+
if depolarizing_probs is None:
|
|
562
|
+
depolarizing_probs = np.arange(0.0, 0.11, 0.02)
|
|
563
|
+
|
|
564
|
+
if amplitude_damping_probs is None:
|
|
565
|
+
amplitude_damping_probs = np.zeros_like(depolarizing_probs)
|
|
566
|
+
|
|
567
|
+
# ---------- NOISE TYPE HANDLING ----------
|
|
568
|
+
if noise_type == "depolarizing":
|
|
569
|
+
amplitude_damping_probs = [0.0] * len(depolarizing_probs)
|
|
570
|
+
|
|
571
|
+
elif noise_type == "amplitude":
|
|
572
|
+
amplitude_damping_probs = depolarizing_probs
|
|
573
|
+
depolarizing_probs = [0.0] * len(amplitude_damping_probs)
|
|
574
|
+
|
|
575
|
+
elif noise_type == "combined":
|
|
576
|
+
amplitude_damping_probs = depolarizing_probs.copy()
|
|
577
|
+
|
|
578
|
+
else:
|
|
579
|
+
raise ValueError(f"Unknown noise type '{noise_type}'")
|
|
580
|
+
|
|
581
|
+
# --- Reference (noiseless) ---
|
|
582
|
+
print("\n🔹 Computing noiseless reference runs...")
|
|
583
|
+
ref_energies, ref_states = [], []
|
|
584
|
+
for s in seeds:
|
|
585
|
+
np.random.seed(int(s))
|
|
586
|
+
res = run_vqe(
|
|
587
|
+
molecule=molecule,
|
|
588
|
+
n_steps=steps,
|
|
589
|
+
stepsize=stepsize,
|
|
590
|
+
plot=False,
|
|
591
|
+
ansatz_name=ansatz_name,
|
|
592
|
+
optimizer_name=optimizer_name,
|
|
593
|
+
noisy=False,
|
|
594
|
+
mapping=mapping,
|
|
595
|
+
force=force,
|
|
596
|
+
seed=int(s),
|
|
597
|
+
)
|
|
598
|
+
ref_energies.append(res["energy"])
|
|
599
|
+
state = np.array(res["final_state_real"]) + 1j * np.array(
|
|
600
|
+
res["final_state_imag"]
|
|
601
|
+
)
|
|
602
|
+
ref_states.append(state)
|
|
603
|
+
|
|
604
|
+
reference_energy = float(np.mean(ref_energies))
|
|
605
|
+
reference_state = ref_states[0] / np.linalg.norm(ref_states[0])
|
|
606
|
+
print(f"Reference mean energy = {reference_energy:.6f} Ha")
|
|
607
|
+
|
|
608
|
+
# --- Noisy sweeps ---
|
|
609
|
+
energy_means, energy_stds = [], []
|
|
610
|
+
fidelity_means, fidelity_stds = [], []
|
|
611
|
+
|
|
612
|
+
for p_dep, p_amp in zip(depolarizing_probs, amplitude_damping_probs):
|
|
613
|
+
noisy_energies, fidelities = [], []
|
|
614
|
+
for s in seeds:
|
|
615
|
+
np.random.seed(int(s))
|
|
616
|
+
res = run_vqe(
|
|
617
|
+
molecule=molecule,
|
|
618
|
+
n_steps=steps,
|
|
619
|
+
stepsize=stepsize,
|
|
620
|
+
plot=False,
|
|
621
|
+
ansatz_name=ansatz_name,
|
|
622
|
+
optimizer_name=optimizer_name,
|
|
623
|
+
noisy=True,
|
|
624
|
+
depolarizing_prob=float(p_dep),
|
|
625
|
+
amplitude_damping_prob=float(p_amp),
|
|
626
|
+
mapping=mapping,
|
|
627
|
+
force=force,
|
|
628
|
+
seed=int(s),
|
|
629
|
+
)
|
|
630
|
+
noisy_energies.append(res["energy"])
|
|
631
|
+
state = np.array(res["final_state_real"]) + 1j * np.array(
|
|
632
|
+
res["final_state_imag"]
|
|
633
|
+
)
|
|
634
|
+
state = state / np.linalg.norm(state)
|
|
635
|
+
fidelities.append(compute_fidelity(reference_state, state))
|
|
636
|
+
|
|
637
|
+
noisy_energies = np.array(noisy_energies)
|
|
638
|
+
dE = noisy_energies - reference_energy
|
|
639
|
+
|
|
640
|
+
energy_means.append(float(np.mean(dE)))
|
|
641
|
+
energy_stds.append(float(np.std(dE)))
|
|
642
|
+
fidelity_means.append(float(np.mean(fidelities)))
|
|
643
|
+
fidelity_stds.append(float(np.std(fidelities)))
|
|
644
|
+
|
|
645
|
+
print(
|
|
646
|
+
f"Noise p_dep={float(p_dep):.2f}, p_amp={float(p_amp):.2f}: "
|
|
647
|
+
f"ΔE={energy_means[-1]:.6f} ± {energy_stds[-1]:.6f}, "
|
|
648
|
+
f"⟨F⟩={fidelity_means[-1]:.4f}"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
noise_levels = (
|
|
652
|
+
amplitude_damping_probs if noise_type == "amplitude" else depolarizing_probs
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
plot_noise_statistics(
|
|
656
|
+
molecule,
|
|
657
|
+
noise_levels,
|
|
658
|
+
energy_means,
|
|
659
|
+
energy_stds,
|
|
660
|
+
fidelity_means,
|
|
661
|
+
fidelity_stds,
|
|
662
|
+
optimizer_name=optimizer_name,
|
|
663
|
+
ansatz_name=ansatz_name,
|
|
664
|
+
noise_type=noise_type.capitalize(),
|
|
665
|
+
show=show,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
print(f"\n✅ Multi-seed noise study complete for {molecule}")
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# ================================================================
|
|
672
|
+
# GEOMETRY SCAN
|
|
673
|
+
# ================================================================
|
|
674
|
+
def run_vqe_geometry_scan(
|
|
675
|
+
molecule="H2_BOND",
|
|
676
|
+
param_name="bond",
|
|
677
|
+
param_values=None,
|
|
678
|
+
ansatz_name="UCCSD",
|
|
679
|
+
optimizer_name="Adam",
|
|
680
|
+
steps=30,
|
|
681
|
+
stepsize=0.2,
|
|
682
|
+
seeds=None,
|
|
683
|
+
force=False,
|
|
684
|
+
mapping: str = "jordan_wigner",
|
|
685
|
+
show: bool = True,
|
|
686
|
+
):
|
|
687
|
+
"""
|
|
688
|
+
Geometry scan using run_vqe + generate_geometry, mirroring the H₂O and LiH notebooks.
|
|
689
|
+
|
|
690
|
+
Parameters
|
|
691
|
+
----------
|
|
692
|
+
show : bool
|
|
693
|
+
Whether to display the generated plot.
|
|
694
|
+
|
|
695
|
+
Returns
|
|
696
|
+
-------
|
|
697
|
+
list of tuples
|
|
698
|
+
[(param_value, mean_E, std_E), ...]
|
|
699
|
+
"""
|
|
700
|
+
from vqe_qpe_common.plotting import (
|
|
701
|
+
build_filename,
|
|
702
|
+
save_plot,
|
|
703
|
+
format_molecule_name,
|
|
704
|
+
)
|
|
705
|
+
import matplotlib.pyplot as plt
|
|
706
|
+
|
|
707
|
+
if param_values is None:
|
|
708
|
+
raise ValueError("param_values must be specified")
|
|
709
|
+
|
|
710
|
+
seeds = seeds or [0]
|
|
711
|
+
results = []
|
|
712
|
+
|
|
713
|
+
for val in param_values:
|
|
714
|
+
print(f"\n⚙️ Geometry: {param_name} = {val:.3f}")
|
|
715
|
+
symbols, coordinates = generate_geometry(molecule, val)
|
|
716
|
+
|
|
717
|
+
energies_for_val = []
|
|
718
|
+
for s in seeds:
|
|
719
|
+
np.random.seed(int(s))
|
|
720
|
+
res = run_vqe(
|
|
721
|
+
molecule=molecule,
|
|
722
|
+
n_steps=steps,
|
|
723
|
+
stepsize=stepsize,
|
|
724
|
+
ansatz_name=ansatz_name,
|
|
725
|
+
optimizer_name=optimizer_name,
|
|
726
|
+
symbols=symbols,
|
|
727
|
+
coordinates=coordinates,
|
|
728
|
+
noisy=False,
|
|
729
|
+
plot=False,
|
|
730
|
+
seed=int(s),
|
|
731
|
+
force=force,
|
|
732
|
+
mapping=mapping,
|
|
733
|
+
)
|
|
734
|
+
energies_for_val.append(res["energy"])
|
|
735
|
+
|
|
736
|
+
mean_E = float(np.mean(energies_for_val))
|
|
737
|
+
std_E = float(np.std(energies_for_val))
|
|
738
|
+
results.append((val, mean_E, std_E))
|
|
739
|
+
print(f" → Mean E = {mean_E:.6f} ± {std_E:.6f} Ha")
|
|
740
|
+
|
|
741
|
+
# --- Plot ---
|
|
742
|
+
params, means, stds = zip(*results)
|
|
743
|
+
|
|
744
|
+
plt.errorbar(params, means, yerr=stds, fmt="o-", capsize=4)
|
|
745
|
+
plt.xlabel(f"{param_name.capitalize()} (Å or °)")
|
|
746
|
+
plt.ylabel("Ground-State Energy (Ha)")
|
|
747
|
+
plt.title(
|
|
748
|
+
f"{molecule} Energy vs {param_name.capitalize()} ({ansatz_name}, {optimizer_name})"
|
|
749
|
+
)
|
|
750
|
+
plt.grid(True, alpha=0.3)
|
|
751
|
+
plt.tight_layout()
|
|
752
|
+
|
|
753
|
+
if show:
|
|
754
|
+
plt.show()
|
|
755
|
+
|
|
756
|
+
mol_norm = format_molecule_name(molecule)
|
|
757
|
+
fname = build_filename(
|
|
758
|
+
molecule=mol_norm,
|
|
759
|
+
topic="vqe_geometry_scan",
|
|
760
|
+
extras={
|
|
761
|
+
"ans": ansatz_name,
|
|
762
|
+
"opt": optimizer_name,
|
|
763
|
+
"param": param_name,
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
save_plot(fname)
|
|
767
|
+
|
|
768
|
+
min_idx = int(np.argmin(means))
|
|
769
|
+
print(
|
|
770
|
+
f"Minimum energy: {means[min_idx]:.6f} ± {stds[min_idx]:.6f} "
|
|
771
|
+
f"at {param_name}={params[min_idx]:.3f}"
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
return results
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# ================================================================
|
|
778
|
+
# MAPPING COMPARISON
|
|
779
|
+
# ================================================================
|
|
780
|
+
def run_vqe_mapping_comparison(
|
|
781
|
+
molecule="H2",
|
|
782
|
+
ansatz_name="UCCSD",
|
|
783
|
+
optimizer_name="Adam",
|
|
784
|
+
mappings=None,
|
|
785
|
+
steps=50,
|
|
786
|
+
stepsize=0.2,
|
|
787
|
+
noisy=False,
|
|
788
|
+
depolarizing_prob=0.0,
|
|
789
|
+
amplitude_damping_prob=0.0,
|
|
790
|
+
force=False,
|
|
791
|
+
show=True,
|
|
792
|
+
mapping_kwargs=None,
|
|
793
|
+
):
|
|
794
|
+
"""
|
|
795
|
+
Compare different fermion-to-qubit mappings by:
|
|
796
|
+
|
|
797
|
+
- Building qubit Hamiltonians via build_hamiltonian
|
|
798
|
+
- Running VQE (re-using caching) via run_vqe for each mapping
|
|
799
|
+
- Plotting energy convergence curves and printing summary
|
|
800
|
+
|
|
801
|
+
Parameters
|
|
802
|
+
----------
|
|
803
|
+
show : bool
|
|
804
|
+
Whether to display the generated plot.
|
|
805
|
+
|
|
806
|
+
Returns
|
|
807
|
+
-------
|
|
808
|
+
dict
|
|
809
|
+
{
|
|
810
|
+
mapping_name: {
|
|
811
|
+
"final_energy": float,
|
|
812
|
+
"energies": [...],
|
|
813
|
+
"num_qubits": int,
|
|
814
|
+
"num_terms": int or None,
|
|
815
|
+
},
|
|
816
|
+
...
|
|
817
|
+
}
|
|
818
|
+
"""
|
|
819
|
+
import matplotlib.pyplot as plt
|
|
820
|
+
from vqe_qpe_common.plotting import build_filename, save_plot
|
|
821
|
+
|
|
822
|
+
mappings = mappings or ["jordan_wigner", "bravyi_kitaev", "parity"]
|
|
823
|
+
results = {}
|
|
824
|
+
|
|
825
|
+
print(f"\n🔍 Comparing mappings for {molecule} ({ansatz_name}, {optimizer_name})")
|
|
826
|
+
|
|
827
|
+
for mapping in mappings:
|
|
828
|
+
print(f"\n⚙️ Running mapping: {mapping}")
|
|
829
|
+
|
|
830
|
+
# Build Hamiltonian once to inspect complexity
|
|
831
|
+
H, qubits, symbols, coordinates, basis = build_hamiltonian(
|
|
832
|
+
molecule, mapping=mapping
|
|
833
|
+
)
|
|
834
|
+
basis = basis.lower()
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
num_terms = len(H.ops)
|
|
838
|
+
except AttributeError:
|
|
839
|
+
try:
|
|
840
|
+
num_terms = len(H.terms()[0]) if callable(H.terms) else len(H.data)
|
|
841
|
+
except Exception:
|
|
842
|
+
num_terms = (
|
|
843
|
+
len(getattr(H, "data", [])) if hasattr(H, "data") else None
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
# Run VQE using the high-level entrypoint (handles ansatz + noise plumbing)
|
|
847
|
+
res = run_vqe(
|
|
848
|
+
molecule=molecule,
|
|
849
|
+
ansatz_name=ansatz_name,
|
|
850
|
+
optimizer_name=optimizer_name,
|
|
851
|
+
n_steps=steps,
|
|
852
|
+
stepsize=stepsize,
|
|
853
|
+
noisy=noisy,
|
|
854
|
+
depolarizing_prob=depolarizing_prob,
|
|
855
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
856
|
+
mapping=mapping,
|
|
857
|
+
force=force,
|
|
858
|
+
plot=False,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
results[mapping] = {
|
|
862
|
+
"final_energy": res["energy"],
|
|
863
|
+
"energies": res["energies"],
|
|
864
|
+
"num_qubits": qubits,
|
|
865
|
+
"num_terms": num_terms,
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
# --- Plot mappings ---
|
|
869
|
+
plt.figure(figsize=(8, 5))
|
|
870
|
+
for mapping in mappings:
|
|
871
|
+
data = results[mapping]
|
|
872
|
+
label = mapping.replace("_", "-").title()
|
|
873
|
+
plt.plot(
|
|
874
|
+
range(len(data["energies"])),
|
|
875
|
+
data["energies"],
|
|
876
|
+
label=label,
|
|
877
|
+
linewidth=2,
|
|
878
|
+
alpha=0.9,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
plt.xlabel("Iteration")
|
|
882
|
+
plt.ylabel("Energy (Ha)")
|
|
883
|
+
plt.title(f"{molecule} VQE: Energy Convergence by Mapping ({ansatz_name})")
|
|
884
|
+
plt.legend(frameon=False, fontsize=10)
|
|
885
|
+
plt.grid(True, alpha=0.3)
|
|
886
|
+
plt.tight_layout(pad=2)
|
|
887
|
+
|
|
888
|
+
if show:
|
|
889
|
+
plt.show()
|
|
890
|
+
|
|
891
|
+
fname = build_filename(
|
|
892
|
+
molecule=molecule,
|
|
893
|
+
topic="mapping_comparison",
|
|
894
|
+
extras={"ansatz": ansatz_name, "opt": optimizer_name},
|
|
895
|
+
)
|
|
896
|
+
save_plot(fname)
|
|
897
|
+
|
|
898
|
+
print(
|
|
899
|
+
f"\n📉 Saved mapping comparison plot to {IMG_DIR}/{fname}\nResults Summary:"
|
|
900
|
+
)
|
|
901
|
+
for mapping, data in results.items():
|
|
902
|
+
print(
|
|
903
|
+
f" {mapping:15s} → E = {data['final_energy']:.8f} Ha, "
|
|
904
|
+
f"Qubits = {data['num_qubits']}, Terms = {data['num_terms']}"
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
return results
|