RAN MAC Scheduler — 5G/LTE L1/L2 downlink scheduler in C++
Published:
A compact, production-quality C++17 project that demonstrates three skills at once: modern C++, Radio Access Network (RAN) L1/L2 protocol knowledge, and a complete CI/CD pipeline.
🔗 Source code: github.com/nerostamas/Radio-Access-Network-RAN-MAC-Scheduler
Why this shows depth
This project is small on purpose — small enough to read in one sitting, yet correct enough to reproduce the exact throughput-vs-fairness trade-off described in the 3GPP literature. That combination is what demonstrates real skill rather than boilerplate.
On the C++ side, it is written the way shipped systems are: a layered architecture with a clean phy / mac / metrics boundary, an abstract Scheduler interface with polymorphic strategies behind a std::unique_ptr factory, RAII throughout with no raw new/delete, const-correct and [[nodiscard]] APIs, strong typing via purpose-built aliases (Cqi, Tti, Bits), and a deterministic, seedable RNG so every run is reproducible. It compiles cleanly under both GCC and Clang with -Wall -Wextra -Wpedantic and passes AddressSanitizer, UndefinedBehaviorSanitizer, clang-tidy, and cppcheck.
On the RAN side, the code is not a toy loop — it mirrors how a real gNB/eNB operates every millisecond: UEs report channel quality (CQI), the PHY performs link adaptation (CQI → MCS → transport block size) subject to the resource-block budget, the MAC scheduler grants the shared grid per TTI, and HARQ handles retransmission with soft-combining gain. The fact that Max C/I maximizes cell throughput while starving cell-edge UEs, and Proportional Fair recovers fairness at a modest throughput cost, falls directly out of the implementation — exactly as the theory predicts. Reproducing that behavior from first principles is the clearest evidence that the L1/L2 concepts are genuinely understood, not just named.
What it does
It simulates the air-interface boundary where throughput and fairness are decided: a base station (gNB/eNB) allocates a shared time–frequency resource-block grid among competing UEs, every Transmission Time Interval (TTI), with HARQ retransmissions for reliability.
- L1 (PHY): channel quality (CQI) → modulation & coding scheme (MCS) → transport block size (TBS) per resource block, per TTI.
- L2 (MAC): per-UE buffers, stop-and-wait HARQ, and three classic downlink schedulers.
RAN concepts modeled
| Concept | Layer |
|---|---|
| Resource-Block grid & TTI loop | L1 / L2 |
| CQI → MCS → Transport Block Size | L1 |
| Per-UE logical-channel buffers | L2 |
| Downlink scheduling (RR / Max-C·I / Proportional Fair) | L2 (MAC) |
| HARQ stop-and-wait retransmission | L2 (MAC) |
| Throughput & Jain’s fairness KPIs | — |
Schedulers and the trade-off they reveal
Running the simulator (10 UEs, 50 RBs, 1000 TTIs, seed 42) reproduces the textbook throughput-vs-fairness behavior, which is what proves the model is faithful:
| Scheduler | Cell throughput | Jain’s fairness |
|---|---|---|
| Round Robin | 17.4 Mbps | 0.83 (even, channel-agnostic) |
| Max C/I | 30.6 Mbps | 0.34 (cell-edge UEs starve) |
| Proportional Fair | 23.0 Mbps | 0.85 (best balance) |
Engineering practices
- C++17 with a clean
phy/mac/metricsseparation,-Wall -Wextra -Wpedantic, and zero build warnings. - CMake build with a reusable core library, a CLI (
ran_sim), and an opt-in sanitizer / coverage build. - 24 GoogleTest unit tests driven by CTest, covering link adaptation, HARQ state transitions, KPI math, and end-to-end scheduler behavior (including determinism).
- GitHub Actions CI/CD on every push and PR:
clang-formatstyle gate- build matrix — GCC + Clang, Debug + Release
- GoogleTest via CTest
- AddressSanitizer + UndefinedBehaviorSanitizer test run
- static analysis — clang-tidy and cppcheck
- gcov/lcov coverage summary
Try it
git clone https://github.com/nerostamas/Radio-Access-Network-RAN-MAC-Scheduler
cd Radio-Access-Network-RAN-MAC-Scheduler
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure
./build/ran_sim --ues 10 --tti 1000 --rbs 50 --scheduler pf --seed 42
