def gen_cnf(filename, graph): """ Generate QuickBB input file for the graph. This function ALWAYS expects a simple Graph (not MultiGraph) without self loops, because QuickBB does not understand these situations. Parameters ---------- filename : str Output file name graph : networkx.Graph Undirected graphical model """ v = graph.number_of_nodes() e = graph.number_of_edges() log.info(f"generating config file {filename}") cnf = "c a configuration of -qtree simulator\n" cnf += f"p cnf {v} {e}\n" # Convert possible MultiGraph to Graph (avoid repeated edges) for edge in graph.edges(): u, v = edge # print only if this is not a self-loop # Node numbering in QuickBB is 1-based if u != v: cnf += '{} {} 0\n'.format(int(u) + 1, int(v) + 1) # print("cnf file:",cnf) with open(filename, 'w+') as fp: fp.write(cnf)
def run_quickbb(cnffile, command=defs.QUICKBB_COMMAND, outfile='output/quickbb_out.qbb', statfile='output/quickbb_stat.qbb', extra_args=" --min-fill-ordering --time 60 "): """ Run QuickBB program and collect its output Parameters ---------- cnffile : str Path to the QuickBB input file command : str, optional QuickBB command name outfile : str, optional QuickBB output file statfile : str, optional QuickBB stat file extra_args : str, optional Optional commands to QuickBB. Default: --min-fill-ordering --time 60 Returns ------- output : str Process output """ # try: # os.remove(outfile) # os.remove(statfile) # except FileNotFoundError as e: # log.warn(e) # pass sh = command + " " # this makes Docker process too slow and sometimes fails # sh += f"--outfile {outfile} --statfile {statfile} " sh += f"--cnffile {cnffile} " if extra_args is not None: sh += extra_args log.info("excecuting quickbb: " + sh) process = subprocess.Popen(sh.split(), stdout=subprocess.PIPE) output, error = process.communicate() if error: log.error(error) # log.info(output) # with open(outfile, 'r') as fp: # log.info("OUTPUT:\n"+fp.read()) # with open(statfile, 'r') as fp: # log.info("STAT:\n"+fp.read()) return output
def make_clique_on(old_graph, clique_nodes, name_prefix='C'): """ Adds a clique on the specified indices. No checks is done whether some edges exist in the clique. The name of the clique is formed from the name_prefix and the lowest element in the clique_nodes Parameters ---------- graph : networkx.Graph or networkx.MultiGraph graph to modify clique_nodes : list list of nodes to include into clique name_prefix : str prefix for the clique name Returns ------- new_graph : type(graph) New graph with clique """ clique_nodes = tuple(int(var) for var in clique_nodes) graph = copy.deepcopy(old_graph) if len(clique_nodes) == 0: return graph edges = [ tuple(sorted(edge)) for edge in itertools.combinations(clique_nodes, 2) ] node_idx = min(clique_nodes) graph.add_edges_from(edges, tensor={ 'name': name_prefix + f'{node_idx}', 'indices': clique_nodes, 'data_key': None }) clique_size = len(clique_nodes) log.info(f"Clique of size {clique_size} on vertices: {clique_nodes}") return graph
def get_amplitudes_from_cirq(filename, initial_state=0): """ Calculates amplitudes for a circuit in file filename using Cirq """ import cirq n_qubits, circuit = ops.read_circuit_file(filename) cirq_circuit = cirq.Circuit() for layer in circuit: cirq_circuit.append(op.to_cirq_1d_circ_op() for op in layer) print("Circuit:") print(cirq_circuit) simulator = cirq.Simulator() result = simulator.simulate(cirq_circuit, initial_state=initial_state) log.info("Simulation completed\n") # Cirq for some reason computes all amplitudes with phase -1j return result.final_state_vector
def run_quickbb(cnffile, wait_time=60, command=defs.QUICKBB_COMMAND, cwd=None, extra_args=" --min-fill-ordering"): """ Run QuickBB program and collect its output Parameters ---------- cnffile : str Path to the QuickBB input file wait_time : int, default 60 command : str, optional QuickBB command name cwd : str, default None Current work directory extra_args : str, optional Optional commands to QuickBB. Default: --min-fill-ordering --time 60 Returns ------- output : str Process output """ if command is None: raise ValueError('No QuickBB command given.' ' Did you install QuickBB?') sh = command + f" --time {int(wait_time)} " sh += f"--cnffile {cnffile} " if extra_args is not None: sh += extra_args log.info("excecuting quickbb: " + sh) process = subprocess.Popen(sh.split(), stdout=subprocess.PIPE, cwd=cwd) output, error = process.communicate() if error: log.error(error) return output
def split_graph_random(old_graph, n_var_parallel=0): """ Splits a graphical model with randomly chosen nodes to parallelize over. Parameters ---------- old_graph : networkx.Graph graph to contract (after eliminating variables which are parallelized over) n_var_parallel : int number of variables to eliminate by parallelization Returns ------- idx_parallel : list of Idx variables removed by parallelization graph : networkx.Graph new graph without parallelized variables """ graph = copy.deepcopy(old_graph) indices = [var for var in graph.nodes(data=False)] idx_parallel = np.random.choice( indices, size=n_var_parallel, replace=False) idx_parallel_var = [Var(var, size=graph.nodes[var]) for var in idx_parallel] for idx in idx_parallel: remove_node(graph, idx) log.info("Removed indices by parallelization:\n{}".format(idx_parallel)) log.info("Removed {} variables".format(len(idx_parallel))) peo, treewidth = get_peo(graph) return sorted(idx_parallel_var, key=int), graph
def read_circuit_stream(stream, max_depth=None): """ Read circuit file and return quantum circuit in the form of a list of lists Parameters ---------- filename : str circuit file in the format of Sergio Boixo max_depth : int maximal depth of gates to read Returns ------- qubit_count : int number of qubits in the circuit circuit : list of lists quantum circuit as a list of layers of gates """ operation_search_patt = r'(?P<operation>' + r'|'.join( LABEL_TO_GATE_DICT.keys()) + r')(?P<qubits>( \d+(?!\.))+)' params_search_patt_1 = r'(?P<operation>' + r'|'.join( LABEL_TO_GATE_DICT.keys( )) + r')(?P<qubits>( \d+)+)\ (?P<alpha>-?\d+\.\d+)$' params_search_patt_2 = r'(?P<operation>' + r'|'.join( LABEL_TO_GATE_DICT.keys() ) + r')(?P<qubits>( \d+)+)\ (?P<alpha>-?\d+\.\d+) (?P<beta>-?\d+\.\d+)$' circuit = [] circuit_layer = [] qubit_count = int(stream.readline()) log.info("There are {:d} qubits in circuit".format(qubit_count)) n_ignored_layers = 0 current_layer = 0 for idx, line in enumerate(stream): if not line or line == '\n': break m = re.search(r'(?P<layer>[0-9]+) (?=[a-z])', line) if m is None: raise Exception("file format error at line {}: {}".format( idx, line)) # Read circuit layer by layer layer_num = int(m.group('layer')) if max_depth is not None and layer_num > max_depth: n_ignored_layers = layer_num - max_depth continue if layer_num > current_layer: circuit.append(circuit_layer) circuit_layer = [] current_layer = layer_num op_str = line[m.end():] m = re.search(operation_search_patt, op_str) if m is None: raise Exception("file format error in {}".format(op_str)) op_identif = m.group('operation') q_idx = tuple(int(qq) for qq in m.group('qubits').split()) op_cls = LABEL_TO_GATE_DICT[op_identif] # A conditional on using simplification of fsim gate. if op_identif == 'fs': if False: circuit_layer.append(cZ(*q_idx)) circuit_layer.append(H(q_idx[0])) circuit_layer.append(H(q_idx[1])) circuit_layer.append(cZ(*q_idx)) else: circuit_layer.append(cZ(*q_idx)) circuit_layer.append(SWAP(*q_idx)) else: if issubclass(op_cls, ParametricGate): if op_cls.parameter_count == 1: m = re.search(params_search_patt_1, op_str) if m is None: raise Exception( f'Could not find parameter for gate. `{line})') q_idx = tuple(int(qq) for qq in m.group('qubits').split()) alpha = m.group('alpha') try: op = op_cls(*q_idx, alpha=float(alpha)) except: print('Error in creating gate for line', line) raise elif op_cls.parameter_count == 2: m = re.search(params_search_patt_2, op_str) if m is None: raise Exception( f'Could not find parameter for gate. `{line})') q_idx = tuple(int(qq) for qq in m.group('qubits').split()) alpha = m.group('alpha') beta = m.group('beta') op = op_cls(*q_idx, alpha=float(alpha), beta=float(beta)) else: op = op_cls(*q_idx) circuit_layer.append(op) circuit.append(circuit_layer) # last layer if n_ignored_layers > 0: log.info("Ignored {} layers".format(n_ignored_layers)) return qubit_count, circuit
def read_circuit_file(filename, max_depth=None): log.info("reading file {}".format(filename)) with open(filename, "r") as fp: n, qc = read_circuit_stream(fp, max_depth=max_depth) return n, qc
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
def eval_circuit(n_qubits, circuit, final_state, initial_state=0, measured_final=None, measured_initial=None, pdict={}): """ Evaluate a circuit with specified initial and final states. Parameters ---------- n_qubits: int Number of qubits in the circuit circuit: list of lists List of lists of gates final_state: int Values of measured qubits at the end of the circuit (bra). Bitwise coded with the length of min(n_qubits, len(measured_final) initial_state: int Values of the measured qubits at the beginning of the circuit (ket). Bitwise coded with the length of min(n_qubits, len(measured_initial) measured_final: list, default None Iterable with the positions of qubits which are measured. If not all qubits are measured, then a subset of amplitudes will be evaluated measured_initial: list, default None Iterable with the positions of qubits which are measured initially. If not all are measured, then the resulting amplitudes will be evaluated for a subset of initial states. pdict: dict, default {} Returns ------- amplitudes: numpy.array """ # Check which qubits are measured all_qubits = set(range(n_qubits)) if measured_final is None: measured_final = tuple(range(n_qubits)) else: if not set(measured_final).issubset(all_qubits): raise ValueError(f'measured_final qubits outside allowed' f' range: {measured_final}') if measured_initial is None: measured_initial = tuple(range(n_qubits)) else: if not set(measured_initial).issubset(all_qubits): raise ValueError(f'measured_initial qubits outside allowed' f' range: {measured_initial}') # Prepare graphical model buckets, data_dict, bra_vars, ket_vars = opt.circ2buckets( n_qubits, circuit, pdict=pdict) # Collect free qubit variables free_final = sorted(all_qubits - set(measured_final)) free_bra_vars = [bra_vars[idx] for idx in free_final] bra_vars = [bra_vars[idx] for idx in measured_final] free_initial = sorted(all_qubits - set(measured_initial)) free_ket_vars = [ket_vars[idx] for idx in free_initial] ket_vars = [ket_vars[idx] for idx in measured_initial] if len(free_bra_vars) > 0: log.info('Evaluate amplitudes over all final states of qubits:') log.info(f'{free_final}') if len(free_ket_vars) > 0: log.info('Evaluate amplitudes over all initial states of qubits:') log.info(f'{free_initial}') graph_initial = gm.buckets2graph( buckets, ignore_variables=bra_vars+ket_vars) graph = gm.make_clique_on(graph_initial, free_bra_vars+free_ket_vars) # Get PEO peo_initial, treewidth = gm.get_peo(graph) # transform peo so free_bra_vars and free_ket_vars are at the end # this fixes the layout of the tensor peo = gm.get_equivalent_peo(graph, peo_initial, free_bra_vars+free_ket_vars) # place bra and ket variables to beginning, so these variables # will be contracted first perm_buckets, perm_dict = opt.reorder_buckets( buckets, bra_vars + ket_vars + peo) perm_graph, _ = gm.relabel_graph_nodes( graph, perm_dict) # extract bra and ket variables from variable list and sort according # to qubit order ket_vars = sorted([perm_dict[idx] for idx in ket_vars], key=str) bra_vars = sorted([perm_dict[idx] for idx in bra_vars], key=str) # make proper slice dictionaries. We choose ket = |0>, # bra = |0> on fixed entries slice_dict = utils.slice_from_bits(initial_state, ket_vars) slice_dict.update(utils.slice_from_bits(final_state, bra_vars)) slice_dict.update({var: slice(None) for var in free_bra_vars}) slice_dict.update({var: slice(None) for var in free_ket_vars}) # Finally make numpy buckets and calculate sliced_buckets = npfr.get_sliced_np_buckets( perm_buckets, data_dict, slice_dict) result = opt.bucket_elimination( sliced_buckets, npfr.process_bucket_np, n_var_nosum=len(free_bra_vars+free_ket_vars)) return result.data
def split_graph_by_metric( old_graph, n_var_parallel=0, metric_fn=get_node_by_degree, forbidden_nodes=()): """ Parallel-splitted version of :py:meth:`get_peo` with nodes to split chosen according to the metric function. Metric function should take a graph and return a list of pairs (node : metric_value) Parameters ---------- old_graph : networkx.Graph or networkx.MultiGraph graph to split by parallelizing over variables and to contract Parallel edges and self-loops in the graph are removed (if any) before the calculation of metric n_var_parallel : int number of variables to eliminate by parallelization metric_fn : function, optional function to evaluate node metric. Default get_node_by_degree forbidden_nodes : iterable, optional nodes in this list will not be considered for deletion. Default (). Returns ------- idx_parallel : list variables removed by parallelization graph : networkx.Graph new graph without parallelized variables """ # graph = get_simple_graph(old_graph) # import pdb # pdb.set_trace() graph = copy.deepcopy(old_graph) # convert everything to int forbidden_nodes = [int(var) for var in forbidden_nodes] # get nodes by metric in descending order nodes_by_metric = metric_fn(graph) nodes_by_metric.sort(key=lambda pair: int(pair[1]), reverse=True) nodes_by_metric_allowed = [] for node, metric in nodes_by_metric: if node not in forbidden_nodes: nodes_by_metric_allowed.append((node, metric)) idx_parallel = [] for ii in range(n_var_parallel): node, metric = nodes_by_metric_allowed[ii] idx_parallel.append(node) # create var objects from nodes idx_parallel_var = [Var(var, size=graph.nodes[var]['size']) for var in idx_parallel] for idx in idx_parallel: remove_node(graph, idx) log.info("Removed indices by parallelization:\n{}".format(idx_parallel)) log.info("Removed {} variables".format(len(idx_parallel))) return idx_parallel_var, graph
def read_circuit_file(filename, max_depth=None): """ Read circuit file and return quantum circuit in the form of a list of lists Parameters ---------- filename : str circuit file in the format of Sergio Boixo max_depth : int maximal depth of gates to read Returns ------- qubit_count : int number of qubits in the circuit circuit : list of lists quantum circuit as a list of layers of gates """ label_to_gate_dict = { 'i': I, 'h': H, 't': T, 'z': Z, 'cz': cZ, 'x': X, 'y': Y, 'x_1_2': X_1_2, 'y_1_2': Y_1_2, } operation_search_patt = r'(?P<operation>' + r'|'.join( label_to_gate_dict.keys()) + r')(?P<qubits>( \d+)+)' log.info("reading file {}".format(filename)) circuit = [] circuit_layer = [] with open(filename, "r") as fp: qubit_count = int(fp.readline()) log.info("There are {:d} qubits in circuit".format(qubit_count)) n_ignored_layers = 0 current_layer = 0 for idx, line in enumerate(fp): m = re.search(r'(?P<layer>[0-9]+) (?=[a-z])', line) if m is None: raise Exception("file format error at line {}".format(idx)) # Read circuit layer by layer layer_num = int(m.group('layer')) if max_depth is not None and layer_num > max_depth: n_ignored_layers = layer_num - max_depth continue if layer_num > current_layer: circuit.append(circuit_layer) circuit_layer = [] current_layer = layer_num op_str = line[m.end():] m = re.search(operation_search_patt, op_str) if m is None: raise Exception("file format error in {}".format(op_str)) op_identif = m.group('operation') q_idx = tuple(int(qq) for qq in m.group('qubits').split()) op = label_to_gate_dict[op_identif](*q_idx) circuit_layer.append(op) circuit.append(circuit_layer) # last layer if n_ignored_layers > 0: log.info("Ignored {} layers".format(n_ignored_layers)) return qubit_count, circuit