superquantx 0.1.0__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.
- superquantx/__init__.py +321 -0
- superquantx/algorithms/__init__.py +55 -0
- superquantx/algorithms/base_algorithm.py +413 -0
- superquantx/algorithms/hybrid_classifier.py +628 -0
- superquantx/algorithms/qaoa.py +406 -0
- superquantx/algorithms/quantum_agents.py +1006 -0
- superquantx/algorithms/quantum_kmeans.py +575 -0
- superquantx/algorithms/quantum_nn.py +544 -0
- superquantx/algorithms/quantum_pca.py +499 -0
- superquantx/algorithms/quantum_svm.py +346 -0
- superquantx/algorithms/vqe.py +553 -0
- superquantx/algorithms.py +863 -0
- superquantx/backends/__init__.py +265 -0
- superquantx/backends/base_backend.py +321 -0
- superquantx/backends/braket_backend.py +420 -0
- superquantx/backends/cirq_backend.py +466 -0
- superquantx/backends/ocean_backend.py +491 -0
- superquantx/backends/pennylane_backend.py +419 -0
- superquantx/backends/qiskit_backend.py +451 -0
- superquantx/backends/simulator_backend.py +455 -0
- superquantx/backends/tket_backend.py +519 -0
- superquantx/circuits.py +447 -0
- superquantx/cli/__init__.py +28 -0
- superquantx/cli/commands.py +528 -0
- superquantx/cli/main.py +254 -0
- superquantx/client.py +298 -0
- superquantx/config.py +326 -0
- superquantx/exceptions.py +287 -0
- superquantx/gates.py +588 -0
- superquantx/logging_config.py +347 -0
- superquantx/measurements.py +702 -0
- superquantx/ml.py +936 -0
- superquantx/noise.py +760 -0
- superquantx/utils/__init__.py +83 -0
- superquantx/utils/benchmarking.py +523 -0
- superquantx/utils/classical_utils.py +575 -0
- superquantx/utils/feature_mapping.py +467 -0
- superquantx/utils/optimization.py +410 -0
- superquantx/utils/quantum_utils.py +456 -0
- superquantx/utils/visualization.py +654 -0
- superquantx/version.py +33 -0
- superquantx-0.1.0.dist-info/METADATA +365 -0
- superquantx-0.1.0.dist-info/RECORD +46 -0
- superquantx-0.1.0.dist-info/WHEEL +4 -0
- superquantx-0.1.0.dist-info/entry_points.txt +2 -0
- superquantx-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,553 @@
|
|
1
|
+
"""Variational Quantum Eigensolver (VQE) implementation.
|
2
|
+
|
3
|
+
This module provides a VQE implementation for finding ground state energies
|
4
|
+
and eigenvalues of quantum systems using parameterized quantum circuits.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
from scipy.optimize import minimize
|
12
|
+
|
13
|
+
from .base_algorithm import OptimizationQuantumAlgorithm
|
14
|
+
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
class VQE(OptimizationQuantumAlgorithm):
|
19
|
+
"""Variational Quantum Eigensolver for finding ground states.
|
20
|
+
|
21
|
+
VQE is a hybrid quantum-classical algorithm that uses a parameterized
|
22
|
+
quantum circuit (ansatz) to find the ground state energy of a given
|
23
|
+
Hamiltonian by minimizing the expectation value.
|
24
|
+
|
25
|
+
The algorithm works by:
|
26
|
+
1. Preparing a parameterized quantum state |ψ(θ)⟩
|
27
|
+
2. Measuring the expectation value ⟨ψ(θ)|H|ψ(θ)⟩
|
28
|
+
3. Classically optimizing parameters θ to minimize energy
|
29
|
+
4. Iterating until convergence
|
30
|
+
|
31
|
+
Args:
|
32
|
+
backend: Quantum backend for circuit execution
|
33
|
+
hamiltonian: Target Hamiltonian (matrix or operator)
|
34
|
+
ansatz: Parameterized circuit ansatz ('UCCSD', 'RealAmplitudes', etc.)
|
35
|
+
optimizer: Classical optimizer ('COBYLA', 'L-BFGS-B', etc.)
|
36
|
+
shots: Number of measurement shots
|
37
|
+
maxiter: Maximum optimization iterations
|
38
|
+
initial_params: Initial parameter values
|
39
|
+
**kwargs: Additional parameters
|
40
|
+
|
41
|
+
Example:
|
42
|
+
>>> # Define H2 molecule Hamiltonian
|
43
|
+
>>> H2_hamiltonian = create_h2_hamiltonian(bond_distance=0.74)
|
44
|
+
>>> vqe = VQE(backend='pennylane', hamiltonian=H2_hamiltonian, ansatz='UCCSD')
|
45
|
+
>>> result = vqe.optimize()
|
46
|
+
>>> ground_energy = result.result['optimal_value']
|
47
|
+
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
hamiltonian: Union[np.ndarray, Any],
|
53
|
+
ansatz: Union[str, Callable] = 'RealAmplitudes',
|
54
|
+
backend: Union[str, Any] = 'simulator',
|
55
|
+
optimizer: str = 'COBYLA',
|
56
|
+
shots: int = 1024,
|
57
|
+
maxiter: int = 1000,
|
58
|
+
initial_params: Optional[np.ndarray] = None,
|
59
|
+
include_custom_gates: bool = False,
|
60
|
+
client = None,
|
61
|
+
**kwargs
|
62
|
+
) -> None:
|
63
|
+
super().__init__(backend=backend, shots=shots, **kwargs)
|
64
|
+
|
65
|
+
self.hamiltonian = hamiltonian
|
66
|
+
self.ansatz = ansatz
|
67
|
+
self.optimizer = optimizer
|
68
|
+
self.maxiter = maxiter
|
69
|
+
self.initial_params = initial_params
|
70
|
+
self.include_custom_gates = include_custom_gates
|
71
|
+
self.client = client
|
72
|
+
|
73
|
+
# VQE-specific attributes
|
74
|
+
self.n_qubits = None
|
75
|
+
self.n_params = None
|
76
|
+
self.ansatz_circuit = None
|
77
|
+
self.hamiltonian_terms = None
|
78
|
+
|
79
|
+
# Convergence tracking
|
80
|
+
self.energy_history = []
|
81
|
+
self.gradient_history = []
|
82
|
+
self.convergence_threshold = 1e-6
|
83
|
+
|
84
|
+
self._initialize_hamiltonian()
|
85
|
+
|
86
|
+
logger.info(f"Initialized VQE with ansatz={ansatz}, optimizer={optimizer}")
|
87
|
+
|
88
|
+
def _initialize_hamiltonian(self) -> None:
|
89
|
+
"""Initialize and validate Hamiltonian."""
|
90
|
+
if isinstance(self.hamiltonian, np.ndarray):
|
91
|
+
if len(self.hamiltonian.shape) != 2 or self.hamiltonian.shape[0] != self.hamiltonian.shape[1]:
|
92
|
+
raise ValueError("Hamiltonian must be a square matrix")
|
93
|
+
self.n_qubits = int(np.log2(self.hamiltonian.shape[0]))
|
94
|
+
if 2**self.n_qubits != self.hamiltonian.shape[0]:
|
95
|
+
raise ValueError("Hamiltonian dimension must be a power of 2")
|
96
|
+
|
97
|
+
# Decompose Hamiltonian into Pauli strings if needed
|
98
|
+
self.hamiltonian_terms = self._decompose_hamiltonian()
|
99
|
+
else:
|
100
|
+
# Assume it's already in the correct format for the backend
|
101
|
+
self.hamiltonian_terms = self.hamiltonian
|
102
|
+
self.n_qubits = self._infer_qubits_from_hamiltonian()
|
103
|
+
|
104
|
+
def _decompose_hamiltonian(self) -> List[Tuple[float, str]]:
|
105
|
+
"""Decompose Hamiltonian into Pauli string representation."""
|
106
|
+
if hasattr(self.backend, 'decompose_hamiltonian'):
|
107
|
+
return self.backend.decompose_hamiltonian(self.hamiltonian)
|
108
|
+
else:
|
109
|
+
return self._fallback_decomposition()
|
110
|
+
|
111
|
+
def _fallback_decomposition(self) -> List[Tuple[float, str]]:
|
112
|
+
"""Fallback Hamiltonian decomposition."""
|
113
|
+
logger.warning("Using fallback Hamiltonian decomposition")
|
114
|
+
# Simple placeholder - would need proper Pauli decomposition
|
115
|
+
return [(1.0, 'Z0'), (0.5, 'Z1')]
|
116
|
+
|
117
|
+
def _infer_qubits_from_hamiltonian(self) -> int:
|
118
|
+
"""Infer number of qubits from Hamiltonian representation."""
|
119
|
+
if hasattr(self.hamiltonian_terms, '__len__'):
|
120
|
+
return 2 # Default fallback
|
121
|
+
return 2
|
122
|
+
|
123
|
+
def _create_ansatz_circuit(self, params: np.ndarray) -> Any:
|
124
|
+
"""Create ansatz circuit with given parameters.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
params: Circuit parameters
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Parameterized quantum circuit
|
131
|
+
|
132
|
+
"""
|
133
|
+
try:
|
134
|
+
if hasattr(self.backend, 'create_ansatz'):
|
135
|
+
return self.backend.create_ansatz(
|
136
|
+
ansatz_type=self.ansatz,
|
137
|
+
n_qubits=self.n_qubits,
|
138
|
+
params=params,
|
139
|
+
include_custom_gates=self.include_custom_gates
|
140
|
+
)
|
141
|
+
else:
|
142
|
+
return self._fallback_ansatz(params)
|
143
|
+
except Exception as e:
|
144
|
+
logger.error(f"Failed to create ansatz circuit: {e}")
|
145
|
+
return self._fallback_ansatz(params)
|
146
|
+
|
147
|
+
def _fallback_ansatz(self, params: np.ndarray) -> Any:
|
148
|
+
"""Fallback ansatz implementation."""
|
149
|
+
logger.warning("Using fallback ansatz implementation")
|
150
|
+
return None
|
151
|
+
|
152
|
+
def _compute_expectation_value(self, params: np.ndarray) -> float:
|
153
|
+
"""Compute expectation value ⟨ψ(θ)|H|ψ(θ)⟩.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
params: Circuit parameters
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
Hamiltonian expectation value
|
160
|
+
|
161
|
+
"""
|
162
|
+
try:
|
163
|
+
# Create ansatz circuit
|
164
|
+
circuit = self._create_ansatz_circuit(params)
|
165
|
+
|
166
|
+
# Compute expectation value
|
167
|
+
if hasattr(self.backend, 'compute_expectation'):
|
168
|
+
expectation = self.backend.compute_expectation(
|
169
|
+
circuit=circuit,
|
170
|
+
hamiltonian=self.hamiltonian_terms,
|
171
|
+
shots=self.shots
|
172
|
+
)
|
173
|
+
else:
|
174
|
+
expectation = self._fallback_expectation(circuit, params)
|
175
|
+
|
176
|
+
# Store energy history
|
177
|
+
self.energy_history.append(expectation)
|
178
|
+
|
179
|
+
return float(expectation)
|
180
|
+
|
181
|
+
except Exception as e:
|
182
|
+
logger.error(f"Error computing expectation value: {e}")
|
183
|
+
return float('inf')
|
184
|
+
|
185
|
+
def _fallback_expectation(self, circuit: Any, params: np.ndarray) -> float:
|
186
|
+
"""Fallback expectation value computation."""
|
187
|
+
logger.warning("Using fallback expectation computation")
|
188
|
+
# Simple placeholder - would compute ⟨ψ|H|ψ⟩ classically
|
189
|
+
return np.random.random() - 0.5
|
190
|
+
|
191
|
+
def _compute_gradient(self, params: np.ndarray) -> np.ndarray:
|
192
|
+
"""Compute parameter gradients using parameter-shift rule.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
params: Current parameters
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
Gradient vector
|
199
|
+
|
200
|
+
"""
|
201
|
+
gradients = np.zeros_like(params)
|
202
|
+
shift = np.pi / 2 # Parameter-shift rule
|
203
|
+
|
204
|
+
for i in range(len(params)):
|
205
|
+
# Forward shift
|
206
|
+
params_plus = params.copy()
|
207
|
+
params_plus[i] += shift
|
208
|
+
energy_plus = self._compute_expectation_value(params_plus)
|
209
|
+
|
210
|
+
# Backward shift
|
211
|
+
params_minus = params.copy()
|
212
|
+
params_minus[i] -= shift
|
213
|
+
energy_minus = self._compute_expectation_value(params_minus)
|
214
|
+
|
215
|
+
# Gradient via parameter-shift rule
|
216
|
+
gradients[i] = 0.5 * (energy_plus - energy_minus)
|
217
|
+
|
218
|
+
self.gradient_history.append(np.linalg.norm(gradients))
|
219
|
+
return gradients
|
220
|
+
|
221
|
+
def fit(self, X: Optional[np.ndarray] = None, y: Optional[np.ndarray] = None, **kwargs) -> 'VQE':
|
222
|
+
"""Fit VQE (setup for optimization).
|
223
|
+
|
224
|
+
Args:
|
225
|
+
X: Not used in VQE
|
226
|
+
y: Not used in VQE
|
227
|
+
**kwargs: Additional parameters
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
Self for method chaining
|
231
|
+
|
232
|
+
"""
|
233
|
+
logger.info(f"Setting up VQE for {self.n_qubits} qubits")
|
234
|
+
|
235
|
+
# Determine number of parameters based on ansatz
|
236
|
+
self.n_params = self._get_ansatz_param_count()
|
237
|
+
|
238
|
+
# Initialize parameters if not provided
|
239
|
+
if self.initial_params is None:
|
240
|
+
self.initial_params = self._generate_initial_params()
|
241
|
+
|
242
|
+
# Reset histories
|
243
|
+
self.energy_history = []
|
244
|
+
self.gradient_history = []
|
245
|
+
self.optimization_history_ = []
|
246
|
+
|
247
|
+
self.is_fitted = True
|
248
|
+
return self
|
249
|
+
|
250
|
+
def _get_ansatz_param_count(self) -> int:
|
251
|
+
"""Get number of parameters for the ansatz."""
|
252
|
+
if hasattr(self.backend, 'get_ansatz_param_count'):
|
253
|
+
return self.backend.get_ansatz_param_count(self.ansatz, self.n_qubits)
|
254
|
+
else:
|
255
|
+
# Default parameter counts for common ansatzes
|
256
|
+
param_counts = {
|
257
|
+
'RealAmplitudes': 2 * self.n_qubits,
|
258
|
+
'UCCSD': 4 * self.n_qubits, # Simplified estimate
|
259
|
+
'EfficientSU2': 3 * self.n_qubits,
|
260
|
+
'TwoLocal': 2 * self.n_qubits,
|
261
|
+
}
|
262
|
+
return param_counts.get(self.ansatz, 2 * self.n_qubits)
|
263
|
+
|
264
|
+
def _generate_initial_params(self) -> np.ndarray:
|
265
|
+
"""Generate random initial parameters."""
|
266
|
+
return np.random.uniform(-np.pi, np.pi, self.n_params)
|
267
|
+
|
268
|
+
def predict(self, X: Optional[np.ndarray] = None, **kwargs) -> np.ndarray:
|
269
|
+
"""Get ground state wavefunction coefficients.
|
270
|
+
|
271
|
+
Args:
|
272
|
+
X: Not used
|
273
|
+
**kwargs: Additional parameters
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
Ground state wavefunction
|
277
|
+
|
278
|
+
"""
|
279
|
+
if not self.optimal_params_:
|
280
|
+
raise ValueError("VQE must be optimized before prediction")
|
281
|
+
|
282
|
+
# Create circuit with optimal parameters
|
283
|
+
circuit = self._create_ansatz_circuit(self.optimal_params_)
|
284
|
+
|
285
|
+
# Get state vector
|
286
|
+
if hasattr(self.backend, 'get_statevector'):
|
287
|
+
statevector = self.backend.get_statevector(circuit)
|
288
|
+
else:
|
289
|
+
# Fallback: return random normalized state
|
290
|
+
statevector = np.random.random(2**self.n_qubits) + 1j * np.random.random(2**self.n_qubits)
|
291
|
+
statevector /= np.linalg.norm(statevector)
|
292
|
+
|
293
|
+
return np.array(statevector)
|
294
|
+
|
295
|
+
def _run_optimization(self, objective_function=None, initial_params: Optional[np.ndarray] = None, **kwargs):
|
296
|
+
"""Run VQE optimization.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
objective_function: Not used (VQE has its own objective)
|
300
|
+
initial_params: Initial parameter guess
|
301
|
+
**kwargs: Additional optimization parameters
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
Optimization result
|
305
|
+
|
306
|
+
"""
|
307
|
+
if not self.is_fitted:
|
308
|
+
raise ValueError("VQE must be fitted before optimization")
|
309
|
+
|
310
|
+
# Use provided initial parameters or default
|
311
|
+
if initial_params is None:
|
312
|
+
initial_params = self.initial_params
|
313
|
+
|
314
|
+
logger.info(f"Starting VQE optimization with {len(initial_params)} parameters")
|
315
|
+
|
316
|
+
# Define objective function for minimization
|
317
|
+
def objective(params):
|
318
|
+
energy = self._compute_expectation_value(params)
|
319
|
+
|
320
|
+
# Store optimization history
|
321
|
+
self.optimization_history_.append({
|
322
|
+
'params': params.copy(),
|
323
|
+
'energy': energy,
|
324
|
+
'iteration': len(self.optimization_history_)
|
325
|
+
})
|
326
|
+
|
327
|
+
return energy
|
328
|
+
|
329
|
+
# Run classical optimization
|
330
|
+
try:
|
331
|
+
result = minimize(
|
332
|
+
fun=objective,
|
333
|
+
x0=initial_params,
|
334
|
+
method=self.optimizer,
|
335
|
+
options={
|
336
|
+
'maxiter': self.maxiter,
|
337
|
+
'disp': True
|
338
|
+
},
|
339
|
+
jac=self._compute_gradient if self.optimizer in ['L-BFGS-B', 'SLSQP'] else None
|
340
|
+
)
|
341
|
+
|
342
|
+
self.optimal_params_ = result.x
|
343
|
+
self.optimal_value_ = result.fun
|
344
|
+
|
345
|
+
logger.info(f"VQE optimization completed. Ground energy: {self.optimal_value_:.6f}")
|
346
|
+
|
347
|
+
return {
|
348
|
+
'optimal_params': self.optimal_params_,
|
349
|
+
'ground_energy': self.optimal_value_,
|
350
|
+
'success': result.success,
|
351
|
+
'message': result.message,
|
352
|
+
'n_iterations': result.nfev,
|
353
|
+
}
|
354
|
+
|
355
|
+
except Exception as e:
|
356
|
+
logger.error(f"VQE optimization failed: {e}")
|
357
|
+
raise
|
358
|
+
|
359
|
+
def get_energy_landscape(self, param_indices: List[int], param_ranges: List[Tuple[float, float]],
|
360
|
+
resolution: int = 20) -> Dict[str, Any]:
|
361
|
+
"""Compute energy landscape for visualization.
|
362
|
+
|
363
|
+
Args:
|
364
|
+
param_indices: Indices of parameters to vary
|
365
|
+
param_ranges: Ranges for each parameter
|
366
|
+
resolution: Number of points per dimension
|
367
|
+
|
368
|
+
Returns:
|
369
|
+
Dictionary with landscape data
|
370
|
+
|
371
|
+
"""
|
372
|
+
if len(param_indices) != 2:
|
373
|
+
raise ValueError("Energy landscape visualization supports only 2 parameters")
|
374
|
+
|
375
|
+
if not self.optimal_params_:
|
376
|
+
raise ValueError("VQE must be optimized to compute landscape")
|
377
|
+
|
378
|
+
param1_range = np.linspace(*param_ranges[0], resolution)
|
379
|
+
param2_range = np.linspace(*param_ranges[1], resolution)
|
380
|
+
|
381
|
+
landscape = np.zeros((resolution, resolution))
|
382
|
+
base_params = self.optimal_params_.copy()
|
383
|
+
|
384
|
+
for i, p1 in enumerate(param1_range):
|
385
|
+
for j, p2 in enumerate(param2_range):
|
386
|
+
params = base_params.copy()
|
387
|
+
params[param_indices[0]] = p1
|
388
|
+
params[param_indices[1]] = p2
|
389
|
+
landscape[i, j] = self._compute_expectation_value(params)
|
390
|
+
|
391
|
+
return {
|
392
|
+
'param1_range': param1_range,
|
393
|
+
'param2_range': param2_range,
|
394
|
+
'landscape': landscape,
|
395
|
+
'optimal_params': self.optimal_params_[param_indices],
|
396
|
+
'param_indices': param_indices
|
397
|
+
}
|
398
|
+
|
399
|
+
def analyze_convergence(self) -> Dict[str, Any]:
|
400
|
+
"""Analyze VQE convergence properties.
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
Convergence analysis results
|
404
|
+
|
405
|
+
"""
|
406
|
+
if not self.energy_history:
|
407
|
+
raise ValueError("No optimization history available")
|
408
|
+
|
409
|
+
energies = np.array(self.energy_history)
|
410
|
+
gradients = np.array(self.gradient_history) if self.gradient_history else None
|
411
|
+
|
412
|
+
# Basic convergence metrics
|
413
|
+
analysis = {
|
414
|
+
'final_energy': energies[-1],
|
415
|
+
'energy_variance': np.var(energies[-10:]) if len(energies) >= 10 else np.var(energies),
|
416
|
+
'total_iterations': len(energies),
|
417
|
+
'energy_change': abs(energies[-1] - energies[0]) if len(energies) > 1 else 0,
|
418
|
+
}
|
419
|
+
|
420
|
+
# Convergence detection
|
421
|
+
if len(energies) >= 10:
|
422
|
+
recent_change = abs(energies[-1] - energies[-10])
|
423
|
+
analysis['converged'] = recent_change < self.convergence_threshold
|
424
|
+
else:
|
425
|
+
analysis['converged'] = False
|
426
|
+
|
427
|
+
# Gradient analysis
|
428
|
+
if gradients is not None and len(gradients) > 0:
|
429
|
+
analysis.update({
|
430
|
+
'final_gradient_norm': gradients[-1],
|
431
|
+
'gradient_trend': 'decreasing' if gradients[-1] < gradients[0] else 'increasing',
|
432
|
+
'min_gradient_norm': np.min(gradients),
|
433
|
+
})
|
434
|
+
|
435
|
+
# Identify plateaus and oscillations
|
436
|
+
if len(energies) >= 20:
|
437
|
+
# Check for plateaus (little change over many iterations)
|
438
|
+
plateau_threshold = self.convergence_threshold * 10
|
439
|
+
recent_energies = energies[-20:]
|
440
|
+
energy_std = np.std(recent_energies)
|
441
|
+
analysis['plateau_detected'] = energy_std < plateau_threshold
|
442
|
+
|
443
|
+
# Check for oscillations
|
444
|
+
energy_diff = np.diff(energies[-20:])
|
445
|
+
sign_changes = np.sum(np.diff(np.sign(energy_diff)) != 0)
|
446
|
+
analysis['oscillation_detected'] = sign_changes > len(energy_diff) * 0.7
|
447
|
+
|
448
|
+
return analysis
|
449
|
+
|
450
|
+
def compare_with_exact(self, exact_ground_energy: float) -> Dict[str, Any]:
|
451
|
+
"""Compare VQE result with exact ground state energy.
|
452
|
+
|
453
|
+
Args:
|
454
|
+
exact_ground_energy: Known exact ground state energy
|
455
|
+
|
456
|
+
Returns:
|
457
|
+
Comparison analysis
|
458
|
+
|
459
|
+
"""
|
460
|
+
if not self.optimal_value_:
|
461
|
+
raise ValueError("VQE must be optimized for comparison")
|
462
|
+
|
463
|
+
error = abs(self.optimal_value_ - exact_ground_energy)
|
464
|
+
relative_error = error / abs(exact_ground_energy) if exact_ground_energy != 0 else float('inf')
|
465
|
+
|
466
|
+
return {
|
467
|
+
'vqe_energy': self.optimal_value_,
|
468
|
+
'exact_energy': exact_ground_energy,
|
469
|
+
'absolute_error': error,
|
470
|
+
'relative_error': relative_error,
|
471
|
+
'chemical_accuracy': error < 1.6e-3, # 1 kcal/mol in Hartree
|
472
|
+
'energy_above_ground': max(0, self.optimal_value_ - exact_ground_energy)
|
473
|
+
}
|
474
|
+
|
475
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
476
|
+
"""Get VQE parameters."""
|
477
|
+
params = super().get_params(deep)
|
478
|
+
params.update({
|
479
|
+
'ansatz': self.ansatz,
|
480
|
+
'optimizer': self.optimizer,
|
481
|
+
'maxiter': self.maxiter,
|
482
|
+
'include_custom_gates': self.include_custom_gates,
|
483
|
+
'convergence_threshold': self.convergence_threshold,
|
484
|
+
})
|
485
|
+
return params
|
486
|
+
|
487
|
+
def set_params(self, **params) -> 'VQE':
|
488
|
+
"""Set VQE parameters."""
|
489
|
+
if self.is_fitted and any(key in params for key in ['ansatz', 'hamiltonian']):
|
490
|
+
logger.warning("Changing core parameters requires refitting the model")
|
491
|
+
self.is_fitted = False
|
492
|
+
|
493
|
+
return super().set_params(**params)
|
494
|
+
|
495
|
+
|
496
|
+
def create_vqe_for_molecule(
|
497
|
+
molecule_name: str,
|
498
|
+
bond_distance: float = None,
|
499
|
+
backend: str = 'simulator',
|
500
|
+
ansatz: str = 'UCCSD',
|
501
|
+
optimizer: str = 'COBYLA',
|
502
|
+
client = None
|
503
|
+
) -> VQE:
|
504
|
+
"""Create a VQE instance pre-configured for molecular simulation.
|
505
|
+
|
506
|
+
Args:
|
507
|
+
molecule_name: Name of the molecule (e.g., 'H2', 'LiH')
|
508
|
+
bond_distance: Bond distance for the molecule (uses default if None)
|
509
|
+
backend: Quantum backend to use
|
510
|
+
ansatz: Ansatz circuit type
|
511
|
+
optimizer: Classical optimizer
|
512
|
+
client: Optional client for quantum execution
|
513
|
+
|
514
|
+
Returns:
|
515
|
+
Configured VQE instance
|
516
|
+
|
517
|
+
"""
|
518
|
+
# Import molecular data utilities
|
519
|
+
try:
|
520
|
+
from ..datasets.molecular import get_molecular_hamiltonian
|
521
|
+
hamiltonian = get_molecular_hamiltonian(molecule_name, bond_distance)
|
522
|
+
except ImportError:
|
523
|
+
# Fallback: create simple hamiltonian for testing
|
524
|
+
from ..gates import Hamiltonian
|
525
|
+
|
526
|
+
if molecule_name.upper() == 'H2':
|
527
|
+
# Simple H2 Hamiltonian approximation as Pauli strings
|
528
|
+
hamiltonian_dict = {
|
529
|
+
"ZZ": -1.0523732,
|
530
|
+
"ZI": -0.39793742,
|
531
|
+
"IZ": -0.39793742,
|
532
|
+
"XX": -0.01128010,
|
533
|
+
"YY": 0.01128010
|
534
|
+
}
|
535
|
+
hamiltonian = Hamiltonian.from_dict(hamiltonian_dict)
|
536
|
+
elif molecule_name.upper() in ['LIH', 'H2O', 'NH3']:
|
537
|
+
# Generic 2-qubit Hamiltonian for other molecules
|
538
|
+
hamiltonian_dict = {
|
539
|
+
"ZI": -1.0,
|
540
|
+
"IZ": 0.5,
|
541
|
+
"XX": 0.2
|
542
|
+
}
|
543
|
+
hamiltonian = Hamiltonian.from_dict(hamiltonian_dict)
|
544
|
+
else:
|
545
|
+
raise ValueError(f"Unknown molecule: {molecule_name}")
|
546
|
+
|
547
|
+
return VQE(
|
548
|
+
hamiltonian=hamiltonian,
|
549
|
+
ansatz=ansatz,
|
550
|
+
backend=backend,
|
551
|
+
optimizer=optimizer,
|
552
|
+
client=client
|
553
|
+
)
|