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/ansatz.py ADDED
@@ -0,0 +1,420 @@
1
+ """
2
+ vqe.ansatz
3
+ ----------
4
+ Library of parameterized quantum circuits (ansatzes) used in the VQE workflow.
5
+
6
+ Includes
7
+ --------
8
+ - Simple 2-qubit toy ansatzes:
9
+ * TwoQubit-RY-CNOT
10
+ * Minimal
11
+ * RY-CZ
12
+ - Hardware-efficient template:
13
+ * StronglyEntanglingLayers
14
+ - Chemistry-inspired UCC family:
15
+ * UCC-S (singles only)
16
+ * UCC-D (doubles only)
17
+ * UCCSD (singles + doubles)
18
+
19
+ All chemistry ansatzes are constructed to mirror the legacy
20
+ `excitation_ansatz(..., excitation_type=...)` behaviour from the old notebooks,
21
+ while keeping the interface compatible with `vqe.engine.build_ansatz(...)`.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import pennylane as qml
27
+ from pennylane import numpy as np
28
+ from pennylane import qchem
29
+
30
+
31
+ # ================================================================
32
+ # BASIC / TOY ANSATZES
33
+ # ================================================================
34
+ def two_qubit_ry_cnot(params, wires):
35
+ """
36
+ Scalable version of the original 2-qubit RY-CNOT motif.
37
+
38
+ Applies the motif to every adjacent pair of qubits:
39
+ RY(param) on wire i
40
+ CNOT(i → i+1)
41
+ RY(-param) on wire i+1
42
+ CNOT(i → i+1)
43
+
44
+ Number of parameters = len(wires) - 1.
45
+ """
46
+ if len(params) != len(wires) - 1:
47
+ raise ValueError(
48
+ f"TwoQubit-RY-CNOT expects {len(wires)-1} parameters for {len(wires)} wires, "
49
+ f"got {len(params)}."
50
+ )
51
+
52
+ for i in range(len(wires) - 1):
53
+ w0, w1 = wires[i], wires[i + 1]
54
+ theta = params[i]
55
+
56
+ qml.RY(theta, wires=w0)
57
+ qml.CNOT(wires=[w0, w1])
58
+ qml.RY(-theta, wires=w1)
59
+ qml.CNOT(wires=[w0, w1])
60
+
61
+
62
+ def ry_cz(params, wires):
63
+ """
64
+ Single-layer RY rotations followed by a CZ chain.
65
+
66
+ Matches the legacy `vqe_utils.ry_cz` used in H₂ optimizer / ansatz
67
+ comparison notebooks.
68
+
69
+ Shape:
70
+ params.shape == (len(wires),)
71
+ """
72
+ if len(params) != len(wires):
73
+ raise ValueError(
74
+ f"RY-CZ expects one parameter per wire "
75
+ f"(got {len(params)} vs {len(wires)})"
76
+ )
77
+
78
+ # Local rotations
79
+ for theta, w in zip(params, wires):
80
+ qml.RY(theta, wires=w)
81
+
82
+ # Entangling CZ chain
83
+ for w0, w1 in zip(wires[:-1], wires[1:]):
84
+ qml.CZ(wires=[w0, w1])
85
+
86
+
87
+ def minimal(params, wires):
88
+ """
89
+ Minimal 2-qubit circuit: RY rotation + CNOT.
90
+
91
+ Matches the legacy vqe_utils.minimal used in H₂ ansatz comparisons.
92
+
93
+ Behaviour:
94
+ - Uses the first two wires from the provided wire list.
95
+ - Requires at least 2 wires, but can be embedded in a larger register.
96
+ """
97
+ if len(wires) < 2:
98
+ raise ValueError(
99
+ f"Minimal ansatz expects at least 2 wires, got {len(wires)}"
100
+ )
101
+
102
+ qml.RY(params[0], wires=wires[0])
103
+ qml.CNOT(wires=[wires[0], wires[1]])
104
+
105
+
106
+ def hardware_efficient_ansatz(params, wires):
107
+ """
108
+ Standard hardware-efficient ansatz using StronglyEntanglingLayers.
109
+
110
+ Convention:
111
+ params.shape = (n_layers, len(wires), 3)
112
+ """
113
+ qml.templates.StronglyEntanglingLayers(params, wires=wires)
114
+
115
+
116
+ # ================================================================
117
+ # UCC-STYLE CHEMISTRY ANSATZES
118
+ # ================================================================
119
+ def _ucc_cache_key(symbols, coordinates, basis: str):
120
+ """Build a hashable cache key from molecular data."""
121
+ coords = np.array(coordinates, dtype=float).flatten().tolist()
122
+ return (tuple(symbols), tuple(coords), basis.upper())
123
+
124
+
125
+ def _build_ucc_data(symbols, coordinates, basis: str = "STO-3G"):
126
+ """
127
+ Compute (singles, doubles, hf_state) for a given molecule and cache them.
128
+
129
+ This mirrors the legacy notebook logic based on:
130
+ - qchem.hf_state(electrons, spin_orbitals)
131
+ - qchem.excitations(electrons, spin_orbitals)
132
+
133
+ Notes
134
+ -----
135
+ * We intentionally keep the call signature minimal: (symbols, coordinates, basis)
136
+ so that `vqe.engine.build_ansatz(...)` can pass through the values it has
137
+ without needing charge / multiplicity.
138
+ * The cache lives on the function object so repeated calls are cheap.
139
+ """
140
+ if symbols is None or coordinates is None:
141
+ raise ValueError(
142
+ "UCC ansatz requires symbols and coordinates. "
143
+ "Make sure build_hamiltonian(...) is used and passed through."
144
+ )
145
+
146
+ key = _ucc_cache_key(symbols, coordinates, basis)
147
+
148
+ if not hasattr(_build_ucc_data, "_cache"):
149
+ _build_ucc_data._cache = {}
150
+
151
+ if key not in _build_ucc_data._cache:
152
+ try:
153
+ mol = qchem.Molecule(symbols, coordinates, charge=0, basis=basis)
154
+ except TypeError:
155
+ # Backwards-compat for older PennyLane versions without basis kwarg
156
+ mol = qchem.Molecule(symbols, coordinates, charge=0)
157
+
158
+ electrons = mol.n_electrons
159
+ spin_orbitals = 2 * mol.n_orbitals
160
+
161
+ singles, doubles = qchem.excitations(electrons, spin_orbitals)
162
+ hf_state = qchem.hf_state(electrons, spin_orbitals)
163
+
164
+ # Store as simple Python containers so that using them inside QNodes
165
+ # is cheap and avoids unnecessary object conversions.
166
+ singles = [tuple(ex) for ex in singles]
167
+ doubles = [tuple(ex) for ex in doubles]
168
+ hf_state = np.array(hf_state, dtype=int)
169
+
170
+ _build_ucc_data._cache[key] = (singles, doubles, hf_state)
171
+
172
+ return _build_ucc_data._cache[key]
173
+
174
+
175
+ def _apply_ucc_layers(
176
+ params,
177
+ wires,
178
+ *,
179
+ singles,
180
+ doubles,
181
+ hf_state,
182
+ use_singles: bool,
183
+ use_doubles: bool,
184
+ ):
185
+ """
186
+ Shared helper to apply HF preparation + selected UCC excitation layers.
187
+
188
+ Parameter ordering convention (matches legacy notebooks):
189
+ - singles parameters first (if used)
190
+ - doubles parameters after that
191
+ """
192
+ wires = list(wires)
193
+ num_wires = len(wires)
194
+
195
+ if len(hf_state) != num_wires:
196
+ raise ValueError(
197
+ f"HF state length ({len(hf_state)}) does not match number of wires "
198
+ f"({num_wires})."
199
+ )
200
+
201
+ # Prepare Hartree–Fock reference
202
+ qml.BasisState(hf_state, wires=wires)
203
+
204
+ # Determine how many parameters we expect
205
+ n_singles = len(singles) if use_singles else 0
206
+ n_doubles = len(doubles) if use_doubles else 0
207
+ expected = n_singles + n_doubles
208
+
209
+ if len(params) != expected:
210
+ raise ValueError(
211
+ f"UCC ansatz expects {expected} parameters, got {len(params)}."
212
+ )
213
+
214
+ # Apply singles
215
+ offset = 0
216
+ if use_singles:
217
+ for i, exc in enumerate(singles):
218
+ qml.SingleExcitation(params[offset + i], wires=list(exc))
219
+ offset += n_singles
220
+
221
+ # Apply doubles
222
+ if use_doubles:
223
+ for j, exc in enumerate(doubles):
224
+ qml.DoubleExcitation(params[offset + j], wires=list(exc))
225
+
226
+
227
+ def uccsd_ansatz(params, wires, symbols=None, coordinates=None, basis: str = "STO-3G"):
228
+ """
229
+ Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz.
230
+
231
+ Behaviour is chosen to match the legacy usage:
232
+
233
+ excitation_ansatz(
234
+ params,
235
+ wires=range(qubits),
236
+ hf_state=hf,
237
+ excitations=(singles, doubles),
238
+ excitation_type="both",
239
+ )
240
+
241
+ Args
242
+ ----
243
+ params
244
+ 1D array of length len(singles) + len(doubles)
245
+ wires
246
+ Sequence of qubit wires
247
+ symbols, coordinates, basis
248
+ Molecular information (must be provided for chemistry simulations)
249
+ """
250
+ singles, doubles, hf_state = _build_ucc_data(symbols, coordinates, basis=basis)
251
+
252
+ _apply_ucc_layers(
253
+ params,
254
+ wires=wires,
255
+ singles=singles,
256
+ doubles=doubles,
257
+ hf_state=hf_state,
258
+ use_singles=True,
259
+ use_doubles=True,
260
+ )
261
+
262
+
263
+ def uccd_ansatz(params, wires, symbols=None, coordinates=None, basis: str = "STO-3G"):
264
+ """
265
+ UCC-D / UCCD: doubles-only UCC ansatz.
266
+
267
+ Designed to mirror the LiH notebook behaviour where we used
268
+ `excitation_ansatz(..., excitation_type="double")` with zero initial params.
269
+
270
+ Args
271
+ ----
272
+ params
273
+ 1D array of length len(doubles)
274
+ wires
275
+ Sequence of qubit wires
276
+ symbols, coordinates, basis
277
+ Molecular information
278
+ """
279
+ singles, doubles, hf_state = _build_ucc_data(symbols, coordinates, basis=basis)
280
+
281
+ _apply_ucc_layers(
282
+ params,
283
+ wires=wires,
284
+ singles=singles,
285
+ doubles=doubles,
286
+ hf_state=hf_state,
287
+ use_singles=False,
288
+ use_doubles=True,
289
+ )
290
+
291
+
292
+ def uccs_ansatz(params, wires, symbols=None, coordinates=None, basis: str = "STO-3G"):
293
+ """
294
+ UCC-S: singles-only UCC ansatz.
295
+
296
+ Matches the structure of UCCSD/UCCD and the legacy
297
+ `excitation_ansatz(..., excitation_type="single")` behaviour.
298
+ """
299
+ singles, doubles, hf_state = _build_ucc_data(symbols, coordinates, basis=basis)
300
+
301
+ _apply_ucc_layers(
302
+ params,
303
+ wires=wires,
304
+ singles=singles,
305
+ doubles=doubles,
306
+ hf_state=hf_state,
307
+ use_singles=True,
308
+ use_doubles=False,
309
+ )
310
+
311
+
312
+ # ================================================================
313
+ # REGISTRY
314
+ # ================================================================
315
+ ANSATZES = {
316
+ "TwoQubit-RY-CNOT": two_qubit_ry_cnot,
317
+ "RY-CZ": ry_cz,
318
+ "Minimal": minimal,
319
+ "StronglyEntanglingLayers": hardware_efficient_ansatz,
320
+ "UCCSD": uccsd_ansatz,
321
+ "UCC-SD": uccsd_ansatz, # alias
322
+ "UCC-D": uccd_ansatz,
323
+ "UCCD": uccd_ansatz, # alias
324
+ "UCC-S": uccs_ansatz,
325
+ "UCCS": uccs_ansatz, # alias
326
+ }
327
+
328
+
329
+ def get_ansatz(name: str):
330
+ """
331
+ Return ansatz function by name.
332
+
333
+ This is the entry point used by `vqe.engine.build_ansatz(...)`.
334
+ """
335
+ if name not in ANSATZES:
336
+ available = ", ".join(sorted(ANSATZES.keys()))
337
+ raise ValueError(f"Unknown ansatz '{name}'. Available: {available}")
338
+ return ANSATZES[name]
339
+
340
+
341
+ # ================================================================
342
+ # PARAMETER INITIALISATION
343
+ # ================================================================
344
+ def init_params(
345
+ ansatz_name: str,
346
+ num_wires: int,
347
+ scale: float = 0.01,
348
+ requires_grad: bool = True,
349
+ symbols=None,
350
+ coordinates=None,
351
+ basis: str = "STO-3G",
352
+ seed: int = 0,
353
+ ):
354
+ """
355
+ Initialise variational parameters for a given ansatz.
356
+
357
+ Design choices (kept consistent with the legacy notebooks):
358
+
359
+ - TwoQubit-RY-CNOT / Minimal
360
+ * 1 parameter, small random normal ~ N(0, scale²)
361
+
362
+ - RY-CZ
363
+ * `num_wires` parameters, random normal ~ N(0, scale²)
364
+
365
+ - StronglyEntanglingLayers
366
+ * params.shape = (1, num_wires, 3), normal with width ~ π
367
+
368
+ - UCC family (UCC-S / UCC-D / UCCSD and aliases)
369
+ * **All zeros**, starting from θ = 0 as in the original chemistry notebooks.
370
+ The length of the vector is determined from the excitation lists.
371
+
372
+ Returns
373
+ -------
374
+ np.ndarray
375
+ Parameter array with `requires_grad=True`
376
+ """
377
+ np.random.seed(seed)
378
+
379
+ # --- Toy ansatzes --------------------------------------------------------
380
+ if ansatz_name == "TwoQubit-RY-CNOT":
381
+ # scalable: one parameter per adjacent pair
382
+ if num_wires < 2:
383
+ raise ValueError("TwoQubit-RY-CNOT requires at least 2 wires.")
384
+ vals = scale * np.random.randn(num_wires - 1)
385
+
386
+ elif ansatz_name == "Minimal":
387
+ # still a 1-parameter global circuit
388
+ vals = scale * np.random.randn(1)
389
+
390
+ elif ansatz_name == "RY-CZ":
391
+ vals = scale * np.random.randn(num_wires)
392
+
393
+ # --- Chemistry ansatzes (UCC family) ------------------------------------
394
+ elif ansatz_name in ["UCCSD", "UCC-SD", "UCC-D", "UCCD", "UCC-S", "UCCS"]:
395
+ if symbols is None or coordinates is None:
396
+ raise ValueError(
397
+ f"Ansatz '{ansatz_name}' requires symbols/coordinates "
398
+ "to determine excitation count. Ensure you are using "
399
+ "build_hamiltonian(...) and engine.build_ansatz(...)."
400
+ )
401
+
402
+ singles, doubles, _ = _build_ucc_data(symbols, coordinates, basis=basis)
403
+
404
+ if ansatz_name in ["UCC-D", "UCCD"]:
405
+ # doubles-only
406
+ vals = np.zeros(len(doubles))
407
+
408
+ elif ansatz_name in ["UCC-S", "UCCS"]:
409
+ # singles-only
410
+ vals = np.zeros(len(singles))
411
+
412
+ else:
413
+ # UCCSD / UCC-SD: singles + doubles
414
+ vals = np.zeros(len(singles) + len(doubles))
415
+
416
+ else:
417
+ available = ", ".join(sorted(ANSATZES.keys()))
418
+ raise ValueError(f"Unknown ansatz '{ansatz_name}'. Available: {available}")
419
+
420
+ return np.array(vals, requires_grad=requires_grad)