qcompile 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.
qcompile/__init__.py ADDED
@@ -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,5 @@
1
+ qcompile/__init__.py,sha256=rlvc-7CMRY9jIlgpOoSV4S0Lyw-A7ZUDV9ygj_MWaAE,25451
2
+ qcompile-0.1.0.dist-info/METADATA,sha256=yYR9pFS5iIZ3Sgq68y1fDwFCjosswNtV6-aQevwrJFI,1295
3
+ qcompile-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ qcompile-0.1.0.dist-info/top_level.txt,sha256=2PT05p070QuWjJas4bUWIKUPUk4UYUdkXA6zx3-MeGQ,9
5
+ qcompile-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ qcompile