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/engine.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vqe.engine
|
|
3
|
+
----------
|
|
4
|
+
Core plumbing layer for VQE and SSVQE routines.
|
|
5
|
+
|
|
6
|
+
Responsibilities
|
|
7
|
+
----------------
|
|
8
|
+
- Device creation and optional noise insertion
|
|
9
|
+
- Ansatz construction and parameter initialisation
|
|
10
|
+
- Optimizer creation
|
|
11
|
+
- QNode builders for:
|
|
12
|
+
* energy expectation values
|
|
13
|
+
* final states (statevector or density matrix)
|
|
14
|
+
* overlap/fidelity-style quantities
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
from typing import Callable, Iterable, Optional
|
|
21
|
+
|
|
22
|
+
import pennylane as qml
|
|
23
|
+
from pennylane import numpy as np
|
|
24
|
+
|
|
25
|
+
from .ansatz import get_ansatz, init_params
|
|
26
|
+
from .optimizer import get_optimizer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ======================================================================
|
|
30
|
+
# DEVICE & NOISE HANDLING
|
|
31
|
+
# ======================================================================
|
|
32
|
+
def make_device(num_wires: int, noisy: bool = False):
|
|
33
|
+
"""
|
|
34
|
+
Construct a PennyLane device.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
num_wires
|
|
39
|
+
Number of qubits.
|
|
40
|
+
noisy
|
|
41
|
+
If True, use a mixed-state simulator (`default.mixed`);
|
|
42
|
+
otherwise use a statevector simulator (`default.qubit`).
|
|
43
|
+
"""
|
|
44
|
+
dev_name = "default.mixed" if noisy else "default.qubit"
|
|
45
|
+
return qml.device(dev_name, wires=num_wires)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def apply_optional_noise(
|
|
49
|
+
noisy: bool,
|
|
50
|
+
depolarizing_prob: float,
|
|
51
|
+
amplitude_damping_prob: float,
|
|
52
|
+
num_wires: int,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Apply optional noise channels to each qubit after the ansatz.
|
|
56
|
+
|
|
57
|
+
Intended to be called from inside a QNode *after* the variational circuit.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
noisy
|
|
62
|
+
Whether noise is enabled.
|
|
63
|
+
depolarizing_prob
|
|
64
|
+
Probability for DepolarizingChannel.
|
|
65
|
+
amplitude_damping_prob
|
|
66
|
+
Probability for AmplitudeDamping.
|
|
67
|
+
num_wires
|
|
68
|
+
Number of qubits.
|
|
69
|
+
"""
|
|
70
|
+
if not noisy:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
for w in range(num_wires):
|
|
74
|
+
if depolarizing_prob > 0.0:
|
|
75
|
+
qml.DepolarizingChannel(depolarizing_prob, wires=w)
|
|
76
|
+
if amplitude_damping_prob > 0.0:
|
|
77
|
+
qml.AmplitudeDamping(amplitude_damping_prob, wires=w)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ======================================================================
|
|
81
|
+
# ANSATZ CONSTRUCTION
|
|
82
|
+
# ======================================================================
|
|
83
|
+
|
|
84
|
+
# Cache which keyword arguments each ansatz function supports so we
|
|
85
|
+
# don't repeatedly call inspect.signature inside QNodes.
|
|
86
|
+
_ANSATZ_KWARG_CACHE: dict[Callable, set[str]] = {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _supported_ansatz_kwargs(ansatz_fn: Callable) -> set[str]:
|
|
90
|
+
"""Return the set of supported keyword argument names for an ansatz."""
|
|
91
|
+
if ansatz_fn in _ANSATZ_KWARG_CACHE:
|
|
92
|
+
return _ANSATZ_KWARG_CACHE[ansatz_fn]
|
|
93
|
+
|
|
94
|
+
sig = inspect.signature(ansatz_fn).parameters
|
|
95
|
+
supported = {name for name, p in sig.items() if p.kind in (p.KEYWORD_ONLY, p.POSITIONAL_OR_KEYWORD)}
|
|
96
|
+
_ANSATZ_KWARG_CACHE[ansatz_fn] = supported
|
|
97
|
+
return supported
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _call_ansatz(
|
|
101
|
+
ansatz_fn: Callable,
|
|
102
|
+
params,
|
|
103
|
+
wires: Iterable[int],
|
|
104
|
+
symbols=None,
|
|
105
|
+
coordinates=None,
|
|
106
|
+
basis: Optional[str] = None,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Call an ansatz function, forwarding only the keyword arguments it supports.
|
|
110
|
+
|
|
111
|
+
This unifies toy ansatzes (expecting (params, wires)) and chemistry
|
|
112
|
+
ansatzes (which additionally accept symbols / coordinates / basis).
|
|
113
|
+
"""
|
|
114
|
+
wires = list(wires)
|
|
115
|
+
supported = _supported_ansatz_kwargs(ansatz_fn)
|
|
116
|
+
|
|
117
|
+
kwargs = {}
|
|
118
|
+
if "symbols" in supported:
|
|
119
|
+
kwargs["symbols"] = symbols
|
|
120
|
+
if "coordinates" in supported:
|
|
121
|
+
kwargs["coordinates"] = coordinates
|
|
122
|
+
if "basis" in supported and basis is not None:
|
|
123
|
+
kwargs["basis"] = basis
|
|
124
|
+
|
|
125
|
+
return ansatz_fn(params, wires=wires, **kwargs)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_ansatz(
|
|
129
|
+
ansatz_name: str,
|
|
130
|
+
num_wires: int,
|
|
131
|
+
*,
|
|
132
|
+
seed: int = 0,
|
|
133
|
+
symbols=None,
|
|
134
|
+
coordinates=None,
|
|
135
|
+
basis: str = "sto-3g",
|
|
136
|
+
requires_grad: bool = True,
|
|
137
|
+
scale: float = 0.01,
|
|
138
|
+
):
|
|
139
|
+
"""
|
|
140
|
+
Construct an ansatz function and matching initial parameter vector.
|
|
141
|
+
|
|
142
|
+
This is the main entry point used by higher-level routines.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
ansatz_name
|
|
147
|
+
Name of the ansatz in the registry (see vqe.ansatz.ANSATZES).
|
|
148
|
+
num_wires
|
|
149
|
+
Number of qubits.
|
|
150
|
+
seed
|
|
151
|
+
Random seed used for parameter initialisation.
|
|
152
|
+
symbols, coordinates, basis
|
|
153
|
+
Molecular data for chemistry-inspired ansatzes (UCC family).
|
|
154
|
+
requires_grad
|
|
155
|
+
Whether the parameters should be differentiable.
|
|
156
|
+
scale
|
|
157
|
+
Typical scale for random initialisation in toy ansatzes.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
(ansatz_fn, params)
|
|
162
|
+
ansatz_fn: Callable(params) -> circuit on given wires
|
|
163
|
+
params: numpy array of initial parameters
|
|
164
|
+
"""
|
|
165
|
+
ansatz_fn = get_ansatz(ansatz_name)
|
|
166
|
+
params = init_params(
|
|
167
|
+
ansatz_name=ansatz_name,
|
|
168
|
+
num_wires=num_wires,
|
|
169
|
+
scale=scale,
|
|
170
|
+
requires_grad=requires_grad,
|
|
171
|
+
symbols=symbols,
|
|
172
|
+
coordinates=coordinates,
|
|
173
|
+
basis=basis,
|
|
174
|
+
seed=seed,
|
|
175
|
+
)
|
|
176
|
+
return ansatz_fn, params
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ======================================================================
|
|
180
|
+
# OPTIMIZER BUILDER
|
|
181
|
+
# ======================================================================
|
|
182
|
+
def build_optimizer(optimizer_name: str, stepsize: float):
|
|
183
|
+
"""
|
|
184
|
+
Return a PennyLane optimizer instance by name.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
optimizer_name
|
|
189
|
+
Name understood by vqe.optimizer.get_optimizer.
|
|
190
|
+
stepsize
|
|
191
|
+
Learning rate for the optimizer.
|
|
192
|
+
"""
|
|
193
|
+
return get_optimizer(optimizer_name, stepsize=stepsize)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ======================================================================
|
|
197
|
+
# QNODE CONSTRUCTION
|
|
198
|
+
# ======================================================================
|
|
199
|
+
def _choose_diff_method(noisy: bool, diff_method: Optional[str]) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Decide which differentiation method to use for a QNode.
|
|
202
|
+
|
|
203
|
+
Default:
|
|
204
|
+
- parameter-shift when noiseless
|
|
205
|
+
- finite-diff when noisy
|
|
206
|
+
"""
|
|
207
|
+
if diff_method is not None:
|
|
208
|
+
return diff_method
|
|
209
|
+
return "finite-diff" if noisy else "parameter-shift"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def make_energy_qnode(
|
|
213
|
+
H,
|
|
214
|
+
dev,
|
|
215
|
+
ansatz_fn: Callable,
|
|
216
|
+
num_wires: int,
|
|
217
|
+
*,
|
|
218
|
+
noisy: bool = False,
|
|
219
|
+
depolarizing_prob: float = 0.0,
|
|
220
|
+
amplitude_damping_prob: float = 0.0,
|
|
221
|
+
symbols=None,
|
|
222
|
+
coordinates=None,
|
|
223
|
+
basis: str = "sto-3g",
|
|
224
|
+
diff_method: Optional[str] = None,
|
|
225
|
+
):
|
|
226
|
+
"""
|
|
227
|
+
Build a QNode that returns the energy expectation value ⟨H⟩.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
H
|
|
232
|
+
PennyLane Hamiltonian.
|
|
233
|
+
dev
|
|
234
|
+
PennyLane device.
|
|
235
|
+
ansatz_fn
|
|
236
|
+
Ansatz function from vqe.ansatz.
|
|
237
|
+
num_wires
|
|
238
|
+
Number of qubits.
|
|
239
|
+
noisy
|
|
240
|
+
Whether to insert noise channels after the ansatz.
|
|
241
|
+
depolarizing_prob, amplitude_damping_prob
|
|
242
|
+
Noise strengths.
|
|
243
|
+
symbols, coordinates, basis
|
|
244
|
+
Molecular data passed through to chemistry ansatzes.
|
|
245
|
+
diff_method
|
|
246
|
+
Optional override for the QNode differentiation method.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
energy(params) -> float
|
|
251
|
+
QNode that evaluates ⟨H⟩ at given parameters.
|
|
252
|
+
"""
|
|
253
|
+
diff_method = _choose_diff_method(noisy, diff_method)
|
|
254
|
+
|
|
255
|
+
@qml.qnode(dev, diff_method=diff_method)
|
|
256
|
+
def energy(params):
|
|
257
|
+
_call_ansatz(
|
|
258
|
+
ansatz_fn,
|
|
259
|
+
params,
|
|
260
|
+
wires=range(num_wires),
|
|
261
|
+
symbols=symbols,
|
|
262
|
+
coordinates=coordinates,
|
|
263
|
+
basis=basis,
|
|
264
|
+
)
|
|
265
|
+
apply_optional_noise(
|
|
266
|
+
noisy,
|
|
267
|
+
depolarizing_prob,
|
|
268
|
+
amplitude_damping_prob,
|
|
269
|
+
num_wires,
|
|
270
|
+
)
|
|
271
|
+
return qml.expval(H)
|
|
272
|
+
|
|
273
|
+
return energy
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def make_state_qnode(
|
|
277
|
+
dev,
|
|
278
|
+
ansatz_fn: Callable,
|
|
279
|
+
num_wires: int,
|
|
280
|
+
*,
|
|
281
|
+
noisy: bool = False,
|
|
282
|
+
depolarizing_prob: float = 0.0,
|
|
283
|
+
amplitude_damping_prob: float = 0.0,
|
|
284
|
+
symbols=None,
|
|
285
|
+
coordinates=None,
|
|
286
|
+
basis: str = "sto-3g",
|
|
287
|
+
diff_method: Optional[str] = None,
|
|
288
|
+
):
|
|
289
|
+
"""
|
|
290
|
+
Build a QNode that returns the final state for given parameters.
|
|
291
|
+
|
|
292
|
+
For noiseless devices (default.qubit) this returns a statevector.
|
|
293
|
+
For mixed-state devices (default.mixed) this returns a density matrix.
|
|
294
|
+
|
|
295
|
+
Returns
|
|
296
|
+
-------
|
|
297
|
+
state(params) -> np.ndarray
|
|
298
|
+
"""
|
|
299
|
+
diff_method = _choose_diff_method(noisy, diff_method)
|
|
300
|
+
|
|
301
|
+
@qml.qnode(dev, diff_method=diff_method)
|
|
302
|
+
def state(params):
|
|
303
|
+
_call_ansatz(
|
|
304
|
+
ansatz_fn,
|
|
305
|
+
params,
|
|
306
|
+
wires=range(num_wires),
|
|
307
|
+
symbols=symbols,
|
|
308
|
+
coordinates=coordinates,
|
|
309
|
+
basis=basis,
|
|
310
|
+
)
|
|
311
|
+
apply_optional_noise(
|
|
312
|
+
noisy,
|
|
313
|
+
depolarizing_prob,
|
|
314
|
+
amplitude_damping_prob,
|
|
315
|
+
num_wires,
|
|
316
|
+
)
|
|
317
|
+
return qml.state()
|
|
318
|
+
|
|
319
|
+
return state
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def make_overlap00_fn(
|
|
323
|
+
dev,
|
|
324
|
+
ansatz_fn: Callable,
|
|
325
|
+
num_wires: int,
|
|
326
|
+
*,
|
|
327
|
+
noisy: bool = False,
|
|
328
|
+
depolarizing_prob: float = 0.0,
|
|
329
|
+
amplitude_damping_prob: float = 0.0,
|
|
330
|
+
symbols=None,
|
|
331
|
+
coordinates=None,
|
|
332
|
+
basis: str = "sto-3g",
|
|
333
|
+
diff_method: Optional[str] = None,
|
|
334
|
+
):
|
|
335
|
+
"""
|
|
336
|
+
Construct a function overlap00(p_i, p_j) ≈ |⟨ψ_i|ψ_j⟩|².
|
|
337
|
+
|
|
338
|
+
Uses the "adjoint trick":
|
|
339
|
+
1. Prepare |ψ_i⟩ with ansatz(params=p_i)
|
|
340
|
+
2. Apply adjoint(ansatz)(params=p_j)
|
|
341
|
+
3. Measure probabilities; |⟨ψ_i|ψ_j⟩|² = Prob(|00...0⟩)
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
dev
|
|
346
|
+
PennyLane device.
|
|
347
|
+
ansatz_fn
|
|
348
|
+
Ansatz function.
|
|
349
|
+
num_wires
|
|
350
|
+
Number of qubits.
|
|
351
|
+
noisy, depolarizing_prob, amplitude_damping_prob
|
|
352
|
+
Noise control (applied both forward and adjoint).
|
|
353
|
+
symbols, coordinates, basis
|
|
354
|
+
Molecular data for the ansatz.
|
|
355
|
+
diff_method
|
|
356
|
+
Optional differentiation method override.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
overlap00(p_i, p_j) -> float
|
|
361
|
+
"""
|
|
362
|
+
diff_method = _choose_diff_method(noisy, diff_method)
|
|
363
|
+
|
|
364
|
+
def _apply(params):
|
|
365
|
+
_call_ansatz(
|
|
366
|
+
ansatz_fn,
|
|
367
|
+
params,
|
|
368
|
+
wires=range(num_wires),
|
|
369
|
+
symbols=symbols,
|
|
370
|
+
coordinates=coordinates,
|
|
371
|
+
basis=basis,
|
|
372
|
+
)
|
|
373
|
+
apply_optional_noise(
|
|
374
|
+
noisy,
|
|
375
|
+
depolarizing_prob,
|
|
376
|
+
amplitude_damping_prob,
|
|
377
|
+
num_wires,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@qml.qnode(dev, diff_method=diff_method)
|
|
381
|
+
def _overlap(p_i, p_j):
|
|
382
|
+
_apply(p_i)
|
|
383
|
+
qml.adjoint(_apply)(p_j)
|
|
384
|
+
return qml.probs(wires=range(num_wires))
|
|
385
|
+
|
|
386
|
+
def overlap00(p_i, p_j):
|
|
387
|
+
probs = _overlap(p_i, p_j)
|
|
388
|
+
return probs[0]
|
|
389
|
+
|
|
390
|
+
return overlap00
|
vqe/hamiltonian.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vqe.hamiltonian
|
|
3
|
+
---------------
|
|
4
|
+
Molecular Hamiltonian and geometry utilities for VQE simulations.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
- A registry of static molecular presets (`MOLECULES`)
|
|
8
|
+
- `generate_geometry`: parametric generators for bond lengths / angles
|
|
9
|
+
- `build_hamiltonian`: construction of qubit Hamiltonians with mappings
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pennylane as qml
|
|
15
|
+
from pennylane import qchem
|
|
16
|
+
from pennylane import numpy as np
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ================================================================
|
|
20
|
+
# STATIC MOLECULE REGISTRY
|
|
21
|
+
# ================================================================
|
|
22
|
+
|
|
23
|
+
#: Canonical presets for common molecules used across the project.
|
|
24
|
+
#:
|
|
25
|
+
#: Each entry has:
|
|
26
|
+
#: - symbols: list[str] → atomic species
|
|
27
|
+
#: - coordinates: np.ndarray → shape (N, 3), in Å
|
|
28
|
+
#: - charge: int → total molecular charge
|
|
29
|
+
#: - basis: str → basis set name (string used in JSON/configs)
|
|
30
|
+
MOLECULES = {
|
|
31
|
+
"H2": {
|
|
32
|
+
"symbols": ["H", "H"],
|
|
33
|
+
"coordinates": np.array(
|
|
34
|
+
[
|
|
35
|
+
[0.0, 0.0, 0.0],
|
|
36
|
+
[0.0, 0.0, 0.7414],
|
|
37
|
+
]
|
|
38
|
+
),
|
|
39
|
+
"charge": 0,
|
|
40
|
+
"basis": "STO-3G",
|
|
41
|
+
},
|
|
42
|
+
"LIH": {
|
|
43
|
+
"symbols": ["Li", "H"],
|
|
44
|
+
"coordinates": np.array(
|
|
45
|
+
[
|
|
46
|
+
[0.0, 0.0, 0.0],
|
|
47
|
+
[0.0, 0.0, 1.6],
|
|
48
|
+
]
|
|
49
|
+
),
|
|
50
|
+
"charge": 0,
|
|
51
|
+
"basis": "STO-3G",
|
|
52
|
+
},
|
|
53
|
+
"H2O": {
|
|
54
|
+
"symbols": ["O", "H", "H"],
|
|
55
|
+
"coordinates": np.array(
|
|
56
|
+
[
|
|
57
|
+
[0.000000, 0.000000, 0.000000],
|
|
58
|
+
[0.758602, 0.000000, 0.504284],
|
|
59
|
+
[-0.758602, 0.000000, 0.504284],
|
|
60
|
+
]
|
|
61
|
+
),
|
|
62
|
+
"charge": 0,
|
|
63
|
+
"basis": "STO-3G",
|
|
64
|
+
},
|
|
65
|
+
# Canonical H3+ geometry: equilateral triangle in the xy-plane
|
|
66
|
+
# (matches your H3+_Noiseless_both_Adam JSON and SSVQE notebook)
|
|
67
|
+
"H3+": {
|
|
68
|
+
"symbols": ["H", "H", "H"],
|
|
69
|
+
"coordinates": np.array(
|
|
70
|
+
[
|
|
71
|
+
[0.000000, 1.000000, 0.000000],
|
|
72
|
+
[-0.866025, -0.500000, 0.000000],
|
|
73
|
+
[0.866025, -0.500000, 0.000000],
|
|
74
|
+
]
|
|
75
|
+
),
|
|
76
|
+
"charge": +1,
|
|
77
|
+
"basis": "STO-3G",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ================================================================
|
|
83
|
+
# PARAMETRIC GEOMETRY GENERATORS
|
|
84
|
+
# ================================================================
|
|
85
|
+
def generate_geometry(molecule: str, param_value: float):
|
|
86
|
+
"""
|
|
87
|
+
Generate atomic symbols and coordinates for a parameterised molecule.
|
|
88
|
+
|
|
89
|
+
Supported identifiers (case-insensitive):
|
|
90
|
+
- "H2_BOND" : varies the H–H bond length (Å)
|
|
91
|
+
- "LIH_BOND" : varies the Li–H bond length (Å)
|
|
92
|
+
- "H2O_ANGLE" : varies the H–O–H bond angle (degrees)
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
molecule: Molecule identifier including the parameter tag,
|
|
96
|
+
e.g. "H2_BOND", "LiH_BOND", "H2O_ANGLE".
|
|
97
|
+
param_value: Geometry parameter:
|
|
98
|
+
- bond length in Å for *_BOND
|
|
99
|
+
- angle in degrees for *_ANGLE
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
(symbols, coordinates):
|
|
103
|
+
- symbols: list[str]
|
|
104
|
+
- coordinates: np.ndarray of shape (N, 3), in Å
|
|
105
|
+
"""
|
|
106
|
+
mol = molecule.upper()
|
|
107
|
+
|
|
108
|
+
if mol == "H2O_ANGLE":
|
|
109
|
+
# Water with fixed bond length, variable angle
|
|
110
|
+
bond_length = 0.9584 # Å
|
|
111
|
+
angle_rad = np.deg2rad(param_value)
|
|
112
|
+
x = bond_length * np.sin(angle_rad / 2)
|
|
113
|
+
z = bond_length * np.cos(angle_rad / 2)
|
|
114
|
+
|
|
115
|
+
symbols = ["O", "H", "H"]
|
|
116
|
+
coordinates = np.array(
|
|
117
|
+
[
|
|
118
|
+
[0.0, 0.0, 0.0], # Oxygen
|
|
119
|
+
[x, 0.0, z], # Hydrogen 1
|
|
120
|
+
[-x, 0.0, z], # Hydrogen 2
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
return symbols, coordinates
|
|
124
|
+
|
|
125
|
+
if mol == "H2_BOND":
|
|
126
|
+
# Dihydrogen with variable bond length along z
|
|
127
|
+
symbols = ["H", "H"]
|
|
128
|
+
coordinates = np.array(
|
|
129
|
+
[
|
|
130
|
+
[0.0, 0.0, 0.0],
|
|
131
|
+
[0.0, 0.0, float(param_value)],
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
return symbols, coordinates
|
|
135
|
+
|
|
136
|
+
if mol == "LIH_BOND":
|
|
137
|
+
# Lithium hydride with variable Li–H separation along z
|
|
138
|
+
symbols = ["Li", "H"]
|
|
139
|
+
coordinates = np.array(
|
|
140
|
+
[
|
|
141
|
+
[0.0, 0.0, 0.0],
|
|
142
|
+
[0.0, 0.0, float(param_value)],
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
return symbols, coordinates
|
|
146
|
+
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Unsupported parametric molecule '{molecule}'. "
|
|
149
|
+
"Supported: H2_BOND, LiH_BOND, H2O_ANGLE."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ================================================================
|
|
154
|
+
# HAMILTONIAN BUILDER
|
|
155
|
+
# ================================================================
|
|
156
|
+
def _get_preset(molecule: str):
|
|
157
|
+
"""Internal helper: fetch preset entry from MOLECULES by name (case-insensitive)."""
|
|
158
|
+
key = molecule.upper()
|
|
159
|
+
if key in MOLECULES:
|
|
160
|
+
return MOLECULES[key]
|
|
161
|
+
|
|
162
|
+
# Handle "H3PLUS" alias for convenience
|
|
163
|
+
if key in {"H3PLUS", "H3_PLUS"}:
|
|
164
|
+
return MOLECULES["H3+"]
|
|
165
|
+
|
|
166
|
+
available = ", ".join(MOLECULES.keys())
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"Unsupported molecule '{molecule}'. "
|
|
169
|
+
f"Available static presets: {available}, or parametric: H2_BOND, LiH_BOND, H2O_ANGLE."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_hamiltonian(molecule: str, mapping: str = "jordan_wigner"):
|
|
174
|
+
"""
|
|
175
|
+
Construct the qubit Hamiltonian for a given molecule using PennyLane's qchem.
|
|
176
|
+
|
|
177
|
+
This is the **single source of truth** for Hamiltonian construction
|
|
178
|
+
in the VQE/SSVQE workflows.
|
|
179
|
+
|
|
180
|
+
Supports:
|
|
181
|
+
- Static presets:
|
|
182
|
+
"H2", "LiH", "H2O", "H3+"
|
|
183
|
+
- Parametric variants:
|
|
184
|
+
"H2_BOND", "LiH_BOND", "H2O_ANGLE"
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
molecule:
|
|
188
|
+
Molecule identifier (case-insensitive). Examples:
|
|
189
|
+
- "H2", "LiH", "H2O", "H3+"
|
|
190
|
+
- "H2_BOND", "LiH_BOND", "H2O_ANGLE"
|
|
191
|
+
mapping:
|
|
192
|
+
Fermion-to-qubit mapping scheme.
|
|
193
|
+
One of {"jordan_wigner", "bravyi_kitaev", "parity"}.
|
|
194
|
+
Case-insensitive; stored in the config as lower-case.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
(H, num_qubits, symbols, coordinates, basis)
|
|
198
|
+
- H: qml.Hamiltonian
|
|
199
|
+
- num_qubits: int
|
|
200
|
+
- symbols: list[str]
|
|
201
|
+
- coordinates: np.ndarray, in Å
|
|
202
|
+
- basis: str (e.g. "STO-3G")
|
|
203
|
+
"""
|
|
204
|
+
mapping = mapping.lower()
|
|
205
|
+
mol = molecule.upper()
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------
|
|
208
|
+
# Parametric molecules: delegate to generator with defaults
|
|
209
|
+
# ------------------------------------------------------------
|
|
210
|
+
if "BOND" in mol or "ANGLE" in mol:
|
|
211
|
+
# Use reasonable defaults consistent with your notebooks:
|
|
212
|
+
# - H2_BOND, LiH_BOND: bond length default is ~0.74–1.0 Å,
|
|
213
|
+
# but actual scans always call generate_geometry
|
|
214
|
+
# - H2O_ANGLE: default around 104.5°
|
|
215
|
+
if mol == "H2O_ANGLE":
|
|
216
|
+
default_param = 104.5 # degrees
|
|
217
|
+
else:
|
|
218
|
+
default_param = 0.74 # Å; purely a placeholder
|
|
219
|
+
|
|
220
|
+
symbols, coordinates = generate_geometry(molecule, default_param)
|
|
221
|
+
charge = 0
|
|
222
|
+
basis = "STO-3G"
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------
|
|
225
|
+
# Static presets from the registry
|
|
226
|
+
# ------------------------------------------------------------
|
|
227
|
+
else:
|
|
228
|
+
preset = _get_preset(mol)
|
|
229
|
+
symbols = preset["symbols"]
|
|
230
|
+
coordinates = preset["coordinates"]
|
|
231
|
+
charge = preset["charge"]
|
|
232
|
+
basis = preset["basis"]
|
|
233
|
+
|
|
234
|
+
# ------------------------------------------------------------
|
|
235
|
+
# Build molecular Hamiltonian with mapping
|
|
236
|
+
# ------------------------------------------------------------
|
|
237
|
+
try:
|
|
238
|
+
H, num_qubits = qchem.molecular_hamiltonian(
|
|
239
|
+
symbols,
|
|
240
|
+
coordinates,
|
|
241
|
+
charge=charge,
|
|
242
|
+
basis=basis,
|
|
243
|
+
mapping=mapping,
|
|
244
|
+
unit="angstrom",
|
|
245
|
+
)
|
|
246
|
+
except TypeError:
|
|
247
|
+
# Fallback for older PennyLane versions that do not support `mapping=`
|
|
248
|
+
print(
|
|
249
|
+
f"⚠️ Mapping '{mapping}' not supported in this PennyLane version — "
|
|
250
|
+
"defaulting to Jordan–Wigner."
|
|
251
|
+
)
|
|
252
|
+
H, num_qubits = qchem.molecular_hamiltonian(
|
|
253
|
+
symbols,
|
|
254
|
+
coordinates,
|
|
255
|
+
charge=charge,
|
|
256
|
+
basis=basis,
|
|
257
|
+
unit="angstrom",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return H, num_qubits, symbols, coordinates, basis
|