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/io_utils.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vqe.io_utils
|
|
3
|
+
------------
|
|
4
|
+
Utility functions for reproducible VQE runs:
|
|
5
|
+
|
|
6
|
+
- Run configuration construction & hashing
|
|
7
|
+
- JSON-safe serialization
|
|
8
|
+
- File/directory management for results & images
|
|
9
|
+
|
|
10
|
+
This module is the single source of truth for:
|
|
11
|
+
- Where new package results are stored
|
|
12
|
+
- How run configurations are represented and hashed
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json, os
|
|
18
|
+
import hashlib
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict
|
|
21
|
+
|
|
22
|
+
# ================================================================
|
|
23
|
+
# BASE PATHS
|
|
24
|
+
# ================================================================
|
|
25
|
+
|
|
26
|
+
# Root of the repository/package (…/ <repo_root> / vqe / io_utils.py)
|
|
27
|
+
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
|
28
|
+
|
|
29
|
+
# New, package-scoped locations for results and images
|
|
30
|
+
RESULTS_DIR = BASE_DIR / "results" / "vqe"
|
|
31
|
+
IMG_DIR = BASE_DIR / "images" / "vqe"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ================================================================
|
|
35
|
+
# INTERNAL HELPERS
|
|
36
|
+
# ================================================================
|
|
37
|
+
|
|
38
|
+
def _round_floats(x: Any, ndigits: int = 8) -> Any:
|
|
39
|
+
"""
|
|
40
|
+
Recursively round floats / numpy scalars / arrays for stable hashing.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
x:
|
|
45
|
+
Arbitrary Python or numpy object.
|
|
46
|
+
ndigits:
|
|
47
|
+
Number of decimal places to round to.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
Any
|
|
52
|
+
Object of the same container structure with floats rounded.
|
|
53
|
+
"""
|
|
54
|
+
# Plain Python float
|
|
55
|
+
if isinstance(x, float):
|
|
56
|
+
return round(x, ndigits)
|
|
57
|
+
|
|
58
|
+
# Numpy scalar-like: has .item() that is a float
|
|
59
|
+
try:
|
|
60
|
+
if hasattr(x, "item"):
|
|
61
|
+
scalar = x.item()
|
|
62
|
+
if isinstance(scalar, float):
|
|
63
|
+
return round(float(scalar), ndigits)
|
|
64
|
+
except Exception:
|
|
65
|
+
# Fall through if .item() misbehaves
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Numpy arrays / array-like -> convert to list and recurse
|
|
69
|
+
if hasattr(x, "tolist"):
|
|
70
|
+
return _round_floats(x.tolist(), ndigits)
|
|
71
|
+
|
|
72
|
+
# Python containers
|
|
73
|
+
if isinstance(x, (list, tuple)):
|
|
74
|
+
return type(x)(_round_floats(v, ndigits) for v in x)
|
|
75
|
+
|
|
76
|
+
# Everything else left untouched
|
|
77
|
+
return x
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _to_serializable(obj: Any) -> Any:
|
|
81
|
+
"""
|
|
82
|
+
Convert tensors / numpy arrays / complex containers into JSON-safe types.
|
|
83
|
+
|
|
84
|
+
Rules of thumb:
|
|
85
|
+
- numpy / tensor-like with .tolist() → list / nested lists
|
|
86
|
+
- numpy scalar with .item() → float (when possible)
|
|
87
|
+
- dict / list / tuple → recurse
|
|
88
|
+
- everything else returned unchanged
|
|
89
|
+
"""
|
|
90
|
+
# Numpy / tensor scalar
|
|
91
|
+
if hasattr(obj, "item"):
|
|
92
|
+
try:
|
|
93
|
+
return obj.item()
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# Numpy arrays and friends
|
|
98
|
+
if hasattr(obj, "tolist"):
|
|
99
|
+
return obj.tolist()
|
|
100
|
+
|
|
101
|
+
# Dicts
|
|
102
|
+
if isinstance(obj, dict):
|
|
103
|
+
return {k: _to_serializable(v) for k, v in obj.items()}
|
|
104
|
+
|
|
105
|
+
# Lists / tuples
|
|
106
|
+
if isinstance(obj, (list, tuple)):
|
|
107
|
+
return [_to_serializable(v) for v in obj]
|
|
108
|
+
|
|
109
|
+
# Primitive or unknown: hope json can handle it
|
|
110
|
+
return obj
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ================================================================
|
|
114
|
+
# RUN CONFIGURATION & HASHING
|
|
115
|
+
# ================================================================
|
|
116
|
+
|
|
117
|
+
def make_run_config_dict(
|
|
118
|
+
symbols,
|
|
119
|
+
coordinates,
|
|
120
|
+
basis: str,
|
|
121
|
+
ansatz_desc: str,
|
|
122
|
+
optimizer_name: str,
|
|
123
|
+
stepsize: float,
|
|
124
|
+
max_iterations: int,
|
|
125
|
+
seed: int,
|
|
126
|
+
mapping: str,
|
|
127
|
+
noisy: bool = False,
|
|
128
|
+
depolarizing_prob: float = 0.0,
|
|
129
|
+
amplitude_damping_prob: float = 0.0,
|
|
130
|
+
molecule_label: str | None = None,
|
|
131
|
+
) -> Dict[str, Any]:
|
|
132
|
+
"""
|
|
133
|
+
Construct a canonical dictionary describing a VQE/SSVQE run configuration.
|
|
134
|
+
|
|
135
|
+
This dictionary is used to generate a stable hash signature
|
|
136
|
+
(see :func:`run_signature`) so that identical configurations
|
|
137
|
+
always map to the same cache key / filename.
|
|
138
|
+
"""
|
|
139
|
+
cfg: Dict[str, Any] = {
|
|
140
|
+
"symbols": list(symbols),
|
|
141
|
+
"geometry": _round_floats(coordinates, 8),
|
|
142
|
+
"basis": basis,
|
|
143
|
+
"ansatz": ansatz_desc,
|
|
144
|
+
"optimizer": {
|
|
145
|
+
"name": optimizer_name,
|
|
146
|
+
"stepsize": float(stepsize),
|
|
147
|
+
"iterations_planned": int(max_iterations),
|
|
148
|
+
},
|
|
149
|
+
"optimizer_name": optimizer_name, # convenient flat copy
|
|
150
|
+
"seed": int(seed),
|
|
151
|
+
"noisy": bool(noisy),
|
|
152
|
+
"depolarizing_prob": float(depolarizing_prob),
|
|
153
|
+
"amplitude_damping_prob": float(amplitude_damping_prob),
|
|
154
|
+
"mapping": mapping.lower(),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if molecule_label is not None:
|
|
158
|
+
cfg["molecule"] = str(molecule_label)
|
|
159
|
+
|
|
160
|
+
return cfg
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_signature(cfg: Dict[str, Any]) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Generate a short, stable hash identifier from a run configuration.
|
|
166
|
+
|
|
167
|
+
The configuration is JSON-serialized with sorted keys and compact
|
|
168
|
+
separators, then hashed with SHA-256. The first 12 hex characters
|
|
169
|
+
are used as the signature.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
cfg:
|
|
174
|
+
Configuration dictionary as returned by :func:`make_run_config_dict`.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
str
|
|
179
|
+
12-character hexadecimal signature.
|
|
180
|
+
"""
|
|
181
|
+
payload = json.dumps(cfg, sort_keys=True, separators=(",", ":"))
|
|
182
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:12]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ================================================================
|
|
186
|
+
# FILESYSTEM UTILITIES
|
|
187
|
+
# ================================================================
|
|
188
|
+
|
|
189
|
+
def ensure_dirs() -> None:
|
|
190
|
+
"""
|
|
191
|
+
Ensure that the standard result and image directories exist.
|
|
192
|
+
|
|
193
|
+
- RESULTS_DIR: where JSON run records are written (package_results/)
|
|
194
|
+
- IMG_DIR: where plots and figures are saved (vqe/images/)
|
|
195
|
+
"""
|
|
196
|
+
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
IMG_DIR.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _result_path_from_prefix(prefix: str) -> Path:
|
|
201
|
+
"""
|
|
202
|
+
Build the full JSON path from a filename prefix (without extension).
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
prefix:
|
|
207
|
+
Filename prefix, e.g. "H2_Adam_s0__abc123def456".
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
Path
|
|
212
|
+
Path to the JSON file within RESULTS_DIR.
|
|
213
|
+
"""
|
|
214
|
+
return RESULTS_DIR / f"{prefix}.json"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def save_run_record(prefix: str, record: Dict[str, Any]) -> str:
|
|
218
|
+
"""
|
|
219
|
+
Save a run record (config + results) as JSON in RESULTS_DIR.
|
|
220
|
+
|
|
221
|
+
The final path is:
|
|
222
|
+
RESULTS_DIR / f"{prefix}.json"
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
prefix:
|
|
227
|
+
Unique filename prefix, typically including molecule, optimizer,
|
|
228
|
+
seed, and the run signature.
|
|
229
|
+
record:
|
|
230
|
+
Dictionary containing configuration, results, and any metadata.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
str
|
|
235
|
+
String path to the saved JSON file.
|
|
236
|
+
"""
|
|
237
|
+
ensure_dirs()
|
|
238
|
+
path = _result_path_from_prefix(prefix)
|
|
239
|
+
serializable_record = _to_serializable(record)
|
|
240
|
+
|
|
241
|
+
with path.open("w") as f:
|
|
242
|
+
json.dump(serializable_record, f, indent=2)
|
|
243
|
+
|
|
244
|
+
return str(path)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def make_filename_prefix(cfg: dict, *, noisy: bool, seed: int, hash_str: str, ssvqe: bool = False):
|
|
248
|
+
"""Return unified Option-C filename prefix."""
|
|
249
|
+
# Molecule lives in config only indirectly; infer from symbols
|
|
250
|
+
# OR require callers to inject molecule into cfg beforehand.
|
|
251
|
+
mol = cfg.get("molecule", "MOL") # We will fix cfg in core/ssvqe.
|
|
252
|
+
|
|
253
|
+
# Ansatz string taken directly
|
|
254
|
+
ans = cfg.get("ansatz", "ANSATZ")
|
|
255
|
+
|
|
256
|
+
# Optimizer name
|
|
257
|
+
if "optimizer" in cfg and "name" in cfg["optimizer"]:
|
|
258
|
+
opt = cfg["optimizer"]["name"]
|
|
259
|
+
else:
|
|
260
|
+
opt = "OPT"
|
|
261
|
+
|
|
262
|
+
noise_tag = "noisy" if noisy else "noiseless"
|
|
263
|
+
algo_tag = "SSVQE" if ssvqe else "VQE"
|
|
264
|
+
|
|
265
|
+
return f"{mol}__{ans}__{opt}__{algo_tag}__{noise_tag}__s{seed}__{hash_str}"
|
vqe/optimizer.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vqe.optimizer
|
|
3
|
+
-------------
|
|
4
|
+
Lightweight wrapper over PennyLane optimizers with a unified interface.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
- get_optimizer(name, stepsize)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import pennylane as qml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ================================================================
|
|
15
|
+
# AVAILABLE OPTIMIZERS
|
|
16
|
+
# ================================================================
|
|
17
|
+
_OPTIMIZERS = {
|
|
18
|
+
"Adam": qml.AdamOptimizer,
|
|
19
|
+
"adam": qml.AdamOptimizer, # alias
|
|
20
|
+
|
|
21
|
+
"GradientDescent": qml.GradientDescentOptimizer,
|
|
22
|
+
"gd": qml.GradientDescentOptimizer, # alias
|
|
23
|
+
|
|
24
|
+
"Momentum": qml.MomentumOptimizer,
|
|
25
|
+
"Nesterov": qml.NesterovMomentumOptimizer,
|
|
26
|
+
|
|
27
|
+
"RMSProp": qml.RMSPropOptimizer,
|
|
28
|
+
"Adagrad": qml.AdagradOptimizer,
|
|
29
|
+
|
|
30
|
+
# ---- NEW ADDITIONS (SPSA SUPPORT) ----
|
|
31
|
+
"SPSA": qml.SPSAOptimizer,
|
|
32
|
+
"spsa": qml.SPSAOptimizer, # alias
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ================================================================
|
|
37
|
+
# MAIN FACTORY
|
|
38
|
+
# ================================================================
|
|
39
|
+
def get_optimizer(name: str = "Adam", stepsize: float = 0.2):
|
|
40
|
+
"""
|
|
41
|
+
Return a PennyLane optimizer instance by name.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
name: Optimizer identifier (case-insensitive).
|
|
45
|
+
stepsize: Learning rate.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
An instantiated optimizer.
|
|
49
|
+
"""
|
|
50
|
+
key = name.lower()
|
|
51
|
+
for k, cls in _OPTIMIZERS.items():
|
|
52
|
+
if k.lower() == key:
|
|
53
|
+
return cls(stepsize)
|
|
54
|
+
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Unknown optimizer '{name}'. "
|
|
57
|
+
f"Available: {', '.join(_OPTIMIZERS.keys())}"
|
|
58
|
+
)
|
vqe/ssvqe.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vqe.ssvqe
|
|
3
|
+
---------
|
|
4
|
+
Subspace-Search Variational Quantum Eigensolver (SSVQE).
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Ground and excited states via a shared variational ansatz
|
|
8
|
+
- Orthogonality enforced with |⟨ψ_i|ψ_j⟩|² penalties
|
|
9
|
+
- Any ansatz and optimizer defined in the vqe package
|
|
10
|
+
- Optional depolarizing / amplitude-damping noise
|
|
11
|
+
- Caching and reproducibility via vqe.io_utils
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import itertools
|
|
19
|
+
import pennylane as qml
|
|
20
|
+
from pennylane import numpy as np
|
|
21
|
+
|
|
22
|
+
from .hamiltonian import build_hamiltonian
|
|
23
|
+
from .engine import (
|
|
24
|
+
make_device,
|
|
25
|
+
build_ansatz,
|
|
26
|
+
build_optimizer,
|
|
27
|
+
make_energy_qnode,
|
|
28
|
+
make_overlap00_fn,
|
|
29
|
+
)
|
|
30
|
+
from .io_utils import (
|
|
31
|
+
ensure_dirs,
|
|
32
|
+
make_run_config_dict,
|
|
33
|
+
run_signature,
|
|
34
|
+
save_run_record,
|
|
35
|
+
make_filename_prefix,
|
|
36
|
+
RESULTS_DIR,
|
|
37
|
+
)
|
|
38
|
+
from .visualize import plot_ssvqe_convergence_multi
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ================================================================
|
|
42
|
+
# MAIN ENTRYPOINT
|
|
43
|
+
# ================================================================
|
|
44
|
+
def run_ssvqe(
|
|
45
|
+
molecule: str = "H3+",
|
|
46
|
+
*,
|
|
47
|
+
num_states: int = 2,
|
|
48
|
+
penalty_weight: float = 10.0,
|
|
49
|
+
ansatz_name: str = "UCCSD",
|
|
50
|
+
optimizer_name: str = "Adam",
|
|
51
|
+
steps: int = 100,
|
|
52
|
+
stepsize: float = 0.4,
|
|
53
|
+
seed: int = 0,
|
|
54
|
+
noisy: bool = False,
|
|
55
|
+
depolarizing_prob: float = 0.0,
|
|
56
|
+
amplitude_damping_prob: float = 0.0,
|
|
57
|
+
symbols=None,
|
|
58
|
+
coordinates=None,
|
|
59
|
+
basis: str = "sto-3g",
|
|
60
|
+
plot: bool = True,
|
|
61
|
+
force: bool = False,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Run a Subspace-Search VQE (SSVQE) optimization to obtain ground and
|
|
65
|
+
excited states of a molecular Hamiltonian.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
molecule: Molecule label (e.g. "H2", "LiH", "H3+").
|
|
69
|
+
num_states: Number of eigenstates to target (>= 2).
|
|
70
|
+
penalty_weight: Weight on the orthogonality penalty term.
|
|
71
|
+
ansatz_name: Name of the ansatz from `vqe.ansatz`.
|
|
72
|
+
optimizer_name: Name of optimizer from `vqe.optimizer`.
|
|
73
|
+
steps: Number of optimization iterations.
|
|
74
|
+
stepsize: Optimizer step size.
|
|
75
|
+
seed: Random seed for reproducibility.
|
|
76
|
+
noisy: If True, use a mixed-state simulator and insert noise.
|
|
77
|
+
depolarizing_prob: Depolarizing noise probability per wire.
|
|
78
|
+
amplitude_damping_prob: Amplitude damping probability per wire.
|
|
79
|
+
symbols, coordinates, basis: Optional explicit molecular data.
|
|
80
|
+
plot: Whether to plot E0 / E1 convergence (if num_states >= 2).
|
|
81
|
+
force: If True, ignore cached results and recompute.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
dict with keys:
|
|
85
|
+
- "energies_per_state": list[list[float]]
|
|
86
|
+
- "final_params": list[list[float]]
|
|
87
|
+
- "config": dict
|
|
88
|
+
"""
|
|
89
|
+
assert num_states >= 2, "SSVQE requires at least two target states."
|
|
90
|
+
|
|
91
|
+
np.random.seed(seed)
|
|
92
|
+
ensure_dirs()
|
|
93
|
+
|
|
94
|
+
# ============================================================
|
|
95
|
+
# 1. Build Hamiltonian and molecular data
|
|
96
|
+
# ============================================================
|
|
97
|
+
if symbols is None or coordinates is None:
|
|
98
|
+
H, num_wires, symbols, coordinates, basis = build_hamiltonian(molecule)
|
|
99
|
+
else:
|
|
100
|
+
# Default charge heuristic (matches your prior H3+ usage)
|
|
101
|
+
charge = +1 if molecule.upper() == "H3+" else 0
|
|
102
|
+
H, num_wires = qml.qchem.molecular_hamiltonian(
|
|
103
|
+
symbols, coordinates, charge=charge, basis=basis, unit="angstrom"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# ============================================================
|
|
107
|
+
# 2. Ansatz and initial parameters (shared structure)
|
|
108
|
+
# ============================================================
|
|
109
|
+
ansatz_fn, init_params = build_ansatz(
|
|
110
|
+
ansatz_name,
|
|
111
|
+
num_wires,
|
|
112
|
+
seed=seed,
|
|
113
|
+
symbols=symbols,
|
|
114
|
+
coordinates=coordinates,
|
|
115
|
+
basis=basis,
|
|
116
|
+
)
|
|
117
|
+
# One parameter set per state, all with same shape
|
|
118
|
+
param_sets = [np.array(init_params, requires_grad=True) for _ in range(num_states)]
|
|
119
|
+
|
|
120
|
+
# ============================================================
|
|
121
|
+
# 3. Device and QNodes
|
|
122
|
+
# ============================================================
|
|
123
|
+
dev = make_device(num_wires, noisy=noisy)
|
|
124
|
+
|
|
125
|
+
energy_qnode = make_energy_qnode(
|
|
126
|
+
H,
|
|
127
|
+
dev,
|
|
128
|
+
ansatz_fn,
|
|
129
|
+
num_wires,
|
|
130
|
+
noisy=noisy,
|
|
131
|
+
depolarizing_prob=depolarizing_prob,
|
|
132
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
133
|
+
symbols=symbols,
|
|
134
|
+
coordinates=coordinates,
|
|
135
|
+
basis=basis,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
overlap00 = make_overlap00_fn(
|
|
139
|
+
dev,
|
|
140
|
+
ansatz_fn,
|
|
141
|
+
num_wires,
|
|
142
|
+
noisy=noisy,
|
|
143
|
+
depolarizing_prob=depolarizing_prob,
|
|
144
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
145
|
+
symbols=symbols,
|
|
146
|
+
coordinates=coordinates,
|
|
147
|
+
basis=basis,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# ============================================================
|
|
151
|
+
# 4. Config + caching
|
|
152
|
+
# ============================================================
|
|
153
|
+
cfg = make_run_config_dict(
|
|
154
|
+
symbols=symbols,
|
|
155
|
+
coordinates=coordinates,
|
|
156
|
+
basis=basis,
|
|
157
|
+
ansatz_desc=f"SSVQE({ansatz_name})_{num_states}states",
|
|
158
|
+
optimizer_name=optimizer_name,
|
|
159
|
+
stepsize=stepsize,
|
|
160
|
+
max_iterations=steps,
|
|
161
|
+
seed=seed,
|
|
162
|
+
mapping="jordan_wigner",
|
|
163
|
+
noisy=noisy,
|
|
164
|
+
depolarizing_prob=depolarizing_prob,
|
|
165
|
+
amplitude_damping_prob=amplitude_damping_prob,
|
|
166
|
+
molecule_label=molecule, # <<< add this
|
|
167
|
+
)
|
|
168
|
+
cfg["penalty_weight"] = float(penalty_weight)
|
|
169
|
+
cfg["num_states"] = int(num_states)
|
|
170
|
+
|
|
171
|
+
sig = run_signature(cfg)
|
|
172
|
+
safe_molecule = molecule.replace("+", "plus")
|
|
173
|
+
prefix = make_filename_prefix(
|
|
174
|
+
cfg,
|
|
175
|
+
noisy=noisy,
|
|
176
|
+
seed=seed,
|
|
177
|
+
hash_str=sig,
|
|
178
|
+
ssvqe=True,
|
|
179
|
+
)
|
|
180
|
+
result_path = RESULTS_DIR / f"{prefix}.json"
|
|
181
|
+
|
|
182
|
+
if not force and result_path.exists():
|
|
183
|
+
print(f"📂 Using cached SSVQE result: {result_path}")
|
|
184
|
+
with open(result_path, "r") as f:
|
|
185
|
+
record = json.load(f)
|
|
186
|
+
return record["result"]
|
|
187
|
+
|
|
188
|
+
# ============================================================
|
|
189
|
+
# 5. Cost function: total energy + orthogonality penalties
|
|
190
|
+
# ============================================================
|
|
191
|
+
opt = build_optimizer(optimizer_name, stepsize=stepsize)
|
|
192
|
+
|
|
193
|
+
def _unpack_flat(flat, templates):
|
|
194
|
+
"""Unpack a flat parameter vector into a list of arrays matching templates."""
|
|
195
|
+
arrays = []
|
|
196
|
+
idx = 0
|
|
197
|
+
for tmpl in templates:
|
|
198
|
+
size = int(np.prod(tmpl.shape))
|
|
199
|
+
vec = flat[idx:idx + size]
|
|
200
|
+
arrays.append(np.reshape(vec, tmpl.shape))
|
|
201
|
+
idx += size
|
|
202
|
+
return arrays
|
|
203
|
+
|
|
204
|
+
def cost(flat_params):
|
|
205
|
+
# Split into per-state parameter arrays
|
|
206
|
+
unpacked = _unpack_flat(flat_params, param_sets)
|
|
207
|
+
|
|
208
|
+
# Sum of individual energies
|
|
209
|
+
total = sum(energy_qnode(p) for p in unpacked)
|
|
210
|
+
|
|
211
|
+
# Add orthogonality penalties between all distinct pairs
|
|
212
|
+
for i, j in itertools.combinations(range(len(unpacked)), 2):
|
|
213
|
+
total = total + penalty_weight * overlap00(unpacked[i], unpacked[j])
|
|
214
|
+
|
|
215
|
+
return total
|
|
216
|
+
|
|
217
|
+
# Flatten initial parameters for joint optimization
|
|
218
|
+
flat = np.concatenate([p.ravel() for p in param_sets])
|
|
219
|
+
flat = np.array(flat, requires_grad=True)
|
|
220
|
+
|
|
221
|
+
energies_per_state = [[] for _ in range(num_states)]
|
|
222
|
+
|
|
223
|
+
# ============================================================
|
|
224
|
+
# 6. Optimization loop
|
|
225
|
+
# ============================================================
|
|
226
|
+
for step in range(steps):
|
|
227
|
+
try:
|
|
228
|
+
flat, _ = opt.step_and_cost(cost, flat)
|
|
229
|
+
except AttributeError:
|
|
230
|
+
flat = opt.step(cost, flat)
|
|
231
|
+
|
|
232
|
+
# Unpack back into per-state parameter arrays
|
|
233
|
+
unpacked = _unpack_flat(flat, param_sets)
|
|
234
|
+
|
|
235
|
+
# Record energies for each state
|
|
236
|
+
for k in range(num_states):
|
|
237
|
+
energies_per_state[k].append(float(energy_qnode(unpacked[k])))
|
|
238
|
+
|
|
239
|
+
# Update param_sets for the next iteration (keep grad enabled)
|
|
240
|
+
param_sets = [np.array(u, requires_grad=True) for u in unpacked]
|
|
241
|
+
|
|
242
|
+
# ============================================================
|
|
243
|
+
# 7. Package and save results
|
|
244
|
+
# ============================================================
|
|
245
|
+
result = {
|
|
246
|
+
"energies_per_state": energies_per_state,
|
|
247
|
+
"final_params": [u.tolist() for u in param_sets],
|
|
248
|
+
"config": cfg,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
save_run_record(prefix, {"config": cfg, "result": result})
|
|
252
|
+
print(f"💾 Saved SSVQE run to {result_path}")
|
|
253
|
+
|
|
254
|
+
# ============================================================
|
|
255
|
+
# 8. Optional plotting (E0 / E1 only, if available)
|
|
256
|
+
# ============================================================
|
|
257
|
+
if plot and num_states >= 2:
|
|
258
|
+
try:
|
|
259
|
+
E0 = energies_per_state[0]
|
|
260
|
+
E1 = energies_per_state[1]
|
|
261
|
+
plot_ssvqe_convergence_multi(
|
|
262
|
+
molecule,
|
|
263
|
+
E0_list=E0,
|
|
264
|
+
E1_list=E1,
|
|
265
|
+
optimizer_name=optimizer_name,
|
|
266
|
+
ansatz=ansatz_name,
|
|
267
|
+
)
|
|
268
|
+
except Exception as exc: # plotting is non-fatal
|
|
269
|
+
print(f"⚠️ SSVQE plotting failed (non-fatal): {exc}")
|
|
270
|
+
|
|
271
|
+
return result
|