Sequential Quantum Gate Decomposer  v1.9.3
Powerful decomposition of general unitarias into one- and two-qubit gates gates
partition.py
Go to the documentation of this file.
1 from squander.gates.gates_Wrapper import CNOT, CH, CZ, CRY
2 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
3 from squander import utils
4 from itertools import dropwhile
5 from typing import List, Tuple
6 import numpy as np
7 import os
8 
9 #@brief Retrieves qubit indices used by a SQUANDER gate
10 #@param gate The SQUANDER gate
11 #@return Set of qubit indices used by the gate
12 def get_qubits(gate):
13  return ({gate.get_Target_Qbit(), gate.get_Control_Qbit()}
14  if isinstance(gate, (CH, CRY, CNOT, CZ)) else {gate.get_Target_Qbit()})
15 
16 
17 #@brief Partitions a flat circuit into subcircuits using Kahn's algorithm
18 #@param c The SQUANDER circuit to be partitioned
19 #@param max_qubit Maximum number of qubits allowed per partition
20 #@param preparts Optional prefedefined partitioning scheme
21 #@return Tuple:
22 # - Partitioned 2-level circuit
23 # - Tuples specifying new parameter positions: source_idx, dest_idx, param_count
24 # - Partition assignments
25 def kahn_partition(c, max_qubit, preparts=None):
26  top_circuit = Circuit(c.get_Qbit_Num())
27 
28  # Build dependency graphs
29  gate_dict = {i: gate for i, gate in enumerate(c.get_Gates())}
30  g, rg = { i: set() for i in gate_dict }, { i: set() for i in gate_dict }
31 
32  for gate in gate_dict:
33  for child in c.get_Children(gate_dict[gate]):
34  g[gate].add(child)
35  rg[child].add(gate)
36 
37  L, S = [], {m for m in rg if len(rg[m]) == 0}
38  param_order = []
39 
40  def partition_condition(gate):
41  return len(get_qubits(gate_dict[gate]) | curr_partition) > max_qubit
42 
43  c = Circuit(c.get_Qbit_Num())
44  curr_partition = set()
45  curr_idx = 0
46  total = 0
47  parts = [[]]
48 
49  while S:
50  if preparts is None:
51  n = next(dropwhile(partition_condition, S), None)
52  elif isinstance(next(iter(preparts[0])), tuple):
53  Scomp = {(frozenset(get_qubits(gate_dict[x])), gate_dict[x].get_Name()): x for x in S}
54  n = next(iter(Scomp.keys() & preparts[len(parts)-1]), None)
55  if n is not None: n = Scomp[n]
56  else:
57  n = next(iter(S & preparts[len(parts)-1]), None)
58  assert (n is None) == (len(preparts[len(parts)-1]) == len(parts[-1])) #sanity check valid partitioning
59 
60  if n is None: # partition cannot be expanded
61  # Add partition to circuit
62  top_circuit.add_Circuit(c)
63  total += len(c.get_Gates())
64  # Reset for next partition
65  curr_partition = set()
66  c = Circuit(c.get_Qbit_Num())
67  parts.append([])
68  if preparts is None: n = next(iter(S))
69  elif isinstance(next(iter(preparts[0])), tuple):
70  Scomp = {(frozenset(get_qubits(gate_dict[x])), gate_dict[x].get_Name()): x for x in S}
71  n = next(iter(Scomp.keys() & preparts[len(parts)-1]), None)
72  if n is not None: n = Scomp[n]
73  else: n = next(iter(S & preparts[len(parts)-1]))
74 
75 
76  # Add gate to current partition
77  parts[-1].append(n)
78  curr_partition |= get_qubits(gate_dict[n])
79  c.add_Gate(gate_dict[n])
80  param_order.append((
81  gate_dict[n].get_Parameter_Start_Index(),
82  curr_idx,
83  gate_dict[n].get_Parameter_Num()
84  ))
85  curr_idx += gate_dict[n].get_Parameter_Num()
86 
87  # Update dependencies
88  L.append(n)
89  S.remove(n)
90  assert len(rg[n]) == 0
91  for child in set(g[n]):
92  g[n].remove(child)
93  rg[child].remove(n)
94  if not rg[child]:
95  S.add(child)
96 
97  # Add the last partition
98  top_circuit.add_Circuit(c)
99  total += len(c.get_Gates())
100  assert total == len(gate_dict)
101  print(parts)
102  return top_circuit, param_order, parts
103 
104 
105 #@brief Finds the next biggest optimal partition using ILP
106 #@param c The SQUANDER circuit to be partitioned
107 #@param max_qubits_per_partition Maximum qubits allowed per partition
108 #@param prevparts Previously determined partitions to exclude
109 #@return gates Set of gate indices forming next biggest partition
110 def find_next_biggest_partition(c, max_qubits_per_partition, prevparts=None):
111  import pulp
112  gatedict = {i: gate for i, gate in enumerate(c.get_Gates())}
113  all_qubits = set(range(c.get_Qbit_Num()))
114 
115  num_gates = len(gatedict)
116  prob = pulp.LpProblem("MaxSinglePartition", pulp.LpMinimize)
117  a = pulp.LpVariable.dicts("a", (i for i in range(num_gates)), cat="Binary") #is gate i in previous partition to p
118  x = pulp.LpVariable.dicts("x", (i for i in range(num_gates)), cat="Binary") #is gate i in partition p
119  z = pulp.LpVariable.dicts("z", (q for q in all_qubits), cat="Binary") #is qubit q in paritition p
120  # Constraint 1: not gate is in both a and x
121  for i in gatedict:
122  prob += x[i] + a[i] <= 1
123  # Constraint 2: if gate g_i uses qubit q, then z_{q} = 1
124  for i in gatedict:
125  gate_qubits = get_qubits(gatedict[i])
126  for q in gate_qubits:
127  prob += x[i] <= z[q]
128  # Constraint 3: the partition uses at most k qubits
129  prob += pulp.lpSum(z[q] for q in all_qubits) <= max_qubits_per_partition
130  # Constraint 4: previous partitions are filtered out, and are none or all included if in pre-partition
131  for part in prevparts:
132  for i in part:
133  prob += x[i] == 0
134  prob += a[i] == a[next(iter(part))]
135  # Constraint 5: ordering constraints for dependencies
136  for j in gatedict:
137  for i in c.get_Children(gatedict[j]): #there exists a topological ordering of all the gates enabled in x
138  prob += a[j] >= a[i]
139  prob += x[j] + a[j] >= x[i]
140  prob.setObjective(-pulp.lpSum(x[i] for i in range(num_gates)))
141  #from gurobilic import get_gurobi_options
142  #prob.solve(pulp.GUROBI(manageEnv=True, msg=False, envOptions=get_gurobi_options()))
143  prob.solve(pulp.GUROBI(manageEnv=True, msg=False, timeLimit=180, Threads=os.cpu_count()))
144  #prob.solve(pulp.PULP_CBC_CMD(msg=False))
145  gates = {i for i in range(num_gates) if int(pulp.value(x[i]))}
146  qubits = set.union(*(get_qubits(gatedict[i]) for i in gates))
147  #print(f"Status: {pulp.LpStatus[prob.status]} Found partition with {len(gates)} gates: {gates} and {len(qubits)} qubits: {qubits}")
148  return gates
149 
150 
151 #@brief Partitions a circuit using ILP to maximize gates per partition
152 #@param c The SQUANDER circuit to be partitioned
153 #@param max_qubits_per_partition Maximum qubits allowed per partition
154 #@return Tuple:
155 # - Partitioned 2-level circuit
156 # - Tuples specifying new parameter positions: source_idx, dest_idx, param_count
157 # - Partition assignments
158 def ilp_max_partitions(c, max_qubits_per_partition):
159  gatedict = {i: gate for i, gate in enumerate(c.get_Gates())}
160  num_gates = len(gatedict)
161  parts = []
162  while sum(len(x) for x in parts) != num_gates:
163  parts.append(find_next_biggest_partition(c, max_qubits_per_partition, parts))
164  gatedict = {i: gate for i, gate in enumerate(c.get_Gates())}
165  partdict = {gate: i for i, part in enumerate(parts) for gate in part}
166  g, rg = {i: set() for i in range(len(parts))}, {i: set() for i in range(len(parts))}
167  for gate in gatedict:
168  for child in c.get_Children(gatedict[gate]):
169  if partdict[gate] == partdict[child]: continue
170  g[partdict[gate]].add(partdict[child])
171  rg[partdict[child]].add(partdict[gate])
172  L, S = [], {m for m in rg if len(rg[m]) == 0}
173  while len(S) != 0:
174  n = S.pop()
175  L.append(parts[n])
176  assert len(rg[n]) == 0
177  for m in set(g[n]):
178  g[n].remove(m)
179  rg[m].remove(n)
180  if len(rg[m]) == 0:
181  S.add(m)
182  assert len(L) == len(parts)
183  return kahn_partition(c, max_qubits_per_partition, L)
184 
185 
186 
187 def translate_param_order(params: np.ndarray, param_order: List[Tuple[int,int]]) -> np.ndarray:
188  """
189  Call to reorder circuit parameters based on partitioned execution order
190 
191  Args:
192 
193  params ( np.ndarray ) Original parameter array
194 
195  param_order (List[int] ) Tuples specifying new parameter positions: source_idx, dest_idx, param_count
196 
197  Return:
198 
199  Returns with the reordered Reordered parameter array
200  """
201 
202  reordered = np.empty_like(params)
203 
204  for s_idx, n_idx, n_params in param_order:
205  reordered[n_idx:n_idx + n_params] = params[s_idx:s_idx + n_params]
206 
207  return reordered
208 
209 
210 
211 def PartitionCircuit( circ: Circuit, parameters: np.ndarray, max_partition_size: int, use_ilp : bool = False) -> tuple[Circuit, np.ndarray]:
212  """
213  Call to partition a circuit
214 
215  Args:
216 
217  circ ( Circuit ) A circuit to be partitioned
218 
219  parameters ( np.ndarray ) A parameter array associated with the input circuit
220 
221  max_partition_size (int) : The maximal number of qubits in the partitions
222 
223  use_ilp (bool, optional) Set True to use ILP partitioning startegy (slow, but giving optimal result), or False (default) to use Kahn topological sorting
224 
225  Return:
226 
227  Returns with the paritioned circuit and the associated parameter array. Partitions are organized into subcircuits of the resulting circuit
228  """
229 
230  if use_ilp:
231  partitioned_circ, param_order, _ = ilp_max_partitions(circ, max_partition_size)
232 
233  else:
234  partitioned_circ, param_order, _ = kahn_partition(circ, max_partition_size)
235 
236 
237  param_reordered = translate_param_order(parameters, param_order)
238 
239  return partitioned_circ, param_reordered
240 
241 
242 def PartitionCircuitQasm(filename: str, max_partition_size: int, use_ilp : bool = False) -> tuple[Circuit, np.ndarray]:
243  """
244  Call to partition a circuit loaded from a qasm file
245 
246  Args:
247 
248  filename ( str ) A path to a qasm file to be imported
249 
250  max_partition_size (int) : The maximal number of qubits in the partitions
251 
252  use_ilp (bool, optional) Set True to use ILP partitioning startegy (slow, but giving optimal result), or False (default) otherwise
253 
254  Return:
255 
256  Returns with the paritioned circuit and the associated parameter array. Partitions are organized into subcircuits of the resulting circuit
257  """
258 
259  circ, parameters = utils.qasm_to_squander_circuit(filename)
260  return PartitionCircuit( circ, parameters, max_partition_size, use_ilp )
261 
262 
263 if __name__ == "__main__":
264  PartitionCircuitQasm("examples/partitioning/qasm_samples/heisenberg-16-20.qasm", 4, True)
def get_Parameter_Start_Index(self)
Definition: qgd_Circuit.py:405
def translate_param_order
Definition: partition.py:187
def find_next_biggest_partition(c, max_qubits_per_partition, prevparts=None)
Definition: partition.py:110
def ilp_max_partitions(c, max_qubits_per_partition)
Definition: partition.py:158
def PartitionCircuitQasm
Definition: partition.py:242
def PartitionCircuit
Definition: partition.py:211
def get_qubits(gate)
Definition: partition.py:12
def get_Parameter_Num(self)
Call to get the number of free parameters in the gate structure used for the decomposition.
def kahn_partition(c, max_qubit, preparts=None)
Definition: partition.py:25