def circ2buckets(qubit_count, circuit, pdict={}, max_depth=None): """ Takes a circuit in the form of list of lists, builds corresponding buckets. Buckets contain Tensors defining quantum gates. Each bucket corresponds to a variable. Each bucket can hold tensors acting on it's variable and variables with higher index. Parameters ---------- qubit_count : int number of qubits in the circuit circuit : list of lists quantum circuit as returned by :py:meth:`operators.read_circuit_file` pdict : dict Dictionary with placeholders if any parameteric gates were unresolved max_depth : int Maximal depth of the circuit which should be used Returns ------- buckets : list of lists list of lists (buckets) data_dict : dict Dictionary with all tensor data bra_variables : list variables of the output qubits ket_variables: list variables of the input qubits """ # import pdb # pdb.set_trace() if max_depth is None: max_depth = len(circuit) data_dict = {} # Let's build buckets for bucket elimination algorithm. # The circuit is built from left to right, as it operates # on the ket ( |0> ) from the left. We thus first place # the bra ( <x| ) and then put gates in the reverse order # Fill the variable `frame` layer_variables = [ Var(qubit, name=f'o_{qubit}') for qubit in range(qubit_count) ] current_var_idx = qubit_count # Save variables of the bra bra_variables = [var for var in layer_variables] # Initialize buckets for qubit in range(qubit_count): buckets = [[] for qubit in range(qubit_count)] # Place safeguard measurement circuits before and after # the circuit measurement_circ = [[ops.M(qubit) for qubit in range(qubit_count)]] combined_circ = functools.reduce( lambda x, y: itertools.chain(x, y), [measurement_circ, reversed(circuit[:max_depth])]) # Start building the graph in reverse order for layer in combined_circ: for op in layer: # CUSTOM # Swap variables on swap gate if isinstance(op, ops.SWAP): q1, q2 = op.qubits _v1 = layer_variables[q1] layer_variables[q1] = layer_variables[q2] layer_variables[q2] = _v1 continue # build the indices of the gate. If gate # changes the basis of a qubit, a new variable # has to be introduced and current_var_idx is increased. # The order of indices # is always (a_new, a, b_new, b, ...), as # this is how gate tensors are chosen to be stored variables = [] current_var_idx_copy = current_var_idx min_var_idx = current_var_idx for qubit in op.qubits: if qubit in op.changed_qubits: variables.extend( [layer_variables[qubit], Var(current_var_idx_copy)]) current_var_idx_copy += 1 else: variables.extend([layer_variables[qubit]]) min_var_idx = min(min_var_idx, int(layer_variables[qubit])) # fill placeholders in parameters if any for par, value in op.parameters.items(): if isinstance(value, ops.placeholder): op._parameters[par] = pdict[value] data_key = (op.name, hash((op.name, tuple(op.parameters.items())))) # Build a tensor t = Tensor(op.name, variables, data_key=data_key) # Insert tensor data into data dict data_dict[data_key] = op.gen_tensor() # Append tensor to buckets # first_qubit_var = layer_variables[op.qubits[0]] buckets[min_var_idx].append(t) # Create new buckets and update current variable frame for qubit in op.changed_qubits: layer_variables[qubit] = Var(current_var_idx) buckets.append([]) current_var_idx += 1 # Finally go over the qubits, append measurement gates # and collect ket variables ket_variables = [] op = ops.M(0) # create a single measurement gate object data_key = (op.name, hash((op.name, tuple(op.parameters.items())))) data_dict.update({data_key: op.gen_tensor()}) for qubit in range(qubit_count): var = layer_variables[qubit] new_var = Var(current_var_idx, name=f'i_{qubit}', size=2) ket_variables.append(new_var) # update buckets and variable `frame` buckets[int(var)].append( Tensor(op.name, indices=[var, new_var], data_key=data_key)) buckets.append([]) layer_variables[qubit] = new_var current_var_idx += 1 return buckets, data_dict, bra_variables, ket_variables
def circ2graph(qubit_count, circuit, pdict={}, max_depth=None, omit_terminals=True): """ Constructs a graph from a circuit in the form of a list of lists. Parameters ---------- qubit_count : int number of qubits in the circuit circuit : list of lists quantum circuit as returned by :py:meth:`operators.read_circuit_file` pdict : dict Dictionary with placeholders if any parameteric gates were unresolved max_depth : int, default None Maximal depth of the circuit which should be used omit_terminals : bool, default True If terminal nodes should be excluded from the final graph. Returns ------- graph : networkx.MultiGraph Graph which corresponds to the circuit data_dict : dict Dictionary with all tensor data """ import functools import qtree.operators as ops if max_depth is None: max_depth = len(circuit) data_dict = {} # Let's build the graph. # The circuit is built from left to right, as it operates # on the ket ( |0> ) from the left. We thus first place # the bra ( <x| ) and then put gates in the reverse order # Fill the variable `frame` layer_variables = list(range(qubit_count)) current_var_idx = qubit_count # Initialize the graph graph = nx.MultiGraph() # Populate nodes and save variables of the bra bra_variables = [] for var in layer_variables: graph.add_node(var, name=f'o_{var}', size=2) bra_variables.append(Var(var, name=f"o_{var}")) # Place safeguard measurement circuits before and after # the circuit measurement_circ = [[ops.M(qubit) for qubit in range(qubit_count)]] combined_circ = functools.reduce( lambda x, y: itertools.chain(x, y), [measurement_circ, reversed(circuit[:max_depth])]) # Start building the graph in reverse order for layer in combined_circ: for op in layer: # build the indices of the gate. If gate # changes the basis of a qubit, a new variable # has to be introduced and current_var_idx is increased. # The order of indices # is always (a_new, a, b_new, b, ...), as # this is how gate tensors are chosen to be stored variables = [] current_var_idx_copy = current_var_idx for qubit in op.qubits: if qubit in op.changed_qubits: variables.extend( [layer_variables[qubit], current_var_idx_copy]) graph.add_node(current_var_idx_copy, name='v_{}'.format(current_var_idx_copy), size=2) current_var_idx_copy += 1 else: variables.extend([layer_variables[qubit]]) # Form a tensor and add a clique to the graph # fill placeholders in gate's parameters if any for par, value in op.parameters.items(): if isinstance(value, ops.placeholder): op._parameters[par] = pdict[value] data_key = hash((op.name, tuple(op.parameters.items()))) tensor = { 'name': op.name, 'indices': tuple(variables), 'data_key': data_key } # Insert tensor data into data dict data_dict[data_key] = op.gen_tensor() if len(variables) > 1: edges = itertools.combinations(variables, 2) else: edges = [(variables[0], variables[0])] graph.add_edges_from(edges, tensor=tensor) # Update current variable frame for qubit in op.changed_qubits: layer_variables[qubit] = current_var_idx current_var_idx += 1 # Finally go over the qubits, append measurement gates # and collect ket variables ket_variables = [] op = ops.M(0) # create a single measurement gate object for qubit in range(qubit_count): var = layer_variables[qubit] new_var = current_var_idx ket_variables.append(Var(new_var, name=f'i_{qubit}', size=2)) # update graph and variable `frame` graph.add_node(new_var, name=f'i_{qubit}', size=2) data_key = hash((op.name, tuple(op.parameters.items()))) tensor = { 'name': op.name, 'indices': (var, new_var), 'data_key': data_key } graph.add_edge(var, new_var, tensor=tensor) layer_variables[qubit] = new_var current_var_idx += 1 if omit_terminals: graph.remove_nodes_from(tuple(map(int, bra_variables + ket_variables))) v = graph.number_of_nodes() e = graph.number_of_edges() log.info(f"Generated graph with {v} nodes and {e} edges") # log.info(f"last index contains from {layer_variables}") return graph, data_dict, bra_variables, ket_variables