From 39095120be48f3c228f39f27ae53331ca3f96e8f Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Thu, 4 Jan 2024 13:19:06 -0600 Subject: [PATCH 1/8] Set up bare bones cut finding tutorial --- .../01_gate_cutting_to_reduce_circuit_width.ipynb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb b/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb index ff85dd277..a65497c39 100644 --- a/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb +++ b/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb @@ -58,7 +58,7 @@ "source": [ "### Specify some observables\n", "\n", - "Currently, only `Pauli` observables with phase equal to 1 are supported. Full support for `SparsePauliOp` is expected in CKT v0.5.0." + "Currently, only `Pauli` observables with phase equal to 1 are supported. Full support for `SparsePauliOp` is expected in CKT v0.6.0." ] }, { @@ -307,9 +307,7 @@ "source": [ "### Reconstruct the expectation values\n", "\n", - "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n", - "\n", - "Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now." + "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit." ] }, { @@ -321,10 +319,6 @@ "source": [ "from circuit_knitting.cutting import reconstruct_expectation_values\n", "\n", - "for label, circuits in subexperiments.items():\n", - " for i, circuit in enumerate(circuits):\n", - " results[label].metadata[i][\"num_qpd_bits\"] = len(circuit.cregs[0])\n", - "\n", "reconstructed_expvals = reconstruct_expectation_values(\n", " results,\n", " coefficients,\n", From 652dc2d469b2b697087779de8b35083a6d2aa234 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Thu, 4 Jan 2024 13:19:46 -0600 Subject: [PATCH 2/8] Set up bare bones gate cut finding tutorial --- .../LO_gate_cut_optimizer/__init__.py | 1 + .../best_first_search.py | 356 +++++++++++++++ .../circuit_interface.py | 406 +++++++++++++++++ .../LO_gate_cut_optimizer/cut_optimization.py | 253 +++++++++++ .../LO_gate_cut_optimizer/cutting_actions.py | 323 ++++++++++++++ .../disjoint_subcircuits_state.py | 408 ++++++++++++++++++ .../lo_cuts_only_optimizer.py | 174 ++++++++ .../optimization_settings.py | 122 ++++++ .../quantum_device_constraints.py | 36 ++ .../search_space_generator.py | 224 ++++++++++ .../LO_gate_cut_optimizer/utils.py | 123 ++++++ .../cutting/cut_finding/__init__.py | 0 .../cut_finding/notebooks/Demo_notebook.ipynb | 374 ++++++++++++++++ .../cutting/cut_finding/test/__init__.py | 1 + .../test/test_circuit_interface.py | 62 +++ .../test/test_disjoint_subcircuits_state.py | 95 ++++ .../test/test_optimization_settings.py | 21 + .../test/test_quantum_device_constraints.py | 10 + .../cutting/cut_finding/test/test_utils.py | 46 ++ .../tutorials/LO_gate_cut_finder.ipynb | 156 +++++++ docs/circuit_cutting/tutorials/bell.qpy | Bin 0 -> 142 bytes 21 files changed, 3191 insertions(+) create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/__init__.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/best_first_search.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/circuit_interface.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cutting_actions.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/disjoint_subcircuits_state.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/lo_cuts_only_optimizer.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/optimization_settings.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/quantum_device_constraints.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/search_space_generator.py create mode 100644 circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/utils.py create mode 100644 circuit_knitting/cutting/cut_finding/__init__.py create mode 100644 circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb create mode 100644 circuit_knitting/cutting/cut_finding/test/__init__.py create mode 100644 circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py create mode 100644 circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py create mode 100644 circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py create mode 100644 circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py create mode 100644 circuit_knitting/cutting/cut_finding/test/test_utils.py create mode 100644 docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb create mode 100644 docs/circuit_cutting/tutorials/bell.qpy diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/__init__.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/__init__.py @@ -0,0 +1 @@ + diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/best_first_search.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/best_first_search.py new file mode 100644 index 000000000..b44415d91 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/best_first_search.py @@ -0,0 +1,356 @@ +"""File containing the classes required to implement Dijkstra's (best-first) search algorithm.""" +import heapq +import numpy as np +from itertools import count + + +class BestFirstPriorityQueue: + """Class that implements priority queues for best-first search. + + The tuples that are pushed onto the priority queues have the form: + + (, , , , ) + + (numeric or tuple) is a numeric cost or tuple of numeric + lexically-ordered costs that are to be minimized. + + (int) is the negative of the search depth of the + search state represented by the tuple. Thus, if several search states + have identical costs, priority is given to the deepest states to + encourage depth-first behavior. + + is a pseudo-random number that randomly break ties in a + stable manner if several search states have identical costs at identical + search depths. + + is a sequential count of push operations that is used to + break further ties just in case two states with the same costs and + depths are somehow assigned the same pseudo-random numbers. + + is a state object generated by the optimization process. + Because of the design of the tuple entries that precede it, state objects + never get evaluated in the heap-managment comparisons that are performed + internally by the priority-queue implementation. + + Member Variables: + + rand_gen is a Numpy random number generator. + + unique is a Python sequence counter. + + pqueue is a Python priority queue (currently heapq, with plans to move to + queue.PriorityQueue if parallelization is ultimately required). + """ + + def __init__(self, rand_seed): + """A BestFirstPriorityQueue object must be initialized with a specification + of a random seed (int) for the pseudo-random number generator. + If None is used as the random seed, then a seed is + obtained using an operating-system call to achieve a randomized + initialization. + """ + self.rand_gen = np.random.default_rng(rand_seed) + self.unique = count() + self.pqueue = list() # queue.PriorityQueue() + + def put(self, state, depth, cost): + """Push state onto the priority queue. The search depth and cost + of the state must also be provided as input. + """ + + heapq.heappush( + self.pqueue, + (cost, (-depth), self.rand_gen.random(), next(self.unique), state), + ) + + def get(self): + """Pop and return the lowest cost state currently on the + queue, along with the search depth of that state and its cost. + None, None, None is returned if the priority queue is empty. + """ + if self.qsize() == 0: + return None, None, None + + best = heapq.heappop(self.pqueue) # self.pqueue.get() + + return best[-1], (-best[1]), best[0] + + def qsize(self): + """Return the size of the priority queue.""" + return len(self.pqueue) # self.pqueue.qsize() + + def clear(self): + """Clear all entries in the priority queue.""" + self.pqueue.clear() + + +class BestFirstSearch: + + """Class that implements best-first search. The search proceeds by + choosing the deepest, lowest-cost state in the search frontier and + generating next states. Successive calls to the optimizationPass() + method will resume the search at the next deepest, lowest-cost state + in the search frontier. The costs of goal states that are returned + are used to constrain subsequent searches. None is returned if no + (additional) feasible solutions can be found, or when no (additional) + solutions can be found without exceeding the lowest upper-bound cost + across the goal states previously returned. + + Member Variables: + + rand_seed (int) is the seed to use when initializing Numpy random number + generators in the bounded best-first priority-queue objects. + + cost_func (lambda state, *args) is a function that computes cost values + from search states. Input arguments to the optimizationPass() method are + also passed to the cost_func. The cost returned can be numeric or tuples + of numerics. In the latter case, lexicographical comparisons are + performed per Python semantics. + + next_state_func (lambda state, *args) is a function that returns a list + of next states generated from the input state. Input arguments to the + optimizationPass() method are also passed to the next_state_func. + + goal_state_func (lambda state, *args) is a function that returns True if + the input state is a solution state of the search. Input arguments to the + optimizationPass() method are also passed to the goal_state_func. + + upperbound_cost_func (lambda goal_state, *args) can either be None or a + function that returns an upper bound to the optimal cost given a goal_state + as input. The upper bound is used to prune next-states from the search in + subsequent calls to the optimizationPass() method. If upperbound_cost_func + is None, the cost of the goal_state as determined by cost_func is used as + an upper bound to the optimal cost. Input arguments to the + optimizationPass() method are also passed to the upperbound_cost_func. + + mincost_bound_func (lambda *args) can either be None or a function that + returns a cost bound that is compared to the minimum cost across all + vertices in a search frontier. If the minimum cost exceeds the min-cost + bound, the search is terminated even if a goal state has not yet been found. + Returning None is equivalent to returning an infinite min-cost bound. A + mincost_bound_func that is None is likewise equivalent to an infinite + min-cost bound. + + stop_at_first_min (Boolean) is a flag that indicates whether or not to + stop the search after the first minimum-cost goal state has been reached. + + max_backjumps (int or None) is the maximum number of backjump operations that + can be performed before the search is forced to terminate. None indicates + that no restriction is placed in the number of backjump operations. + + pqueue (BestFirstPriorityQueue) is a best-first priority-queue object. + + upperbound_cost (numeric or tuple) is the cost bound obtained by applying + the upperbound_cost_func to the goal states that are encountered. + + mincost_bound (numeric or tuple) is the cost bound imposed on the minimum + cost across all vertices in the search frontier. The search is forced to + terminate when the minimum cost exceeds this cost bound. + + minimum_reached (Boolean) is a flag that indicates whether or not the + first minimum-cost goal state has been reached. + + num_states_visited (int) is the number of states that have been dequeued + and processed in the search. + + num_next_states (int) is the number of next-states generated from the + states visited. + + num_enqueues (int) is the number of next-states pushed onto the search + priority queue after cost pruning. + + num_backjumps (int) is the number of times a backjump operation is + performed. In the case of best-first search, a backjump occurs when the + depth of the lowest-cost state in the search frontier is less than or + equal to the depth of the previous lowest-cost state. + """ + + def __init__( + self, optimization_settings, search_functions, stop_at_first_min=False + ): + """A BestFirstSearch object must be initialized with a list of + initial states, a random seed for the numpy pseudo-random number + generators that are used to break ties, together with an object + that holds the various functions that are used by the search + engine to generate and explore the search space. A Boolean flag + can optionally be provided to indicate whether to stop the search + after the first minimum-cost goal state has been reached (True), + or whether subsequent calls to the optimizationPass() method should + return any additional minimum-cost goal states that might exist + (False). The default is not to stop at the first minimum. A limit + on the maximum number of backjumps can also be optionally provided + to terminate the search if the number of backjumps exceeds the + specified limit without finding the (next) optimal goal state. + """ + + self.rand_seed = optimization_settings.getRandSeed() + self.cost_func = search_functions.cost_func + self.next_state_func = search_functions.next_state_func + self.goal_state_func = search_functions.goal_state_func + self.upperbound_cost_func = search_functions.upperbound_cost_func + self.mincost_bound_func = search_functions.mincost_bound_func + + self.stop_at_first_min = stop_at_first_min + self.max_backjumps = optimization_settings.getMaxBackJumps() + + self.pqueue = BestFirstPriorityQueue(self.rand_seed) + + self.upperbound_cost = None + self.mincost_bound = None + self.minimum_reached = False + self.num_states_visited = 0 + self.num_next_states = 0 + self.num_enqueues = 0 + self.num_backjumps = 0 + self.penultimate_stats = None + + def initialize(self, initial_state_list, *args): + self.pqueue.clear() + + self.upperbound_cost = None + self.mincost_bound = None + self.minimum_reached = False + self.num_states_visited = 0 + self.num_next_states = 0 + self.num_enqueues = 0 + self.num_backjumps = 0 + self.penultimate_stats = self.getStats() + + self.put(initial_state_list, 0, args) + + def optimizationPass(self, *args): + """Perform best-first search until either a goal state is found and + returned, or cost-bounds are reached or no further goal states can be + found, in which case None is returned. The cost of the returned state + is also returned. Any input arguments to optimizationPass() are passed + along to the search-space functions employed. + """ + + if self.mincost_bound_func is not None: + self.mincost_bound = self.mincost_bound_func(*args) + + prev_depth = None + + while ( + self.pqueue.qsize() > 0 + and (not self.stop_at_first_min or not self.minimum_reached) + and (self.max_backjumps is None or self.num_backjumps < self.max_backjumps) + ): + state, depth, cost = self.pqueue.get() + + self.updateMinimumReached(cost) + + if cost is None or self.costBoundsExceeded(cost, args): + return None, None + + self.num_states_visited += 1 + + if prev_depth is not None and depth <= prev_depth: + self.num_backjumps += 1 + + prev_depth = depth + + if self.goal_state_func(state, *args): + self.penultimate_stats = self.getStats() + self.updateUpperBoundGoalState(state, *args) + self.updateMinimumReached(cost) + + return state, cost + + next_state_list = self.next_state_func(state, *args) + self.put(next_state_list, depth + 1, args) + + # If all states have been explored, then the minimum has been reached + if self.pqueue.qsize() == 0: + self.minimum_reached = True + + return None, None + + def minimumReached(self): + """Return True if the optimization reached a global minimum.""" + + return self.minimum_reached + + def getStats(self, penultimate=False): + """Return a Numpy array containing the number of states visited + (dequeued), the number of next-states generated, the number of + next-states that are enqueued after cost pruning, and the number + of backjumps performed. Numpy arrays are employed to facilitate + the aggregation of search statisitcs. + """ + + if penultimate: + return self.penultimate_stats + + return np.array( + ( + self.num_states_visited, + self.num_next_states, + self.num_enqueues, + self.num_backjumps, + ), + dtype=int, + ) + + def getUpperBoundCost(self): + """Return the current upperbound cost""" + + return self.upperbound_cost + + def updateUpperBoundCost(self, cost_bound): + """Update the cost upper bound based on an + input cost bound. + """ + + if cost_bound is not None and ( + self.upperbound_cost is None or cost_bound < self.upperbound_cost + ): + self.upperbound_cost = cost_bound + + def updateUpperBoundGoalState(self, goal_state, *args): + """Update the cost upper bound based on a + goal state reached in the search. + """ + + if self.upperbound_cost_func is not None: + bound = self.upperbound_cost_func(goal_state, *args) + else: + bound = self.cost_func(goal_state, *args) + + if self.upperbound_cost is None or bound < self.upperbound_cost: + self.upperbound_cost = bound + + def put(self, state_list, depth, args): + """Push a list of (next) states onto the + best-first priority queue. + """ + + self.num_next_states += len(state_list) + + for state in state_list: + cost = self.cost_func(state, *args) + + if self.upperbound_cost is None or cost <= self.upperbound_cost: + self.pqueue.put(state, depth, cost) + self.num_enqueues += 1 + + def updateMinimumReached(self, min_cost): + """Update the minimum_reached flag indicating + that a global optimum has been reached. + """ + if min_cost is None or ( + self.upperbound_cost is not None and self.upperbound_cost <= min_cost + ): + self.minimum_reached = True + + return self.minimum_reached + + def costBoundsExceeded(self, cost, args): + """Return True if any cost bounds + have been exceeded. + """ + + return cost is not None and ( + (self.mincost_bound is not None and cost > self.mincost_bound) + or (self.upperbound_cost is not None and cost > self.upperbound_cost) + ) diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/circuit_interface.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/circuit_interface.py new file mode 100644 index 000000000..a594ef684 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/circuit_interface.py @@ -0,0 +1,406 @@ +"""File containing the classes required to represent quantum circuits in a format native to the circuit cutting optimizer.""" +import copy +import string +import numpy as np +from abc import ABC, abstractmethod + + +class CircuitInterface(ABC): + + """Base class for accessing and manipulating external circuit + representations, and for converting external circuit representations + to the internal representation used by the circuit cutting optimization code. + + Derived classes must override the default implementations of the abstract + methods defined in this base class. + """ + + @abstractmethod + def getNumQubits(self): + """Derived classes must override this function and return the number + of qubits in the input circuit. + """ + + assert False, "Derived classes must override getNumQubits()" + + @abstractmethod + def getMultiQubitGates(self): + """Derived classes must override this function and return a list that + specifies the multiqubit gates in the input circuit. + + The returned list is of the form: + [ ... [ ] ...] + + The can be any object that uniquely identifies the gate + in the circuit. The can be used as an argument in other + member functions implemented by the derived class to replace the gate + with the decomposition determined by the optimizer. + + The must of the form + (, , ..., ) + + The must be a hashable identifier that can be used to + look up cutting rules for the specified gate. Gate names are typically + the Qiskit names of the gates. + + The must be a non-negative integer with qubits numbered + starting with zero. Derived classes are responsible for constructing the + mappings from external qubit identifiers to the corresponding qubit IDs. + + The can be of the form + None + [] + [None] + [, ..., ] + + A cut constraint of None indicates that no constraints are placed + on how or whether cuts can be performed. An empty list [] or the + list [None] indicates that no cuts are to be performed and the gate + is to be applied without cutting. A list of cut types of the form + [ ... ] indicates precisely which types of + cuts can be considered. In this case, the cut type None must be + explicitly included to indicate the possibilty of not cutting, if + not cutting is to be considered. In the current version of the code, + the allowed cut types are 'None', 'GateCut', 'WireCut', and 'AbsorbGate'. + """ + + assert False, "Derived classes must override getMultiQubitGates()" + + @abstractmethod + def insertGateCut(self, gate_ID, cut_type): + """Derived classes must override this function and mark the specified + gate as being cut. In this release, the cut type can be only be "LO". + Other cut types, including "LOCC" will be added in future releases. + """ + + assert False, "Derived classes must override insertGateCut()" + + @abstractmethod + def defineSubcircuits(self, list_of_list_of_wires): + """Derived classes must override this function. The input is a + list of subcircuits where each subcircuit is specified as a + list of wire IDs. + """ + + assert False, "Derived classes must override defineSubcircuits()" + + +class SimpleGateList(CircuitInterface): + + """Derived class that converts a simple list of gates into + the form needed by the circuit-cutting optimizer code. + + Elements of the list must be of the form: + 'barrier' + ('barrier' ) + ( ... ) + + Qubit names can be any hashable objects. Gate names can also be any + hashable objects, but they must be consistent with the names used by the + optimizer to look up cutting rules for the specified gates. + + The constructor can be supplied with a list of qubit names to force a + preferred ordering in the assignment of numeric qubit IDs to each name. + + Member Variables: + + qubit_names (NameToIDMap) is an object that maps qubit names to + numerical qubit IDs. + + num_qubits (int) is the number of qubits in the input circuit. Qubit IDs + whose values are greater than or equal to num_qubits represent qubits + that were introduced as the result of wire cutting. These qubits are + assigned generated names of the form ('cut', ) in the + qubit_names object, where is the name of the wire/qubit + that was cut to create the new wire/qubit. + + circuit (list) is the internal representation of the circuit, which is + a list of the following form: + + [ ... [, None] ...] + + where the qubit names have been replaced with qubit IDs in the gate + specifications. + + new_circuit (list) is a list of gate specifications that define + the cut circuit. As with circuit, qubit IDs are used to identify + wires/qubits. + + cut_type (list) is a list that assigns cut-type annotations to gates + in new_circuit to indicate which quasiprobability decomposition to + use for the corresponding gate/wire cut. + + new_gate_ID_map (list) is a list that maps the positions of gates + in circuit to their new positions in new_circuit. + + output_wires (list) maps qubit IDs in circuit to the corresponding + output wires of new_circuit so that observables defined for circuit + can be remapped to new_circuit. + + subcircuits (list) is a list of list of wire IDs, where each list of + wire IDs defines a subcircuit. + """ + + def __init__(self, input_circuit, init_qubit_names=[]): + self.qubit_names = NameToIDMap(init_qubit_names) + + self.circuit = list() + self.new_circuit = list() + self.cut_type = list() + + for gate in input_circuit: + self.cut_type.append(None) + if not isinstance(gate, list) and not isinstance(gate, tuple): + self.circuit.append([copy.deepcopy(gate), None]) + self.new_circuit.append(copy.deepcopy(gate)) + + else: + gate_spec = [gate[0]] + [self.qubit_names.getID(x) for x in gate[1:]] + self.circuit.append([copy.deepcopy(gate_spec), None]) + self.new_circuit.append(copy.deepcopy(gate_spec)) + + self.new_gate_ID_map = np.arange(len(self.circuit), dtype=int) + self.num_qubits = self.qubit_names.getArraySizeNeeded() + self.output_wires = np.arange(self.num_qubits, dtype=int) + + # Initialize the list of subcircuits assuming no cutting + self.subcircuits = list(list(range(self.num_qubits))) + + def getNumQubits(self): + """Return the number of qubits in the input circuit""" + + return self.num_qubits + + def getNumWires(self): + """Return the number of wires/qubits in the cut circuit""" + + return self.qubit_names.getNumItems() + + def getMultiQubitGates(self): + """Extract the multiqubit gates from the circuit and prepends the + index of the gate in the circuits to the gate specification. + + The elements of the resulting list therefore have the form + [ ] + + The and have the forms + described above. + + The is the list index of the corresponding element in + self.circuit + """ + + subcircuit = list() + for k, gate in enumerate(self.circuit): + if isinstance(gate[0], list): + if len(gate[0]) > 2 and gate[0][0] != "barrier": + subcircuit.append([k] + gate) + + return subcircuit + + def insertGateCut(self, gate_ID, cut_type): + """Mark the specified gate as being cut. In this release, the cut + type can be only be "LO". Other cut types, including "LOCC" will + be added in future releases. + """ + + gate_pos = self.new_gate_ID_map[gate_ID] + self.cut_type[gate_pos] = cut_type + + def exportCutCircuit(self, name_mapping="default"): + """Return a list of gates representing the cut circuit. If None + is provided as the name_mapping, then the original qubit names are + used with additional names of the form ("cut", ) introduced as + needed to represent cut wires. If "default" is used as the mapping + then the defaultWireNameMapping() method defines the name mapping. + Otherwise, the name_mapping is assumed to be a dictionary that maps + internal wire names to desired names. + """ + + wire_map = self.makeWireMapping(name_mapping) + out = copy.deepcopy(self.new_circuit) + + self.replaceWireIDs(out, wire_map) + + return out + + def defineSubcircuits(self, list_of_list_of_wires): + """The input is a list of subcircuits where each subcircuit is + specified as a list of wire IDs. + """ + + self.subcircuits = list_of_list_of_wires + + def getWireNames(self): + """Return a list of the internal wire names used in the circuit, + which consists of the original qubit names together with additional + names of form ("cut", ) introduced to represent cut wires. + """ + + return list(self.qubit_names.getItems()) + + def exportSubcircuitsAsString(self, name_mapping="default"): + """Return a string that maps qubits/wires in the output circuit + to subcircuits per the Circuit Knitting Toolbox convention. This + method only works with mappings to numeric qubit/wire names, such + as provided by "default" or a custom name_mapping.""" + + wire_map = self.makeWireMapping(name_mapping) + + out = list(range(self.getNumWires())) + alphabet = string.ascii_uppercase + string.ascii_lowercase + + for k, subcircuit in enumerate(self.subcircuits): + for wire in subcircuit: + out[wire_map[wire]] = alphabet[k] + + return "".join(out) + + def makeWireMapping(self, name_mapping): + """Return a wire-mapping array given an input specification of a + name mapping. If None is provided as the input name_mapping, then + the original qubit names are mapped to themselves. If "default" + is used as the name_mapping, then the defaultWireNameMapping() + method is used to define the name mapping. Otherwise, name_mapping + itself is assumed to be the dictionary to use. + """ + + if name_mapping is None: + name_mapping = dict() + for name in self.getWireNames(): + name_mapping[name] = name + + elif name_mapping == "default": + name_mapping = self.defaultWireNameMapping() + + wire_mapping = [None for x in range(self.qubit_names.getArraySizeNeeded())] + + for k in self.qubit_names.getIDs(): + wire_mapping[k] = name_mapping[self.qubit_names.getName(k)] + + return wire_mapping + + def defaultWireNameMapping(self): + """Return a dictionary that maps wire names in self.qubit_names to + default numeric output qubit names when exporting a cut circuit. Any + cut wires are assigned numeric names that are adjacent to the numeric + name of the wire prior to cutting so that Move operators are then + applied between adjacent qubits. + """ + + name_pairs = [(name, self.sortOrder(name)) for name in self.getWireNames()] + + name_pairs.sort(key=lambda x: x[1]) + + name_map = dict() + for k, pair in enumerate(name_pairs): + name_map[pair[0]] = k + + return name_map + + def sortOrder(self, name): + """Reorder wires, using the heuristic defined below, so that the two sides + of a wire cut are adjacent in the exported circuit. Only acts non-trivially + in the presence of wire cuts. + """ + + if isinstance(name, tuple): + if name[0] == "cut": + x = self.sortOrder(name[1]) + x_int = int(x) + x_frac = x - x_int + return x_int + 0.5 * x_frac + 0.5 + + return self.qubit_names.getID(name) + + def replaceWireIDs(self, gate_list, wire_map): + """Iterate through a list of gates and replaces wire IDs with the + values defined by the wire_map. + """ + + for gate in gate_list: + for k in range(1, len(gate)): + gate[k] = wire_map[gate[k]] + + +class NameToIDMap: + + """Class used to map hashable items (e.g., qubit names) to natural numbers + (e.g., qubit IDs)""" + + def __init__(self, init_names=[]): + """Allow the name dictionary to be initialized with the names + in init_names in the order the names appear in order to force a + preferred ordering in the assigment of item IDs to those names. + """ + + self.next_ID = 0 + self.item_dict = dict() + self.ID_dict = dict() + + for name in init_names: + self.getID(name) + + def getID(self, item_name): + """Return the numeric ID associated with the specified hashable item. + If the hashable item does not yet appear in the item dictionary, a new + item ID is assigned. + """ + + if item_name not in self.item_dict: + while self.next_ID in self.ID_dict: + self.next_ID += 1 + + self.item_dict[item_name] = self.next_ID + self.ID_dict[self.next_ID] = item_name + self.next_ID += 1 + + return self.item_dict[item_name] + + def defineID(self, item_ID, item_name): + """Assign a specific ID number to an item name.""" + + assert item_ID not in self.ID_dict, f"item ID {item_ID} already assigned" + assert ( + item_name not in self.item_dict + ), f"item name {item_name} already assigned" + + self.item_dict[item_name] = item_ID + self.ID_dict[item_ID] = item_name + + def getName(self, item_ID): + """Return the name associated with the specified item ID. + None is returned if item_ID does not (yet) exist. + """ + + if item_ID not in self.ID_dict: + return None + + return self.ID_dict[item_ID] + + def getNumItems(self): + """Return the number of hashable items loaded thus far.""" + + return len(self.item_dict) + + def getArraySizeNeeded(self): + """Return one plus the maximum item ID assigned thus far, + or zero if no items have been assigned. The value returned + is thus the minimum size needed to construct a Python/Numpy + array that maps item IDs to other values. + """ + + if self.getNumItems() == 0: + return 0 + + return 1 + max(self.ID_dict.keys()) + + def getItems(self): + """Return an iterator over the hashable items loaded thus far.""" + + return self.item_dict.keys() + + def getIDs(self): + """Return an iterator over the hashable items loaded thus far.""" + + return self.ID_dict.keys() diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py new file mode 100644 index 000000000..51257e2fd --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py @@ -0,0 +1,253 @@ +""" File containing the classes required to search for optimal cut locations.""" +import numpy as np +from .cutting_actions import disjoint_subcircuit_actions +from .utils import selectSearchEngine, greedyBestFirstSearch +from .disjoint_subcircuits_state import DisjointSubcircuitsState +from .search_space_generator import ( + getActionSubset, + SearchFunctions, + SearchSpaceGenerator, +) + + +class CutOptimizationFuncArgs: + + """Class for passing relevant arguments to the CutOptimization + search-space generating functions. + """ + + def __init__(self): + self.entangling_gates = None + self.search_actions = None + self.max_gamma = None + self.qpu_width = None + self.greedy_multiplier = None + + +def CutOptimizationCostFunc(state, func_args): + """Return the cost function whose goal is to minimize the gamma + lower bound while giving preference to circuit partitions + that balance the width of the resulting partitions. + """ + return (state.lowerBoundGamma(), state.getMaxWidth()) + + +def CutOptimizationStratumFunc(state, func_args): + """Return stratum function for stratified beam search. + """ + return int(np.log2(state.lowerBoundGamma())) + + +def CutOptimizationUpperBoundCostFunc(goal_state, func_args): + """Return the gamma upper bound.""" + return (goal_state.upperBoundGamma(), np.inf) + + +def CutOptimizationMinCostBoundFunc(func_args): + """Return an a priori min-cost bound defined in the optimization settings.""" + if func_args.max_gamma is None: + return None + + return (func_args.max_gamma, np.inf) + + +def CutOptimizationNextStateFunc(state, func_args): + """Generate a list of next states from the input state.""" + + # Get the entangling gate spec that is to be processed next based + # on the search level of the input state + gate_spec = func_args.entangling_gates[state.getSearchLevel()] + + # Determine which search actions can be performed, taking into + # account any user-specified constraints that might have been + # placed on how the current entangling gate is to be handled + # in the search + if len(gate_spec[1]) == 3: + action_list = func_args.search_actions.getGroup("TwoQubitGates") + else: + action_list = func_args.search_actions.getGroup("MultiqubitGates") + + action_list = getActionSubset(action_list, gate_spec[2]) + + # Apply the search actions to generate a list of next states + next_state_list = [] + for action in action_list: + next_state_list.extend(action.nextState(state, gate_spec, func_args.qpu_width)) + + return next_state_list + + +def CutOptimizationGoalStateFunc(state, func_args): + """Return True if the input state is a goal state (i.e., the cutting + decisions made satisfy the device constraints and the optimization settings). + """ + return state.getSearchLevel() >= len(func_args.entangling_gates) + + +# Object that holds the search-space functions for generating +# the cut optimization search space. +cut_optimization_search_funcs = SearchFunctions( + cost_func=CutOptimizationCostFunc, + stratum_func=CutOptimizationStratumFunc, + upperbound_cost_func=CutOptimizationUpperBoundCostFunc, + next_state_func=CutOptimizationNextStateFunc, + goal_state_func=CutOptimizationGoalStateFunc, + mincost_bound_func=CutOptimizationMinCostBoundFunc, +) + + +def greedyCutOptimization( + circuit_interface, + optimization_settings, + device_constraints, + search_space_funcs=cut_optimization_search_funcs, + search_actions=disjoint_subcircuit_actions, +): + func_args = CutOptimizationFuncArgs() + func_args.entangling_gates = circuit_interface.getMultiQubitGates() + func_args.search_actions = search_actions + func_args.max_gamma = optimization_settings.getMaxGamma() + func_args.qpu_width = device_constraints.getQPUWidth() + + start_state = DisjointSubcircuitsState(circuit_interface.getNumQubits()) + + return greedyBestFirstSearch(start_state, search_space_funcs, func_args) + + +################################################################################ + + +class CutOptimization: + + """Class that implements the action of circuit cutting to create disjoint + subcircuits. It uses upper and lower bounds on the resulting gamma in order + to decide where and how to cut while deferring the exact + choices of quasiprobability decompositions. + + Member Variables: + + circuit (CircuitInterface) is the interface object for the circuit + to be cut. + + settings (OptimizationSettings) is an object that contains the settings + that control the optimization process. + + constraints (DeviceConstraints) is an object that contains the device + constraints that solutions must obey. + + search_funcs (SearchFunctions) is an object that holds the functions + needed to generate and explore the cut optimization search space. + + func_args (CutOptimizationFuncArgs) is an object that contains the + necessary device constraints and optimization settings parameters that + aree needed by the cut optimization search-space function. + + search_actions (ActionNames) is an object that contains the allowed + actions that are used to generate the search space. + + search_engine (BestFirstSearch) is an object that implements the + search algorithm. + """ + + def __init__( + self, + circuit_interface, + optimization_settings, + device_constraints, + search_engine_config={ + "CutOptimization": SearchSpaceGenerator( + functions=cut_optimization_search_funcs, + actions=disjoint_subcircuit_actions, + ) + }, + ): + """A CutOptimization object must be initialized with + a specification of all of the parameters of the optimization to be + performed: i.e., the circuit to be cut, the optimization settings, + the target-device constraints, the functions for generating the + search space, and the allowed search actions.""" + + generator = search_engine_config["CutOptimization"] + search_space_funcs = generator.functions + search_space_actions = generator.actions + + # Extract the subset of allowed actions as defined in the settings object + cut_groups = optimization_settings.getCutSearchGroups() + cut_actions = search_space_actions.copy(cut_groups) + + self.circuit = circuit_interface + self.settings = optimization_settings + self.constraints = device_constraints + self.search_funcs = search_space_funcs + self.search_actions = cut_actions + + self.func_args = CutOptimizationFuncArgs() + self.func_args.entangling_gates = self.circuit.getMultiQubitGates() + self.func_args.search_actions = self.search_actions + self.func_args.max_gamma = self.settings.getMaxGamma() + self.func_args.qpu_width = self.constraints.getQPUWidth() + + # Perform an initial greedy best-first search to determine an upper + # bound for the optimal gamma + self.greedy_goal_state = greedyCutOptimization( + self.circuit, + self.settings, + self.constraints, + search_space_funcs=self.search_funcs, + search_actions=self.search_actions, + ) + ################################################################################ + + # Push the start state onto the search_engine + start_state = DisjointSubcircuitsState(self.circuit.getNumQubits()) + + sq = selectSearchEngine( + "CutOptimization", + self.settings, + self.search_funcs, + stop_at_first_min=False, + ) + + sq.initialize([start_state], self.func_args) + + # Use the upper bound for the optimal gamma to constrain the search + if self.greedy_goal_state is not None: + sq.updateUpperBoundGoalState(self.greedy_goal_state, self.func_args) + + self.search_engine = sq + self.goal_state_returned = False + + def optimizationPass(self): + """Produce, in each call, a goal state representing a distinct set of cutting decisions. + The first goal state returned corresponds to cutting decisions that minimize the + lower bound on the resulting gamma. None is returned once no additional choices of cuts + can be made without exceeding the minimum upper bound across all cutting decisions + previously returned, subject to the optimization settings. + """ + state, cost = self.search_engine.optimizationPass(self.func_args) + + if state is None and not self.goal_state_returned: + state = self.greedy_goal_state + cost = self.search_funcs.cost_func(state, self.func_args) + + self.goal_state_returned = True + + return state, cost + + def minimumReached(self): + """Return True if the optimization reached a global minimum.""" + return self.search_engine.minimumReached() + + def getStats(self, penultimate=False): + """Return the search-engine statistics.""" + return self.search_engine.getStats(penultimate=penultimate) + + def getUpperBoundCost(self): + """Return the current upperbound cost.""" + return self.search_engine.getUpperBoundCost() + + def updateUpperBoundCost(self, cost_bound): + """Update the cost upper bound based on an + input cost bound. + """ + self.search_engine.updateUpperBoundCost(cost_bound) diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cutting_actions.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cutting_actions.py new file mode 100644 index 000000000..f5c2a9d44 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cutting_actions.py @@ -0,0 +1,323 @@ +""" File containing classes needed to implement the actions involved in circuit cutting.""" +import numpy as np +from abc import ABC, abstractmethod +from .search_space_generator import ActionNames + +### This is an object that holds action names for constructing disjoint subcircuits +disjoint_subcircuit_actions = ActionNames() + + +class DisjointSearchAction(ABC): + + """Base class for search actions for constructing disjoint subcircuits.""" + + @abstractmethod + def getName(self): + """Derived classes must return the look-up name of the action.""" + + assert False, "Derived classes must override getName()" + + @abstractmethod + def getGroupNames(self): + """Derived classes must return a list of group names.""" + + assert False, "Derived classes must override getGroupNames()" + + @abstractmethod + def nextStatePrimitive(self, state, gate_spec, max_width): + """Derived classes must return a list of search states that + result from applying all variations of the action to gate_spec + in the specified DisjointSubcircuitsState state, subject to the + constraint that the number of resulting qubits (wires) in each + subcircuit cannot exceed max_width. + """ + + assert False, "Derived classes must override nextState()" + + def nextState(self, state, gate_spec, max_width): + """Return a list of search states that result from applying the + action to gate_spec in the specified DisjointSubcircuitsState + state, subject to the constraint that the number of resulting + qubits (wires) in each subcircuit cannot exceed max_width. + """ + + next_list = self.nextStatePrimitive(state, gate_spec, max_width) + + for next_state in next_list: + next_state.setNextLevel(state) + + return next_list + + def registerCut(self, assignment_settings, gate_spec, cut_args): + """Derived classes must register the action in the specified + AssignmentSettings object, where the action was applied to gate_spec + with the action arguments cut_args. + """ + + assert False, "Derived classes must override registerCut()" + + def initializeCut(self, assignment_settings, gate_spec, cut_args): + """Derived classes must initialize the action in the specified + AssignmentSettings object, where the action was applied to gate_spec + with the action arguments cut_args. Intialization is performed after + all actions have been registered.""" + + assert False, "Derived classes must override initializeCut()" + + def nextAssignment( + self, assign_state, constraint_obj, gate_spec, cut_args, assign_actions + ): + """Returns a list of next assignment states that result from + applying assignment actions to the input assignment state. + """ + + next_list = self.nextAssignmentPrimitive( + assign_state, constraint_obj, gate_spec, cut_args, assign_actions + ) + + for next_state in next_list: + next_state.setNextLevel(assign_state) + + return next_list + + +class ActionApplyGate(DisjointSearchAction): + + """Action class that implements the action of + applying a two-qubit gate without decomposition""" + + def getName(self): + """Return the look-up name of ActionApplyGate""" + + return None + + def getGroupNames(self): + """Return the group name of ActionApplyGate""" + + return [None, "TwoQubitGates", "MultiqubitGates"] + + def nextStatePrimitive(self, state, gate_spec, max_width): + """Return the new state that results from applying + ActionApplyGate to state given the two-qubit gate + specification: gate_spec. + """ + + if len(gate_spec[1]) > 3: + # The function multiqubitNextState handles + # gates that act on 3 or more qubits. + return self.multiqubitNextState(state, gate_spec, max_width) + + gate = gate_spec[1] # extract the gate from gate specification. + r1 = state.findQubitRoot(gate[1]) # extract the root wire for the first qubit + # acted on by the given 2-qubit gate. + r2 = state.findQubitRoot(gate[2]) # extract the root wire for the second qubit + # acted on by the given 2-qubit gate. + + # If applying the gate would cause the number of qubits to exceed + # the qubit limit, then do not apply the gate + if r1 != r2 and state.width[r1] + state.width[r2] > max_width: + return list() + + # If the gate cannot be applied because it would violate the + # merge constraints, then do not apply the gate + if state.checkDoNotMergeRoots(r1, r2): + return list() + + new_state = state.copy() + + if r1 != r2: + new_state.mergeRoots(r1, r2) + + new_state.addAction(self, gate_spec) + + return [new_state] + + def multiqubitNextState(self, state, gate_spec, max_width): + """Return the new state that results from applying + ActionApplyGate to state given a multiqubit gate specification: gate_spec. + """ + + gate = gate_spec[1] + roots = list(set([state.findQubitRoot(q) for q in gate[1:]])) + new_width = sum([state.width[r] for r in roots]) + + # If applying the gate would cause the number of qubits to exceed + # the qubit limit, then do not apply the gate + if new_width > max_width: + return list() + + new_state = state.copy() + + r0 = roots[0] + for r in roots[1:]: + new_state.mergeRoots(r, r0) + r0 = new_state.findWireRoot(r0) + + # If the gate cannot be applied because it would violate the + # merge constraints, then do not apply the gate + if not new_state.verifyMergeConstraints(): + return list() + + new_state.addAction(self, gate_spec) + + return [new_state] + + +### Adds ActionApplyGate to the object disjoint_subcircuit_actions +disjoint_subcircuit_actions.defineAction(ActionApplyGate()) + + +class ActionCutTwoQubitGate(DisjointSearchAction): + + """Action class that implements the action of + cutting a two-qubit gate. + . + + TODO: The list of supported gates needs to be expanded. + """ + + def __init__(self): + """The values in gate_dict are tuples in + (gamma_LB, num_bell_pairs, gamma_UB) format. + lowerBoundGamma is computed from gamma_LB using the + DisjointSubcircuitsState.lowerBoundGamma() method. + """ + self.gate_dict = { + "cx": (1, 1, 3), + "swap": (1, 2, 7), + "iswap": (1, 2, 7), + "crx": ( + lambda t: ( + 1 + 2 * np.abs(np.sin(t[1] / 2)), + 0, + 1 + 2 * np.abs(np.sin(t[1] / 2)), + ) + ), + "cry": ( + lambda t: ( + 1 + 2 * np.abs(np.sin(t[1] / 2)), + 0, + 1 + 2 * np.abs(np.sin(t[1] / 2)), + ) + ), + "crz": ( + lambda t: ( + 1 + 2 * np.abs(np.sin(t[1] / 2)), + 0, + 1 + 2 * np.abs(np.sin(t[1] / 2)), + ) + ), + "rxx": ( + lambda t: ( + 1 + 2 * np.abs(np.sin(t[1])), + 0, + 1 + 2 * np.abs(np.sin(t[1])), + ) + ), + "ryy": ( + lambda t: ( + 1 + 2 * np.abs(np.sin(t[1])), + 0, + 1 + 2 * np.abs(np.sin(t[1])), + ) + ), + "rzz": ( + lambda t: ( + 1 + 2 * np.abs(np.sin(t[1])), + 0, + 1 + 2 * np.abs(np.sin(t[1])), + ) + ), + } + + def getName(self): + """Return the look-up name of ActionCutTwoQubitGate.""" + return "CutTwoQubitGate" + + def getGroupNames(self): + """Return the group name of ActionCutTwoQubitGate.""" + return ["GateCut", "TwoQubitGates"] + + def nextStatePrimitive(self, state, gate_spec, max_width): + """Return the new state that results from applying + ActionCutTwoQubitGate to state given the gate_spec. + """ + + # If the gate is not a two-qubit gate, then return the empty list + if len(gate_spec[1]) != 3: + return list() + + gamma_LB, num_bell_pairs, gamma_UB = self.getCostParams(gate_spec) + + if gamma_LB is None: + return list() + + gate = gate_spec[1] + q1 = gate[1] + q2 = gate[2] + w1 = state.getWire(q1) + w2 = state.getWire(q2) + r1 = state.findQubitRoot(q1) + r2 = state.findQubitRoot(q2) + + if r1 == r2: + return list() + + new_state = state.copy() + + new_state.assertDoNotMergeRoots(r1, r2) + + new_state.gamma_LB *= gamma_LB + + for k in range(num_bell_pairs): + new_state.bell_pairs.append((r1, r2)) + + new_state.gamma_UB *= gamma_UB + + new_state.addAction(self, gate_spec, (1, w1), (2, w2)) + + return [new_state] + + def getCostParams(self, gate_spec): + """Call lookupCostParams function.""" + return lookupCostParams(self.gate_dict, gate_spec, (None, None, None)) + + def registerCut(self, assignment_settings, gate_spec, cut_args): + """Register the gate cuts made by a ActionCutTwoQubitGate action + in an AssignmentSettings object. + """ + + assignment_settings.registerGateCut(gate_spec, cut_args[0][0]) + assignment_settings.registerGateCut(gate_spec, cut_args[1][0]) + + def initializeCut(self, assignment_settings, gate_spec, cut_args): + """Initialize the gate cuts made by a ActionCutTwoQubitGate action + in an AssignmentSettings object. + """ + + assignment_settings.initGateCut(gate_spec, cut_args[0][0]) + assignment_settings.initGateCut(gate_spec, cut_args[1][0]) + + def exportCuts(self, circuit_interface, wire_map, gate_spec, args): + """Insert an LO gate cut into the input circuit for the specified gate + and cut arguments. + """ + + circuit_interface.insertGateCut(gate_spec[0], "LO") + + +### Adds ActionCutTwoQubitGate to the object disjoint_subcircuit_actions +disjoint_subcircuit_actions.defineAction(ActionCutTwoQubitGate()) + + +def lookupCostParams(gate_dict, gate_spec, default_value): + gate_name = gate_spec[1][0] + + if gate_name in gate_dict: + return gate_dict[gate_name] + + elif isinstance(gate_name, tuple) or isinstance(gate_name, list): + if gate_name[0] in gate_dict: + return gate_dict[gate_name[0]](gate_name) + + return default_value diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/disjoint_subcircuits_state.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/disjoint_subcircuits_state.py new file mode 100644 index 000000000..13ed3a1d3 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/disjoint_subcircuits_state.py @@ -0,0 +1,408 @@ +"""File containing the class needed for representing search-space states when cutting circuits.""" +import copy +import numpy as np +from collections import Counter, namedtuple + + +class DisjointSubcircuitsState: + + """Class for representing search-space states when cutting + circuits to construct disjoint subcircuits. Only minimally + sufficient information is stored in order to minimize the + memory footprint. + + Each wire cut introduces a new wire. A mapping from qubit IDs + in QASM-like statements to wire IDs is therefore created + and maintained. Groups of wires form subcircuits, and these + subcircuits can then be merged via search actions. The mapping + from wires to subcircuits is represented using an up-tree data + structure over wires. The number of wires (width) in each + subcircuit is also tracked to ensure subcircuits will fit on + target quantum devices. + + Member Variables: + + wiremap (int Numpy array) provides the mapping from qubit IDs + to wire IDs. + + num_wires (int) is the number of wires in the cut circuit. + + uptree (int Numpy array) contains the uptree data structure that + defines groups of wires that form subcircuits. The uptree array + map wire IDs to parent wire IDs in a subcircuit. If a wire points + to itself, then that wire is the root wire in the corresponding + subcircuit. Otherwise, you need to follow the parent links to find + the root wire that corresponds to that subcircuit. + + width (int Numpy array) contains the number of wires in each + subcircuit. The values of width are valid only for root wire IDs. + + bell_pairs (list) is a list of pairs of subcircuits (wires) that + define the virtual Bell pairs that would need to be constructed in + order to implement optimal LOCC cuts using ancillas. + + gamma_LB (float) is the cumulative lower-bound gamma for circuit cuts + that cannot be constructed using Bell pairs, such as LO gate cuts + for small-angled rotations. + + gamma_UB (float) is the cumulative upper-bound gamma for all circuit + cuts assuming all cuts are LO. + + no_merge (list) contains a list of subcircuit merging constaints. + Each constraint can either be a pair of wire IDs or a list of pairs + of wire IDs. In the case of a pair of wire IDs, the constraint is + that the subcircuits that contain those wire IDs cannot be merged + by subsequent search actions. In the case of a list of pairs of + wire IDs, the constraint is that at least one pair of corresponding + subcircuits cannot be merged. + + actions (list) contains a list of circuit-cutting actions that have + been performed on the circuit. Elements of the list have the form + + [, , (, ..., )] + + The is the object that was used to generate the + circuit cut. The is the specification of the + cut gate using the format defined in the CircuitInterface class + description. The trailing entries are the arguments needed by the + to apply further search-space generating objects + in Stage Two in order to explore the space of QPD assignments to + the circuit-cutting action. + + level (int) is the level in the search tree at which this search + state resides, with 0 being the root of the search tree. + """ + + def __init__(self, num_qubits=None): + """A DisjointSubcircuitsState object must be initialized with + a specification of the number of qubits in the circuit and the + maximum number of wire cuts that can be performed. + """ + if not ( + num_qubits is None or (isinstance(num_qubits, int) and num_qubits >= 0) + ): + raise ValueError("num_qubits must either be None or a positive integer.") + + if num_qubits is None: + self.wiremap = None + self.num_wires = None + + self.uptree = None + self.width = None + + self.bell_pairs = None + self.gamma_LB = None + self.gamma_UB = None + + self.no_merge = None + self.actions = None + self.level = None + + else: + max_wires = num_qubits + + self.wiremap = np.arange(num_qubits) + self.num_wires = num_qubits + + self.uptree = np.arange(max_wires) + self.width = np.ones(max_wires, dtype=int) + + self.bell_pairs = list() + self.gamma_LB = 1.0 + self.gamma_UB = 1.0 + + self.no_merge = list() + self.actions = list() + self.level = 0 + + def __copy__(self): + new_state = DisjointSubcircuitsState() + + new_state.wiremap = self.wiremap.copy() + new_state.num_wires = self.num_wires + + new_state.uptree = self.uptree.copy() + new_state.width = self.width.copy() + + new_state.bell_pairs = self.bell_pairs.copy() + new_state.gamma_LB = self.gamma_LB + new_state.gamma_UB = self.gamma_UB + + new_state.no_merge = self.no_merge.copy() + new_state.actions = self.actions.copy() + new_state.level = None + + return new_state + + def copy(self): + """Make shallow copy.""" + return copy.copy(self) + + def print(self, simple=False): + """Print the various properties of a DisjointSubcircuitState. + In its current state, this function only works for 2 qubit LO gate cuts. + It will be updated as more functionalities are added in the future. + """ + cut_actions = debugActionListWithNames(self.actions) + cut_actions_sublist = [] + Cut = namedtuple("Cut", ["Action", "Gate"]) + + for i in range(len(cut_actions)): + if cut_actions[i][0] == "CutTwoQubitGate": + cut_actions_sublist.append( + Cut( + Action=cut_actions[i][0], + Gate=[cut_actions[i][1][0], cut_actions[i][1][1]], + ) + ) + + if simple: # print only a subset of properties. + # print(self.lowerBoundGamma(), self.gamma_UB, self.getMaxWidth()) + print(cut_actions_sublist) + # print(self.no_merge) + else: + print("wiremap", self.wiremap) + print("num_wires", self.num_wires) + print("uptree", self.uptree) + print("width", self.width) + print("bell_pairs", self.bell_pairs) + print("gamma_LB", self.gamma_LB) + print("lowerBound", self.lowerBoundGamma()) + print("gamma_UB", self.gamma_UB) + print("no_merge", self.no_merge) + print("actions", debugActionListWithNames(self.actions)) + print("level", self.level) + + def getNumQubits(self): + """Return the number of qubits in the circuit.""" + if self.wiremap is not None: + return self.wiremap.shape[0] + + def getMaxWidth(self): + """Return the maximum width across subcircuits.""" + if self.width is not None: + return np.amax(self.width) + + def getSubCircuitIndices(self): + """Return a list of root indices for the subcircuits in + the current cut circuit. + """ + if self.uptree is not None: + return [i for i, j in enumerate(self.uptree[: self.num_wires]) if i == j] + + def getWireRootMapping(self): + """Return a list of root wires for each wire in + the current cut circuit. + """ + return [self.findWireRoot(i) for i in range(self.num_wires)] + + def findRootBellPair(self, bell_pair): + """Find the root wires for a Bell pair (represented as a pair + of wires) and return a sorted tuple representing the Bell pair. + """ + r0 = self.findWireRoot(bell_pair[0]) + r1 = self.findWireRoot(bell_pair[1]) + return (r0, r1) if (r0 < r1) else (r1, r0) + + def lowerBoundGamma(self): + """Calculate a lower bound for gamma using the current + counts for the different circuit cuts. + """ + root_bell_pairs = map(lambda x: self.findRootBellPair(x), self.bell_pairs) + return self.gamma_LB * calcRootBellPairsGamma(root_bell_pairs) + + def upperBoundGamma(self): + """Calculate an upper bound for gamma using the current + counts for the different circuit cuts. + """ + return self.gamma_UB + + def canExpandSubcircuit(self, root, num_wires, max_width): + """Return True if num_wires can be added to subcircuit root + without exceeding the maximum allowed number of qubits. + """ + return self.width[root] + num_wires <= max_width + + def getWire(self, qubit): + """Return the ID of the wire currently associated with qubit.""" + return self.wiremap[qubit] + + def findWireRoot(self, wire): + """Return the ID of the root wire in the subcircuit + that contains wire and collapses the path to the root. + """ + # Find the root wire in the subcircuit + root = wire + while root != self.uptree[root]: + root = self.uptree[root] + + # Collapse the path to the root + while wire != root: + parent = self.uptree[wire] + self.uptree[wire] = root + wire = parent + + return root + + def findQubitRoot(self, qubit): + """Return the ID of the root wire in the subcircuit currently + associated with qubit and collapses the path to the root. + """ + return self.findWireRoot(self.wiremap[qubit]) + + def checkDoNotMergeRoots(self, root_1, root_2): + """Return True if the subcircuits represented by + root wire IDs root_1 and root_2 should not be merged. + """ + + assert root_1 == self.uptree[root_1] and root_2 == self.uptree[root_2], ( + "Arguments must be roots: " + + f"{root_1} != {self.uptree[root_1]} " + + f"or {root_2} != {self.uptree[root_2]}" + ) + + for clause in self.no_merge: + if isinstance(clause[0], tuple) or isinstance(clause[0], list): + constraint = False + for pair in clause: + r1 = self.findWireRoot(pair[0]) + r2 = self.findWireRoot(pair[1]) + if r1 != r2 and not ( + (r1 == root_1 and r2 == root_2) + or (r1 == root_2 and r2 == root_1) + ): + constraint = True + break + if not constraint: + return True + + else: + r1 = self.findWireRoot(clause[0]) + r2 = self.findWireRoot(clause[1]) + + assert r1 != r2, "Do-Not-Merge clauses must not be identical" + + if (r1 == root_1 and r2 == root_2) or (r1 == root_2 and r2 == root_1): + return True + + return False + + def verifyMergeConstraints(self): + """Return True if all merge constraints are satisfied""" + for clause in self.no_merge: + if isinstance(clause[0], tuple) or isinstance(clause[0], list): + constraint = False + for pair in clause: + r1 = self.findWireRoot(pair[0]) + r2 = self.findWireRoot(pair[1]) + if r1 != r2: + constraint = True + break + if not constraint: + return False + + else: + r1 = self.findWireRoot(clause[0]) + r2 = self.findWireRoot(clause[1]) + if r1 == r2: + return False + + return True + + def assertDoNotMergeRoots(self, wire_1, wire_2): + """Add a constraint that the subcircuits associated + with wires IDs wire_1 and wire_2 should not be merged. + """ + + assert self.findWireRoot(wire_1) != self.findWireRoot( + wire_2 + ), f"{wire_1} cannot be the same subcircuit as {wire_2}" + + self.no_merge.append((wire_1, wire_2)) + + def assertDoNotMergeRootPairs(self, pair_list): + """Add a constraint that at least one of the pairs of + subcircuits defined in pair_list should not be merged. + """ + + self.no_merge.append(pair_list) + + def mergeRoots(self, root_1, root_2): + """Merge the subcircuits associated with root wire IDs root_1 + and root_2, and updates the statistics (i.e., width) + associated with the newly merged subcircuit. + """ + + assert root_1 == self.uptree[root_1] and root_2 == self.uptree[root_2], ( + "Arguments must be roots: " + + f"{root_1} != {self.uptree[root_1]} " + + f"or {root_2} != {self.uptree[root_2]}" + ) + + assert root_1 != root_2, f"Cannot merge root {root_1} with itself" + + merged_root = min(root_1, root_2) + other_root = max(root_1, root_2) + self.uptree[other_root] = merged_root + self.width[merged_root] += self.width[other_root] + + def addAction(self, action_obj, gate_spec, *args): + """Append the specified action to the list of search-space + actions that have been performed. + """ + if action_obj.getName() is not None: + self.actions.append([action_obj, gate_spec, args]) + + def getSearchLevel(self): + """Return the search level.""" + return self.level + + def setNextLevel(self, state): + """Set the search level of self to one plus the search + level of the input state. + """ + self.level = state.level + 1 + + def exportCuts(self, circuit_interface): + """Export LO cuts into the input circuit_interface for each of + the cutting decisions made. This wire map assumes no reuse of measured qubits. + """ + wire_map = np.arange(self.num_wires) + + for action, gate_spec, cut_args in self.actions: + action.exportCuts(circuit_interface, wire_map, gate_spec, cut_args) + + root_list = self.getSubCircuitIndices() + wires_to_roots = self.getWireRootMapping() + + subcircuits = [ + list({wire_map[w] for w, r in enumerate(wires_to_roots) if r == root}) + for root in root_list + ] + + circuit_interface.defineSubcircuits(subcircuits) + + +def calcRootBellPairsGamma(root_bell_pairs): + """Calculate the minimum-achievable LOCC gamma for circuit + cuts that utilize virtual Bell pairs. The input can be a list + or iterator over hashable identifiers that represent Bell pairs + across disconnected subcircuits in a cut circuit. There must be + a one-to-one mapping between identifiers and pairs of subcircuits. + Repeated identifiers are interpreted as mutiple Bell pairs across + the same pair of subcircuits, and the counts of such repeats are + used to calculate gamma. + """ + gamma = 1.0 + for n in Counter(root_bell_pairs).values(): + gamma *= 2 ** (n + 1) - 1 + + return gamma + + +def debugActionListWithNames(action_list): + """Replace the action objects that appear in action lists in + DisjointSubcircuitsState objects with the corresponding action names + for readability. + """ + return [[x[0].getName()] + x[1:] for x in action_list] diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/lo_cuts_only_optimizer.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/lo_cuts_only_optimizer.py new file mode 100644 index 000000000..738e0c08f --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/lo_cuts_only_optimizer.py @@ -0,0 +1,174 @@ +"""File containing the wrapper class for optimizing LO gate cuts.""" +from .search_space_generator import SearchFunctions, SearchSpaceGenerator +from .cut_optimization import CutOptimizationNextStateFunc +from .cut_optimization import CutOptimizationUpperBoundCostFunc +from .cut_optimization import ( + disjoint_subcircuit_actions, + CutOptimization, +) +from .cut_optimization import ( + CutOptimizationGoalStateFunc, + CutOptimizationMinCostBoundFunc, +) + + +# Functions for generating the LO gate cuts search space +CutOptimization_search_funcs = SearchFunctions( + cost_func=CutOptimizationUpperBoundCostFunc, + upperbound_cost_func=CutOptimizationUpperBoundCostFunc, + next_state_func=CutOptimizationNextStateFunc, + goal_state_func=CutOptimizationGoalStateFunc, + mincost_bound_func=CutOptimizationMinCostBoundFunc, +) + + +class LOCutsOnlyOptimizer: + + """Wrapper class for optimizing circuit cuts for the case in which + only LO quasiprobability decompositions are employed. + + The search_engine_config dictionary that configures the optimization + algorithms must be specified in the constructor. For flexibility, the + circuit_interface, optimization_settings, and device_constraints can + be specified either in the constructor or in the optimize() method. In + the latter case, the values provided overwrite the previous values. + + The circuit_interface object that is passed to the optimize() + method is updated to reflect the optimized circuit cuts that were + identified. + + Member Variables: + + circuit_interface (CircuitInterface) defines the circuit to be cut. + + optimization_settings (OptimizationSettings) defines the settings + to be used for the optimization. + + device_constraints (DeviceConstraints) defines the capabilties of + the target quantum hardware. + + search_engine_config (dict) maps names of stages of optimization to + the corresponding SearchSpaceGenerator functions and actions that + are used to perform the search for each stage. + + cut_optimization (CutOptimization) is the object created to + perform the circuit cutting optimization. + + best_result (DisjointSubcircuitsState) is the lowest-cost + DisjointSubcircuitsState object identified in the search. + """ + + def __init__( + self, + circuit_interface=None, + optimization_settings=None, + device_constraints=None, + search_engine_config={ + "CutOptimization": SearchSpaceGenerator( + functions=CutOptimization_search_funcs, + actions=disjoint_subcircuit_actions, + ) + }, + ): + self.circuit_interface = circuit_interface + self.optimization_settings = optimization_settings + self.device_constraints = device_constraints + self.search_engine_config = search_engine_config + + self.cut_optimization = None + self.best_result = None + + def optimize( + self, + circuit_interface=None, + optimization_settings=None, + device_constraints=None, + ): + """Method to optimize the cutting of a circuit. + + Input Arguments: + + circuit_interface (CircuitInterface) defines the circuit to be + cut. This object is then updated with the optimized cuts that + were identified. + + optimization_settings (OptimizationSettings) defines the settings + to be used for the optimization. + + device_constraints (DeviceConstraints) defines the capabilties of + the target quantum hardware. + + Returns: + + The lowest-cost DisjointSubcircuitsState object identified in + the search, or None if no solution could be found. In the + case of the former, the circuit_interface object is also + updated as a side effect to incorporate the cuts found. + """ + + if circuit_interface is not None: + self.circuit_interface = circuit_interface + + if optimization_settings is not None: + self.optimization_settings = optimization_settings + + if device_constraints is not None: + self.device_constraints = device_constraints + + assert self.circuit_interface is not None, "circuit_interface cannot be None" + + assert ( + self.optimization_settings is not None + ), "optimization_settings cannot be None" + + assert self.device_constraints is not None, "device_constraints cannot be None" + + # Perform CutOptimization + self.cut_optimization = CutOptimization( + self.circuit_interface, + self.optimization_settings, + self.device_constraints, + search_engine_config=self.search_engine_config, + ) + + out_1 = list() + + while True: + state, cost = self.cut_optimization.optimizationPass() + if state is None: + break + out_1.append((cost, state)) + + min_cost = min(out_1, key=lambda x: x[0], default=None) + + if min_cost is not None: + self.best_result = min_cost[-1] + self.best_result.exportCuts(self.circuit_interface) + else: + self.best_result = None + + return self.best_result + + def getResults(self): + """Return the optimization results.""" + return self.best_result + + def getStats(self, penultimate=False): + """Return the stats associated with optimization results.""" + return { + "CutOptimization": self.cut_optimization.getStats( + penultimate=penultimate + ) + } + + def minimumReached(self): + """Return a Boolean flag indicating whether the global + minimum was reached. + """ + return self.cut_optimization.minimumReached() + + +def printStateList(state_list): + for x in state_list: + print() + x.print(simple=True) diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/optimization_settings.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/optimization_settings.py new file mode 100644 index 000000000..eeef3f833 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/optimization_settings.py @@ -0,0 +1,122 @@ +"""File containing class for specifying parameters that control the optimization.""" +class OptimizationSettings: + + """Class for specifying parameters that control the optimization. + In this release, only LO gate cuts are supported. Other cut types, + including "LOCC" gate and wire cuts will be added in future releases. + + Member Variables: + + max_gamma (int) is a constraint on the maximum value of gamma that a + solution to the optimization is allowed to have to be considered feasible. + All other potential solutions are discarded. + + engine_selections (dict) is a dictionary that defines the selections + of search engines for the various stages of optimization. In this release + only "BestFirst" or Dijkstra's best-first search is supported. In future + relesases the choices "Greedy" and "BeamSearch", which correspond respectively + to bounded-greedy and best-first search and beam search will be added. + + max_backjumps (int) is a constraint on the maximum number of backjump + operations that can be performed by the search algorithm. This constraint + does not apply to beam search. + + beam_width (int) is the beam width used in the optimization. Only the B + best partial solutions are maintained at each level in the search, where B + is the beam width. This constraint only applies to beam search algorithms. + + rand_seed (int) is a seed used to provide a repeatable initialization + of the pesudorandom number generators used by the optimization, which + is useful for debugging purposes. If None is used as the random seed, + then a seed is obtained using an operating-system call to achieve an + unrepeatable randomized initialization, which is useful in practice. + + gate_cut_LO (bool) is a flag that indicates that LO gate cuts should be + included in the optimization. + + Raises: + + ValueError: max_gamma must be a positive definite integer. + ValueError: max_backjumps must be a positive semi-definite integer. + ValueError: beam_width must be a positive definite integer. + """ + + def __init__( + self, + max_gamma=1024, + max_backjumps=10000, + beam_width=30, + rand_seed=None, + LO=True, + engine_selections={"CutOptimization": "BestFirst"}, + ): + if not (isinstance(max_gamma, int) and max_gamma > 0): + raise ValueError("max_gamma must be a positive definite integer.") + + if not (isinstance(max_backjumps, int) and max_backjumps >= 0): + raise ValueError("max_backjumps must be a positive semi-definite integer.") + + if not (isinstance(beam_width, int) and beam_width > 0): + raise ValueError("beam_width must be a positive definite integer.") + + self.max_gamma = max_gamma + self.max_backjumps = max_backjumps + self.beam_width = beam_width + self.rand_seed = rand_seed + self.engine_selections = engine_selections.copy() + self.gate_cut_LO = LO + + def getMaxGamma(self): + """Return the max gamma.""" + return self.max_gamma + + def getMaxBackJumps(self): + """Return the maximum number of allowed search backjumps.""" + return self.max_backjumps + + def getBeamWidth(self): + """Return the beam width.""" + return self.beam_width + + def getRandSeed(self): + """Return the random seed.""" + return self.rand_seed + + def setEngineSelection(self, stage_of_optimization, engine_name): + """Return the name of the search engine to employ.""" + self.engine_selections[stage_of_optimization] = engine_name + + def getEngineSelection(self, stage_of_optimization): + """Return the name of the search engine to employ.""" + return self.engine_selections[stage_of_optimization] + + def clearAllCutTypes(self): + """Reset the flags for all cut types. In this release, only LO gate + cuts are supported. Other cut types, including "LOCC" gate and wire cuts will + be added in future releases. + """ + + self.gate_cut_LO = False + + def setGateCutTypes(self): + """Select which gate-cut types to include in the optimization. + The default is to include all gate-cut types. In this release, only LO gate + cuts are supported. Other cut types, including "LOCC" gate and wire cuts will + be added in future releases. + """ + + self.gate_cut_LO + + def getCutSearchGroups(self): + """Return a list of search-action groups to include in the + optimization for cutting circuits into disjoint subcircuits. In this release, + only LO gate cuts are supported. Other cut types, including "LOCC" gate and + wire cuts will be added in future releases. + """ + + out = [None] + + if self.gate_cut_LO: + out.append("GateCut") + + return out diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/quantum_device_constraints.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/quantum_device_constraints.py new file mode 100644 index 000000000..262cf2076 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/quantum_device_constraints.py @@ -0,0 +1,36 @@ +"""File containing the class used for specifying characteristics of the target QPU.""" +class DeviceConstraints: + + """Class for specifying any characteristics of the target quantum + processor that the optimizer must respect in order for the resulting + subcircuits to be executable on the target processor. The constraints + are trivial for LO cuts. Further constraints will be added as LOCC + is incorporated. + + + Member Variables: + + qubits_per_QPU (int) : The number of qubits that are available on the + individual QPUs that make up the quantum processor. + + num_QPUs (int) : The number of QPUs in the target quantum processor. + + Raises: + + ValueError: qubits_per_QPU must be a positive integer. + ValueError: num_QPUs must be a positive integer. + """ + + def __init__(self, qubits_per_QPU, num_QPUs): + if not (isinstance(qubits_per_QPU, int) and qubits_per_QPU > 0): + raise ValueError("qubits_per_QPU must be a positive definite integer.") + + if not (isinstance(num_QPUs, int) and num_QPUs > 0): + raise ValueError("num_QPUs must be a positive definite integer.") + + self.qubits_per_QPU = qubits_per_QPU + self.num_QPUs = num_QPUs + + def getQPUWidth(self): + """Return the number of qubits supported on each individual QPU.""" + return self.qubits_per_QPU diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/search_space_generator.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/search_space_generator.py new file mode 100644 index 000000000..d03d8f83f --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/search_space_generator.py @@ -0,0 +1,224 @@ +"""File containing the classes needed to generate and explore a search space.""" +class ActionNames: + + """Class that maps action names to individual action objects + and group names and to lists of action objects, where the + action objects are used to generate a search space. + + Member Variables: + + action_dict (dict) maps action names to action objects. + + group_dict (dict) maps group names to lists of action objects. + """ + + def __init__(self): + self.action_dict = dict() + self.group_dict = dict() + + def copy(self, list_of_groups=None): + """Return a copy of self that contains only those actions + whose group affiliations intersect with list_of_groups. + The default is to return a copy containing all actions. + """ + + action_list = getActionSubset(self.action_dict.values(), list_of_groups) + + new_container = ActionNames() + for action in action_list: + new_container.defineAction(action) + + return new_container + + def defineAction(self, action_object): + """Insert the specified action object into the look-up + dictionaries using the name of the action and its group + names. + """ + + assert ( + action_object.getName() not in self.action_dict + ), f"Action {action_object.getName()} is already defined" + + self.action_dict[action_object.getName()] = action_object + + group_name = action_object.getGroupNames() + + if isinstance(group_name, list) or isinstance(group_name, tuple): + for name in group_name: + if name not in self.group_dict: + self.group_dict[name] = list() + self.group_dict[name].append(action_object) + else: + if group_name not in self.group_dict: + self.group_dict[group_name] = list() + self.group_dict[group_name].append(action_object) + + def defineActionList(self, action_list): + """Insert the action object in the specified action list + into the look-up dictionaries using the name of the action + and its group names. + """ + + for action in action_list: + self.defineAction(action) + + def getAction(self, action_name): + """Return the action object associated with the specified name. + None is returned if there is no associated action object. + """ + + if action_name in self.action_dict: + return self.action_dict[action_name] + return None + + def getGroup(self, group_name): + """Return the list of action objects associated with the group_name. + None is returned if there are no associated action objects. + """ + + if group_name in self.group_dict: + return self.group_dict[group_name] + return None + + def getGroupActionNames(self, group_name): + """Returns a list of the names of action objects associated with + the group_name. None is returned if there are no associated action + objects.""" + + if group_name in self.group_dict: + return [a.getName() for a in self.group_dict[group_name]] + return None + + def getActionNameList(self): + """Return a list of action names that have been defined.""" + + return list(self.action_dict.keys()) + + def getGroupNameList(self): + """Return a list of group names that have been defined.""" + + return list(self.group_dict.keys()) + + +def getActionSubset(action_list, action_groups): + """Return the subset of actions in action_list whose group affiliations + intersect with action_groups.""" + + if action_groups is None: + return action_list + + if len(action_groups) == 0: + action_groups = [None] + + groups = set(action_groups) + + return [ + a for a in action_list if len(groups.intersection(set(a.getGroupNames()))) > 0 + ] + + +class SearchFunctions: + + """Container class for holding functions needed to generate and explore + a search space. In addition to the required input arguments, the function + signatures are assumed to also allow additional input arguments that are + needed to perform the corresponding computations. In particular, an + ActionNames object should be incorporated into the additional input + arguments in order to generate next-states. For simplicity, all search + algorithms will assume that all search-space functions employ the same set + of additional arguments. + + Member Variables: + + cost_func (lambda state, *args) is a function that computes cost values + from search states. The cost returned can be numeric or tuples of + numerics. In the latter case, lexicographical comparisons are performed + per Python semantics. + + stratum_func (lambda state, *args) is a function that computes stratum + identifiers from search states, which are then used to stratify the search + space when stratified beam search is employed. The stratum_func can be + None, in which case each level of the search has only one stratum, which + is then labeled None. + + greedy_bound_func (lambda current_best_cost, *args) can be either + None or a function that computes upper bounds to costs that are used during + the greedy depth-first phases of search. If None is provided, the upper + bound is taken to be infinity. In greedy search, the search proceeds in a + greedy best-first, depth-first fashion until either a goal state is reached, + a deadend is reached, or the cost bound provided by the greedy_bound_func is + exceeded. In the latter two cases, the search backjumps to the lowest cost + state in the search frontier and the search proceeds from there. The + inputs passed to the greedy_bound_func are the current lowest cost in the + search frontier and the input arguments that were passed to the + optimizationPass() method of the search algorithm. If the greedy_bound_func + simply returns current_best_cost, then the search behavior is equivalent to + pure best-first search. Returning None is equivalent to returning an + infinite greedy bound, which produces a purely greedy best-first, + depth-first search. + + next_state_func (lambda state, *args) is a function that returns a list + of next states generated from the input state. An ActionNames object + should be incorporated into the additional input arguments in order to + generate next-states. + + goal_state_func (lambda state, *args) is a function that returns True if + the input state is a solution state of the search. + + upperbound_cost_func (lambda goal_state, *args) can either be None or a + function that returns an upper bound to the optimal cost given a goal_state + as input. The upper bound is used to prune next-states from the search in + subsequent calls to the optimizationPass() method of the search algorithm. + If upperbound_cost_func is None, the cost of the goal_state as determined + by cost_func is used as an upper bound to the optimal cost. If the + upperbound_cost_func returns None, the effect is equivalent to returning + an infinite upper bound (i.e., no cost pruning is performed on subsequent + calls to the optimizationPass method. + + mincost_bound_func (lambda *args) can either be None or a function that + returns a cost bound that is compared to the minimum cost across all + vertices in a search frontier. If the minimum cost exceeds the min-cost + bound, the search is terminated even if a goal state has not yet been found. + Returning None is equivalent to returning an infinite min-cost bound (i.e., + min-cost checking is effectively not performed). A mincost_bound_func that + is None is likewise equivalent to an infinite min-cost bound. + """ + + def __init__( + self, + cost_func=None, + stratum_func=None, + greedy_bound_func=None, + next_state_func=None, + goal_state_func=None, + upperbound_cost_func=None, + mincost_bound_func=None, + ): + self.cost_func = cost_func + self.greedy_bound_func = greedy_bound_func + self.next_state_func = next_state_func + self.goal_state_func = goal_state_func + self.upperbound_cost_func = upperbound_cost_func + self.mincost_bound_func = mincost_bound_func + + +class SearchSpaceGenerator: + + """Container class for holding both the functions and the + associated actions needed to generate and explore a search space. + + Member Variables: + + functions (SearchFunctions) is a container class that holds + the functions needed to generate and explore a search space. + + actions (ActionNames) is a container class that holds the search + action objects needed to generate and explore a search space. + The actions are expected to be passed as arguments to the search + functions by a search engine. + """ + + def __init__(self, functions=None, actions=None): + self.functions = functions + self.actions = actions diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/utils.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/utils.py new file mode 100644 index 000000000..5a27f8940 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/utils.py @@ -0,0 +1,123 @@ +"""File containing helper functions that are used in the code.""" +from qiskit import QuantumCircuit +from qiskit.circuit import Instruction +from .best_first_search import BestFirstSearch + + +def QCtoCCOCircuit(circuit: QuantumCircuit): + """Convert a qiskit quantum circuit object into a circuit list that is compatible with SimpleGateList + and can therefore be used as an input to the CircuitCuttingOptimizer. + + Args: + circuit: qiskit.QuantumCircuit() object. + + Returns: + circuit_list_rep: list of circuit gates along with qubit numbers associated with each gate, represented in a + form that is compatible with SimpleGateList and is of the form: + + ['barrier', + ('barrier', ), + (( [, ]), ... )]. + """ + + circuit_list_rep = list() + num_circuit_instructions = len(circuit.data) + + for i in range(num_circuit_instructions): + gate_instruction = circuit.data[i] + instruction_name = gate_instruction.operation.name + qubit_ref = gate_instruction.qubits + params = gate_instruction.operation.params + circuit_element = instruction_name + + if (circuit_element == "barrier") and ( + len(qubit_ref) == circuit.num_qubits + ): # barrier across all qubits is not assigned to a specific qubit. + circuit_list_rep.append(circuit_element) + + else: + circuit_element = (circuit_element,) + if params: + circuit_element += tuple(params[i] for i in range(len(params))) + circuit_element = (circuit_element,) + + for j in range(len(qubit_ref)): + qubit_index = qubit_ref[j].index + circuit_element += (qubit_index,) + circuit_list_rep.append(circuit_element) + + return circuit_list_rep + +def CCOtoQCCircuit(interface): + """Convert the cut circuit outputted by the CircuitCuttingOptimizer into a qiskit.QuantumCircuit object. + + Args: + interface: A SimpleGateList object whose attributes carry information about the cut circuit. + + Returns: + qc_cut: The SimpleGateList converted into a qiskit.QuantumCircuit object, + """ + cut_circuit_list = interface.exportCutCircuit(name_mapping=None) + num_qubits = interface.getNumWires() + cut_circuit_list_len = len(cut_circuit_list) + cut_types = interface.cut_type + qc_cut= QuantumCircuit(num_qubits) + for i in range(cut_circuit_list_len): + op = cut_circuit_list[i] #the operation, including gate names and qubits acted on. + gate_qubits = len(op) - 1 #number of qubits involved in the operation. + if cut_types[i] is None: #only append gates that are not cut to qc_cut. May replace cut gates with TwoQubitQPDGate's in future. + if type(op[0]) is tuple: + params = [i for i in op[0][1:]] + gate_name = op[0][0] + else: + params = [] + gate_name = op[0] + inst=Instruction(gate_name, gate_qubits, 0, params) + qc_cut.append(inst, op[1:len(op)]) + return qc_cut + +def selectSearchEngine( + stage_of_optimization, + optimization_settings, + search_space_funcs, + stop_at_first_min=False, +): + engine = optimization_settings.getEngineSelection(stage_of_optimization) + + if engine == "BestFirst": + return BestFirstSearch( + optimization_settings, + search_space_funcs, + stop_at_first_min=stop_at_first_min, + ) + + else: + assert False, f"Invalid stage_of_optimization {stage_of_optimization}" + + +def greedyBestFirstSearch(state, search_space_funcs, *args): + """Perform greedy best-first search using the input starting state and + the input search-space functions. The resulting goal state is returned, + or None if a deadend is reached (no backtracking is performed). Any + additional input arguments are passed as additional arguments to the + search-space functions. + """ + + if search_space_funcs.goal_state_func(state, *args): + return state + + best = min( + [ + (search_space_funcs.cost_func(next_state, *args), k, next_state) + for k, next_state in enumerate( + search_space_funcs.next_state_func(state, *args) + ) + ], + default=(None, None, None), + ) + + if best[-1] is not None: + return greedyBestFirstSearch(best[-1], search_space_funcs, *args) + + else: + return None diff --git a/circuit_knitting/cutting/cut_finding/__init__.py b/circuit_knitting/cutting/cut_finding/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb b/circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb new file mode 100644 index 000000000..666609cd9 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb @@ -0,0 +1,374 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import numpy as np\n", + "sys.path.insert(0, '..')\n", + "from qiskit import QuantumCircuit\n", + "from LO_gate_cut_optimizer.circuit_interface import SimpleGateList\n", + "from LO_gate_cut_optimizer.optimization_settings import OptimizationSettings\n", + "from LO_gate_cut_optimizer.lo_cuts_only_optimizer import LOCutsOnlyOptimizer\n", + "from LO_gate_cut_optimizer.quantum_device_constraints import DeviceConstraints\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test of LO gate cut optimizer using Best-First Search (Dijkstra's algorithm)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + " Gamma = 27.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[13, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 3, 6]])]\n", + "Subcircuits: AAAABBBB \n", + "\n", + "\n", + "\n", + "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + " Gamma = 14348907.0 , min_reached = False\n", + "[Cut(Action='CutTwoQubitGate', Gate=[3, ['cx', 0, 3]]), Cut(Action='CutTwoQubitGate', Gate=[4, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[5, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 4, 7]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 5, 7]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 6, 7]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[13, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[18, ['cx', 0, 3]]), Cut(Action='CutTwoQubitGate', Gate=[19, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[20, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[24, ['cx', 4, 7]]), Cut(Action='CutTwoQubitGate', Gate=[25, ['cx', 5, 7]]), Cut(Action='CutTwoQubitGate', Gate=[26, ['cx', 6, 7]])]\n", + "Subcircuits: AAABCCCD \n", + "\n" + ] + } + ], + "source": [ + "#test circuit\n", + "circuit = [('cx', 0, 1), ('cx', 0, 2), ('cx', 1, 2),\n", + " ('cx', 0, 3), ('cx', 1, 3), ('cx', 2, 3),\n", + " \n", + " ('cx', 4, 5), ('cx', 4, 6), ('cx', 5, 6),\n", + " ('cx', 4, 7), ('cx', 5, 7), ('cx', 6, 7),\n", + " \n", + " \n", + " ('cx', 3, 4), ('cx', 3, 5), ('cx', 3, 6), \n", + " \n", + " \n", + " ('cx', 0, 1), ('cx', 0, 2), ('cx', 1, 2),\n", + " ('cx', 0, 3), ('cx', 1, 3), ('cx', 2, 3),\n", + " \n", + " ('cx', 4, 5), ('cx', 4, 6), ('cx', 5, 6),\n", + " ('cx', 4, 7), ('cx', 5, 7), ('cx', 6, 7),\n", + " ]\n", + "\n", + "interface = SimpleGateList(circuit)\n", + "\n", + "settings = OptimizationSettings(rand_seed = 12345)\n", + "\n", + "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", + "\n", + "qubits_per_QPU=4\n", + "num_QPUs = 2\n", + "\n", + "\n", + "\n", + "\n", + "for num_qpus in range(num_QPUs, 1, -1):\n", + " for qpu_qubits in range(qubits_per_QPU, 2, -1):\n", + " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " \n", + " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", + " num_QPUs = num_QPUs)\n", + "\n", + " op = LOCutsOnlyOptimizer(interface, \n", + " settings, \n", + " constraint_obj)\n", + " \n", + " out = op.optimize()\n", + "\n", + " print(' Gamma =', None if (out is None) else out.upperBoundGamma(),\n", + " ', min_reached =', op.minimumReached())\n", + " if (out is not None):\n", + " out.print(simple=True)\n", + " else:\n", + " print(out)\n", + "\n", + " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n", + "\n", + " \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cutting efficient SU(2) Circuit from Circuit Knitting Toolbox, tutorial 1." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApQAAAD2CAYAAABobBdEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA4X0lEQVR4nO3de3wU9dn//9duTgRIwjnhrCABkShElINAEYsUQRQVod71VFsPKLcIxa+t3kqtP6v267cVRUpRQa1K+2hF1AqC4eBdRQUMEAQV5JQA4RQSkpCQhOzvjymBQEJ2M7uZmQ/v5+ORh8lkdva63M81uZjDZ3yBQCCAiIiIiEg9+Z0OQERERES8TQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYku00wGYZt0CKMl3OgqIbwa9x9rbhltygfDkYxLTPhu35KNxZja3jDNQ3Yh51FCGWUk+FB9yOorwMCkX05j22ZiWj7iTaePMtHzE23TKW0RERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtuinHIc/Nv4Ola18HwO/z0yKxLb27DuOua35Pq6T2DkcXOtPyMYlJn41JuYi7mTTWTMpF3EtHKB2Udv5g/vY/e3nr0V38+pa32bonk9+9Oc7psOrNtHxMYtJnY1Iu4m4mjTWTchF3UkPpoOioWFokptAqqT0XdxnCqH53s2nnKopLjzgdWr2Ylo9JTPpsTMpF3M2ksWZSLuJOaihd4mDBHj7N+gd+fxR+f5TT4dhmWj4mMemzMSkXcTeTxppJuYh76BpKB63ftoJrH21KIFDJsfISAG4aMpX42CYAPPnGTVyaejWj+t8NwNbdmTz99i38eXImsTGNHIu7NnXl8++sBby59LfVXrNr/yYmjnmBawfe1+DxnktMGmsa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", + "text/plain": [ + "
" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import EfficientSU2\n", + "from LO_gate_cut_optimizer.utils import QCtoCCOCircuit\n", + "\n", + "qc = EfficientSU2(4, entanglement=\"linear\", reps=2).decompose()\n", + "qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)\n", + "\n", + "circuit_ckt_su2=QCtoCCOCircuit(qc)\n", + "\n", + "qc.draw(\"mpl\", scale=0.8)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 1.0 , min_reached = True\n", + "[]\n", + "Subcircuits: AAAA \n", + "\n", + "\n", + "\n", + "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 9.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[17, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[25, ['cx', 2, 3]])]\n", + "Subcircuits: AAAB \n", + "\n", + "\n", + "\n", + "---------- 2 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 9.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 1, 2]]), Cut(Action='CutTwoQubitGate', Gate=[20, ['cx', 1, 2]])]\n", + "Subcircuits: AABB \n", + "\n" + ] + } + ], + "source": [ + "interface = SimpleGateList(circuit_ckt_su2)\n", + "\n", + "settings = OptimizationSettings(rand_seed = 12345)\n", + "\n", + "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", + "\n", + "\n", + "qubits_per_QPU=4\n", + "num_QPUs=2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "for num_qpus in range(num_QPUs, 1, -1):\n", + " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", + " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " \n", + " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", + " num_QPUs = num_QPUs)\n", + "\n", + " op = LOCutsOnlyOptimizer(interface, \n", + " settings, \n", + " constraint_obj)\n", + " \n", + " out = op.optimize()\n", + "\n", + " print('Gamma =', None if (out is None) else out.upperBoundGamma(),\n", + " ', min_reached =', op.minimumReached())\n", + " if (out is not None):\n", + " out.print(simple=True)\n", + " else:\n", + " print(out)\n", + " \n", + " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cutting 7 qubit circuit from Circuit Knitting Toolbox, tutorial 3." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvQAAAHwCAYAAADJpfudAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABk5ElEQVR4nO3deXhU5f3//+dMFhKSEJagARLWJAKBJAIiBBWDsRURBCtuSKtflIogtCLpYq3667cqSBVxhdrW+v18immlWpAiqIBiBAyyR/YQJMugIWELAbLM748jkUACmWFmzpzJ63FduULOnOU9Q3LmNfe5z33bnE6nExERERERsSS72QWIiIiIiIj7FOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCgs0uQBrmdEJtldlVNJ09BGw2s6sQERERaX4U6P1UbRWsnGt2FU2XMRWCQs2uQkRERKT5UZcbERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6EREREREL0zj0AWTz3lU89kZGvWVhoRHEtU8is994Rg95hKAg/ZeLiIiIBBKluwCUkXY3A3vejBMn5cccfPTV27yx+FG++XY7v7x9vtnliYiIiIgHKdAHoMRO/cjsf2/dzyPTH2bCrJ4s/fJN7r/pj7SObG9idSIiIiLiSepD3wyEh0bQs8sgnE4nxYf2ml2OiIiIiHiQAn0zUfJ9kG/Vsq3JlYiIiIiIJ6nLTQA6WXWCIxWlOJ1GH/rFa95gT9FGesYPJK59ktnliYiIiIgHNYsW+tLSUrKyskhISCAs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dmmmahnavLtRBNmte/nUE2w242qEp28cFhHrCA4yrsg1VzabEd6b85XI8FD/HX70UinQNxNvPpZndgkiIiIi4gW62CwiIiIiYmEK9CIiIiIiFqZALyIiIiJiYepD76fsIZAx1ewqms7ux1NCi4iIiAQyBXo/ZbNBkAUn9xERERER31KXGxERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsbBgswuQhjmdUFtldhVNZw8Bm83sKkRERESaHwV6P1VbBSvnml1F02VMhaBQs6sQERERaX7U5UZERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTDfFiogEuOoaKDkCjsNwqtpYdroavjkEHVtDcJCZ1YmIyKVSoBcRCUAnq2D9PsjNh8JyqKmt/3hlFbzwIQTZjVA/oBtc1R1aarQqERHLUaAXEQkgJ6tg6RZYu+eH1vgLqamFA2XG15JNMLAH3JyqYC8iYiUK9AFk895VPPZGRr1lYaERxLVPIrPfeEYPeYSgIP2XiwSqnSXwzlooP+He9qdr4PNdsPUA3Hk19O7k2fpERMQ7lO4CUEba3QzseTNOnJQfc/DRV2/zxuJH+ebb7fzy9vlmlyciHuZ0wrKt8OFWz+zvSCXMXwXDesPINM0CLSLi7xToA1Bip35k9r+37ueR6Q8zYVZPln75Jvff9EdaR7Y3sToR8bQPNsEnX3t+vyu+hqpquG2AQr2IiD/TsJXNQHhoBD27DMLpdFJ8aK/Z5YiIB3220zth/ozVu+DjPO/tX0RELp1a6JuJku+DfKuWbU2uREQ85eARWLTBtW0evQlahcPRSmOUm6ZYugV6dYQ4nT5ERPySWugD0MmqExypKOXw8e/YV7KVuf+ezJ6ijfSMH0hc+ySzyxMRD6ithX+sherai697tlbh0Lql8b3Jx3J+f6wa144lIiK+EfCBvrS0lKysLBISEggLCyM+Pp5p06ZRUVHBhAkTsNlsvPLKK2aX6VFvL3+S259qz9inL2PiCyksXvMa1/S5jafv+4/Z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", + "text/plain": [ + "
" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc_0 = QuantumCircuit(7)\n", + "for i in range(7):\n", + " qc_0.rx(np.pi / 4, i)\n", + "qc_0.cx(0, 3)\n", + "qc_0.cx(1, 3)\n", + "qc_0.cx(2, 3)\n", + "qc_0.cx(3, 4)\n", + "qc_0.cx(3, 5)\n", + "qc_0.cx(3, 6)\n", + "qc_0.cx(0, 3)\n", + "qc_0.cx(1, 3)\n", + "qc_0.cx(2, 3)\n", + "\n", + "qc_0.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "---------- 7 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 1.0 , min_reached = True\n", + "[]\n", + "Subcircuits: AAAAAAA \n", + "\n", + "\n", + "\n", + "---------- 6 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 3.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", + "Subcircuits: AAAAAAB \n", + "\n", + "\n", + "\n", + "---------- 5 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 9.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", + "Subcircuits: AAAABAC \n", + "\n", + "\n", + "\n", + "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 27.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", + "Subcircuits: AAAABCD \n", + "\n", + "\n", + "\n", + "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 243.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", + "Subcircuits: AABACDE \n", + "\n", + "\n", + "\n", + "---------- 2 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 2187.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[8, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", + "Subcircuits: ABCADEF \n", + "\n" + ] + } + ], + "source": [ + "circuit_cktq7=QCtoCCOCircuit(qc_0)\n", + "\n", + "interface = SimpleGateList(circuit_cktq7)\n", + "\n", + "settings = OptimizationSettings(rand_seed = 12345)\n", + "\n", + "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", + "\n", + "\n", + "\n", + "qubits_per_QPU=7\n", + "num_QPUs=2\n", + "\n", + "\n", + "\n", + "for num_qpus in range(num_QPUs, 1, -1):\n", + " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", + " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " \n", + " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", + " num_QPUs = num_QPUs)\n", + "\n", + " op = LOCutsOnlyOptimizer(interface, \n", + " settings, \n", + " constraint_obj)\n", + " \n", + " out = op.optimize()\n", + "\n", + " print('Gamma =', None if (out is None) else out.upperBoundGamma(),\n", + " ', min_reached =', op.minimumReached())\n", + " if (out is not None):\n", + " out.print(simple=True)\n", + " else:\n", + " print(out)\n", + "\n", + " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cco", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/circuit_knitting/cutting/cut_finding/test/__init__.py b/circuit_knitting/cutting/cut_finding/test/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/test/__init__.py @@ -0,0 +1 @@ + diff --git a/circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py b/circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py new file mode 100644 index 000000000..bb9d280e3 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py @@ -0,0 +1,62 @@ +from circuit_cutting_optimizer.circuit_interface import SimpleGateList + + +class TestCircuitInterface: + def test_CircuitConversion(self): + """Test conversion of circuits to the internal representation + used by the circuit-cutting optimizer. + """ + + trial_circuit = [ + ("h", "q1"), + ("barrier", "q1"), + ("s", "q0"), + "barrier", + ("cx", "q1", "q0"), + ] + circuit_converted = SimpleGateList(trial_circuit) + + assert circuit_converted.getNumQubits() == 2 + assert circuit_converted.getNumWires() == 2 + assert circuit_converted.qubit_names.item_dict == {"q1": 0, "q0": 1} + assert circuit_converted.getMultiQubitGates() == [[4, ["cx", 0, 1], None]] + assert circuit_converted.circuit == [ + [["h", 0], None], + [["barrier", 0], None], + [["s", 1], None], + ["barrier", None], + [["cx", 0, 1], None], + ] + + def test_CutInterface(self): + """Test the internal representation of circuit cuts.""" + + trial_circuit = [ + ("cx", 0, 1), + ("cx", 2, 3), + ("cx", 1, 2), + ("cx", 0, 1), + ("cx", 2, 3), + ] + circuit_converted = SimpleGateList(trial_circuit) + circuit_converted.insertGateCut(2, "LO") + circuit_converted.defineSubcircuits([[0, 1], [2, 3]]) + + assert list(circuit_converted.new_gate_ID_map) == [0, 1, 2, 3, 4] + assert circuit_converted.cut_type == [None, None, "LO", None, None] + assert ( + circuit_converted.exportSubcircuitsAsString(name_mapping="default") + == "AABB" + ) + assert circuit_converted.exportCutCircuit(name_mapping="default") == [ + ["cx", 0, 1], + ["cx", 2, 3], + ["cx", 1, 2], + ["cx", 0, 1], + ["cx", 2, 3], + ] # in the absence of any wire cuts. + assert ( + circuit_converted.makeWireMapping(name_mapping="default") + == circuit_converted.makeWireMapping(name_mapping=None) + == [0, 1, 2, 3] + ) # the two methods are the same in the absence of wire cuts. diff --git a/circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py b/circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py new file mode 100644 index 000000000..477d8c0fa --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py @@ -0,0 +1,95 @@ +from pytest import mark, raises, fixture +from circuit_cutting_optimizer.circuit_interface import SimpleGateList +from circuit_cutting_optimizer.disjoint_subcircuits_state import ( + DisjointSubcircuitsState, +) +from circuit_cutting_optimizer.cut_optimization import ( + disjoint_subcircuit_actions, +) + + +@mark.parametrize("num_qubits", [2.1, -1]) +def test_StateInitialization(num_qubits): + """Test that initialization values are valid data types.""" + + with raises(ValueError): + _ = DisjointSubcircuitsState(num_qubits) + + +@fixture +def testCircuit(): + circuit = [ + ("h", "q1"), + ("barrier", "q1"), + ("s", "q0"), + "barrier", + ("cx", "q1", "q0"), + ] + + interface = SimpleGateList(circuit) + + state = DisjointSubcircuitsState( + interface.getNumQubits() + ) # initialization of DisjoingSubcircuitsState object. + + two_qubit_gate = interface.getMultiQubitGates()[0] + + return state, two_qubit_gate + + +def test_StateUnCut(testCircuit): + state, _ = testCircuit + + assert list(state.wiremap) == [0, 1] + + assert state.num_wires == 2 + + assert list(state.uptree) == [0, 1] + + assert list(state.width) == [1, 1] + + assert list(state.no_merge) == [] + + assert state.getSearchLevel() == 0 + + +def test_ApplyGate(testCircuit): + state, two_qubit_gate = testCircuit + + next_state = disjoint_subcircuit_actions.getAction(None).nextState( + state, two_qubit_gate, 10 + )[0] + + assert list(next_state.wiremap) == [0, 1] + + assert next_state.num_wires == 2 + + assert next_state.findQubitRoot(1) == 0 + + assert list(next_state.uptree) == [0, 0] + + assert list(next_state.width) == [2, 1] + + assert list(next_state.no_merge) == [] + + assert next_state.getSearchLevel() == 1 + + +def test_CutGate(testCircuit): + state, two_qubit_gate = testCircuit + + next_state = disjoint_subcircuit_actions.getAction("CutTwoQubitGate").nextState( + state, two_qubit_gate, 10 + )[0] + + assert list(next_state.wiremap) == [0, 1] + + assert next_state.num_wires == 2 + + assert list(next_state.uptree) == [0, 1] + + assert list(next_state.width) == [1, 1] + + assert list(next_state.no_merge) == [(0, 1)] + + assert next_state.getSearchLevel() == 1 diff --git a/circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py b/circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py new file mode 100644 index 000000000..f76f6ce7c --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py @@ -0,0 +1,21 @@ +import pytest +from circuit_cutting_optimizer.optimization_settings import OptimizationSettings + + +@pytest.mark.parametrize( + "max_gamma, max_backjumps, beam_width ", + [(2.1, 1.2, -1.4), (1.2, 1.5, 2.3), (0, 1, 1), (1, 1, 0)], +) +def test_OptimizationParameters(max_gamma, max_backjumps, beam_width): + """Test optimization parameters for being valid data types.""" + + with pytest.raises(ValueError): + _ = OptimizationSettings( + max_gamma=max_gamma, max_backjumps=max_backjumps, beam_width=beam_width + ) + + +def test_AllCutSearchGroups(): + """Test for the existence of all enabled cutting search groups.""" + + assert OptimizationSettings(LO=True).getCutSearchGroups() == [None, "GateCut"] diff --git a/circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py b/circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py new file mode 100644 index 000000000..3aeb3ea7d --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py @@ -0,0 +1,10 @@ +import pytest +from circuit_cutting_optimizer.quantum_device_constraints import DeviceConstraints + + +@pytest.mark.parametrize("qubits_per_QPU, num_QPUs", [(2.1, 1.2), (1.2, 0), (-1, 1)]) +def test_DeviceConstraints(qubits_per_QPU, num_QPUs): + """Test device constraints for being valid data types.""" + + with pytest.raises(ValueError): + _ = DeviceConstraints(qubits_per_QPU, num_QPUs) diff --git a/circuit_knitting/cutting/cut_finding/test/test_utils.py b/circuit_knitting/cutting/cut_finding/test/test_utils.py new file mode 100644 index 000000000..cc4497b34 --- /dev/null +++ b/circuit_knitting/cutting/cut_finding/test/test_utils.py @@ -0,0 +1,46 @@ +import pytest +from qiskit import QuantumCircuit +from qiskit.circuit.library import EfficientSU2 +from circuit_cutting_optimizer.utils import QCtoCCOCircuit + +# test circuit 1. +qc1 = QuantumCircuit(2) +qc1.h(1) +qc1.barrier(1) +qc1.s(0) +qc1.barrier() +qc1.cx(1, 0) + +# test circuit 2 +qc2 = EfficientSU2(2, entanglement="linear", reps=2).decompose() +qc2.assign_parameters([0.4] * len(qc2.parameters), inplace=True) + + +@pytest.mark.parametrize( + "input, output", + [ + (qc1, [("h", 1), ("barrier", 1), ("s", 0), "barrier", ("cx", 1, 0)]), + ( + qc2, + [ + (("ry", 0.4), 0), + (("rz", 0.4), 0), + (("ry", 0.4), 1), + (("rz", 0.4), 1), + ("cx", 0, 1), + (("ry", 0.4), 0), + (("rz", 0.4), 0), + (("ry", 0.4), 1), + (("rz", 0.4), 1), + ("cx", 0, 1), + (("ry", 0.4), 0), + (("rz", 0.4), 0), + (("ry", 0.4), 1), + (("rz", 0.4), 1), + ], + ), + ], +) +def test_QCtoCCOCircuit(input, output): + circuit_internal = QCtoCCOCircuit(input) + assert circuit_internal == output diff --git a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb new file mode 100644 index 000000000..84b7a45ae --- /dev/null +++ b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb @@ -0,0 +1,156 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "#Imports\n", + "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.circuit_interface import SimpleGateList\n", + "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.optimization_settings import OptimizationSettings\n", + "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.lo_cuts_only_optimizer import LOCutsOnlyOptimizer\n", + "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.quantum_device_constraints import DeviceConstraints" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cut finding for efficient SU(2) Circuit from tutorial 1." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualize the circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApQAAAD2CAYAAABobBdEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA4X0lEQVR4nO3de3wU9dn//9duTgRIwjnhrCABkShElINAEYsUQRQVod71VFsPKLcIxa+t3kqtP6v267cVRUpRQa1K+2hF1AqC4eBdRQUMEAQV5JQA4RQSkpCQhOzvjymBQEJ2M7uZmQ/v5+ORh8lkdva63M81uZjDZ3yBQCCAiIiIiEg9+Z0OQERERES8TQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYku00wGYZt0CKMl3OgqIbwa9x9rbhltygfDkYxLTPhu35KNxZja3jDNQ3Yh51FCGWUk+FB9yOorwMCkX05j22ZiWj7iTaePMtHzE23TKW0RERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtuinHIc/Nv4Ola18HwO/z0yKxLb27DuOua35Pq6T2DkcXOtPyMYlJn41JuYi7mTTWTMpF3EtHKB2Udv5g/vY/e3nr0V38+pa32bonk9+9Oc7psOrNtHxMYtJnY1Iu4m4mjTWTchF3UkPpoOioWFokptAqqT0XdxnCqH53s2nnKopLjzgdWr2Ylo9JTPpsTMpF3M2ksWZSLuJOaihd4mDBHj7N+gd+fxR+f5TT4dhmWj4mMemzMSkXcTeTxppJuYh76BpKB63ftoJrH21KIFDJsfISAG4aMpX42CYAPPnGTVyaejWj+t8NwNbdmTz99i38eXImsTGNHIu7NnXl8++sBby59LfVXrNr/yYmjnmBawfe1+DxnktMGmsa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", + "text/plain": [ + "
" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#visualize the circuit \n", + "from qiskit.circuit.library import EfficientSU2\n", + "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.utils import QCtoCCOCircuit\n", + "\n", + "qc = EfficientSU2(4, entanglement=\"linear\", reps=2).decompose()\n", + "qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)\n", + "\n", + "circuit_ckt_su2=QCtoCCOCircuit(qc)\n", + "\n", + "qc.draw(\"mpl\", scale=0.8)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform cut finding" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 1.0 , min_reached = True\n", + "[]\n", + "Subcircuits: AAAA \n", + "\n", + "\n", + "\n", + "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 9.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[17, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[25, ['cx', 2, 3]])]\n", + "Subcircuits: AAAB \n", + "\n", + "\n", + "\n", + "---------- 2 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 9.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 1, 2]]), Cut(Action='CutTwoQubitGate', Gate=[20, ['cx', 1, 2]])]\n", + "Subcircuits: AABB \n", + "\n" + ] + } + ], + "source": [ + "interface = SimpleGateList(circuit_ckt_su2)\n", + "\n", + "settings = OptimizationSettings(rand_seed = 12345)\n", + "\n", + "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", + "\n", + "qubits_per_QPU=4\n", + "num_QPUs=2\n", + "\n", + "\n", + "for num_qpus in range(num_QPUs, 1, -1):\n", + " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", + " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " \n", + " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", + " num_QPUs = num_QPUs)\n", + "\n", + " op = LOCutsOnlyOptimizer(interface, \n", + " settings, \n", + " constraint_obj)\n", + " \n", + " out = op.optimize()\n", + "\n", + " print('Gamma =', None if (out is None) else out.upperBoundGamma(),\n", + " ', min_reached =', op.minimumReached())\n", + " if (out is not None):\n", + " out.print(simple=True)\n", + " else:\n", + " print(out)\n", + " \n", + " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ckt", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/circuit_cutting/tutorials/bell.qpy b/docs/circuit_cutting/tutorials/bell.qpy new file mode 100644 index 0000000000000000000000000000000000000000..69edbd5fc4c4a0d3e16610c90e6b462667aa07ea GIT binary patch literal 142 zcmWIa4EFX6;bf3xWPkw1LI& Date: Thu, 4 Jan 2024 13:22:52 -0600 Subject: [PATCH 3/8] Make cosmetic edits to cut finding bare bones tutorial From 3ca5075a38c59b2a8c5a2b4a4d6a0341a5388cdf Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Thu, 4 Jan 2024 13:33:22 -0600 Subject: [PATCH 4/8] Update cut finding tutorial --- .../tutorials/LO_gate_cut_finder.ipynb | 165 +++++++++++++++++- 1 file changed, 158 insertions(+), 7 deletions(-) diff --git a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb index 84b7a45ae..7ada7ceb0 100644 --- a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb +++ b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb @@ -2,11 +2,10 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "#Imports\n", "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.circuit_interface import SimpleGateList\n", "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.optimization_settings import OptimizationSettings\n", "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.lo_cuts_only_optimizer import LOCutsOnlyOptimizer\n", @@ -29,7 +28,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -39,13 +38,12 @@ "
" ] }, - "execution_count": 7, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "#visualize the circuit \n", "from qiskit.circuit.library import EfficientSU2\n", "from circuit_knitting.cutting.cut_finding.LO_gate_cut_optimizer.utils import QCtoCCOCircuit\n", "\n", @@ -66,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -128,7 +126,160 @@ " else:\n", " print(out)\n", " \n", - " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n" + " print('Subcircuits:', \n", + " interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cut finding for 7 qubit circuit from Circuit Knitting Toolbox, tutorial 3." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create and visualize the circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvQAAAHwCAYAAADJpfudAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABk5ElEQVR4nO3deXhU5f3//+dMFhKSEJagARLWJAKBJAIiBBWDsRURBCtuSKtflIogtCLpYq3667cqSBVxhdrW+v18immlWpAiqIBiBAyyR/YQJMugIWELAbLM748jkUACmWFmzpzJ63FduULOnOU9Q3LmNfe5z33bnE6nExERERERsSS72QWIiIiIiIj7FOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCgs0uQBrmdEJtldlVNJ09BGw2s6sQERERaX4U6P1UbRWsnGt2FU2XMRWCQs2uQkRERKT5UZcbERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6EREREREL0zj0AWTz3lU89kZGvWVhoRHEtU8is994Rg95hKAg/ZeLiIiIBBKluwCUkXY3A3vejBMn5cccfPTV27yx+FG++XY7v7x9vtnliYiIiIgHKdAHoMRO/cjsf2/dzyPTH2bCrJ4s/fJN7r/pj7SObG9idSIiIiLiSepD3wyEh0bQs8sgnE4nxYf2ml2OiIiIiHiQAn0zUfJ9kG/Vsq3JlYiIiIiIJ6nLTQA6WXWCIxWlOJ1GH/rFa95gT9FGesYPJK59ktnliYiIiIgHNYsW+tLSUrKyskhISCAs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dmmmahnavLtRBNmte/nUE2w242qEp28cFhHrCA4yrsg1VzabEd6b85XI8FD/HX70UinQNxNvPpZndgkiIiIi4gW62CwiIiIiYmEK9CIiIiIiFqZALyIiIiJiYepD76fsIZAx1ewqms7ux1NCi4iIiAQyBXo/ZbNBkAUn9xERERER31KXGxERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsbBgswuQhjmdUFtldhVNZw8Bm83sKkRERESaHwV6P1VbBSvnml1F02VMhaBQs6sQERERaX7U5UZERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTDfFiogEuOoaKDkCjsNwqtpYdroavjkEHVtDcJCZ1YmIyKVSoBcRCUAnq2D9PsjNh8JyqKmt/3hlFbzwIQTZjVA/oBtc1R1aarQqERHLUaAXEQkgJ6tg6RZYu+eH1vgLqamFA2XG15JNMLAH3JyqYC8iYiUK9AFk895VPPZGRr1lYaERxLVPIrPfeEYPeYSgIP2XiwSqnSXwzlooP+He9qdr4PNdsPUA3Hk19O7k2fpERMQ7lO4CUEba3QzseTNOnJQfc/DRV2/zxuJH+ebb7fzy9vlmlyciHuZ0wrKt8OFWz+zvSCXMXwXDesPINM0CLSLi7xToA1Bip35k9r+37ueR6Q8zYVZPln75Jvff9EdaR7Y3sToR8bQPNsEnX3t+vyu+hqpquG2AQr2IiD/TsJXNQHhoBD27DMLpdFJ8aK/Z5YiIB3220zth/ozVu+DjPO/tX0RELp1a6JuJku+DfKuWbU2uREQ85eARWLTBtW0evQlahcPRSmOUm6ZYugV6dYQ4nT5ERPySWugD0MmqExypKOXw8e/YV7KVuf+ezJ6ijfSMH0hc+ySzyxMRD6ithX+sherai697tlbh0Lql8b3Jx3J+f6wa144lIiK+EfCBvrS0lKysLBISEggLCyM+Pp5p06ZRUVHBhAkTsNlsvPLKK2aX6VFvL3+S259qz9inL2PiCyksXvMa1/S5jafv+4/Z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", + "text/plain": [ + "
" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "from qiskit import QuantumCircuit\n", + "\n", + "qc_0 = QuantumCircuit(7)\n", + "for i in range(7):\n", + " qc_0.rx(np.pi / 4, i)\n", + "qc_0.cx(0, 3)\n", + "qc_0.cx(1, 3)\n", + "qc_0.cx(2, 3)\n", + "qc_0.cx(3, 4)\n", + "qc_0.cx(3, 5)\n", + "qc_0.cx(3, 6)\n", + "qc_0.cx(0, 3)\n", + "qc_0.cx(1, 3)\n", + "qc_0.cx(2, 3)\n", + "\n", + "qc_0.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Perform cut finding" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "---------- 7 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 1.0 , min_reached = True\n", + "[]\n", + "Subcircuits: AAAAAAA \n", + "\n", + "\n", + "\n", + "---------- 6 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 3.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", + "Subcircuits: AAAAAAB \n", + "\n", + "\n", + "\n", + "---------- 5 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 9.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", + "Subcircuits: AAAABAC \n", + "\n", + "\n", + "\n", + "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 27.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", + "Subcircuits: AAAABCD \n", + "\n", + "\n", + "\n", + "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 243.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", + "Subcircuits: AABACDE \n", + "\n", + "\n", + "\n", + "---------- 2 Qubits per QPU, 2 QPUs ----------\n", + "Gamma = 2187.0 , min_reached = True\n", + "[Cut(Action='CutTwoQubitGate', Gate=[8, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", + "Subcircuits: ABCADEF \n", + "\n" + ] + } + ], + "source": [ + "circuit_cktq7=QCtoCCOCircuit(qc_0)\n", + "\n", + "interface = SimpleGateList(circuit_cktq7)\n", + "\n", + "settings = OptimizationSettings(rand_seed = 12345)\n", + "\n", + "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", + "\n", + "\n", + "\n", + "qubits_per_QPU=7\n", + "num_QPUs=2\n", + "\n", + "\n", + "\n", + "for num_qpus in range(num_QPUs, 1, -1):\n", + " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", + " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " \n", + " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", + " num_QPUs = num_QPUs)\n", + "\n", + " op = LOCutsOnlyOptimizer(interface, \n", + " settings, \n", + " constraint_obj)\n", + " \n", + " out = op.optimize()\n", + "\n", + " print('Gamma =', None if (out is None) else out.upperBoundGamma(),\n", + " ', min_reached =', op.minimumReached())\n", + " if (out is not None):\n", + " out.print(simple=True)\n", + " else:\n", + " print(out)\n", + "\n", + " print('Subcircuits:', \n", + " interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')" ] } ], From 662638607e9ee841bb99bc8f62d363e246b0579b Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Thu, 4 Jan 2024 13:36:00 -0600 Subject: [PATCH 5/8] make minor edits to turotial --- docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb index 7ada7ceb0..045631a48 100644 --- a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb +++ b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb @@ -134,7 +134,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Cut finding for 7 qubit circuit from Circuit Knitting Toolbox, tutorial 3." + "## Cut finding for 7 qubit circuit from Tutorial 3." ] }, { @@ -146,7 +146,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -156,7 +156,7 @@ "
" ] }, - "execution_count": 18, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -190,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 25, "metadata": {}, "outputs": [ { From fdb6ca838d8674a024779d7efb36679074fb0af0 Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Thu, 4 Jan 2024 13:38:06 -0600 Subject: [PATCH 6/8] reformat tutorial --- .../tutorials/LO_gate_cut_finder.ipynb | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb index 045631a48..0bfb519ec 100644 --- a/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb +++ b/docs/circuit_cutting/tutorials/LO_gate_cut_finder.ipynb @@ -28,7 +28,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -38,7 +38,7 @@ "
" ] }, - "execution_count": 15, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -64,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -73,21 +73,21 @@ "text": [ "\n", "\n", - "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + "--- 4 Qubits per QPU, 2 QPUs ---\n", "Gamma = 1.0 , min_reached = True\n", "[]\n", "Subcircuits: AAAA \n", "\n", "\n", "\n", - "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + "--- 3 Qubits per QPU, 2 QPUs ---\n", "Gamma = 9.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[17, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[25, ['cx', 2, 3]])]\n", "Subcircuits: AAAB \n", "\n", "\n", "\n", - "---------- 2 Qubits per QPU, 2 QPUs ----------\n", + "--- 2 Qubits per QPU, 2 QPUs ---\n", "Gamma = 9.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 1, 2]]), Cut(Action='CutTwoQubitGate', Gate=[20, ['cx', 1, 2]])]\n", "Subcircuits: AABB \n", @@ -108,7 +108,7 @@ "\n", "for num_qpus in range(num_QPUs, 1, -1):\n", " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", - " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " print(f'\\n\\n--- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ---')\n", " \n", " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", " num_QPUs = num_QPUs)\n", @@ -190,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -199,42 +199,42 @@ "text": [ "\n", "\n", - "---------- 7 Qubits per QPU, 2 QPUs ----------\n", + "--- 7 Qubits per QPU, 2 QPUs ---\n", "Gamma = 1.0 , min_reached = True\n", "[]\n", "Subcircuits: AAAAAAA \n", "\n", "\n", "\n", - "---------- 6 Qubits per QPU, 2 QPUs ----------\n", + "--- 6 Qubits per QPU, 2 QPUs ---\n", "Gamma = 3.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", "Subcircuits: AAAAAAB \n", "\n", "\n", "\n", - "---------- 5 Qubits per QPU, 2 QPUs ----------\n", + "--- 5 Qubits per QPU, 2 QPUs ---\n", "Gamma = 9.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", "Subcircuits: AAAABAC \n", "\n", "\n", "\n", - "---------- 4 Qubits per QPU, 2 QPUs ----------\n", + "--- 4 Qubits per QPU, 2 QPUs ---\n", "Gamma = 27.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", "Subcircuits: AAAABCD \n", "\n", "\n", "\n", - "---------- 3 Qubits per QPU, 2 QPUs ----------\n", + "--- 3 Qubits per QPU, 2 QPUs ---\n", "Gamma = 243.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", "Subcircuits: AABACDE \n", "\n", "\n", "\n", - "---------- 2 Qubits per QPU, 2 QPUs ----------\n", + "--- 2 Qubits per QPU, 2 QPUs ---\n", "Gamma = 2187.0 , min_reached = True\n", "[Cut(Action='CutTwoQubitGate', Gate=[8, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", "Subcircuits: ABCADEF \n", @@ -260,7 +260,7 @@ "\n", "for num_qpus in range(num_QPUs, 1, -1):\n", " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", - " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", + " print(f'\\n\\n--- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ---')\n", " \n", " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", " num_QPUs = num_QPUs)\n", From bc7d72ac3c3229b7662c56b4598e5a26eaba792c Mon Sep 17 00:00:00 2001 From: Ibrahim Date: Fri, 5 Jan 2024 10:09:29 -0600 Subject: [PATCH 7/8] Remove extraneous files. --- .../cut_finding/notebooks/Demo_notebook.ipynb | 374 ------------------ .../cutting/cut_finding/test/__init__.py | 1 - .../test/test_circuit_interface.py | 62 --- .../test/test_disjoint_subcircuits_state.py | 95 ----- .../test/test_optimization_settings.py | 21 - .../test/test_quantum_device_constraints.py | 10 - .../cutting/cut_finding/test/test_utils.py | 46 --- docs/circuit_cutting/tutorials/bell.qpy | Bin 142 -> 0 bytes 8 files changed, 609 deletions(-) delete mode 100644 circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb delete mode 100644 circuit_knitting/cutting/cut_finding/test/__init__.py delete mode 100644 circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py delete mode 100644 circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py delete mode 100644 circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py delete mode 100644 circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py delete mode 100644 circuit_knitting/cutting/cut_finding/test/test_utils.py delete mode 100644 docs/circuit_cutting/tutorials/bell.qpy diff --git a/circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb b/circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb deleted file mode 100644 index 666609cd9..000000000 --- a/circuit_knitting/cutting/cut_finding/notebooks/Demo_notebook.ipynb +++ /dev/null @@ -1,374 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import numpy as np\n", - "sys.path.insert(0, '..')\n", - "from qiskit import QuantumCircuit\n", - "from LO_gate_cut_optimizer.circuit_interface import SimpleGateList\n", - "from LO_gate_cut_optimizer.optimization_settings import OptimizationSettings\n", - "from LO_gate_cut_optimizer.lo_cuts_only_optimizer import LOCutsOnlyOptimizer\n", - "from LO_gate_cut_optimizer.quantum_device_constraints import DeviceConstraints\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Test of LO gate cut optimizer using Best-First Search (Dijkstra's algorithm)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "---------- 4 Qubits per QPU, 2 QPUs ----------\n", - " Gamma = 27.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[13, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 3, 6]])]\n", - "Subcircuits: AAAABBBB \n", - "\n", - "\n", - "\n", - "---------- 3 Qubits per QPU, 2 QPUs ----------\n", - " Gamma = 14348907.0 , min_reached = False\n", - "[Cut(Action='CutTwoQubitGate', Gate=[3, ['cx', 0, 3]]), Cut(Action='CutTwoQubitGate', Gate=[4, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[5, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 4, 7]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 5, 7]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 6, 7]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[13, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[18, ['cx', 0, 3]]), Cut(Action='CutTwoQubitGate', Gate=[19, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[20, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[24, ['cx', 4, 7]]), Cut(Action='CutTwoQubitGate', Gate=[25, ['cx', 5, 7]]), Cut(Action='CutTwoQubitGate', Gate=[26, ['cx', 6, 7]])]\n", - "Subcircuits: AAABCCCD \n", - "\n" - ] - } - ], - "source": [ - "#test circuit\n", - "circuit = [('cx', 0, 1), ('cx', 0, 2), ('cx', 1, 2),\n", - " ('cx', 0, 3), ('cx', 1, 3), ('cx', 2, 3),\n", - " \n", - " ('cx', 4, 5), ('cx', 4, 6), ('cx', 5, 6),\n", - " ('cx', 4, 7), ('cx', 5, 7), ('cx', 6, 7),\n", - " \n", - " \n", - " ('cx', 3, 4), ('cx', 3, 5), ('cx', 3, 6), \n", - " \n", - " \n", - " ('cx', 0, 1), ('cx', 0, 2), ('cx', 1, 2),\n", - " ('cx', 0, 3), ('cx', 1, 3), ('cx', 2, 3),\n", - " \n", - " ('cx', 4, 5), ('cx', 4, 6), ('cx', 5, 6),\n", - " ('cx', 4, 7), ('cx', 5, 7), ('cx', 6, 7),\n", - " ]\n", - "\n", - "interface = SimpleGateList(circuit)\n", - "\n", - "settings = OptimizationSettings(rand_seed = 12345)\n", - "\n", - "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", - "\n", - "qubits_per_QPU=4\n", - "num_QPUs = 2\n", - "\n", - "\n", - "\n", - "\n", - "for num_qpus in range(num_QPUs, 1, -1):\n", - " for qpu_qubits in range(qubits_per_QPU, 2, -1):\n", - " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", - " \n", - " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", - " num_QPUs = num_QPUs)\n", - "\n", - " op = LOCutsOnlyOptimizer(interface, \n", - " settings, \n", - " constraint_obj)\n", - " \n", - " out = op.optimize()\n", - "\n", - " print(' Gamma =', None if (out is None) else out.upperBoundGamma(),\n", - " ', min_reached =', op.minimumReached())\n", - " if (out is not None):\n", - " out.print(simple=True)\n", - " else:\n", - " print(out)\n", - "\n", - " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n", - "\n", - " \n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Cutting efficient SU(2) Circuit from Circuit Knitting Toolbox, tutorial 1." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAApQAAAD2CAYAAABobBdEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA4X0lEQVR4nO3de3wU9dn//9duTgRIwjnhrCABkShElINAEYsUQRQVod71VFsPKLcIxa+t3kqtP6v267cVRUpRQa1K+2hF1AqC4eBdRQUMEAQV5JQA4RQSkpCQhOzvjymBQEJ2M7uZmQ/v5+ORh8lkdva63M81uZjDZ3yBQCCAiIiIiEg9+Z0OQERERES8TQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYku00wGYZt0CKMl3OgqIbwa9x9rbhltygfDkYxLTPhu35KNxZja3jDNQ3Yh51FCGWUk+FB9yOorwMCkX05j22ZiWj7iTaePMtHzE23TKW0RERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtuinHIc/Nv4Ola18HwO/z0yKxLb27DuOua35Pq6T2DkcXOtPyMYlJn41JuYi7mTTWTMpF3EtHKB2Udv5g/vY/e3nr0V38+pa32bonk9+9Oc7psOrNtHxMYtJnY1Iu4m4mjTWTchF3UkPpoOioWFokptAqqT0XdxnCqH53s2nnKopLjzgdWr2Ylo9JTPpsTMpF3M2ksWZSLuJOaihd4mDBHj7N+gd+fxR+f5TT4dhmWj4mMemzMSkXcTeTxppJuYh76BpKB63ftoJrH21KIFDJsfISAG4aMpX42CYAPPnGTVyaejWj+t8NwNbdmTz99i38eXImsTGNHIu7NnXl8++sBby59LfVXrNr/yYmjnmBawfe1+DxnktMGmsaZ9JQVDeqGwmepxvK9evX8/jjj7NixQoCgQDDhg1j1qxZpKamMmrUKObPn+90iGfVo2M/Hp7wOmUVpaxc/3cyt3zCnT95qur3E697gYdmDmJQ2g0kxLfghXfv44HrX3LdjuqEuvIZlDaWQWknn8/12cb3eG3Rbxje93Ynwg1JIAC5BVBYCnHR0KEFRHno+L5JY83kcWYa1Y17qG4k0jzbUGZkZDB69Gg6d+7MY489Rnx8PPPmzWPkyJEUFRXRu3dvp0OsU1xMPO1bXQDA+Sm92HvoB156bxJTxs0BoFVSe24cMoW/fDiNHp360aFVKundrnIy5LOqK59THcjP4cUF9/P0XYtoFNu4oUMNWiAAq7fDis2wJ//k8sR4GNQNhvWEaA+cMTJprJk4zkwTCMDaHbB8E+zOP7k8MR4GdoOrekKM6qZBqW4k0jz0b8WTDhw4wPjx40lPTyczM5Np06bxwAMPkJGRwa5duwA80VCe7tbh0/l4zVy+y15TtWzMwPvZue8b/rb8Ge659nkHowtdTfkAVFZW8sw7P2PClY/Qpd3FDkVXt0AA3vsa3l5VvZkEOFICH22APy+DsgpHwrPFpLHm9XFmmkAA3s+Ev35evZkEq24Wb4BZGaobp6luJNw82VA+++yzHD58mLlz5xIfH1+1PCkpifT0dMCbDWWH1t0YcOG1zF38aNUyv9/P6P73cnmPa2jWtLWD0YWupnwA3sp4isaNErl+0CSHIgvO2h2w8tuzr7N1P3yQ2SDhhJVJY83r48w0mTth+eazr7PtALy3tmHiCSfVjUjtPNlQzp8/n8GDB5Oamlrj75OTk0lJSQGgoqKCBx98kBYtWtCsWTPuuusuSktLGzLckIwbOo213y9h/Q8rqpb5fH58Pk9+VGfks3H7Zyz+6lWm3TzX2cDqEAhYp7mD8cUPUFIW2XgiwaSx5tVxZqIVdfwj7ISvtkHxscjGEgmqG5Gaee4aytzcXHbv3s348ePP+F1lZSVZWVn06dOnatnTTz/N8uXLycrKIjY2ljFjxvDwww8zY8aMoN6voqKC3NzcoOMrL08GYupc7+EJ82pcftF5A1n6h0DQ71d7HOXk5OyzuY3gcoHg8ikqyefZ+bcybfw8Epu0DDEW+/mEIu9oNDmHU4Jat/w4fLohj4tSjkY4qlPeM8yfjb1YGm6smTbOTJNfEsWuQ22DWrei0qqbtLaqm/pvQ3Uj4ZeSkkJ0dOjtoecayuLiYgB8Pt8Zv1u4cCH79++vdrr7lVde4bnnnqN9e+vxUtOnT2fcuHH88Y9/JCqq7qvCc3Nz6dixY9DxzZm6kfNSLgp6/Uj5/vvv+dE9vWxtI9y5fLBqFnlH9jLr/YeqLb+67+3cOOShWl5lCUc+oWiXegXjHv930Ov/+vH/j8xF/y+CEVXnlnEG7htrXhpnpknp2o/xv/0i6PUf/91zrPng2QhGVJ3qpnaqGzkhOzubDh06hPw6zzWUHTt2JCoqipUrV1ZbvnPnTiZNsq75ONFQ5ufnk52dXa3BTE9Pp7CwkB07dtC1a9eGCtuWEZfdwYjL7nA6DNt+OuzX/HTYr50OIyhlJaE9PaK8tDBCkTQsE8aal8aZaUKtm7IS1Y1bqG7ELl8gELB/7L6B/fznP2fu3LmMGTOGUaNGkZ2dzZw5c0hOTmbDhg1s3ryZHj16kJ2dTadOndi7d2/VNZXl5eXExsaSmZkZ1I07oZ7y3vZRMmVHgjulEkmxieV0ucbeKQi35ALhyScUgQC88lUKBaVRwJlHw0/lI8A9/ffSNK6yYYLDvM/GLfk09DgzTSAAr61O5nBJNMHUzS/75ZLY6HjDBId7xhmobsS9zplT3gAzZswgJiaGhQsXsmzZMgYMGMCCBQt48skn2bp1a9XNOgkJCQAUFBRUNZT5+fnVfleX6OjokA79ZseAG+7PiImJqdch61O5JRcITz6hGloEC7+ue71LOvno0bVd5AM6hWmfjVvycWKcmWZoMSwI4g7uXh189LwguOstw8Ut4wxUN2Ie792WBjRt2pTZs2eTm5tLYWEhS5YsYcCAAWzcuJG0tDT8fiutZs2a0bFjR9atW1f12szMTBISEjjvvPOcCV48Y0h36FlHn9iyKdx4WcPEI+IFg1KhVx29RYsmMO7yholHRBqGJxvKmuTn55OTk3PGaexf/OIX/P73v2fPnj0cOHCA6dOnc8cddwR1Q46c26L88PMh1tNwGp12Vsnvgz6dYfIISHDfU9ZEHBPlhzsHW0/DqalueneCh0ZYT80REXN48pR3TbKysoAzJzT/zW9+w8GDB7nooouorKzkpptu4tlnG+6uQvG26CgY0wdGpMHqbfCP1dbyh34CHVs4G5uIW0X54do+cPVpdTN5BHQKbUYaEfEIY45Q1tZQRkdHM2PGDA4fPkxBQQGvvvpqtafrOO2jL1/hwZcGMnnmILbvzapxnamzhvKnf97bwJHVj2n5nBAXXf00nhePSpr02ZiUi8lOrxsvHpU0aayZlIu4jzEN5cSJEwkEAvTv39/pUIJ25GgeH66axfP3rWTquFd5eeGDZ6zzxaYPaRwX3A1ETjMtH5OY9NmYlIu4m0ljzaRcxJ2MaSi96LtdX3Fx16FER8XQsU13CooPUll5cuqZyspK3v98JmMG3u9glMEzLR+TmPTZmJSLuJtJY82kXMSd1FA6qLAkj4T45lU/x8clUFxaUPXzkrWvMyjtBmJjvHF+1bR8TGLSZ2NSLuJuJo01k3IRd1JD6aCm8c0pKsmv+rnkWCFNGiUBUFZeyrKv32JE3zsdii50puVjEpM+G5NyEXczaayZlIu4kzF3eXtRj079eGPJExw/XkHu4R0kNWlVNYfm3rztFJXm89hroyksySOvMJela95geN/bHI66dqblYxKTPhuTchF3M2msmZSLuJMaSgclNm7ByMt/wZRZQ/D5/EwaO5PV3y6msCSPYX1u4eUH1wCw/ocVLF833/XFbVo+JjHpszEpF3E3k8aaSbmIO3nyWd5utmouFB9yOgpo0hIG2Dx74ZZcIDz52JV/FKYvsL6fPhaaNXYuFtM+G7fk44ZxZhrVTc1UN2IaXUMpIiIiIraooRQRERERW3QNZZjFN3M6Aks44nBLLuCuWNzATf8/TBprbolDIsNNn6/qRkyjhjLMeo91OoLwMSkX05j22ZiWj7iTaePMtHzE23TKW0RERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtaihFRERExBY1lCIiIiJiixpKEREREbFFDaWIiIiI2KKGUkRERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtaihFRERExBY1lCIiIiJiixpKEREREbEl2ukATLNuAZTkOx0FxDeD3mPtbcMtuUB48hH3cstYU92Il7hlrKluBNRQhl1JPhQfcjqK8DApF3E3k8aaSbmIu5k01kzK5VylU94iIiIiYosaShERERGxRQ2liIiIiNiiayhFzqKoFLbsg+xDkHP45PJ/rYMLkqFrG2iV4Fh4Iq5UVApb98GuPMjJO7n8w3Un66a16kbEKGooRWqQfQhWfAvrdsHxyjN/v3q79QWQmgJDusNF7cHna9g4RdwkOw9WbobMWupmzXbrC6BbMgzuDmkdVDciJlBD6ZDn5t/B0rWvA+D3+WmR2JbeXYdx1zW/p1VSe4ejC50p+ZRVwKINsGIzBIJ8zfe51levDjDuckiKj2iI5zRTxtkJpuRTftyqm+WbIRBk4WzZZ331bAfj+0FS48jGeC4zZZydYFo+ptA1lA5KO38wf/ufvbz16C5+fcvbbN2Tye/eHOd0WPXm9XyOlMCfPv7PH8V6vH5jDjz3L9ilqS8iyuvj7HRez6ewBF74GJZtCr6ZPNWmPfDsv2DHwfDHJid5fZydzrR8TKCG0kHRUbG0SEyhVVJ7Lu4yhFH97mbTzlUUlx5xOrR68XI+RaXw0iewJ7/2dfw+6+hjUrz1fU2Kj8HLGdapP4kML4+zmng5n+JjMDOj+vXFpwumbo6WwawM/WMskrw8zmpiWj4mUEPpEgcL9vBp1j/w+6Pw+6OcDsc2L+UTCMDbq2B/HfuhhEbw2xusr4RGta9XWg6vfWr9VyLLS+MsGF7KJxCAd76A3IKzrxds3RyrsOqmpCy8ccqZvDTOgmFaPl6laygdtH7bCq59tCmBQCXHyksAuGnIVOJjmwDw5Bs3cWnq1YzqfzcAW3dn8vTbt/DnyZnExpxlz+yQuvL5d9YC3lz622qv2bV/ExPHvMC1A+9r8HhP+GqbddotnA4Xw/uZcPPl4d2uqG7AHXWzdod1mUc45R+FhV/DhP7h3a6obsAddWMyTzeU69ev5/HHH2fFihUEAgGGDRvGrFmzSE1NZdSoUcyfP9/pEM+qR8d+PDzhdcoqSlm5/u9kbvmEO3/yVNXvJ173Ag/NHMSgtBtIiG/BC+/exwPXv+TK4oa68xmUNpZBaScfkPrZxvd4bdFvGN73difCBaDiuDWVSSR8vgWG9oA2iZHZfn0dLrZi23kIKgPQJgEGdIOOLZyOLDiqG+fr5nglfJAZmW1/8QMMvRBSkiKz/frKP2rVzY6DVt20ToABF0Cnlk5HFhzVjfN1YzrPNpQZGRmMHj2azp0789hjjxEfH8+8efMYOXIkRUVF9O7d2+kQ6xQXE0/7VhcAcH5KL/Ye+oGX3pvElHFzAGiV1J4bh0zhLx9Oo0enfnRolUp6t6ucDPms6srnVAfyc3hxwf08fdciGsU6d3vnhmwoLI3c9j/bAmMvjdz2QxEIwEfr4ZNvqt90tHUffL7Vukv91oEQF+NYiEFR3ThfNxtzoKAkctv/bAvc2Ddy2w9FIACLs2DJxuo3HW3dB6u2WtOF3XoFNFLdNCgv1o3pPHkN5YEDBxg/fjzp6elkZmYybdo0HnjgATIyMti1axeAJxrK0906fDofr5nLd9lrqpaNGXg/O/d9w9+WP8M91z7vYHShqykfgMrKSp5552dMuPIRurS72KHoLCfmxIvk9utz52skLNoAS7+p/Q72jTnw2v9CZQ3zB7qZ6qbhrW6Auql0Sd18vBE+zqq9jr/ZDa+urHneTTdT3Ui4ebKhfPbZZzl8+DBz584lPv7kpH9JSUmkp6cD3mwoO7TuxoALr2Xu4kerlvn9fkb3v5fLe1xDs6atHYwudDXlA/BWxlM0bpTI9YMmORSZJRCwTvtGUvExOFQU2fcIRv5Rq5msy3d7rT+QXqK6aXi7IjzFT0kZHCiM7HsE40gJLMmqe70t+yArzNeTRprqRsLNk6e858+fz+DBg0lNTa3x98nJyaSkpADw97//nRkzZrBu3TpatWrFjh07QnqviooKcnNzg16/vDwZqP+5j3FDpzF55hWs/2EFl3QdCoDP58fnC633Ly8vJydnX73jsLZhLxc4M5+N2z9j8VevMmvy1yHGYj+f0x0pjaL4WNtqy/y+2u9ETYyv+ftTFZaeeWRl/dZDdG8dwfODQfhsRyKBQDAXcwbIyDpGc1/DTgqouqnOzXVTdMzPkdJ21ZZFom42bDnEhcnO1s0XOxOoDARzMadVN638qpv6Mr1uvCQlJYXo6NDbQ18g4JYTcsHJzc2lbdu2TJkyheefr35IvrKykrZt29KnTx8WL14MwNKlSzl06BD79u3jj3/8Y8gNZU5ODh07dgx6/TlTN3JeykUhvUddPl49j+9z1jBp7EtBv2ZH7jf88vlett433LkUleRz35/SmTruVXpfcGVIrw1HPqdrc/6l/PR31U+PJMVb05vU1xPvnnlt2fJ597Phk5frv9EwGPOrDznvkpFB/aEoLT7M7Hsa9g4d1U3t3FY3rTpdwn89va7askjUzco3J7Pu4xfqv9EwGD15AV0uvQ5fEM+GLCstYtYvGvYB5aqb2rmtbrwkOzubDh06hPw6zx2hLC4uBqixwBcuXMj+/furne4ePnw4AO+9915DhCdn8cGqWeQd2cus9x+qtvzqvrdz45CHanlV5ATzRyI87+P8lSW+EOZm0zxu7nLO1o1fdSP157a6ORd47ghlWVkZjRs3pk+fPqxevbpq+c6dO7niiivYvXs377zzDhMmTKj2uvfee4/JkydH/JT3to+SKTvi/O1+sYnldLnG3iF7t+QC4cnndIePRvPq6pRqy+o6dTd1pPX984us66tOV9Opu2t65NEz+WgYIq6/5VuTWLs7mKMnAVISyvlZ+v6Ix3Qqt4w11U3d8kuieOWr0C4VqU/djOieR1qKs3Wz4ock1uQEVzdtmpZz26Wqm/pySy4Qmbrxkvqe8vbcEcrY2Fhuu+025s6dy3XXXceoUaPIzs5mzpw5JCcns3v37rDekBMdHR3Sod/sGHDDgx5iYmLqdcj6VG7JBcKTz+naBSAu03pCxwmVgeCmQzlSEvy0KWldW9C2mbOTPA5PgLVB3Wzj48qLYsP+/7oubhlrqpu6tQ9Ao8zqT4KKSN10aUGHFs7WzdWJsCaom218DO2purHDLblAZOrmXOD8OYV6mDFjBnfffTdffvklU6dO5csvv2TBggW0a9eOxo0b13qzjsip/D7oEOG/V7FR7pjYPCUJLj2v7vWSE6FP54iHIx7m80V+Mu+YKGjbLLLvEYw2iXBZl7rXa50Al54f+XhE3MxzRygBmjZtyuzZs5k9e3a15Rs3biQtLQ2/C669EW/o0xl+iOBZqks6Q5RLhuOE/lBWUfv0JslJcN8wiPXkXkEaUp/O8H3wVwKF7OKO7qmb8ZdDWTmsz675920S4d4rIU51I+c4Y0ogPz+fnJwcRo0aVW358ePHKS8vp7y8nEAgQGlpKT6fj7i4OIciFTfpe771CLlTT3uH06BukdlufcREwZ1DrEZg+Sb47j8NQccWMLg79O6kZlKCk36e9cztU097h9MgF51kio6C2wfDllxYvhm+3Wst79ACBqdazbXqRsSjp7xrkpVlzT57+vWTb775JvHx8dx8883s2rWL+Ph4unfv7kCENfvoy1d48KWBTJ45iO17a55Bd+qsofzpn/c2cGT147V8GsXAlT0js+2e7dz3nF+/D3q0hZ8OOLnsrh/B5V289UfRa+OsLl7LJy4aropQ3XRvC+e1isy268vvs+Ka0P/ksl/8CPp1Vd04ybR8vM74hvKOO+4gEAhU+wr1Tu9IOXI0jw9XzeL5+1YyddyrvLzwwTPW+WLThzSOa9i5zerLq/kMvwjaNw/vNhvFwM39rOvNJLy8Os5q49V8hvW0jm6HU1w0TFDdRIRXx1ltTMvHBMY0lBMnTiQQCNC/f/+6V3aJ73Z9xcVdhxIdFUPHNt0pKD5I5SkPUq6srOT9z2cyZuD9DkYZPK/mE+WH266AJnVcBVFYak3A/MS71ve18fvgvwZAs8bhjVMsXh1ntfFqPifqpmmY6sbng1sGQPMm4Y1TLF4dZ7UxLR8TGNNQelFhSR4J8ScPjcXHJVBcWlD185K1rzMo7QZiY2qZ4M1lvJzPiRtSzvbH8cTUKAUlZ86Zd4LfB7deAWnBP1xJQuTlcVYTL+fTOhEmXlX7HJQQfN381wC4pFNk4hRvj7OamJaPCdRQOqhpfHOKSvKrfi45VkiTRtZzY8vKS1n29VuM6HunQ9GFzuv5dGhhTcDco23d69YkORH++2pNuxNpXh9np/N6Pu2aW3VzYbu6161J6wSYNNy6QU4ix+vj7HSm5WMCD11ObJ4enfrxxpInOH68gtzDO0hq0qpqyqO9edspKs3nsddGU1iSR15hLkvXvMHwvrc5HHXtTMineRO450pYsx2WbYa9+XW/JikerkiFKy+07qSWyDJhnJ3KhHyaNYa7h8LaHVbd7Dlc92sS4+GKblbdeOnGFq8yYZydyrR8TKAydlBi4xaMvPwXTJk1BJ/Pz6SxM1n97WIKS/IY1ucWXn5wDQDrf1jB8nXzXV8MpuTj81mTGfc9H7YfsKYJyc6DfQVQdhyi/dCyqXVEs2sbuKi9e+bMOxeYMs5OMCUfn8+qmUvPgx0HYfMeyMmD3P/UTZTPqpuOLVU3TjBlnJ1gWj4m8NyzvN1u1VwoPuR0FNCkJQywebTfLblAePKRk/KPwvQF1vfTxzp/A5FbxprqRs5GdVMz1Y2ArqEUEREREZvUUIqIiIiILWooRURERMQW3ZQTZvHNnI7AEo443JILuCsWCT+3fL6qG/ESt3y+qhsBNZRh13us0xGEj0m5iLuZNNZMykXczaSxZlIu5yqd8hYRERERW9RQioiIiIgtaihFRERExBY1lCIiIiJiixpKEREREbFFDaWIiIiI2KKGUkRERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtaihFRERExBY1lCIiIiJiixpKEREREbFFDaWIiIiI2KKGUkRERERsiXY6ANOsWwAl+U5HAfHNoPdYe9twSy4QnnxEGoJpdeOWfLQPMJtbxhmobupLDWWYleRD8SGnowgPk3IRaSim1Y1p+Yg7mTbOTMsnGDrlLSIiIiK2qKEUEREREVvUUIqcYwIBOFx88ucDR+B4pXPxiHjB6XWzX3UjUo2uoRQ5B1Qchw3Z8NU22HUIjpad/N3MDIiJgvbNoXdnuPx8aBznXKwiblFxHLJyrLrZebB63bz8n7pp1xx6d4LLu0AT1Y2cw9RQOuS5+XewdO3rAPh9floktqV312Hcdc3vaZXU3uHoQmdaPqYIBKw/hh+ug8LS2tcrPw47Dlpf/1oHP+oBP0mD6KiGivTcZFLdmJRLIABrtsMHmXCkjrrZedD6+mg9DO4OIy+2Gk2JHJPGmkm56JS3g9LOH8zf/mcvbz26i1/f8jZb92TyuzfHOR1WvZmWj9cVlcKcFfDOF2dvJk9Xfhw++Qb+7yLYczhi4cl/mFQ3JuRSfAxe/RTeWnX2ZvJ05cdh2Sb4w0eQkxe5+MRiwlg7wZRc1FA6KDoqlhaJKbRKas/FXYYwqt/dbNq5iuLSI06HVi+m5eNlhSXw4lLYtKf+28gtgBlLraOWEjkm1Y3XcykqhZeWwsac+m9j/xGr9rYfCF9cciavj7VTmZKLGkqXOFiwh0+z/oHfH4Xf7/3zJabl4yXlx+HPy2HfWfZFfh8kxVtffl/t65WWw1+Ww8HC8McpZzKpbryWS8VxmL0c9hbUvk6wdXOswtrWfm/1A57ltbF2Nl7ORddQOmj9thVc+2hTAoFKjpWXAHDTkKnExzYB4Mk3buLS1KsZ1f9uALbuzuTpt2/hz5MziY1p5Fjctakrn39nLeDNpb+t9ppd+zcxccwLXDvwvgaP11SLN8DuOk5VJzSC395gff/Eu1BQUvu6R8us0+b3//jsf0SlfkzaD3h5H/BxFmTXcao6lLopLbfqZtKPwa9DN2GnunFH3ZzK0w3l+vXrefzxx1mxYgWBQIBhw4Yxa9YsUlNTGTVqFPPnz3c6xLPq0bEfD094nbKKUlau/zuZWz7hzp88VfX7ide9wEMzBzEo7QYS4lvwwrv38cD1L7muGE6oK59BaWMZlHbyGVCfbXyP1xb9huF9b3ciXCPtPgzLNod/uz/shy+2wsBu4d+2XYGAdTS2sBQaRVt33UZ56A+4SfsBr+4D9uZDxqbwb3f7Afh8KwxKDf+2w2HfEThSAnHR1iwPqhtneLVuTufZhjIjI4PRo0fTuXNnHnvsMeLj45k3bx4jR46kqKiI3r17Ox1ineJi4mnf6gIAzk/pxd5DP/DSe5OYMm4OAK2S2nPjkCn85cNp9OjUjw6tUknvdpWTIZ9VXfmc6kB+Di8uuJ+n71pEo9jGDR2qsVZ+azVYkbB8Mwy4AHwuOUoZCMDaHbBiM+ScckQ2KR6uSIVhF3rjLnWT9gNe3Qes/BYqI1Q3KzZb/xBz09H9E3Vz6hHZxHi4ohsM6+mNu9RVN87Xzek89O+Rkw4cOMD48eNJT08nMzOTadOm8cADD5CRkcGuXbsAPNFQnu7W4dP5eM1cvsteU7VszMD72bnvG/62/BnuufZ5B6MLXU35AFRWVvLMOz9jwpWP0KXdxQ5FZ57iY5C5M3LbP1AIW/ZFbvuhCASsKV3++nn1ZhKs05AfrbeuIy2rcCY+O0zaD3hhH3C0zGqwIuVgEXy3N3LbD9WH6+DNz848vX+kBBZtgFnLVDdO80Ld1MSTDeWzzz7L4cOHmTt3LvHx8VXLk5KSSE9PB7zZUHZo3Y0BF17L3MWPVi3z+/2M7n8vl/e4hmZNWzsYXehqygfgrYynaNwokesHTXIoMjP9sN+6ISeSvrVx13g4Ze6s+9T+1n3w/tcNE084mbQf8MI+YHtD1I1LGsr1u6wpwc5m235YsLZh4gkn1Y3zPNlQzp8/n8GDB5OaWvOFKcnJyaSkpHDs2DF++ctf0qVLFxISEkhNTeXFF19s4GhDM27oNNZ+v4T1P6yoWubz+fH5PPlRnZHPxu2fsfirV5l281xnAzNQ9qEGeA+XzK+34tvg1vtiW/Wnm3iFSfsBt+8DGmJMN0RtBiPYulm9zTrj4TWqG2d57hrK3Nxcdu/ezfjx48/4XWVlJVlZWfTp0weAiooKUlJSWLJkCV26dGHDhg2MGDGC5ORkbr755qDer6Kigtzc3KDjKy9PBmLqXO/hCfNqXH7ReQNZ+gf7F/OUl5eTk2Pv/GSwuUBw+RSV5PPs/FuZNn4eiU1ahhiL/XxMt2NfC+DkNTV+n3VXak0S42v+/nSFpdWvLdtz+Dg5Oc4ebjlcEsWuQ22DWrfiOHy6IY9eKUcjHNVJ4a4be7E03H7Aq/uA7bmRr5u9+c7XTUFpFNsPBFk3lbByQx4Xt1Xd1H8b3q2blJQUoqNDbw8911AWFxcD4KvhzoCFCxeyf//+qtPdTZo04Xe/+13V73v37s2YMWP497//HXRDmZubS8eOHYOOb87UjZyXclHQ60fK999/z4/u6WVrG+HO5YNVs8g7spdZ7z9UbfnVfW/nxiEP1fIqSzjyMd2YX33I+b1HVf186hQnZzN1ZO2/O31qlEOHj4RUD5GQckF/xk9fFfT6j01/hrX/+kMEI6rOLfsAcN9+wI37gNEPvUfXS6+r+jkSdVNQWOJ43SR36cuEJ1cHvf70p/4vq9//fQQjqk51U7uGrpvs7Gw6dOgQcpyeayg7duxIVFQUK1eurLZ8586dTJpkXVdQ2/WT5eXl/O///i+/+tWvIh1mWI247A5GXHaH02HY9tNhv+anw37tdBjGqqwob4D3cP78cXlpaLOsl5WYMbu0CfsBN+4DKo+fG3VzLMQ6KCsx42kGqpuG4wsEIjXJSOT8/Oc/Z+7cuYwZM4ZRo0aRnZ3NnDlzSE5OZsOGDWzevJkePXqc8bp77rmHr7/+ms8++4zY2Nig3ivUU97bPkqm7Ehwh+0jKTaxnC7X2Dtk75ZcIDz5mG75D0mszUmo+rmuU3cnjrA8v8i6w7Mmp5+6a5d4jFv6OPtMuUAAXvkqhYLSKODsc7H4CHB3/1wS4iJ818UpTKsbt+QTqX3Aym1JrM6ObN0kJ5Rxa/r+MEVcP4EAvLY6mcMl0dRVNxDgl/1ySWqkuqkvt+RTn1zOmVPeADNmzCAmJoaFCxeybNkyBgwYwIIFC3jyySfZunVrjTfrTJkyhVWrVrFs2bKgm0mA6OjokA79ZseA8/8WhZiYmHodsj6VW3KB8ORjup7lsPaUZxBXBs7+JI8TjpQEtx5A15Q4V3wOVxbBe0HcwZ3W0ceFXYO7bixcTKsbt+QTqX1Az+OwOvvkzxGpm+RYV9TNsKPwzzV1r3dRex8XXaC6scMt+TTk305PNpRNmzZl9uzZzJ49u9ryjRs3kpaWhv+051xNnjyZjIwMli1bRqtWrRoyVJEG06WNddwhkqccuiZHcOMhGNwdvs+FTWeZxqhFE7jpsoaLSbypS2trsv5InqtzS90M7Abf5cLGnNrXadYYbr684WISc3jvXvpa5Ofnk5OTc8b1k//93//NJ598wrJly2jd2jvzUImEqnkT6Nk+cttPaAS9Irj9UET54edDrKfhxJ32z2K/D3p3gskjzn4nrghAUuPIjusmcXCJs/fjVInyw52D4aqe0Oi0s7E+nxXnQz+x/p+IhMqTRyhrkpWVBVS/IWfnzp28+OKLxMXFcf7551ctHzx4MIsWLWroEEUibkh3+GZ3ZLY9sJu7HmUYHQVj0mFEGny1Hf75nxtYJ4+ATqHNrCHnuCHdIessR+3sGHiBu+omyg/X9oGr06z5Jv/xn7p5SHUjNhlzhLKmhrJz584EAgFKS0spKiqq+nJTM/nRl6/w4EsDmTxzENv3ZtW4ztRZQ/nTP+9t4Mjqx7R8vKZ7W+jTOfzbbZ1gHdVwo7gYSDvlEiEvHpU0qW68mEu3FLj0vPBvt1VT+LFLZzuLi4ZeqhvXMCEXYxrKiRMnEggE6N+/v9OhBO3I0Tw+XDWL5+9bydRxr/LywgfPWOeLTR/SOC6hhle7j2n5eNWNfSGxlrtUTygstebKe+Jd6/uz8fvglgEQa8z5DHcxqW68nMsNfSGpjqYq1Lr5af8zL8mQ8PDyWDudKbkY01B60Xe7vuLirkOJjoqhY5vuFBQfpLKysur3lZWVvP/5TMYMvN/BKINnWj5e1bQR3DsMGp9lMoMTd7IWlFSf3uR0Ph/8bCCcr8uPI8akuvFyLk3irLppElf7OkHXDVYz6ZabcUzk5bF2OlNyUUPpoMKSPBLim1f9HB+XQHFpQdXPS9a+zqC0G4iNqeNwk0uYlo+XtWsOk4ZDKxv/oG0UY13An35e2MKSGphUN17PpW0zq25a26yb2wfDZV3CFpbUwOtj7VSm5KKG0kFN45tTVJJf9XPJsUKaNEoCoKy8lGVfv8WIvnc6FF3oTMvH69o2g4evgR/1qHsa49P1bAePjIaLXXJ3qslMqhsTcklJgmnXwJUXWkfoQ9GjLfyfUdYsAxJZJoy1E0zJRVd3OKhHp368seQJjh+vIPfwDpKatKqaQ3Nv3naKSvN57LXRFJbkkVeYy9I1bzC8720OR1070/IxQWw0jL3Uuov18y3WXZ1Harn2Ky4aLukEg1J1t2dDMqluTMklNhquS4fBqbBqK3y5rfan4sRGW9PtnKibUJtQqR9TxhqYk4saSgclNm7ByMt/wZRZQ/D5/EwaO5PV3y6msCSPYX1u4eUHrUcarP9hBcvXzXflADqVafmYpGVTa6qQ0b0h/yhk51k3FQQCEB8L7ZtDmwTw65xFgzOpbkzKBaBFUxjVG665xLpuMvvQyccqqm6cZdJYMyUXTz7L281WzYXiQ05HAU1awgCbR8jdkguEJx8xV/5RmL7A+n76WOtpH04xrW7cko/2AeGnuqmZ6qZ+9O8qEREREbFFDaWIiIiI2KJrKMMsvpnTEVjCEYdbcgF3xSJyNm4aqybtB9wSh0SGmz5f1U39qKEMs95jnY4gfEzKRaShmFY3puUj7mTaODMtn2DolLeIiIiI2KKGUkRERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtaihFRERExBY1lCIiIiJiixpKEREREbFFDaWIiIiI2KKGUkRERERsUUMpIiIiIraooRQRERERW9RQioiIiIgtaihFRERExBY1lCIiIiJiS7TTAZhm3QIoyXc6CohvBr3H2tuGW3KB8OQj0hBMqxu35KN9gNncMs5AdVNfaijDrCQfig85HUV4mJSLSEMxrW5My0fcybRxZlo+wdApbxERERGxRQ2liIiIiNiihlJEREREbNE1lCLiScXHYMs+yD4EOYdPLv9oPXRtY321SnAuPhE3Kj4GW/dBdp71dcK/1sMFbaBLG2itupF6UEMpIp6SkwcrvoV1O6Gi8szff7XN+gLongKDu8NF7cHna9g4Rdxk92GrbjJ31Fw3q7dZXwCpKTA4FXp1UN1I8NRQOuS5+XewdO3rAPh9floktqV312Hcdc3vaZXU3uHoQmdaPuI+5cdh0QZYvhkCgeBe812u9dWrA9x8OSTGRzbGUJlUNyblYpKK4/BxFmRsgsog6+b7XOvrovZW3SQ1jmyMoTJprJmUi66hdFDa+YP52//s5a1Hd/HrW95m655MfvfmOKfDqjfT8hH3KCyFFz6GZZuCbyZPtTEHnv0X7HLhNB4m1Y1JuZigqBReWAJLvwm+mTzVN7ututlxMPyx2WXSWDMlFzWUDoqOiqVFYgqtktpzcZchjOp3N5t2rqK49IjTodWLafmIOxQfg5mfVL9O8nR+HyTFW1/+Wk7RFR+DlzOqXzfmBibVjUm5eN3RIMZ7MHVztAxmZbjvH2MmjTVTclFD6RIHC/bwadY/8Puj8PujnA7HNtPyEWcEAvD2KsgtOPt6CY3gtzdYXwmNal+vtBxe+9T6rxuZVDcm5eI1gQDM/xL25J99vWDr5liFVTclZWENM2xMGmtezkXXUDpo/bYVXPtoUwKBSo6VlwBw05CpxMc2AeDJN27i0tSrGdX/bgC27s7k6bdv4c+TM4mNOUv1O6SufP6dtYA3l/622mt27d/ExDEvcO3A+xo8XnG/tTus027hdLgY3s+0rg1zA5P2A9oHuEPmTtiQHd5t5h+FhV/DhP7h3W59qW7cVzeebijXr1/P448/zooVKwgEAgwbNoxZs2aRmprKqFGjmD9/vtMhnlWPjv14eMLrlFWUsnL938nc8gl3/uSpqt9PvO4FHpo5iEFpN5AQ34IX3r2PB65/yXXFcEJd+QxKG8ugtJMPFf1s43u8tug3DO97uxPhissdr4QPMiOz7c+3wNAe0CYxMtsPhUn7Ae0DnFdZaf2DKRK++AGGXggpSZHZfihUN+6rG882lBkZGYwePZrOnTvz2GOPER8fz7x58xg5ciRFRUX07t3b6RDrFBcTT/tWFwBwfkov9h76gZfem8SUcXMAaJXUnhuHTOEvH06jR6d+dGiVSnq3q5wM+azqyudUB/JzeHHB/Tx91yIaxbrsFkJxhY05UFASue1/tgXGXhq57QfLpP2A9gHO27THOpoYKZ99DzdeFrntB0t147668eQ1lAcOHGD8+PGkp6eTmZnJtGnTeOCBB8jIyGDXrl0AnmgoT3fr8Ol8vGYu32WvqVo2ZuD97Nz3DX9b/gz3XPu8g9GFrqZ8ACorK3nmnZ8x4cpH6NLuYoeiE7c7MZdkpKzeVr87XyPNpP2A9gENL+J1s906Cuo2qhvnebKhfPbZZzl8+DBz584lPv7kxHJJSUmkp6cD3mwoO7TuxoALr2Xu4kerlvn9fkb3v5fLe1xDs6atHYwudDXlA/BWxlM0bpTI9YMmORSZuF0gADsjfFfp0TI4VBTZ96gPk/YD2gc0vJ0RnuKntBz2F0b2PepDdeM8T57ynj9/PoMHDyY1NbXG3ycnJ5OSkgLAxIkT+eCDDygoKCAhIYFx48bx3HPPERsbG9R7VVRUkJubG3Rs5eXJQEzQ659u3NBpTJ55Bet/WMElXYcC4PP58flC6/3Ly8vJydlX7zisbdjLBc7MZ+P2z1j81avMmvx1iLHYz0e8o/CYn6LSdtWW+X2134l66oTltU1eXlh65hHJ9VsO0aNNeM+rR6JuwLv7Ae0DGk5xmZ+CksjXzYateVQkh/e8uuqmOifrJiUlhejo0NtDXyBQn2mCnZObm0vbtm2ZMmUKzz9f/RB2ZWUlbdu2pU+fPixevBiATZs20blzZ5o0acLBgwcZN24cP/rRj5g+fXpQ75eTk0PHjh2Djm/O1I2cl3JR0OsH4+PV8/g+Zw2Txr4U9Gt25H7DL5/vZet9w51LUUk+9/0pnanjXqX3BVeG9Npw5CPe0fq8PtzyVPUdZ1K8Nb1JfT3x7pnXZK54fRLrlwZfV8GIxD4AzNgPaB8QWS079OJnz2RVWxaJuvn0rSlkLvpj/TdaA9VN7Rq6brKzs+nQoUNIrwEPHqEsLi4GwFfDA0YXLlzI/v37q53u7tmzZ9X3gUAAv9/Pli1bIh6nnOmDVbPIO7KXWe8/VG351X1v58YhD9XyKjkX+WiYBwj7/J686seztA+IrJr+LkbmfVQ3DckrdeO5I5RlZWU0btyYPn36sHr16qrlO3fu5IorrmD37t288847TJgwoep3zzzzDE899RTFxcW0bNmSRYsWcdllwd2mFuop720fJVN2xN5h+3CITSynyzX2Dtm7JRcITz7iHfklUbzyVdtqy+o6dTd1pPX984vgSA1nsWs6dfeT7nn0SgnvqTvT6sYt+WgfULcjpVH85cvI182I1DzS2qpuzsYt+dQnl/qe8vbcEcrY2Fhuu+025s6dy3XXXceoUaPIzs5mzpw5JCcns3v37jNuyHnkkUd45JFH2Lx5M2+99RZt27ateeM1iI6ODunQb3YMuOFhAjExMfU6ZH0qt+QC4clHvKNdAOK+tp7QcUJlILhphI6UBD/dUFrXFrRv3qJ+QdbCtLpxSz7aB9QtEIDGmdYNZydEom56dWlBh5aqm7NxSz4NWTeePG49Y8YM7r77br788kumTp3Kl19+yYIFC2jXrh2NGzeu9WadCy+8kEsuuYRbb721gSMWkVD4fdCxZWTfIybKHRM0i4SLzwcdw9vnnSHKD22bRfY9xJs8d4QSoGnTpsyePZvZs2dXW75x40bS0tLwn+W6qPLycr7//vtIhygiNvXpBFsjeIbzko7WH0cRk/TpDN8Ff5VWyC7uCNHeesS0NBBjdqf5+fnk5ORUO91dUFDAvHnzyM/PJxAIsGHDBp566ilGjBjhXKAiEpRLz4e4CP6T94qaT2SIeFr6edAogpfuDeoWuW2LtxnTUGZlWVMlnNpQ+nw+/vrXv9KlSxcSEhK4/vrrueaaa3jxxRcdivJMH335Cg++NJDJMwexfW9WjetMnTWUP/3z3gaOrH5My0ec0ygGrupZ93r10aMtnNcqMtuuD5PqxqRcvCg2Gn4c/tl3AEhNgS5tIrPt+jBprJmQiydPedekpoYyMTGRTz75xKGI6nbkaB4frprFjElfsPfQNma8ex9/uHdZtXW+2PQhjeMSHIowNKblI8676iLYkA05h8O3zUYxML6fdb2ZG5hUNybl4mVXXmjVza4wPm0qLlp1Eymm5GLMEcqJEycSCATo37+/06EE7btdX3Fx16FER8XQsU13CooPUnnKQ1IrKyt5//OZjBl4v4NRBs+0fMR5UX649QpoEnf29QpLrQmYn3jX+r42Ph/8tD80bxLeOO0wqW5MysXLovxw60BoGq66ASb0h5ZNwxqmLSaNNVNyMaah9KLCkjwS4ptX/Rwfl0BxaUHVz0vWvs6gtBuIjallEjGXMS0fcYfkJLhv2Nn/OJ6YGqWg5Mw5807w++BnA+CSTpGJs75MqhuTcvG61okw8ara56CE4OvmpwOsm33cxKSxZkouaigd1DS+OUUl+VU/lxwrpEkjax6TsvJSln39FiP63ulQdKEzLR9xjw4trAmYu6fU7/WtE2DScOtGH7cxqW5MysUE7ZpbddMj+KmXq2mVAPf/GC7vEt64wsGksWZKLsZcQ+lFPTr1440lT3D8eAW5h3eQ1KRV1ZRHe/O2U1Saz2OvjaawJI+8wlyWrnmD4X1vczjq2pmWj7hL8yZw7zBYvR2Wb4K9BXW/JrERDOwGw3paNyu4kUl1Y1IupmjWGO65EtbugGWbYE9+3a9J+E/dXKW6aRCm5OK5Ry+63aq5UBzChdD/+uIvLFkzD5/Pz6SxM8k7spfCkjyG9bmlap31P6xg+br5TL7xz0Fvt0lLGGDzHzSh5gLuzkfMEQjAtgPw7R7IzoN9BVB2HKL90KKpNblz1zbQq0PDzzVpWt2YtE871wUCsP0AbD6tbqL80LIJdGj5n7pp3/BzTapu3JtLsNRQhll9iiISnGooI0V/TMQrTKsbt+SjfYDZ3DLOQHVTX7qGUkRERERsUUMpIiIiIraooRQRERERW1x6/5Z3xTdzOgJLOOJwSy7grlhEzsZNY9Wk/YBb4pDIcNPnq7qpH92UIyIiIiK26JS3iIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMQWNZQiIiIiYosaShERERGxRQ2liIiIiNiihlJEREREbFFDKSIiIiK2qKEUEREREVvUUIqIiIiILWooRURERMSW/x/BpNnG91G9nQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library import EfficientSU2\n", - "from LO_gate_cut_optimizer.utils import QCtoCCOCircuit\n", - "\n", - "qc = EfficientSU2(4, entanglement=\"linear\", reps=2).decompose()\n", - "qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)\n", - "\n", - "circuit_ckt_su2=QCtoCCOCircuit(qc)\n", - "\n", - "qc.draw(\"mpl\", scale=0.8)\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "---------- 4 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 1.0 , min_reached = True\n", - "[]\n", - "Subcircuits: AAAA \n", - "\n", - "\n", - "\n", - "---------- 3 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 9.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[17, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[25, ['cx', 2, 3]])]\n", - "Subcircuits: AAAB \n", - "\n", - "\n", - "\n", - "---------- 2 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 9.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 1, 2]]), Cut(Action='CutTwoQubitGate', Gate=[20, ['cx', 1, 2]])]\n", - "Subcircuits: AABB \n", - "\n" - ] - } - ], - "source": [ - "interface = SimpleGateList(circuit_ckt_su2)\n", - "\n", - "settings = OptimizationSettings(rand_seed = 12345)\n", - "\n", - "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", - "\n", - "\n", - "qubits_per_QPU=4\n", - "num_QPUs=2\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for num_qpus in range(num_QPUs, 1, -1):\n", - " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", - " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", - " \n", - " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", - " num_QPUs = num_QPUs)\n", - "\n", - " op = LOCutsOnlyOptimizer(interface, \n", - " settings, \n", - " constraint_obj)\n", - " \n", - " out = op.optimize()\n", - "\n", - " print('Gamma =', None if (out is None) else out.upperBoundGamma(),\n", - " ', min_reached =', op.minimumReached())\n", - " if (out is not None):\n", - " out.print(simple=True)\n", - " else:\n", - " print(out)\n", - " \n", - " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Cutting 7 qubit circuit from Circuit Knitting Toolbox, tutorial 3." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvQAAAHwCAYAAADJpfudAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABk5ElEQVR4nO3deXhU5f3//+dMFhKSEJagARLWJAKBJAIiBBWDsRURBCtuSKtflIogtCLpYq3667cqSBVxhdrW+v18immlWpAiqIBiBAyyR/YQJMugIWELAbLM748jkUACmWFmzpzJ63FduULOnOU9Q3LmNfe5z33bnE6nExERERERsSS72QWIiIiIiIj7FOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCFOhFRERERCxMgV5ERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTIFeRERERMTCgs0uQBrmdEJtldlVNJ09BGw2s6sQERERaX4U6P1UbRWsnGt2FU2XMRWCQs2uQkRERKT5UZcbERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6EREREREL0zj0AWTz3lU89kZGvWVhoRHEtU8is994Rg95hKAg/ZeLiIiIBBKluwCUkXY3A3vejBMn5cccfPTV27yx+FG++XY7v7x9vtnliYiIiIgHKdAHoMRO/cjsf2/dzyPTH2bCrJ4s/fJN7r/pj7SObG9idSIiIiLiSepD3wyEh0bQs8sgnE4nxYf2ml2OiIiIiHiQAn0zUfJ9kG/Vsq3JlYiIiIiIJ6nLTQA6WXWCIxWlOJ1GH/rFa95gT9FGesYPJK59ktnliYiIiIgHNYsW+tLSUrKyskhISCAs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dmmmahnavLtRBNmte/nUE2w242qEp28cFhHrCA4yrsg1VzabEd6b85XI8FD/HX70UinQNxNvPpZndgkiIiIi4gW62CwiIiIiYmEK9CIiIiIiFqZALyIiIiJiYepD76fsIZAx1ewqms7ux1NCi4iIiAQyBXo/ZbNBkAUn9xERERER31KXGxERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsTAFehERERERC1OgFxERERGxMAV6ERERERELU6AXEREREbEwBXoREREREQtToBcRERERsbBgswuQhjmdUFtldhVNZw8Bm83sKkRERESaHwV6P1VbBSvnml1F02VMhaBQs6sQERERaX7U5UZERERExMIU6EVERERELEyBXkRERETEwhToRUREREQsTDfFiogEuOoaKDkCjsNwqtpYdroavjkEHVtDcJCZ1YmIyKVSoBcRCUAnq2D9PsjNh8JyqKmt/3hlFbzwIQTZjVA/oBtc1R1aarQqERHLUaAXEQkgJ6tg6RZYu+eH1vgLqamFA2XG15JNMLAH3JyqYC8iYiUK9AFk895VPPZGRr1lYaERxLVPIrPfeEYPeYSgIP2XiwSqnSXwzlooP+He9qdr4PNdsPUA3Hk19O7k2fpERMQ7lO4CUEba3QzseTNOnJQfc/DRV2/zxuJH+ebb7fzy9vlmlyciHuZ0wrKt8OFWz+zvSCXMXwXDesPINM0CLSLi7xToA1Bip35k9r+37ueR6Q8zYVZPln75Jvff9EdaR7Y3sToR8bQPNsEnX3t+vyu+hqpquG2AQr2IiD/TsJXNQHhoBD27DMLpdFJ8aK/Z5YiIB3220zth/ozVu+DjPO/tX0RELp1a6JuJku+DfKuWbU2uREQ85eARWLTBtW0evQlahcPRSmOUm6ZYugV6dYQ4nT5ERPySWugD0MmqExypKOXw8e/YV7KVuf+ezJ6ijfSMH0hc+ySzyxMRD6ithX+sherai697tlbh0Lql8b3Jx3J+f6wa144lIiK+EfCBvrS0lKysLBISEggLCyM+Pp5p06ZRUVHBhAkTsNlsvPLKK2aX6VFvL3+S259qz9inL2PiCyksXvMa1/S5jafv+4/Z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", - "text/plain": [ - "
" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc_0 = QuantumCircuit(7)\n", - "for i in range(7):\n", - " qc_0.rx(np.pi / 4, i)\n", - "qc_0.cx(0, 3)\n", - "qc_0.cx(1, 3)\n", - "qc_0.cx(2, 3)\n", - "qc_0.cx(3, 4)\n", - "qc_0.cx(3, 5)\n", - "qc_0.cx(3, 6)\n", - "qc_0.cx(0, 3)\n", - "qc_0.cx(1, 3)\n", - "qc_0.cx(2, 3)\n", - "\n", - "qc_0.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "---------- 7 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 1.0 , min_reached = True\n", - "[]\n", - "Subcircuits: AAAAAAA \n", - "\n", - "\n", - "\n", - "---------- 6 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 3.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", - "Subcircuits: AAAAAAB \n", - "\n", - "\n", - "\n", - "---------- 5 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 9.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", - "Subcircuits: AAAABAC \n", - "\n", - "\n", - "\n", - "---------- 4 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 27.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]])]\n", - "Subcircuits: AAAABCD \n", - "\n", - "\n", - "\n", - "---------- 3 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 243.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", - "Subcircuits: AABACDE \n", - "\n", - "\n", - "\n", - "---------- 2 Qubits per QPU, 2 QPUs ----------\n", - "Gamma = 2187.0 , min_reached = True\n", - "[Cut(Action='CutTwoQubitGate', Gate=[8, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[9, ['cx', 2, 3]]), Cut(Action='CutTwoQubitGate', Gate=[10, ['cx', 3, 4]]), Cut(Action='CutTwoQubitGate', Gate=[11, ['cx', 3, 5]]), Cut(Action='CutTwoQubitGate', Gate=[12, ['cx', 3, 6]]), Cut(Action='CutTwoQubitGate', Gate=[14, ['cx', 1, 3]]), Cut(Action='CutTwoQubitGate', Gate=[15, ['cx', 2, 3]])]\n", - "Subcircuits: ABCADEF \n", - "\n" - ] - } - ], - "source": [ - "circuit_cktq7=QCtoCCOCircuit(qc_0)\n", - "\n", - "interface = SimpleGateList(circuit_cktq7)\n", - "\n", - "settings = OptimizationSettings(rand_seed = 12345)\n", - "\n", - "settings.setEngineSelection('CutOptimization', 'BestFirst')\n", - "\n", - "\n", - "\n", - "qubits_per_QPU=7\n", - "num_QPUs=2\n", - "\n", - "\n", - "\n", - "for num_qpus in range(num_QPUs, 1, -1):\n", - " for qpu_qubits in range(qubits_per_QPU, 1, -1):\n", - " print(f'\\n\\n---------- {qpu_qubits} Qubits per QPU, {num_qpus} QPUs ----------')\n", - " \n", - " constraint_obj = DeviceConstraints(qubits_per_QPU = qpu_qubits, \n", - " num_QPUs = num_QPUs)\n", - "\n", - " op = LOCutsOnlyOptimizer(interface, \n", - " settings, \n", - " constraint_obj)\n", - " \n", - " out = op.optimize()\n", - "\n", - " print('Gamma =', None if (out is None) else out.upperBoundGamma(),\n", - " ', min_reached =', op.minimumReached())\n", - " if (out is not None):\n", - " out.print(simple=True)\n", - " else:\n", - " print(out)\n", - "\n", - " print('Subcircuits:', interface.exportSubcircuitsAsString(name_mapping='default'),'\\n')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cco", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/circuit_knitting/cutting/cut_finding/test/__init__.py b/circuit_knitting/cutting/cut_finding/test/__init__.py deleted file mode 100644 index 8b1378917..000000000 --- a/circuit_knitting/cutting/cut_finding/test/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py b/circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py deleted file mode 100644 index bb9d280e3..000000000 --- a/circuit_knitting/cutting/cut_finding/test/test_circuit_interface.py +++ /dev/null @@ -1,62 +0,0 @@ -from circuit_cutting_optimizer.circuit_interface import SimpleGateList - - -class TestCircuitInterface: - def test_CircuitConversion(self): - """Test conversion of circuits to the internal representation - used by the circuit-cutting optimizer. - """ - - trial_circuit = [ - ("h", "q1"), - ("barrier", "q1"), - ("s", "q0"), - "barrier", - ("cx", "q1", "q0"), - ] - circuit_converted = SimpleGateList(trial_circuit) - - assert circuit_converted.getNumQubits() == 2 - assert circuit_converted.getNumWires() == 2 - assert circuit_converted.qubit_names.item_dict == {"q1": 0, "q0": 1} - assert circuit_converted.getMultiQubitGates() == [[4, ["cx", 0, 1], None]] - assert circuit_converted.circuit == [ - [["h", 0], None], - [["barrier", 0], None], - [["s", 1], None], - ["barrier", None], - [["cx", 0, 1], None], - ] - - def test_CutInterface(self): - """Test the internal representation of circuit cuts.""" - - trial_circuit = [ - ("cx", 0, 1), - ("cx", 2, 3), - ("cx", 1, 2), - ("cx", 0, 1), - ("cx", 2, 3), - ] - circuit_converted = SimpleGateList(trial_circuit) - circuit_converted.insertGateCut(2, "LO") - circuit_converted.defineSubcircuits([[0, 1], [2, 3]]) - - assert list(circuit_converted.new_gate_ID_map) == [0, 1, 2, 3, 4] - assert circuit_converted.cut_type == [None, None, "LO", None, None] - assert ( - circuit_converted.exportSubcircuitsAsString(name_mapping="default") - == "AABB" - ) - assert circuit_converted.exportCutCircuit(name_mapping="default") == [ - ["cx", 0, 1], - ["cx", 2, 3], - ["cx", 1, 2], - ["cx", 0, 1], - ["cx", 2, 3], - ] # in the absence of any wire cuts. - assert ( - circuit_converted.makeWireMapping(name_mapping="default") - == circuit_converted.makeWireMapping(name_mapping=None) - == [0, 1, 2, 3] - ) # the two methods are the same in the absence of wire cuts. diff --git a/circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py b/circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py deleted file mode 100644 index 477d8c0fa..000000000 --- a/circuit_knitting/cutting/cut_finding/test/test_disjoint_subcircuits_state.py +++ /dev/null @@ -1,95 +0,0 @@ -from pytest import mark, raises, fixture -from circuit_cutting_optimizer.circuit_interface import SimpleGateList -from circuit_cutting_optimizer.disjoint_subcircuits_state import ( - DisjointSubcircuitsState, -) -from circuit_cutting_optimizer.cut_optimization import ( - disjoint_subcircuit_actions, -) - - -@mark.parametrize("num_qubits", [2.1, -1]) -def test_StateInitialization(num_qubits): - """Test that initialization values are valid data types.""" - - with raises(ValueError): - _ = DisjointSubcircuitsState(num_qubits) - - -@fixture -def testCircuit(): - circuit = [ - ("h", "q1"), - ("barrier", "q1"), - ("s", "q0"), - "barrier", - ("cx", "q1", "q0"), - ] - - interface = SimpleGateList(circuit) - - state = DisjointSubcircuitsState( - interface.getNumQubits() - ) # initialization of DisjoingSubcircuitsState object. - - two_qubit_gate = interface.getMultiQubitGates()[0] - - return state, two_qubit_gate - - -def test_StateUnCut(testCircuit): - state, _ = testCircuit - - assert list(state.wiremap) == [0, 1] - - assert state.num_wires == 2 - - assert list(state.uptree) == [0, 1] - - assert list(state.width) == [1, 1] - - assert list(state.no_merge) == [] - - assert state.getSearchLevel() == 0 - - -def test_ApplyGate(testCircuit): - state, two_qubit_gate = testCircuit - - next_state = disjoint_subcircuit_actions.getAction(None).nextState( - state, two_qubit_gate, 10 - )[0] - - assert list(next_state.wiremap) == [0, 1] - - assert next_state.num_wires == 2 - - assert next_state.findQubitRoot(1) == 0 - - assert list(next_state.uptree) == [0, 0] - - assert list(next_state.width) == [2, 1] - - assert list(next_state.no_merge) == [] - - assert next_state.getSearchLevel() == 1 - - -def test_CutGate(testCircuit): - state, two_qubit_gate = testCircuit - - next_state = disjoint_subcircuit_actions.getAction("CutTwoQubitGate").nextState( - state, two_qubit_gate, 10 - )[0] - - assert list(next_state.wiremap) == [0, 1] - - assert next_state.num_wires == 2 - - assert list(next_state.uptree) == [0, 1] - - assert list(next_state.width) == [1, 1] - - assert list(next_state.no_merge) == [(0, 1)] - - assert next_state.getSearchLevel() == 1 diff --git a/circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py b/circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py deleted file mode 100644 index f76f6ce7c..000000000 --- a/circuit_knitting/cutting/cut_finding/test/test_optimization_settings.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest -from circuit_cutting_optimizer.optimization_settings import OptimizationSettings - - -@pytest.mark.parametrize( - "max_gamma, max_backjumps, beam_width ", - [(2.1, 1.2, -1.4), (1.2, 1.5, 2.3), (0, 1, 1), (1, 1, 0)], -) -def test_OptimizationParameters(max_gamma, max_backjumps, beam_width): - """Test optimization parameters for being valid data types.""" - - with pytest.raises(ValueError): - _ = OptimizationSettings( - max_gamma=max_gamma, max_backjumps=max_backjumps, beam_width=beam_width - ) - - -def test_AllCutSearchGroups(): - """Test for the existence of all enabled cutting search groups.""" - - assert OptimizationSettings(LO=True).getCutSearchGroups() == [None, "GateCut"] diff --git a/circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py b/circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py deleted file mode 100644 index 3aeb3ea7d..000000000 --- a/circuit_knitting/cutting/cut_finding/test/test_quantum_device_constraints.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest -from circuit_cutting_optimizer.quantum_device_constraints import DeviceConstraints - - -@pytest.mark.parametrize("qubits_per_QPU, num_QPUs", [(2.1, 1.2), (1.2, 0), (-1, 1)]) -def test_DeviceConstraints(qubits_per_QPU, num_QPUs): - """Test device constraints for being valid data types.""" - - with pytest.raises(ValueError): - _ = DeviceConstraints(qubits_per_QPU, num_QPUs) diff --git a/circuit_knitting/cutting/cut_finding/test/test_utils.py b/circuit_knitting/cutting/cut_finding/test/test_utils.py deleted file mode 100644 index cc4497b34..000000000 --- a/circuit_knitting/cutting/cut_finding/test/test_utils.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest -from qiskit import QuantumCircuit -from qiskit.circuit.library import EfficientSU2 -from circuit_cutting_optimizer.utils import QCtoCCOCircuit - -# test circuit 1. -qc1 = QuantumCircuit(2) -qc1.h(1) -qc1.barrier(1) -qc1.s(0) -qc1.barrier() -qc1.cx(1, 0) - -# test circuit 2 -qc2 = EfficientSU2(2, entanglement="linear", reps=2).decompose() -qc2.assign_parameters([0.4] * len(qc2.parameters), inplace=True) - - -@pytest.mark.parametrize( - "input, output", - [ - (qc1, [("h", 1), ("barrier", 1), ("s", 0), "barrier", ("cx", 1, 0)]), - ( - qc2, - [ - (("ry", 0.4), 0), - (("rz", 0.4), 0), - (("ry", 0.4), 1), - (("rz", 0.4), 1), - ("cx", 0, 1), - (("ry", 0.4), 0), - (("rz", 0.4), 0), - (("ry", 0.4), 1), - (("rz", 0.4), 1), - ("cx", 0, 1), - (("ry", 0.4), 0), - (("rz", 0.4), 0), - (("ry", 0.4), 1), - (("rz", 0.4), 1), - ], - ), - ], -) -def test_QCtoCCOCircuit(input, output): - circuit_internal = QCtoCCOCircuit(input) - assert circuit_internal == output diff --git a/docs/circuit_cutting/tutorials/bell.qpy b/docs/circuit_cutting/tutorials/bell.qpy deleted file mode 100644 index 69edbd5fc4c4a0d3e16610c90e6b462667aa07ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmWIa4EFX6;bf3xWPkw1LI& Date: Fri, 5 Jan 2024 10:54:13 -0600 Subject: [PATCH 8/8] remove stratum function, not needed for Dijkstra. --- .../cut_finding/LO_gate_cut_optimizer/cut_optimization.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py index 51257e2fd..52e1124c6 100644 --- a/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py +++ b/circuit_knitting/cutting/cut_finding/LO_gate_cut_optimizer/cut_optimization.py @@ -32,12 +32,6 @@ def CutOptimizationCostFunc(state, func_args): return (state.lowerBoundGamma(), state.getMaxWidth()) -def CutOptimizationStratumFunc(state, func_args): - """Return stratum function for stratified beam search. - """ - return int(np.log2(state.lowerBoundGamma())) - - def CutOptimizationUpperBoundCostFunc(goal_state, func_args): """Return the gamma upper bound.""" return (goal_state.upperBoundGamma(), np.inf) @@ -88,7 +82,6 @@ def CutOptimizationGoalStateFunc(state, func_args): # the cut optimization search space. cut_optimization_search_funcs = SearchFunctions( cost_func=CutOptimizationCostFunc, - stratum_func=CutOptimizationStratumFunc, upperbound_cost_func=CutOptimizationUpperBoundCostFunc, next_state_func=CutOptimizationNextStateFunc, goal_state_func=CutOptimizationGoalStateFunc,