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