Skip to content

1. Utilities

1.1 State representation conversion

graphiq.backends.state_rep_conversion

Convert between different representations of quantum states; including density matrices, graph, and stabilizer.

density_to_graph(input_matrix, threshold=0.1, validate=True)

Converts a density matrix state representation to a graph representation via an adjacency matrix. It assumes qubit systems.

Parameters:

Name Type Description Default
input_matrix numpy.ndarray

a density matrix to be converted to a graph

required
threshold double | float

a minimum threshold value of negativity for the state to assume entangled

0.1
validate bool

validate the input density is a graph state if True

True

Returns:

Type Description
numpy.ndarray | list[(float, numpy.ndarray)]

an adjacency matrix representation for a pure state or a list of adjacency matrices for a mixed state

Raises:

Type Description
AssertionError

if input_matrix is not a valid graph state when validate is True

Source code in graphiq/backends/state_rep_conversion.py
def density_to_graph(input_matrix, threshold=0.1, validate=True):
    """
    Converts a density matrix state representation to a graph representation via an adjacency matrix.
    It assumes qubit systems.

    :param input_matrix: a density matrix to be converted to a graph
    :type input_matrix: numpy.ndarray
    :param threshold: a minimum threshold value of negativity for the state to assume entangled
    :type threshold: double or float
    :param validate: validate the input density is a graph state if True
    :type validate: bool
    :raise AssertionError: if input_matrix is not a valid graph state when validate is True
    :return: an adjacency matrix representation for a pure state or a list of adjacency matrices for a mixed state
    :rtype: numpy.ndarray or list[(float, numpy.ndarray)]
    """
    if dmf.is_pure(input_matrix):
        graph_adj = _density_to_graph_pure(input_matrix, threshold)
    else:
        # eigen-decomposition
        eigenvalues, eigenvectors = linalg.eigh(input_matrix)
        graph_adj = []
        for i in range(len(eigenvectors)):
            rho_i = eigenvectors[i] @ np.conjugate(eigenvectors[i].T)
            graph_adj.append((eigenvalues, _density_to_graph_pure(rho_i, threshold)))

    if validate:
        assert np.allclose(
            input_matrix, graph_to_density(graph_adj)
        ), "Input matrix is not a graph state."
    return graph_adj

density_to_stabilizer(input_matrix)

Convert a density matrix to stabilizer via graph representation

This works only for graph states, not for general stabilizer states that are not graph states

Parameters:

Name Type Description Default
input_matrix numpy.ndarray

a density matrix

required

Returns:

Type Description
list[(float, StabilizerTableau)]

a stabilizer representation

Source code in graphiq/backends/state_rep_conversion.py
def density_to_stabilizer(input_matrix):
    """
    Convert a density matrix to stabilizer via graph representation

    This works only for graph states, not for general stabilizer states that are not graph states

    :param input_matrix: a density matrix
    :type input_matrix: numpy.ndarray
    :return: a stabilizer representation
    :rtype: list[(float, StabilizerTableau)]
    """
    # first check if the state is pure
    if dmf.is_pure(input_matrix):
        graph = _density_to_graph_pure(input_matrix)
        stabilizer = _graph_to_stabilizer_pure(graph)
        return [(1.0, stabilizer)]
    else:
        # eigen-decomposition
        eigenvalues, eigenvectors = linalg.eigh(input_matrix)
        stabilizer = []
        for i in range(len(eigenvectors)):
            rho_i = eigenvectors[i] @ np.conjugate(eigenvectors[i].T)
            graph = density_to_graph(rho_i)
            tmp_stabilizer = graph_to_stabilizer(graph)
            stabilizer.append((eigenvalues[i], tmp_stabilizer[0][1]))

        return stabilizer

graph_to_density(input_graph)

Builds a density matrix representation from a graph (adjacency matrix or networkx.Graph)

Parameters:

Name Type Description Default
input_graph networkx.Graph | numpy.ndarray

the graph from which we will build a density matrix

required

Returns:

Type Description
numpy.ndarray

a density matrix as a numpy array

Raises:

Type Description
TypeError

if input_graph is not of the type of networkx.Graph or a graphiq.backends.graph.state.Graph or an adjacency matrix given by a numpy array

Source code in graphiq/backends/state_rep_conversion.py
def graph_to_density(input_graph):
    """
    Builds a density matrix representation from a graph (adjacency matrix or networkx.Graph)

    :param input_graph: the graph from which we will build a density matrix
    :type input_graph: networkx.Graph or numpy.ndarray
    :raise TypeError: if input_graph is not of the type of networkx.Graph or a graphiq.backends.graph.state.Graph
        or an adjacency matrix given by a numpy array
    :return: a density matrix as a numpy array
    :rtype: numpy.ndarray
    """

    if isinstance(input_graph, list):
        rho = 0
        for p_i, graph_i in input_graph:
            state_i = _graph_to_density_pure(graph_i)
            rho += p_i * state_i
        return rho
    elif isinstance(input_graph, (nx.Graph, np.ndarray)):
        return _graph_to_density_pure(input_graph)

    else:
        raise TypeError(
            "Input graph must be NetworkX graph or adjacency matrix "
            "using numpy.ndarray or mixed state representation for graph. "
        )

graph_to_stabilizer(input_graph)

Convert a graph to stabilizer represented by StabilizerTableau

Parameters:

Name Type Description Default
input_graph networkX.Graph | numpy.ndarray

the input graph to be converted to the stabilizer

required

Returns:

Type Description
list[(float, StabilizerTableau)]

two binary matrices representing the stabilizer generators

Raises:

Type Description
TypeError

if input_graph is not of the type of networkx.Graph or an adjacency matrix given by a numpy array or a list

Source code in graphiq/backends/state_rep_conversion.py
def graph_to_stabilizer(input_graph):
    """
    Convert a graph to stabilizer represented by StabilizerTableau

    :param input_graph: the input graph to be converted to the stabilizer
    :type input_graph: networkX.Graph or numpy.ndarray
    :raise TypeError: if input_graph is not of the type of networkx.Graph or
            an adjacency matrix given by a numpy array or a list
    :return: two binary matrices representing the stabilizer generators
    :rtype: list[(float, StabilizerTableau)]
    """
    if isinstance(input_graph, list):
        results = []
        for p_i, graph_i in input_graph:
            tableau_i = _graph_to_stabilizer_pure(graph_i)
            results.append((p_i, tableau_i))
        return results
    else:
        tableau = _graph_to_stabilizer_pure(input_graph)
        return [(1.0, tableau)]

mixed_graph_equivalency(graph1, graph2)

Identify if two mixed state graph representations are the same.

Assuming no repetitive graph in each graph list. Two graphs are equal (not isomorphism).

Parameters:

Name Type Description Default
graph1 list((float, networkX.Graph)) | networkX.Graph

the first graph

required
graph2 list((float, networkX.Graph)) | networkX.Graph

the second graph

required

Returns:

Type Description
bool

True if graph1 is the same as graph2; False otherwise

Source code in graphiq/backends/state_rep_conversion.py
def mixed_graph_equivalency(graph1, graph2):
    """
    Identify if two mixed state graph representations are the same.

    Assuming no repetitive graph in each graph list. Two graphs are equal (not isomorphism).

    :param graph1: the first graph
    :type graph1: list((float, networkX.Graph)) or networkX.Graph
    :param graph2: the second graph
    :type graph2: list((float, networkX.Graph)) or networkX.Graph
    :return: True if graph1 is the same as graph2; False otherwise
    :rtype: bool
    """
    if isinstance(graph1, list) and len(graph1) == 1:
        graph1 = graph1[0][1]
    if isinstance(graph2, list) and len(graph2) == 1:
        graph2 = graph2[0][1]
    if isinstance(graph1, list) and isinstance(graph2, list):
        if len(graph1) == len(graph2):
            graph2_copy = copy.deepcopy(graph2)
            for p_i, g_i in graph1:
                for q_i, t_i in graph2_copy:
                    if np.equal(p_i, q_i) and nx.utils.graphs_equal(g_i, t_i):
                        graph2_copy.remove((q_i, t_i))
                        break
            return len(graph2_copy) == 0
        else:
            return False
    elif isinstance(graph1, nx.Graph) and isinstance(graph2, nx.Graph):
        return nx.utils.graphs_equal(graph1, graph2)
    else:
        return False

mixed_stabilizer_equivalency(stab1, stab2)

Identify if two mixed state stabilizer representations are the same.

Parameters:

Name Type Description Default
stab1 list[(float, StabilizerTableau)] | StabilizerTableau

the first stabilizer state

required
stab2 list[(float, StabilizerTableau)] | StabilizerTableau

the second stabilizer state

required

Returns:

Type Description
bool

if stab1 is the same as stab2

Source code in graphiq/backends/state_rep_conversion.py
def mixed_stabilizer_equivalency(stab1, stab2):
    """
     Identify if two mixed state stabilizer representations are the same.

    :param stab1: the first stabilizer state
    :type stab1: list[(float, StabilizerTableau)] or StabilizerTableau
    :param stab2: the second stabilizer state
    :type stab2: list[(float, StabilizerTableau)] or StabilizerTableau
    :return: if stab1 is the same as stab2
    :rtype: bool
    """
    if isinstance(stab1, list) and len(stab1) == 1:
        stab1 = stab1[0][1]
    if isinstance(stab2, list) and len(stab2) == 1:
        stab2 = stab2[0][1]
    if isinstance(stab1, list) and isinstance(stab2, list):
        if len(stab1) == len(stab2):
            stab2_copy = copy.deepcopy(stab2)
            for p_i, s_i in stab1:
                for q_i, t_i in stab2_copy:
                    if np.equal(p_i, q_i) and s_i == t_i:
                        stab2_copy.remove((q_i, t_i))
                        break
            return len(stab2_copy) == 0
        else:
            return False
    elif isinstance(stab1, StabilizerTableau) and isinstance(stab2, StabilizerTableau):
        return stab1 == stab2
    else:
        return False

stabilizer_to_density(input_stabilizer)

Convert a stabilizer to a density matrix

Parameters:

Name Type Description Default
input_stabilizer list[(float, StabilizerTableau)] | StabilizerTableau

the stabilizer representation

required

Returns:

Type Description
numpy.ndarray

a density matrix

Source code in graphiq/backends/state_rep_conversion.py
def stabilizer_to_density(input_stabilizer):
    """
    Convert a stabilizer to a density matrix

    :param input_stabilizer: the stabilizer representation
    :type input_stabilizer: list[(float, StabilizerTableau)] or StabilizerTableau
    :return: a density matrix
    :rtype: numpy.ndarray
    """
    if isinstance(input_stabilizer, list):
        if isinstance(input_stabilizer[0], str):
            x_matrix, z_matrix = sfu.string_to_symplectic(input_stabilizer)
            stab = StabilizerTableau([x_matrix, z_matrix])
            return _stabilizer_to_density_pure(stab)
        elif isinstance(input_stabilizer[0], tuple):
            rho = 0

            for p_i, tableau_i in input_stabilizer:
                rho += p_i * _stabilizer_to_density_pure(tableau_i)
        else:
            raise ValueError(
                "Invalid stabilizer input: use either StabilizerTableau or a mixed state representation."
            )
    elif isinstance(input_stabilizer, StabilizerTableau):
        return _stabilizer_to_density_pure(input_stabilizer)
    else:
        raise ValueError(
            "Invalid stabilizer input: use either StabilizerTableau or a mixed state representation."
        )

stabilizer_to_graph(input_stabilizer, validate=True)

Convert a stabilizer to graph.

Parameters:

Name Type Description Default
input_stabilizer list[(float, StabilizerTableau)] | StabilizerTableau

the stabilizer representation in terms of (probability, StabilizerTableau)

required
validate bool

validate if input_stabilizer is a graph state when enabled

True

Returns:

Type Description
[(float, numpy.ndarray)]

a graph representation

Raises:

Type Description
AssertionError

if input stabilizer is not a graph state when validate is True

Source code in graphiq/backends/state_rep_conversion.py
def stabilizer_to_graph(input_stabilizer, validate=True):
    """
    Convert a stabilizer to graph.

    :param input_stabilizer: the stabilizer representation in terms of (probability, StabilizerTableau)
    :type input_stabilizer: list[(float, StabilizerTableau)] or StabilizerTableau
    :param validate: validate if input_stabilizer is a graph state when enabled
    :type validate: bool
    :raises AssertionError: if input stabilizer is not a graph state when validate is True
    :return: a graph representation
    :rtype: [(float, numpy.ndarray)]
    """
    graph_list = []
    if isinstance(input_stabilizer, list):
        for each_stabilizer in input_stabilizer:
            tableau = each_stabilizer[1]
            graph = _graph_finder(tableau.x_matrix, tableau.z_matrix)
            graph_list.append((each_stabilizer[0], graph))
    elif isinstance(input_stabilizer, StabilizerTableau):
        tableau = input_stabilizer
        graph = _graph_finder(tableau.x_matrix, tableau.z_matrix)
        graph_list.append((1.0, graph))
    else:
        raise ValueError(
            "Please input the stabilizer representation in terms of probability, X matrix, Z matrix"
        )

    if validate:
        assert mixed_stabilizer_equivalency(
            input_stabilizer, graph_to_stabilizer(graph_list)
        ), "Input stabilizer is not a graph state."

    return graph_list

state_to_graph(state)

A helper function to turn any valid representation into a graph. It also returns the StabilizerTableau corresponding to the initial state, and the gate sequence needed to convert the state into a graph-state.

Parameters:

Name Type Description Default
state StabilizerTableau | CliffordTableau | networkX.Graph | np.ndarray

the state to be converted to graph

required

Returns:

Type Description
tuple (networkX.Graph, StabilizerTableau, list)

tuple (graph, input state's stabilizer tableau, gate_list)

Source code in graphiq/backends/state_rep_conversion.py
def state_to_graph(state):
    """
    A helper function to turn any valid representation into a graph. It also returns the StabilizerTableau corresponding
     to the initial state, and the gate sequence needed to convert the state into a graph-state.

    :param state: the state to be converted to graph
    :type state: StabilizerTableau or CliffordTableau or networkX.Graph or np.ndarray
    :return: tuple (graph, input state's stabilizer tableau, gate_list)
    :rtype: tuple (networkX.Graph, StabilizerTableau, list)
    """
    state = copy.deepcopy(state)
    # returns graph, input state's tableau, gate_list
    if isinstance(state, nx.Graph):
        tab = get_stabilizer_tableau_from_graph(state)
        return state, tab, []
    elif isinstance(state, np.ndarray):
        try:
            graph = nx.from_numpy_array(state)
            tab = get_stabilizer_tableau_from_graph(graph)
            return graph, [], tab
        except:
            raise ValueError(
                "the input numpy array is not a valid adjacency matrix, try fixing it or using other valid input types"
            )

    elif isinstance(state, CliffordTableau):
        z_matrix = state.stabilizer_z
        x_matrix = state.stabilizer_x
        tab = state.to_stabilizer()
    elif isinstance(state, StabilizerTableau):
        z_matrix = state.z_matrix
        x_matrix = state.x_matrix
        tab = state
    else:
        raise ValueError(
            "input data should either be a adjacency matrix, graph, Clifford or Stabilizer tableau"
        )
    graph, (h_pos, p_dag_pos) = _graph_finder(x_matrix, z_matrix, get_ops_data=True)
    gate_list = [("H", pos) for pos in h_pos] + [("P_dag", pos) for pos in p_dag_pos]

    # phase correction; adding Z gates at the end to make the phase of the transformed state equal to an ideal graph
    g_tab = get_stabilizer_tableau_from_graph(graph)
    phase_correction = _phase_correction(tab, g_tab, gate_list)

    gate_list += phase_correction
    return graph, tab, gate_list

1.2 Local-Clifford equivalence

graphiq.backends.lc_equivalence_check

Local-Clifford equivalence check determines whether two graph states are local-Clifford equivalent via :py:func:is_lc_equivalent function. This section is based on articles [1] and [2]; please refer to them for more details.

Any n-partite stabilizer state is defined by a set of :math:n commuting operators called the stabilizer generators. Each member of this set is called a stabilizer, and consists of tensor product of local Pauli operators on each of the qubits. The state is the unique common eigenstate of this set with eigenvalue equal to +1. The set itself is not unique and any set of \(n\) independent members of the stabilizer group, that is generated by this generator set, is also valid. A binary representation of the stabilizers can be obtained by the following mapping:

$$ \sigma_0 = I \rightarrow 00,\ \sigma_x \rightarrow 01,\ \sigma_z \rightarrow 10,\ \sigma_y \rightarrow 11 $$ This can be generalized to \(n\)-qubit case, which gives rise to a \(2n\)-dimensional vector. For example, \(\sigma_x \otimes \sigma_y \otimes \cdots \otimes \sigma_z \rightarrow \left( 0, 1, \cdots, 1| 1, 1, \cdots, 0 \right)\). We note that this convention is different from the one used by many other authors. Nevertheless, as we treat \(X\) and \(Z\) matrices separately, this different convention does not cause any issue.

The generators set can then be represented by \(2n \times n\) matrix, which can be thought of as two \(n \times n\) square matrices on top of each other. A column in this matrix is a \(2n\)-dimensional vector representing one stabilizer operator. We name this matrix \(S\), the top matrix \(Z\) matrix, and the bottom matrix \(X\) matrix.

\[ S = \begin{bmatrix} Z \\X \end{bmatrix} = \begin{bmatrix} \theta \\ I \end{bmatrix} \]

Under this mapping, if \(Z_{ij}\) is equal to one and \(X_{ij}\) is zero, then it means a Pauli \(Z\) operator acting on qubit \(i\) in the \(j\)-th stabilizer operator. The other way around which is \(Z_{ij} = 0\) and \(X_{ij} =1\) is a Pauli X acting on qubit \(i\). And if both elements are 0 or 1, they correspond to identity \(I\) and Pauli \(Y\) operators, respectively. For the special case of graph states, the \(X\) matrix is equal to identity \(I\) and the \(Z\) matrix is the adjacency matrix of the graph usually shown by \(\theta\).

A general \(n\)-qubit local Clifford operation in this formalism can be represented by a \(2n \times 2n\) matrix \(Q\),which is made of 4 diagonal square matrices \(A, B, C\) and \(D\), that is, $$ Q = \begin{bmatrix} A & B \ C & D \end{bmatrix}. $$

Each \(2 \times 2\) sub-matrix, consisting of \(i\)-th diagonal elements of the \(A, B, C, D\), acts locally on the \(i\)-th qubit in the system, that is,

\[ Q^{(i)} := \begin{bmatrix} a_{i} & b_{i}\\ c_{i} & d_{i} \end{bmatrix}. \]

Considering two graph states with stabilizer generators \(S = \begin{bmatrix} \theta \\ I \end{bmatrix}\) and \(S'= \begin{bmatrix} \theta' \\ I \end{bmatrix}\) the necessary and sufficient condition for LC equivalence is the existence of the Clifford operator Q such that:

\[ S^{T}Q^{T}PS' = 0 \]

where P is the \(2n \times 2n\) matrix of the form \(P =\begin{bmatrix} 0 & I \\ I & 0 \end{bmatrix}\). This gives rise to a system of \(n^2\) linear equations to find \(4n\) unknown elements of \(Q\)

\[ \left(\sum_{i=1}^{n} \theta_{i j} \theta_{i k}^{\prime} c_{i}\right)+\theta_{j k} a_{k}+\theta_{j k}^{\prime} d_{ j}+\delta_{j k} b_{j}=0 \]

The unknowns \(a_{i},\ b_{i},\ c_{i},\ d_{i}\) should also satisfy the constraints

\[ a_{i}d_{i} + b_{i}c_{i}=1 \]

to be a valid Clifford operation. (This ensures that the Clifford operation on a qubit is invertible.)

In order to solve for \(Q\), one should first find a basis for the solution space of the system of linear equations.

\[ B = \left\{ b_{1},\ldots,b_{d} \right\} \]

Remember that if the system of equations is overdetermined after removing linearly dependent constraints, then there exists no solution (other than :math:Q=0 which is not a valid Clifford operation) and graph states are not equivalent for sure. It is proved that a solution for a valid \(Q\) exists, if and only if the set

\[ \left\{b+b^{\prime} \mid b, b^{\prime} \in B\right\} \]

has a member that satisfies the constraint. Note that this is only true for the cases where \(B\) is at least of size 4. However, this is rarely the case and has not practical significance. For smaller \(B\), one should test all possible solutions (at most 16) to see if a valid \(Q\) can be found.

In order to find a basis for set B, one needs to use the \(n^2 \times 4n\) coefficient matrix of the system of linear equations, find the linearly dependent columns (this is done in the code by a row-reduction process), and choose any complete basis \(B_{dependent}\) for that subspace, and use each member of that basis to form a set of new system of linear equations.

\[ \begin{align} A &x =& y \in B_{dependent} \\ &x =& A^{-1} y = b \in B \end{align} \]

Where the new full rank coefficient matrix \(A\) is obtained from the old coefficient matrix after row-reduction and removing the linearly dependent rows and columns, and the inhomogeneous vectors \(y\) are the members of the basis \(B_{dependent}\). Solving for vector \(x\) will result in finding one of the members \(b\) of solution basis \(B\). As evident here, the size of \(B\) is equal to the size of \(B_{dependent}\) which is equal to the number of linearly dependent columns in the initial coefficient matrix.

If \(Q\) is found, the graphs are local-Clifford equivalent. In this case, one can translate the sub-matrices \(Q^{(i)}\) to local Clifford operations (combination of \(H\) and \(P\) gates) needed (via :py:func:local_clifford_ops function) to convert one graph state to the other.

All general local-Clifford operations \(Q\) on graph states are proved to be equivalent to a series of local complementations on the graph. A list of local complementation graph operations can also be found ( :py:func:lc_graph_operations function) that transform one graph to the other one in a step by step graph transform. Please read section IV of [2]_ for details. In order to do so one starts with making a so-called :math:R matrix which is defined as

\[ R = C\theta + D \]

where \(C\) and \(D\) are the lower blocks of \(Q\), the Clifford operator needed to transform the initial graph to the target one, and \(\theta\) is the adjacency matrix. The transformation \(f_{i}\) on R is defined as \(f_{i}(R) = R(\Gamma_{i}R + X_{ii}\Gamma_{i} + I)\) and the transformation \(f_{jk} := f_{j}f_{k}f_{j}\). It is proved that if a sequence of transformations of \(R\) exist such that

$$ f_{j_{M}k_{M}} \cdots f_{j_{1}k_{1}} f_{i_{N}} \cdots f_{i_{1}} (R) = I $$ then the same sequence of local complementations on corresponding graph nodes \(j_{M}k_{M}\) and \(i_{N}\) transforms the graph state the same as \(Q\) does.

In order to find the sequence of "f" transformations, one checks for elements equal to 1 in the diagonal of the \(R\)-matrix, if such element is found in \(i\)-th location and the corresponding row is not already equal to the canonical unit vector \(e_i = \left\{0, 0, \cdots, 1, 0, \cdots, 0 \right\}\) the \(f_i\) transformation is applied on \(R\)-matrix and added to the sequence. This is repeated until no such row is found. Then one searches for diagonal elements equal to zero, and for each of them searches the corresponding column to find any element equal to 1, suppose that the zero diagonal is at \(j\)-th location and the element = 1 in that column is at :math:k-th row. Then a double \(f_{jk}\) transformation is applied of \(R\)-matrix and added to the sequence. This is repeated until no more eligible element can be found in the diagonal of the \(R\)-matrix.

.. [1] Maarten Van den Nest, Jeroen Dehaene, and Bart De Moor Phys. Rev. A 70, 034302 – Published 17 September 2004

.. [2] Maarten Van den Nest, Jeroen Dehaene, and Bart De Moor Phys. Rev. A 69, 022316 – Published 24 February 2004

is_lc_equivalent(adj_matrix1, adj_matrix2, mode='deterministic', seed=0)

Determines whether two graph states are local-Clifford equivalent or not, given the adjacency matrices of the two. It takes two adjacency matrices as input and returns a numpy.ndarray containing \(n\) (\(2 \times 2\) arrays = clifford operations on each qubit.

Parameters:

Name Type Description Default
adj_matrix1 numpy.ndarray

the adjacency matrix of the first graph. This is equal to the binary matrix for representing Pauli Z part of the symplectic binary representation of the stabilizer generators

required
adj_matrix2 numpy.ndarray

the adjacency matrix of the second graph

required
mode str

the chosen mode for finding solutions. It can be either 'deterministic' (default) or 'random'.

'deterministic'
seed int

an optional input to set the random seed for the random search approach

0

Returns:

Type Description
bool, numpy.ndarray | None

If a solution is found, returns an array of single-qubit Clifford :math:2 \\times 2 matrices in the symplectic formalism. If not, graphs are not LC equivalent and returns None.

Raises:

Type Description
AssertionError

if the number of rows in the row reduced matrix is less than the rank of coefficient matrix or if the number of linearly dependent columns is not equal to :math:4n - rank (for :math:n being the number of nodes in the graph)

Source code in graphiq/backends/lc_equivalence_check.py
def is_lc_equivalent(adj_matrix1, adj_matrix2, mode="deterministic", seed=0):
    r"""
    Determines whether two graph states are local-Clifford equivalent or not, given the adjacency matrices of the two.
    It takes two adjacency matrices as input and returns a numpy.ndarray containing $n$ ($2 \times 2$ arrays
    = clifford operations on each qubit.

    :param adj_matrix1: the adjacency matrix of the first graph. This is equal to the binary matrix for representing
        Pauli Z part of the symplectic binary representation of the stabilizer generators
    :type adj_matrix1: numpy.ndarray
    :param adj_matrix2: the adjacency matrix of the second graph
    :type adj_matrix2: numpy.ndarray
    :param mode: the chosen mode for finding solutions. It can be either 'deterministic' (default) or 'random'.
    :type mode: str
    :param seed: an optional input to set the random seed for the random search approach
    :type seed: int
    :raises AssertionError: if the number of rows in the row reduced matrix is less than the rank of coefficient matrix
        or if the number of linearly dependent columns is not equal to :math:`4n - rank`
        (for :math:`n` being the number of nodes in the graph)
    :return: If a solution is found, returns an array of single-qubit Clifford :math:`2 \\times 2` matrices
        in the symplectic formalism. If not, graphs are not LC equivalent and returns None.
    :rtype: bool, numpy.ndarray or None
    """

    n_nodes = np.shape(adj_matrix1)[0]
    # get the coefficient matrix for the system of linear equations
    coeff_matrix = _coeff_maker(adj_matrix1, adj_matrix2)

    # row reduction applied
    reduced_coeff_matrix, _, last_nonzero_row_index = slinalg.row_reduction(
        coeff_matrix, coeff_matrix * 0
    )

    rank = last_nonzero_row_index + 1
    # check for rank to see how many independent equations are there = rank of the matrix
    if rank >= 4 * n_nodes:
        # Those two graph states are not LC equivalent for sure
        return False, None
    # update the matrix to remove zero rows
    reduced_coeff_matrix = np.array([row for row in reduced_coeff_matrix if row.any()])
    assert (
        np.shape(reduced_coeff_matrix)[0] == rank
    ), "The number of remaining rows is less than the rank!"
    # rank = np.shape(reduced_coeff_matrix)[0]

    # finding linearly dependent columns
    col_list = _col_finder(reduced_coeff_matrix)
    length = len(col_list)
    assert length == 4 * n_nodes - rank, "column list is not correct"

    # if solution basis' length, which is the dimension of the solution basis, is less than 4 then we should check every
    # possible vector in solution basis:
    solution_basis = _solution_basis_finder(reduced_coeff_matrix, col_list)
    if len(solution_basis) < 5:
        basis_dimension = len(solution_basis)
        all_solutions = [*range(2**basis_dimension)]
        all_solutions = [list(format(i, f"0{basis_dimension}b")) for i in all_solutions]
        all_solutions = np.array(all_solutions).astype(int)
        all_solutions = all_solutions.T
        solution_basis = np.transpose(solution_basis, axes=(1, 2, 0)).reshape(
            4 * n_nodes, basis_dimension
        )
        all_solutions = (solution_basis @ all_solutions) % 2
        for solution in all_solutions.T:
            if _is_valid_clifford(solution.reshape(4 * n_nodes, 1)):
                # convert the solution to an array of n * (2 X 2) matrices
                valid_solution = solution.reshape(4 * n_nodes, 1)
                return True, valid_solution.reshape(n_nodes, 2, 2)
        return False, None

    if mode == "random":
        # Use random mode to get the fast convergence for large states
        # Check for random solutions 1000 times
        rand_solution = _random_checker(
            reduced_coeff_matrix, col_list, trial_count=1000, seed=seed
        )
        if isinstance(rand_solution, np.ndarray):
            # random result
            return True, rand_solution.reshape(n_nodes, 2, 2)
        else:
            return False, None

    elif mode == "deterministic":
        basis = _solution_basis_finder(reduced_coeff_matrix, col_list)
        possible_combinations = combinations(basis, 2)
        solution_set = []
        for basis_combination in possible_combinations:
            possible_solution = basis_combination[0] + basis_combination[1]
            solution_set.append(possible_solution % 2)
        for solution in solution_set:
            if _is_valid_clifford(solution):
                # convert the solution to an array of n * (2 X 2) matrices
                return True, solution.reshape(n_nodes, 2, 2)

        # states are NOT LC equivalent
        return False, None
    else:
        raise ValueError(
            'The mode should be either "random" or "deterministic" (default)'
        )

iso_equal_check(graph1, graph2)

Checks if the graph1 is local-Clifford equivalent to any graph that is isomorphic to graph2

Parameters:

Name Type Description Default
graph1 networkx.Graph

initial graph

required
graph2 networkx.Graph

target graph

required

Returns:

Type Description
(bool, networkx.Graph)

If equivalent (True,the graph that graph1 is equivalent to) and if not, (False, graph1 itself)

Source code in graphiq/backends/lc_equivalence_check.py
def iso_equal_check(graph1, graph2):
    """
    Checks if the graph1 is local-Clifford equivalent to any graph that is isomorphic to graph2

    :param graph1: initial graph
    :type graph1: networkx.Graph
    :param graph2: target graph
    :type graph2: networkx.Graph
    :return: If equivalent (True,the graph that graph1 is equivalent to) and if not, (False, graph1 itself)
    :rtype: (bool, networkx.Graph)
    """
    iso_graphs_g2 = iso_graph_finder(graph2)
    adj_matrix1 = nx.to_numpy_array(graph1)
    iso_g2_adj_matrices = [nx.to_numpy_array(graph) for graph in iso_graphs_g2]
    for adj_matrix2 in iso_g2_adj_matrices:
        success, solution = is_lc_equivalent(adj_matrix1, adj_matrix2)
        if isinstance(solution, np.ndarray):
            return True, nx.to_networkx_graph(adj_matrix2)

    return False, graph1

iso_graph_finder(input_graph)

Generates the list of all graphs that are isomorphic to the input graph G. Scales with n! and faces runtime or memory issues for large graphs.

Parameters:

Name Type Description Default
input_graph networkx.Graph

input graph

required

Returns:

Type Description
list[networkx.Graph]

the list of graphs that are isomorphic to G

Source code in graphiq/backends/lc_equivalence_check.py
def iso_graph_finder(input_graph):
    """
    Generates the list of all graphs that are isomorphic to the input graph G.
    Scales with n! and faces runtime or memory issues for large graphs.

    :param input_graph: input graph
    :type input_graph: networkx.Graph
    :return: the list of graphs that are isomorphic to G
    :rtype: list[networkx.Graph]
    """
    iso_graphs = []
    list_nodes = sorted(input_graph)
    n_nodes = len(list_nodes)
    all_permu = list(permutations(list_nodes, len(list_nodes)))

    for each_permu in all_permu:
        adj_matrix = np.zeros((n_nodes, n_nodes))
        map_dict = dict(zip(list_nodes, each_permu))
        g_copy = nx.relabel_nodes(input_graph, map_dict, copy=True)
        for node_id1 in list_nodes:
            for node_id2 in list(g_copy.neighbors(node_id1)):
                adj_matrix[node_id1, node_id2] = 1

        g_copy = nx.to_networkx_graph(adj_matrix)
        iso_graphs.append(g_copy)

    return iso_graphs

lc_graph_operations(adj_matrix, solution)

Finds a list of local complementations needed to be applied on each node of the given graph to transform it into the target graph given the Clifford transformation matrix, which is the output of the solver function.

Parameters:

Name Type Description Default
adj_matrix numpy.ndarray

The adjacency matrix of the first graph. This is equal to the binary matrix for representing Pauli Z part of the symplectic binary representation of the stabilizer generators

required
solution numpy.ndarray

an array of single-qubit Clifford :math:2 \times 2 matrices in the symplectic formalism

required

Returns:

Type Description
list[str]

a list of the names of the operations that need to be applied on each qubit in the correct order.

Source code in graphiq/backends/lc_equivalence_check.py
def lc_graph_operations(adj_matrix, solution):
    """
    Finds a list of local complementations needed to be applied on each node of the given graph
    to transform it into the target graph given the Clifford transformation matrix,
    which is the output of the solver function.

    :param adj_matrix: The adjacency matrix of the first graph. This is equal to the binary matrix for
        representing Pauli Z part of the symplectic binary representation of the stabilizer generators
    :type adj_matrix: numpy.ndarray
    :param solution: an array of single-qubit Clifford :math:`2 \\times 2` matrices in the symplectic formalism
    :type solution: numpy.ndarray
    :return: a list of the names of the operations that need to be applied on each qubit in the correct order.
    :rtype: list[str]
    """
    # takes an adjacency matrix and the solution (Clifford operation) and returns the list of local complementations
    # needed for graph transformation.
    r_matrix = _R_matrix(adj_matrix, solution)
    n = np.shape(r_matrix)[0]
    singles_list = []
    doubles_list = []

    while _condition(r_matrix):
        r_matrix, s_list = _singles(r_matrix)
        singles_list.extend(s_list)

    while not np.array_equal(r_matrix, np.eye(n)):
        r_matrix, d_list = _doubles(r_matrix)
        doubles_list.extend(d_list)
    g_list = singles_list
    for i in doubles_list:
        g_list.append(i[0])
        g_list.append(i[1])
        g_list.append(i[0])
    return g_list

local_clifford_ops(solution)

Finds a list of operators needed to be applied on each qubit of the first graph to transform in to the second, given the Clifford transformation matrix, which is the output of the solver function.

Parameters:

Name Type Description Default
solution numpy.ndarray

an array of single-qubit Clifford :math:2 \times 2 matrices in the symplectic formalism

required

Returns:

Type Description
list[str]

a list of the names of the operations that need to be applied on each qubit in the correct order.

Source code in graphiq/backends/lc_equivalence_check.py
def local_clifford_ops(solution):
    """
    Finds a list of operators needed to be applied on each qubit of the first graph to transform in to the second,
    given the Clifford transformation matrix, which is the output of the solver function.

    :param solution: an array of single-qubit Clifford :math:`2 \\times 2` matrices in the symplectic formalism
    :type solution: numpy.ndarray
    :return: a list of the names of the operations that need to be applied on each qubit in the correct order.
    :rtype: list[str]
    """
    # The order of the operations is the same as the qubits' labels in the graphs

    # allowed operations on single qubits in binary symplectic representation
    identity = np.array([[1, 0], [0, 1]])
    hadamard = np.array([[0, 1], [1, 0]])
    p = np.array([[1, 1], [0, 1]])
    ph = np.array([[1, 1], [1, 0]])
    hp_dagger = np.array([[0, 1], [1, 1]])
    php = np.array([[1, 0], [1, 1]])

    ops_list = [identity, hadamard, p, ph, hp_dagger, php]
    ops_list_str = ["I", "H", "P", "P H", "H P_dag", "P H P"]
    ops_dict = zip(list(range(len(ops_list))), ops_list_str)
    ops_dict = dict(ops_dict)
    ops_names = []
    for i in solution:
        for j in range(len(ops_list)):
            if np.array_equal(i, ops_list[j]):
                ops_names.append(ops_dict[j])
    return ops_names

local_comp_graph(input_graph, node_id)

Applies a local complementation on the node (specified by node_id) of the input graph input_graph and returns the resulting graph.

Parameters:

Name Type Description Default
input_graph networkx.Graph

input graph

required
node_id int

the index of the node to apply tho local complementation on

required

Returns:

Type Description
networkx.Graph

a new transformed graph

Raises:

Type Description
AssertionError

if the node index node_id is not in the graph

Source code in graphiq/backends/lc_equivalence_check.py
def local_comp_graph(input_graph, node_id):
    """
    Applies a local complementation on the node (specified by node_id) of the input graph input_graph
    and returns the resulting graph.

    :param input_graph: input graph
    :type input_graph: networkx.Graph
    :param node_id: the index of the node to apply tho local complementation on
    :type node_id: int
    :raises AssertionError: if the node index node_id is not in the graph
    :return: a new transformed graph
    :rtype: networkx.Graph
    """
    # TODO: integrate this with the state representation functions
    n_nodes = input_graph.number_of_nodes()
    assert n_nodes > node_id >= 0, "node index is not in the graph"
    adj_matrix = nx.to_numpy_array(input_graph).astype(int)
    identity = np.eye(n_nodes, n_nodes)
    gamma_matrix = np.zeros((n_nodes, n_nodes))

    # gamma has only a single 1 element on position = diag(i)
    gamma_matrix[node_id, node_id] = 1

    new_adj_matrix = (
        adj_matrix
        @ (
            gamma_matrix @ adj_matrix
            + adj_matrix[node_id, node_id] * gamma_matrix
            + identity
        )
        % 2
    ) % 2
    for j in range(n_nodes):
        new_adj_matrix[j, j] = 0
    new_graph = nx.to_networkx_graph(new_adj_matrix)

    return new_graph

1.3 Graph relabelling module

graphiq.utils.relabel_module

Utility functionality for creating isomorphic graphs to explore alternative circuits.

automorph_check(adj1, labels_arr)

Given an initial adjacency matrix (of a graph), and an array of new permutations of the nodes' labels, this function return an array of distinct adjacency matrices resulted from those permutations. Note that the order will not remain the same as the order of the input labels.

Parameters:

Name Type Description Default
adj1 numpy.ndarray

initial adjacency matrix

required
labels_arr numpy.ndarray

an array of new labeling

required

Returns:

Type Description
numpy.ndarray

An array of adjacency matrices

Source code in graphiq/utils/relabel_module.py
def automorph_check(adj1, labels_arr):
    """
    Given an initial adjacency matrix (of a graph), and an array of new permutations of the nodes' labels, this function
    return an array of distinct adjacency matrices resulted from those permutations.
    Note that the order will not remain the same as the order of the input labels.

    :param adj1: initial adjacency matrix
    :type adj1: numpy.ndarray
    :param labels_arr:  an array of new labeling
    :type labels_arr: numpy.ndarray
    :return: An array of adjacency matrices
    :rtype: numpy.ndarray
    """
    # uses set to remove redundancies
    # the set includes the adjacency matrices in flatten form so that they can turn into tuples to be members of a set
    adj_set = {tuple(adj1.astype(int).flatten())}
    adj_list = [adj1]
    n_node = adj1.shape[0]
    for label in labels_arr:
        new_adj = relabel(adj1, label)
        adj_set.add(tuple(new_adj.flatten()))
    # remove the initial adjacency matrix from the set to just keep the new ones, since the original adj1 is already in
    # the final list of adjacencies as the first element
    adj_set.remove(tuple(adj1.astype(int).flatten()))
    for flat_adj in adj_set:
        remade_adj = np.array(flat_adj)
        adj_list.append(remade_adj.reshape(n_node, n_node))
    return np.array(adj_list)

check_isomorphism(graph, g_list, _only_auto=False)

check if the provided graph is isomorphic to any graph in the g_list

Parameters:

Name Type Description Default
graph nx.Graph

graph to check

required
g_list list

graph list to check against

required
_only_auto bool

if set to true, instead of isomorphism, only automorphism will be checked

False

Returns:

Type Description
bool

True of False, if an isomorphism case was detected.

Source code in graphiq/utils/relabel_module.py
def check_isomorphism(graph, g_list, _only_auto=False):
    """
    check if the provided graph is isomorphic to any graph in the g_list

    :param graph: graph to check
    :type graph: nx.Graph
    :param g_list: graph list to check against
    :type g_list: list
    :param _only_auto: if set to true, instead of isomorphism, only automorphism will be checked
    :type _only_auto: bool
    :return: True of False, if an isomorphism case was detected.
    :rtype: bool
    """
    iso = False
    check = _equal_graphs if _only_auto else nx.is_isomorphic
    for g in g_list:
        if check(graph, g):
            iso = True
            break
    return iso

emitter_sorted(adj_arr)

Takes an iterable of adjacency matrices (ideally output of the function) as input and sort them by number of emitters needed to generate those graph states.

Parameters:

Name Type Description Default
adj_arr numpy.ndarray

An array of adjacency matrices which can be the output of the function

required

Returns:

Type Description
list

a list of tuples (adj matrices, n_emitter) sorted by number of emitters

Source code in graphiq/utils/relabel_module.py
def emitter_sorted(adj_arr):
    """
    Takes an iterable of adjacency matrices (ideally output of the <iso_finder> function) as input and sort them by number of
    emitters needed to generate those graph states.

    :param adj_arr: An array of adjacency matrices which can be the output of the <iso_finder> function
    :type adj_arr: numpy.ndarray
    :return: a list of tuples (adj matrices, n_emitter) sorted by number of emitters
    :rtype: list
    """
    tuple_list = []
    for adj in adj_arr:
        g = nx.from_numpy_array(adj)
        n_emit = height_max(graph=g)
        tuple_list.append((adj, n_emit))
    sorted_list = sorted(tuple_list, key=lambda x: x[1])
    return sorted_list

get_relabel_map(g1, g2)

Finds the map between nodes of g1 and g2 if they are isomorphic.

Parameters:

Name Type Description Default
g1 nx.Graph | np.ndarray

networkx graph or adjacency matrix

required
g2 nx.Graph | np.ndarray

networkx graph or adjacency matrix

required

Returns:

Type Description
dict

a dictionary mapping each node of g1 to a node of g2. The map may not be unique.

Source code in graphiq/utils/relabel_module.py
def get_relabel_map(g1, g2):
    """
    Finds the map between nodes of g1 and g2 if they are isomorphic.

    :param g1: networkx graph or adjacency matrix
    :type g1: nx.Graph or np.ndarray
    :param g2: networkx graph or adjacency matrix
    :type g2: nx.Graph or np.ndarray
    :return: a dictionary mapping each node of g1 to a node of g2. The map may not be unique.
    :rtype: dict
    """
    # g1 and g2 can be adj matrices or nx graphs.
    if not isinstance(g1, nx.Graph):
        g1 = nx.from_numpy_array(g1)
        assert isinstance(g1, nx.Graph)
    if not isinstance(g2, nx.Graph):
        g2 = nx.from_numpy_array(g2)
        assert isinstance(g2, nx.Graph)
    if np.array_equal(nx.to_numpy_array(g1), nx.to_numpy_array(g2)):
        return {**{-1: "self"}, **dict(zip(g1.nodes(), g2.nodes()))}
    GM = isomorphism.GraphMatcher(g1, g2)
    assert GM.is_isomorphic()
    return GM.mapping

iso_finder(adj_matrix, n_iso, rel_inc_thresh=0.2, allow_exhaustive=True, sort_emit=False, label_map=False, thresh=None, seed=None)

The function permutes the labels of the vertices of a graph to get n_iso distinct isomorphic graphs. The original graph will also be returned as the first element of the list and counted toward the number of found cases. The maximum number of possible distinct cases may be less than n_iso. The graph G with the nodes relabeled using consecutive integers.

Parameters:

Name Type Description Default
adj_matrix numpy.ndarray

initial adjacency matrix or graph

required
n_iso int

number of the isomorphic graphs to look for (including the original graph)

required
rel_inc_thresh float

a threshold value between 0 and 1. The closer to 0 the closer we get to an exhaustive search.

0.2
allow_exhaustive bool

whether exhaustive search over all possible permutations is allowed or not. The runtime may become too long if this parameter is True.

True
sort_emit bool

if True the outcome is sorted by the number of emitters needed to generate each graph

False
label_map bool

if True, the function also returns a list of dictionaries, each representing the label mapping between the original input graph and the one in the returned list.

False
thresh int

a threshold value that determines how many random trials is performed in search for new permutations. The default is 5 times the number of needed permutations (=n_iso).

None
seed int

the seed for random sampling of labels

None

Returns:

Type Description
numpy.ndarray | tuple (numpy.ndarray, list[dict])

An array of adjacency matrices. The length of the array may be less than n_iso since the maximum number of isomorphic graphs for the particular input may be reached. Or the function iteration is interrupted due to threshold values. If the label_map is True, function returns a tuple, the first item being the array of adjacency matrices and the second one a list of dictionaries that are label maps between the original and relabeled graphs.

Source code in graphiq/utils/relabel_module.py
def iso_finder(
    adj_matrix,
    n_iso,
    rel_inc_thresh=0.2,
    allow_exhaustive=True,
    sort_emit=False,
    label_map=False,
    thresh=None,
    seed=None,
):
    """
    The function permutes the labels of the vertices of a graph to get n_iso distinct isomorphic graphs. The original
    graph will also be returned as the first element of the list and counted toward the number of found cases.
    The maximum number of possible distinct cases may be less than n_iso. The graph G with the nodes relabeled using
    consecutive integers.

    :param adj_matrix: initial adjacency matrix or graph
    :type adj_matrix: numpy.ndarray
    :param n_iso: number of the isomorphic graphs to look for (including the original graph)
    :type n_iso: int
    :param rel_inc_thresh: a threshold value between 0 and 1. The closer to 0 the closer we get to an exhaustive search.
    :type rel_inc_thresh: float
    :param allow_exhaustive: whether exhaustive search over all possible permutations is allowed or not. The runtime may
    become too long if this parameter is True.
    :type allow_exhaustive: bool
    :param sort_emit: if True the outcome is sorted by the number of emitters needed to generate each graph
    :type sort_emit: bool
    :param label_map: if True, the function also returns a list of dictionaries, each representing the label mapping
     between the original input graph and the one in the returned list.
    :type label_map: bool
    :param thresh: a threshold value that determines how many random trials is performed in search for new permutations.
    The default is 5 times the number of needed permutations (=n_iso).
    :type thresh: int
    :param seed: the seed for random sampling of labels
    :type seed: int
    :return: An array of adjacency matrices. The length of the array may be less than n_iso since the maximum number of
    isomorphic graphs for the particular input may be reached. Or the function iteration is interrupted due to threshold
    values. If the label_map is True, function returns a tuple, the first item being the array of adjacency matrices and
     the second one a list of dictionaries that are label maps between the original and relabeled graphs.
    :rtype: numpy.ndarray or tuple (numpy.ndarray, list[dict])
    """
    n_node = adj_matrix.shape[0]
    n_max = np.math.factorial(n_node)
    n_label = n_iso
    labels_arr = _label_finder(n_label, n_node, seed=seed, thresh=thresh)
    adj_arr = automorph_check(adj_matrix, labels_arr)
    if len(adj_arr) >= n_iso:
        adj_arr = adj_arr[:n_iso]
        return adj_arr
    else:
        rel_inc = 1  # relative increase ratio of the number of non-redundant cases as the number of labeling increase
        all_checked = False
        while len(adj_arr) < n_iso and rel_inc > rel_inc_thresh and not all_checked:
            success_ratio = len(adj_arr) / n_label if len(adj_arr) != 0 else 0.5
            add_n = int(n_label / success_ratio) + 1 - n_label
            n_label = int(n_label / success_ratio) + 1
            if n_label > n_max:
                n_label = n_max
                all_checked = True
                if (not allow_exhaustive) and success_ratio < 0.5:
                    warnings.warn(
                        f"Only {len(adj_arr)} isomorphic graphs were found due to high symmetry."
                        f"This may not be the maximum value. Allow for exhaustive search to check"
                    )
                    return adj_arr

            # update the seed used in _add_labels to get new values when we repeat it in the loop
            seed = seed + 1 if (seed is not None) else None
            labels_arr = _add_labels(
                labels_arr,
                add_n,
                exhaustive=allow_exhaustive * all_checked,
                seed=seed,
                thresh=thresh,
            )
            n1 = len(adj_arr)
            adj_arr = automorph_check(adj_matrix, labels_arr)
            n2 = len(adj_arr)
            if n2 >= n1:
                # update increase ratio and relative increase ratio
                inc_ratio = n2 / (n1 + 1)  # plus one is to avoid division by zero
                rel_inc = inc_ratio * success_ratio
        if all_checked:
            warnings.warn(f"Maximum of {n2} possible isomorphic graphs exist")
        elif rel_inc < rel_inc_thresh:
            warnings.warn(
                f"Only {n2} isomorphic graph were found. Consider decreasing rel_inc_thresh to possibly "
                f"get more."
            )
        else:
            pass
        if sort_emit:
            adj_arr = np.array([x[0] for x in emitter_sorted(adj_arr[:n_iso])])
        if label_map:
            mapping = []
            for new_adj in adj_arr:
                mapping.append(get_relabel_map(adj_matrix, new_adj))
            return adj_arr[:n_iso], mapping
        return adj_arr[:n_iso]

lc_orbit_finder(graph, comp_depth=None, orbit_size_thresh=None, with_iso=False, rand=False, rep_allowed=False)

Given a graph this functions tries all possible local-complementation sequences of length up to comp_depth to come up with new distinct graphs in the orbit of the input graph. The comp_depth determines the maximum depth of the orbit explored.

Parameters:

Name Type Description Default
graph Graph

original graph

required
comp_depth int

the maximum length of the sequence of local-complementations applied on the graph; if None, continue till the required number of graphs are found, or no new graphs are found.

None
orbit_size_thresh int

sets a limit on the maximum number of orbit graphs to look for

None
with_iso bool

if true, isomorph graphs will be kept in the orbit

False
rep_allowed

if true the orbit finder does not check for iso or auto morphism; useful for large graphs.

False

Returns:

Type Description
list[nx.Graph]

list of distinct graphs in the orbit of original graph

Source code in graphiq/utils/relabel_module.py
def lc_orbit_finder(
    graph: nx.Graph,
    comp_depth=None,
    orbit_size_thresh=None,
    with_iso=False,
    rand=False,
    rep_allowed=False,
):
    """
    Given a graph this functions tries all possible local-complementation sequences of length up to comp_depth to
    come up with new distinct graphs in the orbit of the input graph. The comp_depth determines the maximum depth of the
     orbit explored.

    :param graph: original graph
    :type graph: nx.Graph
    :param comp_depth: the maximum length of the sequence of local-complementations applied on the graph; if None,
                        continue till the required number of graphs are found, or no new graphs are found.
    :type comp_depth: int
    :param orbit_size_thresh: sets a limit on the maximum number of orbit graphs to look for
    :type orbit_size_thresh: int
    :param with_iso: if true, isomorph graphs will be kept in the orbit
    :type with_iso: bool
    :param rep_allowed: if true the orbit finder does not check for iso or auto morphism; useful for large graphs.
    :type rand: bool
    :return: list of distinct graphs in the orbit of original graph
    :rtype: list[nx.Graph]
    """
    orbit_list = [graph.copy()]
    node_list = [*graph.nodes]
    new_g = graph
    if rand:
        for i in range(min(10, len(graph))):
            node_x = np.random.randint(0, len(graph))
            new_g = local_comp_graph(new_g, node_x)
        orbit_list = [new_g]
    if orbit_size_thresh == 1:
        return orbit_list
    new_graphs = 1
    i = 0
    if comp_depth is None:
        cond = lambda x: True
    else:
        cond = lambda x: bool(x < comp_depth)

    while cond(i):
        len_before = len(orbit_list)
        # iterate over the new graphs appended to the end of the orbit list
        for graph in orbit_list[-new_graphs:]:
            if rand:
                np.random.shuffle(node_list)
            for node in node_list:
                if graph.degree(node) > 1:
                    g_lc = local_comp_graph(graph, node)
                    if not rep_allowed:
                        if not check_isomorphism(g_lc, orbit_list, _only_auto=with_iso):
                            orbit_list.append(g_lc)
                    else:
                        orbit_list.append(g_lc)
                if (
                    orbit_size_thresh is not None
                    and len(orbit_list) >= orbit_size_thresh
                ):
                    return orbit_list[:orbit_size_thresh]
                if (
                    rand and len(orbit_list) > len_before
                ):  # in random case we only keep 1 new graph
                    break
        # orbit_list = remove_iso(orbit_list)
        len_after = len(orbit_list)
        new_graphs = len_after - len_before
        # print("new graphs", new_graphs)
        if new_graphs == 0:
            break
        i += 1
    return orbit_list

linear_partial_orbit(graph)

Takes a linear cluster state, and returns a list of distinct graphs in the orbit. Not exhaustive. The first graph in the list is the original graph state.

Parameters:

Name Type Description Default
graph Graph

original graph

required

Returns:

Type Description
list

a list of graphs in the LC orbit of the linear cluster state

Source code in graphiq/utils/relabel_module.py
def linear_partial_orbit(graph: nx.Graph):
    """
    Takes a linear cluster state, and returns a list of distinct graphs in the orbit. Not exhaustive.
    The first graph in the list is the original graph state.
    :param graph: original graph
    :type graph: nx.Graph
    :return: a list of graphs in the LC orbit of the linear cluster state
    :rtype: list
    """
    orbit_list = []
    n = len(graph)
    g = graph.copy()
    # check that graph is linear
    degrees = [degree for _, degree in g.degree()]
    assert (
        n - 1 == graph.size() and max(degrees) == 2
    ), "input graph is not a linear graph"
    for lc_ops in _partial_orbit(n):
        new_g = g
        for x in lc_ops:
            new_g = local_comp_graph(new_g, x)
        orbit_list.append(new_g)
    return orbit_list

relabel(adj_matrix, new_labels)

This function relabels the vertices of a graph given the map to new labels. The initial graph's vertices is assumed to have been labeled according to its adjacency matrix that is with the nodes labeled using consecutive integers.

Parameters:

Name Type Description Default
adj_matrix numpy.ndarray

The adjacency matrix of the graph

required
new_labels numpy.ndarray

A permutation of the initial labels that is [0, 1, ..., n-1] where n is the number of vertices.

required

Returns:

Type Description
numpy.ndarray

Transformed adjacency matrix according to new labels

Source code in graphiq/utils/relabel_module.py
def relabel(adj_matrix, new_labels):
    """
    This function relabels the vertices of a graph given the map to new labels. The initial graph's vertices is assumed
    to have been labeled according to its adjacency matrix that is with the nodes labeled using consecutive integers.

    :param adj_matrix: The adjacency matrix of the graph
    :type adj_matrix: numpy.ndarray
    :param new_labels: A permutation of the initial labels that is [0, 1, ..., n-1] where n is the number of vertices.
    :type new_labels: numpy.ndarray
    :return: Transformed adjacency matrix according to new labels
    :rtype: numpy.ndarray
    """
    p_matrix = _perm2matrix(new_labels)
    permuted_adj_matrix = p_matrix.T @ adj_matrix @ p_matrix
    return permuted_adj_matrix.astype(int)

remove_iso(g_list)

Takes an input list of graphs and removes all the isomorphic cases, returning a list of distinct graphs

Parameters:

Name Type Description Default
g_list list[nx.Graph]

list of graphs

required

Returns:

Type Description
list[nx.Graph]

list of distinct graphs

Source code in graphiq/utils/relabel_module.py
def remove_iso(g_list):
    """
    Takes an input list of graphs and removes all the isomorphic cases, returning a list of distinct graphs

    :param g_list: list of graphs
    :type g_list: list[nx.Graph]
    :return: list of distinct graphs
    :rtype: list[nx.Graph]
    """
    non_iso = [*g_list]
    i, j = 0, 1
    while i < len(non_iso):
        g = non_iso[i]
        while i + j < len(non_iso):
            gg = non_iso[i + j]
            if nx.is_isomorphic(g, gg):
                del non_iso[i + j]
            else:
                j += 1
        i += 1
        j = 1
    return non_iso

rgs_orbit_finder(graph)

Takes a repeater graph state, and returns the full list of distinct graphs in the orbit. The first graph in the list is the original graph state.

Parameters:

Name Type Description Default
graph Graph

original graph

required

Returns:

Type Description
list

a full list of graphs in the LC orbit of the graph state

Source code in graphiq/utils/relabel_module.py
def rgs_orbit_finder(graph: nx.Graph):
    """
    Takes a repeater graph state, and returns the full list of distinct graphs in the orbit.
    The first graph in the list is the original graph state.
    :param graph: original graph
    :type graph: nx.Graph
    :return: a full list of graphs in the LC orbit of the graph state
    :rtype: list
    """
    n = len(graph)
    g = graph.copy()

    leaf_nodes = [x for x in g.nodes if g.degree(x) == 1]
    core_nodes = [x for x in g.nodes if g.degree(x) != 1]
    assert int((n / 2) + n / 2 * (n / 2 - 1) / 2) == graph.size() and len(
        leaf_nodes
    ) == int(n / 2), "input graph is not a repeater graph"

    first_core = core_nodes.pop(0)
    g_lc = local_comp_graph(g, first_core)
    orbit_list = [graph, g_lc]
    while core_nodes:
        g_lc = local_comp_graph(g_lc, core_nodes.pop(0))
        orbit_list.append(g_lc)
        g_lc_2 = local_comp_graph(g_lc, first_core)
        orbit_list.append(g_lc_2)
        if core_nodes:
            g_lc = local_comp_graph(g_lc, core_nodes.pop(0))
            orbit_list.append(g_lc)

    return orbit_list

1.4 Circuit comparison

graphiq.utils.circuit_comparison

Functions to compare quantum circuits

CNOT

Bases: ControlledPairOperationBase

CNOT gate Operation

Source code in graphiq/circuit/ops.py
class CNOT(ControlledPairOperationBase):
    """
    CNOT gate Operation
    """

    _openqasm_info = oq_lib.cnot_info()

    def __init__(
        self, control=0, control_type="e", target=0, target_type="e", noise=nm.NoNoise()
    ):
        super().__init__(control, control_type, target, target_type, noise)

CZ

Bases: ControlledPairOperationBase

Controlled-Z gate Operation

Source code in graphiq/circuit/ops.py
class CZ(ControlledPairOperationBase):
    """
    Controlled-Z gate Operation
    """

    _openqasm_info = oq_lib.cz_info()

    def __init__(
        self, control=0, control_type="e", target=0, target_type="e", noise=nm.NoNoise()
    ):
        super().__init__(control, control_type, target, target_type, noise)

CircuitStorage

Class for storing circuits, check if circuit is duplicate before adding to circuit list

Source code in graphiq/utils/circuit_comparison.py
class CircuitStorage:
    """
    Class for storing circuits, check if circuit is duplicate before adding to circuit list
    """

    def __init__(self, check_function=None, disable_circuit_comparison=False):
        self.circuit_list = []
        self.disable_circuit_comparison = disable_circuit_comparison

        if check_function:
            self._check_func = check_function
        else:
            self._check_func = check_redundant_circuit

    def add_new_circuit(self, new_circuit):
        """
        Function to add new circuit to circuit list, check if new circuit is redundant.

        :param new_circuit: new circuit to be added
        :type new_circuit: CircuitDAG
        :return: Return True if added, False otherwise.
        :rtype: bool
        """
        if self.disable_circuit_comparison:
            self.circuit_list.append(new_circuit)
            return True

        if self.is_redundant(new_circuit):
            return False

        self.circuit_list.append(new_circuit)
        return True

    def is_redundant(self, new_circuit):
        """
        Function to check if circuit is redundant.

        :param new_circuit: circuit to be checked
        :type new_circuit: CircuitDAG
        :return: True if redundant, False otherwise.
        :rtype: bool
        """
        f = self._check_func

        if self.circuit_list and f:
            for circuit in self.circuit_list:
                if f(circuit, new_circuit):
                    return True
        return False

add_new_circuit(new_circuit)

Function to add new circuit to circuit list, check if new circuit is redundant.

Parameters:

Name Type Description Default
new_circuit CircuitDAG

new circuit to be added

required

Returns:

Type Description
bool

Return True if added, False otherwise.

Source code in graphiq/utils/circuit_comparison.py
def add_new_circuit(self, new_circuit):
    """
    Function to add new circuit to circuit list, check if new circuit is redundant.

    :param new_circuit: new circuit to be added
    :type new_circuit: CircuitDAG
    :return: Return True if added, False otherwise.
    :rtype: bool
    """
    if self.disable_circuit_comparison:
        self.circuit_list.append(new_circuit)
        return True

    if self.is_redundant(new_circuit):
        return False

    self.circuit_list.append(new_circuit)
    return True

is_redundant(new_circuit)

Function to check if circuit is redundant.

Parameters:

Name Type Description Default
new_circuit CircuitDAG

circuit to be checked

required

Returns:

Type Description
bool

True if redundant, False otherwise.

Source code in graphiq/utils/circuit_comparison.py
def is_redundant(self, new_circuit):
    """
    Function to check if circuit is redundant.

    :param new_circuit: circuit to be checked
    :type new_circuit: CircuitDAG
    :return: True if redundant, False otherwise.
    :rtype: bool
    """
    f = self._check_func

    if self.circuit_list and f:
        for circuit in self.circuit_list:
            if f(circuit, new_circuit):
                return True
    return False

ClassicalCNOT

Bases: ClassicalControlledPairOperationBase

Classical CNOT gate Operation

Source code in graphiq/circuit/ops.py
class ClassicalCNOT(ClassicalControlledPairOperationBase):
    """
    Classical CNOT gate Operation
    """

    _openqasm_info = oq_lib.classical_cnot_info()

ClassicalCZ

Bases: ClassicalControlledPairOperationBase

Classical CZ gate Operation

Source code in graphiq/circuit/ops.py
class ClassicalCZ(ClassicalControlledPairOperationBase):
    """
    Classical CZ gate Operation
    """

    _openqasm_info = oq_lib.classical_cz_info()

ClassicalControlledPairOperationBase

Bases: OperationBase

This is used as a base class for our classical controlled gates (e.g. classical CNOT, classical CPHASE). Each ClassicalControlledPairOperationBase should have control and target registers/qubits specified, and a c_register target register/cbit specified.

Source code in graphiq/circuit/ops.py
class ClassicalControlledPairOperationBase(OperationBase):
    """
    This is used as a base class for our classical controlled gates (e.g. classical CNOT, classical CPHASE).
    Each ClassicalControlledPairOperationBase should have control and target registers/qubits specified, and
    a c_register target register/cbit specified.
    """

    def __init__(
        self,
        control,
        control_type,
        target,
        target_type,
        c_register=0,
        noise=nm.NoNoise(),
    ):
        """
        Creates the classically controlled gate

        :param control: the control register/qubit
        :type control: int OR tuple (of ints, length 2)
        :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type control_type: str
        :param target: target register/qubit for the Operation
        :type target: int OR tuple (of ints, length 2)
        :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
        :param c_register: the classical register/cbit
        :type c_register: int OR tuple (of ints, length 2)
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :return: nothing
        :rtype: None
        """
        if isinstance(noise, list):
            assert len(noise) == 2
        else:
            noise = 2 * [noise]
        super().__init__(
            q_registers=(control, target),
            q_registers_type=(control_type, target_type),
            c_registers=(c_register,),
            noise=noise,
        )

        self.control = control
        self.control_type = control_type
        self.target = target
        self.target_type = target_type
        self.c_register = c_register
        self.add_labels("two-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.control, self.target fields

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 2
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.control = q_reg[0]
        self.target = q_reg[1]

    @OperationBase.c_registers.setter
    def c_registers(self, c_reg):
        """
        Handle to modify the register-cbit pair on which the operation acts. This also automatically updates the
        self.c_register field

        :param c_reg: the new c_register value to set
        :raises ValueError: if the new c_reg object does not have a length of 1
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)
        self.c_register = c_reg[0]

__init__(control, control_type, target, target_type, c_register=0, noise=nm.NoNoise())

Creates the classically controlled gate

Parameters:

Name Type Description Default
control int OR tuple (of ints, length 2)

the control register/qubit

required
control_type str

'p' if photonic qubit, 'e' if emitter qubit

required
target int OR tuple (of ints, length 2)

target register/qubit for the Operation

required
target_type

'p' if photonic qubit, 'e' if emitter qubit

required
c_register int OR tuple (of ints, length 2)

the classical register/cbit

0
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def __init__(
    self,
    control,
    control_type,
    target,
    target_type,
    c_register=0,
    noise=nm.NoNoise(),
):
    """
    Creates the classically controlled gate

    :param control: the control register/qubit
    :type control: int OR tuple (of ints, length 2)
    :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type control_type: str
    :param target: target register/qubit for the Operation
    :type target: int OR tuple (of ints, length 2)
    :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
    :param c_register: the classical register/cbit
    :type c_register: int OR tuple (of ints, length 2)
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :return: nothing
    :rtype: None
    """
    if isinstance(noise, list):
        assert len(noise) == 2
    else:
        noise = 2 * [noise]
    super().__init__(
        q_registers=(control, target),
        q_registers_type=(control_type, target_type),
        c_registers=(c_register,),
        noise=noise,
    )

    self.control = control
    self.control_type = control_type
    self.target = target
    self.target_type = target_type
    self.c_register = c_register
    self.add_labels("two-qubit")

c_registers(c_reg)

Handle to modify the register-cbit pair on which the operation acts. This also automatically updates the self.c_register field

Parameters:

Name Type Description Default
c_reg

the new c_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new c_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.c_registers.setter
def c_registers(self, c_reg):
    """
    Handle to modify the register-cbit pair on which the operation acts. This also automatically updates the
    self.c_register field

    :param c_reg: the new c_register value to set
    :raises ValueError: if the new c_reg object does not have a length of 1
    :return: function returns nothing
    :rtype: None
    """
    self._update_c_reg(c_reg)
    self.c_register = c_reg[0]

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.control, self.target fields

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 2

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.control, self.target fields

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 2
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.control = q_reg[0]
    self.target = q_reg[1]

ControlledPairOperationBase

Bases: OperationBase

This is used as a base class for our quantum controlled gates (e.g. CNOT, CPHASE). Each ControlledPairOperationBase should have control and target registers/qubits specified

Source code in graphiq/circuit/ops.py
class ControlledPairOperationBase(OperationBase):
    """
    This is used as a base class for our quantum controlled gates (e.g. CNOT, CPHASE). Each ControlledPairOperationBase
    should have control and target registers/qubits specified
    """

    def __init__(self, control, control_type, target, target_type, noise=nm.NoNoise()):
        """
        Creates a control gate object

        :param control: control register/qubit for the Operation
        :type control: int OR tuple (of ints, length 2)
        :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type control_type: str
        :param target: target register/qubit for the Operation
        :type target: int OR tuple (of ints, length 2)
        :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type target_type: str
        :return: function returns nothing
        :rtype: None
        """

        if isinstance(noise, list):
            assert len(noise) == 2
        else:
            noise = 2 * [noise]
        super().__init__(
            q_registers=(control, target),
            q_registers_type=(control_type, target_type),
            noise=noise,
        )

        self.control = control
        self.control_type = control_type
        self.target = target
        self.target_type = target_type
        self.add_labels("two-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.control, self.target fields

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 2
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)

        self.control = q_reg[0]
        self.target = q_reg[1]

__init__(control, control_type, target, target_type, noise=nm.NoNoise())

Creates a control gate object

Parameters:

Name Type Description Default
control int OR tuple (of ints, length 2)

control register/qubit for the Operation

required
control_type str

'p' if photonic qubit, 'e' if emitter qubit

required
target int OR tuple (of ints, length 2)

target register/qubit for the Operation

required
target_type str

'p' if photonic qubit, 'e' if emitter qubit

required

Returns:

Type Description
None

function returns nothing

Source code in graphiq/circuit/ops.py
def __init__(self, control, control_type, target, target_type, noise=nm.NoNoise()):
    """
    Creates a control gate object

    :param control: control register/qubit for the Operation
    :type control: int OR tuple (of ints, length 2)
    :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type control_type: str
    :param target: target register/qubit for the Operation
    :type target: int OR tuple (of ints, length 2)
    :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type target_type: str
    :return: function returns nothing
    :rtype: None
    """

    if isinstance(noise, list):
        assert len(noise) == 2
    else:
        noise = 2 * [noise]
    super().__init__(
        q_registers=(control, target),
        q_registers_type=(control_type, target_type),
        noise=noise,
    )

    self.control = control
    self.control_type = control_type
    self.target = target
    self.target_type = target_type
    self.add_labels("two-qubit")

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.control, self.target fields

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 2

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.control, self.target fields

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 2
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)

    self.control = q_reg[0]
    self.target = q_reg[1]

Hadamard

Bases: OneQubitOperationBase

Hadamard gate Operation

Source code in graphiq/circuit/ops.py
class Hadamard(OneQubitOperationBase):
    """
    Hadamard gate Operation
    """

    _openqasm_info = oq_lib.hadamard_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

Identity

Bases: OneQubitOperationBase

Identity Operation

Source code in graphiq/circuit/ops.py
class Identity(OneQubitOperationBase):
    """
    Identity Operation
    """

    _openqasm_info = oq_lib.empty_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

Input

Bases: InputOutputOperationBase

Input Operation. Serves as a placeholder in the circuit so that we know that this is where a given qubit/cbit is introduced (i.e. there are no prior operations on it)

Source code in graphiq/circuit/ops.py
class Input(InputOutputOperationBase):
    """
    Input Operation. Serves as a placeholder in the circuit so that we know that this is where a given
    qubit/cbit is introduced (i.e. there are no prior operations on it)
    """

    def __init__(self, register=None, reg_type="e"):
        super().__init__(register, reg_type=reg_type)

InputOutputOperationBase

Bases: OperationBase

This is used as a base class for our Input and Output Operations. These operations largely act as "dummy Operations" signalling the input / output of a state (useful for circuit DAG representation, for example)

Source code in graphiq/circuit/ops.py
class InputOutputOperationBase(OperationBase):
    """
    This is used as a base class for our Input and Output Operations. These operations largely act as "dummy Operations"
    signalling the input / output of a state (useful for circuit DAG representation, for example)
    """

    # IO Operations don't need openqasm representations, since they are dummy gates useful to circuit representation
    # and do not actually modify the circuit output
    _openqasm_info = oq_lib.empty_info()

    def __init__(self, register, reg_type, noise=nm.NoNoise()):
        """
        Creates an IO base class Operation

        :param register: the register/qubit which this I/O operation acts on
        :type register: int OR tuple (of ints, length 2)
        :param reg_type: the input/output is for a quantum photonic register if 'p',
                         for a quantum emitter register if 'e',
                         and for a classical register if 'c'
        :type reg_type: str
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :return: nothing
        :rtype: None
        """
        if reg_type == "p" or reg_type == "e":
            super().__init__(
                q_registers=(register,), q_registers_type=(reg_type,), noise=noise
            )
        elif reg_type == "c":
            super().__init__(c_registers=(register,), noise=noise)
        else:
            raise ValueError(
                "Register type must be either quantum photonic (reg_type='p'), "
                "quantum emitter (reg_type='e'), or classical (reg_type='c')"
            )
        self.reg_type = reg_type
        self.register = register

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.register field, if the I/O is quantum

        :param q_reg: the new q_register value to set (if any)
        :raises ValueError: if the new q_reg object does not match the length of self.q_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.register = q_reg[0]

    @OperationBase.c_registers.setter
    def c_registers(self, c_reg):
        """
        Handle to modify the register-cbit pairs on which the operation acts. This also automatically updates the
        self.register field, if the I/O is classical

        :param c_reg: the new c_register value to set (if any)
        :raises ValueError: if the new c_reg object does not match the length of self.c_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)
        if self.reg_type == "c":
            self.register = c_reg[0]

__init__(register, reg_type, noise=nm.NoNoise())

Creates an IO base class Operation

Parameters:

Name Type Description Default
register int OR tuple (of ints, length 2)

the register/qubit which this I/O operation acts on

required
reg_type str

the input/output is for a quantum photonic register if 'p', for a quantum emitter register if 'e', and for a classical register if 'c'

required
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def __init__(self, register, reg_type, noise=nm.NoNoise()):
    """
    Creates an IO base class Operation

    :param register: the register/qubit which this I/O operation acts on
    :type register: int OR tuple (of ints, length 2)
    :param reg_type: the input/output is for a quantum photonic register if 'p',
                     for a quantum emitter register if 'e',
                     and for a classical register if 'c'
    :type reg_type: str
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :return: nothing
    :rtype: None
    """
    if reg_type == "p" or reg_type == "e":
        super().__init__(
            q_registers=(register,), q_registers_type=(reg_type,), noise=noise
        )
    elif reg_type == "c":
        super().__init__(c_registers=(register,), noise=noise)
    else:
        raise ValueError(
            "Register type must be either quantum photonic (reg_type='p'), "
            "quantum emitter (reg_type='e'), or classical (reg_type='c')"
        )
    self.reg_type = reg_type
    self.register = register

c_registers(c_reg)

Handle to modify the register-cbit pairs on which the operation acts. This also automatically updates the self.register field, if the I/O is classical

Parameters:

Name Type Description Default
c_reg

the new c_register value to set (if any)

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new c_reg object does not match the length of self.c_registers (Operations should not have variable register numbers)

Source code in graphiq/circuit/ops.py
@OperationBase.c_registers.setter
def c_registers(self, c_reg):
    """
    Handle to modify the register-cbit pairs on which the operation acts. This also automatically updates the
    self.register field, if the I/O is classical

    :param c_reg: the new c_register value to set (if any)
    :raises ValueError: if the new c_reg object does not match the length of self.c_registers (Operations
                       should not have variable register numbers)
    :return: function returns nothing
    :rtype: None
    """
    self._update_c_reg(c_reg)
    if self.reg_type == "c":
        self.register = c_reg[0]

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.register field, if the I/O is quantum

Parameters:

Name Type Description Default
q_reg

the new q_register value to set (if any)

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not match the length of self.q_registers (Operations should not have variable register numbers)

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.register field, if the I/O is quantum

    :param q_reg: the new q_register value to set (if any)
    :raises ValueError: if the new q_reg object does not match the length of self.q_registers (Operations
                       should not have variable register numbers)
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.register = q_reg[0]

MeasurementCNOTandReset

Bases: ClassicalControlledPairOperationBase

Measurement-controlled X gate Operation with resetting the control qubit after measurement

Source code in graphiq/circuit/ops.py
class MeasurementCNOTandReset(ClassicalControlledPairOperationBase):
    """
    Measurement-controlled X gate Operation with resetting the control qubit after measurement
    """

    _openqasm_info = oq_lib.measurement_cnot_and_reset()

    def __init__(
        self,
        control: object = 0,
        control_type: object = "e",
        target: object = 0,
        target_type: object = "p",
        c_register: object = 0,
        noise: object = nm.NoNoise(),
    ) -> object:
        super().__init__(control, control_type, target, target_type, c_register, noise)

MeasurementZ

Bases: OperationBase

Z Measurement Operation

Source code in graphiq/circuit/ops.py
class MeasurementZ(OperationBase):
    """
    Z Measurement Operation
    """

    # TODO: maybe create a base class for measurements in the future IFF we also want to support other measurements

    _openqasm_info = oq_lib.z_measurement_info()

    def __init__(self, register=0, reg_type="e", c_register=0, noise=nm.NoNoise()):
        """
        Creates a Z measurement Operation

        :param register: the quantum register on which the measurement is performed
        :type register: int OR tuple (of ints, length 2)
        :param reg_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type reg_type: str
        :param c_register: the classical register to which the measurement result is saved
        :type c_register: int OR tuple (of ints, length 2)
        :param noise: Noise model
        :type noise: src.noise.noise_models.NoiseBase
        :return: this function returns nothing
        :rtype: None
        """
        super().__init__(
            q_registers=(register,),
            q_registers_type=(reg_type,),
            c_registers=(c_register,),
            noise=noise,
        )

        self.register = register
        self.reg_type = reg_type
        self.c_register = c_register
        self.add_labels("one-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pair which is measured. This also automatically updates the
        self.register field

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 1
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.register = q_reg[0]

    @OperationBase.c_registers.setter
    def c_registers(self, c_reg):
        """
        Handle to modify the register-cbit pair to which measurements are saved. This also automatically updates the
        self.c_register field

        :param c_reg: the new c_register value to set
        :raises ValueError: if the new c_reg object does not have a length of 1
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)
        self.c_register = c_reg[0]

__init__(register=0, reg_type='e', c_register=0, noise=nm.NoNoise())

Creates a Z measurement Operation

Parameters:

Name Type Description Default
register int OR tuple (of ints, length 2)

the quantum register on which the measurement is performed

0
reg_type str

'p' if photonic qubit, 'e' if emitter qubit

'e'
c_register int OR tuple (of ints, length 2)

the classical register to which the measurement result is saved

0
noise src.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

this function returns nothing

Source code in graphiq/circuit/ops.py
def __init__(self, register=0, reg_type="e", c_register=0, noise=nm.NoNoise()):
    """
    Creates a Z measurement Operation

    :param register: the quantum register on which the measurement is performed
    :type register: int OR tuple (of ints, length 2)
    :param reg_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type reg_type: str
    :param c_register: the classical register to which the measurement result is saved
    :type c_register: int OR tuple (of ints, length 2)
    :param noise: Noise model
    :type noise: src.noise.noise_models.NoiseBase
    :return: this function returns nothing
    :rtype: None
    """
    super().__init__(
        q_registers=(register,),
        q_registers_type=(reg_type,),
        c_registers=(c_register,),
        noise=noise,
    )

    self.register = register
    self.reg_type = reg_type
    self.c_register = c_register
    self.add_labels("one-qubit")

c_registers(c_reg)

Handle to modify the register-cbit pair to which measurements are saved. This also automatically updates the self.c_register field

Parameters:

Name Type Description Default
c_reg

the new c_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new c_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.c_registers.setter
def c_registers(self, c_reg):
    """
    Handle to modify the register-cbit pair to which measurements are saved. This also automatically updates the
    self.c_register field

    :param c_reg: the new c_register value to set
    :raises ValueError: if the new c_reg object does not have a length of 1
    :return: function returns nothing
    :rtype: None
    """
    self._update_c_reg(c_reg)
    self.c_register = c_reg[0]

q_registers(q_reg)

Handle to modify the register-qubit pair which is measured. This also automatically updates the self.register field

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pair which is measured. This also automatically updates the
    self.register field

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 1
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.register = q_reg[0]

OneQubitGateWrapper

Bases: OneQubitOperationBase

This wrapper class allows us to compose a list of one-qubit operation and treat them as a single component within the circuit (this allows us, for example, to create every local Clifford gate with other gates, without having to separately implement every combination within the compiler)

Source code in graphiq/circuit/ops.py
class OneQubitGateWrapper(OneQubitOperationBase):
    """
    This wrapper class allows us to compose a list of one-qubit operation and treat them as a single component
    within the circuit (this allows us, for example, to create every local Clifford gate with other gates, without
    having to separately implement every combination within the compiler)
    """

    def __init__(self, operations: list, register=0, reg_type="e", noise=nm.NoNoise()):
        if isinstance(noise, nm.NoNoise):
            noise = len(operations) * [nm.NoNoise()]
        super().__init__(register, reg_type, noise)
        if len(operations) == 0:
            raise ValueError(
                "Operation list for the single qubit gate wrapper must be of length 1 or more"
            )
        for op_class in operations:
            assert issubclass(op_class, OneQubitOperationBase)
            # can only contain base classes
            assert not isinstance(op_class, OneQubitGateWrapper)
        self.operations = operations
        self._openqasm_info = oq_lib.single_qubit_wrapper_info(operations)

    def unwrap(self):
        """
        Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
        of multiple other operations) in the reverse order, which corresponds to the order of applying gates

        :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
        :rtype: list
        """
        if isinstance(self.noise, list):
            assert len(self.noise) == len(self.operations)

            gates = [
                self.operations[i](
                    register=self.register, reg_type=self.reg_type, noise=self.noise[i]
                )
                for i in range(len(self.operations))
            ]
        else:
            gates = [
                self.operations[i](
                    register=self.register, reg_type=self.reg_type, noise=nm.NoNoise()
                )
                for i in range(len(self.operations))
            ]
            noise = Identity(
                register=self.register, reg_type=self.reg_type, noise=self.noise
            )
            if self.noise.noise_parameters["After gate"]:
                gates.insert(0, noise)
            else:
                gates.append(noise)
        return gates[::-1]

    def openqasm_info(self):
        return self._openqasm_info

unwrap()

Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed of multiple other operations) in the reverse order, which corresponds to the order of applying gates

Returns:

Type Description
list

a sequence of base operations (i.e. operations which are not compositions of other operations)

Source code in graphiq/circuit/ops.py
def unwrap(self):
    """
    Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
    of multiple other operations) in the reverse order, which corresponds to the order of applying gates

    :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
    :rtype: list
    """
    if isinstance(self.noise, list):
        assert len(self.noise) == len(self.operations)

        gates = [
            self.operations[i](
                register=self.register, reg_type=self.reg_type, noise=self.noise[i]
            )
            for i in range(len(self.operations))
        ]
    else:
        gates = [
            self.operations[i](
                register=self.register, reg_type=self.reg_type, noise=nm.NoNoise()
            )
            for i in range(len(self.operations))
        ]
        noise = Identity(
            register=self.register, reg_type=self.reg_type, noise=self.noise
        )
        if self.noise.noise_parameters["After gate"]:
            gates.insert(0, noise)
        else:
            gates.append(noise)
    return gates[::-1]

OneQubitOperationBase

Bases: OperationBase

This is used as a base class for any one-qubit operation (one-qubit operations should all depend on a single parameter, "register"

Source code in graphiq/circuit/ops.py
class OneQubitOperationBase(OperationBase):
    """
    This is used as a base class for any one-qubit operation (one-qubit operations should
    all depend on a single parameter, "register"
    """

    def __init__(self, register, reg_type, noise=nm.NoNoise()):
        """
        Creates a one-qubit operation base class object

        :param register: the (quantum) register on which the single-qubit operation acts
        :type register: int OR tuple (of ints, length 2)
        :param reg_type: 'e' if emitter qubit, 'p' if a photonic qubit
        :type reg_type: str
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :return: nothing
        :rtype: None
        """
        super().__init__(
            q_registers=(register,), q_registers_type=(reg_type,), noise=noise
        )
        self.register = register
        self.reg_type = reg_type
        self.add_labels("one-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.register field

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 1
        :return: nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.register = q_reg[0]

__init__(register, reg_type, noise=nm.NoNoise())

Creates a one-qubit operation base class object

Parameters:

Name Type Description Default
register int OR tuple (of ints, length 2)

the (quantum) register on which the single-qubit operation acts

required
reg_type str

'e' if emitter qubit, 'p' if a photonic qubit

required
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def __init__(self, register, reg_type, noise=nm.NoNoise()):
    """
    Creates a one-qubit operation base class object

    :param register: the (quantum) register on which the single-qubit operation acts
    :type register: int OR tuple (of ints, length 2)
    :param reg_type: 'e' if emitter qubit, 'p' if a photonic qubit
    :type reg_type: str
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :return: nothing
    :rtype: None
    """
    super().__init__(
        q_registers=(register,), q_registers_type=(reg_type,), noise=noise
    )
    self.register = register
    self.reg_type = reg_type
    self.add_labels("one-qubit")

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.register field

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.register field

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 1
    :return: nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.register = q_reg[0]

OperationBase

Bases: ABC

Base class from which operations will inherit

Source code in graphiq/circuit/ops.py
class OperationBase(ABC):
    """
    Base class from which operations will inherit
    """

    _openqasm_info = None  # This is the information necessary to add our Operation into an openQASM script

    # If _openqasm_info is None, a given operation cannot be added to openQASM

    def __init__(
        self,
        q_registers=tuple(),
        q_registers_type=tuple(),
        c_registers=tuple(),
        noise=nm.NoNoise(),
        params=tuple(),
        param_info=dict(),
    ):
        """
        Creates an Operation base object (which is largely responsible for holding the registers on which
        an operation act--this provides a consistent API for the circuit class to use when dealing with
        any arbitrary operation).

        :param q_registers: (a, ..., b) indices (indicating registers a, ..., b) OR
                            ((a, b), ...) indicating photonic qubit b of register a OR
                            any combination of (reg, bit) notation and reg notation
                            These tuples can be of length 1 or empty as well, depending on the number
                            of registers the gate requires
        :type q_registers: tuple (tuple of: integers OR tuples of length 2)
        :param q_registers_type: tuple of strings, each is either 'p' (photonic qubit) or 'e' (emitter qubit)
        :type q_registers: tuple (of str)
        :param c_registers: same as photon/emitter_registers, but for the classical registers
        :type c_registers: tuple (tuple of: integers OR tuples of length 2)
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :param params: an ordered list of parameter values for a parameterized gate
        :type params: tuple
        :param param_info: a dictionary that specifies all the information regarding parameters
        :type param_info: dict
        :raises AssertionError: if photon_register, emitter_register, c_registers are not tuples,
                               OR if the elements of the tuple do not correspond to the notation described above
        :return: nothing
        :rtype: None
        """
        assert isinstance(q_registers, tuple)
        assert isinstance(q_registers_type, tuple)
        assert len(q_registers) == len(q_registers_type)
        assert isinstance(c_registers, tuple)

        for q, reg_type in zip(q_registers, q_registers_type):
            assert isinstance(q, int) or (
                isinstance(q, tuple)
                and len(q) == 2
                and isinstance(q[0], int)
                and isinstance(q[1], int)
            ), f"Invalid photon_registers: photon_registers tuple must only contain tuples of length 2 or integers"
            assert reg_type == "e" or reg_type == "p"

        for c in c_registers:
            assert isinstance(c, int) or (
                isinstance(c, tuple)
                and len(c) == 2
                and isinstance(c[0], int)
                and isinstance(c[1], int)
            ), f"Invalid c_register: c_register tuple must only contain tuples of length 2 or integers"

        self._q_registers = q_registers
        self._q_registers_type = q_registers_type
        self._c_registers = c_registers
        self._labels = []
        self.noise = noise
        self.params = params
        self.param_info = param_info

    @classmethod
    def openqasm_info(cls):
        """
        Returns the information needed to generate an openQASM script using this operation
        (needed imports, how to define a gate, how to use a gate), packaged as a single object.
        This is used by the Circuit classes to generate openQASM scripts when needed

        :return: the operation class's openqasm information (possibly None)
        """
        if cls._openqasm_info is None:
            raise ValueError("Operation does not have an openQASM translation")
        return cls._openqasm_info

    @property
    def q_registers(self):
        """
        Returns the quantum registers tuple of the Operation class

        :return: the registers tuple
        :rtype: tuple
        """
        return self._q_registers

    @property
    def q_registers_type(self):
        """
        Returns the quantum registers types ('e' for emitter, 'p' for photons) tuple of the Operation class

        :return: the registers type
        :rtype: tuple
        """
        return self._q_registers_type

    @property
    def c_registers(self):
        """
        Returns the c_registers tuple of the Operation class

        :return: the c_registers_tuple
        :rtype: tuple
        """
        return self._c_registers

    @q_registers.setter
    def q_registers(self, q_reg):
        """
        Allows us to change the emitter_registers object on which an Operation acts
        This should only be used by the circuit class!
        Subclass-specific implementations of this function also update other
        register-related fields (e.g. register, target, control) automatically
        when photon_registers is updated

        :param q_reg: new emitter_register which the operation should target
        :type q_reg: tuple
        :raises ValueError: if the new q_reg object does not match the length of self.emitter_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)

    @c_registers.setter
    def c_registers(self, c_reg):
        """
        Allows us to change the c_registers object on which an Operation acts
        This should only be used by the circuit class!
        Subclass-specific implementations of this function also update other
        register-related fields (e.g. c_register) automatically
        when c_registers is updated

        :param c_reg: new c_register which the operation should target
        :type c_reg: tuple
        :raises ValueError: if the new q_reg object does not match the length of self.q_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)

    @q_registers_type.setter
    def q_registers_type(self, q_regs_type):
        self._q_registers_type = q_regs_type

    @property
    def labels(self):
        """
        Returns the list of labels of this operation

        :return: list of labels
        :rtype: list[str]
        """
        return self._labels

    @labels.setter
    def labels(self, new_labels):
        """
        Sets the labels to be new_labels

        :return: nothing
        :rtype: None
        """
        self._labels = new_labels

    def add_labels(self, new_labels):
        """
        Adds new labels to existing labels

        :return: nothing
        :rtype: None
        """
        if isinstance(new_labels, list):
            self._labels += new_labels
        else:
            self._labels.append(new_labels)

    def unwrap(self):
        """
        Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
        of multiple other operations) in the reverse order, which corresponds to the order of applying operations.

        :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
        :rtype: list
        """
        return [self][::-1]

    def _update_q_reg(self, q_reg):
        """
        Helper function to verify the validity of the new q_registers tuple and to update the fields
        This is broken into a separate function because subclasses will need to use this as well,
        and using the super() keyword gets messy with class properties

        :param q_reg: new emitter_register which the operation should target
        :type q_reg: tuple
        :raises ValueError: if the new q_reg object does not match the length of self.emitter_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        if len(q_reg) != len(self._q_registers):
            raise ValueError(
                f"The number of quantum registers on which the operation acts cannot be changed!"
            )
        self._q_registers = q_reg

    def _update_c_reg(self, c_reg):
        """
        Helper function to verify the validity of the new c_registers tuple and to update the fields
        This is broken into a separate function because subclasses will need to use this as well,
        and using the super() keyword gets messy with class properties

        :param c_reg: new c_register which the operation should target
        :type c_reg: tuple
        :raises ValueError: if the new c_reg object does not match the length of self.c_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        if len(c_reg) != len(self._c_registers):
            raise ValueError(
                f"The number of classical registers on which the operation acts cannot be changed!"
            )
        self._c_registers = c_reg

    def parse_q_reg_types(self):
        """
        Find a proper string description of the register types relevant for this operation

        :raises ValueError: if the quantum register type is not supported
        :return: a string description
        :rtype: str
        """
        type_description = ""
        for i in range(len(self.q_registers_type)):
            if self.q_registers_type[i] == "e":
                type_description += "Emitter-"
            elif self.q_registers_type[i] == "p":
                type_description += "Photonic-"
            else:
                raise ValueError("Detected a non-supported quantum register type.")

        return type_description[:-1]

    @staticmethod
    def _validate_param_info(param_info, n_params):
        """
        Validate the param_info

        :param param_info: param_info for a gate
        :type param_info: None or dict
        :param n_params: number of parameters
        :type n_params: int
        :return: whether the param_info is valid
        :rtype: bool
        """
        if param_info is None:
            return True
        else:
            if not isinstance(param_info, dict):
                return False
            else:
                if ("bounds" not in param_info.keys()) or (
                    "labels" not in param_info.keys()
                ):
                    return False
                else:
                    return (
                        len(param_info["bounds"]) == n_params
                        and len(param_info["labels"]) == n_params
                    )

c_registers property writable

Returns the c_registers tuple of the Operation class

Returns:

Type Description
tuple

the c_registers_tuple

labels property writable

Returns the list of labels of this operation

Returns:

Type Description
list[str]

list of labels

q_registers property writable

Returns the quantum registers tuple of the Operation class

Returns:

Type Description
tuple

the registers tuple

q_registers_type property writable

Returns the quantum registers types ('e' for emitter, 'p' for photons) tuple of the Operation class

Returns:

Type Description
tuple

the registers type

__init__(q_registers=tuple(), q_registers_type=tuple(), c_registers=tuple(), noise=nm.NoNoise(), params=tuple(), param_info=dict())

Creates an Operation base object (which is largely responsible for holding the registers on which an operation act--this provides a consistent API for the circuit class to use when dealing with any arbitrary operation).

Parameters:

Name Type Description Default
q_registers tuple (tuple of: integers OR tuples of length 2)

(a, ..., b) indices (indicating registers a, ..., b) OR ((a, b), ...) indicating photonic qubit b of register a OR any combination of (reg, bit) notation and reg notation These tuples can be of length 1 or empty as well, depending on the number of registers the gate requires

tuple()
q_registers_type

tuple of strings, each is either 'p' (photonic qubit) or 'e' (emitter qubit)

tuple()
c_registers tuple (tuple of: integers OR tuples of length 2)

same as photon/emitter_registers, but for the classical registers

tuple()
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()
params tuple

an ordered list of parameter values for a parameterized gate

tuple()
param_info dict

a dictionary that specifies all the information regarding parameters

dict()

Returns:

Type Description
None

nothing

Raises:

Type Description
AssertionError

if photon_register, emitter_register, c_registers are not tuples, OR if the elements of the tuple do not correspond to the notation described above

Source code in graphiq/circuit/ops.py
def __init__(
    self,
    q_registers=tuple(),
    q_registers_type=tuple(),
    c_registers=tuple(),
    noise=nm.NoNoise(),
    params=tuple(),
    param_info=dict(),
):
    """
    Creates an Operation base object (which is largely responsible for holding the registers on which
    an operation act--this provides a consistent API for the circuit class to use when dealing with
    any arbitrary operation).

    :param q_registers: (a, ..., b) indices (indicating registers a, ..., b) OR
                        ((a, b), ...) indicating photonic qubit b of register a OR
                        any combination of (reg, bit) notation and reg notation
                        These tuples can be of length 1 or empty as well, depending on the number
                        of registers the gate requires
    :type q_registers: tuple (tuple of: integers OR tuples of length 2)
    :param q_registers_type: tuple of strings, each is either 'p' (photonic qubit) or 'e' (emitter qubit)
    :type q_registers: tuple (of str)
    :param c_registers: same as photon/emitter_registers, but for the classical registers
    :type c_registers: tuple (tuple of: integers OR tuples of length 2)
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :param params: an ordered list of parameter values for a parameterized gate
    :type params: tuple
    :param param_info: a dictionary that specifies all the information regarding parameters
    :type param_info: dict
    :raises AssertionError: if photon_register, emitter_register, c_registers are not tuples,
                           OR if the elements of the tuple do not correspond to the notation described above
    :return: nothing
    :rtype: None
    """
    assert isinstance(q_registers, tuple)
    assert isinstance(q_registers_type, tuple)
    assert len(q_registers) == len(q_registers_type)
    assert isinstance(c_registers, tuple)

    for q, reg_type in zip(q_registers, q_registers_type):
        assert isinstance(q, int) or (
            isinstance(q, tuple)
            and len(q) == 2
            and isinstance(q[0], int)
            and isinstance(q[1], int)
        ), f"Invalid photon_registers: photon_registers tuple must only contain tuples of length 2 or integers"
        assert reg_type == "e" or reg_type == "p"

    for c in c_registers:
        assert isinstance(c, int) or (
            isinstance(c, tuple)
            and len(c) == 2
            and isinstance(c[0], int)
            and isinstance(c[1], int)
        ), f"Invalid c_register: c_register tuple must only contain tuples of length 2 or integers"

    self._q_registers = q_registers
    self._q_registers_type = q_registers_type
    self._c_registers = c_registers
    self._labels = []
    self.noise = noise
    self.params = params
    self.param_info = param_info

add_labels(new_labels)

Adds new labels to existing labels

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def add_labels(self, new_labels):
    """
    Adds new labels to existing labels

    :return: nothing
    :rtype: None
    """
    if isinstance(new_labels, list):
        self._labels += new_labels
    else:
        self._labels.append(new_labels)

openqasm_info() classmethod

Returns the information needed to generate an openQASM script using this operation (needed imports, how to define a gate, how to use a gate), packaged as a single object. This is used by the Circuit classes to generate openQASM scripts when needed

Returns:

Type Description

the operation class's openqasm information (possibly None)

Source code in graphiq/circuit/ops.py
@classmethod
def openqasm_info(cls):
    """
    Returns the information needed to generate an openQASM script using this operation
    (needed imports, how to define a gate, how to use a gate), packaged as a single object.
    This is used by the Circuit classes to generate openQASM scripts when needed

    :return: the operation class's openqasm information (possibly None)
    """
    if cls._openqasm_info is None:
        raise ValueError("Operation does not have an openQASM translation")
    return cls._openqasm_info

parse_q_reg_types()

Find a proper string description of the register types relevant for this operation

Returns:

Type Description
str

a string description

Raises:

Type Description
ValueError

if the quantum register type is not supported

Source code in graphiq/circuit/ops.py
def parse_q_reg_types(self):
    """
    Find a proper string description of the register types relevant for this operation

    :raises ValueError: if the quantum register type is not supported
    :return: a string description
    :rtype: str
    """
    type_description = ""
    for i in range(len(self.q_registers_type)):
        if self.q_registers_type[i] == "e":
            type_description += "Emitter-"
        elif self.q_registers_type[i] == "p":
            type_description += "Photonic-"
        else:
            raise ValueError("Detected a non-supported quantum register type.")

    return type_description[:-1]

unwrap()

Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed of multiple other operations) in the reverse order, which corresponds to the order of applying operations.

Returns:

Type Description
list

a sequence of base operations (i.e. operations which are not compositions of other operations)

Source code in graphiq/circuit/ops.py
def unwrap(self):
    """
    Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
    of multiple other operations) in the reverse order, which corresponds to the order of applying operations.

    :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
    :rtype: list
    """
    return [self][::-1]

Output

Bases: InputOutputOperationBase

Input Operation. Serves as a placeholder in the circuit so that we know that this is the final operation on a qubit/cbit (i.e. there are no subsequent operations on it)

Source code in graphiq/circuit/ops.py
class Output(InputOutputOperationBase):
    """
    Input Operation. Serves as a placeholder in the circuit so that we know that this is the final operation on a
    qubit/cbit (i.e. there are no subsequent operations on it)
    """

    def __init__(self, register=None, reg_type="e"):
        super().__init__(register, reg_type=reg_type)

ParameterizedControlledRotationQubit

Bases: ControlledPairOperationBase

Parameterized two qubit controlled gate,

Source code in graphiq/circuit/ops.py
class ParameterizedControlledRotationQubit(ControlledPairOperationBase):
    """
    Parameterized two qubit controlled gate,
    """

    _openqasm_info = (
        oq_lib.cparameterized_info()
    )  # todo, change to appropriate openQASM info

    def __init__(
        self,
        control=0,
        control_type="e",
        target=0,
        target_type="e",
        noise=nm.NoNoise(),
        params=None,
        param_info=None,
    ):
        super().__init__(control, control_type, target, target_type, noise)
        if params is None:
            params = (0.0, 0.0, 0.0)
        else:
            if len(params) != 3:
                raise ValueError("Length of params must be 3")

        if param_info is None:
            param_info = {
                "bounds": ((-np.pi, np.pi), (-np.pi, np.pi), (-np.pi, np.pi)),
                "labels": ("theta", "phi", "lambda"),
            }
        else:
            if not self._validate_param_info(param_info, 3):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

ParameterizedOneQubitRotation

Bases: OneQubitOperationBase

Parameterized one qubit rotation.

Source code in graphiq/circuit/ops.py
class ParameterizedOneQubitRotation(OneQubitOperationBase):
    """
    Parameterized one qubit rotation.
    """

    _openqasm_info = (
        oq_lib.parameterized_info()
    )  # todo, change to appropriate openQASM info

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0, 0.0, 0.0)

        else:
            if len(params) != 3:
                raise ValueError("Length of params must be 3")

        if param_info is None:
            param_info = {
                "bounds": ((-np.pi, np.pi), (-np.pi, np.pi), (-np.pi, np.pi)),
                "labels": ("theta", "phi", "lambda"),
            }
        else:
            if not self._validate_param_info(param_info, 3):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

Phase

Bases: OneQubitOperationBase

Phase gate operation, P = diag(1, i)

Source code in graphiq/circuit/ops.py
class Phase(OneQubitOperationBase):
    """
    Phase gate operation, P = diag(1, i)
    """

    _openqasm_info = oq_lib.phase_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

PhaseDagger

Bases: OneQubitOperationBase

Phase gate operation, P_dag = diag(1, -i)

Source code in graphiq/circuit/ops.py
class PhaseDagger(OneQubitOperationBase):
    """
    Phase gate operation, P_dag = diag(1, -i)
    """

    _openqasm_info = oq_lib.phase_dagger_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

RX

Bases: ParameterizedOneQubitRotation

Rotation around the X axis

Source code in graphiq/circuit/ops.py
class RX(ParameterizedOneQubitRotation):
    """
    Rotation around the X axis
    """

    _openqasm_info = oq_lib.rx_info()

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0,)

        else:
            if len(params) != 1:
                raise ValueError("Length of params must be 1")

        if param_info is None:
            param_info = {
                "bounds": (-np.pi, np.pi),
                "labels": "theta",
            }
        else:
            if not self._validate_param_info(param_info, 1):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

RY

Bases: ParameterizedOneQubitRotation

Rotation around the Y axis

Source code in graphiq/circuit/ops.py
class RY(ParameterizedOneQubitRotation):
    """
    Rotation around the Y axis
    """

    _openqasm_info = oq_lib.ry_info()

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0,)
        else:
            if len(params) != 1:
                raise ValueError("Length of params must be 1")
        if param_info is None:
            param_info = {
                "bounds": (-np.pi, np.pi),
                "labels": "theta",
            }
        else:
            if not self._validate_param_info(param_info, 1):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

RZ

Bases: ParameterizedOneQubitRotation

Rotation around the Z axis

Source code in graphiq/circuit/ops.py
class RZ(ParameterizedOneQubitRotation):
    """
    Rotation around the Z axis
    """

    _openqasm_info = oq_lib.rz_info()

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0,)

        else:
            if len(params) != 1:
                raise ValueError("Length of params must be 1")
        if param_info is None:
            param_info = {
                "bounds": (-np.pi, np.pi),
                "labels": "phi",
            }
        else:
            if not self._validate_param_info(param_info, 1):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

SigmaX

Bases: OneQubitOperationBase

Pauli X gate Operation

Source code in graphiq/circuit/ops.py
class SigmaX(OneQubitOperationBase):
    """
    Pauli X gate Operation
    """

    _openqasm_info = oq_lib.sigma_x_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

SigmaY

Bases: OneQubitOperationBase

Pauli Y gate Operation

Source code in graphiq/circuit/ops.py
class SigmaY(OneQubitOperationBase):
    """
    Pauli Y gate Operation
    """

    _openqasm_info = oq_lib.sigma_y_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

SigmaZ

Bases: OneQubitOperationBase

Pauli Z gate Operation

Source code in graphiq/circuit/ops.py
class SigmaZ(OneQubitOperationBase):
    """
    Pauli Z gate Operation
    """

    _openqasm_info = oq_lib.sigma_z_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

add_control_target_to_dag(circuit)

Process the input circuit DAG and add control_target attribute to edges.

Parameters:

Name Type Description Default
circuit CircuitDAG

circuit

required

Returns:

Type Description
None

nothing

Source code in graphiq/utils/circuit_comparison.py
def add_control_target_to_dag(circuit):
    """
    Process the input circuit DAG and add control_target attribute to edges.

    :param circuit: circuit
    :type circuit: CircuitDAG
    :return: nothing
    :rtype: None
    """

    for node in circuit.node_dict["Input"]:
        op = circuit.dag.nodes[node]["op"]
        reg_type = op.reg_type
        register = op.register

        out_edges = circuit.dag.out_edges(nbunch=node, keys=True)
        edge = circuit.edge_from_reg(out_edges, f"{reg_type}{register}")
        next_node = edge[1]
        label = edge[2]

        while next_node not in circuit.node_dict["Output"]:
            op = circuit.dag.nodes[next_node]["op"]
            control_target = _create_edge_control_target_attr(op, reg_type, register)
            circuit.dag[node][next_node][label]["control_target"] = control_target

            node = next_node
            out_edges = circuit.dag.out_edges(nbunch=node, keys=True)
            edge = circuit.edge_from_reg(out_edges, f"{reg_type}{register}")
            next_node = edge[1]
            label = edge[2]

        control_target = _create_edge_control_target_attr(op, reg_type, register)
        circuit.dag[node][next_node][label]["control_target"] = control_target

circuit_is_isomorphic(circuit1, circuit2)

Compare 2 circuits using nx.is_isomorphic from their DAG.

Parameters:

Name Type Description Default
circuit1 CircuitDAG

circuit 1

required
circuit2 CircuitDAG

circuit 2

required

Returns:

Type Description
Boolean

True if 2 circuits is isomorphic, False otherwise.

Source code in graphiq/utils/circuit_comparison.py
def circuit_is_isomorphic(circuit1, circuit2):
    """
    Compare 2 circuits using nx.is_isomorphic from their DAG.

    :param circuit1: circuit 1
    :type circuit1: CircuitDAG
    :param circuit2: circuit 2
    :type circuit2: CircuitDAG
    :return: True if 2 circuits is isomorphic, False otherwise.
    :rtype: Boolean
    """
    add_control_target_to_dag(circuit1)
    add_control_target_to_dag(circuit2)

    def node_match(n1, n2):
        # get operation for node 1 and node 2
        op1 = n1["op"]
        op2 = n2["op"]

        # Compare the type of the 2 operations and the q_register_type tuple
        if type(op1) != type(op2) or op1.q_registers_type != op2.q_registers_type:
            return False

        # For ControlledPairOperationBase, compare the control_type and target_type
        if isinstance(op1, ControlledPairOperationBase):
            if (
                op1.control_type != op2.control_type
                or op1.target_type != op2.target_type
            ):
                return False

        # For OneQubitGateWrapper, compare the operations list
        if type(op1) == type(op2) == OneQubitGateWrapper:
            if op1.operations != op2.operations:
                return False

        return True

    def edge_match(e1, e2):
        # Get the first key of the edge dict, normally only 1 key per edge unless we have 2 nodes that are connected by
        #  2 edges
        val1 = next(iter(e1))
        val2 = next(iter(e2))

        # Check for the control_target attribute
        if e1[val1]["control_target"] != e2[val2]["control_target"]:
            return False
        return True

    return is_isomorphic(
        circuit1.dag, circuit2.dag, node_match=node_match, edge_match=edge_match
    )

class_to_name_mapping(class_op)

Function to map operation class to string. It's used to convert circuit object to json.

Parameters:

Name Type Description Default
class_op operation

operation class

required

Returns:

Type Description
Source code in graphiq/circuit/ops.py
def class_to_name_mapping(class_op):
    """
    Function to map operation class to string. It's used to convert circuit object to json.

    :param class_op: operation class
    :type class_op: operation
    :return:
    """
    mapping = {
        CNOT: "CX",
        SigmaX: "x",
        SigmaY: "y",
        SigmaZ: "z",
        Hadamard: "h",
        Phase: "s",
        CZ: "cz",
        ClassicalCNOT: "classical x",
        ClassicalCZ: "classical z",
        MeasurementCNOTandReset: "measurement-controlled x and reset",
    }
    if class_op in mapping:
        return mapping[class_op]
    return None

compare_circuits(circuit1, circuit2, method='direct')

Compare two circuits by using GED or direct loop method

Parameters:

Name Type Description Default
circuit1 CircuitDAG

circuit that to be compared

required
circuit2 CircuitDAG

circuit that to be compared

required
method str

Determine which comparison function to use

'direct'

Returns:

Type Description
bool

whether two circuits are the same

Source code in graphiq/utils/circuit_comparison.py
def compare_circuits(circuit1, circuit2, method="direct"):
    """
    Compare two circuits by using GED or direct loop method

    :param circuit1: circuit that to be compared
    :type circuit1: CircuitDAG
    :param circuit2: circuit that to be compared
    :type circuit2: CircuitDAG
    :param method: Determine which comparison function to use
    :type method: str
    :return: whether two circuits are the same
    :rtype: bool
    """
    if method == "direct":
        return direct(circuit1, circuit2)
    elif method == "GED_full":
        return ged(circuit1, circuit2, full=True)
    elif method == "GED_approximate":
        return ged(circuit1, circuit2, full=False)
    elif method == "GED_adaptive":
        return ged_adaptive(circuit1, circuit2)
    elif method == "is_isomorphic":
        return circuit_is_isomorphic(circuit1, circuit2)
    else:
        raise ValueError(f"Method {method} is not supported.")

direct(circuit1, circuit2)

Directly compare two circuits by iterating from input nodes to output nodes

Parameters:

Name Type Description Default
circuit1 CircuitDAG

circuit that to be compared

required
circuit2 CircuitDAG

circuit that to be compared

required

Returns:

Type Description
bool

whether two circuits are the same

Source code in graphiq/utils/circuit_comparison.py
def direct(circuit1, circuit2):
    """
    Directly compare two circuits by iterating from input nodes to output nodes

    :param circuit1: circuit that to be compared
    :type circuit1: CircuitDAG
    :param circuit2: circuit that to be compared
    :type circuit2: CircuitDAG
    :return: whether two circuits are the same
    :rtype: bool
    """
    circuit1 = circuit1.copy()
    circuit1.unwrap_nodes()
    circuit2 = circuit2.copy()
    circuit2.unwrap_nodes()

    circuit1.remove_identity()
    circuit2.remove_identity()

    n_reg_match = circuit1.register == circuit2.register
    n_nodes_match = circuit1.dag.number_of_nodes() == circuit2.dag.number_of_nodes()

    if n_reg_match and n_nodes_match:
        for in_node in circuit1.node_dict["Input"]:
            node1 = in_node
            node2 = in_node
            op = circuit1.dag.nodes[node1]["op"]
            reg = f"{op.reg_type}{op.register}"
            out_node = f"{reg}_out"
            while node1 != out_node:
                out_edge = [
                    edge
                    for edge in list(circuit1.dag.out_edges(node1, keys=True))
                    if edge[2] == reg
                ]
                node1 = out_edge[0][1]

                out_edge_compare = [
                    edge
                    for edge in list(circuit2.dag.out_edges(node2, keys=True))
                    if edge[2] == reg
                ]
                node2 = out_edge_compare[0][1]

                op1 = circuit1.dag.nodes[node1]["op"]
                op2 = circuit2.dag.nodes[node2]["op"]
                control_match = (
                    op1.q_registers_type == op2.q_registers_type
                    and op1.q_registers == op2.q_registers
                )
                if isinstance(op1, type(op2)) and control_match:
                    pass
                else:
                    return False
        return True
    else:
        return False

find_local_clifford_by_matrix(matrix)

Find local Clifford by its matrix representation

Parameters:

Name Type Description Default
matrix numpy.ndarray

a matrix representation of one-qubit Clifford gate

required

Returns:

Type Description
list

the local Clifford gate specified by a list of basic gates it consists of or None if the input matrix is not a valid one-qubit Clifford gate

Raises:

Type Description
ValueError

if the matrix does not correspond to a valid one-qubit Clifford gate.

Source code in graphiq/circuit/ops.py
def find_local_clifford_by_matrix(matrix):
    """
    Find local Clifford by its matrix representation

    :param matrix: a matrix representation of one-qubit Clifford gate
    :type matrix: numpy.ndarray
    :raises ValueError: if the matrix does not correspond to a valid one-qubit Clifford gate.
    :return: the local Clifford gate specified by a list of basic gates it consists of
        or None if the input matrix is not a valid one-qubit Clifford gate
    :rtype: list
    """
    gate_list1, gate_list2 = local_clifford_composition()
    for op1 in gate_list1:
        for op2 in gate_list2:
            matrix1 = local_clifford_to_matrix_map(op1)
            matrix2 = local_clifford_to_matrix_map(op2)
            product_matrix = matrix1 @ matrix2
            if dmf.check_equivalent_unitaries(matrix, product_matrix):
                return op1 + op2

    raise ValueError("Invalid one-qubit Clifford gate.")

ged(circuit1, circuit2, full=True)

Calculate Graph Edit Distance (GED) between two circuits. Further reading on GED: https://networkx.org/documentation/stable/reference/algorithms/similarity.html

Parameters:

Name Type Description Default
circuit1 CircuitDAG

circuit that to be compared

required
circuit2 CircuitDAG

circuit that to be compared

required
full bool

Determine which GED function to use

True

Returns:

Type Description
bool

whether two circuits are the same

Source code in graphiq/utils/circuit_comparison.py
def ged(circuit1, circuit2, full=True):
    """
    Calculate Graph Edit Distance (GED) between two circuits.
    Further reading on GED:
    https://networkx.org/documentation/stable/reference/algorithms/similarity.html

    :param circuit1: circuit that to be compared
    :type circuit1: CircuitDAG
    :param circuit2: circuit that to be compared
    :type circuit2: CircuitDAG
    :param full: Determine which GED function to use
    :type full: bool
    :return: whether two circuits are the same
    :rtype: bool
    """
    circuit1 = circuit1.copy()
    circuit1.unwrap_nodes()
    circuit2 = circuit2.copy()
    circuit2.unwrap_nodes()

    circuit1.remove_identity()
    circuit2.remove_identity()
    dag1 = circuit1.dag
    dag2 = circuit2.dag

    def node_match(n1, n2):
        reg_match = (
            n1["op"].q_registers_type == n2["op"].q_registers_type
            and n1["op"].q_registers == n2["op"].q_registers
        )
        ops_match = isinstance(n1["op"], type(n2["op"]))

        return reg_match and ops_match

    def edge_match(e1, e2):
        # TODO: may treat different emitters the same
        #       if the difference of two circuits is just relabeling of emitter registers
        return e1 == e2

    if full:
        sim = nx.algorithms.similarity.graph_edit_distance(
            dag1,
            dag2,
            node_match=node_match,
            edge_match=edge_match,
            upper_bound=30,
            timeout=10.0,
        )
    else:
        sim = nx.algorithms.similarity.optimize_graph_edit_distance(
            dag1,
            dag2,
            node_match=node_match,
            edge_match=edge_match,
            upper_bound=30,
        )
        sim = next(sim)

    return sim == 0

ged_adaptive(circuit1, circuit2, threshold=30)

Switch between exact and approximate GED calculation adaptively

Parameters:

Name Type Description Default
circuit1 CircuitDAG

circuit that to be compared

required
circuit2 CircuitDAG

circuit that to be compared

required
threshold int

threshold

30

Returns:

Type Description
bool

exact/approximated GED between circuits(cost needed to transform self.dag to circuit_compare.dag)

Source code in graphiq/utils/circuit_comparison.py
def ged_adaptive(circuit1, circuit2, threshold=30):
    """
    Switch between exact and approximate GED calculation adaptively

    :param circuit1: circuit that to be compared
    :type circuit1: CircuitDAG
    :param circuit2: circuit that to be compared
    :type circuit2: CircuitDAG
    :param threshold: threshold
    :type threshold: int
    :return: exact/approximated GED between circuits(cost needed to transform self.dag to circuit_compare.dag)
    :rtype: bool
    """

    full = (
        max(circuit1.dag.number_of_nodes(), circuit2.dag.number_of_nodes()) < threshold
    )
    sim = ged(circuit1, circuit2, full=full)
    return sim

local_clifford_to_matrix_map(gate)

Find the 2 X 2 matrix corresponding to the local Clifford gate

Parameters:

Name Type Description Default
gate list | a subclass of OperationBase

the local Clifford gate

required

Returns:

Type Description
numpy.ndarray

the 2 X 2 matrix corresponding to the local Clifford gate

Source code in graphiq/circuit/ops.py
def local_clifford_to_matrix_map(gate):
    """
    Find the 2 X 2 matrix corresponding to the local Clifford gate

    :param gate: the local Clifford gate
    :type gate: list or a subclass of OperationBase
    :return: the 2 X 2 matrix corresponding to the local Clifford gate
    :rtype: numpy.ndarray
    """
    mapping = {
        Identity.__name__: np.eye(2),
        Hadamard.__name__: dmf.hadamard(),
        Phase.__name__: dmf.phase(),
        SigmaX.__name__: dmf.sigmax(),
        SigmaY.__name__: dmf.sigmay(),
        SigmaZ.__name__: dmf.sigmaz(),
    }

    if isinstance(gate, list):
        result = np.eye(2)
        for op in gate:
            if op.__name__ in mapping.keys():
                result = result @ mapping[op.__name__]
            else:
                raise ValueError(f"Cannot support the operator of type {op.__name__}")
        return result
    else:
        if gate.__name__ in mapping.keys():
            return mapping[gate.__name__]
        else:
            raise ValueError(f"Cannot support the operator of type {gate.__name__}")

local_cliffords_name_to_matrix_map()

Find all one-qubit local Clifford gates in the matrix representation

Returns:

Type Description
map

all one-qubit local Clifford gates in the matrix representation

Source code in graphiq/circuit/ops.py
def local_cliffords_name_to_matrix_map():
    """
    Find all one-qubit local Clifford gates in the matrix representation

    :return: all one-qubit local Clifford gates in the matrix representation
    :rtype: map
    """
    a, b = local_clifford_composition()

    def gate_matrix(c):
        # where c is a tuple of lists
        matrix1 = local_clifford_to_matrix_map(c[0])
        matrix2 = local_clifford_to_matrix_map(c[1])

        return matrix1 @ matrix2

    return map(gate_matrix, itertools.product(a, b))

name_to_class_map(name)

Maps our openqasm naming scheme to operation classes. Does not handle multi-gate wrappers Does not handle multi-line openqasm components

Parameters:

Name Type Description Default
name str

gate name in openqasm

required

Returns:

Type Description
OperationBase class

the operation class corresponding to the openqasm name if the name is a valid name

Source code in graphiq/circuit/ops.py
def name_to_class_map(name):
    """
    Maps our openqasm naming scheme to operation classes. Does not handle multi-gate wrappers
    Does not handle multi-line openqasm components

    :param name: gate name in openqasm
    :type name: str
    :return: the operation class corresponding to the openqasm name if the name is a valid name
    :rtype: OperationBase class
    """
    mapping = {
        "CX": CNOT,
        "cx": CNOT,
        "x": SigmaX,
        "y": SigmaY,
        "z": SigmaZ,
        "h": Hadamard,
        "s": Phase,
        "p": Phase,
        "cz": CZ,
        "classical x": ClassicalCNOT,
        "classical z": ClassicalCZ,
        "classical reset x": MeasurementCNOTandReset,
    }
    if name in mapping:
        return mapping[name]
    return None

one_qubit_cliffords()

Returns an iterator of single-qubit clifford gates

Returns:

Type Description
map

iterator covering each single-qubit clifford gate

Source code in graphiq/circuit/ops.py
def one_qubit_cliffords():
    """
    Returns an iterator of single-qubit clifford gates

    :return: iterator covering each single-qubit clifford gate
    :rtype: map
    """
    a, b = local_clifford_composition()

    def flatten_gates(c):
        return c[0] + c[1]  # where c is a tuple of lists

    return map(flatten_gates, itertools.product(a, b))

remove_redundant_circuits(circuit_list)

The function will remove redundant circuit in a list of circuits by running the circuit_is_isomorphic() function. The function returns a new list of circuits with redundant circuits are removed.

Parameters:

Name Type Description Default
circuit_list list

a list of circuits

required

Returns:

Type Description
list

a new list of circuits

Source code in graphiq/utils/circuit_comparison.py
def remove_redundant_circuits(circuit_list):
    """
    The function will remove redundant circuit in a list of circuits by running the circuit_is_isomorphic() function.
    The function returns a new list of circuits with redundant circuits are removed.

    :param circuit_list: a list of circuits
    :type circuit_list: list
    :return: a new list of circuits
    :rtype: list
    """
    new_circuit_list = []

    for new_circuit in circuit_list:
        if new_circuit_list:
            check_isomorphic = False

            for circuit in new_circuit_list:
                current_circuit = circuit.copy()
                current_circuit.unwrap_nodes()
                current_circuit.remove_identity()

                to_add_circuit = new_circuit.copy()
                to_add_circuit.unwrap_nodes()
                to_add_circuit.remove_identity()

                if circuit_is_isomorphic(current_circuit, to_add_circuit):
                    check_isomorphic = True
                    break
            if not check_isomorphic:
                new_circuit_list.append(new_circuit)
        else:
            new_circuit_list.append(new_circuit)

    return new_circuit_list

simplify_local_clifford(gate_list)

Simplify the list of basic gates that represents a local Clifford

Parameters:

Name Type Description Default
gate_list list

original list of basic gates that represents a local Clifford

required

Returns:

Type Description
list

simplified list of basic gates that represents the same local Clifford

Source code in graphiq/circuit/ops.py
def simplify_local_clifford(gate_list):
    """
    Simplify the list of basic gates that represents a local Clifford

    :param gate_list: original list of basic gates that represents a local Clifford
    :type gate_list: list
    :return: simplified list of basic gates that represents the same local Clifford
    :rtype: list
    """
    matrix = local_clifford_to_matrix_map(gate_list)

    return find_local_clifford_by_matrix(matrix)