import numpy as np

from qiskit_aer import AerSimulator
from qiskit.quantum_info import Statevector, Operator
from qiskit.visualization import plot_histogram
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
sim = AerSimulator()

from util import zero, one

QI: Superdense Coding#

In this notebook, we’ll introduce superdense coding. It is a protocol that enables two parties to share two classical bits of information using a shared and entangled qubit.

References

  1. Qiskit Notebook on Superdense Coding

  2. Introduction to Quantum Information Science: Lecture 9 by Scott Aaronson

  3. Quantum Computation and Quantum Information: Chapter 2, Nielsen and Chuang

Protocol#

The superdense coding protocol involves

  1. an entangled qubit that Alice and Bob will share,

  2. a message encoding step that Alice will apply to a send a message, and

  3. a message decoding step that Bob will apply to decode the sent message.

Part 1: Create Bell State#

  1. We need a third party, Charlie, to create an entangled bell state.

  2. Charlie will send qubit \(|q_1\rangle\) to Alice.

  3. Charlie will send qubit \(|q_0\rangle\) to Bob.

def create_alice_bob() -> QuantumCircuit:
    alice_qubit = QuantumRegister(1, "q_Alice")
    bob_qubit = QuantumRegister(1, "q_Bob")
    return QuantumCircuit(bob_qubit, alice_qubit)
def create_bell_state(qc: QuantumCircuit) -> QuantumCircuit:
    qc.h(1)
    qc.cx(1, 0)
    return qc
create_bell_state(create_alice_bob()).draw(output="mpl", style="iqp")
../_images/17b09cd786941a2102cefd5ab9febe638a26f8b273e642e281448bd580b6a01e.png

Part 2: Encode Message#

Alice will use the following encoding scheme to encode classical bits into quantum state.

  1. 00: \(\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle\)

  2. 10: \(\frac{1}{\sqrt{2}}|00\rangle - \frac{1}{\sqrt{2}}|11\rangle\)

  3. 01: \(\frac{1}{\sqrt{2}}|01\rangle + \frac{1}{\sqrt{2}}|10\rangle\)

  4. 11: \(\frac{1}{\sqrt{2}}|01\rangle - \frac{1}{\sqrt{2}}|10\rangle\)

To implement this coding scheme, Alice can use a message-specific quantum circuit. Thus she will end up with four circuits.

def encode_message(qc, msg: str) -> QuantumCircuit:
    # Adapted from: https://learn.qiskit.org/course/ch-algorithms/superdense-coding
    # Create quantum circuit that sends `msg`
    if len(msg) != 2 or not set(msg).issubset({"0", "1"}):
        raise ValueError(f"message '{msg}' is invalid")
                
    # Encode message
    if msg[1] == "1":
        qc.x(1)
    if msg[0] == "1":
        qc.z(1)
    if msg[0] == "0" and msg[1] == "0":
        qc.id(1)
    return qc

Messsage 00#

The message for 00 is given by

\[ I (\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) = \frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle \]
message = "00"
qc_00 = create_bell_state(create_alice_bob())
qc_00.barrier(label="Charlie hand off point")
qc_00 = encode_message(qc_00, message)
qc_00.draw(output="mpl", style="iqp")
../_images/905db3889e374cf3a455714e1189d0aed8fd769821089d29cdafba52757472dd.png
m_00 = (zero ^ zero).evolve(qc_00)
m_00.draw("latex")
\[\frac{\sqrt{2}}{2} |00\rangle+\frac{\sqrt{2}}{2} |11\rangle\]

Messsage 10#

The message for 10 is given by

\[ (Z \otimes I) (|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) = \frac{1}{\sqrt{2}}|00\rangle - \frac{1}{\sqrt{2}}|11\rangle \,. \]
message = "10"
qc_10 = create_bell_state(create_alice_bob())
qc_10.barrier(label="Charlie hand off point")
qc_10 = encode_message(qc_10, message)
qc_10.draw(output="mpl", style="iqp")
../_images/68cf7917d3a1b5e145823beed5d96fbe8875e4fe757db66ed8e044cba3b81aa6.png
m_10 = (zero ^ zero).evolve(qc_10)
m_10.draw("latex")
\[\frac{\sqrt{2}}{2} |00\rangle- \frac{\sqrt{2}}{2} |11\rangle\]

Messsage 01#

The message for 01 is given by

\[ (X \otimes I) |00\rangle + \frac{1}{\sqrt{2}}|11\rangle) = \frac{1}{\sqrt{2}}|01\rangle + \frac{1}{\sqrt{2}}|10\rangle \]
message = "01"
qc_01 = create_bell_state(create_alice_bob())
qc_01.barrier(label="Charlie hand off point")
qc_01 = encode_message(qc_01, message)
qc_01.draw(output="mpl", style="iqp")
../_images/ec531052ea60cc460f4595dfd476ee8395e08c6c5870eaddee9ec78916ba56ba.png
m_01 = (zero ^ zero).evolve(qc_01)
m_01.draw("latex")
\[\frac{\sqrt{2}}{2} |01\rangle+\frac{\sqrt{2}}{2} |10\rangle\]

Messsage 11#

The message for 11 is given by

\[ (ZX \otimes I) (|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) = \frac{1}{\sqrt{2}}|01\rangle - \frac{1}{\sqrt{2}}|10\rangle \]
message = "11"
qc_11 = create_bell_state(create_alice_bob())
qc_11.barrier(label="Charlie hand off point")
qc_11 = encode_message(qc_11, message)
qc_11.draw(output="mpl", style="iqp")
../_images/7e6fd2eb0a91518bbf34423b6b0fb47dec3e75dab48f2a2fe7b77c72b598b7cb.png
m_11 = (zero ^ zero).evolve(qc_11)
m_11.draw("latex")
\[\frac{\sqrt{2}}{2} |01\rangle- \frac{\sqrt{2}}{2} |10\rangle\]

Observation 1: All messages are orthogonal#

The messages are all orthogonal to each other.

# Orthogonality check
messages = [m_00, m_10, m_01, m_11]
for i, m1 in enumerate(messages):
    for j, m2 in enumerate(messages):
        if i != j and not np.allclose(np.dot(m_00, m_01), [0]):
            print("Not orthogonal", m1, m2)

Observation 2: Ambiguity#

Although the encoding of the message for 01 and 11 are orthogonal, we still cannot distinguish them by measurement unless we choose an appropriate basis. We demonstrate the ambiguity in distinguishing 01 and 11 now.

def test_encode_message(msg):
    qc = QuantumCircuit(2, 2)
    qc = create_bell_state(qc)
    qc = encode_message(qc, msg)
    qc.measure(0, 0); qc.measure(1, 1)
    result = sim.run(qc).result()
    counts = result.get_counts(qc)
    return counts, qc
counts, qc = test_encode_message("01")
plot_histogram(counts)
../_images/92d3c69ed52734fce601774016c459b962085e4482946eeeab99c31d21aa478d.png
# Gives same histogram as 10
counts, qc = test_encode_message("11")
plot_histogram(counts)
../_images/781552f370c28e0aabb65cf9f544159a7e22958489bafbf5730c4a968788af2a.png

Part 3: Decode Message#

After Alice encodes the message, Charlie will transport the qubit from Alice to Bob. Thus Bob will have both qubits now. However, Bob cannot directly measure the qubits to the get the results due to the ambiguity in distinguishing 01 from 11. Instead, Bob will need to perform decoding in an appropriate basis. This is accomplished with the circuit below.

def decode_message(qc: QuantumCircuit) -> QuantumCircuit:
    qc.cx(1, 0)
    qc.h(1)
    return qc
qc_10 = create_alice_bob()
qc_10.initialize(m_10)
qc_10 = decode_message(qc_10)
qc_10.draw(output="mpl", style="iqp")
../_images/a0ca482c828b5d66f49fa09f6f01dc87ab3b8d911dd487188f9ef8a42b7dd5ba.png
(zero ^ zero).evolve(qc_10).draw('latex')
\[ |10\rangle\]
qc_11 = create_alice_bob()
qc_11.initialize(m_11)
qc_11 = decode_message(qc_11)
qc_11.draw(output="mpl", style="iqp")
../_images/82a99f8ff5222228669d6af82f480329604713ba2de09782acc42fc384cfad51.png
(zero ^ zero).evolve(qc_11).draw('latex')
\[ |11\rangle\]

Why does this work?#

  1. If Bob obtains \(\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle\) (for 00), then

\[\begin{align*} (H \otimes I) CNOT (\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) & = (H \otimes I) (|+\rangle \otimes |0\rangle) \\ & = |0\rangle \otimes |0\rangle \,. \end{align*}\]
  1. If Bob obtains \(\frac{1}{\sqrt{2}}|00\rangle - \frac{1}{\sqrt{2}}|11\rangle\) (for 10), then

\[\begin{align*} (H \otimes I) CNOT (\frac{1}{\sqrt{2}}|00\rangle + \frac{1}{\sqrt{2}}|11\rangle) & = (H \otimes I) (|-\rangle \otimes |0\rangle) \\ & = |1\rangle \otimes |0\rangle \,. \end{align*}\]
  1. If Bob obtains \(\frac{1}{\sqrt{2}}|01\rangle + \frac{1}{\sqrt{2}}|10\rangle\) (for 01), then

\[\begin{align*} (H \otimes I) CNOT (\frac{1}{\sqrt{2}}|01\rangle + \frac{1}{\sqrt{2}}|10\rangle) & = (H \otimes I) (|+\rangle \otimes |1\rangle) \\ & = |0\rangle \otimes |1\rangle \,. \end{align*}\]
  1. If Bob obtains \(\frac{1}{\sqrt{2}}|01\rangle - \frac{1}{\sqrt{2}}|10\rangle\) (for 11), then

\[\begin{align*} (H \otimes I) CNOT (\frac{1}{\sqrt{2}}|01\rangle - \frac{1}{\sqrt{2}}|10\rangle) & = (H \otimes I) (|-\rangle \otimes |1\rangle) \\ & = |1\rangle \otimes |1\rangle \,. \end{align*}\]

Putting it Together#

We put the entire protocol together for end-to-end experimentation.

def superdense_coding(msg: str):
    alice_qubit = QuantumRegister(1, "q_Alice")
    bob_qubit = QuantumRegister(1, "q_Bob")
    result = ClassicalRegister(2, "decode")
    qc = QuantumCircuit(bob_qubit, alice_qubit, result)

    # Part 1: Third party creates entangled state
    qc = create_bell_state(qc)
    qc.barrier(label="Charlie hand off point")
    
    # Part 2: Alice encode message on qubit 1
    qc = encode_message(qc, msg)
    qc.barrier(label="Charlie sends \nAlice's qubit to Bob")
    
    # Part 3: Bob puts in basis and measures
    qc = decode_message(qc)
    qc.measure(0, 0); qc.measure(1, 1)

    result = sim.run(qc).result()
    counts = result.get_counts(qc)
    return counts, qc
counts, qc = superdense_coding("00")
qc.draw(output="mpl", style="iqp")
../_images/4168dc50dfc49906b126a377801e3c6e25a124839b72f9d7a1626501e0e444ac.png
plot_histogram(counts)
../_images/4e176c9c8091ffdb0e5abf0bc2b4cc11ab17c097d4fd895e62e056650277acb9.png
# Change message
message = "01"
counts, qc = superdense_coding(message)
plot_histogram(counts)
../_images/819229a9ffb38351d8a5c0184280fbe43bceb20e2adc99b87621040b4e1a0db0.png

Summary#

  1. First example of quantum protocol.

  2. We were able to transmit 2 classical bits using 1 entangled qubit.

  3. Thus we have a quantum system that is split across two parties.