This tutorial introduces classical communication in Q2NS by combining ns-3 UDP networking with quantum protocols.
Many quantum protocols require both quantum resources and classical messages. Q2NS simulations therefore often use:
- quantum channels for qubits or entanglement
- classical network stacks for measurement results or control messages
By the end of this tutorial you will be able to:
- create a simple UDP channel between Q2NS nodes
- combine quantum and classical communication in one protocol
- simulate protocols where quantum and classical information arrive at different times
We will build toward a realistic teleportation protocol in three steps:
- Teleportation without classical communication
- Minimal classical communication with UDP
- Teleportation with classical corrections over UDP
1. Why Classical Communication Is Required
Entanglement can extend the capabilities of classical networks, but it cannot transmit information by itself. Classical communication is always required, a consequence of the no-signaling theorem.
A standard example is quantum teleportation in which Alice can "teleport" a quantum state to Bob using a pre-shared entangled pair, local quantum operations, and purely classical communication:
- Alice and Bob share an entangled pair and go to different locations
- Alice prepares a local quantum state
- Alice performs a Bell-state measurement (BSM)
- Alice sends the two measurement results to Bob
- Bob uses those two classical bits to apply corrections to his state
Without those classical bits, Bob cannot recover Alice's state.
2. Example 1: Teleportation Without Classical Communication
We begin with a simplified version that ignores the classical message. This demonstrates the basic quantum operations but is not physically realistic. From a coding perspective, this example does not introduce anything new, so it is good practice to work through it carefully.
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/q2ns-qubit.h"
#include "ns3/core-module.h"
#include "ns3/simulator.h"
#include <iostream>
std::cout << "[DEMO] Teleportation (A->B) starting\n";
ns3::RngSeedManager::SetSeed(1);
ns3::RngSeedManager::SetRun(1);
ch->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
std::pair<int, int> ms;
Simulator::Schedule(NanoSeconds(1), [&]() {
auto [qA, qBremote] = A->CreateBellPair();
bool ok = A->Send(qBremote, B->GetId());
std::cout << "[SEND][quantum] A->B: " << (ok ? "ok" : "failed") << "\n";
auto qAToTeleport = A->CreateQubit();
A->Apply(gates::H(), {qAToTeleport});
ms = A->MeasureBell(qAToTeleport, qA);
std::cout << "[A] BSM results: " << ms.first << ", " << ms.second << "\n";
});
B->SetRecvCallback([&](std::shared_ptr<Qubit> q){
std::cout << "[RECV][quantum][B]: yes\n";
if(ms.second)
B->Apply(gates::X(), {q});
if(ms.first)
B->Apply(gates::Z(), {q});
int mx = B->Measure(q, Basis::X);
std::cout << "[B][VERIFY] Final state correct: " << ((mx==0) ? "yes":"no") << "\n";
});
Simulator::Stop(Seconds(10));
Simulator::Run();
std::cout << "[DONE] Teleportation finished\n";
Simulator::Destroy();
}
Main user-facing facade for creating and configuring a quantum network.
ns3::Ptr< QNode > CreateNode(const std::string &label="")
Create a QNode with an optional human-readable label.
ns3::Ptr< QChannel > InstallQuantumLink(ns3::Ptr< QNode > a, ns3::Ptr< QNode > b)
Install a duplex quantum link between two nodes.
void SetQStateBackend(QStateBackend b)
Set the default backend used for newly created quantum states.
What this example shows
- Bell pair creation
- Sending a qubit through a quantum link
- Bell-state measurement
- Pauli corrections
What is unrealistic
Bob uses Alice's measurement results immediately, but there is no way he could actually know them this way in practice. In reality, Alice must send them through a classical channel to Bob. Only after Bob has received both his half of the EPR pair and the classical message, can he perform these corrections. In some situations, these kinds of abstractions are perfectly fine, e.g. for learning about protocols. However, for more realistic simulations, we need to understand how to simulate classical communication.
3. Example 2: Minimal Classical Communication with UDP
Before combining classical and quantum communication, we first show a minimal example of just classical communication. In this example, we package and send the classical message in the simplest possible form that can work on the classical network stack, known as User Datagram Protocol (UDP) packets. The channel is also one of the simplest channels that just directly connects two nodes, known as a point-to-point (P2P) channel, and without any physical characteristics other than the delay. In the future, you can explore more advanced protocols, e.g. TCP, and channel types, e.g. specifying fiber optic with physical characteristics or broadcasting on wifi.
We start with our includes, in this case using both q2ns and standard ns-3 libraries that are necessary for installing the classical network capabilities. We also set the seed and run at the very beginning of main, as usual.
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include <iostream>
ns3::RngSeedManager::SetSeed(1);
ns3::RngSeedManager::SetRun(1);
Next, we create a NetController and 2 QNodes representing Alice and Bob. This might look a little strange to those familiar with ns-3, since we typically use ns3::Nodes and NodeContainers, but the goal here is to show the integration of Q2NS into ns-3, specifically that QNode inherits directly from ns3::Node and therefore has the full set of standard classical networking capabilities along with its new quantum networking capabilities.
Now we configure the classical networking capabilities. This is standard ns-3, with no Q2NS flavor other than using the nodes, A and B, directly rather than the often used NodeContainer helper. This also means that you can copy this code exactly for connecting any two QNodes with the P2P classical channel to send UDP packets.
To start, we use the InternetStackHelper so the nodes will be able to send IP/UDP packets.
InternetStackHelper internet;
internet.Install(A);
internet.Install(B);
Then we establish a point-to-point classical channel between the nodes, including configuring the DataRate and Delay.
PointToPointHelper p2p;
p2p.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
p2p.SetChannelAttribute("Delay", StringValue("1ms"));
NetDeviceContainer devices = p2p.Install(A, B);
Next we assign IPv4 addresses to the two ends of the link. ns-3 requires this before we can send UDP packets.
Ipv4AddressHelper ip;
ip.SetBase("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = ip.Assign(devices);
Lastly, we create the UDP sockets for Alice and Bob to actually be able to communicate on this channel. This requires setting a port number, 9000 being the common choice for examples (NOTE: using it in a real world application can lead to security risks). This is handled slightly differently for each node since Alice is the sender and Bob is the receiver.
Though it is not required, the general convention is to set up the receiver's socket first to establish where packets will arrive, then we can establish who will send them. So we will start by creating Bob's socket. First we construct an address object representing that Bob's socket should receive packets from any interface (Ipv4Address::GetAny()) with this port number. Then we bind Bob's socket to this address object.
const uint16_t port = 9000;
Ptr<Socket> bobSocket = Socket::CreateSocket(B, UdpSocketFactory::GetTypeId());
InetSocketAddress local = InetSocketAddress(Ipv4Address::GetAny(), port);
bobSocket->Bind(local);
We typically set the receiver's callback at this point. This must take a Ptr<Socket> socket so the function knows which socket has received a packet, which is especially important for reusability and clarity when there are many sockets in a simulation. Then, it gets packets from the socket through socket->Recv(), one at a time until there are none left. Then we can do whatever we want with each packet, in this case just printing the size as a small verification that we received the full, expected packet.
bobSocket->SetRecvCallback([](Ptr<Socket> socket) {
while (Ptr<Packet> packet = socket->Recv()) {
std::cout << "[B][classical] Received UDP packet at " << Simulator::Now().GetSeconds()
<< " s, size = " << packet->GetSize() << " bytes\n";
}
});
The last configuration step is to create Alice's socket. In this case, she only needs to establish where her socket should send packets. So, she gets Bob's IP address with interfaces.GetAddress(1), creates an address object with this and the port number, and connects her socket to it. This means that aliceSocket->Send(...) will automatically transmit packets to Bob, without having to specify his address every time.
Ptr<Socket> aliceSocket = Socket::CreateSocket(A, UdpSocketFactory::GetTypeId());
InetSocketAddress remote = InetSocketAddress(interfaces.GetAddress(1), port);
aliceSocket->Connect(remote);
Finally, we set up the simulation itself by scheduling a simple event where Alice sends a packet consisting of 4 bytes. It's worth noting that network packets carry data in bytes, so we send messages as byte payloads even if the actual information content only contains a single bit.
Simulator::Schedule(Seconds(1.0), [aliceSocket]() {
Ptr<Packet> packet = Create<Packet>(4);
aliceSocket->Send(packet);
std::cout << "[A][classical] Sent UDP packet at " << Simulator::Now().GetSeconds() << " s\n";
});
Simulator::Stop(Seconds(10));
Simulator::Run();
std::cout << "[DONE] Classical communication (A->B) finished\n";
Simulator::Destroy();
return 0;
}
By putting this all together, we get the final simulation file
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include <iostream>
std::cout << "[DEMO] Classical communication (A->B) starting\n";
ns3::RngSeedManager::SetSeed(1);
ns3::RngSeedManager::SetRun(1);
InternetStackHelper internet;
internet.Install(A);
internet.Install(B);
PointToPointHelper p2p;
p2p.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
p2p.SetChannelAttribute("Delay", StringValue("1ms"));
NetDeviceContainer devices = p2p.Install(A, B);
Ipv4AddressHelper ip;
ip.SetBase("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = ip.Assign(devices);
const uint16_t port = 9000;
Ptr<Socket> bobSocket = Socket::CreateSocket(B, UdpSocketFactory::GetTypeId());
InetSocketAddress local = InetSocketAddress(Ipv4Address::GetAny(), port);
bobSocket->Bind(local);
bobSocket->SetRecvCallback([](Ptr<Socket> socket) {
while (Ptr<Packet> packet = socket->Recv()) {
std::cout << "[B][classical] Received UDP packet at " << Simulator::Now().GetSeconds()
<< " s, size = " << packet->GetSize() << " bytes\n";
}
});
Ptr<Socket> aliceSocket = Socket::CreateSocket(A, UdpSocketFactory::GetTypeId());
InetSocketAddress remote = InetSocketAddress(interfaces.GetAddress(1), port);
aliceSocket->Connect(remote);
Simulator::Schedule(Seconds(1.0), [aliceSocket]() {
Ptr<Packet> packet = Create<Packet>(4);
aliceSocket->Send(packet);
std::cout << "[A][classical] Sent UDP packet at " << Simulator::Now().GetSeconds() << " s\n";
});
Simulator::Stop(Seconds(10));
Simulator::Run();
std::cout << "[DONE] Classical communication (A->B) finished\n";
Simulator::Destroy();
return 0;
}
4. Example 3: Teleportation with Classical Communication
Now we combine both layers. The Bell-pair half travels through a quantum channel, while the measurement results are sent as a UDP packet. Bob must wait until both arrive to perform his corrections, only ever using locally available information at any given time.
We start with our includes, which is just the combination of the first two examples:
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/q2ns-qubit.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/simulator.h"
#include <iostream>
Then we define a small struct to track Bob's locally available information. This will be updated as Bob receives the qubits and classical packets and only be used by Bob, while avoiding global variables or unrealistic information transfer.
struct BobInfo {
std::shared_ptr<Qubit> qBremote;
bool qubitArrived = false;
bool bitsArrived = false;
};
We also define a function that represents Bob checking if he has all the necessary information and, once he does, performing corrections. This will be part of the callback used by Bob's node for both classical packets and qubits so that it does not matter which one arrives last. Other than the first conditional, this is effectively identical to the callback used in the basic teleportation code from the beginning.
void TryCorrections(Ptr<QNode> bob, BobInfo& bobInfo) {
if (!bobInfo.qubitArrived || !bobInfo.bitsArrived) {
return;
}
std::cout << "[B] Applying corrections: Z^" << bobInfo.m1 << " X^" << bobInfo.m2 << "|state>\n";
if (bobInfo.m2) {
bob->Apply(gates::X(), {bobInfo.qBremote});
}
if (bobInfo.m1) {
bob->Apply(gates::Z(), {bobInfo.qBremote});
}
int mx = bob->Measure(bobInfo.qBremote, Basis::X);
std::cout << "[B][VERIFY] Final state is correct: " << ((mx == 0) ? "yes" : "no") << "\n";
}
Now we can define our main, starting as usual:
std::cout << "[DEMO] Teleportation (A->B) with classical communication starting\n";
ns3::RngSeedManager::SetSeed(1);
ns3::RngSeedManager::SetRun(1);
The quantum network configuration is the same as our first example:
ch->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
The classical network configuration is nearly the same as our previous example, except we will wait to define Bob's classical callback later. This is purely for clarity's sake in this example and does not affect the simulation compared to defining the callback right after we bind Bob's socket to an address.
InternetStackHelper internet;
internet.Install(A);
internet.Install(B);
PointToPointHelper p2p;
p2p.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
p2p.SetChannelAttribute("Delay", StringValue("1ms"));
NetDeviceContainer devices = p2p.Install(A, B);
Ipv4AddressHelper ip;
ip.SetBase("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = ip.Assign(devices);
const uint16_t port = 9000;
Ptr<Socket> bobSocket = Socket::CreateSocket(B, UdpSocketFactory::GetTypeId());
bobSocket->Bind(InetSocketAddress(Ipv4Address::GetAny(), port));
Ptr<Socket> aliceSocket = Socket::CreateSocket(A, UdpSocketFactory::GetTypeId());
aliceSocket->Connect(InetSocketAddress(interfaces.GetAddress(1), port));
Now we create an instance of Bob's local information struct and define his callbacks. The quantum and classical callbacks can be defined in either order, as long as both are defined before the events that trigger them are scheduled. We will adopt the convention of quantum and then classical here.
In the quantum callback, Bob records that the qubit arrived and stores it. Then he attempts corrections, which will only succeed if he already received the classical packet.
BobInfo bobInfo;
B->SetRecvCallback([B, &bobInfo](std::shared_ptr<Qubit> q) {
std::cout << "[RECV][quantum][B]: yes\n";
bobInfo.qubitArrived = true;
bobInfo.qBremote = q;
TryCorrections(B, bobInfo);
});
In the classical callback, Bob records that the classical packet arrived and decodes and stores it. The standard method for decoding a packet is packet->CopyData(copyDataHere, numberOfBytesToCopy). Since we are only interested in the single bit storing the measurement result, rather than the whole byte contents, we use a mask of & 1. This is not strictly necessary here, but is good practice in general so we include it for completeness.
After this, he attempts corrections, which will only succeed if he already received the qubit.
bobSocket->SetRecvCallback([B, &bobInfo](Ptr<Socket> socket) {
while (Ptr<Packet> packet = socket->Recv()) {
bobInfo.bitsArrived = true;
uint8_t bytes[2] = {0, 0};
packet->CopyData(bytes, 2);
bobInfo.m1 = bytes[0] & 1;
bobInfo.m2 = bytes[1] & 1;
std::cout << "[RECV][classical][B] m1=" << bobInfo.m1 << ", m2=" << bobInfo.m2 << "\n";
TryCorrections(B, bobInfo);
}
});
Finally, we setup the simulation itself by scheduling an event consisting of three actions by Alice:
- Creating a Bell pair and sending half to Bob (same as the first example)
- Creating a single qubit, preparing it in the |+> state, and performing a BSM (same as the first example)
- Sending the BSM results as a packet through her socket
This will trigger the callbacks and result in a realistic simulation of quantum teleportation.
Simulator::Schedule(NanoSeconds(1), [&]() {
auto [qA, qBremote] = A->CreateBellPair();
bool ok = A->Send(qBremote, B->GetId());
std::cout << "[SEND][quantum] A->B: " << (ok ? "ok" : "failed") << "\n";
auto qAToTeleport = A->CreateQubit();
A->Apply(gates::H(), {qAToTeleport});
auto [
m1,
m2] = A->MeasureBell(qAToTeleport, qA);
std::cout <<
"[A] BSM results: " <<
m1 <<
", " <<
m2 <<
"\n";
uint8_t bytes[2] = {
static_cast<uint8_t
>(
m1),
static_cast<uint8_t
>(
m2)};
aliceSocket->Send(Create<Packet>(bytes, 2));
std::cout <<
"[SEND][classical] A->B: m1=" <<
m1 <<
", m2=" <<
m2 <<
"\n";
});
By putting this all together, we get the final simulation file
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/q2ns-qubit.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/simulator.h"
#include <iostream>
namespace {
struct BobInfo {
std::shared_ptr<Qubit> qBremote;
bool qubitArrived = false;
bool bitsArrived = false;
};
void TryCorrections(Ptr<QNode> bob, BobInfo& bobInfo) {
if (!bobInfo.qubitArrived || !bobInfo.bitsArrived) {
return;
}
std::cout << "[B] Applying corrections: Z^" << bobInfo.m1 << " X^" << bobInfo.m2 << "|state>\n";
if (bobInfo.m2) {
bob->Apply(gates::X(), {bobInfo.qBremote});
}
if (bobInfo.m1) {
bob->Apply(gates::Z(), {bobInfo.qBremote});
}
int mx = bob->Measure(bobInfo.qBremote, Basis::X);
std::cout << "[B][VERIFY] Final state is correct: " << ((mx == 0) ? "yes" : "no") << "\n";
}
}
std::cout << "[DEMO] Teleportation (A->B) with classical communication starting\n";
ns3::RngSeedManager::SetSeed(1);
ns3::RngSeedManager::SetRun(1);
ch->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
InternetStackHelper internet;
internet.Install(A);
internet.Install(B);
PointToPointHelper p2p;
p2p.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
p2p.SetChannelAttribute("Delay", StringValue("1ms"));
NetDeviceContainer devices = p2p.Install(A, B);
Ipv4AddressHelper ip;
ip.SetBase("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = ip.Assign(devices);
const uint16_t port = 9000;
Ptr<Socket> bobSocket = Socket::CreateSocket(B, UdpSocketFactory::GetTypeId());
bobSocket->Bind(InetSocketAddress(Ipv4Address::GetAny(), port));
Ptr<Socket> aliceSocket = Socket::CreateSocket(A, UdpSocketFactory::GetTypeId());
aliceSocket->Connect(InetSocketAddress(interfaces.GetAddress(1), port));
BobInfo bobInfo;
B->SetRecvCallback([B, &bobInfo](std::shared_ptr<Qubit> q) {
std::cout << "[RECV][quantum][B]: yes\n";
bobInfo.qubitArrived = true;
bobInfo.qBremote = q;
});
bobSocket->SetRecvCallback([B, &bobInfo](Ptr<Socket> socket) {
while (Ptr<Packet> packet = socket->Recv()) {
bobInfo.bitsArrived = true;
uint8_t bytes[2] = {0, 0};
packet->CopyData(bytes, 2);
bobInfo.m1 = bytes[0] & 1;
bobInfo.m2 = bytes[1] & 1;
std::cout << "[RECV][classical][B] m1=" << bobInfo.m1 << ", m2=" << bobInfo.m2 << "\n";
}
});
Simulator::Schedule(NanoSeconds(1), [&]() {
auto [qA, qBremote] = A->CreateBellPair();
bool ok = A->Send(qBremote, B->GetId());
std::cout << "[SEND][quantum] A->B: " << (ok ? "ok" : "failed") << "\n";
auto qAToTeleport = A->CreateQubit();
A->Apply(gates::H(), {qAToTeleport});
auto [
m1,
m2] = A->MeasureBell(qAToTeleport, qA);
std::cout <<
"[A] BSM results: " <<
m1 <<
", " <<
m2 <<
"\n";
uint8_t bytes[2] = {
static_cast<uint8_t
>(
m1),
static_cast<uint8_t
>(
m2)};
aliceSocket->Send(Create<Packet>(bytes, 2));
std::cout <<
"[SEND][classical] A->B: m1=" <<
m1 <<
", m2=" <<
m2 <<
"\n";
});
Simulator::Stop(Seconds(10));
Simulator::Run();
std::cout << "[DONE] Teleportation (A->B) with classical communication finished\n";
Simulator::Destroy();
return 0;
}
void TryCorrections(Ptr< QNode > bob, BobInfo &bobInfo)
Exercises
Exercise 1: Change the arrival ordering
In the teleportation with classical communication example directly above, the qubits will arrive before the classical packet. Change the code so that the classical packet arrives first and confirm that the protocol is still successful.
Solution
There are several approaches here, but the simplest is to make the delay for the quantum channel significantly longer than the classical channel (without going over the upper limit set by Simulator::Stop(...)):
ch->SetAttribute("Delay", TimeValue(MilliSeconds(10)));
NOTE: For the classical network, end-to-end arrival time depends on both propagation delay and transmission time. The transmission time is packet-size divided by data rate. Since UDP packets still carry protocol overhead and pass through the network stack, making the classical packet arrive first may require changing both the point-to-point delay and the data rate, or increasing the quantum-link delay significantly enough to create a clear timing difference. In fact, simply playing with these parameters and seeing the results is a great way to start exploring how classical networks behave, if you are not already familiar.
Exercise 2: Entanglement swapping with classical communication
Entanglement swapping is another core protocol in quantum networking. It uses the same basic idea as teleportation to allow nodes to share entanglement through intermediary nodes, known as repeaters. Let's consider a simple repeater chain: Alice – Repeater – Bob. The protocol goes as follows:
- Repeater shares half of one Bell pair with Alice and half of another Bell pair with Bob
- Repeater performs a BSM on its two remaining local qubits
- Repeater sends the results to one of the nodes, let's choose Bob, who then applies the same Pauli corrections as he did in teleportation
- Alice's and Bob's qubits are now entangled, without ever directly communicating
Here is a basic version with no classical communication:
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/q2ns-qubit.h"
#include "ns3/core-module.h"
#include "ns3/simulator.h"
#include <iostream>
chRA->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
chRB->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
std::pair<int,int> ms;
Simulator::Schedule(NanoSeconds(1), [&](){
auto [qAremote, qR1] = R->CreateBellPair();
auto [qR2, qBremote] = R->CreateBellPair();
R->Send(qAremote, A->GetId());
R->Send(qBremote, B->GetId());
ms = R->MeasureBell(qR1, qR2);
std::cout<<"[R] BSM results: "<<ms.first<<","<<ms.second<<"\n";
});
B->SetRecvCallback([&](std::shared_ptr<Qubit> q){
if(ms.second)
B->Apply(gates::X(),{q});
if(ms.first)
B->Apply(gates::Z(),{q});
std::cout<<"[B] corrections applied\n";
});
Simulator::Run();
Simulator::Destroy();
}
This faces the same problem as our basic teleportation protocol: there is no way Bob could actually know the results of the repeater's BSM instantaneously. Your goal in this exercise is to make this entanglement swapping protocol realistic by adding UDP packet communication and anything else necessary.
Solution
#include "ns3/q2ns-netcontroller.h"
#include "ns3/q2ns-qnode.h"
#include "ns3/q2ns-qubit.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/simulator.h"
#include <iostream>
namespace {
struct BobInfo {
std::shared_ptr<Qubit> qBremote;
bool qubitArrived = false;
bool bitsArrived = false;
};
void TryCorrections(Ptr<QNode> bob, BobInfo& bobInfo) {
if (!bobInfo.qubitArrived || !bobInfo.bitsArrived) {
return;
}
std::cout << "[B] Applying corrections: Z^" << bobInfo.m1 << " X^" << bobInfo.m2 << "|state>\n";
if (bobInfo.m2) {
bob->Apply(gates::X(), {bobInfo.qBremote});
}
if (bobInfo.m1) {
bob->Apply(gates::Z(), {bobInfo.qBremote});
}
std::cout << "[B] corrections applied\n";
}
}
std::cout << "[DEMO] Entanglement swapping (A-R-B) with classical communication starting\n";
ns3::RngSeedManager::SetSeed(1);
ns3::RngSeedManager::SetRun(1);
chRA->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
chRB->SetAttribute("Delay", TimeValue(NanoSeconds(10)));
InternetStackHelper internet;
internet.Install(R);
internet.Install(B);
PointToPointHelper p2p;
p2p.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
p2p.SetChannelAttribute("Delay", StringValue("1ms"));
NetDeviceContainer devices = p2p.Install(R, B);
Ipv4AddressHelper ip;
ip.SetBase("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = ip.Assign(devices);
const uint16_t port = 9000;
Ptr<Socket> bobSocket = Socket::CreateSocket(B, UdpSocketFactory::GetTypeId());
bobSocket->Bind(InetSocketAddress(Ipv4Address::GetAny(), port));
Ptr<Socket> repeaterSocket = Socket::CreateSocket(R, UdpSocketFactory::GetTypeId());
repeaterSocket->Connect(InetSocketAddress(interfaces.GetAddress(1), port));
BobInfo bobInfo;
B->SetRecvCallback([B, &bobInfo](std::shared_ptr<Qubit> q) {
std::cout << "[RECV][quantum][B]: yes\n";
bobInfo.qubitArrived = true;
bobInfo.qBremote = q;
});
bobSocket->SetRecvCallback([B, &bobInfo](Ptr<Socket> socket) {
while (Ptr<Packet> packet = socket->Recv()) {
bobInfo.bitsArrived = true;
uint8_t bytes[2] = {0, 0};
packet->CopyData(bytes, 2);
bobInfo.m1 = bytes[0] & 1;
bobInfo.m2 = bytes[1] & 1;
std::cout << "[RECV][classical][B] m1=" << bobInfo.m1 << ", m2=" << bobInfo.m2 << "\n";
}
});
Simulator::Schedule(NanoSeconds(1), [&]() {
auto [qAremote, qR1] = R->CreateBellPair();
auto [qR2, qBremote] = R->CreateBellPair();
bool okA = R->Send(qAremote, A->GetId());
bool okB = R->Send(qBremote, B->GetId());
std::cout << "[SEND][quantum] R->A: " << (okA ? "ok" : "failed") << "\n";
std::cout << "[SEND][quantum] R->B: " << (okB ? "ok" : "failed") << "\n";
auto [
m1,
m2] = R->MeasureBell(qR1, qR2);
std::cout <<
"[R] BSM results: " <<
m1 <<
", " <<
m2 <<
"\n";
uint8_t bytes[2] = {
static_cast<uint8_t
>(
m1),
static_cast<uint8_t
>(
m2)};
repeaterSocket->Send(Create<Packet>(bytes, 2));
std::cout <<
"[SEND][classical] R->B: m1=" <<
m1 <<
", m2=" <<
m2 <<
"\n";
});
Simulator::Stop(Seconds(10));
Simulator::Run();
std::cout << "[DONE] Entanglement swapping (A-R-B) with classical communication finished\n";
Simulator::Destroy();
}
Note that the nature of entanglement does not allow any physically realistic way for Bob to verify his state with just one pair and without communicating with Alice. However, sometimes it is important to verify for ourselves that protocols we are designing are working as intended. In these cases, we can print the state:
std::cout << bob->GetState(bobInfo.qBremote) << "\n";
Or if you are generally trying to compare the overlap of two states, we have analysis::Fidelity and could do something like:
auto [q0, q1] = bob->CreateBellPair();
std::cout << "Fidelity with Bell pair: " << analysis::Fidelity(bobInfo.qBremote, bob->GetState(q0)) << "\n";
Related Publications
[1] Quantum Internet Architecture: Unlocking Quantum-Native Routing via Quantum Addressing (invited paper). Marcello Caleffi and Angela Sara Cacciapuoti – in IEEE Transactions on Communications, vol. 74, pp. 3577–3599, 2026.
[2] An Extensible Quantum Network Simulator Built on ns-3: Q2NS Design and Evaluation. Adam Pearson, Francesco Mazza, Marcello Caleffi, Angela Sara Cacciapuoti – Computer Networks (Elsevier) 2026.
[3] Q2NS: A Modular Framework for Quantum Network Simulation in ns-3 (invited paper). Adam Pearson, Francesco Mazza, Marcello Caleffi, Angela Sara Cacciapuoti – Proc. QCNC 2026.
[4] Q2NS Demo: a Quantum Network Simulator based on ns-3. Francesco Mazza, Adam Pearson, Marcello Caleffi, Angela Sara Cacciapuoti – 2026.
Acknowledgement
This work has been funded by the European Union under Horizon Europe ERC-CoG grant QNattyNet, n. 101169850. Views and opinions expressed are those of the author(s) only and do not necessarily reflect those of the European Union or the European Research Council Executive Agency. Neither the European Union nor the granting authority can be held responsible for them.