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
qpe/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qpe
|
|
3
|
+
===
|
|
4
|
+
Quantum Phase Estimation (QPE) module of the VQE/QPE PennyLane simulation suite.
|
|
5
|
+
|
|
6
|
+
This subpackage provides:
|
|
7
|
+
• Unified Hamiltonian construction for molecules
|
|
8
|
+
• Noiseless and noisy Quantum Phase Estimation (QPE)
|
|
9
|
+
• Probability distribution & sweep plotting (unified with VQE)
|
|
10
|
+
• JSON-based caching and reproducible run signatures
|
|
11
|
+
• Noise channels and controlled time evolution utilities
|
|
12
|
+
|
|
13
|
+
Primary user-facing API:
|
|
14
|
+
- run_qpe()
|
|
15
|
+
- plot_qpe_distribution()
|
|
16
|
+
- plot_qpe_sweep()
|
|
17
|
+
- save_qpe_result()
|
|
18
|
+
- load_qpe_result()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.2.0"
|
|
22
|
+
__docformat__ = "restructuredtext"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------
|
|
26
|
+
# Public API Imports
|
|
27
|
+
# ---------------------------------------------------------------------
|
|
28
|
+
from .hamiltonian import ( # noqa: F401
|
|
29
|
+
build_hamiltonian,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .core import ( # noqa: F401
|
|
33
|
+
run_qpe,
|
|
34
|
+
bitstring_to_phase,
|
|
35
|
+
phase_to_energy_unwrapped,
|
|
36
|
+
hartree_fock_energy,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from .visualize import ( # noqa: F401
|
|
40
|
+
plot_qpe_distribution,
|
|
41
|
+
plot_qpe_sweep,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from .io_utils import ( # noqa: F401
|
|
45
|
+
save_qpe_result,
|
|
46
|
+
load_qpe_result,
|
|
47
|
+
signature_hash,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from .noise import apply_noise_all # noqa: F401
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------
|
|
54
|
+
# Public API Surface
|
|
55
|
+
# ---------------------------------------------------------------------
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Hamiltonian
|
|
58
|
+
"build_hamiltonian",
|
|
59
|
+
|
|
60
|
+
# Core QPE
|
|
61
|
+
"run_qpe",
|
|
62
|
+
"bitstring_to_phase",
|
|
63
|
+
"phase_to_energy_unwrapped",
|
|
64
|
+
"hartree_fock_energy",
|
|
65
|
+
|
|
66
|
+
# Visualization
|
|
67
|
+
"plot_qpe_distribution",
|
|
68
|
+
"plot_qpe_sweep",
|
|
69
|
+
|
|
70
|
+
# I/O + Caching
|
|
71
|
+
"save_qpe_result",
|
|
72
|
+
"load_qpe_result",
|
|
73
|
+
"signature_hash",
|
|
74
|
+
|
|
75
|
+
# Noise
|
|
76
|
+
"apply_noise_all",
|
|
77
|
+
]
|
qpe/__main__.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qpe.__main__
|
|
3
|
+
============
|
|
4
|
+
Command-line interface for Quantum Phase Estimation (QPE).
|
|
5
|
+
|
|
6
|
+
This CLI mirrors the modern structure of the VQE CLI:
|
|
7
|
+
• clean argument parsing
|
|
8
|
+
• shared plotting conventions (common.plotting)
|
|
9
|
+
• cached result loading
|
|
10
|
+
• separation of concerns (no logic mixed with plotting or circuit code)
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
python -m qpe --molecule H2 --ancillas 4 --t 1.0 --shots 2000
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import argparse
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
import pennylane as qml
|
|
21
|
+
from pennylane import numpy as np
|
|
22
|
+
from pennylane import qchem
|
|
23
|
+
|
|
24
|
+
from qpe.hamiltonian import build_hamiltonian
|
|
25
|
+
from qpe.core import run_qpe
|
|
26
|
+
from qpe.io_utils import (
|
|
27
|
+
save_qpe_result,
|
|
28
|
+
load_qpe_result,
|
|
29
|
+
signature_hash,
|
|
30
|
+
ensure_dirs,
|
|
31
|
+
)
|
|
32
|
+
from qpe.visualize import plot_qpe_distribution
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------
|
|
36
|
+
# Molecule presets (simple & script-friendly)
|
|
37
|
+
# ---------------------------------------------------------------------
|
|
38
|
+
MOLECULES = {
|
|
39
|
+
"H2": {
|
|
40
|
+
"symbols": ["H", "H"],
|
|
41
|
+
"coordinates": np.array([[0.0, 0.0, 0.0],
|
|
42
|
+
[0.0, 0.0, 0.7414]]),
|
|
43
|
+
"charge": 0,
|
|
44
|
+
"basis": "STO-3G",
|
|
45
|
+
},
|
|
46
|
+
"LiH": {
|
|
47
|
+
"symbols": ["Li", "H"],
|
|
48
|
+
"coordinates": np.array([[0.0, 0.0, 0.0],
|
|
49
|
+
[0.0, 0.0, 1.6]]),
|
|
50
|
+
"charge": 0,
|
|
51
|
+
"basis": "STO-3G",
|
|
52
|
+
},
|
|
53
|
+
"H2O": {
|
|
54
|
+
"symbols": ["O", "H", "H"],
|
|
55
|
+
"coordinates": np.array([
|
|
56
|
+
[0.000000, 0.000000, 0.000000],
|
|
57
|
+
[0.758602, 0.000000, 0.504284],
|
|
58
|
+
[-0.758602, 0.000000, 0.504284],
|
|
59
|
+
]),
|
|
60
|
+
"charge": 0,
|
|
61
|
+
"basis": "STO-3G",
|
|
62
|
+
},
|
|
63
|
+
"H3+": {
|
|
64
|
+
"symbols": ["H", "H", "H"],
|
|
65
|
+
"coordinates": np.array([
|
|
66
|
+
[0.0, 0.0, 0.0],
|
|
67
|
+
[0.0, 0.0, 0.872],
|
|
68
|
+
[0.755, 0.0, 0.436],
|
|
69
|
+
]),
|
|
70
|
+
"charge": +1,
|
|
71
|
+
"basis": "STO-3G",
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Minimal atomic number table for electron counting
|
|
76
|
+
Z = {"H": 1, "Li": 3, "O": 8}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def infer_electrons(symbols, charge: int) -> int:
|
|
80
|
+
"""Infer number of electrons for a molecule from symbols and charge."""
|
|
81
|
+
return int(sum(Z[s] for s in symbols) - charge)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------
|
|
85
|
+
# Arguments
|
|
86
|
+
# ---------------------------------------------------------------------
|
|
87
|
+
def parse_args():
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
description="Quantum Phase Estimation (QPE) simulator",
|
|
90
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"-m", "--molecule",
|
|
95
|
+
required=True,
|
|
96
|
+
choices=MOLECULES.keys(),
|
|
97
|
+
help="Molecule to simulate",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--ancillas", type=int, default=4,
|
|
102
|
+
help="Number of ancilla qubits",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--t", type=float, default=1.0,
|
|
107
|
+
help="Evolution time in exp(-iHt)",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--trotter-steps", type=int, default=2,
|
|
112
|
+
help="Trotter steps for time evolution",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--shots", type=int, default=2000,
|
|
117
|
+
help="Number of measurement shots",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Noise model
|
|
121
|
+
parser.add_argument("--noisy", action="store_true",
|
|
122
|
+
help="Enable noise model")
|
|
123
|
+
parser.add_argument("--p-dep", type=float, default=0.0,
|
|
124
|
+
help="Depolarizing probability")
|
|
125
|
+
parser.add_argument("--p-amp", type=float, default=0.0,
|
|
126
|
+
help="Amplitude damping probability")
|
|
127
|
+
|
|
128
|
+
# Plotting
|
|
129
|
+
parser.add_argument("--plot", action="store_true",
|
|
130
|
+
help="Show plot after simulation")
|
|
131
|
+
parser.add_argument("--save-plot", action="store_true",
|
|
132
|
+
help="Save QPE probability distribution")
|
|
133
|
+
|
|
134
|
+
parser.add_argument("--force", action="store_true",
|
|
135
|
+
help="Force rerun even if cached result exists")
|
|
136
|
+
|
|
137
|
+
return parser.parse_args()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------
|
|
141
|
+
# Main logic
|
|
142
|
+
# ---------------------------------------------------------------------
|
|
143
|
+
def main():
|
|
144
|
+
args = parse_args()
|
|
145
|
+
ensure_dirs()
|
|
146
|
+
|
|
147
|
+
cfg = MOLECULES[args.molecule]
|
|
148
|
+
|
|
149
|
+
print(f"\n🧮 QPE Simulation")
|
|
150
|
+
print(f"• Molecule: {args.molecule}")
|
|
151
|
+
print(f"• Ancillas: {args.ancillas}")
|
|
152
|
+
print(f"• Shots: {args.shots}")
|
|
153
|
+
print(f"• t: {args.t}")
|
|
154
|
+
print(f"• Trotter: {args.trotter_steps}")
|
|
155
|
+
|
|
156
|
+
# Noise summary
|
|
157
|
+
noise_params = None
|
|
158
|
+
if args.noisy:
|
|
159
|
+
noise_params = {
|
|
160
|
+
"p_dep": args.p_dep,
|
|
161
|
+
"p_amp": args.p_amp,
|
|
162
|
+
}
|
|
163
|
+
print(f"• Noise: dep={args.p_dep}, amp={args.p_amp}")
|
|
164
|
+
else:
|
|
165
|
+
print("• Noise: OFF")
|
|
166
|
+
|
|
167
|
+
# Hamiltonian + HF state
|
|
168
|
+
symbols = cfg["symbols"]
|
|
169
|
+
coords = cfg["coordinates"]
|
|
170
|
+
charge = cfg["charge"]
|
|
171
|
+
basis = cfg["basis"]
|
|
172
|
+
|
|
173
|
+
start_time = time.time()
|
|
174
|
+
H, n_qubits = build_hamiltonian(symbols, coords, charge, basis)
|
|
175
|
+
|
|
176
|
+
electrons = infer_electrons(symbols, charge)
|
|
177
|
+
hf_state = qchem.hf_state(electrons, n_qubits)
|
|
178
|
+
|
|
179
|
+
# Caching
|
|
180
|
+
sig = signature_hash(
|
|
181
|
+
molecule=args.molecule,
|
|
182
|
+
n_ancilla=args.ancillas,
|
|
183
|
+
t=args.t,
|
|
184
|
+
noise=bool(noise_params),
|
|
185
|
+
shots=args.shots,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
cached = None if args.force else load_qpe_result(args.molecule, sig)
|
|
189
|
+
|
|
190
|
+
if cached is not None:
|
|
191
|
+
print("\n📂 Loaded cached result.")
|
|
192
|
+
result = cached
|
|
193
|
+
else:
|
|
194
|
+
print("\n▶️ Running new QPE simulation...")
|
|
195
|
+
result = run_qpe(
|
|
196
|
+
hamiltonian=H,
|
|
197
|
+
hf_state=hf_state,
|
|
198
|
+
n_ancilla=args.ancillas,
|
|
199
|
+
t=args.t,
|
|
200
|
+
trotter_steps=args.trotter_steps,
|
|
201
|
+
noise_params=noise_params,
|
|
202
|
+
shots=args.shots,
|
|
203
|
+
molecule_name=args.molecule,
|
|
204
|
+
)
|
|
205
|
+
save_qpe_result(result)
|
|
206
|
+
|
|
207
|
+
elapsed = time.time() - start_time
|
|
208
|
+
|
|
209
|
+
# Summary
|
|
210
|
+
print("\n✅ QPE completed.")
|
|
211
|
+
print(f"Most probable state : {result['best_bitstring']}")
|
|
212
|
+
print(f"Estimated energy : {result['energy']:.8f} Ha")
|
|
213
|
+
print(f"Hartree–Fock energy : {result['hf_energy']:.8f} Ha")
|
|
214
|
+
print(f"ΔE (QPE − HF) : {result['energy'] - result['hf_energy']:+.8f} Ha")
|
|
215
|
+
print(f"⏱ Elapsed : {elapsed:.2f}s")
|
|
216
|
+
print(f"Total qubits : system={n_qubits}, ancillas={args.ancillas}")
|
|
217
|
+
|
|
218
|
+
# Plot
|
|
219
|
+
if args.plot or args.save_plot:
|
|
220
|
+
plot_qpe_distribution(
|
|
221
|
+
result,
|
|
222
|
+
show=args.plot,
|
|
223
|
+
save=args.save_plot,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
try:
|
|
229
|
+
main()
|
|
230
|
+
except KeyboardInterrupt:
|
|
231
|
+
print("\n⏹ QPE simulation interrupted.")
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"\n❌ Error: {e}\n")
|
qpe/core.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qpe.core
|
|
3
|
+
========
|
|
4
|
+
Core Quantum Phase Estimation (QPE) implementation.
|
|
5
|
+
|
|
6
|
+
This module is deliberately focused on:
|
|
7
|
+
• Circuit construction (QPE, with optional noise)
|
|
8
|
+
• Classical post-processing (bitstrings → phases → energies)
|
|
9
|
+
|
|
10
|
+
It does **not**:
|
|
11
|
+
• Build Hamiltonians (see common.hamiltonian)
|
|
12
|
+
• Deal with filenames or JSON I/O (see qpe.io_utils)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections import Counter
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
import pennylane as qml
|
|
21
|
+
from pennylane import numpy as np
|
|
22
|
+
|
|
23
|
+
from qpe.noise import apply_noise_all
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------
|
|
27
|
+
# Inverse Quantum Fourier Transform
|
|
28
|
+
# ---------------------------------------------------------------------
|
|
29
|
+
def inverse_qft(wires: list[int]) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Apply the inverse Quantum Fourier Transform (QFT) on a list of wires.
|
|
32
|
+
|
|
33
|
+
The input is assumed to be ordered [a_0, a_1, ..., a_{n-1}]
|
|
34
|
+
with a_0 the most-significant ancilla.
|
|
35
|
+
"""
|
|
36
|
+
n = len(wires)
|
|
37
|
+
|
|
38
|
+
# Mirror ordering
|
|
39
|
+
for i in range(n // 2):
|
|
40
|
+
qml.SWAP(wires=[wires[i], wires[n - i - 1]])
|
|
41
|
+
|
|
42
|
+
# Controlled phase ladder + Hadamards
|
|
43
|
+
for j in range(n):
|
|
44
|
+
k = n - j - 1
|
|
45
|
+
qml.Hadamard(wires=k)
|
|
46
|
+
for m in range(k):
|
|
47
|
+
angle = -np.pi / (2 ** (k - m))
|
|
48
|
+
qml.ControlledPhaseShift(angle, wires=[wires[m], wires[k]])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------
|
|
52
|
+
# Controlled powered evolution U^(2^power)
|
|
53
|
+
# ---------------------------------------------------------------------
|
|
54
|
+
def controlled_powered_evolution(
|
|
55
|
+
hamiltonian: qml.Hamiltonian,
|
|
56
|
+
system_wires: list[int],
|
|
57
|
+
control_wire: int,
|
|
58
|
+
t: float,
|
|
59
|
+
power: int,
|
|
60
|
+
trotter_steps: int = 1,
|
|
61
|
+
noise_params: Optional[Dict[str, float]] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Apply controlled-U^(2^power) = controlled exp(-i H t 2^power).
|
|
65
|
+
|
|
66
|
+
Uses ApproxTimeEvolution in PennyLane, with optional noise applied
|
|
67
|
+
after each controlled segment.
|
|
68
|
+
|
|
69
|
+
Args
|
|
70
|
+
----
|
|
71
|
+
hamiltonian:
|
|
72
|
+
Molecular Hamiltonian acting on the *system* wires.
|
|
73
|
+
(Already mapped onto system_wires.)
|
|
74
|
+
system_wires:
|
|
75
|
+
Wires of the system register.
|
|
76
|
+
control_wire:
|
|
77
|
+
Ancilla controlling the evolution.
|
|
78
|
+
t:
|
|
79
|
+
Base evolution time in exp(-i H t).
|
|
80
|
+
power:
|
|
81
|
+
Exponent; this block implements U^(2^power).
|
|
82
|
+
trotter_steps:
|
|
83
|
+
Number of Trotter steps per exp(-i H t).
|
|
84
|
+
noise_params:
|
|
85
|
+
Optional dict {"p_dep": float, "p_amp": float}.
|
|
86
|
+
"""
|
|
87
|
+
n_repeat = 2**power
|
|
88
|
+
|
|
89
|
+
for _ in range(n_repeat):
|
|
90
|
+
# Controlled ApproxTimeEvolution
|
|
91
|
+
qml.ctrl(qml.ApproxTimeEvolution, control=control_wire)(
|
|
92
|
+
hamiltonian, t, trotter_steps
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Optional noise on all active wires
|
|
96
|
+
if noise_params:
|
|
97
|
+
apply_noise_all(
|
|
98
|
+
wires=system_wires + [control_wire],
|
|
99
|
+
p_dep=noise_params.get("p_dep", 0.0),
|
|
100
|
+
p_amp=noise_params.get("p_amp", 0.0),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------
|
|
105
|
+
# Hartree–Fock Reference Energy
|
|
106
|
+
# ---------------------------------------------------------------------
|
|
107
|
+
def hartree_fock_energy(hamiltonian: qml.Hamiltonian, hf_state: np.ndarray) -> float:
|
|
108
|
+
"""Compute ⟨HF|H|HF⟩ in Hartree."""
|
|
109
|
+
num_qubits = len(hf_state)
|
|
110
|
+
dev = qml.device("default.qubit", wires=num_qubits)
|
|
111
|
+
|
|
112
|
+
@qml.qnode(dev)
|
|
113
|
+
def circuit():
|
|
114
|
+
qml.BasisState(hf_state, wires=range(num_qubits))
|
|
115
|
+
return qml.expval(hamiltonian)
|
|
116
|
+
|
|
117
|
+
return float(circuit())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------
|
|
121
|
+
# Phase / Energy utilities
|
|
122
|
+
# ---------------------------------------------------------------------
|
|
123
|
+
def bitstring_to_phase(bits: str, msb_first: bool = True) -> float:
|
|
124
|
+
"""
|
|
125
|
+
Convert bitstring → fractional phase in [0, 1).
|
|
126
|
+
|
|
127
|
+
Args
|
|
128
|
+
----
|
|
129
|
+
bits:
|
|
130
|
+
String of "0"/"1", e.g. "0110".
|
|
131
|
+
msb_first:
|
|
132
|
+
If False, interpret the string as LSB-first.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
float
|
|
137
|
+
Phase in [0, 1).
|
|
138
|
+
"""
|
|
139
|
+
b = bits if msb_first else bits[::-1]
|
|
140
|
+
return float(sum((ch == "1") * (0.5**i) for i, ch in enumerate(b, start=1)))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def phase_to_energy_unwrapped(
|
|
144
|
+
phase: float,
|
|
145
|
+
t: float,
|
|
146
|
+
ref_energy: Optional[float] = None,
|
|
147
|
+
) -> float:
|
|
148
|
+
"""
|
|
149
|
+
Convert a phase in [0, 1) into an energy, unwrapped around a reference.
|
|
150
|
+
|
|
151
|
+
The base relation is:
|
|
152
|
+
E ≈ -2π * phase / t (mod 2π / t)
|
|
153
|
+
|
|
154
|
+
We first wrap E into (-π/t, π/t], then (if ref_energy is given) shift
|
|
155
|
+
by ± 2π/t to choose the branch closest to ref_energy.
|
|
156
|
+
"""
|
|
157
|
+
base = -2 * np.pi * phase / t
|
|
158
|
+
|
|
159
|
+
# Wrap into (-π/t, π/t]
|
|
160
|
+
while base > np.pi / t:
|
|
161
|
+
base -= 2 * np.pi / t
|
|
162
|
+
while base <= -np.pi / t:
|
|
163
|
+
base += 2 * np.pi / t
|
|
164
|
+
|
|
165
|
+
if ref_energy is not None:
|
|
166
|
+
spaced = 2 * np.pi / t
|
|
167
|
+
candidates = [base + k * spaced for k in (-1, 0, 1)]
|
|
168
|
+
base = min(candidates, key=lambda x: abs(x - ref_energy))
|
|
169
|
+
|
|
170
|
+
return float(base)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------
|
|
174
|
+
# QPE main runner
|
|
175
|
+
# ---------------------------------------------------------------------
|
|
176
|
+
def run_qpe(
|
|
177
|
+
*,
|
|
178
|
+
hamiltonian: qml.Hamiltonian,
|
|
179
|
+
hf_state: np.ndarray,
|
|
180
|
+
n_ancilla: int = 4,
|
|
181
|
+
t: float = 1.0,
|
|
182
|
+
trotter_steps: int = 1,
|
|
183
|
+
noise_params: Optional[Dict[str, float]] = None,
|
|
184
|
+
shots: int = 5000,
|
|
185
|
+
molecule_name: str = "molecule",
|
|
186
|
+
) -> Dict[str, Any]:
|
|
187
|
+
"""
|
|
188
|
+
Run a (noisy or noiseless) Quantum Phase Estimation simulation.
|
|
189
|
+
|
|
190
|
+
This function is intentionally "pure":
|
|
191
|
+
• It returns a result dict
|
|
192
|
+
• It does not know about filenames or JSON paths
|
|
193
|
+
• Caching is handled by qpe.io_utils and the CLI / notebooks
|
|
194
|
+
|
|
195
|
+
Args
|
|
196
|
+
----
|
|
197
|
+
hamiltonian:
|
|
198
|
+
Molecular Hamiltonian acting on a system register of size N.
|
|
199
|
+
Its wires are assumed to be [0, 1, ..., N-1].
|
|
200
|
+
hf_state:
|
|
201
|
+
Hartree–Fock state as a 0/1 array of length N.
|
|
202
|
+
n_ancilla:
|
|
203
|
+
Number of ancilla qubits used for phase estimation.
|
|
204
|
+
t:
|
|
205
|
+
Evolution time in exp(-i H t).
|
|
206
|
+
trotter_steps:
|
|
207
|
+
Number of ApproxTimeEvolution Trotter steps.
|
|
208
|
+
noise_params:
|
|
209
|
+
Optional dict {"p_dep": float, "p_amp": float}. If None, run noiselessly.
|
|
210
|
+
shots:
|
|
211
|
+
Number of measurement samples.
|
|
212
|
+
molecule_name:
|
|
213
|
+
Label used in the result dictionary for downstream I/O.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
dict
|
|
218
|
+
{
|
|
219
|
+
"molecule": str,
|
|
220
|
+
"counts": dict[str, int],
|
|
221
|
+
"probs": dict[str, float],
|
|
222
|
+
"best_bitstring": str,
|
|
223
|
+
"phase": float,
|
|
224
|
+
"energy": float,
|
|
225
|
+
"hf_energy": float,
|
|
226
|
+
"n_ancilla": int,
|
|
227
|
+
"t": float,
|
|
228
|
+
"noise": dict,
|
|
229
|
+
"shots": int,
|
|
230
|
+
}
|
|
231
|
+
"""
|
|
232
|
+
num_qubits = len(hf_state)
|
|
233
|
+
|
|
234
|
+
ancilla_wires = list(range(n_ancilla))
|
|
235
|
+
system_wires = list(range(n_ancilla, n_ancilla + num_qubits))
|
|
236
|
+
|
|
237
|
+
dev_name = "default.mixed" if noise_params else "default.qubit"
|
|
238
|
+
dev = qml.device(dev_name, wires=n_ancilla + num_qubits, shots=shots)
|
|
239
|
+
|
|
240
|
+
# Remap Hamiltonian wires to system register indices
|
|
241
|
+
wire_map = {i: system_wires[i] for i in range(num_qubits)}
|
|
242
|
+
H_sys = hamiltonian.map_wires(wire_map)
|
|
243
|
+
|
|
244
|
+
@qml.qnode(dev)
|
|
245
|
+
def circuit():
|
|
246
|
+
# Prepare HF state on system register
|
|
247
|
+
qml.BasisState(np.array(hf_state, dtype=int), wires=system_wires)
|
|
248
|
+
|
|
249
|
+
# Hadamards on ancilla register
|
|
250
|
+
for a in ancilla_wires:
|
|
251
|
+
qml.Hadamard(wires=a)
|
|
252
|
+
|
|
253
|
+
# Controlled-U ladder: U^(2^{n-1}), U^(2^{n-2}), ..., U
|
|
254
|
+
for k, a in enumerate(ancilla_wires):
|
|
255
|
+
power = n_ancilla - 1 - k
|
|
256
|
+
controlled_powered_evolution(
|
|
257
|
+
hamiltonian=H_sys,
|
|
258
|
+
system_wires=system_wires,
|
|
259
|
+
control_wire=a,
|
|
260
|
+
t=t,
|
|
261
|
+
power=power,
|
|
262
|
+
trotter_steps=trotter_steps,
|
|
263
|
+
noise_params=noise_params,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Inverse QFT on ancillas
|
|
267
|
+
inverse_qft(ancilla_wires)
|
|
268
|
+
|
|
269
|
+
return qml.sample(wires=ancilla_wires)
|
|
270
|
+
|
|
271
|
+
# Run circuit
|
|
272
|
+
samples = np.array(circuit(), dtype=int)
|
|
273
|
+
samples = np.atleast_2d(samples)
|
|
274
|
+
|
|
275
|
+
bitstrings = ["".join(str(int(b)) for b in s) for s in samples]
|
|
276
|
+
counts = dict(Counter(bitstrings))
|
|
277
|
+
probs = {b: c / shots for b, c in counts.items()}
|
|
278
|
+
|
|
279
|
+
# HF reference energy
|
|
280
|
+
E_hf = hartree_fock_energy(hamiltonian, hf_state)
|
|
281
|
+
|
|
282
|
+
# Decode phases + candidate energies
|
|
283
|
+
rows = []
|
|
284
|
+
for b, c in counts.items():
|
|
285
|
+
ph_m = bitstring_to_phase(b, msb_first=True)
|
|
286
|
+
ph_l = bitstring_to_phase(b, msb_first=False)
|
|
287
|
+
e_m = phase_to_energy_unwrapped(ph_m, t, ref_energy=E_hf)
|
|
288
|
+
e_l = phase_to_energy_unwrapped(ph_l, t, ref_energy=E_hf)
|
|
289
|
+
rows.append((b, c, ph_m, ph_l, e_m, e_l))
|
|
290
|
+
|
|
291
|
+
if not rows:
|
|
292
|
+
raise RuntimeError("QPE returned no measurement outcomes.")
|
|
293
|
+
|
|
294
|
+
# Most likely observation
|
|
295
|
+
best_row = max(rows, key=lambda r: r[1])
|
|
296
|
+
best_b = best_row[0]
|
|
297
|
+
|
|
298
|
+
# Choose energy estimate closest to HF reference
|
|
299
|
+
candidate_Es = (best_row[4], best_row[5])
|
|
300
|
+
best_energy = min(candidate_Es, key=lambda x: abs(x - E_hf))
|
|
301
|
+
best_phase = best_row[2] if best_energy == best_row[4] else best_row[3]
|
|
302
|
+
|
|
303
|
+
result: Dict[str, Any] = {
|
|
304
|
+
"molecule": molecule_name,
|
|
305
|
+
"counts": counts,
|
|
306
|
+
"probs": probs,
|
|
307
|
+
"best_bitstring": best_b,
|
|
308
|
+
"phase": float(best_phase),
|
|
309
|
+
"energy": float(best_energy),
|
|
310
|
+
"hf_energy": float(E_hf),
|
|
311
|
+
"n_ancilla": int(n_ancilla),
|
|
312
|
+
"t": float(t),
|
|
313
|
+
"noise": dict(noise_params or {}),
|
|
314
|
+
"shots": int(shots),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result
|