In this post, we'll walk through a complete, end-to-end project using the ns-3 network simulator. We will build a configurable Wi-Fi simulation, write a script to run scenarios automatically, and finally, visualize the results.
Let's get started! 🚀
The Goal: Testing Wi-Fi Under Pressure
Our objective is to measure and compare the total network throughput for different Wi-Fi standards (802.11n, ac, ax, and be) as we increase the number of competing devices (stations). This will show us how well each standard handles network congestion.
Our workflow is broken into three parts:
-
The ns-3 C++ Script: The core simulation logic.
-
The Automation Script: A shell script to run our experiments.
-
The Visualization Script: A Python notebook to plot our data.
Part 1: The ns-3 Simulation Script 🔬
This C++ script is the engine of our experiment. It's allowing us to change key parameters like the number of stations and the Wi-Fi standard directly from the command line.
Key Features of the Script:
-
Command-Line Arguments: We use
ns3::CommandLineto easily change parameters without recompiling the code. -
Scalable Setup: The number of stations (
nWifi) is a variable, and we use aGridPositionAllocatorto place them neatly in the simulation world, no matter how many there are. -
Multi-Standard Support: An
if/else ifblock allows us to switch betweenWIFI_STANDARD_80211n,WIFI_STANDARD_80211ac,WIFI_STANDARD_80211ax, andWIFI_STANDARD_80211be. -
Robust Application Setup: We manually create the
PacketSinkapplication on the Access Point (AP). This gives us direct control and avoids some common helper class issues. Then, we loop through all our stations and install anOnOffApplicationon each one to generate traffic. -
Data Collection: A simple but powerful trace callback (
RxTrace) is connected to the AP's sink application. Every time a packet is received, this function is called, and we add its size to ourtotalBytesReceivedcounter. -
CSV Output: After the simulation finishes, it calculates the final average throughput and appends a single, clean line of data to a CSV file (e.g.,
ax,10,120.447).
wifi-perf-test.cc (Place in scratch/fathan/)
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/internet-module.h"
#include "ns3/mobility-module.h"
#include "ns3/wifi-module.h"
#include "ns3/applications-module.h"
#include "ns3/command-line-module.h"
#include <fstream>
#include <string>
#include <vector>
#include <numeric>
#include <iostream>
using namespace ns3;
static uint64_t totalBytesReceived = 0;
static void RxTrace(Ptr<const Packet> packet, const Address &from)
{
totalBytesReceived += packet->GetSize();
}
int main (int argc, char *argv[])
{
uint32_t nWifi = 1;
std::string wifiStandard = "ax";
double simulationTime = 10.0;
std::string outputFilename = "throughput_results.csv";
CommandLine cmd;
cmd.AddValue("nWifi", "Number of STA devices", nWifi);
cmd.AddValue("standard", "WiFi standard to use (n, ac, ax, be)", wifiStandard);
cmd.AddValue("outputFile", "Name of the output file", outputFilename);
cmd.Parse(argc, argv);
NodeContainer wifiApNode;
wifiApNode.Create(1);
NodeContainer wifiStaNodes;
wifiStaNodes.Create(nWifi);
YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
YansWifiPhyHelper phy;
phy.SetChannel(channel.Create());
phy.SetErrorRateModel("ns3::NistErrorRateModel");
WifiMacHelper mac;
WifiHelper wifi;
if (wifiStandard == "n") {
wifi.SetStandard(WIFI_STANDARD_80211n);
} else if (wifiStandard == "ac") {
wifi.SetStandard(WIFI_STANDARD_80211ac);
} else if (wifiStandard == "ax") {
wifi.SetStandard(WIFI_STANDARD_80211ax);
} else if (wifiStandard == "be") {
wifi.SetStandard(WIFI_STANDARD_80211be);
} else {
NS_FATAL_ERROR("Invalid WiFi standard provided.");
}
wifi.SetRemoteStationManager("ns3::IdealWifiManager");
mac.SetType("ns3::StaWifiMac");
NetDeviceContainer staDevices = wifi.Install(phy, mac, wifiStaNodes);
mac.SetType("ns3::ApWifiMac");
NetDeviceContainer apDevice = wifi.Install(phy, mac, wifiApNode);
MobilityHelper mobility;
mobility.SetPositionAllocator("ns3::GridPositionAllocator",
"MinX", DoubleValue(5.0),
"MinY", DoubleValue(5.0),
"DeltaX", DoubleValue(5.0),
"DeltaY", DoubleValue(5.0),
"GridWidth", UintegerValue(10));
mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
mobility.Install(wifiStaNodes);
Ptr<ListPositionAllocator> apPositionAlloc = CreateObject<ListPositionAllocator>();
apPositionAlloc->Add(Vector(0.0, 0.0, 0.0));
mobility.SetPositionAllocator(apPositionAlloc);
mobility.Install(wifiApNode);
InternetStackHelper stack;
stack.Install(wifiApNode);
stack.Install(wifiStaNodes);
Ipv4AddressHelper address;
address.SetBase("10.1.1.0", "255.255.255.0");
address.Assign(staDevices);
Ipv4InterfaceContainer apInterface = address.Assign(apDevice);
uint16_t port = 9;
ObjectFactory sinkFactory;
sinkFactory.SetTypeId("ns3::PacketSink");
Ptr<Application> sinkApp = sinkFactory.Create<Application>();
Address sinkAddress = InetSocketAddress(Ipv4Address::GetAny(), port);
sinkApp->SetAttribute("Local", AddressValue(sinkAddress));
wifiApNode.Get(0)->AddApplication(sinkApp);
ApplicationContainer sinkApps(sinkApp);
sinkApps.Start(Seconds(0.0));
sinkApps.Stop(Seconds(simulationTime));
sinkApp->TraceConnectWithoutContext("Rx", MakeCallback(&RxTrace));
OnOffHelper onoff("ns3::UdpSocketFactory", InetSocketAddress(apInterface.GetAddress(0), port));
onoff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]"));
onoff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]"));
onoff.SetAttribute("DataRate", DataRateValue(DataRate("1Gbps")));
onoff.SetAttribute("PacketSize", UintegerValue(1400));
ApplicationContainer sourceApps = onoff.Install(wifiStaNodes);
sourceApps.Start(Seconds(1.0));
sourceApps.Stop(Seconds(simulationTime));
Simulator::Stop(Seconds(simulationTime));
Simulator::Run();
double avgThroughput = 0;
double duration = simulationTime - 1.0;
if (duration > 0) {
avgThroughput = (totalBytesReceived * 8.0) / (duration * 1000000);
}
std::ofstream outFile(outputFilename, std::ios_base::app);
outFile << wifiStandard << "," << nWifi << "," << avgThroughput << std::endl;
outFile.close();
Simulator::Destroy();
return 0;
}
Part 2: The Shell Script 💻
Running a simulation once is easy. Running it 16 times (4 standards x 4 station counts) is tedious and error-prone. To solve this, we use a simple shell script to act as our experiment manager.
How it Works:
-
Timestamped Files: It generates a unique filename using the current date and time (e.g.,
20250722-1730_throughput_results.csv). This is great practice for keeping track of experimental runs. -
Nested Loops: It loops through our defined arrays of Wi-Fi standards and station counts.
-
Execution: Inside the loop, it constructs and runs the
ns3command, passing the correct standard and station count as command-line arguments for each run. -
CSV Header: Before starting the loops, it creates the output file and writes the header row (
standard,numStations,Throughput).
This script turns a full day of manual work into a single command that you can run while you grab a coffee.
run-sim.sh (Place in scratch/fathan/)
#!/bin/bash
# Path to the ns-3 executable, now two directories up
NS3_PATH="../../ns3"
# Path to the C++ script inside the scratch folder
SCRIPT_PATH="fathan/wifi-perf-test"
# 1. Create a timestamp variable in YYYYMMDD-HHMM format
TIMESTAMP=$(date +'%Y%m%d-%H%M')
# 2. Use the timestamp variable to create a unique output filename
OUTPUT_FILE="scratch/fathan/${TIMESTAMP}_throughput_results.csv"
# Create the CSV file header in the correct final location
echo "standard,numStations,Throughput" > $OUTPUT_FILE
# Arrays of parameters to loop through
STANDARDS=("n" "ac" "ax" "be")
STATIONS=(1 5 10 20)
# Loop through each combination
for std in "${STANDARDS[@]}"; do
for sta in "${STATIONS[@]}"; do
echo "Running: Standard=$std, Stations=$sta"
# The C++ program will now also write to the correct file
$NS3_PATH run "scratch/$SCRIPT_PATH --standard=$std --nWifi=$sta --outputFile=$OUTPUT_FILE"
done
done
echo "All simulations complete. Results are in $OUTPUT_FILE"
Part 3: Visualizing the Results 📊
We use a Python script to transform our data into insight.
The Process:
-
Load Data: We use the powerful
pandaslibrary to load ourresults.csvfile into a DataFrame. -
Create the Plot: We use the
seabornandmatplotliblibraries to create a grouped bar chart. -
Customize and Save: We add titles, labels, and a legend to make the chart clear.
The final chart will look something like this, clearly showing how throughput changes for each standard as the network gets more crowded.

visualize_results.ipynb (update filename)
# Cell 1: Imports
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Cell 2: Load Data
# --- IMPORTANT: Replace with the actual name of your timestamped CSV file ---
filename = "throughput_results.csv"
try:
df = pd.read_csv(filename)
print("Successfully loaded the data:")
print(df.head())
except FileNotFoundError:
print(f"Error: The file '{filename}' was not found.")
df = None
# Cell 3: Plotting
if df is not None:
sns.set_theme(style="whitegrid")
plt.figure(figsize=(14, 8))
sns.barplot(
data=df,
x="numStations",
y="Throughput",
hue="standard",
palette="viridis",
hue_order=['n', 'ac', 'ax', 'be']
)
plt.title('Network Throughput vs. Number of Stations', fontsize=18, fontweight='bold')
plt.xlabel('Number of Competing Stations', fontsize=14)
plt.ylabel('Average Throughput (Mbps)', fontsize=14)
plt.legend(title='Wi-Fi Standard')
plt.tight_layout()
plt.savefig("throughput_comparison_chart.png", dpi=300)
plt.show()