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.
@@ -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