"""
Copyright (c) Entropica Labs Pte Ltd 2025.
Use, distribution and reproduction of this program in its source or compiled
form is prohibited without the express written consent of Entropica Labs Pte
Ltd.
"""
# pylint: disable=too-many-lines
from __future__ import annotations
from itertools import product
import numpy as np
import networkx as nx
from . import matrices # pylint: disable=cyclic-import
from .stabilizer import Stabilizer
# pylint: disable=too-many-locals
[docs]
def cartesian_product_tanner_graphs(
t_graph_1: ClassicalTannerGraph, t_graph_2: ClassicalTannerGraph
) -> TannerGraph:
"""
Computes the Cartesian product of two ClassicalTannerGraphs Gi(Vi,Ci,Ei), where Vi
is the set of data nodes, Ci is the set of check nodes and Ei is the set of edges.
Each ClassicalTannerGraph is associated with a classical code. See arXiv:2109.14609
for a detailed description of the construction.
The cartesian product yields a TannerGraph G(V,C,E) describing a quantum CSS code.
The nodes of the product TannerGraph are labelled via two coordinates (v1,v2), each
inherited from each classical code. The set of data nodes V is built out of the
union of the nodes resulting from the product of the data nodes (v1,v2) and
of the check nodes (c1,c2) of each classical code. The set of check nodes C follows
from the product of check and data nodes from each classical codes, i.e. (c1,v2) and
(v1,c2). We assign X check nodes to be the first partition of check nodes and Z
checks to be the second one. Edges are drawn between nodes (a1,a2) and (b1,b2), if
either a2=b2 and G1 contains the edge (a1,b1) or a1=b1 and G2 contains the edge
(a2,b2). The edges of product Tanner graphs are decorated with a cardinality label
{`N`,`S`,`E`,`W`} according to the definitions introduced in arXiv:2109.14609.
Note that we assign the first and second input ClassicalTannerGraphs to X and Z
checks respectively, regardless of their associated `check_type` attribute.
Furthermore, we impose a single integer labelling of the nodes, of the form `(i,)`
in the input ClassicalTannerGraphs. If the any of the inputs does not satisfy this
requirement, we build a relabelled graph using the `relabelled_graph()` method of
the ClassicalTannerGraph class. While this constraint is fundamental from the
cartesian product perspective, it is required in order to decorate the edges with
cardinal labels.
Parameters
----------
t_graph_1: ClassicalTannerGraph
Tanner graph of the first classical code.
t_graph_2: ClassicalTannerGraph
Tanner graph of the second classical code.
Returns
-------
tanner_product: TannerGraph
Tanner graph from cartesian product of t_graph_1 and t_graph_2, describing the full HGP code.
"""
# Check inputs are of valid type
if not all(
isinstance(t_graph, ClassicalTannerGraph) for t_graph in [t_graph_1, t_graph_2]
):
raise TypeError("Both inputs must be of type ClassicalTannerGraph.")
# Ensure single integer tuple labelling of nodes
# If not satisfied, generate relabelled graph
if any(
not isinstance(n, tuple) or (len(n) != 1) or (not isinstance(n[0], int))
for n in t_graph_1.graph.nodes()
):
t_graph_1 = t_graph_1.relabelled_graph()
if any(
not isinstance(n, tuple) or (len(n) != 1) or (not isinstance(n[0], int))
for n in t_graph_2.graph.nodes()
):
t_graph_2 = t_graph_2.relabelled_graph()
# Initialize cartesian product graph
graph_product = nx.Graph()
# Extract size of node sets for both graphs
len1 = len(t_graph_1.graph.nodes)
len2 = len(t_graph_2.graph.nodes)
# Add nodes to the product graph
for t1_node, t2_node in product(
t_graph_1.graph.nodes(data=True), t_graph_2.graph.nodes(data=True)
):
# Nodes are indexed with a tuple, inherited from the indices in
# the original codes
node_pair = (t1_node[0][0], t2_node[0][0])
label1 = t1_node[1]["label"]
label2 = t2_node[1]["label"]
# If both nodes are of the same type they are a data qubit
if (label1 != "data" and label2 != "data") or (
label1 == "data" and label2 == "data"
):
graph_product.add_node(node_pair, label="data")
# If nodes are different they correspond to a check node
else:
# We choose that if the nodes of the first code are checks, these
# become X checks
if label2 == "data":
graph_product.add_node(node_pair, label="X")
# If the nodes of the second code are checks, these become Z checks
else:
graph_product.add_node(node_pair, label="Z")
# Add horizontal edges to the product graph, via edges inherited from the first code
for (t1_node1, t1_node2), t2_node in product(
t_graph_1.graph.edges(), t_graph_2.graph.nodes(data=False)
):
# Define edges such that they follow the (check,data) order
unordered_edge = ((t1_node1[0], t2_node[0]), (t1_node2[0], t2_node[0]))
edge = (
unordered_edge
if graph_product.nodes[unordered_edge[1]]["label"] == "data"
else unordered_edge[::-1]
)
# Compute their "label-distance"
diff = edge[1][0] - edge[0][0]
# Associate with E or W cardinality according to their label-distance
is_east = diff % len1 <= len1 / 2
cardinality = "E" if is_east else "W"
# Add edge to the product
graph_product.add_edge(*edge, cardinality=cardinality)
# Add vertical edges to the product graph, via edges inherited from the second code
for t1_node, (t2_node1, t2_node2) in product(
t_graph_1.graph.nodes(data=False), t_graph_2.graph.edges()
):
# Define edges such that they follow the (check,data) order
unordered_edge = ((t1_node[0], t2_node1[0]), (t1_node[0], t2_node2[0]))
edge = (
unordered_edge
if graph_product.nodes[unordered_edge[1]]["label"] == "data"
else unordered_edge[::-1]
)
# Compute their "label-distance"
diff = edge[1][1] - edge[0][1]
# Associate with N or S cardinality according to their label-distance
is_north = diff % len2 <= len2 / 2
cardinality = "N" if is_north else "S"
# Add edge to the product
graph_product.add_edge(*edge, cardinality=cardinality)
# Convert into TannerGraph object
tanner_product = TannerGraph(graph_product)
return tanner_product
[docs]
def verify_css_code_stabilizers(
stabilizers: list[Stabilizer] | tuple[Stabilizer],
) -> bool:
"""
Verifies that the list of stabilizers define a valid CSS code.
Parameters
----------
stabilizers : list[Stabilizer] | tuple[Stabilizer]
List of stabilizers defining a quantum CSS code.
Returns
-------
valid: bool
If True, the stabilizers define a valid CSS code.
"""
# Check input
if not isinstance(stabilizers, list) and not isinstance(stabilizers, tuple):
raise TypeError("Input must be a list or tuple.")
if len(stabilizers) == 0:
raise ValueError("No stabilizers provided.")
if not all(isinstance(stabilizer, Stabilizer) for stabilizer in stabilizers):
raise TypeError("Input must be a list or tuple of Stabilizer objects.")
# Verify all stabilizers are either of X or Z type
valid = all(
set(stabilizer.pauli) == {"X"} or set(stabilizer.pauli) == {"Z"}
for stabilizer in stabilizers
)
# If all stabilizers are X and Z, verify that they all commute
if valid:
x_stabs = [stab for stab in stabilizers if set(stab.pauli) == {"X"}]
z_stabs = [stab for stab in stabilizers if set(stab.pauli) == {"Z"}]
for x_stab in x_stabs:
for z_stab in z_stabs:
if not x_stab.commutes_with(z_stab):
raise ValueError(
f"Input Stabilizers {x_stab} and {z_stab} do not commute."
)
return valid
[docs]
class ClassicalTannerGraph:
"""Classical Tanner Graph representation."""
def __init__(
self,
input: nx.Graph | tuple[Stabilizer, ...] | matrices.ClassicalParityCheckMatrix,
# pylint: disable=redefined-builtin
):
"""The Classical Tanner Graph class stores a bipartite graph representation of a
classical error-correcting code. The graph is bipartite, with data nodes on one
partition and check nodes on the other. The check nodes can be labelled with
either 'X' or 'Z' if the code is to be related with quantum error properties,
or it can be generically labelled as "check". If a networkx.Graph is given as
input, the graph will go through a set of verifications to ensure its validity
to represent the classical code. The input can also be a tuple of stabilizers,
which need to be of the same time pauli type, and the graph will be generated
from them. The graph can be converted into a list of stabilizers. The check type
of the Tanner graph can be modified using the set_check_type method. Lastly, the
class can also be instantiated from ClassicalParityCheckMatrix object, where the
matrix is mapped to a Tanner graph and the check nodes are labeled as 'check'.
Parameters
----------
input : nx.Graph | tuple[Stabilizer,...] | ClassicalParityCheckMatrix
Input graph, tuple of stabilizers, or ClassicalParityCheckMatrix to
build the Classical Tanner graph.
"""
self.check_type = None
# Initialize graph based on input type
if isinstance(input, nx.Graph):
self.verify_input_graph(input)
self.graph = input
elif isinstance(input, tuple) and all(
isinstance(item, Stabilizer) for item in input
):
self.graph = self.generate_graph_from_stabilizers(input)
elif isinstance(input, matrices.ClassicalParityCheckMatrix):
self.graph = self.generate_graph_from_matrix(input)
else:
raise TypeError(
"A networkx.Graph, a tuple of Stabilizer or a "
"ClassicalParityCheckMatrix must be provided."
)
# Assign data nodes
self.data_nodes = [
n for n, attr in self.graph.nodes(data=True) if attr["label"] == "data"
]
# Assign check nodes
self.check_nodes = [
n
for n, attr in self.graph.nodes(data=True)
if attr["label"] == self.check_type
]
[docs]
def generate_graph_from_stabilizers(
self, stabilizers: tuple[Stabilizer]
) -> nx.Graph:
"""Generate a Classical Tanner graph from a tuple of stabilizers.
Parameters
----------
stabilizers : tuple[Stabilizer]
Tuple of stabilizers to generate the Classical Tanner graph. All must be of
the same pauli type.
Returns
-------
t_graph : nx.Graph
Classical Tanner graph, as a networkx object generated from the input
stabilizers.
"""
# Extract Pauli type from Stabilizers
pauli_type = set(p for stab in stabilizers for p in stab.pauli)
# Verify the pauli type is unique
if len(pauli_type) > 1:
raise ValueError(
"Input stabilizers must be of the same type to define a classical"
" Tanner graph."
)
# Assign check type based on the unique pauli type
self.check_type = next(iter(pauli_type))
# Verify that there is only a single ancilla per Stabilizer
if any(len(stab.ancilla_qubits) != 1 for stab in stabilizers):
raise ValueError("All Stabilizers must contain a single ancilla qubit.")
# Verify that all ancillas are different
check_qubits = {q for stab in stabilizers for q in stab.ancilla_qubits}
if len(check_qubits) != len(stabilizers):
raise ValueError("All ancilla qubits must be different.")
# Extract data_qubits
data_qubits = {q for stab in stabilizers for q in stab.data_qubits}
# Initialize graph and add nodes
t_graph = nx.Graph()
t_graph.add_nodes_from(data_qubits, label="data")
t_graph.add_nodes_from(check_qubits, label=self.check_type)
# Generate edges
edges = [
(q, stab.ancilla_qubits[0])
for stab in stabilizers
for q in stab.data_qubits
]
t_graph.add_edges_from(edges)
return t_graph
[docs]
def to_stabilizers(self, pauli_type: str = None) -> list[Stabilizer]:
"""Convert the Classical Tanner graph to a list of stabilizers. Input pauli_type
is required if check nodes have not been assigned a check_type. If check nodes
have been assigned a check_type, the input pauli_type will override the
assignment.
Parameters
----------
pauli_type : str | None
Pauli type of the stabilizers, either 'X' or 'Z'. If None, the check_type
of the Tanner graph will be used. Default is None.
Returns
-------
stabilizers : list[Stabilizer]
List of stabilizers generated from the Classical Tanner graph.
"""
# Raise error if pauli_type input is missing and nodes do not have a pauli label
if pauli_type is None and self.check_type == "check":
raise ValueError(
"Check nodes have not been assigned a pauli type. Please provide a"
" pauli_type input."
)
# If no pauli_type is given, use the check_type
if pauli_type is None:
pauli_type = self.check_type
# Ensure input pauli_type is valid
elif pauli_type is not None and pauli_type not in ["X", "Z"]:
raise ValueError("Pauli type must be either 'X' or 'Z'.")
# Verify that all nodes can be converted into coordinates
all_nodes = list(self.graph.nodes)
if not all(
isinstance(t, tuple)
and len(t) == len(all_nodes[0])
and all(isinstance(e, int) for e in t)
for t in all_nodes
):
raise ValueError(
"Nodes are not tuples of equal size and cannot be "
"converted to list of stabilizers. Consider using "
"relabelled_graph() method, to generate a re-indexed graph and later"
" convert to Stabilizer list."
)
# Generate stabilizers by checking the neighbors of each check node
def generate_stabilizer(check_node):
data_nodes = list(self.graph.neighbors(check_node))
return Stabilizer(
pauli=pauli_type * len(data_nodes),
data_qubits=[data_node + (0,) for data_node in data_nodes],
ancilla_qubits=[check_node + (1,)],
)
stabilizers = [generate_stabilizer(node) for node in self.check_nodes]
return stabilizers
[docs]
def relabelled_graph(self) -> ClassicalTannerGraph:
"""Relabel a Classical Tanner graph to identify nodes with tuples of
integers which can later be converted into Stabilizers. The nodes are a
assigned a single-element tuple with an integer index. The data nodes are counted
first, followed by the check nodes. The old node labels are stored in the node
attributes under the key 'original_node'.
Returns
-------
relabelled_tanner : ClassicalTannerGraph
Relabelled Classical Tanner graph.
"""
# Initialize graph
t_graph = nx.Graph()
# Add nodes
data_nodes_info = {
(i,): {"label": "data", "original_node": n}
for i, n in enumerate(self.data_nodes)
}
data_nodes_dict = {v["original_node"]: k for k, v in data_nodes_info.items()}
check_nodes_info = {
(i + len(data_nodes_info),): {"label": self.check_type, "original_node": n}
for i, n in enumerate(self.check_nodes)
}
check_nodes_dict = {v["original_node"]: k for k, v in check_nodes_info.items()}
t_graph.add_nodes_from(data_nodes_info.items())
t_graph.add_nodes_from(check_nodes_info.items())
# Add edges
edges = [
(
(data_nodes_dict[u], check_nodes_dict[v])
if self.graph.nodes[u]["label"] == "data"
else (data_nodes_dict[v], check_nodes_dict[u])
)
for u, v in self.graph.edges()
]
t_graph.add_edges_from(edges)
# Construct new relabelled Classical Tanner graph
relabelled_tanner = ClassicalTannerGraph(t_graph)
return relabelled_tanner
[docs]
def set_check_type(self, new_check_type: str) -> None:
"""Modify the check type of the Tanner graph.
Parameters
----------
new_check_type : str
New check type to be assigned to the Tanner graph. Must be either 'X', 'Z'
or 'check'.
"""
# Ensure correct input
if new_check_type not in ["X", "Z", "check"]:
raise ValueError("Check type must be either 'X', 'Z' or 'check'.")
# Create a dictionary of updated labels
updated_labels = {
n: new_check_type
for n, attr in self.graph.nodes(data=True)
if attr["label"] == self.check_type
}
# Apply the updates to the graph
nx.set_node_attributes(self.graph, updated_labels, "label")
# Update the check type
self.check_type = new_check_type
[docs]
def generate_graph_from_matrix(
self, h_matrix: matrices.ClassicalParityCheckMatrix
) -> nx.Graph:
"""Generate Tanner graph from a ClassicalParityCheckMatrix object.
Parameters
----------
h_matrix : matrices.ClassicalParityCheckMatrix
Parity-check matrix to generate the Tanner graph.
Returns
-------
g : nx.Graph
Classical Tanner graph, as a networkx object generated from the input
parity-check matrix.
"""
# Initialize graph
g = nx.Graph()
# Extract the dimensions of the parity-check matrix
n_rows, n_cols = h_matrix.matrix.shape
# Add data qubit nodes from number of columns
data_nodes = [(i,) for i in range(n_cols)]
g.add_nodes_from(data_nodes, label="data")
# Add check nodes from number of rows and set check_type
check_nodes = [(i,) for i in range(n_cols, n_cols + n_rows)]
g.add_nodes_from(check_nodes, label="check")
self.check_type = "check"
# Extract the edges from the indices associated with non-vanishing terms
for i, row in enumerate(h_matrix.matrix):
for j in np.where(row == 1)[0]:
g.add_edge(check_nodes[i], data_nodes[j])
return g
def __eq__(self, other: ClassicalTannerGraph) -> bool:
"""Check if two Classical Tanner graphs are equal. Two graphs are equal if they
have the same nodes and edges, and the same check type.
Parameters
----------
other : ClassicalTannerGraph
Another Classical Tanner graph to compare with.
Returns
-------
bool
True if the Classical Tanner graphs are equal, False otherwise.
"""
if not isinstance(other, ClassicalTannerGraph):
raise TypeError(
"Comparison is only supported between ClassicalTannerGraph objects."
)
def are_node_attributes_not_the_same():
return set(self.data_nodes) != set(other.data_nodes) or set(
self.check_nodes
) != set(other.check_nodes)
def are_check_types_not_the_same():
return self.check_type != other.check_type
def are_nodes_not_the_same():
return self.graph.nodes(data=True) != other.graph.nodes(data=True)
# Check edges by sorting the tuples to ensure order does not matter
def are_edges_not_the_same():
return set(tuple(sorted(edge)) for edge in self.graph.edges()) != set(
tuple(sorted(edge)) for edge in other.graph.edges()
)
# Check class attributes and graph properties
return not (
are_node_attributes_not_the_same()
or are_check_types_not_the_same()
or are_nodes_not_the_same()
or are_edges_not_the_same()
)
[docs]
class TannerGraph:
"""A class representing a Tanner graph."""
def __init__(
self,
input: nx.Graph | tuple[Stabilizer] | matrices.ParityCheckMatrix,
# pylint: disable=redefined-builtin
):
"""
The TannerGraph class stores a bipartite graph representation of a quantum
CSS code. The graph is bipartite, with data nodes on one partition and check
nodes on the other. The check nodes can be labelled with either 'X' or 'Z',
associated with the type of stabilizers they represent and type of errors they
check for, e.g. Z check nodes check for X errors and vice versa. If a
networkx.Graph is given as input, the graph will go through a set of
verifications to ensure its validity to represent the code. The input can also
be a tuple of stabilizers, which need to satisfy the CSS condition, i.e.
all must be either X- or Z-type, and the graph will be generated from them.
Parameters
----------
input : nx.Graph | tuple[Stabilizer] | ParityCheckMatrix
Input graph or tuple of stabilizers to build the Tanner graph.
"""
if isinstance(input, nx.Graph):
self.verify_input_graph(input)
self.graph = input
elif isinstance(input, tuple) and all(
isinstance(item, Stabilizer) for item in input
):
self.graph = self.generate_graph_from_stabilizers(input)
elif isinstance(input, matrices.ParityCheckMatrix):
self.graph = self.generate_graph_from_matrix(input)
else:
raise TypeError(
"A networkx.Graph, a tuple of Stabilizers or a ParityCheckMatrix must "
"be provided."
)
# Assign data nodes
self.data_nodes = [
n for n, attr in self.graph.nodes(data=True) if attr["label"] == "data"
]
# Assign X check nodes
self.x_nodes = [
n for n, attr in self.graph.nodes(data=True) if attr["label"] == "X"
]
# Assign Z check nodes
self.z_nodes = [
n for n, attr in self.graph.nodes(data=True) if attr["label"] == "Z"
]
[docs]
def generate_graph_from_matrix(
self, h_matrix: matrices.ParityCheckMatrix
) -> nx.Graph:
"""
Generate Tanner graph from an input parity-check matrix. Note that, given our
definition of TannerGraph, we only allow for ParityCheckMatrix objects
describing CSS codes.
Parameters
----------
h_matrix : ParityCheckMatrix
Parity-check matrix to generate the Tanner graph. Must be a CSS code.
Returns
-------
g : nx.Graph
Tanner graph describing the code, as a networkx graph.
"""
# Verify input matrix is CSS
if not h_matrix.is_css:
raise ValueError("Parity-check matrix does not define a CSS code.")
hx_matrix, hz_matrix = h_matrix.get_components()
# Initialize graph
g = nx.Graph()
# Generate nodes
data_nodes = [(i,) for i in range(h_matrix.n_datas)]
x_nodes = [
(i,) for i in range(h_matrix.n_datas, h_matrix.n_datas + h_matrix.n_xstabs)
]
z_nodes = [
(i,)
for i in range(
h_matrix.n_datas + h_matrix.n_xstabs,
h_matrix.n_datas + h_matrix.n_stabs,
)
]
g.add_nodes_from(data_nodes, label="data")
g.add_nodes_from(x_nodes, label="X")
g.add_nodes_from(z_nodes, label="Z")
# Generate edges
for i, row in enumerate(hx_matrix.matrix):
for j in np.where(row == 1)[0]:
g.add_edge(data_nodes[j], x_nodes[i])
for i, row in enumerate(hz_matrix.matrix):
for j in np.where(row == 1)[0]:
g.add_edge(data_nodes[j], z_nodes[i])
return g
[docs]
def generate_graph_from_stabilizers(
self, stabilizers: tuple[Stabilizer, ...]
) -> nx.Graph:
"""
Generate Tanner graph from a tuple of stabilizers.
Parameters
----------
stabilizers : tuple[Stabilizer,...]
Stabilizers to generate the Tanner graph.
"""
# Ensure input is not empty
if len(stabilizers) == 0:
raise ValueError("Input tuple of stabilizers is empty.")
# Verify list of stabilizers define a CSS code
if not verify_css_code_stabilizers(stabilizers):
raise ValueError("Input stabilizers do not define a CSS code.")
# Extract properties of datas and checks
data_qubits = {qubit for stab in stabilizers for qubit in stab.data_qubits}
x_qubits = {
qubit
for stab in stabilizers
for qubit in stab.ancilla_qubits
if set(stab.pauli) == {"X"}
}
z_qubits = {
qubit
for stab in stabilizers
for qubit in stab.ancilla_qubits
if set(stab.pauli) == {"Z"}
}
# Initialize graph
t_graph = nx.Graph()
t_graph.add_nodes_from(data_qubits, label="data")
t_graph.add_nodes_from(x_qubits, label="X")
t_graph.add_nodes_from(z_qubits, label="Z")
# Generate edges
edges = [
(q, stab.ancilla_qubits[0])
for stab in stabilizers
for q in stab.data_qubits
]
t_graph.add_edges_from(edges)
return t_graph
[docs]
def to_stabilizers(self) -> list[Stabilizer]:
"""
Convert Tanner graph to a list of stabilizers.
"""
# Verify that nodes can be converted into coordinates
all_nodes = list(self.graph.nodes)
if not all(
isinstance(t, tuple)
and len(t) == len(all_nodes[0])
and all(isinstance(e, int) for e in t)
for t in all_nodes
):
raise ValueError(
"Nodes are not tuples of equal size and cannot be "
"converted to list of stabilizers. Consider using "
"relabel_graph() method, to re-index your graph and later"
" convert to Stabilizer list."
)
stabilizers = []
for x_node in self.x_nodes:
data_nodes = list(self.graph.neighbors(x_node))
stabilizer = Stabilizer(
pauli="X" * len(data_nodes),
data_qubits=[data_node + (0,) for data_node in data_nodes],
ancilla_qubits=[x_node + (1,)],
)
stabilizers.append(stabilizer)
for z_node in self.z_nodes:
data_nodes = list(self.graph.neighbors(z_node))
stabilizer = Stabilizer(
pauli="Z" * len(data_nodes),
data_qubits=[data_node + (0,) for data_node in data_nodes],
ancilla_qubits=[z_node + (1,)],
)
stabilizers.append(stabilizer)
return stabilizers
[docs]
def relabelled_graph(self) -> TannerGraph:
"""
Relabel a Tanner graph to identify nodes with tuples of integers which can
later be converted into Stabilizers. The nodes are a assigned a single-element
tuple with an integer index. The data nodes are counted first, followed by the
X check nodes and the Z check nodes. The old node labels are stored in the node
attributes under the key 'original_node'.
The relabelled graph is guaranteed to allow for conversion into Stabilizers
through the `to_stabilizers()` method.
Returns
-------
relabelled_tanner : TannerGraph
Relabelled Tanner graph.
"""
# Initialize graph
t_graph = nx.Graph()
# Add nodes
data_nodes_info = {
(i,): {"label": "data", "original_node": n}
for i, n in enumerate(self.data_nodes)
}
data_nodes_dict = {v["original_node"]: k for k, v in data_nodes_info.items()}
x_nodes_info = {
(i + len(data_nodes_info),): {"label": "X", "original_node": n}
for i, n in enumerate(self.x_nodes)
}
x_nodes_dict = {v["original_node"]: k for k, v in x_nodes_info.items()}
z_nodes_info = {
(i + len(data_nodes_info) + len(x_nodes_info),): {
"label": "Z",
"original_node": n,
}
for i, n in enumerate(self.z_nodes)
}
z_nodes_dict = {v["original_node"]: k for k, v in z_nodes_info.items()}
t_graph.add_nodes_from(data_nodes_info.items())
t_graph.add_nodes_from(x_nodes_info.items())
t_graph.add_nodes_from(z_nodes_info.items())
# Add edges
for u, v in self.graph.edges():
if self.graph.nodes[u]["label"] == "data":
u, v = v, u
if self.graph.nodes[u]["label"] == "X":
t_graph.add_edge(data_nodes_dict[v], x_nodes_dict[u])
else:
t_graph.add_edge(data_nodes_dict[v], z_nodes_dict[u])
# Construct new relabelled Tanner graph
relabelled_tanner = TannerGraph(t_graph)
return relabelled_tanner
[docs]
def get_components(
self,
) -> tuple[ClassicalTannerGraph | None, ClassicalTannerGraph | None]:
"""
Compute the X and Z components of the Tanner graph, associated with
X and Z stabilizers, respectively.
If the code contains only one type of stabilizer, the component of missing type
will return None. The two of them cannot be None simultaneously.
Returns
-------
t_graph_x, t_graph_z: tuple[ClassicalTannerGraph | None, ClassicalTannerGraph | None]
Tuple containing the X and Z tanner graphs induced by the data nodes and the
respective set of check nodes.
"""
# Create subgraphs induced by the selected nodes and convert
# into Classical Tanner Graphs
if len(self.x_nodes) == 0:
z_component = self.graph.copy().subgraph(self.data_nodes + self.z_nodes)
t_graph_z = ClassicalTannerGraph(z_component)
t_graph_x = None
elif len(self.z_nodes) == 0:
x_component = self.graph.copy().subgraph(self.data_nodes + self.x_nodes)
t_graph_x = ClassicalTannerGraph(x_component)
t_graph_z = None
else:
x_component = self.graph.copy().subgraph(self.data_nodes + self.x_nodes)
z_component = self.graph.copy().subgraph(self.data_nodes + self.z_nodes)
t_graph_x = ClassicalTannerGraph(x_component)
t_graph_z = ClassicalTannerGraph(z_component)
return t_graph_x, t_graph_z
def __eq__(self, other: TannerGraph) -> bool:
"""
Check if two Tanner graphs are equal. Two Tanner graphs are equal if they have
the same graphs and same lists of data, x and z nodes.
Parameters
----------
other : TannerGraph
TannerGraph to compare with.
Returns
-------
bool
True if the two Tanner graphs are equal, False otherwise.
"""
if not isinstance(other, TannerGraph):
raise TypeError("Comparison is only supported between TannerGraph objects.")
def are_node_attributes_not_the_same():
return (
set(self.data_nodes) != set(other.data_nodes)
or set(self.x_nodes) != set(other.x_nodes)
or set(self.z_nodes) != set(other.z_nodes)
)
def are_nodes_not_the_same():
return self.graph.nodes(data=True) != other.graph.nodes(data=True)
# Check edges by sorting the tuples to ensure order does not matter
def are_edges_not_the_same():
return set(tuple(sorted(edge)) for edge in self.graph.edges()) != set(
tuple(sorted(edge)) for edge in other.graph.edges()
)
return not (
are_node_attributes_not_the_same()
or are_nodes_not_the_same()
or are_edges_not_the_same()
)