qcompile 0.1.0__tar.gz
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.
- qcompile-0.1.0/PKG-INFO +29 -0
- qcompile-0.1.0/pyproject.toml +57 -0
- qcompile-0.1.0/qcompile/__init__.py +585 -0
- qcompile-0.1.0/qcompile.egg-info/PKG-INFO +29 -0
- qcompile-0.1.0/qcompile.egg-info/SOURCES.txt +7 -0
- qcompile-0.1.0/qcompile.egg-info/dependency_links.txt +1 -0
- qcompile-0.1.0/qcompile.egg-info/requires.txt +7 -0
- qcompile-0.1.0/qcompile.egg-info/top_level.txt +1 -0
- qcompile-0.1.0/setup.cfg +4 -0
qcompile-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qcompile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-guided hardware-aware quantum circuit layout optimization for Qiskit
|
|
5
|
+
Author-email: Shagun Tembhurne <shaxtembhurne@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shaxtembhurne/qcompile
|
|
8
|
+
Project-URL: Repository, https://github.com/shaxtembhurne/qcompile
|
|
9
|
+
Project-URL: Issues, https://github.com/shaxtembhurne/qcompile/issues
|
|
10
|
+
Keywords: quantum-computing,qiskit,compiler,transpiler,layout,optimization,graph-neural-network,machine-learning
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: numpy>=1.24
|
|
24
|
+
Requires-Dist: networkx>=3.0
|
|
25
|
+
Requires-Dist: joblib>=1.3
|
|
26
|
+
Requires-Dist: qiskit>=1.0
|
|
27
|
+
Requires-Dist: qiskit-ibm-runtime>=0.20
|
|
28
|
+
Requires-Dist: torch>=2.0
|
|
29
|
+
Requires-Dist: torch-geometric>=2.3
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "qcompile"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "AI-guided hardware-aware quantum circuit layout optimization for Qiskit"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Shagun Tembhurne", email = "shaxtembhurne@gmail.com" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
license = { text = "MIT" }
|
|
18
|
+
|
|
19
|
+
keywords = [
|
|
20
|
+
"quantum-computing",
|
|
21
|
+
"qiskit",
|
|
22
|
+
"compiler",
|
|
23
|
+
"transpiler",
|
|
24
|
+
"layout",
|
|
25
|
+
"optimization",
|
|
26
|
+
"graph-neural-network",
|
|
27
|
+
"machine-learning"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Development Status :: 3 - Alpha",
|
|
32
|
+
"Intended Audience :: Science/Research",
|
|
33
|
+
"Intended Audience :: Developers",
|
|
34
|
+
"License :: OSI Approved :: MIT License",
|
|
35
|
+
"Programming Language :: Python :: 3",
|
|
36
|
+
"Programming Language :: Python :: 3.10",
|
|
37
|
+
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Operating System :: OS Independent",
|
|
39
|
+
"Topic :: Scientific/Engineering",
|
|
40
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
dependencies = [
|
|
44
|
+
"numpy>=1.24",
|
|
45
|
+
"networkx>=3.0",
|
|
46
|
+
"joblib>=1.3",
|
|
47
|
+
"qiskit>=1.0",
|
|
48
|
+
"qiskit-ibm-runtime>=0.20",
|
|
49
|
+
"torch>=2.0",
|
|
50
|
+
"torch-geometric>=2.3",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://github.com/shaxtembhurne/qcompile"
|
|
55
|
+
Repository = "https://github.com/shaxtembhurne/qcompile"
|
|
56
|
+
Issues = "https://github.com/shaxtembhurne/qcompile/issues"
|
|
57
|
+
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# ALPHAZERO + A* NEURAL QUANTUM COMPILER (LIVE IBM HARDWARE AWARE)
|
|
3
|
+
# Reusable Module for AI-Guided SWAP, DEPTH & NOISE Reduction
|
|
4
|
+
# ============================================================================
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import warnings
|
|
8
|
+
import networkx as nx
|
|
9
|
+
import random
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
import logging
|
|
14
|
+
from joblib import Parallel, delayed
|
|
15
|
+
|
|
16
|
+
from qiskit import QuantumCircuit
|
|
17
|
+
from qiskit.transpiler import CouplingMap, Layout
|
|
18
|
+
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
|
|
19
|
+
from qiskit_ibm_runtime import QiskitRuntimeService
|
|
20
|
+
|
|
21
|
+
import torch
|
|
22
|
+
import torch.nn.functional as F
|
|
23
|
+
from torch_geometric.data import Data, DataLoader
|
|
24
|
+
from torch_geometric.nn import GATv2Conv
|
|
25
|
+
|
|
26
|
+
# Suppress warnings to keep the terminal beautiful
|
|
27
|
+
warnings.filterwarnings("ignore")
|
|
28
|
+
logging.getLogger("qiskit_runtime_service").setLevel(logging.ERROR)
|
|
29
|
+
logging.getLogger("qiskit_ibm_runtime").setLevel(logging.ERROR) # <-- ADD THIS LINE
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# UTILITY: TERMINAL SPINNER
|
|
32
|
+
# ============================================================================
|
|
33
|
+
class TerminalSpinner:
|
|
34
|
+
def __init__(self, message="Processing..."):
|
|
35
|
+
self.spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
36
|
+
self.message = message
|
|
37
|
+
self.running = False
|
|
38
|
+
self.thread = None
|
|
39
|
+
|
|
40
|
+
def spin(self):
|
|
41
|
+
while self.running:
|
|
42
|
+
for char in self.spinner_chars:
|
|
43
|
+
if not self.running:
|
|
44
|
+
break
|
|
45
|
+
sys.stdout.write(f"\r\033[36m{char}\033[0m {self.message}")
|
|
46
|
+
sys.stdout.flush()
|
|
47
|
+
time.sleep(0.08)
|
|
48
|
+
|
|
49
|
+
def start(self):
|
|
50
|
+
self.running = True
|
|
51
|
+
self.thread = threading.Thread(target=self.spin)
|
|
52
|
+
self.thread.start()
|
|
53
|
+
|
|
54
|
+
def stop(self, success_msg=None):
|
|
55
|
+
self.running = False
|
|
56
|
+
if self.thread:
|
|
57
|
+
self.thread.join()
|
|
58
|
+
sys.stdout.write('\r' + ' ' * (len(self.message) + 20) + '\r')
|
|
59
|
+
if success_msg:
|
|
60
|
+
sys.stdout.write(f"\033[32m✓\033[0m {success_msg}\n")
|
|
61
|
+
sys.stdout.flush()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ============================================================================
|
|
65
|
+
# UTILITY: LIVE IBM HARDWARE & ESP CALCULATION
|
|
66
|
+
# ============================================================================
|
|
67
|
+
def fetch_live_ibm_hardware_data(backend_name="ibm_fez", max_physical_qubits=30):
|
|
68
|
+
try:
|
|
69
|
+
service = QiskitRuntimeService()
|
|
70
|
+
backend = service.backend(backend_name)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
sys.stdout.write('\r' + ' ' * 80 + '\r')
|
|
73
|
+
print(f"\n\033[91m\033[1m[IBM AUTH ERROR] Failed to connect to IBM Quantum Service.\033[0m")
|
|
74
|
+
print(f"\033[93mDetails: {e}\033[0m")
|
|
75
|
+
print("\n\033[1mTo fix this, ensure you have saved your IBM token using:")
|
|
76
|
+
print(" from qiskit_ibm_runtime import QiskitRuntimeService")
|
|
77
|
+
print(" QiskitRuntimeService.save_account(token=\"YOUR_IBM_API_TOKEN\", overwrite=True)\033[0m\n")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
target = backend.target
|
|
81
|
+
num_phys_qubits = target.num_qubits
|
|
82
|
+
|
|
83
|
+
readout_errors_raw = []
|
|
84
|
+
for q in range(num_phys_qubits):
|
|
85
|
+
try:
|
|
86
|
+
err = target['measure'][(q,)].error
|
|
87
|
+
readout_errors_raw.append(err if err is not None else 0.05)
|
|
88
|
+
except Exception:
|
|
89
|
+
readout_errors_raw.append(0.05)
|
|
90
|
+
|
|
91
|
+
start_node = int(np.argmin(readout_errors_raw))
|
|
92
|
+
raw_cmap = target.build_coupling_map()
|
|
93
|
+
|
|
94
|
+
graph = nx.Graph()
|
|
95
|
+
graph.add_edges_from(list(raw_cmap.get_edges()))
|
|
96
|
+
|
|
97
|
+
visited = set([start_node])
|
|
98
|
+
queue = [start_node]
|
|
99
|
+
|
|
100
|
+
while queue and len(visited) < max_physical_qubits:
|
|
101
|
+
current = queue.pop(0)
|
|
102
|
+
neighbors = sorted(list(graph.neighbors(current)), key=lambda n: readout_errors_raw[n])
|
|
103
|
+
for neighbor in neighbors:
|
|
104
|
+
if neighbor not in visited:
|
|
105
|
+
visited.add(neighbor)
|
|
106
|
+
queue.append(neighbor)
|
|
107
|
+
if len(visited) == max_physical_qubits:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
active_nodes = list(visited)
|
|
111
|
+
node_mapping = {old_idx: new_idx for new_idx, old_idx in enumerate(active_nodes)}
|
|
112
|
+
|
|
113
|
+
sliced_edges = []
|
|
114
|
+
for u, v in raw_cmap.get_edges():
|
|
115
|
+
if u in visited and v in visited:
|
|
116
|
+
sliced_edges.append((node_mapping[u], node_mapping[v]))
|
|
117
|
+
|
|
118
|
+
cmap = CouplingMap(sliced_edges)
|
|
119
|
+
|
|
120
|
+
readout_errors = np.zeros(max_physical_qubits)
|
|
121
|
+
for old_idx, new_idx in node_mapping.items():
|
|
122
|
+
try:
|
|
123
|
+
readout_errors[new_idx] = target['measure'][(old_idx,)].error
|
|
124
|
+
except Exception:
|
|
125
|
+
readout_errors[new_idx] = 0.05
|
|
126
|
+
|
|
127
|
+
gate_errors = {}
|
|
128
|
+
two_q_gate = 'ecr' if 'ecr' in target else ('cx' if 'cx' in target else 'cz')
|
|
129
|
+
|
|
130
|
+
for old_u, old_v in raw_cmap.get_edges():
|
|
131
|
+
if old_u in visited and old_v in visited:
|
|
132
|
+
new_u, new_v = node_mapping[old_u], node_mapping[old_v]
|
|
133
|
+
try:
|
|
134
|
+
err = target[two_q_gate][(old_u, old_v)].error
|
|
135
|
+
if err is None or err >= 1.0:
|
|
136
|
+
gate_errors[(new_u, new_v)] = 0.10
|
|
137
|
+
else:
|
|
138
|
+
gate_errors[(new_u, new_v)] = err
|
|
139
|
+
except Exception:
|
|
140
|
+
gate_errors[(new_u, new_v)] = 0.10
|
|
141
|
+
|
|
142
|
+
# Return the full raw target device data alongside the slice mapping for proper benchmarking
|
|
143
|
+
return cmap, readout_errors, gate_errors, backend, node_mapping
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def calculate_esp_global(routed_qc, backend):
|
|
147
|
+
"""Calculates full-chip ESP directly from Qiskit's native target data."""
|
|
148
|
+
esp = 1.0
|
|
149
|
+
target = backend.target
|
|
150
|
+
two_q_gate = 'ecr' if 'ecr' in target else ('cx' if 'cx' in target else 'cz')
|
|
151
|
+
|
|
152
|
+
used_physical_qubits = set()
|
|
153
|
+
for inst in routed_qc.data:
|
|
154
|
+
if inst.operation.name not in ['barrier']:
|
|
155
|
+
for q in inst.qubits:
|
|
156
|
+
used_physical_qubits.add(routed_qc.find_bit(q).index)
|
|
157
|
+
|
|
158
|
+
for q in used_physical_qubits:
|
|
159
|
+
try:
|
|
160
|
+
err = target['measure'][(q,)].error
|
|
161
|
+
esp *= (1.0 - (err if err is not None else 0.05))
|
|
162
|
+
except Exception:
|
|
163
|
+
esp *= 0.95
|
|
164
|
+
|
|
165
|
+
for inst in routed_qc.data:
|
|
166
|
+
op_name = inst.operation.name
|
|
167
|
+
if op_name in ['cx', 'swap', 'ecr', 'cz']:
|
|
168
|
+
q_indices = [routed_qc.find_bit(q).index for q in inst.qubits]
|
|
169
|
+
if len(q_indices) == 2:
|
|
170
|
+
u, v = q_indices[0], q_indices[1]
|
|
171
|
+
try:
|
|
172
|
+
err = target[two_q_gate][(u, v)].error
|
|
173
|
+
if err is None: err = 0.02
|
|
174
|
+
except Exception:
|
|
175
|
+
err = 0.02
|
|
176
|
+
esp *= (1.0 - err)**3 if op_name == 'swap' else (1.0 - err)
|
|
177
|
+
|
|
178
|
+
return esp
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ============================================================================
|
|
182
|
+
# 1. CORE GRAPH NEURAL NETWORK
|
|
183
|
+
# ============================================================================
|
|
184
|
+
class QAOAPolicyGNN(torch.nn.Module):
|
|
185
|
+
def __init__(self, num_node_features, hidden_dim=64):
|
|
186
|
+
super().__init__()
|
|
187
|
+
self.conv1 = GATv2Conv(num_node_features, hidden_dim, heads=4, concat=True)
|
|
188
|
+
self.conv2 = GATv2Conv(hidden_dim * 4, hidden_dim, heads=1, concat=False)
|
|
189
|
+
self.fc = torch.nn.Linear(hidden_dim, 1)
|
|
190
|
+
|
|
191
|
+
def forward(self, data):
|
|
192
|
+
x, edge_index = data.x, data.edge_index
|
|
193
|
+
if hasattr(data, 'batch') and data.batch is not None:
|
|
194
|
+
batch = data.batch
|
|
195
|
+
else:
|
|
196
|
+
batch = torch.zeros(x.size(0), dtype=torch.long, device=x.device)
|
|
197
|
+
|
|
198
|
+
x = F.elu(self.conv1(x, edge_index))
|
|
199
|
+
x = F.elu(self.conv2(x, edge_index))
|
|
200
|
+
logits = self.fc(x).squeeze(-1)
|
|
201
|
+
batch_size = data.num_graphs if hasattr(data, 'num_graphs') and data.num_graphs is not None else 1
|
|
202
|
+
|
|
203
|
+
num_nodes = x.size(0) // batch_size
|
|
204
|
+
return logits.view(batch_size, num_nodes)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ============================================================================
|
|
208
|
+
# 2. THE REUSABLE COMPILER CLASS
|
|
209
|
+
# ============================================================================
|
|
210
|
+
class NeuralAStarCompiler:
|
|
211
|
+
HARVEST_VERIFY_SEEDS = [42, 123, 456]
|
|
212
|
+
FINAL_VERIFY_SEEDS = [42, 123, 456]
|
|
213
|
+
FINAL_TOP_BEAMS = 25
|
|
214
|
+
|
|
215
|
+
def __init__(self, num_qubits, logical_edges, cmap, readout_errs, gate_errs, beam_width=50, alpha=0.5):
|
|
216
|
+
self.num_qubits = num_qubits
|
|
217
|
+
self.logical_edges = logical_edges
|
|
218
|
+
self.beam_width = beam_width
|
|
219
|
+
self.alpha = alpha
|
|
220
|
+
|
|
221
|
+
self.cmap = cmap
|
|
222
|
+
self.num_phys = self.cmap.size()
|
|
223
|
+
self.readout_errs = readout_errs
|
|
224
|
+
self.gate_errs = gate_errs
|
|
225
|
+
|
|
226
|
+
self.dist_matrix = self._build_reliability_matrix()
|
|
227
|
+
|
|
228
|
+
self.hw_graph = nx.Graph(list(self.cmap.get_edges()))
|
|
229
|
+
self.phys_degrees = np.array([self.hw_graph.degree(i) for i in range(self.num_phys)])
|
|
230
|
+
|
|
231
|
+
self.hw_noise_feats = self._get_static_noise_features()
|
|
232
|
+
|
|
233
|
+
hw_edges = list(self.cmap.get_edges())
|
|
234
|
+
self.edge_index = torch.tensor(hw_edges, dtype=torch.long).t().contiguous()
|
|
235
|
+
|
|
236
|
+
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
237
|
+
self._use_amp = self.device.type == 'cuda'
|
|
238
|
+
|
|
239
|
+
raw_model = QAOAPolicyGNN(
|
|
240
|
+
num_node_features=(3 * num_qubits) + 7
|
|
241
|
+
).to(self.device)
|
|
242
|
+
try:
|
|
243
|
+
self.model = torch.compile(raw_model)
|
|
244
|
+
except Exception:
|
|
245
|
+
self.model = raw_model
|
|
246
|
+
|
|
247
|
+
self._adj = {i: [] for i in range(num_qubits)}
|
|
248
|
+
for u, v in logical_edges:
|
|
249
|
+
self._adj[u].append(v)
|
|
250
|
+
self._adj[v].append(u)
|
|
251
|
+
|
|
252
|
+
logical_nx = nx.Graph(logical_edges)
|
|
253
|
+
b_centrality = nx.betweenness_centrality(logical_nx)
|
|
254
|
+
self.routing_order = [i for i, _ in sorted(b_centrality.items(), key=lambda x: x[1], reverse=True)]
|
|
255
|
+
|
|
256
|
+
def _build_reliability_matrix(self):
|
|
257
|
+
G = nx.Graph()
|
|
258
|
+
for u, v in self.cmap.get_edges():
|
|
259
|
+
err = self.gate_errs.get((u, v), 0.02)
|
|
260
|
+
weight = 1.0 - np.log(1 - err + 1e-10)
|
|
261
|
+
G.add_edge(u, v, weight=weight)
|
|
262
|
+
|
|
263
|
+
path_lengths = dict(nx.all_pairs_dijkstra_path_length(G))
|
|
264
|
+
dist = np.zeros((self.num_phys, self.num_phys))
|
|
265
|
+
for i in range(self.num_phys):
|
|
266
|
+
for j in range(self.num_phys):
|
|
267
|
+
dist[i, j] = path_lengths.get(i, {}).get(j, 999.0)
|
|
268
|
+
return dist
|
|
269
|
+
|
|
270
|
+
def _get_static_noise_features(self):
|
|
271
|
+
feats = np.zeros((self.num_phys, 3))
|
|
272
|
+
for i in range(self.num_phys):
|
|
273
|
+
feats[i, 0] = self.readout_errs[i]
|
|
274
|
+
adj_errs = [self.gate_errs[(i, v)] for v in self.cmap.neighbors(i) if (i, v) in self.gate_errs]
|
|
275
|
+
feats[i, 1] = np.mean(adj_errs) if adj_errs else 0.02
|
|
276
|
+
feats[i, 2] = self.phys_degrees[i] / max(self.phys_degrees) if max(self.phys_degrees) > 0 else 0
|
|
277
|
+
return torch.tensor(feats, dtype=torch.float)
|
|
278
|
+
|
|
279
|
+
def _build_circuit(self):
|
|
280
|
+
qc = QuantumCircuit(self.num_qubits)
|
|
281
|
+
qc.h(range(self.num_qubits))
|
|
282
|
+
for i, j in self.logical_edges:
|
|
283
|
+
qc.cx(i, j)
|
|
284
|
+
qc.rz(0.5, j)
|
|
285
|
+
qc.cx(i, j)
|
|
286
|
+
qc.rx(1.0, range(self.num_qubits))
|
|
287
|
+
qc.measure_all()
|
|
288
|
+
return qc
|
|
289
|
+
|
|
290
|
+
def _layout_to_pyg_data(self, layout, target_q):
|
|
291
|
+
total_features = (3 * self.num_qubits) + 1
|
|
292
|
+
x = torch.zeros((self.num_phys, total_features), dtype=torch.float)
|
|
293
|
+
phys_to_logical = {phys: log for log, phys in enumerate(layout) if phys != -1}
|
|
294
|
+
placed_phys = [p for p in layout if p != -1]
|
|
295
|
+
|
|
296
|
+
demand = {i: [] for i in range(self.num_qubits)}
|
|
297
|
+
for u, v in self.logical_edges:
|
|
298
|
+
demand[u].append(v)
|
|
299
|
+
demand[v].append(u)
|
|
300
|
+
|
|
301
|
+
extra_feats = torch.zeros((self.num_phys, 3))
|
|
302
|
+
|
|
303
|
+
for phys_node in range(self.num_phys):
|
|
304
|
+
if phys_node in phys_to_logical:
|
|
305
|
+
logical_q = phys_to_logical[phys_node]
|
|
306
|
+
x[phys_node, logical_q] = 1.0
|
|
307
|
+
for nb in demand[logical_q]: x[phys_node, self.num_qubits + 1 + nb] = 1.0
|
|
308
|
+
else:
|
|
309
|
+
x[phys_node, self.num_qubits] = 1.0
|
|
310
|
+
x[phys_node, (2 * self.num_qubits) + 1 + target_q] = 1.0
|
|
311
|
+
|
|
312
|
+
if placed_phys:
|
|
313
|
+
extra_feats[phys_node, 0] = np.min(self.dist_matrix[phys_node, placed_phys])
|
|
314
|
+
neighbor_count = sum(1 for nb in self.hw_graph.neighbors(phys_node) if nb in placed_phys)
|
|
315
|
+
extra_feats[phys_node, 1] = neighbor_count
|
|
316
|
+
|
|
317
|
+
extra_feats[phys_node, 2] = 1.0 if phys_node not in placed_phys else 0.0
|
|
318
|
+
|
|
319
|
+
x_full = torch.cat([x, self.hw_noise_feats, extra_feats], dim=-1)
|
|
320
|
+
return Data(x=x_full, edge_index=self.edge_index)
|
|
321
|
+
|
|
322
|
+
def _harvest_single_expert(self, seed_offset, qc):
|
|
323
|
+
def calc_esp_local(routed_qc, readout_errs, gate_errs):
|
|
324
|
+
esp = 1.0
|
|
325
|
+
used = set()
|
|
326
|
+
for inst in routed_qc.data:
|
|
327
|
+
if inst.operation.name not in ['barrier']:
|
|
328
|
+
for q in inst.qubits: used.add(routed_qc.find_bit(q).index)
|
|
329
|
+
for q in used:
|
|
330
|
+
if q < len(readout_errs): esp *= (1.0 - readout_errs[q])
|
|
331
|
+
for inst in routed_qc.data:
|
|
332
|
+
op = inst.operation.name
|
|
333
|
+
if op in ['cx', 'swap', 'ecr', 'cz']:
|
|
334
|
+
q_idx = [routed_qc.find_bit(q).index for q in inst.qubits]
|
|
335
|
+
if len(q_idx) == 2:
|
|
336
|
+
u, v = q_idx[0], q_idx[1]
|
|
337
|
+
err = gate_errs.get((u, v), gate_errs.get((v, u), 0.02))
|
|
338
|
+
esp *= (1.0 - err)**3 if op == 'swap' else (1.0 - err)
|
|
339
|
+
return esp
|
|
340
|
+
|
|
341
|
+
pm = generate_preset_pass_manager(optimization_level=3, coupling_map=self.cmap, seed_transpiler=42 + seed_offset)
|
|
342
|
+
routed_qc = pm.run(qc)
|
|
343
|
+
|
|
344
|
+
layout = [-1] * self.num_qubits
|
|
345
|
+
init_layout = routed_qc.layout.initial_layout
|
|
346
|
+
for i, vq in enumerate(qc.qubits):
|
|
347
|
+
layout[i] = init_layout[vq]
|
|
348
|
+
|
|
349
|
+
best_swaps = routed_qc.count_ops().get("swap", 0)
|
|
350
|
+
best_esp = calc_esp_local(routed_qc, self.readout_errs, self.gate_errs)
|
|
351
|
+
|
|
352
|
+
qiskit_layout = Layout({qc.qubits[i]: int(layout[i]) for i in range(len(layout))})
|
|
353
|
+
for vseed in self.HARVEST_VERIFY_SEEDS:
|
|
354
|
+
pm_v = generate_preset_pass_manager(optimization_level=3, coupling_map=self.cmap, initial_layout=qiskit_layout, seed_transpiler=vseed)
|
|
355
|
+
routed_v = pm_v.run(qc)
|
|
356
|
+
s = routed_v.count_ops().get("swap", 0)
|
|
357
|
+
e = calc_esp_local(routed_v, self.readout_errs, self.gate_errs)
|
|
358
|
+
|
|
359
|
+
if s < best_swaps or (s == best_swaps and e > best_esp):
|
|
360
|
+
best_swaps = s
|
|
361
|
+
best_esp = e
|
|
362
|
+
|
|
363
|
+
return layout, best_swaps, best_esp
|
|
364
|
+
|
|
365
|
+
def train(self, num_samples, epochs=40, batch_size=64, spinner=None):
|
|
366
|
+
qc = self._build_circuit()
|
|
367
|
+
|
|
368
|
+
results = Parallel(n_jobs=-1)(delayed(self._harvest_single_expert)(i, qc) for i in range(num_samples))
|
|
369
|
+
results.sort(key=lambda x: (x[1], -x[2]))
|
|
370
|
+
|
|
371
|
+
keep_count = max(10, int(num_samples * 0.25))
|
|
372
|
+
expert_layouts = [res[0] for res in results[:keep_count]]
|
|
373
|
+
|
|
374
|
+
pyg_data_list = []
|
|
375
|
+
for expert_layout in expert_layouts:
|
|
376
|
+
partial = [-1] * self.num_qubits
|
|
377
|
+
for q in self.routing_order:
|
|
378
|
+
target_node = expert_layout[q]
|
|
379
|
+
data = self._layout_to_pyg_data(partial, q)
|
|
380
|
+
data.y = torch.tensor([target_node], dtype=torch.long)
|
|
381
|
+
pyg_data_list.append(data)
|
|
382
|
+
partial[q] = target_node
|
|
383
|
+
|
|
384
|
+
pin = self._use_amp
|
|
385
|
+
loader = DataLoader(
|
|
386
|
+
pyg_data_list, batch_size=batch_size, shuffle=True,
|
|
387
|
+
num_workers=4, pin_memory=pin, persistent_workers=True
|
|
388
|
+
)
|
|
389
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=0.005)
|
|
390
|
+
criterion = torch.nn.CrossEntropyLoss()
|
|
391
|
+
scaler = torch.cuda.amp.GradScaler(enabled=self._use_amp)
|
|
392
|
+
|
|
393
|
+
self.model.train()
|
|
394
|
+
for epoch in range(epochs):
|
|
395
|
+
epoch_loss = 0.0
|
|
396
|
+
batches = 0
|
|
397
|
+
for data in loader:
|
|
398
|
+
data = data.to(self.device, non_blocking=True)
|
|
399
|
+
optimizer.zero_grad(set_to_none=True)
|
|
400
|
+
with torch.autocast(device_type=self.device.type, enabled=self._use_amp):
|
|
401
|
+
logits = self.model(data)
|
|
402
|
+
loss = criterion(logits, data.y)
|
|
403
|
+
scaler.scale(loss).backward()
|
|
404
|
+
scaler.step(optimizer)
|
|
405
|
+
scaler.update()
|
|
406
|
+
|
|
407
|
+
epoch_loss += loss.item()
|
|
408
|
+
batches += 1
|
|
409
|
+
|
|
410
|
+
if spinner:
|
|
411
|
+
avg_loss = epoch_loss / max(1, batches)
|
|
412
|
+
spinner.message = f"Harvesting layouts & training GNN... [\033[93mEpoch {epoch+1}/{epochs}\033[0m | Loss: \033[35m{avg_loss:.4f}\033[0m]"
|
|
413
|
+
|
|
414
|
+
def _local_search_refine(self, qc_base, initial_layout, iterations=200):
|
|
415
|
+
best_layout = list(initial_layout)
|
|
416
|
+
|
|
417
|
+
def get_metrics(l):
|
|
418
|
+
q_map = Layout({qc_base.qubits[i]: int(l[i]) for i in range(len(l))})
|
|
419
|
+
pm = generate_preset_pass_manager(optimization_level=3, coupling_map=self.cmap, initial_layout=q_map, seed_transpiler=42)
|
|
420
|
+
routed = pm.run(qc_base)
|
|
421
|
+
s = routed.count_ops().get("swap", 0)
|
|
422
|
+
|
|
423
|
+
# Simple local tracking
|
|
424
|
+
esp = 1.0
|
|
425
|
+
used = set()
|
|
426
|
+
for inst in routed.data:
|
|
427
|
+
if inst.operation.name not in ['barrier']:
|
|
428
|
+
for q in inst.qubits: used.add(routed.find_bit(q).index)
|
|
429
|
+
for q in used:
|
|
430
|
+
if q < len(self.readout_errs): esp *= (1.0 - self.readout_errs[q])
|
|
431
|
+
return s, esp
|
|
432
|
+
|
|
433
|
+
current_swaps, current_esp = get_metrics(best_layout)
|
|
434
|
+
|
|
435
|
+
for _ in range(iterations):
|
|
436
|
+
test_layout = list(best_layout)
|
|
437
|
+
idx1, idx2 = random.sample(range(self.num_qubits), 2)
|
|
438
|
+
test_layout[idx1], test_layout[idx2] = test_layout[idx2], test_layout[idx1]
|
|
439
|
+
|
|
440
|
+
new_swaps, new_esp = get_metrics(test_layout)
|
|
441
|
+
|
|
442
|
+
if new_swaps < current_swaps or (new_swaps == current_swaps and new_esp > current_esp):
|
|
443
|
+
current_swaps = new_swaps
|
|
444
|
+
current_esp = new_esp
|
|
445
|
+
best_layout = test_layout
|
|
446
|
+
|
|
447
|
+
return best_layout
|
|
448
|
+
|
|
449
|
+
def route(self):
|
|
450
|
+
self.model.eval()
|
|
451
|
+
dist_matrix = self.dist_matrix
|
|
452
|
+
adj = self._adj
|
|
453
|
+
|
|
454
|
+
beams = [(0.0, [-1] * self.num_qubits, list(range(self.num_phys)))]
|
|
455
|
+
|
|
456
|
+
for current_q in self.routing_order:
|
|
457
|
+
batch_graphs = []
|
|
458
|
+
batch_meta = []
|
|
459
|
+
|
|
460
|
+
for score, current_layout, current_avail in beams:
|
|
461
|
+
batch_graphs.append(self._layout_to_pyg_data(current_layout, current_q))
|
|
462
|
+
batch_meta.append((score, current_layout, current_avail))
|
|
463
|
+
|
|
464
|
+
if not batch_graphs: break
|
|
465
|
+
|
|
466
|
+
search_loader = DataLoader(batch_graphs, batch_size=len(batch_graphs), shuffle=False, pin_memory=self._use_amp)
|
|
467
|
+
next_beams = []
|
|
468
|
+
|
|
469
|
+
with torch.no_grad():
|
|
470
|
+
for batch_data in search_loader:
|
|
471
|
+
batch_data = batch_data.to(self.device, non_blocking=True)
|
|
472
|
+
with torch.autocast(device_type=self.device.type, enabled=self._use_amp):
|
|
473
|
+
logits = self.model(batch_data)
|
|
474
|
+
log_probs = F.log_softmax(logits, dim=-1).cpu().numpy()
|
|
475
|
+
|
|
476
|
+
neighbors_of_q = adj[current_q]
|
|
477
|
+
|
|
478
|
+
for b_i, (base_score, current_layout, current_avail) in enumerate(batch_meta):
|
|
479
|
+
lp_row = log_probs[b_i]
|
|
480
|
+
avail_arr = np.array(current_avail, dtype=np.int32)
|
|
481
|
+
|
|
482
|
+
if neighbors_of_q:
|
|
483
|
+
placed = [current_layout[nb] for nb in neighbors_of_q if current_layout[nb] != -1]
|
|
484
|
+
avg_dist = dist_matrix[np.ix_(avail_arr, placed)].min(axis=1) if placed else np.zeros(len(avail_arr))
|
|
485
|
+
else:
|
|
486
|
+
avg_dist = np.zeros(len(avail_arr))
|
|
487
|
+
|
|
488
|
+
scores = base_score + lp_row[avail_arr] - (self.alpha * avg_dist)
|
|
489
|
+
layout_arr = np.array(current_layout, dtype=np.int32)
|
|
490
|
+
|
|
491
|
+
for k, test_node in enumerate(avail_arr):
|
|
492
|
+
new_layout = layout_arr.copy()
|
|
493
|
+
new_layout[current_q] = test_node
|
|
494
|
+
new_avail = [n for n in current_avail if n != int(test_node)]
|
|
495
|
+
next_beams.append((float(scores[k]), new_layout.tolist(), new_avail))
|
|
496
|
+
|
|
497
|
+
next_beams.sort(key=lambda x: x[0], reverse=True)
|
|
498
|
+
beams = next_beams[:self.beam_width]
|
|
499
|
+
|
|
500
|
+
return [b[1] for b in beams]
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# ============================================================================
|
|
504
|
+
# MAIN ENTRY POINT FOR PACKAGE (UNFILTERED DEVICE-WIDE COMPETITION)
|
|
505
|
+
# ============================================================================
|
|
506
|
+
def generate_optimized_layout(num_qubits, logical_edges, backend_name="ibm_fez", train_samples=2500):
|
|
507
|
+
print("\n\033[1m\033[94m>>> ALPHAZERO NEURAL QUANTUM COMPILER <<<\033[0m")
|
|
508
|
+
|
|
509
|
+
# 1. Fetch Data
|
|
510
|
+
spin_hw = TerminalSpinner(f"Connecting to \033[33m{backend_name}\033[0m to extract high-fidelity topology...")
|
|
511
|
+
spin_hw.start()
|
|
512
|
+
cmap, readout, gate, backend, node_mapping = fetch_live_ibm_hardware_data(backend_name, max_physical_qubits=30)
|
|
513
|
+
spin_hw.stop(f"Extracted 30-qubit training slice from {backend_name}.")
|
|
514
|
+
|
|
515
|
+
compiler = NeuralAStarCompiler(num_qubits, logical_edges, cmap, readout, gate)
|
|
516
|
+
qc_base = compiler._build_circuit()
|
|
517
|
+
|
|
518
|
+
# 2. Train GNN
|
|
519
|
+
spin_train = TerminalSpinner("Harvesting layout experts and training GNN...")
|
|
520
|
+
spin_train.start()
|
|
521
|
+
compiler.train(num_samples=train_samples, spinner=spin_train)
|
|
522
|
+
spin_train.stop("GNN model trained successfully.")
|
|
523
|
+
|
|
524
|
+
# 3. Route A*
|
|
525
|
+
spin_route = TerminalSpinner("Navigating hardware topology via A* Beam Search...")
|
|
526
|
+
spin_route.start()
|
|
527
|
+
predicted_layouts = compiler.route()
|
|
528
|
+
spin_route.stop("Top predicted paths generated.")
|
|
529
|
+
|
|
530
|
+
# 4. Local Search
|
|
531
|
+
spin_ls = TerminalSpinner("Refining top layout via local hill-climbing search...")
|
|
532
|
+
spin_ls.start()
|
|
533
|
+
best_raw_layout = compiler._local_search_refine(qc_base, predicted_layouts[0])
|
|
534
|
+
spin_ls.stop("Refinement complete. Final layout finalized.")
|
|
535
|
+
|
|
536
|
+
# Create the inverse mapping to transform 0..29 internal indices back to full hardware IDs
|
|
537
|
+
inverse_mapping = {new_idx: old_idx for old_idx, new_idx in node_mapping.items()}
|
|
538
|
+
|
|
539
|
+
# Generate the global physical hardware layout mapping
|
|
540
|
+
global_layout = Layout({
|
|
541
|
+
qc_base.qubits[i]: int(inverse_mapping[best_raw_layout[i]])
|
|
542
|
+
for i in range(num_qubits)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
# ========================================================================
|
|
546
|
+
# UNFILTERED GLOBAL DEVICE BENCHMARK REPORT
|
|
547
|
+
# ========================================================================
|
|
548
|
+
print("\n\033[1m\033[96m============================================================\033[0m")
|
|
549
|
+
print("\033[1m\033[96m UNFILTERED DEVICE-WIDE COMPILATION BENCHMARK \033[0m")
|
|
550
|
+
print("\033[1m\033[96m============================================================\033[0m")
|
|
551
|
+
|
|
552
|
+
# Baseline: Run against the UNRESTRICTED full chip target map
|
|
553
|
+
pm_baseline = generate_preset_pass_manager(optimization_level=3, backend=backend, seed_transpiler=42)
|
|
554
|
+
routed_baseline = pm_baseline.run(qc_base)
|
|
555
|
+
base_swaps = routed_baseline.count_ops().get("swap", 0)
|
|
556
|
+
base_esp = calculate_esp_global(routed_baseline, backend)
|
|
557
|
+
|
|
558
|
+
# AI Performance: Injected global layout layout applied to the full chip target map
|
|
559
|
+
pm_ai = generate_preset_pass_manager(optimization_level=3, backend=backend, initial_layout=global_layout, seed_transpiler=42)
|
|
560
|
+
routed_ai = pm_ai.run(qc_base)
|
|
561
|
+
ai_swaps = routed_ai.count_ops().get("swap", 0)
|
|
562
|
+
ai_esp = calculate_esp_global(routed_ai, backend)
|
|
563
|
+
|
|
564
|
+
# Metrics Printout
|
|
565
|
+
print(f" Target Hardware System : {backend.name} ({backend.target.num_qubits} Total Qubits)")
|
|
566
|
+
print(f" AI Training Constraint : Optimized inside best local 30-qubit slice")
|
|
567
|
+
print(f" Qiskit Search Constraint : \033[91mNone\033[0m (Allowed to scan full {backend.target.num_qubits}-qubit graph)")
|
|
568
|
+
print("------------------------------------------------------------")
|
|
569
|
+
|
|
570
|
+
# Formatting Swaps Improvement Comparison
|
|
571
|
+
if ai_swaps < base_swaps:
|
|
572
|
+
swap_color = "\033[92m" # Green if AI won
|
|
573
|
+
status = f"[✓ Saved {base_swaps - ai_swaps} SWAPs]"
|
|
574
|
+
elif ai_swaps == base_swaps:
|
|
575
|
+
swap_color = "\033[93m" # Yellow if tied
|
|
576
|
+
status = "[Tied Standard Level 3]"
|
|
577
|
+
else:
|
|
578
|
+
swap_color = "\033[91m" # Red if Qiskit won standard
|
|
579
|
+
status = "[Standard Full-Graph Pick Preferred]"
|
|
580
|
+
|
|
581
|
+
print(f" Standard Qiskit Level 3 : {base_swaps} SWAP gates | ESP: {base_esp*100:.2f}%")
|
|
582
|
+
print(f" {swap_color}AlphaZero + A* Seeded : {ai_swaps} SWAP gates | ESP: {ai_esp*100:.2f}% {status}\033[0m")
|
|
583
|
+
print("\033[1m\033[96m============================================================\033[0m\n")
|
|
584
|
+
|
|
585
|
+
return global_layout
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qcompile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-guided hardware-aware quantum circuit layout optimization for Qiskit
|
|
5
|
+
Author-email: Shagun Tembhurne <shaxtembhurne@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shaxtembhurne/qcompile
|
|
8
|
+
Project-URL: Repository, https://github.com/shaxtembhurne/qcompile
|
|
9
|
+
Project-URL: Issues, https://github.com/shaxtembhurne/qcompile/issues
|
|
10
|
+
Keywords: quantum-computing,qiskit,compiler,transpiler,layout,optimization,graph-neural-network,machine-learning
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: numpy>=1.24
|
|
24
|
+
Requires-Dist: networkx>=3.0
|
|
25
|
+
Requires-Dist: joblib>=1.3
|
|
26
|
+
Requires-Dist: qiskit>=1.0
|
|
27
|
+
Requires-Dist: qiskit-ibm-runtime>=0.20
|
|
28
|
+
Requires-Dist: torch>=2.0
|
|
29
|
+
Requires-Dist: torch-geometric>=2.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qcompile
|
qcompile-0.1.0/setup.cfg
ADDED