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,419 @@
|
|
1
|
+
"""PennyLane backend implementation for SuperQuantX.
|
2
|
+
|
3
|
+
This module provides integration with PennyLane for quantum machine learning
|
4
|
+
and variational quantum algorithms.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
|
12
|
+
from .base_backend import BaseBackend
|
13
|
+
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
# Try to import PennyLane
|
18
|
+
try:
|
19
|
+
import pennylane as qml
|
20
|
+
from pennylane import numpy as pnp
|
21
|
+
PENNYLANE_AVAILABLE = True
|
22
|
+
except ImportError:
|
23
|
+
PENNYLANE_AVAILABLE = False
|
24
|
+
qml = None
|
25
|
+
pnp = None
|
26
|
+
|
27
|
+
class PennyLaneBackend(BaseBackend):
|
28
|
+
"""PennyLane backend for quantum computing operations.
|
29
|
+
|
30
|
+
This backend provides access to PennyLane's quantum devices and
|
31
|
+
automatic differentiation capabilities for variational quantum
|
32
|
+
algorithms.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
device: PennyLane device name ('default.qubit', 'qiskit.aer', etc.)
|
36
|
+
wires: Number of qubits/wires
|
37
|
+
shots: Number of measurement shots
|
38
|
+
**kwargs: Additional device parameters
|
39
|
+
|
40
|
+
"""
|
41
|
+
|
42
|
+
def __init__(self, device: str = 'default.qubit', wires: int = 4, shots: int = 1024, **kwargs):
|
43
|
+
if not PENNYLANE_AVAILABLE:
|
44
|
+
raise ImportError("PennyLane is required for PennyLaneBackend. Install with: pip install pennylane")
|
45
|
+
|
46
|
+
super().__init__(device=device, shots=shots, **kwargs)
|
47
|
+
|
48
|
+
self.wires = wires
|
49
|
+
self.dev = None
|
50
|
+
self.capabilities = {
|
51
|
+
'supports_gradient': True,
|
52
|
+
'supports_parameter_shift': True,
|
53
|
+
'supports_finite_diff': True,
|
54
|
+
'supports_backprop': device in ['default.qubit'],
|
55
|
+
'supports_adjoint': device in ['default.qubit'],
|
56
|
+
}
|
57
|
+
|
58
|
+
def _initialize_backend(self) -> None:
|
59
|
+
"""Initialize PennyLane device."""
|
60
|
+
try:
|
61
|
+
device_kwargs = self.config.copy()
|
62
|
+
device_kwargs.pop('wires', None) # Remove wires from kwargs if present
|
63
|
+
|
64
|
+
# Create PennyLane device
|
65
|
+
if self.shots is not None and self.shots > 0:
|
66
|
+
self.dev = qml.device(self.device, wires=self.wires, shots=self.shots, **device_kwargs)
|
67
|
+
else:
|
68
|
+
# Exact simulation (no shots)
|
69
|
+
self.dev = qml.device(self.device, wires=self.wires, **device_kwargs)
|
70
|
+
|
71
|
+
logger.info(f"Initialized PennyLane device: {self.device} with {self.wires} wires")
|
72
|
+
|
73
|
+
except Exception as e:
|
74
|
+
logger.error(f"Failed to initialize PennyLane device: {e}")
|
75
|
+
raise
|
76
|
+
|
77
|
+
def create_circuit(self, n_qubits: int) -> Callable:
|
78
|
+
"""Create a PennyLane quantum function template."""
|
79
|
+
if n_qubits > self.wires:
|
80
|
+
logger.warning(f"Requested {n_qubits} qubits, but device only has {self.wires} wires")
|
81
|
+
n_qubits = self.wires
|
82
|
+
|
83
|
+
def circuit_template():
|
84
|
+
"""Empty circuit template."""
|
85
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(n_qubits)]
|
86
|
+
|
87
|
+
return circuit_template
|
88
|
+
|
89
|
+
def add_gate(self, circuit: Callable, gate: str, qubits: Union[int, List[int]],
|
90
|
+
params: Optional[List[float]] = None) -> Callable:
|
91
|
+
"""Add a quantum gate to the circuit (conceptual - PennyLane uses functions)."""
|
92
|
+
# In PennyLane, gates are added within quantum functions
|
93
|
+
# This method is more for compatibility with the base interface
|
94
|
+
logger.debug(f"Gate {gate} would be added to qubits {qubits} with params {params}")
|
95
|
+
return circuit
|
96
|
+
|
97
|
+
def add_measurement(self, circuit: Callable, qubits: Optional[List[int]] = None) -> Callable:
|
98
|
+
"""Add measurement operations (conceptual in PennyLane)."""
|
99
|
+
return circuit
|
100
|
+
|
101
|
+
def execute_circuit(self, circuit: Callable, shots: Optional[int] = None) -> Dict[str, Any]:
|
102
|
+
"""Execute quantum circuit and return results."""
|
103
|
+
try:
|
104
|
+
# Create QNode
|
105
|
+
if shots is not None:
|
106
|
+
dev = qml.device(self.device, wires=self.wires, shots=shots)
|
107
|
+
qnode = qml.QNode(circuit, dev)
|
108
|
+
else:
|
109
|
+
qnode = qml.QNode(circuit, self.dev)
|
110
|
+
|
111
|
+
# Execute
|
112
|
+
result = qnode()
|
113
|
+
|
114
|
+
return {
|
115
|
+
'result': result,
|
116
|
+
'shots': shots or self.shots,
|
117
|
+
'device': self.device,
|
118
|
+
}
|
119
|
+
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"Circuit execution failed: {e}")
|
122
|
+
raise
|
123
|
+
|
124
|
+
def get_statevector(self, circuit: Callable) -> np.ndarray:
|
125
|
+
"""Get the statevector from a quantum circuit."""
|
126
|
+
# Create a statevector device
|
127
|
+
dev_statevector = qml.device('default.qubit', wires=self.wires)
|
128
|
+
|
129
|
+
@qml.qnode(dev_statevector)
|
130
|
+
def statevector_circuit():
|
131
|
+
circuit()
|
132
|
+
return qml.state()
|
133
|
+
|
134
|
+
return np.array(statevector_circuit())
|
135
|
+
|
136
|
+
def _get_n_qubits(self, circuit: Any) -> int:
|
137
|
+
"""Get number of qubits in circuit."""
|
138
|
+
return self.wires
|
139
|
+
|
140
|
+
# ========================================================================
|
141
|
+
# Enhanced PennyLane-specific implementations
|
142
|
+
# ========================================================================
|
143
|
+
|
144
|
+
def create_feature_map(self, n_features: int, feature_map: str, reps: int = 1) -> Callable:
|
145
|
+
"""Create quantum feature map for data encoding."""
|
146
|
+
if feature_map == 'ZZFeatureMap':
|
147
|
+
return self._create_zz_feature_map(n_features, reps)
|
148
|
+
elif feature_map == 'PauliFeatureMap':
|
149
|
+
return self._create_pauli_feature_map(n_features, reps)
|
150
|
+
elif feature_map == 'AmplitudeMap':
|
151
|
+
return self._create_amplitude_map(n_features)
|
152
|
+
else:
|
153
|
+
logger.warning(f"Unknown feature map '{feature_map}', using angle encoding")
|
154
|
+
return self._create_angle_encoding_map(n_features)
|
155
|
+
|
156
|
+
def _create_zz_feature_map(self, n_features: int, reps: int) -> Callable:
|
157
|
+
"""Create ZZ feature map circuit."""
|
158
|
+
def zz_feature_map(x):
|
159
|
+
for r in range(reps):
|
160
|
+
# Single-qubit rotations
|
161
|
+
for i in range(min(n_features, self.wires)):
|
162
|
+
qml.Hadamard(wires=i)
|
163
|
+
qml.RZ(x[i], wires=i)
|
164
|
+
|
165
|
+
# Two-qubit interactions
|
166
|
+
for i in range(min(n_features - 1, self.wires - 1)):
|
167
|
+
qml.CNOT(wires=[i, i + 1])
|
168
|
+
qml.RZ(x[i] * x[i + 1], wires=i + 1)
|
169
|
+
qml.CNOT(wires=[i, i + 1])
|
170
|
+
|
171
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_features, self.wires))]
|
172
|
+
|
173
|
+
return zz_feature_map
|
174
|
+
|
175
|
+
def _create_pauli_feature_map(self, n_features: int, reps: int) -> Callable:
|
176
|
+
"""Create Pauli feature map circuit."""
|
177
|
+
def pauli_feature_map(x):
|
178
|
+
for r in range(reps):
|
179
|
+
for i in range(min(n_features, self.wires)):
|
180
|
+
qml.RX(x[i], wires=i)
|
181
|
+
qml.RY(x[i], wires=i)
|
182
|
+
qml.RZ(x[i], wires=i)
|
183
|
+
|
184
|
+
# Entangling layer
|
185
|
+
for i in range(min(n_features - 1, self.wires - 1)):
|
186
|
+
qml.CNOT(wires=[i, i + 1])
|
187
|
+
|
188
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_features, self.wires))]
|
189
|
+
|
190
|
+
return pauli_feature_map
|
191
|
+
|
192
|
+
def _create_amplitude_map(self, n_features: int) -> Callable:
|
193
|
+
"""Create amplitude encoding map."""
|
194
|
+
def amplitude_map(x):
|
195
|
+
# Normalize input
|
196
|
+
norm = np.linalg.norm(x)
|
197
|
+
if norm > 0:
|
198
|
+
normalized_x = x / norm
|
199
|
+
else:
|
200
|
+
normalized_x = x
|
201
|
+
|
202
|
+
# Pad to power of 2
|
203
|
+
n_qubits = int(np.ceil(np.log2(len(normalized_x))))
|
204
|
+
padded_x = np.pad(normalized_x, (0, 2**n_qubits - len(normalized_x)))
|
205
|
+
|
206
|
+
# Amplitude encoding (simplified)
|
207
|
+
qml.QubitStateVector(padded_x, wires=range(min(n_qubits, self.wires)))
|
208
|
+
|
209
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_qubits, self.wires))]
|
210
|
+
|
211
|
+
return amplitude_map
|
212
|
+
|
213
|
+
def _create_angle_encoding_map(self, n_features: int) -> Callable:
|
214
|
+
"""Create angle encoding map."""
|
215
|
+
def angle_encoding_map(x):
|
216
|
+
for i in range(min(n_features, self.wires)):
|
217
|
+
qml.RY(x[i], wires=i)
|
218
|
+
|
219
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_features, self.wires))]
|
220
|
+
|
221
|
+
return angle_encoding_map
|
222
|
+
|
223
|
+
def compute_kernel_matrix(self, X1: np.ndarray, X2: np.ndarray,
|
224
|
+
feature_map: Callable, shots: Optional[int] = None) -> np.ndarray:
|
225
|
+
"""Compute quantum kernel matrix using PennyLane."""
|
226
|
+
n1, n2 = len(X1), len(X2)
|
227
|
+
kernel_matrix = np.zeros((n1, n2))
|
228
|
+
|
229
|
+
# Create kernel evaluation circuit
|
230
|
+
def kernel_circuit(x1, x2):
|
231
|
+
# Encode first data point
|
232
|
+
for i in range(min(len(x1), self.wires)):
|
233
|
+
qml.RY(x1[i], wires=i)
|
234
|
+
|
235
|
+
# Encode second data point with inverse
|
236
|
+
for i in range(min(len(x2), self.wires)):
|
237
|
+
qml.RY(-x2[i], wires=i)
|
238
|
+
|
239
|
+
# Measure probability of |00...0⟩ state
|
240
|
+
return qml.probs(wires=range(min(len(x1), self.wires)))
|
241
|
+
|
242
|
+
# Create QNode
|
243
|
+
dev = qml.device(self.device, wires=self.wires, shots=shots) if shots else self.dev
|
244
|
+
kernel_qnode = qml.QNode(kernel_circuit, dev)
|
245
|
+
|
246
|
+
# Compute kernel matrix
|
247
|
+
for i in range(n1):
|
248
|
+
for j in range(n2):
|
249
|
+
try:
|
250
|
+
probs = kernel_qnode(X1[i], X2[j])
|
251
|
+
kernel_matrix[i, j] = probs[0] # Probability of |00...0⟩
|
252
|
+
except Exception as e:
|
253
|
+
logger.warning(f"Kernel computation failed for ({i},{j}): {e}")
|
254
|
+
kernel_matrix[i, j] = 0.0
|
255
|
+
|
256
|
+
return kernel_matrix
|
257
|
+
|
258
|
+
def create_ansatz(self, ansatz_type: str, n_qubits: int, params: np.ndarray,
|
259
|
+
include_custom_gates: bool = False) -> Callable:
|
260
|
+
"""Create parameterized ansatz circuit."""
|
261
|
+
if ansatz_type == 'RealAmplitudes':
|
262
|
+
return self._create_real_amplitudes_ansatz(n_qubits, params)
|
263
|
+
elif ansatz_type == 'EfficientSU2':
|
264
|
+
return self._create_efficient_su2_ansatz(n_qubits, params)
|
265
|
+
elif ansatz_type == 'TwoLocal':
|
266
|
+
return self._create_two_local_ansatz(n_qubits, params)
|
267
|
+
elif ansatz_type == 'UCCSD':
|
268
|
+
return self._create_uccsd_ansatz(n_qubits, params)
|
269
|
+
else:
|
270
|
+
logger.warning(f"Unknown ansatz '{ansatz_type}', using RealAmplitudes")
|
271
|
+
return self._create_real_amplitudes_ansatz(n_qubits, params)
|
272
|
+
|
273
|
+
def _create_real_amplitudes_ansatz(self, n_qubits: int, params: np.ndarray) -> Callable:
|
274
|
+
"""Create RealAmplitudes ansatz."""
|
275
|
+
def real_amplitudes_circuit():
|
276
|
+
n_layers = len(params) // (n_qubits * 2)
|
277
|
+
param_idx = 0
|
278
|
+
|
279
|
+
for layer in range(n_layers):
|
280
|
+
# Rotation layer
|
281
|
+
for i in range(min(n_qubits, self.wires)):
|
282
|
+
if param_idx < len(params):
|
283
|
+
qml.RY(params[param_idx], wires=i)
|
284
|
+
param_idx += 1
|
285
|
+
|
286
|
+
# Entangling layer
|
287
|
+
for i in range(min(n_qubits - 1, self.wires - 1)):
|
288
|
+
qml.CNOT(wires=[i, i + 1])
|
289
|
+
|
290
|
+
# Second rotation layer
|
291
|
+
for i in range(min(n_qubits, self.wires)):
|
292
|
+
if param_idx < len(params):
|
293
|
+
qml.RZ(params[param_idx], wires=i)
|
294
|
+
param_idx += 1
|
295
|
+
|
296
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_qubits, self.wires))]
|
297
|
+
|
298
|
+
return real_amplitudes_circuit
|
299
|
+
|
300
|
+
def _create_efficient_su2_ansatz(self, n_qubits: int, params: np.ndarray) -> Callable:
|
301
|
+
"""Create EfficientSU2 ansatz."""
|
302
|
+
def efficient_su2_circuit():
|
303
|
+
n_layers = len(params) // (n_qubits * 3)
|
304
|
+
param_idx = 0
|
305
|
+
|
306
|
+
for layer in range(n_layers):
|
307
|
+
# SU(2) rotations
|
308
|
+
for i in range(min(n_qubits, self.wires)):
|
309
|
+
if param_idx + 2 < len(params):
|
310
|
+
qml.RY(params[param_idx], wires=i)
|
311
|
+
qml.RZ(params[param_idx + 1], wires=i)
|
312
|
+
qml.RY(params[param_idx + 2], wires=i)
|
313
|
+
param_idx += 3
|
314
|
+
|
315
|
+
# Entangling layer
|
316
|
+
for i in range(min(n_qubits - 1, self.wires - 1)):
|
317
|
+
qml.CNOT(wires=[i, i + 1])
|
318
|
+
|
319
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_qubits, self.wires))]
|
320
|
+
|
321
|
+
return efficient_su2_circuit
|
322
|
+
|
323
|
+
def _create_two_local_ansatz(self, n_qubits: int, params: np.ndarray) -> Callable:
|
324
|
+
"""Create TwoLocal ansatz."""
|
325
|
+
def two_local_circuit():
|
326
|
+
n_layers = len(params) // (n_qubits * 2)
|
327
|
+
param_idx = 0
|
328
|
+
|
329
|
+
for layer in range(n_layers):
|
330
|
+
# Local rotations
|
331
|
+
for i in range(min(n_qubits, self.wires)):
|
332
|
+
if param_idx + 1 < len(params):
|
333
|
+
qml.RY(params[param_idx], wires=i)
|
334
|
+
qml.RZ(params[param_idx + 1], wires=i)
|
335
|
+
param_idx += 2
|
336
|
+
|
337
|
+
# Two-qubit gates
|
338
|
+
for i in range(0, min(n_qubits - 1, self.wires - 1), 2):
|
339
|
+
qml.CNOT(wires=[i, i + 1])
|
340
|
+
for i in range(1, min(n_qubits - 1, self.wires - 1), 2):
|
341
|
+
qml.CNOT(wires=[i, i + 1])
|
342
|
+
|
343
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_qubits, self.wires))]
|
344
|
+
|
345
|
+
return two_local_circuit
|
346
|
+
|
347
|
+
def _create_uccsd_ansatz(self, n_qubits: int, params: np.ndarray) -> Callable:
|
348
|
+
"""Create UCCSD ansatz (simplified)."""
|
349
|
+
def uccsd_circuit():
|
350
|
+
# Simplified UCCSD - full implementation would require molecular geometry
|
351
|
+
param_idx = 0
|
352
|
+
|
353
|
+
# Singles excitations
|
354
|
+
for i in range(0, min(n_qubits, self.wires), 2):
|
355
|
+
for j in range(1, min(n_qubits, self.wires), 2):
|
356
|
+
if param_idx < len(params) and i != j:
|
357
|
+
# Simplified single excitation
|
358
|
+
qml.CNOT(wires=[i, j])
|
359
|
+
qml.RY(params[param_idx], wires=j)
|
360
|
+
qml.CNOT(wires=[i, j])
|
361
|
+
param_idx += 1
|
362
|
+
|
363
|
+
# Doubles excitations (simplified)
|
364
|
+
for i in range(0, min(n_qubits - 3, self.wires - 3), 2):
|
365
|
+
if param_idx < len(params):
|
366
|
+
# Simplified double excitation
|
367
|
+
qml.CNOT(wires=[i, i + 1])
|
368
|
+
qml.CNOT(wires=[i + 2, i + 3])
|
369
|
+
qml.RY(params[param_idx], wires=i + 1)
|
370
|
+
qml.CNOT(wires=[i, i + 1])
|
371
|
+
qml.CNOT(wires=[i + 2, i + 3])
|
372
|
+
param_idx += 1
|
373
|
+
|
374
|
+
return [qml.expval(qml.PauliZ(i)) for i in range(min(n_qubits, self.wires))]
|
375
|
+
|
376
|
+
return uccsd_circuit
|
377
|
+
|
378
|
+
def compute_expectation(self, circuit: Callable, hamiltonian: Any,
|
379
|
+
shots: Optional[int] = None) -> float:
|
380
|
+
"""Compute expectation value of Hamiltonian."""
|
381
|
+
try:
|
382
|
+
# If hamiltonian is a PennyLane Hamiltonian
|
383
|
+
if hasattr(hamiltonian, 'coeffs') and hasattr(hamiltonian, 'ops'):
|
384
|
+
@qml.qnode(self.dev if shots is None else qml.device(self.device, wires=self.wires, shots=shots))
|
385
|
+
def expectation_circuit():
|
386
|
+
circuit()
|
387
|
+
return qml.expval(hamiltonian)
|
388
|
+
|
389
|
+
return float(expectation_circuit())
|
390
|
+
|
391
|
+
# If hamiltonian is a matrix, decompose it
|
392
|
+
elif isinstance(hamiltonian, np.ndarray):
|
393
|
+
return self._compute_matrix_expectation(circuit, hamiltonian, shots)
|
394
|
+
|
395
|
+
else:
|
396
|
+
logger.warning("Unknown Hamiltonian format, returning 0")
|
397
|
+
return 0.0
|
398
|
+
|
399
|
+
except Exception as e:
|
400
|
+
logger.error(f"Expectation computation failed: {e}")
|
401
|
+
return 0.0
|
402
|
+
|
403
|
+
def _compute_matrix_expectation(self, circuit: Callable, H: np.ndarray, shots: Optional[int]) -> float:
|
404
|
+
"""Compute expectation value for matrix Hamiltonian."""
|
405
|
+
# Get statevector
|
406
|
+
statevector = self.get_statevector(circuit)
|
407
|
+
|
408
|
+
# Compute ⟨ψ|H|ψ⟩
|
409
|
+
expectation = np.real(np.conj(statevector) @ H @ statevector)
|
410
|
+
return float(expectation)
|
411
|
+
|
412
|
+
def get_version_info(self) -> Dict[str, Any]:
|
413
|
+
"""Get PennyLane version information."""
|
414
|
+
info = super().get_version_info()
|
415
|
+
info.update({
|
416
|
+
'pennylane_version': qml.version() if qml else 'Not available',
|
417
|
+
'available_devices': qml.plugin_devices if qml else [],
|
418
|
+
})
|
419
|
+
return info
|