Comparing Wi-Fi Throughput with ns-3

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:

  1. The ns-3 C++ Script: The core simulation logic.

  2. The Automation Script: A shell script to run our experiments.

  3. 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::CommandLine to easily change parameters without recompiling the code.

  • Scalable Setup: The number of stations (nWifi) is a variable, and we use a GridPositionAllocator to place them neatly in the simulation world, no matter how many there are.

  • Multi-Standard Support: An if/else if block allows us to switch between WIFI_STANDARD_80211n, WIFI_STANDARD_80211ac, WIFI_STANDARD_80211ax, and WIFI_STANDARD_80211be.

  • Robust Application Setup: We manually create the PacketSink application 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 an OnOffApplication on 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 our totalBytesReceived counter.

  • 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 ns3 command, 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:

  1. Load Data: We use the powerful pandas library to load our results.csv file into a DataFrame.

  2. Create the Plot: We use the seaborn and matplotlib libraries to create a grouped bar chart.

  3. 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()
Updated on