Source code for mitiq.benchmarks.mirror_circuits

# Copyright (C) Unitary Fund
#
# This source code is licensed under the GPL license (v3) found in the
# LICENSE file in the root directory of this source tree.

"""Functions for creating mirror circuits as defined in
:cite:`Proctor_2021_NatPhys` for benchmarking quantum computers
(with error mitigation)."""

from typing import List, Optional, Tuple

import cirq
import networkx as nx
from cirq.experiments.qubit_characterizations import _single_qubit_cliffords
from numpy import random

from mitiq import QPROGRAM, Bitstring
from mitiq.interface import convert_from_mitiq

single_q_cliffords = _single_qubit_cliffords()
cliffords = single_q_cliffords.c1_in_xy
paulis = [cirq.X, cirq.Y, cirq.Z, cirq.I]


[docs] def random_paulis( connectivity_graph: nx.Graph, random_state: random.RandomState ) -> cirq.Circuit: """Returns a circuit with randomly selected Pauli gates on each qubit. Args: connectivity_graph: Connectivity graph of device to run circuit on. random_state: Random state to select Paulis I, X, Y, Z uniformly at random. """ return cirq.Circuit( paulis[random_state.randint(len(paulis))](cirq.LineQubit(x)) for x in connectivity_graph.nodes )
[docs] def edge_grab( two_qubit_gate_prob: float, connectivity_graph: nx.Graph, random_state: random.RandomState, ) -> nx.Graph: """ Args: two_qubit_gate_prob: Probability of an edge being chosen from the set of candidate edges. connectivity_graph: The connectivity graph for the backend on which the circuit will be run. random_state: Random state to select edges (uniformly at random). Returns: Returns a set of edges for which two qubit gates are to be applied given a two qubit gate density and the connectivity graph that must be satisfied. """ connectivity_graph = connectivity_graph.copy() candidate_edges = nx.Graph() final_edges = nx.Graph() final_edges.add_nodes_from(connectivity_graph) while connectivity_graph.edges: num = random_state.randint(connectivity_graph.size()) edges = list(connectivity_graph.edges) curr_edge = edges[num] candidate_edges.add_edge(*curr_edge) connectivity_graph.remove_nodes_from(curr_edge) for edge in candidate_edges.edges: if random_state.uniform(0.0, 1.0) < two_qubit_gate_prob: final_edges.add_edge(*edge) return final_edges
[docs] def random_cliffords( connectivity_graph: nx.Graph, random_state: random.RandomState, two_qubit_gate: cirq.Gate = cirq.CNOT, ) -> cirq.Circuit: """ Args: connectivity_graph: A graph with the edges for which the two-qubit Clifford gate is to be applied. random_state: Random state to choose Cliffords (uniformly at random). two_qubit_gate: Two-qubit gate to use. Returns: A circuit with a two-qubit Clifford gate applied to each edge in edges, and a random single-qubit Clifford gate applied to every other qubit. """ gates = [ two_qubit_gate.on(cirq.LineQubit(a), cirq.LineQubit(b)) for a, b in list(connectivity_graph.edges) ] qubits = nx.Graph() qubits.add_nodes_from(nx.isolates(connectivity_graph)) gates.extend( list(random_single_cliffords(qubits, random_state).all_operations()) ) return cirq.Circuit(gates)
[docs] def random_single_cliffords( connectivity_graph: nx.Graph, random_state: random.RandomState ) -> cirq.Circuit: """ Args: connectivity_graph: A graph with each node representing a qubit for which a random single-qubit Clifford gate is to be applied. random_state: Random state to choose Cliffords (uniformly at random). Returns: A circuit with a random single-qubit Clifford gate applied on each given qubit. """ gates: List[cirq.Operation] = [] for qubit in connectivity_graph.nodes: num = random_state.randint(len(cliffords)) for clifford_gate in cliffords[num]: gates.append(clifford_gate(cirq.LineQubit(qubit))) return cirq.Circuit(gates)
[docs] def generate_mirror_circuit( nlayers: int, two_qubit_gate_prob: float, connectivity_graph: nx.Graph, two_qubit_gate_name: str = "CNOT", seed: Optional[int] = None, return_type: Optional[str] = None, ) -> Tuple[QPROGRAM, Bitstring]: """ Args: nlayers: The number of random Clifford layers to be generated. two_qubit_gate_prob: Probability of a two-qubit gate being applied. connectivity_graph: The connectivity graph of the backend on which the mirror circuit will be run. This is used to make sure 2-qubit gates are only applied to connected qubits. two_qubit_gate_name: Name of two-qubit gate to use. Options are "CNOT" and "CZ". seed: Seed for generating randomized mirror circuit. return_type: String which specifies the type of the returned circuit. See the keys of ``mitiq.SUPPORTED_PROGRAM_TYPES`` for options. If ``None``, the returned circuit is a ``cirq.Circuit``. Returns: A randomized mirror circuit and the bitstring corresponding to a noise free result. """ if not 0 <= two_qubit_gate_prob <= 1: raise ValueError("two_qubit_gate_prob must be between 0 and 1") supported_two_qubit_gates = {"CZ": cirq.CZ, "CNOT": cirq.CNOT} if two_qubit_gate_name not in supported_two_qubit_gates.keys(): raise ValueError( f"Supported two-qubit gate names are " f"{tuple(supported_two_qubit_gates.keys())} but " f"{two_qubit_gate_name} was provided for `two_qubit_gate_name`." ) two_qubit_gate = supported_two_qubit_gates[two_qubit_gate_name] random_state = random.RandomState(seed) single_qubit_cliffords = random_single_cliffords( connectivity_graph, random_state=random_state ) forward_circuit = cirq.Circuit() quasi_inversion_circuit = cirq.Circuit() quasi_inverse_gates = [] for _ in range(nlayers): forward_circuit.append(random_paulis(connectivity_graph, random_state)) selected_edges = edge_grab( two_qubit_gate_prob, connectivity_graph, random_state ) circ = random_cliffords(selected_edges, random_state, two_qubit_gate) forward_circuit.append(circ) quasi_inverse_gates.append( random_paulis(connectivity_graph, random_state) ) quasi_inverse_gates.append(cirq.inverse(circ)) quasi_inversion_circuit.append( gate for gate in reversed(quasi_inverse_gates) ) rand_paulis = cirq.Circuit(random_paulis(connectivity_graph, random_state)) circuit = ( single_qubit_cliffords + forward_circuit + rand_paulis + quasi_inversion_circuit + cirq.inverse(single_qubit_cliffords) ) # Compute the bitstring this circuit should sample. res = cirq.Simulator().run( circuit + cirq.measure(*sorted(circuit.all_qubits())) ) bitstring = list(res.measurements.values())[0][0].tolist() return_type = "cirq" if not return_type else return_type return convert_from_mitiq(circuit, return_type), bitstring