wings-quantum 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.
- wings/__init__.py +251 -0
- wings/adam.py +132 -0
- wings/ansatz.py +207 -0
- wings/benchmarks.py +605 -0
- wings/campaign.py +661 -0
- wings/cli.py +377 -0
- wings/compat.py +132 -0
- wings/config.py +443 -0
- wings/convenience.py +259 -0
- wings/evaluators/__init__.py +19 -0
- wings/evaluators/cpu.py +72 -0
- wings/evaluators/custatevec.py +783 -0
- wings/evaluators/gpu.py +220 -0
- wings/export.py +243 -0
- wings/optimizer.py +1898 -0
- wings/paths.py +295 -0
- wings/py.typed +2 -0
- wings/results.py +255 -0
- wings/types.py +14 -0
- wings_quantum-0.1.0.dist-info/METADATA +491 -0
- wings_quantum-0.1.0.dist-info/RECORD +25 -0
- wings_quantum-0.1.0.dist-info/WHEEL +5 -0
- wings_quantum-0.1.0.dist-info/entry_points.txt +2 -0
- wings_quantum-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- wings_quantum-0.1.0.dist-info/top_level.txt +1 -0
wings/evaluators/gpu.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""GPU-accelerated circuit evaluator using Qiskit Aer."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from qiskit.circuit import ParameterVector
|
|
8
|
+
from qiskit.quantum_info import Statevector
|
|
9
|
+
|
|
10
|
+
from ..ansatz import get_full_variational_quantum_circuit
|
|
11
|
+
|
|
12
|
+
# Conditional import
|
|
13
|
+
try:
|
|
14
|
+
from qiskit_aer import AerSimulator
|
|
15
|
+
|
|
16
|
+
HAS_AER_GPU = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_AER_GPU = False
|
|
19
|
+
AerSimulator = None
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..config import OptimizerConfig
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
__all__ = ["GPUCircuitEvaluator", "HAS_AER_GPU"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GPUCircuitEvaluator:
|
|
30
|
+
"""
|
|
31
|
+
GPU-accelerated circuit evaluation using Qiskit Aer.
|
|
32
|
+
|
|
33
|
+
Features:
|
|
34
|
+
- Double precision for high-accuracy results
|
|
35
|
+
- Batched execution for efficiency
|
|
36
|
+
- Automatic CPU fallback
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: "OptimizerConfig", target: np.ndarray):
|
|
40
|
+
self.config = config
|
|
41
|
+
self.target = target
|
|
42
|
+
self._target_conj = np.conj(target)
|
|
43
|
+
self.n_params = config.n_params
|
|
44
|
+
self.n_qubits = config.n_qubits
|
|
45
|
+
|
|
46
|
+
# Build circuit template
|
|
47
|
+
self.param_vector = ParameterVector("theta", self.n_params)
|
|
48
|
+
self.circuit = get_full_variational_quantum_circuit(
|
|
49
|
+
thetas=self.param_vector,
|
|
50
|
+
D2=config.n_qubits,
|
|
51
|
+
qubits_num=config.n_qubits,
|
|
52
|
+
input_state=None,
|
|
53
|
+
)
|
|
54
|
+
self._param_list = list(self.param_vector)
|
|
55
|
+
|
|
56
|
+
# Initialize GPU backend
|
|
57
|
+
self._init_gpu_backend()
|
|
58
|
+
|
|
59
|
+
# Statistics
|
|
60
|
+
self.n_gpu_calls = 0
|
|
61
|
+
self.n_circuits_evaluated = 0
|
|
62
|
+
|
|
63
|
+
def _init_gpu_backend(self):
|
|
64
|
+
"""Initialize GPU backend with appropriate settings"""
|
|
65
|
+
self.gpu_available = False
|
|
66
|
+
self.backend = None
|
|
67
|
+
|
|
68
|
+
if not HAS_AER_GPU:
|
|
69
|
+
print(" Aer not available, using CPU statevector")
|
|
70
|
+
self._use_cpu_fallback()
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# Try to create GPU backend
|
|
75
|
+
self.backend = AerSimulator(
|
|
76
|
+
method="statevector",
|
|
77
|
+
device="GPU",
|
|
78
|
+
precision=self.config.gpu_precision, # 'double' for high precision
|
|
79
|
+
blocking_enable=self.config.gpu_blocking,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Verify GPU is actually available
|
|
83
|
+
available_devices = self.backend.available_devices()
|
|
84
|
+
|
|
85
|
+
if "GPU" in available_devices:
|
|
86
|
+
self.gpu_available = True
|
|
87
|
+
print(" ✓ GPU backend initialized")
|
|
88
|
+
print(f" Precision: {self.config.gpu_precision}")
|
|
89
|
+
print(f" Available devices: {available_devices}")
|
|
90
|
+
else:
|
|
91
|
+
print(f" GPU not in available devices: {available_devices}")
|
|
92
|
+
self._use_cpu_fallback()
|
|
93
|
+
|
|
94
|
+
except ImportError as e:
|
|
95
|
+
logger.info(f"GPU backend not available (missing dependency): {e}")
|
|
96
|
+
self._use_cpu_fallback()
|
|
97
|
+
except RuntimeError as e:
|
|
98
|
+
logger.warning(f"GPU initialization failed (runtime error): {e}")
|
|
99
|
+
self._use_cpu_fallback()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(f"Unexpected GPU initialization error ({type(e).__name__}): {e}")
|
|
102
|
+
self._use_cpu_fallback()
|
|
103
|
+
|
|
104
|
+
def _use_cpu_fallback(self):
|
|
105
|
+
"""Fall back to CPU Aer backend"""
|
|
106
|
+
print(" Using CPU Aer backend (fallback)")
|
|
107
|
+
try:
|
|
108
|
+
self.backend = AerSimulator(
|
|
109
|
+
method="statevector",
|
|
110
|
+
device="CPU",
|
|
111
|
+
precision="double",
|
|
112
|
+
)
|
|
113
|
+
self.gpu_available = False
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f" CPU Aer also failed: {e}")
|
|
116
|
+
self.backend = None
|
|
117
|
+
|
|
118
|
+
def get_statevector(self, params: np.ndarray) -> np.ndarray:
|
|
119
|
+
"""
|
|
120
|
+
Get statevector for single parameter set.
|
|
121
|
+
|
|
122
|
+
Uses GPU if available, otherwise falls back to standard Qiskit.
|
|
123
|
+
"""
|
|
124
|
+
if self.backend is None:
|
|
125
|
+
# Ultimate fallback: use standard Qiskit Statevector
|
|
126
|
+
bound_circuit = self.circuit.assign_parameters(dict(zip(self._param_list, params)))
|
|
127
|
+
return Statevector(bound_circuit).data
|
|
128
|
+
|
|
129
|
+
# Bind parameters
|
|
130
|
+
bound_circuit = self.circuit.assign_parameters(dict(zip(self._param_list, params)))
|
|
131
|
+
|
|
132
|
+
# Add save_statevector instruction
|
|
133
|
+
bound_circuit.save_statevector()
|
|
134
|
+
|
|
135
|
+
# Execute on GPU/CPU Aer
|
|
136
|
+
job = self.backend.run(bound_circuit, shots=1)
|
|
137
|
+
result = job.result()
|
|
138
|
+
statevector = result.get_statevector()
|
|
139
|
+
|
|
140
|
+
self.n_gpu_calls += 1
|
|
141
|
+
self.n_circuits_evaluated += 1
|
|
142
|
+
|
|
143
|
+
return np.array(statevector.data, dtype=np.complex128)
|
|
144
|
+
|
|
145
|
+
def get_statevectors_batched(self, params_batch: np.ndarray) -> list[np.ndarray]:
|
|
146
|
+
"""
|
|
147
|
+
Get statevectors for multiple parameter sets in a single GPU call.
|
|
148
|
+
|
|
149
|
+
This is much more efficient than individual calls because:
|
|
150
|
+
1. Single data transfer to GPU
|
|
151
|
+
2. GPU parallelism across circuits
|
|
152
|
+
3. Single result retrieval
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
params_batch: Array of shape (batch_size, n_params)
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of statevectors
|
|
159
|
+
"""
|
|
160
|
+
batch_size = len(params_batch)
|
|
161
|
+
|
|
162
|
+
if self.backend is None:
|
|
163
|
+
# Fallback: sequential evaluation
|
|
164
|
+
return [self.get_statevector(p) for p in params_batch]
|
|
165
|
+
|
|
166
|
+
# Build all circuits
|
|
167
|
+
circuits = []
|
|
168
|
+
for params in params_batch:
|
|
169
|
+
bound_circuit = self.circuit.assign_parameters(dict(zip(self._param_list, params)))
|
|
170
|
+
bound_circuit.save_statevector()
|
|
171
|
+
circuits.append(bound_circuit)
|
|
172
|
+
|
|
173
|
+
# Single batched GPU execution
|
|
174
|
+
job = self.backend.run(circuits, shots=1)
|
|
175
|
+
result = job.result()
|
|
176
|
+
|
|
177
|
+
# Extract all statevectors
|
|
178
|
+
statevectors = []
|
|
179
|
+
for i in range(batch_size):
|
|
180
|
+
sv = result.get_statevector(i)
|
|
181
|
+
statevectors.append(np.array(sv.data, dtype=np.complex128))
|
|
182
|
+
|
|
183
|
+
self.n_gpu_calls += 1
|
|
184
|
+
self.n_circuits_evaluated += batch_size
|
|
185
|
+
|
|
186
|
+
return statevectors
|
|
187
|
+
|
|
188
|
+
def compute_fidelity(self, params: np.ndarray) -> float:
|
|
189
|
+
"""Compute fidelity for single parameter set"""
|
|
190
|
+
psi = self.get_statevector(params)
|
|
191
|
+
overlap = np.dot(self._target_conj, psi)
|
|
192
|
+
return overlap.real**2 + overlap.imag**2
|
|
193
|
+
|
|
194
|
+
def compute_fidelities_batched(self, params_batch: np.ndarray) -> np.ndarray:
|
|
195
|
+
"""
|
|
196
|
+
Compute fidelities for batch of parameter sets.
|
|
197
|
+
|
|
198
|
+
Optimized for GPU: single batched circuit execution,
|
|
199
|
+
then vectorized fidelity computation on CPU.
|
|
200
|
+
"""
|
|
201
|
+
statevectors = self.get_statevectors_batched(params_batch)
|
|
202
|
+
|
|
203
|
+
# Vectorized fidelity computation
|
|
204
|
+
fidelities = np.zeros(len(params_batch))
|
|
205
|
+
for i, psi in enumerate(statevectors):
|
|
206
|
+
overlap = np.dot(self._target_conj, psi)
|
|
207
|
+
fidelities[i] = overlap.real**2 + overlap.imag**2
|
|
208
|
+
|
|
209
|
+
return fidelities
|
|
210
|
+
|
|
211
|
+
def get_stats(self) -> dict:
|
|
212
|
+
"""Return GPU usage statistics"""
|
|
213
|
+
return {
|
|
214
|
+
"gpu_available": self.gpu_available,
|
|
215
|
+
"n_gpu_calls": self.n_gpu_calls,
|
|
216
|
+
"n_circuits_evaluated": self.n_circuits_evaluated,
|
|
217
|
+
"circuits_per_call": (
|
|
218
|
+
self.n_circuits_evaluated / self.n_gpu_calls if self.n_gpu_calls > 0 else 0
|
|
219
|
+
),
|
|
220
|
+
}
|
wings/export.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Circuit export utilities for WINGS.
|
|
2
|
+
|
|
3
|
+
This module provides functions to export optimized circuits to various formats,
|
|
4
|
+
including OpenQASM 2.0, OpenQASM 3.0, and Qiskit QuantumCircuit objects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from qiskit import QuantumCircuit
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .optimizer import GaussianOptimizer
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"build_optimized_circuit",
|
|
19
|
+
"export_to_qasm",
|
|
20
|
+
"export_to_qasm3",
|
|
21
|
+
"save_circuit",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_optimized_circuit(
|
|
26
|
+
optimizer: "GaussianOptimizer",
|
|
27
|
+
params: Optional[NDArray[np.float64]] = None,
|
|
28
|
+
include_measurements: bool = False,
|
|
29
|
+
) -> QuantumCircuit:
|
|
30
|
+
"""
|
|
31
|
+
Build a concrete QuantumCircuit with optimized parameters bound.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
optimizer : GaussianOptimizer
|
|
36
|
+
The optimizer instance containing the ansatz
|
|
37
|
+
params : np.ndarray, optional
|
|
38
|
+
Parameter values to bind. If None, uses optimizer.best_params
|
|
39
|
+
include_measurements : bool, default False
|
|
40
|
+
Whether to add measurement gates to all qubits
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
QuantumCircuit
|
|
45
|
+
Qiskit circuit with parameters bound to concrete values
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
ValueError
|
|
50
|
+
If no parameters provided and optimizer has no best_params
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> results = optimizer.optimize_ultra_precision(target_infidelity=1e-10)
|
|
55
|
+
>>> circuit = build_optimized_circuit(optimizer)
|
|
56
|
+
>>> print(circuit.draw())
|
|
57
|
+
"""
|
|
58
|
+
# Get parameters
|
|
59
|
+
if params is None:
|
|
60
|
+
if optimizer.best_params is None:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"No parameters provided and optimizer has no best_params. "
|
|
63
|
+
"Run optimization first or provide params explicitly."
|
|
64
|
+
)
|
|
65
|
+
params = optimizer.best_params
|
|
66
|
+
|
|
67
|
+
if optimizer.ansatz is None:
|
|
68
|
+
raise ValueError("Optimizer has no ansatz defined")
|
|
69
|
+
|
|
70
|
+
# Build circuit using ansatz
|
|
71
|
+
circuit = optimizer.ansatz(
|
|
72
|
+
params, optimizer.config.n_qubits, **(optimizer.config.ansatz_kwargs or {})
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# If circuit still has unbound parameters, bind them
|
|
76
|
+
if circuit.parameters:
|
|
77
|
+
param_dict = dict(zip(circuit.parameters, params))
|
|
78
|
+
circuit = circuit.assign_parameters(param_dict)
|
|
79
|
+
|
|
80
|
+
# Optionally add measurements
|
|
81
|
+
if include_measurements:
|
|
82
|
+
circuit.measure_all()
|
|
83
|
+
|
|
84
|
+
return circuit
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def export_to_qasm(
|
|
88
|
+
optimizer: "GaussianOptimizer",
|
|
89
|
+
params: Optional[NDArray[np.float64]] = None,
|
|
90
|
+
include_measurements: bool = False,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Export optimized circuit to OpenQASM 2.0 string.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
optimizer : GaussianOptimizer
|
|
98
|
+
The optimizer instance
|
|
99
|
+
params : np.ndarray, optional
|
|
100
|
+
Parameter values. If None, uses optimizer.best_params
|
|
101
|
+
include_measurements : bool, default False
|
|
102
|
+
Whether to include measurement gates
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
str
|
|
107
|
+
OpenQASM 2.0 format string
|
|
108
|
+
|
|
109
|
+
Examples
|
|
110
|
+
--------
|
|
111
|
+
>>> qasm_str = export_to_qasm(optimizer)
|
|
112
|
+
>>> print(qasm_str)
|
|
113
|
+
OPENQASM 2.0;
|
|
114
|
+
include "qelib1.inc";
|
|
115
|
+
qreg q[8];
|
|
116
|
+
ry(0.123456) q[0];
|
|
117
|
+
...
|
|
118
|
+
"""
|
|
119
|
+
from qiskit.qasm2 import dumps
|
|
120
|
+
|
|
121
|
+
circuit = build_optimized_circuit(optimizer, params, include_measurements)
|
|
122
|
+
return dumps(circuit)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def export_to_qasm3(
|
|
126
|
+
optimizer: "GaussianOptimizer",
|
|
127
|
+
params: Optional[NDArray[np.float64]] = None,
|
|
128
|
+
include_measurements: bool = False,
|
|
129
|
+
) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Export optimized circuit to OpenQASM 3.0 string.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
optimizer : GaussianOptimizer
|
|
136
|
+
The optimizer instance
|
|
137
|
+
params : np.ndarray, optional
|
|
138
|
+
Parameter values. If None, uses optimizer.best_params
|
|
139
|
+
include_measurements : bool, default False
|
|
140
|
+
Whether to include measurement gates
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
str
|
|
145
|
+
OpenQASM 3.0 format string
|
|
146
|
+
|
|
147
|
+
Notes
|
|
148
|
+
-----
|
|
149
|
+
Requires qiskit >= 1.0 for OpenQASM 3.0 support.
|
|
150
|
+
"""
|
|
151
|
+
from qiskit.qasm3 import dumps
|
|
152
|
+
|
|
153
|
+
circuit = build_optimized_circuit(optimizer, params, include_measurements)
|
|
154
|
+
return dumps(circuit)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def save_circuit(
|
|
158
|
+
optimizer: "GaussianOptimizer",
|
|
159
|
+
filepath: Union[str, Path],
|
|
160
|
+
params: Optional[NDArray[np.float64]] = None,
|
|
161
|
+
format: str = "qasm",
|
|
162
|
+
include_measurements: bool = False,
|
|
163
|
+
**kwargs,
|
|
164
|
+
) -> Path:
|
|
165
|
+
"""
|
|
166
|
+
Save optimized circuit to file.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
optimizer : GaussianOptimizer
|
|
171
|
+
The optimizer instance
|
|
172
|
+
filepath : str or Path
|
|
173
|
+
Output file path. Extension determines format if format='auto'
|
|
174
|
+
params : np.ndarray, optional
|
|
175
|
+
Parameter values. If None, uses optimizer.best_params
|
|
176
|
+
format : str, default 'qasm'
|
|
177
|
+
Output format: 'qasm' (OpenQASM 2.0), 'qasm3' (OpenQASM 3.0),
|
|
178
|
+
'qpy' (Qiskit QPY binary), 'png' (circuit diagram), 'svg', 'pdf'
|
|
179
|
+
include_measurements : bool, default False
|
|
180
|
+
Whether to include measurement gates
|
|
181
|
+
**kwargs
|
|
182
|
+
Additional arguments passed to the export function
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
Path
|
|
187
|
+
Path to the saved file
|
|
188
|
+
|
|
189
|
+
Examples
|
|
190
|
+
--------
|
|
191
|
+
>>> save_circuit(optimizer, "my_circuit.qasm")
|
|
192
|
+
>>> save_circuit(optimizer, "my_circuit.png", format='png')
|
|
193
|
+
>>> save_circuit(optimizer, "my_circuit.qpy", format='qpy')
|
|
194
|
+
"""
|
|
195
|
+
filepath = Path(filepath)
|
|
196
|
+
|
|
197
|
+
# Auto-detect format from extension
|
|
198
|
+
if format == "auto":
|
|
199
|
+
ext = filepath.suffix.lower()
|
|
200
|
+
format_map = {
|
|
201
|
+
".qasm": "qasm",
|
|
202
|
+
".qasm3": "qasm3",
|
|
203
|
+
".qpy": "qpy",
|
|
204
|
+
".png": "png",
|
|
205
|
+
".svg": "svg",
|
|
206
|
+
".pdf": "pdf",
|
|
207
|
+
}
|
|
208
|
+
format = format_map.get(ext, "qasm")
|
|
209
|
+
|
|
210
|
+
circuit = build_optimized_circuit(optimizer, params, include_measurements)
|
|
211
|
+
|
|
212
|
+
if format == "qasm":
|
|
213
|
+
from qiskit.qasm2 import dumps
|
|
214
|
+
|
|
215
|
+
qasm_str = dumps(circuit)
|
|
216
|
+
filepath.write_text(qasm_str)
|
|
217
|
+
|
|
218
|
+
elif format == "qasm3":
|
|
219
|
+
from qiskit.qasm3 import dumps
|
|
220
|
+
|
|
221
|
+
qasm3_str = dumps(circuit)
|
|
222
|
+
filepath.write_text(qasm3_str)
|
|
223
|
+
|
|
224
|
+
elif format == "qpy":
|
|
225
|
+
from qiskit.qpy import dump
|
|
226
|
+
|
|
227
|
+
with open(filepath, "wb") as f:
|
|
228
|
+
dump(circuit, f)
|
|
229
|
+
|
|
230
|
+
elif format in ("png", "svg", "pdf"):
|
|
231
|
+
# Circuit diagram
|
|
232
|
+
fig = circuit.draw(output="mpl", **kwargs)
|
|
233
|
+
fig.savefig(filepath, format=format, bbox_inches="tight", dpi=150)
|
|
234
|
+
import matplotlib.pyplot as plt
|
|
235
|
+
|
|
236
|
+
plt.close(fig)
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Unknown format: {format}. Use 'qasm', 'qasm3', 'qpy', 'png', 'svg', or 'pdf'"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return filepath
|