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