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