Extending a GHZ State

This example creates a three-party GHZ (Greenberger-Horne-Zeilinger) state across Alice, Bob, and Charlie. Found in examples/new-sdk/extendGHZ/.

A GHZ state is a maximally entangled state shared between three (or more) parties:

\[|GHZ\rangle = \frac{1}{\sqrt{2}} \left(|000\rangle + |111\rangle\right)\]

The protocol

  1. Alice creates an EPR pair in the state \(|\Phi^{+}\rangle_{AB_1}\) with Bob and tells Bob to proceed.

  2. Bob receives his half of an EPR pair that Alice created with him, creates a new EPR pair \(|\Phi^{+}\rangle_{B_2C}\) with Charlie, and applies a CNOT with \(B_1\) as control and \(B_2\) as target.

  3. Bob measures \(B_2\) in the standard basis and sends the outcome \(b_2\) to Charlie.

  4. Charlie receives his half of an EPR pair that Bob created with him and the measurement outcome \(b_2\).

  5. Charlie performs an X correction depending on \(b_2\), \(A\), \(B_1\) and \(C\) now share a GHZ state.

  6. All three measure their remaining qubits — their outcomes \(a\), \(b_1\) and \(c\) are correlated.

The communication flow is:

Alice ──EPR_1──► Bob
Bob ──EPR_2──► Charlie
Charlie ──continue──► Bob
Bob ──b_2──► Charlie
Charlie ──continue──► Bob
Bob ──continue──► Alice

Alice’s code

From aliceTest.py:

async def run_alice(reader: StreamReader, writer: StreamWriter) -> int:
    # This is "Alice": the start node of the GHZ chain
    this_node_name = "Alice"
    remote_node_name = "Bob"  # A node with this name *must* exist in "simulaqron_network.json"

    epr_socket = EPRSocket(remote_node_name)

    # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob.
    # Bob is reached via EPRSocket for quantum and reader/writer for classical.
    sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket])

    # Create an entangled qubit with Bob
    A = epr_socket.create_keep()[0]

    # We need to flush the EPR pair creation, so the reciever does not timeout on the other side.
    sim_conn.flush()

    writer.write("receive_qubit".encode("utf-8"))
    answer = await reader.read(100)

    assert answer.decode("utf-8") == "continue"

    a = A.measure()

    # flush() executes all queued quantum operations and makes measurement
    # results available.  Before flush(), a is just a future/promise.
    sim_conn.flush()

    # int(a) extracts the measurement outcome — only valid after flush().
    a_val = int(a)
    sim_conn.close()

    print(f"{node_name}: My outcome is '{a_val}'")
    return 0

Bob’s code

Bob is the key node — he has EPR sockets to both Alice and Charlie. However, Bob also has 2 roles in the protocol.

Bob acts as a server when communicating with Alice:

async def run_bob(reader: StreamReader, writer: StreamWriter) -> int:
    # This is "Bob": the middle node of the GHZ chain
    this_node_name = "Bob"
    start_node_name = "Alice"  # A node with this name *must* exist in "simulaqron_network.json"
    end_node_name = "Charlie"  # A node with this name *must* exist in "simulaqron_network.json"

    message = await reader.read(100)
    assert message.decode("utf-8") == "receive_qubit"

    epr_socket_alice = EPRSocket(start_node_name)
    epr_socket_charlie = EPRSocket(end_node_name)

    sockets = SocketsConfig(network_config, "default", NodeConfigType.APP)

    charlie_client = SimulaQronClassicalClient(sockets)

    # sim_conn is our connection to the quantum backend (SimulaQron), not to
    # Alice or Charlie.  They are reached via EPRSockets for quantum and
    # reader/writer for classical.
    sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket_alice, epr_socket_charlie])

    # Receive an entangled qubit from Alice
    B_1 = epr_socket_alice.recv_keep()[0]

    # Create a new entangled pair with Charlie
    B_2 = epr_socket_charlie.create_keep()[0]

    # We need to flush the EPR pair creation, so the reciever does not timeout on the other side.
    sim_conn.flush()

    # The next part of the protocol needs to be executed between Bob and Charlie.
    # In this interaction, Bob acts as client
    await charlie_client.connect_and_run(end_node_name, send_to_charlie, sim_conn, B_1, B_2)

    # At this point, we have achieved |GHZ>_{AB_1C}
    # Tell Alice to continue
    writer.write("continue".encode("utf-8"))

    # We can measure the B_1 qubit, part of the GHZ
    b_1 = B_1.measure()

    # flush() executes all queued quantum operations and makes measurement
    # results available. Before flush(), c is just a future/promise.
    sim_conn.flush()

    b_1_val = int(b_1)
    sim_conn.close()
    print(f"{this_node_name}: My outcome is '{b_1_val}'")
    return 0

However, Bob acts as a client when comunicating with Charlie:

async def send_to_charlie(reader: StreamReader, writer: StreamWriter,
                          sim_conn: NetQASMConnection, B_1: Qubit, B_2: Qubit) -> None:
    # Tell Bob to receive the EPR half
    writer.write("receive_qubit".encode("utf-8"))

    # Await for the green light from Charlie
    message = await reader.read(100)
    assert message.decode("utf-8") == "continue"

    # Create the GHZ state by entangling the qubit entangled with Alice
    B_1.cnot(B_2)

    # We now measure the entagled qubit with Charlie
    b_2 = B_2.measure()

    # flush() executes all queued quantum operations and makes measurement
    # results available.  Before flush(), b_2 is just a future/promise.
    sim_conn.flush()

    # int(b_2) extracts the measurement outcome — only valid after flush().
    b_2_val = int(b_2)

    # We send the measurement b_2 to Charlie, for corrections.
    writer.write(f"{b_2_val}".encode("utf-8"))

    # We wait for green light from Charlie, again
    charlie_msg = await reader.read(100)
    assert charlie_msg.decode("utf-8") == "continue"

Charlie’s code

Charlie is the end receiver of the GHZ state. It only needs to communicate with Bob:

async def run_charlie(reader: StreamReader, writer: StreamWriter) -> int:
    # This is "Charlie": the end node of the GHZ chain
    this_node_name = "Charlie"
    remote_node_name = "Bob"
    message = await reader.read(100)

    assert message.decode("utf-8") == "receive_qubit"
    epr_socket = EPRSocket(remote_node_name)

    # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob.
    # Bob is reached via EPRSocket for quantum and reader/writer for classical.
    sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket])

    # Receive an entangled qubit
    C = epr_socket.recv_keep()[0]

    # We need to flush the EPR pair creation, so the reciever does not timeout on the other side.
    sim_conn.flush()

    # Signal Bob to send us the b_2 measurement
    writer.write("continue".encode("utf-8"))

    # Receive b_2 measurement from Bob
    b_2_bytes: bytes = await reader.read(100)
    b_2_val = int(b_2_bytes.decode("utf-8"))

    # Perform an X correction depending on Bob's measurement
    if b_2_val == 1:
        C.X()

    sim_conn.flush()

    # At this point, we have achieved |GHZ>_{AB_1C}
    # Tell Bob to continue
    writer.write("continue".encode("utf-8"))

    # We can measure the C qubit, part of the GHZ
    c = C.measure()

    # flush() executes all queued quantum operations and makes measurement
    # results available.  Before flush(), c is just a future/promise.
    sim_conn.flush()

    # int(c) extracts the measurement outcome — only valid after flush().
    c_val = int(c)
    sim_conn.close()
    print(f"{this_node_name}: My outcome is '{c_val}'")
    return 0

Key concepts

  • Multiple EPR sockets: A single NetQASMConnection can hold EPR sockets to multiple remote nodes.

  • Three-party coordination: Bob acts as both a server (for Alice) and a client (to Charlie), using both SimulaQronClassicalServer and SimulaQronClassicalClient.

  • CNOT extends entanglement: Applying CNOT between two entangled qubits from different pairs creates a GHZ state.

Running

cd examples/new-sdk/extendGHZ
bash run.sh