Examples

"""Find the fixed point of an EPP with noisy CNOT gates."""
import graphepp as gg
import numpy as np


# CNOT gate is modelled as local white noise acting on both of the qubits
# followed by the perfect operation
noise_param = 0.98
ghz5_graph = gg.Graph(
    N=5, E=((0, i) for i in range(1, 5)), sets=[(0,), (i for i in range(1, 5))]
)  # 5-qubit GHZ state

print("Noisy fixed point of the P1-P2 protocol for the 5-qubit GHZ state.")
print(
    f"CNOTs are noisy with local depolarizing noise with error parameter p={noise_param:.2f}"
)

rho = np.zeros(2**ghz5_graph.N, dtype=np.float)
rho[0] = 1.0  # perfect state

for i in range(
    100
):  # 100 iterations are more than enough to reach the fixed point with numerical precision
    rho = gg.wnoise_all(
        rho=rho, p=noise_param, graph=ghz5_graph
    )  # a noisy CNOT is acting on each of the qubits to perform the protocol
    rho = gg.p1(rho=rho, graph=ghz5_graph)
    rho = gg.wnoise_all(rho=rho, p=noise_param, graph=ghz5_graph)
    rho = gg.p2(rho=rho, graph=ghz5_graph)
    fixed_point_fidelity_p2 = rho[0]  # fidelity when ending with p2
rho = gg.wnoise_all(
    rho=rho, p=noise_param, graph=ghz5_graph
)  # a noisy CNOT is acting on each of the qubits to perform the protocol
rho = gg.p1(rho=rho, graph=ghz5_graph)
fixed_point_fidelity_p1 = rho[0]  # fidelity when ending with p1
print(f"Fixed point fidelity when ending with P1: {fixed_point_fidelity_p1}")
print(f"Fixed point fidelity when ending with P2: {fixed_point_fidelity_p2}")

print("=================")
print("Fixed point fidelity as function of CNOT noise parameter")


def noisy_fixed_point(graph, noise_param):
    rho = np.zeros(2**graph.N, dtype=np.float)
    rho[0] = 1.0  # perfect state
    for i in range(
        100
    ):  # 100 iterations are more than enough to reach the fixed point with numerical precision
        rho = gg.wnoise_all(
            rho=rho, p=noise_param, graph=graph
        )  # a noisy CNOT is acting on each of the qubits to perform the protocol
        rho = gg.p1(rho=rho, graph=ghz5_graph)
        rho = gg.wnoise_all(rho=rho, p=noise_param, graph=graph)
        rho = gg.p2(rho=rho, graph=ghz5_graph)
    return rho


ghz5_graph = gg.Graph(
    N=5, E=((0, i) for i in range(1, 5)), sets=[(0,), (i for i in range(1, 5))]
)
noise_list = np.linspace(0.9, 1, num=21)
print("Calculating fidelity list... (this may take a bit)")
fidelity_list = [
    noisy_fixed_point(graph=ghz5_graph, noise_param=p)[0] for p in noise_list
]
print(f"{noise_list=}")
print(f"{fidelity_list=}")
print("One can see that below a certain noise threshold, the EPP stops working.")
print("If pyplot is available, will show a plot.")

try:
    import matplotlib.pyplot as plt

    plotting_possible = True
except ImportError:
    plotting_possible = False

if plotting_possible:
    plt.scatter(noise_list, fidelity_list)
    plt.title("5-qubit GHZ state at EPP fixed point")
    plt.xlabel("Error parameter p for CNOT gates")
    plt.ylabel("Fidelity at fixed point")
    plt.show()
"""Applying the multipartite entanglement purification protocol."""
import graphepp as gg
import numpy as np

# Example 1: apply P1 and P2 to a noisy GHZ state
print("Example 1: apply P1 and P2 to a noisy GHZ state")

# graph state version of the GHZ state
ghz_graph = gg.Graph(N=3, E=((0, 1), (0, 2)), sets=[[0], [1, 2]])
# the sets indicate the coloring of this two-colorable graph state
# note that vertices are counting from 0...N-1

rho = np.zeros(2**ghz_graph.N, dtype=np.float)
rho[0] = 1.0  # this means the state is the perfect graph state
perfect_state = np.copy(rho)
# start with an initally noise state
rho = gg.wnoise_all(rho=rho, p=0.95, graph=ghz_graph)
print(f"Fidelity before: {gg.fidelity(rho, perfect_state):.5f}")
# now apply P1 once then P2 once
rho = gg.p1(rho=rho, graph=ghz_graph)
rho = gg.p2(rho=rho, graph=ghz_graph)
print(f"Fidelity after: {gg.fidelity(rho, perfect_state):.5f}")


# Example 2: Show that a sufficiently high-fidelity state will converge to the
#            the perfect graph state. Here: Linear cluster state of 5 qubits
print("=================")
print("Example 2: Linear cluster state convergence")

linear_cluster_graph = gg.Graph(
    N=5, E=((0, 1), (1, 2), (2, 3), (3, 4)), sets=[[0, 2, 4], [1, 3]]
)
rho = np.zeros(2**linear_cluster_graph.N, dtype=np.float)
rho[0] = 1.0  # the perfect graph state
# first consider a slightly noisy state
mu = gg.wnoise_all(rho=rho, p=0.95, graph=linear_cluster_graph)
print("a) Purification works for sufficiently high-fidelity state.")
print(f"Slightly noisy state initial fidelity: {gg.fidelity(mu, rho):.5f}")
# 100 purification steps should be more than enough to get to the fixed point
# within numerical precision
for i in range(100):
    mu = gg.p1(rho=mu, graph=linear_cluster_graph)
    mu = gg.p2(rho=mu, graph=linear_cluster_graph)
# if initial fidelity is high enough this should converge to the perfect state
print(f"Fidelity after 100 purification steps: {gg.fidelity(mu, rho):.5f}")
print("---------")
mu = gg.wnoise_all(rho=rho, p=0.5, graph=linear_cluster_graph)
print("b) Purification fails for initial states that are too noisy.")
print(f"Very noisy state initial fidelity: {gg.fidelity(mu, rho):.5f}")
for i in range(100):
    mu = gg.p1(rho=mu, graph=linear_cluster_graph)
    mu = gg.p2(rho=mu, graph=linear_cluster_graph)
print(f"Fidelity after 100 purification steps: {gg.fidelity(mu, rho):.5f}")

print("=================")
print("Example 3: GHZ Fidelity change during purification")
ghz_graph = gg.Graph(N=3, E=((0, 1), (0, 2)), sets=[[0], [1, 2]])


rho = np.zeros(2**ghz_graph.N, dtype=np.float)
rho[0] = 1.0
perfect_state = np.copy(rho)
# start with an initally noise state
rho = gg.wnoise_all(rho=rho, p=0.8, graph=ghz_graph)
fidelities = [gg.fidelity(rho, perfect_state)]
for i in range(7):
    rho = gg.p1(rho=rho, graph=ghz_graph)
    fidelities += [gg.fidelity(rho, perfect_state)]
    rho = gg.p2(rho=rho, graph=ghz_graph)
    fidelities += [gg.fidelity(rho, perfect_state)]
print(f"Fidelity change over 14 purification steps: {fidelities}")
print("If pyplot is available, will show a plot.")

try:
    import matplotlib.pyplot as plt

    plotting_possible = True
except ImportError:
    plotting_possible = False

if plotting_possible:
    plt.scatter(np.arange(len(fidelities)), fidelities)
    plt.ylim(0.6, 1.01)
    plt.xlabel("Purification steps")
    plt.ylabel("Fidelity")
    plt.grid()
    plt.show()
"""Perform the entanglement purification protocol for arbitrary graph states.

Here we look at an example for a 6-qubit graph state that is three-colorable.
"""
import graphepp as gg
import numpy as np

# as main graph we use this 6-qubit state that is linked to a
# universal quantum error correction code
# For visualization of this particular coloring and the auxiliary graphs
# see Fig.9 and Fig. 20 in https://arxiv.org/abs/1609.05754
main_graph = gg.Graph(
    N=6,
    E=[(0, 1), (0, 4), (0, 5), (1, 2), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5)],
    sets=[[0, 2], [1, 4], [3, 5]],
)
aux_graph1 = gg.Graph(N=6, E=[(0, 1), (0, 4), (0, 5), (1, 2), (2, 3), (2, 5)])
aux_graph2 = gg.Graph(N=6, E=[(0, 1), (0, 4), (1, 2), (1, 3), (3, 4), (4, 5)])
aux_graph3 = gg.Graph(N=6, E=[(0, 5), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5)])

# assume we have all these states available, with the same noise per qubit
rho = np.zeros(2**main_graph.N, dtype=np.float)
rho[0] = 1.0  # perfect graph state
rho = gg.wnoise_all(rho=rho, p=0.98, graph=main_graph)

mu1 = np.zeros(2**aux_graph1.N, dtype=np.float)
mu1[0] = 1.0
mu1 = gg.wnoise_all(rho=mu1, p=0.98, graph=aux_graph1)

mu2 = np.zeros(2**aux_graph2.N, dtype=np.float)
mu2[0] = 1.0
mu2 = gg.wnoise_all(rho=mu2, p=0.98, graph=aux_graph2)

mu3 = np.zeros(2**aux_graph3.N, dtype=np.float)
mu3[0] = 1.0
mu3 = gg.wnoise_all(rho=mu3, p=0.98, graph=aux_graph3)

# do one protocol for each color, here we assume perfect CNOTs
print(f"Fidelity before: {rho[0]}")
rho = gg.pk(
    rho=rho, sigma=mu1, graph1=main_graph, graph2=aux_graph1, subset=main_graph.sets[0]
)
print(f"Fidelity after first subprotocol: {rho[0]:.5f}")
rho = gg.pk(
    rho=rho, sigma=mu2, graph1=main_graph, graph2=aux_graph2, subset=main_graph.sets[1]
)
print(f"Fidelity after second subprotocol: {rho[0]:.5f}")
rho = gg.pk(
    rho=rho, sigma=mu3, graph1=main_graph, graph2=aux_graph3, subset=main_graph.sets[2]
)
print(f"Fidelity after third subprotocol: {rho[0]:.5f}")