Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
ilp.py
Go to the documentation of this file.
1 import os, heapq
2 from squander.partitioning.tools import get_qubits, build_dependency, parts_to_float_ops, total_float_ops
3 
4 _GUROBI_FALLBACK_WARNING_PRINTED = False
5 _GUROBI_AVAILABLE = None
6 _GUROBI_AVAILABILITY_ERROR = None
7 
8 
10  global _GUROBI_FALLBACK_WARNING_PRINTED
11  if _GUROBI_FALLBACK_WARNING_PRINTED:
12  return
13  _GUROBI_FALLBACK_WARNING_PRINTED = True
14  print(
15  "Warning: Gurobi ILP solver is unavailable; falling back to CBC. "
16  "This can be significantly slower. For best performance, install a "
17  "valid Gurobi license in your home directory or under /opt/gurobi/ "
18  "as described in the Gurobi documentation. "
19  f"Original Gurobi error: {exc}"
20  )
21 
22 
24  global _GUROBI_AVAILABLE, _GUROBI_AVAILABILITY_ERROR
25  if _GUROBI_AVAILABLE is False:
26  raise _GUROBI_AVAILABILITY_ERROR
27  if _GUROBI_AVAILABLE is True:
28  return
29  import gurobipy as gp
30  try:
31  env = gp.Env(params={"OutputFlag": 0})
32  env.dispose()
33  _GUROBI_AVAILABLE = True
34  except Exception as exc:
35  _GUROBI_AVAILABLE = False
36  _GUROBI_AVAILABILITY_ERROR = exc
37  raise
38 
39 
40 def _solve_pulp_with_gurobi_or_cbc(prob, pulp, callback=None, **gurobi_kwargs):
41  try:
43  solver = pulp.GUROBI(manageEnv=True, msg=False, **gurobi_kwargs)
44  if callback is None:
45  prob.solve(solver)
46  else:
47  prob.solve(solver, callback=callback)
48  return "gurobi"
49  except Exception as exc:
51  prob.solve(pulp.PULP_CBC_CMD(msg=False))
52  return "cbc"
53 
54 
55 def topo_sort_partitions(c, parts):
56  """
57  Topologically sort partition groups and re-partition with Kahn’s algorithm.
58 
59  Args:
60  c: SQUANDER circuit object.
61  max_qubits_per_partition (int): Maximum number of qubits allowed in a partition.
62  parts (list[Iterable[int]]): Precomputed gate groups (each is an iterable of gate indices).
63 
64  Returns:
65  tuple: (partitioned_circ, param_order, parts)
66  partitioned_circ: 2-level partitioned circuit (SQUANDER object).
67  param_order (list[tuple[int,int,int]]): Tuples (source_idx, dest_idx, param_count).
68  parts (list[frozenset[int]]): Final partition assignments (topologically sorted).
69  """
70  gatedict = {i: gate for i, gate in enumerate(c.get_Gates())}
71  gate_to_qubit = {i: get_qubits(g) for i, g in gatedict.items()}
72  partdict = {gate: i for i, part in enumerate(parts) for gate in part}
73  g, rg = {i: set() for i in range(len(parts))}, {i: set() for i in range(len(parts))}
74  for gate in gatedict:
75  for child in c.get_Children(gatedict[gate]):
76  if partdict[gate] == partdict[child]: continue
77  g[partdict[gate]].add(partdict[child])
78  rg[partdict[child]].add(partdict[gate])
79  # Stable key per partition: sorted tuple of all qubits touched by its gates.
80  part_qubits = {
81  i: tuple(sorted(set.union(*(gate_to_qubit[g] for g in part))))
82  for i, part in enumerate(parts)
83  }
84  L, S = [], [(part_qubits[m], m) for m in rg if len(rg[m]) == 0]
85  heapq.heapify(S)
86  while S:
87  n = heapq.heappop(S)[1]
88  L.append(n)
89  assert len(rg[n]) == 0
90  for m in set(g[n]):
91  g[n].remove(m)
92  rg[m].remove(n)
93  if len(rg[m]) == 0:
94  heapq.heappush(S, (part_qubits[m], m))
95  assert len(L) == len(parts), (len(L), len(parts), g, rg, partdict)
96  return L
97 
98 #@brief Partitions a circuit using ILP to maximize gates per partition
99 #@param c The SQUANDER circuit to be partitioned
100 #@param max_qubits_per_partition Maximum qubits allowed per partition
101 #@return Tuple:
102 # - Partitioned 2-level circuit
103 # - Tuples specifying new parameter positions: source_idx, dest_idx, param_count
104 # - Partition assignments
105 def ilp_max_partitions(c, max_qubits_per_partition):
106  """
107  Partitions a circuit using ILP to maximize gates per partition
108  Args:
109  c: SQUANDER Circuit to partition
110  max_qubits_per_partition: Max qubits per partition
111 
112  Returns:
113  Partitioned circuit, parameter order (source_idx, dest_idx, param_count), partition assignments
114  """
115  gatedict = {i: gate for i, gate in enumerate(c.get_Gates())}
116  num_gates = len(gatedict)
117  parts = []
118  while sum(len(x) for x in parts) != num_gates:
119  parts.append(find_next_biggest_partition(c, max_qubits_per_partition, parts))
120  L = topo_sort_partitions(c, parts)
121  from squander.partitioning.kahn import kahn_partition_preparts
122  return kahn_partition_preparts(c, max_qubits_per_partition, [parts[i] for i in L])
123 
124 
125 def find_next_biggest_partition(c, max_qubits_per_partition, prevparts=None):
126  """
127  Find the next largest feasible gate partition via ILP (single partition step).
128 
129  Args:
130  c: SQUANDER circuit to be partitioned.
131  max_qubits_per_partition (int): Maximum number of qubits allowed in the partition.
132  prevparts (Iterable[Iterable[int]] | None): Previously selected partitions whose
133  gates must be excluded in this step.
134 
135  Returns:
136  set[int]: Set of gate indices forming the next largest partition.
137  """
138  import pulp
139  gatedict = {i: gate for i, gate in enumerate(c.get_Gates())}
140  all_qubits = set(range(c.get_Qbit_Num()))
141 
142  num_gates = len(gatedict)
143  prob = pulp.LpProblem("MaxSinglePartition", pulp.LpMinimize)
144  a = pulp.LpVariable.dicts("a", (i for i in range(num_gates)), cat="Binary") #is gate i in previous partition to p
145  x = pulp.LpVariable.dicts("x", (i for i in range(num_gates)), cat="Binary") #is gate i in partition p
146  z = pulp.LpVariable.dicts("z", (q for q in all_qubits), cat="Binary") #is qubit q in paritition p
147  # Constraint 1: not gate is in both a and x
148  for i in gatedict:
149  prob += x[i] + a[i] <= 1
150  # Constraint 2: if gate g_i uses qubit q, then z_{q} = 1
151  for i in gatedict:
152  gate_qubits = get_qubits(gatedict[i])
153  for q in gate_qubits:
154  prob += x[i] <= z[q]
155  # Constraint 3: the partition uses at most k qubits
156  prob += pulp.lpSum(z[q] for q in all_qubits) <= max_qubits_per_partition
157  # Constraint 4: previous partitions are filtered out, and are none or all included if in pre-partition
158  for part in prevparts:
159  for i in part:
160  prob += x[i] == 0
161  prob += a[i] == a[next(iter(part))]
162  # Constraint 5: ordering constraints for dependencies
163  for j in gatedict:
164  for i in c.get_Children(gatedict[j]): #there exists a topological ordering of all the gates enabled in x
165  prob += a[j] >= a[i]
166  prob += x[j] + a[j] >= x[i]
167  prob.setObjective(-pulp.lpSum(x[i] for i in range(num_gates)))
168  #from gurobilic import get_gurobi_options
169  #prob.solve(pulp.GUROBI(manageEnv=True, msg=False, envOptions=get_gurobi_options()))
170  _solve_pulp_with_gurobi_or_cbc(prob, pulp, timeLimit=180, Threads=os.cpu_count())
171  #prob.solve(pulp.PULP_CBC_CMD(msg=False))
172  gates = {i for i in range(num_gates) if int(pulp.value(x[i]))}
173  #qubits = set.union(*(get_qubits(gatedict[i]) for i in gates))
174  #print(f"Status: {pulp.LpStatus[prob.status]} Found partition with {len(gates)} gates: {gates} and {len(qubits)} qubits: {qubits}")
175  return gates
176 
177 #https://www.sciencedirect.com/science/article/abs/pii/0020019094901287
178 def nuutila_reach_scc(succ, subg=None):
179  """
180  Compute SCCs and intra-SCC reachability using Nuutila's variant (iterative).
181 
182  Args:
183  succ (dict[Hashable, Iterable[Hashable]]): Successor adjacency list.
184  subg (set[Hashable] | None): Optional node subset to restrict the search.
185 
186  Returns:
187  tuple: (sccs, reach)
188  sccs (dict[Hashable, set]): Map from SCC root to set of vertices in that SCC.
189  reach (dict[Hashable, set]): For each SCC root, the set of vertices reachable
190  from that SCC (including the SCC itself iff it has a self-loop).
191  """
192  index, s, sc, sccs, reach = 0, [], [], {}, {}
193  indexes, lowlink, croot, stackheight = {}, {}, {}, {}
194  def nuutila(v, index):
195  #index/D, lowlink/CCR/component candidate root, final component/C, component stack height/H
196  stack = [(v, None, iter(succ[v]))]
197  while len(stack) != 0:
198  v, w, succv = stack.pop()
199  if w is None:
200  indexes[v], lowlink[v] = index, v
201  stackheight[v] = len(sc)
202  index += 1
203  s.append(v)
204  elif not w in croot:
205  if indexes[lowlink[w]] < indexes[lowlink[v]]: lowlink[v] = lowlink[w]
206  else: sc.append(croot[w])
207  for w in succv:
208  if not subg is None and not w in subg: continue
209  forward_edge = False
210  if not w in indexes: stack.append((v, w, succv)); stack.append((w, None, iter(succ[w]))); break #index = nuutila(w, index)
211  else: forward_edge = indexes[v] < indexes[w]
212  if not w in croot:
213  if indexes[lowlink[w]] < indexes[lowlink[v]]: lowlink[v] = lowlink[w]
214  elif not forward_edge: #(v, w) is not a forward edge - whether w on stack or not...
215  sc.append(croot[w])
216  else:
217  if lowlink[v] == v:
218  sccs[v] = set()
219  is_self_loop = s[-1] != v or v in succ[v]
220  while True:
221  w = s.pop()
222  sccs[v].add(w)
223  if w == v: break
224  reach[v] = set(sccs[v]) if is_self_loop else set()
225  if not subg is None and len(subg) == len(sccs[v]): return index
226  for x in sccs[v]: croot[x] = v
227  l = set()
228  while len(sc) != stackheight[v]:
229  x = sc.pop()
230  l.add(x)
231  for x in sorted(l, reverse=True, key=lambda y: indexes[y]):
232  if not x in reach[v]:
233  reach[v] |= reach[x]; reach[v] |= sccs[x]
234  return index
235  for v in succ:
236  if (subg is None or v in subg) and not v in indexes:
237  index = nuutila(v, index)
238  return sccs, reach #keys are SCCs, values are reachable vertices
239 
240 def _get_topo_order(g, rg, gate_to_qubit):
241  """
242  Get a topological order of a DAG given forward and reverse adjacency.
243 
244  Args:
245  g (dict[int, set[int]]): Forward adjacency (u -> successors).
246  rg (dict[int, set[int]]): Reverse adjacency (v -> predecessors).
247 
248  Returns:
249  list[int]: A topological ordering of nodes.
250  """
251  g = { x: set(y) for x, y in g.items() }
252  rg = { x: set(y) for x, y in rg.items() }
253  S = [(tuple(sorted(gate_to_qubit[m])), m) for m in rg if len(rg[m]) == 0]
254  heapq.heapify(S)
255  L = []
256  while S:
257  n = heapq.heappop(S)[1]
258  L.append(n)
259  assert not rg[n]
260  for m in set(g[n]):
261  g[n].remove(m)
262  rg[m].remove(n)
263  if not rg[m]:
264  heapq.heappush(S, (tuple(sorted(gate_to_qubit[m])), m))
265  return L
266 
267 def contract_single_qubit_chains(go, rgo, gate_to_qubit, topo_order):
268  """
269  Contract maximal chains of single-qubit gates to simplify the DAG.
270 
271  Args:
272  go (dict[int, set[int]]): Forward gate DAG (gate -> children).
273  rgo (dict[int, set[int]]): Reverse gate DAG (gate -> parents).
274  gate_to_qubit (dict[int, set[int]]): Gate index -> acted-on qubits.
275  topo_order (list[int]): A topological order of the original DAG.
276 
277  Returns:
278  tuple: (g, rg, topo_order_reduced, chains)
279  g (dict[int, set[int]]): Reduced forward DAG after contraction.
280  rg (dict[int, set[int]]): Reduced reverse DAG after contraction.
281  topo_order_reduced (list[int]): Topological order without single-qubit gates.
282  chains (set[tuple[int,...]]): Set of contracted chains (each a tuple of gate ids).
283  """
284  # Identify and contract single-qubit chains in the circuit
285  g = { x: set(y) for x, y in go.items() }
286  rg = { x: set(y) for x, y in rgo.items() }
287  single_qubit_gates = {x for x, y in gate_to_qubit.items() if len(y) == 1}
288  single_qubit_chains = {}
289  for gate in topo_order:
290  if gate in single_qubit_gates:
291  # Contract the single-qubit chain
292  if rgo[gate]:
293  v = next(iter(rgo[gate]))
294  single_qubit_chains[gate] = single_qubit_chains[v] if v in single_qubit_chains else []
295  else: single_qubit_chains[gate] = []
296  single_qubit_chains[gate].append(gate)
297  if rg[gate]:
298  v = next(iter(rg[gate]))
299  g[v].remove(gate)
300  g[v] |= g[gate]
301  if g[gate]:
302  v = next(iter(g[gate]))
303  rg[v].remove(gate)
304  rg[v] |= rg[gate]
305  del g[gate]; del rg[gate]
306  topo_order = [x for x in topo_order if not x in single_qubit_gates] #topo_order = _get_topo_order(g, rg, gate_to_qubit)
307  return g, rg, topo_order, set(tuple(x) for x in single_qubit_chains.values())
308 
309 def recombine_single_qubit_chains(g, rg, single_qubit_chains, gate_to_tqubit, L, fusion_info, surrounded_only=False):
310  """
311  Re-insert contracted single-qubit chains back into partition groups.
312 
313  Args:
314  g (dict[int, set[int]]): Forward DAG of original gates.
315  rg (dict[int, set[int]]): Reverse DAG of original gates.
316  single_qubit_chains (set[tuple[int,...]]): Contracted chains to re-attach.
317  gate_to_tqubit (dict[int, int]): Gate -> target qubit id.
318  L (Iterable[Iterable[int]]): Current partition groups.
319  fusion_info (tuple[set[int], set[int]] | None): Optional pre/post placement
320  hints (inpre, inpost) for chain endpoints.
321 
322  Returns:
323  list[frozenset[int]]: Updated partition groups with single-qubit chains merged.
324  """
325  L = [set(x) for x in L]
326  gate_to_part = {x: part for part in L for x in part}
327  if fusion_info is not None: inpre, inpost = fusion_info
328  for chain in single_qubit_chains:
329  #qbitidx = gate_to_tqubit[chain[0]]
330  if rg[chain[0]] and g[chain[-1]]:
331  v = next(iter(rg[chain[0]]))
332  w = next(iter(g[chain[-1]]))
333  if not surrounded_only and (fusion_info is None or chain[0] in inpre) or gate_to_part[v] == gate_to_part[w]:
334  gate_to_part[v] |= frozenset(chain)
335  elif not surrounded_only and (fusion_info is None or chain[-1] in inpost):
336  gate_to_part[w] |= frozenset(chain)
337  else: L.append(frozenset(chain))
338  elif not surrounded_only and rg[chain[0]]:
339  v = next(iter(rg[chain[0]]))
340  if fusion_info is None or chain[0] in inpre:
341  gate_to_part[v] |= frozenset(chain)
342  else: L.append(frozenset(chain))
343  elif not surrounded_only and g[chain[-1]]:
344  v = next(iter(g[chain[-1]]))
345  if fusion_info is None or chain[-1] in inpost:
346  gate_to_part[v] |= frozenset(chain)
347  else: L.append(frozenset(chain))
348  else:
349  L.append(frozenset(chain))
350  return L
352  """
353  Strongly Connected Components (Tarjan) without recursion.
354 
355  Parameters
356  ----------
357  succ : list of iterables
358  succ[u] is an iterable of out-neighbors of u. Nodes are 0..n-1.
359 
360  Returns
361  -------
362  comp_id : list[int]
363  comp_id[u] = index of the SCC containing u (0..k-1).
364  comps : list[list[int]]
365  List of SCCs; each is a list of nodes. Order is discovery order.
366 
367  Notes
368  -----
369  - No recursion; uses an explicit DFS stack of (u, iterator) frames.
370  - Self-loops and parallel edges are handled.
371  - Time O(n + m), space O(n).
372  """
373  index = {u: -1 for u in succ}
374  low = {u: 0 for u in succ}
375  onstk = {u: False for u in succ}
376 
377  tarjan_stack = [] # classic Tarjan stack of nodes
378  comp_id = {u: -1 for u in succ}
379  comps = []
380 
381  idx = 0
382 
383  # DFS frames: (u, iterator over neighbors)
384  dfs_stack = []
385 
386  for s in succ:
387  if index[s] != -1:
388  continue
389 
390  # Start a new DFS at s
391  index[s] = low[s] = idx; idx += 1
392  tarjan_stack.append(s); onstk[s] = True
393  dfs_stack.append((s, iter(succ[s])))
394 
395  while dfs_stack:
396  u, it = dfs_stack[-1]
397  v = next(it, None)
398  if v is None:
399  # Finish u
400  if low[u] == index[u]:
401  # u is root of an SCC; pop until u
402  comp = []
403  while True:
404  w = tarjan_stack.pop()
405  onstk[w] = False
406  comp_id[w] = len(comps)
407  comp.append(w)
408  if w == u:
409  break
410  comps.append(comp)
411  dfs_stack.pop()
412  # Propagate lowlink to parent (tree-edge return)
413  if dfs_stack:
414  p, _ = dfs_stack[-1]
415  if low[u] < low[p]:
416  low[p] = low[u]
417  else:
418  # Process neighbor v
419  if index[v] == -1:
420  # Tree edge: discover v and descend
421  index[v] = low[v] = idx; idx += 1
422  tarjan_stack.append(v); onstk[v] = True
423  dfs_stack.append((v, iter(succ[v])))
424  elif onstk[v]:
425  # Back/cross edge into current DFS stack => update low[u]
426  if index[v] < low[u]:
427  low[u] = index[v]
428  # else: edge to a vertex already assigned to an SCC — ignore
429  return comp_id, comps
430 def get_part_cycle_graph(g, gate_to_parts):
431  """
432  Build a partition-level interaction DAG and prune intra-overlap edges.
433 
434  Args:
435  g (dict[int, set[int]]): Gate DAG (u -> successors v).
436  gate_to_parts (dict[int, Iterable[int]]): Gate -> indices of parts containing it.
437 
438  Returns:
439  dict[int, set[int]]: Pruned successor map among parts (DAG over part indices).
440  """
441  overlaps = {}
442  for v, idxs in gate_to_parts.items():
443  for i in idxs:
444  overlaps.setdefault(i, set()).update(j for j in idxs if j != i)
445  succ = {x: set() for x in overlaps} #build set interaction digraph
446  for u in g:
447  U = gate_to_parts[u]
448  for v in g[u]:
449  V = gate_to_parts[v]
450  for i in U:
451  for j in V:
452  if i == j or j in overlaps[i]: continue
453  succ[i].add(j)
454  scc_id, _ = scc_tarjan_iterative(succ)
455  succ_pruned = {x: set() for x in succ}
456  for i in succ:
457  for j in succ[i]:
458  if scc_id[i] == scc_id[j]: succ_pruned[i].add(j)
459  return succ_pruned
460 def all_cycles_from_dag_edges(succ, max_len=5):
461  """
462  Enumerate chordless directed cycles consistent with a given successor map.
463 
464  Args:
465  succ (dict[Hashable, set[Hashable]]): Successor adjacency among nodes.
466  max_len (int | None): Optional max cycle length bound; None disables bound.
467 
468  Returns:
469  list[list[Hashable]]: List of chordless cycles (each as node list in order).
470  """
471  #import networkx as nx
472  #G = nx.DiGraph(succ)
473  #return list(nx.chordless_cycles(G, max_len))
474  def can_extend(path, in_path, x, s):
475  """Check if we can extend ...->u to x via (u->x) without creating any chord."""
476  u = path[-1]
477  if x in in_path or x < s: # simplicity and duplicate avoidance: only grow to >= start
478  return False
479  # forward chords: forbid v->x from any non-consecutive prior vertex
480  for v in pred[x]: #for v in path[:-1]:
481  if v != u and v in in_path: #if x in succ[v]:
482  return False
483  # backward chords: forbid x->v to any path vertex (incl. x->u, killing 2-cycles)
484  for v in path:
485  if v != s and v in succ[x]:
486  return False
487  return True
488 
489  def can_close(path):
490  """Check if we can close with last->s and remain chordless."""
491  s = path[0]
492  u = path[-1]
493  if s not in succ[u]:
494  return False
495  # No extra arcs from s to internal vertices except s->path[1]
496  for v in path[2:]:
497  if v in succ[s]:
498  return False
499  # No extra arcs to s from internal vertices except u->s
500  for v in path[:-1]:
501  if v != u and s in succ[v]:
502  return False
503  return True
504  pred = {v: set() for v in succ}
505  for u in succ:
506  for v in succ[u]: pred[v].add(u)
507  cycles = []
508  for s in succ:
509  if not succ[s] or not pred[s]: continue #no predecessors could also be skipped
510  if max_len is not None:
511  dist_to_s = {v: None for v in succ}
512  from collections import deque
513  dq = deque([s])
514  dist_to_s[s] = 0
515  while dq:
516  w = dq.popleft()
517  for p in pred[w]:
518  if dist_to_s[p] is None:
519  dist_to_s[p] = dist_to_s[w] + 1
520  dq.append(p)
521  path = [s]
522  in_path = {s}
523  stack = [(s, iter(succ[s]), False)]
524  while stack:
525  u, nbrs, tried_close = stack[-1]
526  # Try closing a cycle once per frame
527  if not tried_close and len(path) >= 2 and (s in succ[u]) and can_close(path):
528  print(path)
529  cycles.append(path.copy())
530  stack[-1] = (u, nbrs, True) #mark as tried to close
531  continue
532  # Advance neighbor iterator
533  x = next(nbrs, None)
534  if x is None:
535  # backtrack
536  stack.pop()
537  in_path.remove(u)
538  path.pop()
539  continue
540 
541  # bump index in the top frame
542  stack[-1] = (u, nbrs, tried_close)
543 
544  if max_len is not None and (dist_to_s[x] is None or len(path) + dist_to_s[x] > max_len):
545  continue
546 
547  if can_extend(path, in_path, x, s):
548  # descend to x
549  path.append(x)
550  in_path.add(x)
551  stack.append((x, iter(succ[x]), False))
552  return cycles
553 def two_cycles_from_dag_edges(g, gate_to_parts, allparts):
554  """
555  Detect pairwise 2-cycles between partitions induced by gate-level edges.
556 
557  Args:
558  g (dict[int, set[int]]): Gate DAG (u -> successors v).
559  gate_to_parts (dict[int, Iterable[int]]): Gate -> indices of parts containing it.
560  allparts (list[frozenset[int]]): The parts (gate sets) by index.
561 
562  Returns:
563  list[tuple[int, int]]: List of (a, b) where a < b and a <-> b two-cycle is found,
564  excluding cases where parts overlap.
565  """
566  # edges: iterable of (u, v) over the original DAG
567  seen = {} # (a,b) with a<b -> 1 if a->b seen, 2 if b->a seen, 3 if both
568  twocycles = [] # list of (a,b) with 2-cycle detected
569  for u in g:
570  for v in g[u]:
571  pu = gate_to_parts[u] # iterable of partition ids containing u
572  pv = gate_to_parts[v] # iterable of partition ids containing v
573  for i in pu:
574  for j in pv:
575  if i == j: continue
576  a, b = (i, j) if i < j else (j, i)
577  bit = 1 if i < j else 2
578  prev = seen.get((a, b), 0)
579  new = prev | bit
580  if new == 3 and prev != 3 and len(allparts[a] & allparts[b]) == 0: # <-- only on first time reaching 3
581  twocycles.append((a, b)) # a<->b found
582  seen[(a, b)] = new
583  return twocycles
584 
585 def sol_to_badsccs(g, allparts, L):
586  gate_to_part = {}
587  for i in L:
588  for gate in allparts[i]: gate_to_part[gate] = i
589  G_part = {i: set() for i in L}
590  for i in L: #build partition get strongly connected components to block invalid cyclic solutions
591  for u in allparts[i]:
592  for v in g[u]:
593  if not v in gate_to_part: continue
594  j = gate_to_part[v]
595  if i != j:
596  G_part[i].add(j)
597  _, scc = scc_tarjan_iterative(G_part)
598  return {frozenset(v) for v in scc if len(v) > 1}
599 
600 def ilp_global_optimal(allparts, g, weighted_info=None, gurobi_direct=False, use_order=False, weights=None):
601  """
602  Select an optimal set of non-overlapping parts via ILP/MIP with cycle cuts.
603 
604  Args:
605  allparts (list[frozenset[int]]): Candidate parts (gate sets).
606  g (dict[int, set[int]]): Gate DAG (u -> successors v).
607  weighted_info (tuple | None): Optional tuple providing cost modeling:
608  (single_qubit_chains, max_qubits_per_partition, go, rgo, gate_to_qubit, gate_to_tqubit).
609  gurobi_direct (bool): If True, build and solve directly in gurobipy with callbacks.
610  use_order (bool): If True, add an ordering formulation to avoid cycles.
611 
612  Returns:
613  tuple: (selected_parts, fusion_info or None)
614  selected_parts (list[frozenset[int]]): Chosen parts minimizing the cost.
615  fusion_info (tuple[set[int], set[int]] | None): (inpre, inpost) sets for
616  chain placements when weighted_info enables fusion costs.
617  """
618  if weighted_info is not None:
619  single_qubit_chains, max_qubits_per_partition, go, rgo, gate_to_qubit, gate_to_tqubit = weighted_info
620  ignored_chains = {x for x in single_qubit_chains if False} # Placeholder for ignored chains of identity, barrier, delay, measure
621  single_qubit_chains_pre = {x[0]: x for x in single_qubit_chains if rgo[x[0]] and not x in ignored_chains}
622  single_qubit_chains_post = {x[-1]: x for x in single_qubit_chains if go[x[-1]] and not x in ignored_chains}
623  single_qubit_chains_prepost = {x[0]: x for x in single_qubit_chains if x[0] in single_qubit_chains_pre and x[-1] in single_qubit_chains_post}
624  def fortet_inequalities(x, y, z): #-z-x<=0 -z+x+y<=1 z-x<=0 z+x-y<=1
625  return [z-x<=0, z-y<=0, x+y-z<=1]
626  N = len(allparts)
627  gate_to_parts = {x: [] for x in g}
628  for i, part in enumerate(allparts):
629  for gate in part: gate_to_parts[gate].append(i)
630  if gurobi_direct:
631  from gurobipy import Env, Model, GRB
632  import gurobipy as gp
633  with Env() as env:
634  env.setParam("OutputFlag", 0)
635  with Model(env=env) as m:
636  m.setParam(GRB.Param.IntegralityFocus, 1)
637  m.setParam(GRB.Param.LazyConstraints, 1)
638  x = m.addVars(range(N), lb=[0]*N, ub=[1]*N, vtype=[GRB.BINARY]*N, name=["x_" + str(i) for i in range(N)])
639  for i in g: m.addConstr(gp.quicksum(x[j] for j in gate_to_parts[i]) == 1)
640  if weights is not None: m.setObjective(gp.quicksum((weights[i]*N+1) * x[i] for i in range(N)), GRB.MINIMIZE)
641  elif weighted_info is None: m.setObjective(gp.quicksum(x[i] for i in range(N)), GRB.MINIMIZE)
642  else:
643  Npre, Npost, Nprepost = len(single_qubit_chains_pre), len(single_qubit_chains_post), len(single_qubit_chains_prepost)
644  pre = m.addVars(list(single_qubit_chains_pre), lb=[0]*Npre, ub=[1]*Npre, vtype=[GRB.BINARY]*Npre, name=["pre_" + str(i) for i in single_qubit_chains_pre])
645  post = m.addVars(list(single_qubit_chains_post), lb=[0]*Npost, ub=[1]*Npost, vtype=[GRB.BINARY]*Npost, name=["post_" + str(i) for i in single_qubit_chains_post])
646  noprepost = m.addVars(list(single_qubit_chains_prepost), lb=[0]*Nprepost, ub=[1]*Nprepost, vtype=[GRB.BINARY]*Nprepost, name=["prepost_" + str(i) for i in single_qubit_chains_prepost])
647  m.update()
648  S, t, u, targets = [], {}, {}, {}
649  surrounded = {s: [] for s in noprepost}
650  for i in range(N):
651  part = allparts[i]
652  surrounded_chains = {t for s in part for t in go[s] if t in single_qubit_chains_prepost and go[single_qubit_chains_prepost[t][-1]] and next(iter(go[single_qubit_chains_prepost[t][-1]])) in part}
653  for v in surrounded_chains: surrounded[v].append(i)
654  fullpart = frozenset.union(part, *(single_qubit_chains_prepost[v] for v in surrounded_chains))
655  qubits = set.union(*(gate_to_qubit[v] for v in fullpart))
656  tqubits = {gate_to_tqubit[v] for v in fullpart}
657  cqubits = qubits - tqubits
658  targets[i] = {}
659  impurities = []
660  is_pure = len({frozenset(gate_to_qubit[x]-{gate_to_tqubit[x]}) for x in fullpart}) == 1
661  for p in part:
662  for s in go[p]:
663  if s in single_qubit_chains_pre:
664  v = gate_to_tqubit[s]
665  if v in cqubits:
666  a = m.addVar(lb = 0, ub = 1, vtype = GRB.BINARY, name=f"pre_t_{i}_{s}")
667  m.update()
668  for z in fortet_inequalities(pre[s], x[i], a): m.addConstr(z)
669  targets[i].setdefault(v, []).append(a)
670  elif not s in fullpart:
671  if is_pure: impurities.append(pre[s])
672  else:
673  if s in noprepost: m.addConstr(pre[s] + post[single_qubit_chains_prepost[s][-1]] >= x[i])
674  else: m.addConstr(pre[s] >= x[i])
675  for s in rgo[p]:
676  if s in single_qubit_chains_post:
677  v = gate_to_tqubit[s]
678  if v in cqubits:
679  a = m.addVar(lb = 0, ub = 1, vtype = GRB.BINARY, name=f"post_t_{i}_{s}")
680  m.update()
681  for z in fortet_inequalities(post[s], x[i], a): m.addConstr(z)
682  targets[i].setdefault(v, []).append(a)
683  elif not s in fullpart:
684  if is_pure: impurities.append(post[s])
685  else:
686  if s in noprepost: m.addConstr(post[s] + pre[single_qubit_chains_prepost[s][0]] >= x[i])
687  else: m.addConstr(post[s] >= x[i])
688  Nu = len(targets[i]); Nt = Nu+1
689  t[i] = m.addVars(range(Nt), lb=[0]*Nt, ub=[1]*Nt, vtype=[GRB.BINARY]*Nt, name=[f"t_{i}_" + str(j) for j in range(Nt)])
690  u[i] = m.addVars(list(targets[i]), lb=[0]*Nu, ub=[1]*Nu, vtype=[GRB.BINARY]*Nu, name=[f"u_{i}_" + str(j) for j in targets[i]])
691  for s in u[i]: m.addConstr(u[i][s] <= x[i])
692  for target in targets[i]:
693  for a in targets[i][target]: m.addConstr(u[i][target] >= a)
694  m.addConstr(gp.quicksum(t[i][j] for j in range(Nt)) == x[i]) #only one target count selected
695  m.addConstr(gp.quicksum(j*t[i][j] for j in range(Nt)) == gp.quicksum(u[i][s] for s in u[i]))
696  if is_pure and impurities:
697  isimpure = m.addVar(lb=0, ub=1, vtype=GRB.BINARY, name=f"impurity_{i}")
698  for z in impurities: m.addConstr(isimpure >= z)
699  gate_qubits = len(qubits)
700  for j in range(Nt):
701  control_qubits = len(cqubits) - j
702  g_size = 2**(gate_qubits-control_qubits)
703  if is_pure and j==0 and impurities:
704  t0 = m.addVars(range(2), lb=[0]*2, ub=[1]*2, vtype=[GRB.BINARY]*2, name=[f"t0_{i}_" + str(k) for k in range(2)])
705  for z in fortet_inequalities(t[i][j], isimpure, t0[0]): m.addConstr(z)
706  for z in fortet_inequalities(t[i][j], 1-isimpure, t0[1]): m.addConstr(z)
707  S.append(t0[0] * (2**max_qubits_per_partition * (g_size * (4 + 2) + 2 * (g_size - 1))))
708  S.append(t0[1] * (2**(max_qubits_per_partition-control_qubits) * (g_size * (4 + 2) + 2 * (g_size - 1))))
709  else:
710  S.append(t[i][j] * (2**(max_qubits_per_partition-(control_qubits if is_pure and j==0 else 0)) * (g_size * (4 + 2) + 2 * (g_size - 1))))
711  for s in pre:
712  if not s in noprepost:
713  S.append((1-pre[s])*(2**max_qubits_per_partition * (2 * (4 + 2) + 2)))
714  else:
715  m.addConstr(pre[s] + post[single_qubit_chains_prepost[s][-1]] + noprepost[s] + gp.quicksum(x[i] for i in surrounded[s]) == 1) #no pre and post for the same gate
716  S.append(noprepost[s]*(2**max_qubits_per_partition * (2 * (4 + 2) + 2)))
717  for s in post:
718  if not single_qubit_chains_post[s][0] in noprepost:
719  S.append((1-post[s])*(2**max_qubits_per_partition * (2 * (4 + 2) + 2)))
720  m.setObjective(gp.quicksum(S)*N+gp.quicksum(x[i] for i in range(N)), GRB.MINIMIZE)
721  def cb(m, where):
722  if where == GRB.Callback.MIPSOL:
723  x_val = m.cbGetSolution([x[i] for i in range(N)])
724  badsccs = sol_to_badsccs(g, allparts, [i for i, xv in enumerate(x_val) if int(round(xv))])
725  for badscc in badsccs:
726  #print([(allparts[x], [g[y] for y in allparts[x]]) for x in badscc]) #canonical partitions {1, 3} and {2, 4} with edges 1->{3, 4}, 2->{3, 4}
727  m.cbLazy(gp.quicksum(x[j] for j in badscc) <= len(badscc) - 1) #remove at least one partition from the SCC
728  m.optimize(cb)
729  if m.status == GRB.OPTIMAL:
730  return [i for i in range(N) if int(round(x[i].getAttr("X")))], ({i for i in pre if int(round(pre[i].getAttr("X")))}, {i for i in post if int(round(post[i].getAttr("X")))}) if weighted_info is not None and single_qubit_chains is not None else None
731  import pulp
732  prob = pulp.LpProblem("OptimalPartitioning", pulp.LpMinimize)
733  x = pulp.LpVariable.dicts("x", (i for i in range(N)), cat="Binary") #is partition i included
734  for i in g: prob += pulp.lpSum(x[j] for j in gate_to_parts[i]) == 1 #constraint that all gates are included exactly once
735  if use_order:
736  order = pulp.LpVariable.dicts("ord", (i for i in range(N)), lowBound=0, upBound=N-1, cat="Continuous") #order of the partition
737  succ = get_part_cycle_graph(g, gate_to_parts)
738  for u in succ:
739  for v in succ[u]:
740  prob += order[u] + 1 <= order[v] + N*(1-x[u]+1-x[v])
741  #for i in range(N): prob += order[i] <= N*x[i]
742  #print(all_cycles_from_dag_edges(succ))
743  #for u, v in two_cycles_from_dag_edges(g, gate_to_parts, allparts):
744  # prob += x[u] + x[v] <= 1 #constraint that no two cycles are included
745  if weights is not None: prob.setObjective(pulp.lpSum((weights[i]*N+1) * x[i] for i in range(N)))
746  elif weighted_info is None: prob.setObjective(pulp.lpSum(x[i] for i in range(N)))
747  else:
748  Npre, Npost, Nprepost = len(single_qubit_chains_pre), len(single_qubit_chains_post), len(single_qubit_chains_prepost)
749  pre = pulp.LpVariable.dicts("pre", list(single_qubit_chains_pre), cat="Binary")
750  post = pulp.LpVariable.dicts("post", list(single_qubit_chains_post), cat="Binary")
751  noprepost = pulp.LpVariable.dicts("prepost", list(single_qubit_chains_prepost), cat="Binary")
752  S, t, u, targets = [], {}, {}, {}
753  surrounded = {s: [] for s in noprepost}
754  for i in range(N):
755  part = allparts[i]
756  surrounded_chains = {t for s in part for t in go[s] if t in single_qubit_chains_prepost and go[single_qubit_chains_prepost[t][-1]] and next(iter(go[single_qubit_chains_prepost[t][-1]])) in part}
757  for v in surrounded_chains: surrounded[v].append(i)
758  fullpart = frozenset.union(part, *(single_qubit_chains_prepost[v] for v in surrounded_chains))
759  qubits = set.union(*(gate_to_qubit[v] for v in fullpart))
760  tqubits = {gate_to_tqubit[v] for v in fullpart}
761  cqubits = qubits - tqubits
762  targets[i] = {}
763  impurities = []
764  is_pure = len({frozenset(gate_to_qubit[x]-{gate_to_tqubit[x]}) for x in fullpart}) == 1
765  for p in part:
766  for s in go[p]:
767  if s in single_qubit_chains_pre:
768  v = gate_to_tqubit[s]
769  if v in cqubits:
770  a = pulp.LpVariable(f"pre_t_{i}_{s}", cat="Binary")
771  for z in fortet_inequalities(pre[s], x[i], a): prob += z
772  targets[i].setdefault(v, []).append(a)
773  elif not s in fullpart:
774  if is_pure: impurities.append(pre[s])
775  else:
776  if s in noprepost: prob += pre[s] + post[single_qubit_chains_prepost[s][-1]] >= x[i]
777  else: prob += pre[s] >= x[i]
778  for s in rgo[p]:
779  if s in single_qubit_chains_post:
780  v = gate_to_tqubit[s]
781  if v in cqubits:
782  a = pulp.LpVariable(f"post_t_{i}_{s}", cat="Binary")
783  for z in fortet_inequalities(post[s], x[i], a): prob += z
784  targets[i].setdefault(v, []).append(a)
785  elif not s in fullpart:
786  if is_pure: impurities.append(post[s])
787  else:
788  if s in noprepost: prob += post[s] + pre[single_qubit_chains_prepost[s][0]] >= x[i]
789  else: prob += post[s] >= x[i]
790  Nu = len(targets[i]); Nt = Nu+1
791  t[i] = pulp.LpVariable.dicts(f"t_{i}", range(Nt), cat="Binary")
792  u[i] = pulp.LpVariable.dicts(f"u_{i}", list(targets[i]), cat="Binary")
793  for s in u[i]: prob += u[i][s] <= x[i]
794  for target in targets[i]:
795  for a in targets[i][target]: prob += u[i][target] >= a
796  prob += pulp.lpSum(t[i][j] for j in range(Nt)) == x[i] #only one target count selected
797  prob += pulp.lpSum(j*t[i][j] for j in range(Nt)) == pulp.lpSum(u[i][s] for s in u[i])
798  if is_pure and impurities:
799  isimpure = pulp.LpVariable(f"impurity_{i}", cat="Binary")
800  for z in impurities: prob += isimpure >= z
801  gate_qubits = len(qubits)
802  for j in range(Nt):
803  control_qubits = len(cqubits) - j
804  g_size = 2**(gate_qubits-control_qubits)
805  if is_pure and j==0 and impurities:
806  t0 = pulp.LpVariable.dicts(f"t0_{i}", range(2), cat="Binary")
807  for z in fortet_inequalities(t[i][j], isimpure, t0[0]): prob += z
808  for z in fortet_inequalities(t[i][j], 1-isimpure, t0[1]): prob += z
809  S.append(t0[0] * (2**max_qubits_per_partition * (g_size * (4 + 2) + 2 * (g_size - 1))))
810  S.append(t0[1] * (2**(max_qubits_per_partition-control_qubits) * (g_size * (4 + 2) + 2 * (g_size - 1))))
811  else:
812  S.append(t[i][j] * (2**(max_qubits_per_partition-(control_qubits if is_pure and j==0 else 0)) * (g_size * (4 + 2) + 2 * (g_size - 1))))
813  for s in pre:
814  if not s in noprepost:
815  S.append((1-pre[s])*(2**max_qubits_per_partition * (2 * (4 + 2) + 2)))
816  else:
817  prob += pre[s] + post[single_qubit_chains_prepost[s][-1]] + noprepost[s] + pulp.lpSum(x[i] for i in surrounded[s]) == 1 #no pre and post for the same gate
818  S.append(noprepost[s]*(2**max_qubits_per_partition * (2 * (4 + 2) + 2)))
819  for s in post:
820  if not single_qubit_chains_post[s][0] in noprepost:
821  S.append((1-post[s])*(2**max_qubits_per_partition * (2 * (4 + 2) + 2)))
822  prob.setObjective(pulp.lpSum(S)*N+pulp.lpSum(x[i] for i in range(N)))
823  use_gurobi = True
824  while True:
825  cb = None
826  if use_gurobi:
827  try:
828  from gurobipy import GRB
829  import gurobipy as gp
830  def cb(m, where):
831  if where == GRB.Callback.MIPSOL:
832  xg = [m.getVarByName(x[i].name) for i in range(N)]
833  x_val = m.cbGetSolution([xg[i] for i in range(N)])
834  badsccs = sol_to_badsccs(g, allparts, [i for i, xv in enumerate(x_val) if int(round(xv))])
835  for badscc in badsccs:
836  #print([(allparts[x], [g[y] for y in allparts[x]]) for x in badscc]) #canonical partitions {1, 3} and {2, 4} with edges 1->{3, 4}, 2->{3, 4}
837  m.cbLazy(gp.quicksum(xg[j] for j in badscc) <= len(badscc) - 1) #remove at least one partition from the SCC
838  except Exception as exc:
840  use_gurobi = False
841  #from gurobilic import get_gurobi_options
842  #prob.solve(pulp.GUROBI(manageEnv=True, msg=False, envOptions=get_gurobi_options()))
843  if use_gurobi:
844  solver = _solve_pulp_with_gurobi_or_cbc(prob, pulp, callback=cb, timeLimit=180, Threads=os.cpu_count(), IntegralityFocus=1, LazyConstraints=1)
845  if solver == "cbc":
846  use_gurobi = False
847  else:
848  prob.solve(pulp.PULP_CBC_CMD(msg=False))
849  #prob.solve(pulp.PULP_CBC_CMD(msg=False))
850  #print(f"Status: {pulp.LpStatus[prob.status]}")
851  L = [i for i in range(N) if int(round(pulp.value(x[i])))]
852  badsccs = sol_to_badsccs(g, allparts, L)
853  if not badsccs: break #if all partitions do not have any cycles with more than one element per SCC terminate
854  for badscc in badsccs:
855  #print([(allparts[x], [g[y] for y in allparts[x]]) for x in badscc]) #canonical partitions {1, 3} and {2, 4} with edges 1->{3, 4}, 2->{3, 4}
856  prob += pulp.lpSum(x[j] for j in badscc) <= len(badscc) - 1 #remove at least one partition from the SCC
857  return L, ({i for i in pre if int(pulp.value(pre[i]))}, {i for i in post if int(pulp.value(post[i]))}) if weighted_info is not None and single_qubit_chains is not None else None
858 
859 def get_all_partitions(c, max_qubits_per_partition):
860  """
861  Generate all feasible parts (gate sets) for partitioning a circuit.
862 
863  Args:
864  c: SQUANDER circuit object.
865  max_qubits_per_partition (int): Max allowed qubits per partition.
866  Returns:
867  tuple: (allparts, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit)
868  allparts (set[frozenset[int]]): All feasible parts (gate sets).
869  g (dict[int, set[int]]): Gate -> successors (original graph).
870  go (dict[int, set[int]]): Gate -> successors.
871  rgo (dict[int, set[int]]): Gate -> predecessors.
872  single_qubit_chains (set[tuple[int]] | None): Set of single-qubit chains (as tuples).
873  gate_to_qubit (dict[int, set[int]]): Gate -> qubits acted on.
874  gate_to_tqubit (dict[int, int | None]): Gate -> target qubit (or None).
875  """
876  gate_dict, go, rgo, gate_to_qubit, S = build_dependency(c)
877  gate_to_tqubit = { i: g.get_Target_Qbit() for i, g in gate_dict.items() }
878  topo_order = _get_topo_order(go, rgo, gate_to_qubit)
879  g, rg, topo_order, single_qubit_chains = contract_single_qubit_chains(go, rgo, gate_to_qubit, topo_order)
880  topo_index = {x: i for i, x in enumerate(topo_order)} #all topological sorts of the DAG are the same as acyclic orderings of the transitive closure
881  _, reach = nuutila_reach_scc(g)
882  _, revreach = nuutila_reach_scc(rg)
883  def bfs_reach(X, g, rg, reach, Q):
884  level, nextlevel, visited = set(X), set(), set()
885  Qs = {}
886  while level:
887  for v in level:
888  R = rg[v] & reach
889  if not R <= visited: continue
890  Qs[v] = Q if v in X else set.union(gate_to_qubit[v], *(Qs[u] for u in R))
891  if len(Qs[v]) <= max_qubits_per_partition:
892  nextlevel |= g[v] & reach
893  visited.add(v)
894  else: del Qs[v]
895  level, nextlevel = nextlevel, level
896  nextlevel.clear()
897  return set(Qs)
898  #https://www.sciencedirect.com/science/article/pii/S1570866708000622
899  allparts = set()
900  for t in topo_index:
901  X, Y, Q = {t}, set(topo_order[topo_index[t]+1:]), gate_to_qubit[t]
902  Anew, Bnew = reach[t] & Y, revreach[t] & Y
903  #assert len(Bnew) == 0
904  prune = Anew - bfs_reach(X, g, rg, Anew, Q)
905  Y -= prune; Anew -= prune
906  stack = [({t}, Y, Anew, Bnew, list(sorted(Anew, key=topo_index.__getitem__)), list(sorted(Bnew, key=topo_index.__getitem__, reverse=True)), Q)]
907  while stack:
908  X, Y, A, B, As, Bs, Q = stack.pop()
909  if A:
910  v = As.pop()
911  A.remove(v)
912  R = A & revreach[v]; R.add(v)
913  elif B:
914  v = Bs.pop()
915  B.remove(v)
916  R = B & reach[v]; R.add(v)
917  else:
918  #assert all((reach[u] & revreach[v]) <= X for u in X for v in X if u != v)
919  allparts.add(frozenset(X)); continue
920  Y.remove(v)
921  stack.append((X, Y, A, B, As, Bs, Q))
922  newQ = set(Q)
923  for x in R:
924  newQ |= gate_to_qubit[x]
925  if len(newQ) > max_qubits_per_partition: break
926  else:
927  Xnew, Ynew, Anew, Bnew = X | R, Y - R, A - R, B - R
928  for x in R: Anew |= reach[x] & Ynew; Bnew |= revreach[x] & Ynew
929  prune = Anew - bfs_reach(Xnew, g, rg, Anew, newQ)
930  Ynew -= prune; Anew -= prune
931  prune = Bnew - bfs_reach(Xnew, rg, g, Bnew, newQ)
932  Ynew -= prune; Bnew -= prune
933  stack.append((Xnew, Ynew, Anew, Bnew, list(sorted(Anew, key=topo_index.__getitem__)), list(sorted(Bnew, key=topo_index.__getitem__, reverse=True)), newQ))
934  return list(allparts), g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit
935 def max_partitions(c, max_qubits_per_partition, use_ilp=True, fusion_cost=False, control_aware=False):
936  """
937  Enumerate feasible parts and select a maximum/optimal partitioning of a circuit.
938 
939  Args:
940  c: SQUANDER circuit object.
941  max_qubits_per_partition (int): Max allowed qubits per partition.
942  use_ilp (bool): If True, solve selection via ILP; else greedy largest-first.
943  fusion_cost (bool): If True, include FLOP-based cost with fusion/controls.
944  control_aware (bool): If True and fusion_cost, treat single-qubit chains as
945  pre/post relative to targets.
946 
947  Returns:
948  tuple: (partitioned_circ, param_order, parts)
949  partitioned_circ: 2-level partitioned circuit (SQUANDER object).
950  param_order (list[tuple[int,int,int]]): (source_idx, dest_idx, param_count).
951  parts (list[frozenset[int]]): Final partition assignment sets.
952  """
953  allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = get_all_partitions(c, max_qubits_per_partition)
954  if use_ilp:
955  weights = parts_to_float_ops(max_qubits_per_partition, gate_to_qubit, None, allparts) if fusion_cost and not control_aware else None
956  L, fusion_info = ilp_global_optimal(allparts, g, (single_qubit_chains, max_qubits_per_partition, go, rgo, gate_to_qubit, gate_to_tqubit) if control_aware else None, weights=weights)
957  else:
958  L, excluded = [], set()
959  weights = parts_to_float_ops(max_qubits_per_partition, gate_to_qubit, None, allparts)
960  for i in sorted(range(len(allparts)), key=lambda i: (len(allparts[i]), -weights[i]), reverse=True):
961  if len(allparts[i] & excluded) != 0: continue
962  if sol_to_badsccs(g, allparts, L + [i]): continue #skip if adding this part creates a cycle
963  excluded |= allparts[i]
964  L.append(i)
965  fusion_info = None
966  #assert sum(len(x) for x in L) == len(g), (sum(len(x) for x in L), len(g))
967  parts = recombine_single_qubit_chains(go, rgo, single_qubit_chains, gate_to_tqubit, [allparts[i] for i in L], fusion_info)
968  #assert sum(len(x) for x in parts) == len(go), (sum(len(x) for x in parts), len(go))
969  L = topo_sort_partitions(c, parts)
970  from squander.partitioning.kahn import kahn_partition_preparts
971  return kahn_partition_preparts(c, max_qubits_per_partition, [parts[i] for i in L])
972 
973 
975  """
976  Quick test harness for partitioning on sample QASM circuits.
977 
978  Args:
979  None
980 
981  Returns:
982  None
983  """
984  K = 3
985  #filename = "examples/partitioning/qasm_samples/heisenberg-16-20.qasm"
986  #filename = "benchmarks/partitioning/test_circuit/0410184_169.qasm"
987  #filename = "benchmarks/partitioning/test_circuit/ham15_107_squander.qasm"
988  #filename = "benchmarks/partitioning/test_circuit/adr4_197_qsearch.qasm"
989  filename = "benchmarks/partitioning/test_circuit/con1_216_squander.qasm"
990  #filename = "benchmarks/partitioning/test_circuit/hwb8_113.qasm"
991  #filename = "benchmarks/partitioning/test_circuit/urf1_278.qasm"
992  #filename = "benchmarks/partitioning/test_circuit/rd73_140.qasm"
993  from squander import utils
994 
995  circ, parameters = utils.qasm_to_squander_circuit(filename)
996  gate_dict = {i: gate for i, gate in enumerate(circ.get_Gates())}
997  gate_to_qubit = { i: get_qubits(g) for i, g in gate_dict.items() }
998  gate_to_tqubit = { i: g.get_Target_Qbit() for i, g in gate_dict.items() }
999 
1000  for partition in (max_partitions(circ, K, False, False), max_partitions(circ, K, False, True), max_partitions(circ, K, True, False), max_partitions(circ, K, True, True), max_partitions(circ, K, True, True, True)):
1001  print(partition[2], len(partition[2]), total_float_ops(circ.get_Qbit_Num(), K, gate_to_qubit, None, partition[2]),
1002  total_float_ops(circ.get_Qbit_Num(), K, gate_to_qubit, gate_to_tqubit, partition[2]))
1003 
1004 if __name__ == "__main__":
1005  _test_max_qasm()
def _check_gurobi_available()
Definition: ilp.py:23
def _solve_pulp_with_gurobi_or_cbc(prob, pulp, callback=None, gurobi_kwargs)
Definition: ilp.py:40
def contract_single_qubit_chains(go, rgo, gate_to_qubit, topo_order)
Definition: ilp.py:267
def max_partitions(c, max_qubits_per_partition, use_ilp=True, fusion_cost=False, control_aware=False)
Definition: ilp.py:935
def sol_to_badsccs(g, allparts, L)
Definition: ilp.py:585
def topo_sort_partitions(c, parts)
Definition: ilp.py:55
def scc_tarjan_iterative(succ)
Definition: ilp.py:351
def kahn_partition_preparts(c, max_qubit, preparts)
Definition: kahn.py:80
def two_cycles_from_dag_edges(g, gate_to_parts, allparts)
Definition: ilp.py:553
def get_qubits
Definition: tools.py:13
def find_next_biggest_partition(c, max_qubits_per_partition, prevparts=None)
Definition: ilp.py:125
def get_part_cycle_graph(g, gate_to_parts)
Definition: ilp.py:430
def all_cycles_from_dag_edges(succ, max_len=5)
Definition: ilp.py:460
def nuutila_reach_scc(succ, subg=None)
Definition: ilp.py:178
def get_all_partitions(c, max_qubits_per_partition)
Definition: ilp.py:859
def _get_topo_order(g, rg, gate_to_qubit)
Definition: ilp.py:240
def ilp_global_optimal(allparts, g, weighted_info=None, gurobi_direct=False, use_order=False, weights=None)
Definition: ilp.py:600
def ilp_max_partitions(c, max_qubits_per_partition)
Definition: ilp.py:105
def _print_gurobi_fallback_warning(exc)
Definition: ilp.py:9
def total_float_ops(num_qubit, max_qubits_per_partition, gate_to_qubit, gate_to_tqubit, allparts)
Definition: tools.py:86
def _test_max_qasm()
Definition: ilp.py:974
def recombine_single_qubit_chains(g, rg, single_qubit_chains, gate_to_tqubit, L, fusion_info, surrounded_only=False)
Definition: ilp.py:309
def build_dependency
Definition: tools.py:136
def parts_to_float_ops(num_qubit, gate_to_qubit, gate_to_tqubit, allparts)
Definition: tools.py:48