diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/AUTHORS ns-3.47/AUTHORS
--- ns-3.46/AUTHORS	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/AUTHORS	2026-02-16 16:08:49.470229400 -0800
@@ -1,4 +1,5 @@
-Rami Abdallah <abdallah.rami@gmail.com>
+Jayesh Akot (akotjayesh@gmail.com)
+Rami Abdallah (abdallah.rami@gmail.com)
 Eduardo Abinader (eduardo.abinader@gmail.com)
 John Abraham (john.abraham.in@gmail.com)
 Danilo Abrignani (dabrignani@gmail.com)
@@ -14,10 +15,12 @@
 Robert Ammon (ammo6818@vandals.uidaho.edu)
 André Apitzsch (andre.apitzsch@etit.tu-chemnitz.de)
 Kirill Andreev (andreev@iitp.ru)
+Animesh (animeshsri.official@gmail.com)
 Dean Armstrong (deanarm@gmail.com)
 Gabriel Arrobo (gab.arrobo@gmail.com)
 Zakaria Helal Arzoo (arzoozakaria@gmail.com)
 Stefano Avallone (stefano.avallone@unina.it)
+Tommy Azzino (tommy.azzino@gmail.com)
 Vignesh Babu (ns3-dev@esk.fraunhofer.de)
 Ghada Badawy (gbadawy@gmail.com)
 Shikha Bakshi (shikhabakshi912@gmail.com)
@@ -88,6 +91,7 @@
 Matteo Drago (matteo.drago1994@gmail.com)
 Gilaras Drakeson (gilaras@gmail.com)
 Jared Dulmage (jared.dulmage@wpli.net)
+Alexey Elfimov (alexey.kocs@gmail.com)
 Mathias Ettinger (mettinger@toulouse.viveris.com)
 Christian Facchini (c.facchini@gmail.com)
 Denis Fakhriev (fakhriev@iitp.ru)
@@ -136,6 +140,7 @@
 Philip Hönnecke (p.hoennecke@tu-braunschweig.de)
 Chris Hood (chood8@gatech.edu)
 Blake Hurd (naimorai@gmail.com)
+Immortus1 (yashas80dj@gmail.com)
 Pasquale Imputato (p.imputato@gmail.com)
 Muhammad Inamullah (inamullah.m@gmail.com)
 Muhammad Iqbal CR (muh.iqbal.cr@gmail.com)
@@ -230,6 +235,7 @@
 Michele Muccio (michelemuccio@virgilio.it)
 Esteban Municio (esteban.municio@urjc.es)
 Sidharth Nabar (snabar@uw.edu)
+Nagabhushanam2005 (thechosentwins2005@gmail.com)
 Sharan Naribole (sharan.naribole@gmail.com)
 Hemanth Narra (hemanth@ittc.ku.edu)
 Carlos Natalino (carlos.natalino@chalmers.se)
@@ -272,6 +278,8 @@
 Ovidiu Poncea (ovidiu.poncea@cs.pub.ro)
 Anna Poon (poon2@llnl.gov)
 Gauri Prasad
+Ishan Prasad (ishan04722042@gmail.com)
+S B L Prateek (sblprateek@gmail.com)
 Vikas Pushkar (vikaskupushkar@gmail.com)
 Martin Quinson (mquinson@debian.org)
 Philipp Raich (philipp.raich@tuwien.ac.at)
@@ -279,6 +287,7 @@
 Manoj Kumar Rana (manoj24.rana@gmail.com)
 Andrea Ranieri (andreran@uno.it)
 Ashish Reddy (ashishajr@gmail.com)
+Dhiraj Lokesh (dhirajlokesh@gmail.com)
 Bruno Ranieri (Yrrsinn@googlemail.com)
 Deepti Rajagopal (deeptir96@gmail.com)
 Satyarth Ratnani (satyarthratnani@gmail.com)
@@ -354,6 +363,7 @@
 Mauro Tortonesi (mauro.tortonesi@unife.it)
 Quincy Tse (quincy.tse@gmail.com)
 Frederic Urbani (frederic.urbani@inria.fr)
+Urval (kheniurval777@gmail.com)
 Andras Varga (andras@omnetpp.org)
 Shrinidhi Anil Varna (shrinidhi99.varna@gmail.com)
 Silviu-Leonard Vatamanelu (silviu48vatamanelu@gmail.com)
@@ -369,8 +379,9 @@
 He Wu (mdzz@u.washington.edu)
 Xiuchao Wu (xiuchao@amazon.com)
 Aron Wussler (aron@wussler.it)
-wzfelix <wzfhrb.cn@gmail.com>
+wzfelix (wzfhrb.cn@gmail.com)
 Heran Yang (heran55@126.com)
+Yaswanthyash1 (yaswanthnamburi1010@gmail.com)
 Yoshihiko Yazawa (yoshiyaz@gmail.com)
 Menglei Zhang (menglei@nyu.edu)
 Theodore Zhang (harryzwh@gmail.com)
@@ -385,3 +396,5 @@
 Parth Pratim (GCI 2018)
 ra1nst0rm3d (GCI 2019)
 SaiMyGuy (GCI 2019)
+TheBear (25639689-thebear132@users.noreply.gitlab.com)
+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/bindings/python/ns__init__.py ns-3.47/bindings/python/ns__init__.py
--- ns-3.46/bindings/python/ns__init__.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/bindings/python/ns__init__.py	2026-02-16 16:08:49.476229400 -0800
@@ -543,8 +543,7 @@
     setattr(cppyy.gbl.ns3, "cppyy", cppyy)
 
     # Set up a few tricks
-    cppyy.cppdef(
-        """
+    cppyy.cppdef("""
         using namespace ns3;
         bool Time_ge(Time& a, Time& b){ return a >= b;}
         bool Time_eq(Time& a, Time& b){ return a == b;}
@@ -552,8 +551,7 @@
         bool Time_le(Time& a, Time& b){ return a <= b;}
         bool Time_gt(Time& a, Time& b){ return a > b;}
         bool Time_lt(Time& a, Time& b){ return a < b;}
-    """
-    )
+    """)
     cppyy.gbl.ns3.Time.__ge__ = cppyy.gbl.Time_ge
     cppyy.gbl.ns3.Time.__eq__ = cppyy.gbl.Time_eq
     cppyy.gbl.ns3.Time.__ne__ = cppyy.gbl.Time_ne
@@ -574,8 +572,7 @@
 
     cppyy.gbl.ns3.Node.__del__ = Node_del
 
-    cppyy.cppdef(
-        """
+    cppyy.cppdef("""
         using namespace ns3;
         std::tuple<bool, TypeId> LookupByNameFailSafe(std::string name)
         {
@@ -583,8 +580,7 @@
             bool ok = TypeId::LookupByNameFailSafe(name, &id);
             return std::make_tuple(ok, id);
         }
-    """
-    )
+    """)
     setattr(cppyy.gbl.ns3, "LookupByNameFailSafe", cppyy.gbl.LookupByNameFailSafe)
 
     return cppyy.gbl.ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/build-support/3rd-party/FindEigen3.cmake ns-3.47/build-support/3rd-party/FindEigen3.cmake
--- ns-3.46/build-support/3rd-party/FindEigen3.cmake	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/build-support/3rd-party/FindEigen3.cmake	2026-02-16 16:08:49.476229400 -0800
@@ -58,9 +58,12 @@
   endif()
 
   if(NOT EIGEN3_VERSION_OK)
-
-    message(STATUS "Eigen3 version ${EIGEN3_VERSION} found in ${EIGEN3_INCLUDE_DIR}, "
-                   "but at least version ${Eigen3_FIND_VERSION} is required")
+    if(${EIGEN3_VERSION} STREQUAL "..")
+      message(WARNING "The Eigen you have installed in ${EIGEN3_INCLUDE_DIR} is not Eigen3. Make sure you did not install Eigen5 by mistake.")
+    else()
+      message(STATUS "Eigen3 version ${EIGEN3_VERSION} found in ${EIGEN3_INCLUDE_DIR}, "
+                    "but at least version ${Eigen3_FIND_VERSION} is required")
+    endif()
   endif()
 endmacro()
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/build-support/custom-modules/ns3-compiler-workarounds.cmake ns-3.47/build-support/custom-modules/ns3-compiler-workarounds.cmake
--- ns-3.46/build-support/custom-modules/ns3-compiler-workarounds.cmake	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/build-support/custom-modules/ns3-compiler-workarounds.cmake	2026-02-16 16:08:49.477229400 -0800
@@ -94,7 +94,7 @@
     "
     STACKTRACE_LIBRARY_IS_LINKED
   )
-
+  set(CMAKE_REQUIRED_LIBRARIES)
   if(STACKTRACE_LIBRARY_IS_LINKED)
     add_definitions(-DSTACKTRACE_LIBRARY_IS_LINKED=1)
   else()
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/build-support/custom-modules/ns3-coverage.cmake ns-3.47/build-support/custom-modules/ns3-coverage.cmake
--- ns-3.46/build-support/custom-modules/ns3-coverage.cmake	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/build-support/custom-modules/ns3-coverage.cmake	2026-02-16 16:08:49.477229400 -0800
@@ -33,7 +33,7 @@
   add_custom_target(
     coverage_gcc
     COMMAND lcov -o ns3.info -c --directory ${CMAKE_BINARY_DIR} ${zero_counters}
-            --keep-going --ignore-errors inconsistent
+            --keep-going --ignore-errors inconsistent,negative
     WORKING_DIRECTORY ${CMAKE_OUTPUT_DIRECTORY}/coverage
     DEPENDS run_test_py
   )
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/build-support/custom-modules/ns3-vcpkg-hunter.cmake ns-3.47/build-support/custom-modules/ns3-vcpkg-hunter.cmake
--- ns-3.46/build-support/custom-modules/ns3-vcpkg-hunter.cmake	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/build-support/custom-modules/ns3-vcpkg-hunter.cmake	2026-02-16 16:08:49.479229500 -0800
@@ -186,6 +186,6 @@
 
 if(${NS3_VCPKG})
   setup_vcpkg()
-  include_directories(${VCPKG_DIR}/installed/${VCPKG_TRIPLET}/include)
+  include_directories(SYSTEM ${VCPKG_DIR}/installed/${VCPKG_TRIPLET}/include)
   link_directories(${VCPKG_DIR}/installed/${VCPKG_TRIPLET}/lib)
 endif()
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/build-support/macros-and-definitions.cmake ns-3.47/build-support/macros-and-definitions.cmake
--- ns-3.46/build-support/macros-and-definitions.cmake	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/build-support/macros-and-definitions.cmake	2026-02-16 16:08:49.479229500 -0800
@@ -256,23 +256,33 @@
     endif()
   endif()
 
-  if(${NS3_CLANG_TIDY})
+  if(${NS3_CLANG_TIDY} OR ${NS3_CLANG_TIDY_FIX})
     find_program(
-      CLANG_TIDY
-      NAMES clang-tidy
-            clang-tidy-15
-            clang-tidy-16
-            clang-tidy-17
-            clang-tidy-18
-            clang-tidy-19
-            clang-tidy-20
+      CLANG_TIDY NAMES clang-tidy-21 clang-tidy-20 clang-tidy-19 clang-tidy-18
+                       clang-tidy-17 clang-tidy
     )
     if("${CLANG_TIDY}" STREQUAL "CLANG_TIDY-NOTFOUND")
       message(FATAL_ERROR "Clang-tidy was not found")
     else()
-      set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY}
-                               -config-file=${CMAKE_SOURCE_DIR}/.clang-tidy
-      ) # --fix)
+      if(${NS3_CLANG_TIDY})
+        set(CMAKE_CXX_CLANG_TIDY
+            ${CLANG_TIDY} --config-file=${CMAKE_SOURCE_DIR}/.clang-tidy
+            --format-style=file -p ${CMAKE_BINARY_DIR}
+        )
+      else()
+        set(CMAKE_CXX_CLANG_TIDY
+            ${CLANG_TIDY}
+            --config-file=${CMAKE_SOURCE_DIR}/.clang-tidy
+            --fix
+            --fix-errors
+            --fix-notes
+            --format-style=file
+            --warnings-as-errors=*
+            --quiet
+            -p
+            ${CMAKE_BINARY_DIR}
+        )
+      endif()
     endif()
   else()
     unset(CMAKE_CXX_CLANG_TIDY)
@@ -394,9 +404,25 @@
   endif()
 
   if(${NS3_SANITIZE})
-    set(CMAKE_CXX_FLAGS
-        "${CMAKE_CXX_FLAGS} -fsanitize=address,leak,undefined -fno-sanitize-recover=all"
-    )
+    set(SANITIZERS "address" "undefined" "leak")
+    set(AVAILABLE_SANITIZERS)
+    foreach(flag ${SANITIZERS})
+      set(CMAKE_REQUIRED_FLAGS "-fsanitize=${flag}")
+      check_cxx_source_compiles("int main() { return 0; }" HAS_FLAG_${flag})
+      if(HAS_FLAG_${flag})
+        list(APPEND AVAILABLE_SANITIZERS ${flag})
+      endif()
+    endforeach()
+    unset(CMAKE_REQUIRED_FLAGS)
+    string(REPLACE ";" "," SANITIZERS_FLAGS "${AVAILABLE_SANITIZERS}")
+    if(AVAILABLE_SANITIZERS)
+      message(STATUS "Sanitizers: found ${SANITIZERS_FLAGS}")
+      set(CMAKE_CXX_FLAGS
+          "${CMAKE_CXX_FLAGS} -fsanitize=${SANITIZERS_FLAGS} -fno-sanitize-recover=all"
+      )
+    else()
+      message(WARNING "Sanitizers: no sanitizers available")
+    endif()
   endif()
 
   if(${NS3_SANITIZE_MEMORY})
@@ -542,7 +568,7 @@
       set(ENABLE_SQLITE True)
       add_definitions(-DHAVE_SQLITE3)
       if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-        include_directories(${SQLite3_INCLUDE_DIRS})
+        include_directories(SYSTEM ${SQLite3_INCLUDE_DIRS})
       endif()
     endif()
   endif()
@@ -558,7 +584,7 @@
       add_definitions(-DHAVE_EIGEN3)
       add_definitions(-DEIGEN_MPL2_ONLY)
       if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-        include_directories(${EIGEN3_INCLUDE_DIR})
+        include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR})
       endif()
     else()
       set(ENABLE_EIGEN_REASON "Eigen was not found")
@@ -588,7 +614,9 @@
           )
         else()
           if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-            include_directories(${GTK3_INCLUDE_DIRS} ${HarfBuzz_INCLUDE_DIRS})
+            include_directories(
+              SYSTEM ${GTK3_INCLUDE_DIRS} ${HarfBuzz_INCLUDE_DIRS}
+            )
           endif()
         endif()
       endif()
@@ -617,7 +645,7 @@
     else()
       add_definitions(-DHAVE_LIBXML2)
       if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-        include_directories(${LIBXML2_INCLUDE_DIR})
+        include_directories(SYSTEM ${LIBXML2_INCLUDE_DIR})
       endif()
     endif()
   endif()
@@ -625,7 +653,7 @@
   set(THREADS_PREFER_PTHREAD_FLAG)
   find_package(Threads QUIET)
   if(NOT ${Threads_FOUND})
-    message(FATAL_ERROR Threads are required by ns-3)
+    message(FATAL_ERROR "Threads are required by ns-3")
   endif()
 
   set(Python3_LIBRARIES)
@@ -662,7 +690,7 @@
         endif()
       endif()
       if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-        include_directories(${Python3_INCLUDE_DIRS})
+        include_directories(SYSTEM ${Python3_INCLUDE_DIRS})
       endif()
     else()
       message(${HIGHLIGHTED_STATUS}
@@ -849,7 +877,7 @@
   find_package(Boost QUIET)
   if(${Boost_FOUND})
     if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-      include_directories(${Boost_INCLUDE_DIRS})
+      include_directories(SYSTEM ${Boost_INCLUDE_DIRS})
     endif()
     set(CMAKE_REQUIRED_INCLUDES ${Boost_INCLUDE_DIRS})
   else()
@@ -865,7 +893,7 @@
       message(STATUS "GSL was found.")
       add_definitions(-DHAVE_GSL)
       if(NOT ${NS3_FORCE_LOCAL_DEPENDENCIES})
-        include_directories(${GSL_INCLUDE_DIRS})
+        include_directories(SYSTEM ${GSL_INCLUDE_DIRS})
       endif()
     endif()
   endif()
@@ -942,8 +970,10 @@
       )
       add_custom_target(
         run-introspected-command-line
-        COMMAND ${CMAKE_COMMAND} -E env NS_COMMANDLINE_INTROSPECTION=..
-                ${Python3_EXECUTABLE} ./test.py --no-build --constrain=example
+        COMMAND
+          ${CMAKE_COMMAND} -E env NS_COMMANDLINE_INTROSPECTION=..
+          ${Python3_EXECUTABLE} ./test.py --no-build --retain
+          --constrain=example
         WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
         DEPENDS all-test-targets # all-test-targets only exists if ENABLE_TESTS
                                  # is set to ON
@@ -951,11 +981,15 @@
 
       file(
         WRITE ${CMAKE_BINARY_DIR}/introspected-command-line-preamble.h
-        "/* This file is automatically generated by
-  CommandLine::PrintDoxygenUsage() from the CommandLine configuration
-  in various example programs.  Do not edit this file!  Edit the
-  CommandLine configuration in those files instead.
-  */\n"
+        "/**
+ * @file
+ * @ingroup utils
+ * Doxygen docs generated from the ns3::CommandLine options for each example.
+ * @note This file is automatically generated by
+ * ns3::CommandLine::PrintDoxygenUsage() from the ns3::CommandLine configuration
+ * in various example programs.  Do not edit this file!  Edit the
+ * ns3::CommandLine configuration in those files instead.
+ */\n"
       )
       add_custom_target(
         assemble-introspected-command-line
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/build-support/test-files/test-package-managers/CMakeLists.txt ns-3.47/build-support/test-files/test-package-managers/CMakeLists.txt
--- ns-3.46/build-support/test-files/test-package-managers/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/build-support/test-files/test-package-managers/CMakeLists.txt	2026-02-16 16:08:49.479229500 -0800
@@ -15,7 +15,7 @@
   set(armadillo
       armadillo
   )
-  include_directories(${CMAKE_BINARY_DIR}/_deps/armadillo-src/include)
+  include_directories(SYSTEM ${CMAKE_BINARY_DIR}/_deps/armadillo-src/include)
 elseif(
   ${TEST_PACKAGE_MANAGER}
   STREQUAL
@@ -24,7 +24,7 @@
   set(armadillo
       ${ARMADILLO_LIBRARIES}
   )
-  include_directories(${ARMADILLO_INCLUDE_DIRS})
+  include_directories(SYSTEM ${ARMADILLO_INCLUDE_DIRS})
 else()
   message(FATAL_ERROR "zoincs")
   message(
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/CHANGES.md ns-3.47/CHANGES.md
--- ns-3.46/CHANGES.md	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/CHANGES.md	2026-02-16 16:16:25.799885300 -0800
@@ -12,6 +12,66 @@
 
 This file is a best-effort approach to solving this issue; we will do our best but can guarantee that there will be things that fall through the cracks, unfortunately. If you, as a user, can suggest improvements to this file based on your experience, please contribute a patch or drop us a note on ns-developers mailing list.
 
+## Changes from ns-3.46.1 to ns-3.47
+
+### New API
+
+* (applications) New IEEE 802.11ax evaluation methodology traffic generator applications have been added: `TgaxVideoTraffic` (video streaming), `TgaxVoipTraffic` (VoIP), `TgaxVdiTraffic` (virtual desktop infrastructure), and `RtMobileGamingTraffic` (real-time mobile gaming).
+* (applications) New trace sources `SourceApplication::ConnectionSucceeded` and `SourceApplication::ConnectionFailed` have been added to report connection success/failure events.
+* (core) Added `SystemPath::FindSelf()` to retrieve the full path to the running executable.
+* (core) Added `NormalRandomVariable::GetStdDev()` and `NormalRandomVariable::SetStdDev()` convenience functions.
+* (internet) Added support for TCP FACK (Forward Acknowledgement).
+* (internet) Added IPv6 support to Global Routing.
+* (internet) Added `PrintRoute` functionality to `GlobalRouting` similar to `TraceRoute`.
+* (internet) Added attribute `TcpLedbat::AllowedIncrease` to control the ALLOWED_INCREASE parameter defined in RFC 6817.
+* (internet) A new static function `Ipv[4,6]Address::CheckCompatible()` has been added to safely check if a string can be parsed as an IPv4 or IPv6 address.
+* (lr-wpan) The `McpsDataIndication` parameters now include rssi value.
+* (network) Added `std::hash` specialization for `Ipv4Address` and `Ipv6Address`, enabling their use in unordered containers.
+* (propagation) Added 3GPP outdoor-to-indoor (O2I) penetration losses for sub-6 GHz frequencies according to TS 38.901 Table 7.4.3-3, including vehicular O2I penetration losses.
+* (visualizer) Add support to `LrWpanNetDevice` in the PyViz visualizer.
+
+### Changes to existing API
+
+* (applications) Common logic for starting/stopping, socket management, protocol TypeId, and TX/RX trace sources has been refactored into the `SourceApplication` and `SinkApplication` base classes.
+* (applications) The trace source `UdpServer::Rx` has a changed trace signature as a result of the move of this trace to the `SinkApplication` class.  Client code using this trace can either update to the new `Rx` trace signature or instead use the same trace sink function or method with the `SinkApplication::RxWithoutAddress` trace.
+* (core) The `nstime.h` header no longer includes `event-id.h`. It is possible that code might fail to compile due to this change. Adding an include to `event-id.h` fixes the issue.
+* (internet) Initializing a Ipv[4,6]Address from a string using the constructor or the `Set` function will result in a crash if the string can not be parsed as an IPv4 or IPv6 address.
+* (internet) The `Ipv[4,6]Address::IsInitialized()` function has been deprecated and returns always `true`. The default value of Ipv4Address created with the constructor that takes no arguments is 0.0.0.0 (previously, it was 102.102.102.102), and an Ipv4Address instance can be checked against that unspecified address value (or use std::optional to denote an address that has not been set yet).
+* (internet) The function `Ipv4InterfaceAddress::SetBroadcast` has been removed from the codebase because the broadcast address must be built from the IP address and mask.
+* (internet-apps) The DHCPv6 model has been refactored, and now supports all the DUID types.
+* (lr-wpan) Renamed `lr-wpan-pd-plme-sap-test` to `lr-wpan-phy-test` to improve clarity. Also this testsuite now includes an additional test called `LrWpanPhyCollisionTestCase`.
+* (lr-wpan) The PHY now includes the processing of the preamble as well as registering the RSSI at the moment of receiving the packet.
+* (network) Address type 0 was previously used as a wildcard. Type 0 is now disallowd for any practical use. In order to create an Address from raw bytes, you must now set the Address type beforehand. An example is in ArpHeader::Deserialize.
+* (network) The address class comparison is now based on std::strong_ordering operator<=> comparison operator.
+* (network) An empty (uninitialized) Address is now printed as "00-00:00".
+* (visualizer) The namespace `visualizer` is now used for PyViz related classes.
+* (visualizer) `PyViz` has been reformatted and modernized to current ns-3 standards.
+
+### Changes to build system
+
+* A new `NS3_CLANG_TIDY_FIX`/`--enable-clang-tidy-fix` option was added to use clang-tidy
+static analysis and automatically apply fixes during build. Not every clang-tidy warning/error
+has automatic fixes, and checks are performed only at build time. Thus requiring rebuilding
+things to re-run checks. As an alternative, one can use `./ns3 run clang-tidy`. And to apply
+fixes, use `./ns3 run "clang-tidy -fix"`.
+
+### Changed behavior
+
+* (wifi) `CcaEdThreshold` can be changed at run-time.
+* (internet) Updated `TcpLedbat::CongestionAvoidance()` to compute `max_cwnd` as `flightsize + AllowedIncrease * MSS` by adding `m_allowedIncrease * tcb->m_segmentSize`, in accordance with RFC 6817.
+* (internet) The default value of Ipv4Address created with the constructor that takes no arguments is 0.0.0.0 (previously, it was 102.102.102.102).
+* (spectrum) The `UpdatePeriod` attribute of `ThreeGppChannelModel` previously enabled periodic i.i.d. updates of the channel model, but is now used to control spatially-consistent updates of the channel.
+* (propagation) Fixed 3GPP low and high O2I loss distributions where `NormalRandomVariable` variance was incorrectly set with standard deviation values from TS 38.901 Table 7.4.3-2, resulting in lower than expected losses.
+* (wifi) Fixed spurious CCA-BUSY notification after channel width has changed.
+* (wifi) Fixed misuse of TX power and power level in `WifiRemoteStationManager`.
+
+## Changes from ns-3.46 to ns-3.46.1
+
+The ns-3.46.1 contains some small build system fixes discovered after the ns-3.46 release, and two
+new module documentation chapters (see [RELEASE_NOTES.md](RELEASE_NOTES.md)). There are no API
+changes, changes to how the build system works, or changed behavior of the models, compared with
+the ns-3.46 release.
+
 ## Changes from ns-3.45 to ns-3.46
 
 ### New API
@@ -22,6 +82,7 @@
 to DisableScanning` attribute to `StaWifiMac` to disable channel scanning; it can be useful to set this attribute to false when a static setup is performed.
 * (wifi) Added a new `EarlyTxopEndDetect` attribute to `EhtFrameExchangeManager` to control whether the Duration/ID value of the frame being transmitted or received by a device shall be used to early detect the end of an ongoing TXOP (held by another device).
 * (wifi) Added a new `DisableScanning` attribute to `StaWifiMac` to disable channel scanning; it can be useful to set this attribute to false when a static setup is performed.
+* (tcp) Added new attributes ``TcpSocketBase::UseAbe``, ``TcpCubic::BetaEcn``, ``TcpNewReno::BetaEcn``, ``TcpLinuxReno::BetaLoss``, ``TcpNewReno::BetaLoss``, ``TcpLinuxReno::BetaLoss`` and  to implement the Alternative Backoff with ECN (ABE) mechanism for NewReno and CUBIC. This mechanism is specified by RFC 8511.
 
 ### Changes to existing API
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/.clang-tidy ns-3.47/.clang-tidy
--- ns-3.46/.clang-tidy	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/.clang-tidy	2026-02-16 16:08:49.469229500 -0800
@@ -14,6 +14,7 @@
   bugprone-terminating-continue,
   bugprone-too-small-loop-variable,
   bugprone-virtual-near-miss,
+  misc-header-include-cycle,
   misc-redundant-expression,
   misc-static-assert,
   modernize-make-shared,
@@ -58,5 +59,4 @@
   - -Wno-unknown-warning-option
 
 FormatStyle: "file"
-HeaderFilterRegex: ".*(ns|NS).*/(contrib|examples|src|scratch|utils)/*/.*h"
 UseColor: true
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/CMakeLists.txt ns-3.47/CMakeLists.txt
--- ns-3.46/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/CMakeLists.txt	2026-02-16 16:08:49.472229500 -0800
@@ -30,6 +30,9 @@
 # maintenance and documentation
 option(NS3_CLANG_FORMAT "Enforce cody style with clang-format" OFF)
 option(NS3_CLANG_TIDY "Use clang-tidy static analysis" OFF)
+option(NS3_CLANG_TIDY_FIX
+       "Use clang-tidy static analysis and apply automatic fixes" OFF
+)
 option(NS3_CLANG_TIMETRACE
        "Collect compilation statistics to analyze the build process" OFF
 )
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/.codespellrc ns-3.47/.codespellrc
--- ns-3.46/.codespellrc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/.codespellrc	2026-02-16 16:08:49.469229500 -0800
@@ -0,0 +1,5 @@
+[codespell]
+skip = .git,.gitlab-ci-local,*.eps,*.pdf,*.svg,*.css_t,AUTHORS,*build*,*.mob,./cmake-*,./venv
+ignore-words = ./utils/codespell-ignored-words
+exclude-file = ./utils/codespell-ignored-lines
+regex = (?<![a-zà-öø-ÿ])[a-zà-öø-ÿ'`]+|[A-ZÀ-ÖØ-Þ][a-zà-öø-ÿ'`]*|[a-zà-öø-ÿ]+'[a-zà-öø-ÿ]*|[a-zà-öø-ÿ]+(?=[_-])|[a-zà-öø-ÿ]+(?=[A-ZÀ-ÖØ-Þ])|\d+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/contrib/.gitignore ns-3.47/contrib/.gitignore
--- ns-3.46/contrib/.gitignore	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/contrib/.gitignore	2026-02-16 16:08:49.480229400 -0800
@@ -0,0 +1,5 @@
+# Ignore everything by default, contrib sources are coming from other places
+*
+
+# Include specific files that should be tracked by Git
+!.gitignore
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/contributing/source/coding-style.rst ns-3.47/doc/contributing/source/coding-style.rst
--- ns-3.46/doc/contributing/source/coding-style.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/contributing/source/coding-style.rst	2026-02-16 16:08:49.481229300 -0800
@@ -268,8 +268,8 @@
 should install one of the supported versions of clang-format, which are listed in the
 following section.
 
-Minimum clang-tidy version
-==========================
+Supported versions of clang-tidy
+================================
 
 Since clang-tidy is a linter that analyzes code and outputs errors found during
 the analysis, developers can use different versions of clang-tidy on the workflow.
@@ -281,7 +281,7 @@
 code with the minimum version of clang-tidy. However, more recent versions can be used,
 which will produce better warnings.
 
-The minimum version is clang-tidy-15.
+The supported versions of clang-tidy are listed in the ``RELEASE_NOTES.md`` document.
 
 Integration with IDEs
 =====================
@@ -877,21 +877,52 @@
   constexpr uint8_t STATUS_PDU{0};
 
 When declaring variables that are easily deducible from context, prefer to declare them
-with ``auto`` instead of repeating the type name. Not only does this improve code readability,
-by making lines shorter, but it also facilitates future code refactoring.
+with ``auto`` instead of repeating the type name. Not only does this improve code readability
+(by reducing redundancy), but it also facilitates future code refactoring.
 
 .. sourcecode:: cpp
 
-  // Avoid repeating the type name when declaring iterators or pointers, and casting variables
-  std::map<uint32_t, std::string>::const_iterator it = myMap.find(key);
+  // Avoid repeating the type name when declaring variables if the type is obvious from the context
   int* ptr = new int[10];
   uint8_t m = static_cast<uint8_t>(97 + (i % 26));
 
   // Prefer to declare them with auto
-  auto it = myMap.find(key);
   auto* ptr = new int[10];
   auto m = static_cast<uint8_t>(97 + (i % 26));
 
+Containers (maps, sets, vectors) and iterators require understanding how C++ type deduction
+works.  Suppose that you want a const iterator when using ``std::map::find()``, matching the
+below declaration:
+
+.. sourcecode:: cpp
+
+  std::map<uint32_t, std::string>::const_iterator it = myMap.find(key);
+
+If ``myMap`` has been declared as a non-const map (which is the typical case in |ns3|), then the
+following will not result in a ``const_iterator`` because ``find()`` returns a non-const iterator
+on a non-const map:
+
+.. sourcecode:: cpp
+
+  auto it = myMap.find(key);
+
+Instead, if you would like auto to deduce a const iterator from ``find()``, you need to make
+``myMap`` into a const lvalue reference, such as:
+
+.. sourcecode:: cpp
+
+  auto it = std::as_const(myMap).find(key);
+
+Note that ``std::map::begin()`` and ``std::map::end()`` have ``cbegin()`` and ``cend()`` variants
+that explicitly return const iterators, but ``std::map::find()`` and some other methods lack
+such variants, requiring use of ``std::as_const()`` as shown above.
+
+.. sourcecode:: cpp
+
+  auto it = myMap.begin();  // declares a non-const iterator
+  auto cit = myMap.cbegin();  // declares a const iterator
+
+If the iterator is a const iterator, prefixing the name ``it`` with ``c`` can improve readability.
 
 Initialization
 ==============
@@ -960,6 +991,45 @@
   Time t{100000000}; // NOT OK, is interpreted differently when ``Time::SetResolution()`` called
   Time t{0.1}; // NOT OK, will round to zero; see above and also merge request !2007
 
+A constant that cannot be declared as ``constexpr`` (such as an ``ns3::Time`` constant) and needs
+to be used in multiple translation units (TUs) shall be declared as ``inline`` and initialized in a
+header file that is then included in all the TUs in which the constant needs to be used:
+
+.. sourcecode:: cpp
+
+  inline const Time WIFI_TU = MicroSeconds(1024);
+
+Indeed, since C++17, there may be more than one definition of a variable declared as ``inline`` in
+the program as long as each definition appears in a different TU and all definitions are identical.
+See wifi-standard-constants.h for an example of how to group such constants in a header file and
+declare them so that the values may be used in Attribute defaults.
+
+This approach is to be preferred over declaring the constant as ``extern`` in a header file and
+initializing the constant in a separate ``.cc`` file, because in such a case it would be dangerous
+to use the constant in the initialization of a static variable (e.g., the ``tid`` static member in
+the static ``GetTypeId`` method of classes inheriting from the ``Object`` class) due to the issue
+of static initialization order.
+
+A constant that cannot be declared as ``constexpr`` (such as an ``ns3::Time`` constant) and needs
+to be used in a single TU shall be declared and initialized in the ``.cc`` file before it is used:
+
+.. sourcecode:: cpp
+
+  // Example of declaring a Time constant locally
+  // If DEFAULT_BEACON_INTERVAL is declared with the class implementation such as
+  // below, and it is used as an an attribute default value, make sure that it
+  // is declared before the object TypeId is registered, such as below:
+  const Time DEFAULT_BEACON_INTERVAL = MicroSeconds(DEFAULT_BEACON_INTERVAL_USEC);
+
+  NS_OBJECT_ENSURE_REGISTERED(ApWifiMac);
+
+Instead, constants that can be declared as ``constexpr`` (such as all the POD types) shall be
+initialized in an header file by using the ``constexpr`` keyword only:
+
+.. sourcecode:: cpp
+
+  constexpr uint16_t MIN_AID{1};
+
 Comments
 ========
 
@@ -1182,10 +1252,46 @@
 Namespaces
 ==========
 
-Code should always be included in a given namespace, namely ``ns3``.
-In order to avoid exposing internal symbols, consider placing the code in an
-anonymous namespace, which can only be accessed by functions in the same file.
+|ns3| uses the ``ns3`` namespace to separate ns-3 model library code from the
+C++ global namespace. The following guidelines apply to the use of
+the ``ns3`` namespace and additional namespaces. Note that these guidelines are
+applied inconsistently within the |ns3| mainline because |ns3| historically had
+limited use of nested namespaces, and a widespread change to use nested namespaces
+in all libraries would hinder backward compatibility of user programs. Therefore,
+the migration to use the below guidelines is gradual, but they should apply to
+newly authored code.
+
+- |ns3| model library code (code within the ``model`` and ``helper`` directories)
+  should be included within the ``ns3`` namespace.
+- We recommend that new ns-3 modules, intended for the mainline, wrap their
+  model code in a nested namespace. The name of the namespace should be the
+  module name in lowercase, without any hyphens. See the ``lr-wpan`` module
+  for an example (the nested namespace in that case is ``lrwpan``). An example
+  of a recently added module following this guideline is the ``zigbee`` module.
+- Tests should use the ``tests`` namespace outside of the ``ns3`` namespace,
+  and can import the ``ns3`` namespace and any nested namespaces with the ``using``
+  directive.
+- Example program code should not be within the ``ns3`` or nested namespaces, but
+  can import those namespaces with the ``using`` directive.
+- In order to avoid exposing internal symbols, consider placing such code in an
+  anonymous namespace, which can only be accessed by functions in the same file.
+
+When using nested namespaces, a question arises as to the use of prefixes on types
+that are declared in the namespace. For instance, consider the ``LrWpanNetDevice``
+class, which is a derived class of ``NetDevice`` and which exists in the
+``ns3::lrwpan`` namespace. When using scope resolution to refer to these types,
+the prefix is repetitive with the namespace; e.g., ``ns3::lrwpan::LrWpanNetDevice``,
+although ``ns3::lrwpan::NetDevice`` would be sufficient to avoid collisions and
+would be shorter. However, use of the shorter version can hinder regular expression
+searches through the codebase for given types, and require more instances of scope
+resolution. This style
+guide leaves this decision of whether to prefix type names with nested namespace
+hints (such as ``LrWpanNetDevice``) or to shorten the type names (such as
+``NetDevice``) for authors to decide, but in the case where a name may be the same
+as used in another module (such as the NetDevice example herein), a prefix on the
+type name is recommended.
 
+The following guidelines apply to the code syntax for namespace usage.
 Code within namespaces should not be indented. To more easily identify the end
 of a namespace, add a trailing comment to its closing brace.
 
@@ -1198,8 +1304,6 @@
 
   } // namespace ns3
 
-Namespace names should follow the snake_case convention.
-
 Unused variables
 ================
 
@@ -1679,7 +1783,10 @@
         ...
     };
 
-- When checking whether a Time value is zero, use ``Time::IsZero()`` rather than comparing it to a zero-valued time object with ``operator==``, to avoid construction of a temporary.  Similar guidance applies to the related functions ``Time::IsPositive()``, ``Time::IsNegative()``, ``Time::IsStrictlyPositive``, and ``Time::IsStrictlyNegative()``.
+- When checking whether a Time value is zero, use ``Time::IsZero()`` rather than comparing it to
+  a zero-valued time object with ``operator==``, to avoid construction of a temporary.
+  Similar guidance applies to the related functions ``Time::IsPositive()``,
+  ``Time::IsNegative()``, ``Time::IsStrictlyPositive``, and ``Time::IsStrictlyNegative()``.
 
   .. sourcecode:: cpp
 
@@ -1691,6 +1798,43 @@
     if (t > Seconds(0))
     {...}
 
+- Use ``std::array`` instead of C-style arrays, e.g.:
+
+  .. sourcecode:: cpp
+
+    // Avoid
+    uint8_t myArray[16];
+
+    // Prefer
+    std::array<uint8_t, 16> myArray;
+
+  This enables many useful operations without writing extra code, such as copy constructors, default
+  comparison operators, etc.
+
+- In C++ 20, a new comparison operator <=> ... was added, and a new pattern started to become a C++
+  best practice: to avoid defining operators separately but to make use of a member declaration such as:
+
+  .. sourcecode:: cpp
+
+    #include <compare>
+    struct IntWrapper
+    {
+      int value{0};
+      constexpr IntWrapper(int value): value{value} { }
+      auto operator<=>(const IntWrapper&) const = default;
+      bool operator==(const IntWrapper&) const = default; // Unnecessary, derived from <=>
+      bool operator!=(const IntWrapper&) const = default; // Unnecessary, derived from ==
+    };
+
+  For new ns-3 code, we recommend using this operator where possible.  There are cases in which
+  the default does not apply, and you can read about some of them
+  `here <https://en.cppreference.com/w/cpp/language/default_comparisons.html>`_ and
+  `here <https://devblogs.microsoft.com/cppblog/simplify-your-code-with-rocket-science-c20s-spaceship-operator/>`_.
+  You may notice in the ns-3 codebase that most code defines operators separately, because the code predates C++20.
+  Some of this code may be changed to use ``<=>`` over time.
+
+  Two examples are in ``ns3::LollipopCounter``, and ``ns3::SequenceNumber``.
+
 Clang-tidy rules
 ================
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/contributing/source/conf.py ns-3.47/doc/contributing/source/conf.py
--- ns-3.46/doc/contributing/source/conf.py	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/doc/contributing/source/conf.py	2026-02-16 16:16:25.803885200 -0800
@@ -49,9 +49,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = "ns-3.46"
+version = "ns-3.47"
 # The full version, including alpha/beta/rc tags.
-release = "ns-3.46"
+release = "ns-3.47"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/contributing/source/external.rst ns-3.47/doc/contributing/source/external.rst
--- ns-3.46/doc/contributing/source/external.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/contributing/source/external.rst	2026-02-16 16:08:49.481229300 -0800
@@ -167,6 +167,19 @@
 maintainer to finalize the page, and then make it active (visible) to
 other users when ready.
 
+Licensing for app store modules
+*******************************
+
+The project recommends the use of GPLv2-compatible licenses for modules
+listed in the ns-3 app store. However, this is not strictly required;
+it is possible for contributors to post and advertise modules that are
+licensed with a non-GPLv2-compatible license, such as GPLv3. An example
+of an advertised extension with a non-compatible license is the
+`EMPIRE shim <https://apps.nsnam.org/app/empire/>`_ module. Please note,
+however, that the project can only distribute modules in its ns-allinone
+releases that are GPLv2-compatible.  A module licensed as GPLv2-or-later
+is compatible for inclusion with the ns-allinone releases.
+
 Code review for apps
 ********************
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/contributing/source/general.rst ns-3.47/doc/contributing/source/general.rst
--- ns-3.46/doc/contributing/source/general.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/contributing/source/general.rst	2026-02-16 16:08:49.484229300 -0800
@@ -24,6 +24,16 @@
 Note that the Free Software Foundation maintains
 `a list <https://www.gnu.org/licenses/license-list.en.html#GPLCompatibleLicenses>`_ of GPLv2-compatible licenses.
 
+There are two variants of GNU GPLv2: GNU GPLv2-only, and GNU GPLv2-or-later.
+Most of the ns-3 codebase is licensed under GNU GPLv2-only; a few files
+are licensed as GNU GPLv2-or-later or other compatible licenses (see
+the `LICENSES` directory for a listing of all licenses in the main
+tree). Strictly speaking, ns-3 as an aggregate is licensed as GNU GPLv2-only
+and cannot be redistributed either as GNU GPLv2-or-later or as GNU GPLv3.
+
+ns-3 cannot accept GNU GPLv3 or Apache licensed code into the mainline
+because those licenses are not compatible with GNU GPLv2-only.
+
 If a contribution is based upon or contains copied code that itself
 uses GNU GPLv2, then the author should in most cases retain the GPLv2 and
 optionally extend the copyright and/or the author (or 'Modified by')
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/doxygen.conf ns-3.47/doc/doxygen.conf
--- ns-3.46/doc/doxygen.conf	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/doxygen.conf	2026-02-16 16:08:49.485229500 -0800
@@ -265,6 +265,8 @@
 ##     template <typename T> void Func ();
 ##   which is called as
 ##     Func<MyClass> ();
+## Hide the caller graph
+## Hide the Referenced by list
 
 ALIASES                = "bugid{1}=<a href=\"http://www.nsnam.org/bugzilla/show_bug.cgi?id=\1\">Bug \1</a>" \
                          "issueid{1}=<a href=\"https://gitlab.com/nsnam/ns-3-dev/-/issues/\1\">Issue \1</a>" \
@@ -272,7 +274,9 @@
                          "pname{1}=<span class=\"params\"><span class=\"paramname\">\1</span></span>" \
                          "RFC{1}=<a href=\"http://datatracker.ietf.org/doc/rfc\1/\">RFC \1</a>" \
                          "deduced=<span class=\"params\"><span class=\"paramdir\">[deduced] </span></span>" \
-                         "explicit=<span class=\"params\"><span class=\"paramdir\">[explicit]</span></span>"
+                         "explicit=<span class=\"params\"><span class=\"paramdir\">[explicit]</span></span>" \
+                         "hidecaller=Caller graph was not generated because of its size. @hidecallergraph <br/>" \
+                         "hideref=Referenced by list suppressed because of its size. @hiderefby <br/>"
 
 # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
 # only. Doxygen will then generate output that is more tailored for C. For
@@ -2275,7 +2279,7 @@
                          DOXYGEN_SHOULD_SKIP_THIS \
                          INT64X64_USE_CAIRO \
                          INT64X64_USE_DOUBLE \
-                         INT64X64_USE_CAIRO \
+                         INT64X64_USE_128 \
                          __attribute__(x)= \
                          __declspec(x)=
 
@@ -2299,6 +2303,7 @@
                          ATTRIBUTE_VALUE_IMPLEMENT_WITH_NAME \
                          NS_UNUSED_GLOBAL \
                          NS_DEPRECATED \
+                         NS_DEPRECATED_3_47 \
                          NS_DEPRECATED_3_46 \
                          NS_DEPRECATED_3_45 \
                          NS_DEPRECATED_3_44 \
@@ -2643,7 +2648,7 @@
 # Minimum value: 0, maximum value: 10000, default value: 50.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
-DOT_GRAPH_MAX_NODES    = 70
+DOT_GRAPH_MAX_NODES    = 50
 
 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
 # generated by dot. A depth value of 3 means that only nodes reachable from the
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/doxygen.warnings.report.sh ns-3.47/doc/doxygen.warnings.report.sh
--- ns-3.46/doc/doxygen.warnings.report.sh	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/doxygen.warnings.report.sh	2026-02-16 16:08:49.485229500 -0800
@@ -1,23 +1,24 @@
 #!/bin/bash
-# -*- indent-tabs-mode:nil; -*-
 
-
-# Process doxygen log to generate sorted list of top offenders.
+# Process Doxygen log to generate sorted list of top offenders.
 #
 
 me=$(basename $0)
 DIR="$(dirname $0)"
-# Trick to get the absolute path, since doxygen prefixes errors that way
+# Trick to get the absolute path, since Doxygen prefixes errors that way
 ROOT=$(cd "$DIR/.."; pwd -P)
 
 # Known log files
-STANDARDLOGFILE=doxygen.log
-WARNINGSLOGFILE=doxygen.warnings.log
+STANDARDLOGFILE=$DIR/doxygen.log
+WARNINGSLOGFILE=$DIR/doxygen.warnings.log
 # Default choice:  generate it
-LOG="$DIR/$WARNINGSLOGFILE"
+LOG=$WARNINGSLOGFILE
 # Verbose log
 VERBLOG="$DIR/doxygen.verbose.log"
 
+# Doxygen config file
+DOXYFILE=$DIR/doxygen.conf
+
 
 # Options ------------------------------
 #
@@ -25,7 +26,7 @@
 # One line synopsis, continue
 function synopsis_short
 {
-    echo "Usage: $me [-beithv] [-s <log-file> | -l | -w] [-m <module>] [-f <regex>] [-F <regex>]"
+    echo "Usage: $me [-behiStv] [-s <log-file> | -l | -w] [-m <module>] [-f <regex>] [-F <regex>]"
 }
 
 # Two line synopsis, then exit with error
@@ -42,14 +43,13 @@
     synopsis_short
     cat <<-EOF
 
-    Run doxygen to generate all errors; report error counts
+    Run Doxygen to generate all errors; report error counts
     by module and file.
 
-    -i  Skip the build, try print-introspected-doxygen anyway.
-
-    -s  Skip doxygen run; use existing <log-file>.
-    -w  Skip doxygen run; use existing warnings log doc/$WARNINGSLOGFILE
-    -l  Skip doxygen run; use the normal doxygen log doc/$STANDARDLOGFILE
+    -i  Skip the build and introspection (introspected-doxygen, command-line)
+    -s  Skip Doxygen run; use existing <log-file>
+    -w  Skip Doxygen run; use existing warnings log $WARNINGSLOGFILE
+    -l  Skip Doxygen run; use the normal Doxygen log $STANDARDLOGFILE
 
     -b  Omit the blacklist filter of files whose warnings we ignore
     -e  Filter out warnings from */examples/*
@@ -57,21 +57,25 @@
     -m  Only include files matching src/<module>
     -f  Only include files matching the <regex>
     -F  Exclude files matching the <regex>
+    -S  Just show the summary counts, not the full listing
 
     -v  Show detailed output from each step.
     -h  Print this usage message
 
-    The default behavior is to modify doxygen.conf temporarily to
+    The default behavior is to modify $DOXYFILE temporarily to
     report all undocumented elements, and to reduce the run time.
-    The output of this special run is kept in doc/$WARNINGSLOGFILE.
+    The output of this special run is kept in $WARNINGSLOGFILE.
     To further reduce the run time, the -i option also skips
-    print-introspected-doxygen, so ns3 doesn\'t have to compile
-    any modified files at all.
+    the build, so ns3 doesn\'t have to compile any modified files at all.
+    This option necessarily skips re-generation of introspected-doxygen.h and
+    introspected-command-line.h, which rely on a clean build,
+    but these files only change when executable code changes, so this
+    isn't usually significant when just amending Doxygen comments.
 
-    The -f, -l, and -s options skip the doxygen run altogether.
+    The -l, -s, and -w options skip the Doxygen run altogether.
     The first two use a specified or the standard log file;
-    the -s option uses the warnings log from a prior run.
-    Only the first of -f <log-file>, -s, or -l will have effect.
+    the -w option uses the warnings log from a prior run.
+    Only the first of -s <log-file>, -w, or -l will have effect.
 
     The -e and -t options exclude examples and test directories
     from the counts.  The -m option only includes a specific module.
@@ -79,6 +83,11 @@
     The -m and -F options append the relevant warnings after the
     numerical report.  These can be used in any combination.
 
+    Some warnings are just nuisances, typically from third-party code
+    or constructs Doxygen has trouble with.  These are pre-loaded in to
+    the black list filter.  If you want to see what files and warnings these
+    are use the -b option.
+
 EOF
     exit 0
 }
@@ -89,6 +98,12 @@
 # Arg -v Verbosity level
 verbosity=0
 
+# Log message, possibly verbosely if -v option given
+# Non-verbose:  Print the msg (without newline), then report status of cmd
+#   verbose -n msg
+#   ...
+#   status_report $? cmd
+# Verbose:  prefix with script name
 function verbose
 {
     if [ "$1" == "-n" ]; then
@@ -98,7 +113,7 @@
             echo -n "${2}..."
         fi
     elif [ $verbosity -eq 1 ]; then
-        echo "$me:  $1 $2"
+        echo "$me: ${1:+ }$1 $2"
     else
         echo "$2"
     fi
@@ -108,6 +123,26 @@
 rm -f $VERBLOG
 exec 6>$VERBLOG
 
+# Swap back to original DOXYFILE
+# Define here so status_report can use it if needed
+function restore_doxyfile()
+{
+    if [ -e $DOXYFILE.bak ]; then
+        rm -f $DOXYFILE
+        mv -f $DOXYFILE.bak $DOXYFILE
+    fi
+}
+
+# Report status of last command
+#   status_report $? long_msg [exitonerr]
+# If status ($?) was success (0)
+#   If verbose
+#     Show detailed log of last command
+#     Verbosely log long_msg
+#   Log "done."
+# If error
+#   If verbose log FAILED and detail log
+#   If exitonerr exit the script
 function status_report
 {
     local status="$1"
@@ -121,6 +156,7 @@
         if [ $exitonerr == "yes" ]; then
             verbose "$long_msg "  "FAILED.  Details:"
             [[ -e $VERBLOG ]] && cat $VERBLOG && rm -f $VERBLOG
+            restore_doxyfile
             exit 1
         else
             verbose "$long_msg "  "FAILED, continuing"
@@ -138,12 +174,12 @@
 logfile_arg=
 # -l
 use_standard=0
-# skip doxygen run; using existing log file
+# skip Doxygen run; using existing log file
 skip_doxy=0
-# skip print-introspected-doxygen, avoiding a build
+# skip build, print-introspected-doxygen and introspected-command-line
 skip_intro=0
 
-# Filtering flags
+# Filtering flags, 1 means on/true
 filter_blacklist=1
 filter_examples=0
 filter_test=0
@@ -153,7 +189,10 @@
 filter_in=""
 filter_out=""
 
-while getopts :bef:F:hilm:s:tvw option ; do
+# Summary only
+summary_only=0
+
+while getopts :bef:F:hilm:Ss:tvw option ; do
 
     case $option in
     (b)  filter_blacklist=0       ;;
@@ -180,6 +219,8 @@
          logfile_arg="$OPTARG"
          ;;
 
+    (S)  summary_only=1           ;;
+
     (t)  filter_test=1            ;;
 
     (v)  verbosity=1
@@ -187,7 +228,7 @@
          ;;
 
     (w)  use_filearg=1
-         logfile_arg="$DIR/$WARNINGSLOGFILE"
+         logfile_arg="$WARNINGSLOGFILE"
          ;;
 
     (:)  echo "$me: Missing argument to -$OPTARG" ; synopsis ;;
@@ -197,6 +238,7 @@
     esac
 done
 
+# Check for existence of alternate Doxygen log file
 function checklogfile
 {
     if [ -e "$1" ] ; then
@@ -214,7 +256,7 @@
 if [[ $use_filearg -eq 1 && "${logfile_arg:-}" != "" ]] ; then
     checklogfile "$logfile_arg"
 elif [ $use_standard -eq 1 ]; then
-    checklogfile "$DIR/$STANDARDLOGFILE"
+    checklogfile "$STANDARDLOGFILE"
 fi
 
 # Log filters --------------------------
@@ -242,17 +284,29 @@
 # Blacklist filter of files whose warnings we ignore
 filter_blacklistRE=""
 
-#   External files: adding our own doxygen makes diffs with upstream very hard
+#   External files: adding our own Doxygen makes diffs with upstream very hard
 #     cairo-wideint
 REappend filter_blacklistRE "cairo-wideint"
+REappend filter_blacklistRE "valgrind\.h"
 
 #   Functions with varying numbers of arguments
 #   Explicit template instantiation declaration
 # REappend filter_blacklistRE "MakeCallback< ObjectBase \\* >(ObjectBase \\*"
 
+#   Global variable declarations which look like function prototypes
+REappend filter_blacklistRE "generic-battery-discharge-example.cc.*battDischFile"
+
 #   ATTRIBUTE_HELPER_CPP( and _HEADER(
-# REappend filter_blacklistRE "ATTRIBUTE_HELPER_\\(CPP\\|HEADER\\)"
+REappend filter_blacklistRE "ATTRIBUTE_HELPER_\\(CPP\\|HEADER\\)"
+
+#   NS_DEPRECATED_...(msg)
+REappend filter_blacklistRE "return type .*NS_DEPRECATED_3_"
+
+#   *.py:  we've included python files since 2012,
+#          but they are now (2025) generating warnings
+REappend filter_blacklistRE "\.py"
 
+#
 # Filter out regular expression for black list, -e, -t and -F
 filter_outRE=""
 [[ $filter_blacklist -eq 1 ]] && REappend filter_outRE "$filter_blacklistRE"
@@ -264,6 +318,7 @@
 # Configuration ------------------------
 #
 
+# Log switch values '0' as "off", not '0' as "ON"
 function on_off
 {
     if [[ "${!1:-}" != "" && "${!1}" != "0" ]] ; then
@@ -273,11 +328,24 @@
     fi
 }
 
+# Log all the filters
+function show_filters
+{
+    echo
+    echo "    Net result of all filters:"
+    [[ "${filter_blacklistRE:-}" != "" ]] && echo "      Blacklist:     \"$filter_blacklistRE\""
+    [[ "${filter_inRE:-}"  != "" ]] && echo "      Filtering in:  \"$filter_inRE\""
+    [[ "${filter_outRE:-}" != "" ]] && echo "      Filtering out: \"$filter_out\""
+
+    echo
+}
+
 if [ $verbosity -eq 1 ]; then
     echo
     echo "$me:"
     echo "    Verbose:          $(on_off verbosity)"
     echo "    Skip build:       $(on_off skip_intro)"
+    echo "    Skip Doxygen:     $(on_off skip_doxy)"
     echo "    Log file to use:  $LOG"
     echo "    Module filter:    $(on_off filter_module)  $filter_module"
     echo "    Examples filter:  $(on_off filter_examples)"
@@ -285,49 +353,43 @@
     echo "    Blacklist filter: $(on_off filter_blacklist)"
     echo "    Filter in:        $(on_off filter_in)  $filter_in"
     echo "    Filter out:       $(on_off filter_out)  $filter_out"
-    echo
+    echo "    Summary only:     $(on_off summary_only)"
 
     #  Show the resulting filters here, in addition to below
-    echo "    Net result of all filters:"
-    [[ "${filter_inRE:-}"  != "" ]] && echo "      Filtering in:   \"$filter_inRE\""
-    [[ "${filter_outRE:-}" != "" ]] && echo "      Filtering out:  \"$filter_outRE\""
-
-    echo
+    show_filters
 fi
 
 
-#  Run doxygen -------------------------
+#  Run Doxygen -------------------------
 #
 
 if [ $skip_doxy -eq 1 ]; then
     echo
-    echo "Skipping doxygen run, using existing log file $LOG"
+    echo "Skipping Doxygen run, using existing log file $LOG"
 
 else
 
-    # We're going to modify doxygen.conf
+    # We're going to modify DOXYFILE
     # In case the user ^C's out of this we need to restore
-    # doxygen.conf, otherwise weird things happen.
-    # function restore_doxygen_conf defined below
-    trap restore_doxygen_conf INT
+    # DOXYFILE, otherwise weird things happen the next time
+    # they build Doxygen
+    trap restore_doxyfile INT
 
-    # Modify doxygen.conf to generate all the warnings
+    # Modify DOXYFILE to generate all the warnings
     # We keep dot active to generate graphs in the documentation
     # (see for example PacketTagList) and warn about ill-formed
-    # graphs, but we disable all the doxygen-generated diagrams
+    # graphs, but we disable all the Doxygen-generated diagrams
     # to shorten the run time.
 
-    conf=doc/doxygen.conf
-    cp $conf ${conf}.bak
-    cat <<-EOF >> $conf
+    cp $DOXYFILE ${DOXYFILE}.bak
+    cat <<-EOF >> $DOXYFILE
 
     # doxygen.warnings.report.sh:
     EXTRACT_ALL = no
     WARNINGS = no
-    WARN_LOGFILE = doc/$WARNINGSLOGFILE
+    WARN_LOGFILE = $WARNINGSLOGFILE
     SOURCE_BROWSER = no
     HTML_OUTPUT = html-warn
-    CLASS_DIAGRAMS = no
     CLASS_GRAPH = no
     COLLABORATION_GRAPH = no
     GROUP_GRAPHS = no
@@ -339,36 +401,26 @@
     DIRECTORY_GRAPH = no
 EOF
 
-    # Swap back to original config
-    function restore_doxygen_conf()
-    {
-        if [ -e $conf.bak ]; then
-            rm -f $conf
-            mv -f $conf.bak $conf
-        fi
-    }
-
-    intro_h="introspected-doxygen.h"
+    doxycmd="doxygen-no-build"
     if [ $skip_intro -eq 1 ]; then
-        verbose "" "Skipping ./ns3 build"
-        verbose -n "Trying print-introspected-doxygen with doxygen build"
-        (cd "$ROOT" && ./ns3 run print-introspected-doxygen --no-build >doc/$intro_h 2>&6 )
-        status_report $? "./ns3 run print-introspected-doxygen" noexit
+        verbose "" "Skipping ./ns3 build, print-introspected-doxygen, and introspected-command-line"
+
     else
         # Run introspection, which may require a build
         verbose -n "Building"
         (cd "$ROOT" && ./ns3 build >&6 2>&6 )
         status_report $? "./ns3 build"
-        verbose -n "Running print-introspected-doxygen with doxygen build"
-        (cd "$ROOT" && ./ns3 run print-introspected-doxygen --no-build >doc/$intro_h 2>&6 )
-        status_report $? "./ns3 run print-introspected-doxygen"
+
+        doxycmd=doxygen
     fi
 
-    verbose -n "Rebuilding doxygen docs with full errors"
-    (cd "$ROOT" && ./ns3 docs doxygen-no-build >&6 2>&6 )
-    status_report $? "./ns3 docs doxygen-no-build"
+    echo "Doxygen@version:" `which doxygen` "@" `doxygen --version`
+    verbose "" "Repo version: $(cd $ROOT && doc/ns3_html_theme/get_version.sh | cut -d ':' -f 2- )"
+    verbose -n "Rebuilding Doxygen docs with full errors"
+    (cd "$ROOT" && ./ns3 docs $doxycmd >&6 2>&6 )
+    status_report $? "./ns3 docs $doxycmd"
 
-    restore_doxygen_conf
+    restore_doxyfile
 fi
 
 # Filter log file
@@ -382,6 +434,7 @@
 
     flog=$(                         \
         echo "$flog"              | \
+        sed "s|^$ROOT/||g"         | \
         sort -t ':' -k1,1 -k2,2n  | \
         uniq                        \
         )
@@ -391,13 +444,12 @@
 
 # Analyze the log ----------------------
 #
-#  Show the resulting filters
-echo
-echo "Net result of all filters:"
-[[ "${filter_inRE:-}"  != "" ]] && echo "Filtering in \"$filter_inRE\""
-[[ "${filter_outRE:-}" != "" ]] && echo "Filtering out \"$filter_outRE\""
+#  Show the resulting filters, if not already shown by verbose
+if [[ $verbosity -ne 1 && ! -z "$filter_log_results" ]]; then
+    show_filters
+fi
 
-verbose -n "Filtering the doxygen log"
+verbose -n "Filtering the Doxygen log $LOG"
 
 filter_log_results=$(filter_log)
 
@@ -439,7 +491,7 @@
     sed 's/^[ \t]*//;s/[ \t]*$//'             \
     )
 
-# Sometimes doxygen can not pinpoint a warning to an exact file.
+# Sometimes Doxygen can not pinpoint a warning to an exact file.
 # In this case the output is of the form:
 # "<operator==>:1: warning: parameters of member ns3::operator== are not documented"
 # or
@@ -512,6 +564,13 @@
 
 # Summarize the log --------------------
 #
+# Return status based on warnings
+exit_status=$((warncount > 0))
+
+if ((warncount == 0)); then
+   echo "No warnings passed the filters."
+   exit $exit_status
+fi
 
 echo
 echo "Report of Doxygen warnings"
@@ -576,9 +635,6 @@
 printf "%6d files\n" $filecount
 printf "%6d warnings\n" $warncount
 
-# Return status based on warnings
-exit_status=$((warncount > 0))
-
 # if [ "${filter_inRE:-}" != "" ] ; then
 #     if [ "$filterin" != "" ] ; then
 #         echo
@@ -592,6 +648,12 @@
 #     fi
 # fi
 
+# Summary only: quit now
+if [ "$summary_only" == 1 ]; then
+    status_report 0 $me
+    exit $exit_status
+fi
+
 if [ "$filterin" != "" ] ; then
     echo
     echo
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/installation/source/conf.py ns-3.47/doc/installation/source/conf.py
--- ns-3.46/doc/installation/source/conf.py	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/doc/installation/source/conf.py	2026-02-16 16:16:25.803885200 -0800
@@ -49,9 +49,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = "ns-3.46"
+version = "ns-3.47"
 # The full version, including alpha/beta/rc tags.
-release = "ns-3.46"
+release = "ns-3.47"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/installation/source/linux.rst ns-3.47/doc/installation/source/linux.rst
--- ns-3.46/doc/installation/source/linux.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/installation/source/linux.rst	2026-02-16 16:08:49.485229500 -0800
@@ -182,3 +182,7 @@
       <policy domain="coder" rights="none" pattern="XPS" />
     </policymap>
 
+If you install eigen3 via brew packaging for Linux (eigen@3), you need to add it to the CMake
+prefix path to be able to locate it properly, since brew does not symlink it.::
+
+  ./ns3 configure -- -DCMAKE_PREFIX_PATH="/home/linuxbrew/.linuxbrew/opt/eigen@3/"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/installation/source/macos.rst ns-3.47/doc/installation/source/macos.rst
--- ns-3.46/doc/installation/source/macos.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/installation/source/macos.rst	2026-02-16 16:08:49.485229500 -0800
@@ -38,7 +38,7 @@
 For a short guide on virtual environments, please see
 `this link <https://www.dataquest.io/blog/a-complete-guide-to-python-virtual-environments/>`_.
 
-Due to an `upstream limitation with Cppyy <https://github.com/wlav/cppyy/issues/150>`_, Python bindings do not work on macOS machines with Apple silicon (M1 and M2 processors).
+Due to an `upstream limitation with Cppyy <https://github.com/wlav/cppyy/issues/150>`_, Python bindings did not work with |ns3| versions older than ns-3.42, but they should work with more recent |ns3| releases.
 
 Requirements
 ************
@@ -118,8 +118,6 @@
 
 Please see below subsections for Python-related package requirements.
 
-For MacPorts packages we show the most recent package version available as of early 2023.
-
 +-----------------------------+----------------------------------+--------------------------+
 | **Feature**                 | **Homebrew packages**            | **MacPort packages**     |
 +=============================+==================================+==========================+
@@ -127,7 +125,7 @@
 +-----------------------------+----------------------------------+--------------------------+
 | Database support            | ``sqlite``                       | ``sqlite3``              |
 +-----------------------------+----------------------------------+--------------------------+
-| NetAnim animator            | ``qt@5``                         | ``qt513``                |
+| NetAnim animator            | ``qt@5``                         | ``qt59``                 |
 +-----------------------------+----------------------------------+--------------------------+
 | MPI-based distributed       |                                  | ``openmpi``              |
 | simulation                  | ``open-mpi``                     |                          |
@@ -142,7 +140,7 @@
 |                             |                                  | `XX`` the Python minor   |
 |                             |                                  | version such as `12`     |
 +-----------------------------+----------------------------------+--------------------------+
-| Eigen3                      | ``eigen``                        | ``eigen3``               |
+| Eigen3                      | ``eigen@3``                      | ``eigen3``               |
 +-----------------------------+----------------------------------+--------------------------+
 | GNU Scientific Library      | ``gsl``                          | ``gsl``                  |
 +-----------------------------+----------------------------------+--------------------------+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/installation/source/system.rst ns-3.47/doc/installation/source/system.rst
--- ns-3.46/doc/installation/source/system.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/installation/source/system.rst	2026-02-16 16:08:49.486229400 -0800
@@ -100,14 +100,16 @@
 
 .. note::
 
-  Check the list of supported versions of clang-format in the ``utils/check-style-clang-format.py`` program
-  or in the `coding-style.rst <https://gitlab.com/nsnam/ns-3-dev/-/blob/master/doc/contributing/source/coding-style.rst>`_ document.
+  Check the list of supported versions of clang-format and clang-tidy in the
+  `RELEASE_NOTES.md <https://gitlab.com/nsnam/ns-3-dev/-/blob/master/RELEASE_NOTES.md>`_ document.
 
 clang-format is strongly recommended to write code that follows the ns-3 code conventions, but
 might be skipped for simpler tasks (e.g., writing a simple simulation script for yourself).
 
 clang-tidy is recommended when writing a module, to both follow code conventions and to provide
-hints on possible bugs in code.
+hints on possible bugs in code. Note: when using CMake's ``include_directories()`` to include a
+third-party dependency directory, it is important to set the ``SYSTEM`` flag, to prevent clang-tidy
+from checking code that is not under our control (e.g. ``include_directories(SYSTEM ${SQLite3_INCLUDE_DIRS})``).
 
 Both are used in the CI system, and a merge request will likely fail if you did not use them.
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/attributes.rst ns-3.47/doc/manual/source/attributes.rst
--- ns-3.46/doc/manual/source/attributes.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/attributes.rst	2026-02-16 16:08:49.524229500 -0800
@@ -221,7 +221,7 @@
 :cpp:class:`Node` object without even knowing the concrete C++ type::
 
     ObjectFactory factory;
-    const std::string typeId = "ns3::Node'';
+    const std::string typeId = "ns3::Node";
     factory.SetTypeId(typeId);
     Ptr<Object> node = factory.Create <Object>();
 
@@ -440,7 +440,7 @@
 
 * Binding the (usually private) member variable :cpp:member:`m_maxSize`
   to a public string ``"MaxSize"``.
-* Providing a default value (0 packets).
+* Providing a default value (100 packets).
 * Providing some help text defining the meaning of the value.
 * Providing a "Checker" (not used in this example) that can be used to set
   bounds on the allowable range of values.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/conf.py ns-3.47/doc/manual/source/conf.py
--- ns-3.46/doc/manual/source/conf.py	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/doc/manual/source/conf.py	2026-02-16 16:16:25.803885200 -0800
@@ -74,9 +74,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = "ns-3.46"
+version = "ns-3.47"
 # The full version, including alpha/beta/rc tags.
-release = "ns-3.46"
+release = "ns-3.47"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/events.rst ns-3.47/doc/manual/source/events.rst
--- ns-3.46/doc/manual/source/events.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/events.rst	2026-02-16 16:08:49.525229700 -0800
@@ -168,14 +168,14 @@
 events. It provides three variants to do so (ordered from most commonly
 used to least commonly used):
 
-* Schedule methods which allow you to schedule an event in the future
+* ``Schedule`` methods which allow you to schedule an event in the future
   by providing the delay between the current simulation time and the
   expiration date of the target event.
-* ScheduleNow methods which allow you to schedule an event for the
+* ``ScheduleNow`` methods which allow you to schedule an event for the
   current simulation time: they will execute _after_ the current event is
   finished executing but _before_ the simulation time is changed for the
   next event.
-* ScheduleDestroy methods which allow you to hook in the shutdown
+* ``ScheduleDestroy`` methods which allow you to hook in the shutdown
   process of the Simulator to cleanup simulation resources: every
   'destroy' event is executed when the user calls the Simulator::Destroy
   method.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/helpers.rst ns-3.47/doc/manual/source/helpers.rst
--- ns-3.46/doc/manual/source/helpers.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/helpers.rst	2026-02-16 16:08:49.525229700 -0800
@@ -25,9 +25,9 @@
    similar objects to which similar or identical operations can be performed.
 #. The helper API is not generic; it does not strive to maximize code reuse. So,
    programming constructs such as polymorphism and templates that achieve code
-   reuse are not as prevalent. For instance, there are separate CsmaNetDevice
-   helpers and PointToPointNetDevice helpers but they do not derive from a
-   common NetDevice base class.
+   reuse are not as prevalent. For instance, there are separate ``CsmaNetDevice``
+   helpers and ``PointToPointNetDevice`` helpers but they do not derive from a
+   common ``NetDevice`` base class.
 #. The helper API typically works with stack-allocated (vs. heap-allocated)
    objects. For some programs, |ns3| users may not need to worry about any low
    level Object Create or Ptr handling; they can make do with containers of
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/new-models.rst ns-3.47/doc/manual/source/new-models.rst
--- ns-3.46/doc/manual/source/new-models.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/new-models.rst	2026-02-16 16:08:49.525229700 -0800
@@ -398,7 +398,7 @@
         extern template class CounterCalculator<uint32_t>;
     }
     //.cc file
-    #include <ns3/.h file>
+    #include "ns3/header-file-name.h"
     namespace ns3
     {
         NS_OBJECT_TEMPLATE_CLASS_DEFINE (CounterCalculator, uint32_t);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/object-model.rst ns-3.47/doc/manual/source/object-model.rst
--- ns-3.46/doc/manual/source/object-model.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/object-model.rst	2026-02-16 16:08:49.525229700 -0800
@@ -150,6 +150,109 @@
 counting (e.g. :cpp:class:`Packet`), and use ``CreateObject<B>`` if B derives
 from :cpp:class:`ns3::Object`.
 
+Reference counting: SimpleRefCount and Ptr versus std::shared_ptr
+*****************************************************************
+
+The |ns3| reference counting smart pointer class :cpp:class:`Ptr` and the base
+class :cpp:class:`SimpleRefCount` were designed and implemented before the C++
+standard library introduced :cpp:class:`std::shared_ptr` (in C++11). For new
+code that creates C++ objects to be managed by a shared pointer, authors must
+use `ns3::Ptr` for any class that derives from `ns3::Object`, for pointers to
+`ns3::Packet`, and for any object that is passed between nodes (including
+configuration objects) or between an `ns3::Node` and an `ns3::Channel`. The
+requirements about using `Ptr<>` when leaving a node's scope are to support
+distributed execution, where the `Ptr<>` enables us to hide the fact that the
+object may be remote and to handle it appropriately. In other situations
+internal to the workings of a model, authors are free to use either the
+traditional |ns3| approach (:cpp:class:`SimpleRefCount` and :cpp:class:`Ptr`),
+or the standard C++ approach (:cpp:class:`std::shared_ptr` and
+:cpp:func:`std::make_shared`).
+
+Both approaches provide similar functionality and have their merits. The
+:cpp:class:`Ptr` class is frequently used in |ns3| and is needed for objects
+that derive from :cpp:class:`Object`, while :cpp:class:`std::shared_ptr` is
+now familiar to C++ developers and is part of the standard library.
+
+Example using std::shared_ptr
++++++++++++++++++++++++++++++
+
+Here is a simple example of declaring a plain C++ struct and managing it with
+:cpp:class:`std::shared_ptr`::
+
+    #include <memory>
+
+    struct ExampleStruct
+    {
+        int m_value;
+        ExampleStruct(int value) : m_value(value) {}
+    };
+
+    struct Container
+    {
+        std::vector<std::shared_ptr<ExampleStruct>> m_items;
+
+        void Add(std::shared_ptr<ExampleStruct> item)
+        {
+            m_items.push_back(item);
+        }
+    };
+
+    // Create an object on the heap using std::make_shared
+    auto example = std::make_shared<ExampleStruct>(42); // The type of example is deduced to std::shared_ptr<ExampleStruct>
+
+    // Use the object
+    std::cout << example->m_value << std::endl;
+
+    // Add the object to a container, creating shared ownership
+    Container container;
+    container.Add(example);
+
+    // If the shared pointer goes out of scope or is nullified (``nullptr``), the object is not deleted.
+    // The object is only deleted when both example and container.m_items go out of scope.
+
+Example using SimpleRefCount and Ptr
++++++++++++++++++++++++++++++++++++++
+
+Here is the equivalent example using |ns3|'s :cpp:class:`SimpleRefCount` and
+:cpp:class:`Ptr`::
+
+    #include "ns3/simple-ref-count.h"
+    #include "ns3/ptr.h"
+
+    class ExampleStruct : public SimpleRefCount<ExampleStruct>
+    {
+    public:
+        int m_value;
+        ExampleStruct(int value) : m_value(value) {}
+    };
+
+    struct Container
+    {
+        std::vector<Ptr<ExampleStruct>> m_items;
+
+        void Add(Ptr<ExampleStruct> item)
+        {
+            m_items.push_back(item);
+        }
+    };
+
+    // Create an object on the heap using Create
+    auto example = Create<ExampleStruct>(42); // The type of example is deduced to Ptr<ExampleStruct>
+
+    // Use the object
+    std::cout << example->m_value << std::endl;
+
+    // Add the object to a container, creating shared ownership
+    Container container;
+    container.Add(example);
+
+    // If the shared pointer goes out of scope or is nullified (``nullptr``), the object is not deleted.
+    // The object is only deleted when both example and container.m_items go out of scope.
+
+Both approaches achieve the same result: automatic memory management with
+reference counting and safe pointer semantics. Choose the approach that best fits
+your code's context and integration with the rest of the |ns3| codebase.
+
 Aggregation
 ***********
 
@@ -252,7 +355,7 @@
     void Set(std::string name, const AttributeValue &value);
     Ptr<T> Create() const;
 
-The first method allows one to use the |ns3| TypeId system to specify the type
+The first method allows one to use the |ns3| :cpp:class:`TypeId` system to specify the type
 of objects created. The second allows one to set attributes on the objects to be
 created, and the third allows one to create the objects themselves.
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/python.rst ns-3.47/doc/manual/source/python.rst
--- ns-3.46/doc/manual/source/python.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/python.rst	2026-02-16 16:08:49.526229600 -0800
@@ -97,8 +97,9 @@
 
    ~$ python3 -m venv myEnv
 
-Note: if you don't have it installed, you will need to install it either
-either via the system package manager, or via pip itself.
+Note: if you don't have venv installed, you will need to install it
+either via the system package manager (e.g. ``apt install python3-venv``),
+or via pip itself (e.g. ``pip install venv``).
 
 After creating the venv, it is necessary to activate it, to properly
 set environment variables that will treat it as the active python installation.
@@ -119,6 +120,105 @@
 passed to CMake for proper configuration and used by the ns-3 script to
 execute scripts. This includes python scripts called by C++ programs.
 
+Important: Some pip packages can fail to install (for example, due to missing dependencies), as shown below.
+Their dependencies can typically be installed via the system package manager (apt, pacman, apk, etc).
+
+It is also not uncommon for frequently used pip packages, such as ``pygraphviz``, to be distributed
+along with their dependencies via the system package manager (for example, ``python3-pygraphviz``
+in Debian-based distros).
+
+However, a previously created venv will not detect pip packages installed via the package manager,
+unless it is recreated with the ``--system-site-packages flag``, as shown below. The drawback of this
+approach is that all system packages will be exposed, which may lead to version conflicts with packages
+you intend to use. If you use the ``pip freeze`` command to generate the ``requirements.txt`` list of dependencies
+for your environment, unrelated system packages will also be included.
+
+.. sourcecode:: console
+
+   (myEnv) ~$ pip install pygraphviz
+   Collecting pygraphviz
+     Using cached pygraphviz-1.14.tar.gz (106 kB)
+     Installing build dependencies ... done
+     Getting requirements to build wheel ... done
+     Preparing metadata (pyproject.toml) ... done
+   Building wheels for collected packages: pygraphviz
+     Building wheel for pygraphviz (pyproject.toml) ... error
+     error: subprocess-exited-with-error
+
+     | Building wheel for pygraphviz (pyproject.toml) did not run successfully.
+     | exit code: 1
+     --> running bdist_wheel
+         running build
+         running build_py
+         creating build/lib.linux-x86_64-cpython-313/pygraphviz
+         copying pygraphviz/testing.py -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         copying pygraphviz/scraper.py -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         copying pygraphviz/graphviz.py -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         copying pygraphviz/agraph.py -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         copying pygraphviz/__init__.py -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         creating build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_unicode.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_subgraph.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_string.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_scraper.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_repr_mimebundle.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_readwrite.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_node_attributes.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_layout.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_html.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_graph.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_edge_attributes.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_drawing.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_close.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_clear.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/test_attribute_defaults.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         copying pygraphviz/tests/__init__.py -> build/lib.linux-x86_64-cpython-313/pygraphviz/tests
+         running egg_info
+         writing pygraphviz.egg-info/PKG-INFO
+         writing dependency_links to pygraphviz.egg-info/dependency_links.txt
+         writing top-level names to pygraphviz.egg-info/top_level.txt
+         reading manifest file 'pygraphviz.egg-info/SOURCES.txt'
+         reading manifest template 'MANIFEST.in'
+         warning: no files found matching '*.swg'
+         warning: no files found matching '*.png' under directory 'doc'
+         warning: no files found matching '*.html' under directory 'doc'
+         warning: no files found matching '*.txt' under directory 'doc'
+         warning: no files found matching '*.css' under directory 'doc'
+         warning: no previously-included files matching '*~' found anywhere in distribution
+         warning: no previously-included files matching '*.pyc' found anywhere in distribution
+         warning: no previously-included files matching '.svn' found anywhere in distribution
+         no previously-included directories found matching 'doc/build'
+         adding license file 'LICENSE'
+         writing manifest file 'pygraphviz.egg-info/SOURCES.txt'
+         copying pygraphviz/graphviz.i -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         copying pygraphviz/graphviz_wrap.c -> build/lib.linux-x86_64-cpython-313/pygraphviz
+         running build_ext
+         building 'pygraphviz._graphviz' extension
+         creating build/temp.linux-x86_64-cpython-313/pygraphviz
+         x86_64-linux-gnu-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/home/gabriel/ns-3-maintainer/ns3env/include -I/usr/include/python3.13 -c pygraphviz/graphviz_wrap.c -o build/temp.linux-x86_64-cpython-313/pygraphviz/graphviz_wrap.o
+         pygraphviz/graphviz_wrap.c:9:9: warning: "SWIG_PYTHON_STRICT_BYTE_CHAR" redefined
+             9 | #define SWIG_PYTHON_STRICT_BYTE_CHAR
+               |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
+         <command-line>: note: this is the location of the previous definition
+         pygraphviz/graphviz_wrap.c:3023:10: fatal error: graphviz/cgraph.h: No such file or directory
+          3023 | #include "graphviz/cgraph.h"
+               |          ^~~~~~~~~~~~~~~~~~~
+         compilation terminated.
+         error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1
+         [end of output]
+
+     note: This error originates from a subprocess, and is likely not a problem with pip.
+     ERROR: Failed building wheel for pygraphviz
+   Failed to build pygraphviz
+   ERROR: Failed to build installable wheels for some pyproject.toml based projects (pygraphviz)
+   (myEnv) ~$ deactivate
+   ~$ rm -R myEnv
+   ~$ apt install python3-pygraphviz
+   ~$ python3 -m venv --system-site-packages myEnv
+   ~$ source ~/myEnv/bin/activate
+   (myEnv) ~$ pip freeze | grep pygraphviz
+   pygraphviz==1.14
+
 An Example Python Script that Runs |ns3|
 ****************************************
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/test-framework.rst ns-3.47/doc/manual/source/test-framework.rst
--- ns-3.46/doc/manual/source/test-framework.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/test-framework.rst	2026-02-16 16:08:49.526229600 -0800
@@ -155,7 +155,7 @@
 
   $ ./test.py --html=nightly.html
 
-In this case, an HTML file named ''nightly.html'' would be created with a pretty
+In this case, an HTML file named ``nightly.html`` would be created with a pretty
 summary of the testing done.  A ''human readable'' format is available for users
 interested in the details.
 
@@ -409,12 +409,12 @@
 report the result of the attempt.
 
 When ``test.py`` runs, by default it will first ensure that the system has
-been completely built.  This can be defeated by selecting the ``--nobuild``
+been completely built.  This can be defeated by selecting the ``--no-build``
 option.
 
 ::
 
-  $ ./test.py --list --nobuild
+  $ ./test.py --list --no-build
 
 will result in a list of the currently built test suites being displayed, similar to:
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/tracing.rst ns-3.47/doc/manual/source/tracing.rst
--- ns-3.46/doc/manual/source/tracing.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/tracing.rst	2026-02-16 16:08:49.527229500 -0800
@@ -270,7 +270,7 @@
 indicates a ``GetObject`` call should be made looking for the type that follows.
 When a node is initialized by an ``InternetStackHelper`` a number of interfaces
 are aggregated to the node. One of these is the TCP level four protocol. The
-runtime type of this protocol object is ``ns3::TcpL4Protocol''. When the
+runtime type of this protocol object is ``ns3::TcpL4Protocol``. When the
 ``GetObject`` is executed, it returns a pointer to the object of this type.
 
 The ``TcpL4Protocol`` class defines an Attribute called "SocketList" which is a
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/working-with-cmake.rst ns-3.47/doc/manual/source/working-with-cmake.rst
--- ns-3.46/doc/manual/source/working-with-cmake.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/working-with-cmake.rst	2026-02-16 16:08:49.527229500 -0800
@@ -628,7 +628,7 @@
    # a.k.a. the module, its examples and tests will have the definitions,
    # compilation options and will be linked to the specified libraries
    add_compile_options(-fopenmp) # CXXFLAGS counterpart
-   include_directories(/usr/local/include/e2sim) # CXXFLAGS -I counterpart
+   include_directories(SYSTEM /usr/local/include/e2sim) # CXXFLAGS -I counterpart
    add_definitions(-DLAPACK -DLUSOLVER=LAPACK) # CXXDEFINES counterpart
    link_directories(/usr/local/src/GoToBLAS2 src/common) # LINKFLAGS -L counterpart
    link_libraries(lapack blas thyme e2sim) # LINKFLAGS -l or LIB counterpart
@@ -830,7 +830,7 @@
 
         # This is the equivalent of -I/optional/search/path/to/custom/sqlite3/include
         # and AFFECTS ALL the targets in the CURRENT DIRECTORY and ITS SUBDIRECTORIES
-        include_directories(${SQLite3_INCLUDE_DIRS})
+        include_directories(SYSTEM ${SQLite3_INCLUDE_DIRS})
 
         # The compiler should be able to locate the headers, but it still needs to be
         # informed of the libraries that should be linked
@@ -1225,7 +1225,7 @@
     # This will make the following work:
     #  include<openflow/openflow.h>
     #  include<openflow.h>
-    include_directories(${openflow_INCLUDE_DIRS})
+    include_directories(SYSTEM ${openflow_INCLUDE_DIRS})
 
     # Manually set definitions
     add_definitions(
@@ -1304,7 +1304,7 @@
     #
     # If calling this from the top-level directory (ns-3-dev), it will
     # be used by all contrib/src modules, examples, etc
-    include_directories(${Boost_INCLUDE_DIRS})
+    include_directories(SYSTEM ${Boost_INCLUDE_DIRS})
 
     # This is a trick for Boost
     # Sometimes you want to check if specific Boost headers are available,
@@ -1455,7 +1455,7 @@
           model/sqlite-data-output.h
       )
       # Include the include directories containing the sqlite3.h header
-      include_directories(${SQLite3_INCLUDE_DIRS})
+      include_directories(SYSTEM ${SQLite3_INCLUDE_DIRS})
       # Copy the list of sqlite3 libraries
       set(sqlite_libraries
           ${SQLite3_LIBRARIES}
@@ -1534,7 +1534,7 @@
     if(PKG_CONFIG_FOUND AND THIRD_PARTY)
       # Include third-party include directories for
       # consumption of the current module and its examples
-      include_directories(${THIRD_PARTY_INCLUDE_DIRS})
+      include_directories(SYSTEM ${THIRD_PARTY_INCLUDE_DIRS})
 
       # Use exported CFLAGS required by the third-party library
       add_compile_options(${THIRD_PARTY_CFLAGS})
@@ -1661,7 +1661,7 @@
     message(STATUS "Armadillo was found? ${ARMADILLO_FOUND}")
 
     # Include and link Armadillo to targets
-    include_directories(${ARMADILLO_INCLUDE_DIRS})
+    include_directories(SYSTEM ${ARMADILLO_INCLUDE_DIRS})
     link_libraries(${ARMADILLO_LIBRARIES})
 
 .. _vcpkg manifests: https://learn.microsoft.com/en-us/vcpkg/users/manifests
@@ -2066,7 +2066,7 @@
     # CPM is kind of jenky. It could get the ARMADILLO_INCLUDE_DIRS
     # from the ArmadilloConfig.cmake file in ${CMAKE_BINARY_DIR}/_deps/armadillo-build,
     # but it doesn't... So add its include directories directly from the source directory
-    include_directories(${CMAKE_BINARY_DIR}/_deps/armadillo-src/include)
+    include_directories(SYSTEM ${CMAKE_BINARY_DIR}/_deps/armadillo-src/include)
 
     # Link to Armadillo and
     link_libraries(armadillo)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/manual/source/working-with-docker.rst ns-3.47/doc/manual/source/working-with-docker.rst
--- ns-3.46/doc/manual/source/working-with-docker.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/manual/source/working-with-docker.rst	2026-02-16 16:08:49.527229500 -0800
@@ -383,6 +383,35 @@
     /usr/bin/cmake3 -S /ns-3-dev -B /ns-3-dev/cmake-cache -DCMAKE_BUILD_TYPE=default -DNS3_ASSERT=ON -DNS3_LOG=ON -DNS3_WARNINGS_AS_ERRORS=OFF -DNS3_NATIVE_OPTIMIZATIONS=OFF -G Ninja -
     -warn-uninitialized
 
+In case SELinux is enabled, in additional to the mapped volume,
+you also need to add a ``:z`` suffix to explicitly give permissions
+for the container to access the files in the volume, as shown below.
+
+.. sourcecode:: console
+
+    $ docker run -it -v ./ns-3-dev:/ns-3-dev:z fedoradebugging:37
+
+    [root@067a8b748816 /]# cd ns-3-dev/
+
+    [root@067a8b748816 ns-3-dev]# ./ns3 configure
+    Warn about uninitialized values.
+    -- The CXX compiler identification is GNU 12.3.1
+    ...
+    -- ---- Summary of ns-3 settings:
+    Build profile                 : default
+    Build directory               : /ns-3-dev/build
+    Build with runtime asserts    : ON
+    Build with runtime logging    : ON
+    Build version embedding       : OFF (not requested)
+    ...
+    -- Configuring done (26.5s)
+    -- Generating done (11.6s)
+    -- Build files have been written to: /ns-3-dev/cmake-cache
+    Finished executing the following commands:
+    /usr/bin/cmake3 -S /ns-3-dev -B /ns-3-dev/cmake-cache -DCMAKE_BUILD_TYPE=default -DNS3_ASSERT=ON -DNS3_LOG=ON -DNS3_WARNINGS_AS_ERRORS=OFF -DNS3_NATIVE_OPTIMIZATIONS=OFF -G Ninja -
+    -warn-uninitialized
+
+
 The container will be automatically stopped after exiting.
 
 .. sourcecode:: console
@@ -572,6 +601,13 @@
 
     echo alias docker=podman >> ~/.bashrc
 
+You can alternatively create a symbolic link, in case your program
+does not load definitions from bashrc.
+
+.. sourcecode:: console
+
+    ln -sf /usr/bin/podman /home/username/.local/bin/docker
+
 To get a docker-like experience, you might want to
 also change a few settings, such as search repositories for unqualified
 image names (without the prepended repository/registry).
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/models/Makefile ns-3.47/doc/models/Makefile
--- ns-3.46/doc/models/Makefile	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/models/Makefile	2026-02-16 16:08:49.528229500 -0800
@@ -19,6 +19,10 @@
 	$(SRC)/antenna/doc/antenna.rst \
 	$(SRC)/aodv/doc/aodv.rst \
 	$(SRC)/applications/doc/applications.rst \
+        $(SRC)/applications/doc/rta-tig-mobile-gaming-traffic.inc \
+        $(SRC)/applications/doc/tgax-video-traffic.inc \
+        $(SRC)/applications/doc/tgax-virtual-desktop-traffic.inc \
+        $(SRC)/applications/doc/tgax-voip-traffic.inc \
 	$(SRC)/bridge/doc/bridge.rst \
 	$(SRC)/brite/doc/brite.rst \
 	$(SRC)/buildings/doc/source/buildings.rst \
@@ -60,6 +64,7 @@
 	$(SRC)/internet/doc/ipv4.rst \
 	$(SRC)/internet/doc/ipv6.rst \
 	$(SRC)/internet/doc/routing-overview.rst \
+	$(SRC)/internet/doc/global-routing.rst \
 	$(SRC)/internet/doc/tcp.rst \
 	$(SRC)/internet/doc/udp.rst \
 	$(SRC)/internet-apps/doc/internet-apps.rst \
@@ -125,6 +130,10 @@
 	$(SRC)/internet/doc/routing-specialization.dia \
 	$(SRC)/internet/doc/global-routing-limitation.dia \
 	$(SRC)/internet/doc/figures/tcp-state-machine.png \
+	$(SRC)/internet/doc/figures/fack-cwnd.png \
+	$(SRC)/internet/doc/figures/fack-inflight.png \
+	$(SRC)/internet/doc/figures/fack-packetDrops.png \
+	$(SRC)/internet/doc/figures/fack-throughput.png\
 	$(SRC)/internet/doc/figures/dctcp-10ms-50mbps-tcp-throughput.pdf \
 	$(SRC)/internet/doc/figures/dctcp-10ms-50mbps-tcp-throughput.png \
 	$(SRC)/internet/doc/figures/dctcp-80ms-50mbps-tcp-throughput.pdf \
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/models/source/conf.py ns-3.47/doc/models/source/conf.py
--- ns-3.46/doc/models/source/conf.py	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/doc/models/source/conf.py	2026-02-16 16:16:25.803885200 -0800
@@ -74,9 +74,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = "ns-3.46"
+version = "ns-3.47"
 # The full version, including alpha/beta/rc tags.
-release = "ns-3.46"
+release = "ns-3.47"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/models/source/internet-models.rst ns-3.47/doc/models/source/internet-models.rst
--- ns-3.46/doc/models/source/internet-models.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/models/source/internet-models.rst	2026-02-16 16:08:49.528229500 -0800
@@ -7,5 +7,6 @@
    ipv4
    ipv6
    routing-overview
+   global-routing
    tcp
    udp
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/ns3_html_theme/get_version.sh ns-3.47/doc/ns3_html_theme/get_version.sh
--- ns-3.46/doc/ns3_html_theme/get_version.sh	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/ns3_html_theme/get_version.sh	2026-02-16 16:08:49.528229500 -0800
@@ -48,17 +48,38 @@
 #
 
 me=`basename $0`
+
+# Controls
+
+# Build public URLs
+public=0
+# Build URLs for nsnam.org
+nsnam=0
+# Build for nsnam automated build directory
+daily=0
+# Build at a tag
+tag=0
+# Show progress details and final JS variables
+verbose=0
+
+
 function say
 {
     echo "$me: $*"
 }
 
+function verb
+{
+    [[ $verbose -eq 1 ]] && say $* || :
+}
+
 function usage
 {
     cat <<-EOF
-    Usage:  $me [-p]              normal versioning
+    Usage:  $me [-pv]             normal versioning
             $me [-n] [-d] [-t]    test options
       -p  build public urls, NS3_WWW_URLS=public is an alternative
+      -v  verbose: show output javascript variables
 
     Testing options:
       -n  pretend we are on nsnam.org
@@ -69,15 +90,7 @@
     exit 1
 }
 
-# script arguments
-say
-say using doxygen: $(which doxygen) $(doxygen --version)
-public=0
-nsnam=0
-daily=0
-tag=0
-
-while getopts :pndth option ; do
+while getopts :pndtvh option ; do
     case $option in
     (p)  public=1 ;;
     (n)  nsnam=1  ;;
@@ -86,12 +99,18 @@
 
     (t)  tag=1    ;;
 
+    (v)  verbose=1 ;;
+
     (h)  usage ;;
     (:)  say "Missing argument to -$OPTARG" ; usage ;;
     (\?) say "Invalid option: -$OPTARG"     ; usage ;;
     esac
 done
 
+# script arguments
+verb
+verb using doxygen: $(which doxygen) $(doxygen --version)
+
 # Hostname, fully qualified, e.g. nsnam-www.coe-hosted.gatech.edu
 HOST=`hostname`
 NSNAM="nsnam-www.coe-hosted.gatech.edu"
@@ -101,18 +120,18 @@
 
 if [ $nsnam -eq 1 ]; then
     HOST=$NSNAM
-    say "-n forcing HOST = $HOST"
+    verb "-n forcing HOST = $HOST"
 fi
 
 if [ $daily -eq 1 ] ; then
     OLDPWD=$PWD
     PWD=/tmp/daily-nsnam/foo
-    say "-d forcing PWD = $PWD"
+    verb "-d forcing PWD = $PWD"
 fi
 
 if [ $tag -eq 1 ]; then
     version="ns-3.14"
-    say "-t forcing tagged version = $version"
+    verb "-t forcing tagged version = $version"
 fi
 
 if  ((nsnam + daily + tag > 0)) ; then
@@ -123,10 +142,10 @@
     ("${NS3_WWW_URLS:-}" == "public") || \
     ( ($HOST == $NSNAM) && ($PWD =~ $DAILY) ) ]] ; then
     PUBLIC=1
-    say "building public docs for nsnam.org"
+    verb "building public docs for nsnam.org"
 else
     PUBLIC=0
-    say "building private docs"
+    verb "building private docs"
 fi
 
 if [ $daily -eq 1 ]; then
@@ -144,7 +163,7 @@
 
 if [ $distance -eq 1 ] | [ $distance -eq 0 ]; then
     version=`git describe HEAD --tags --long | cut -d '-' -f 2`
-    say "at version $version"
+    verb "at version $version"
 
 elif [ $tag -eq 1 ]; then
     distance=1
@@ -214,7 +233,7 @@
 cd - 2>&1 >/dev/null
 
 # Show what was done
-say
-say "outf = $outf:"
-cat -n $outf
-say Done.
+verb
+verb "outf = $outf:"
+[[ $verbose -eq 1 ]] && cat -n $outf
+verb Done.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/tutorial/source/building-topologies.rst ns-3.47/doc/tutorial/source/building-topologies.rst
--- ns-3.46/doc/tutorial/source/building-topologies.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/tutorial/source/building-topologies.rst	2026-02-16 16:08:49.556229600 -0800
@@ -1018,8 +1018,8 @@
               "ActiveProbing", BooleanValue(false));
 
 In the above code, the specific kind of MAC layer that
-will be created by the helper is specified by the TypeId value
-of `ns3::StaWifiMac` type.  The "QosSupported" attribute is
+will be created by the helper is specified by the ``TypeId`` value
+of ``ns3::StaWifiMac`` type.  The "QosSupported" attribute is
 set to true by default for ``WifiMacHelper`` objects when the standard
 is at least 802.11n or newer. The combination
 of these two configurations means that the MAC instance next created
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/tutorial/source/conf.py ns-3.47/doc/tutorial/source/conf.py
--- ns-3.46/doc/tutorial/source/conf.py	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/doc/tutorial/source/conf.py	2026-02-16 16:16:25.803885200 -0800
@@ -71,9 +71,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = "ns-3.46"
+version = "ns-3.47"
 # The full version, including alpha/beta/rc tags.
-release = "ns-3.46"
+release = "ns-3.47"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/tutorial/source/tracing.rst ns-3.47/doc/tutorial/source/tracing.rst
--- ns-3.46/doc/tutorial/source/tracing.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/tutorial/source/tracing.rst	2026-02-16 16:08:49.557229800 -0800
@@ -804,7 +804,7 @@
   * **CourseChange**: The value of the position and/or velocity vector
     changed.
 
-    Callback signature: ``ns3::MobilityModel::CourseChangeCallback``
+    Callback signature: ``ns3::MobilityModel::TracedCallback``
 
 This is exactly what you need to know.  The trace source of interest
 is found in ``ns3::MobilityModel`` (which you knew anyway).  The
@@ -855,12 +855,12 @@
   * **CourseChange**: The value of the position and/or velocity vector
     changed.
 
-    Callback signature: ``ns3::MobilityModel::CourseChangeCallback``
+    Callback signature: ``ns3::MobilityModel::TracedCallback``
 
 The callback signature is given as a link to the relevant ``typedef``,
 where we find
 
-  ``typedef void (* CourseChangeCallback)(std::string context, Ptr<const MobilityModel> * model);``
+  ``typedef void (*TracedCallback)(Ptr<const MobilityModel> model);``
 
   **TracedCallback** signature for course change notifications.
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/doc/tutorial/source/tweaking.rst ns-3.47/doc/tutorial/source/tweaking.rst
--- ns-3.46/doc/tutorial/source/tweaking.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/doc/tutorial/source/tweaking.rst	2026-02-16 16:08:49.557229800 -0800
@@ -42,8 +42,6 @@
 
 * LOG_ERROR --- Log error messages (associated macro: NS_LOG_ERROR);
 * LOG_WARN --- Log warning messages (associated macro: NS_LOG_WARN);
-* LOG_DEBUG --- Log relatively rare, ad-hoc debugging messages
-  (associated macro: NS_LOG_DEBUG);
 * LOG_INFO --- Log informational messages about program progress
   (associated macro: NS_LOG_INFO);
 * LOG_FUNCTION --- Log a message describing each function called
@@ -51,10 +49,12 @@
   and NS_LOG_FUNCTION_NOARGS, used for static functions);
 * LOG_LOGIC -- Log messages describing logical flow within a function
   (associated macro: NS_LOG_LOGIC);
+* LOG_DEBUG --- Log relatively rare, ad-hoc debugging messages
+  (associated macro: NS_LOG_DEBUG);
 * LOG_ALL --- Log everything mentioned above (no associated macro).
 
 For each LOG_TYPE there is also LOG_LEVEL_TYPE that, if used, enables
-logging of all the levels above it in addition to it's level.  (As a
+logging of all the levels above it in addition to its level (see list above).  (As a
 consequence of this, LOG_ERROR and LOG_LEVEL_ERROR and also LOG_ALL
 and LOG_LEVEL_ALL are functionally equivalent.)  For example,
 enabling LOG_INFO will only enable messages provided by NS_LOG_INFO macro,
@@ -643,7 +643,7 @@
     --PrintHelp:                 Print this help message.
 
 If you select the "PrintGroups" argument, you should see a list of all
-registered TypeId groups.  The group names are aligned with the module names
+registered ``TypeId`` groups.  The group names are aligned with the module names
 in the source directory (although with a leading capital letter).  Printing
 out all of the information at once would be too much, so a further filter
 is available to print information on a per-group basis.   So, focusing
@@ -657,7 +657,7 @@
     ns3::PointToPointNetDevice
     ns3::PppHeader
 
-and from here, one can find the possible TypeId names to search for
+and from here, one can find the possible ``TypeId`` names to search for
 attributes, such as in the ``--PrintAttributes=ns3::PointToPointChannel``
 example shown above.
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/channel-models/examples-to-run.py ns-3.47/examples/channel-models/examples-to-run.py
--- ns-3.46/examples/channel-models/examples-to-run.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/channel-models/examples-to-run.py	2026-02-16 16:08:49.557229800 -0800
@@ -8,6 +8,11 @@
 # See test.py for more information.
 cpp_examples = [
     ("three-gpp-v2v-channel-example", "True", "True"),
+    (
+        "three-gpp-v2v-channel-example --updatePeriodMs=1",
+        "True",
+        "True",
+    ),
 ]
 
 # A list of Python examples to run in order to ensure that they remain
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/channel-models/three-gpp-v2v-channel-example.cc ns-3.47/examples/channel-models/three-gpp-v2v-channel-example.cc
--- ns-3.46/examples/channel-models/three-gpp-v2v-channel-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/channel-models/three-gpp-v2v-channel-example.cc	2026-02-16 16:08:49.557229800 -0800
@@ -6,20 +6,21 @@
  */
 
 /*
- * This is an example on how to configure the channel model classes to simulate
+ * @file
+ * This is an example of how to configure the channel model classes to simulate
  * a vehicular environment.
  * The channel condition is determined using the model specified in [1], Table 6.2-1.
  * The pathloss is determined using the model specified in [1], Table 6.2.1-1.
  * The model for the fast fading is the one described in 3GPP TR 38.901 v15.0.0,
  * the model parameters are those specified in [1], Table 6.2.3-1.
  *
- * This example generates the output file 'example-output.txt'. Each row of the
- * file is organized as follows:
- * Time[s] TxPosX[m] TxPosY[m] RxPosX[m] RxPosY[m] ChannelState SNR[dB] Pathloss[dB]
- * We also provide a bash script which reads the output file and generates two
+ * This example generates the output file 'three-gpp-v2v-channel-example-output.txt'. Each row of
+ * the file is organized as follows: Time[s] TxPosX[m] TxPosY[m] RxPosX[m] RxPosY[m] ChannelState
+ * SNR[dB] Pathloss[dB] We also provide a bash script which reads the output file and generates two
  * figures:
- * (i) map.gif, a GIF representing the simulation scenario and vehicle mobility;
- * (ii) snr.png, which represents the behavior of the SNR.
+ * (i) three-gpp-v2v-channel-example-map.gif, a GIF representing the simulation scenario and vehicle
+ * mobility;
+ * (ii) three-gpp-v2v-channel-example-snr.png, which represents the behavior of the SNR.
  *
  * [1] 3GPP TR 37.885, v15.3.0
  */
@@ -64,31 +65,22 @@
 
 /**
  * Perform the beamforming using the DFT beamforming method
- * @param thisDevice the device performing the beamforming
+ * @param txMob the mobility model of the node performing the beamforming
  * @param thisAntenna the antenna object associated to thisDevice
- * @param otherDevice the device towards which point the beam
+ * @param rxMob the mobility model of the node towards which will point the beam
  */
 static void
-DoBeamforming(Ptr<NetDevice> thisDevice,
-              Ptr<PhasedArrayModel> thisAntenna,
-              Ptr<NetDevice> otherDevice)
+DoBeamforming(Ptr<MobilityModel> txMob, Ptr<PhasedArrayModel> thisAntenna, Ptr<MobilityModel> rxMob)
 {
-    PhasedArrayModel::ComplexVector antennaWeights;
-
-    // retrieve the position of the two devices
-    Vector aPos = thisDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
-    Vector bPos = otherDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
-
     // compute the azimuth and the elevation angles
-    Angles completeAngle(bPos, aPos);
-
+    Angles completeAngle(rxMob->GetPosition(), txMob->GetPosition());
     PhasedArrayModel::ComplexVector bf = thisAntenna->GetBeamformingVector(completeAngle);
     thisAntenna->SetBeamformingVector(bf);
 }
 
 /**
  * Compute the average SNR
- * @param params A structure that holds a bunch of parameters needed by ComputSnr function to
+ * @param params A structure that holds a bunch of parameters needed by ComputeSnr function to
  * calculate the average SNR
  */
 static void
@@ -96,13 +88,11 @@
 {
     // check the channel condition
     Ptr<ChannelCondition> cond = m_condModel->GetChannelCondition(params.txMob, params.rxMob);
-
     // apply the pathloss
     double propagationGainDb = m_propagationLossModel->CalcRxPower(0, params.txMob, params.rxMob);
     NS_LOG_DEBUG("Pathloss " << -propagationGainDb << " dB");
     double propagationGainLinear = std::pow(10.0, (propagationGainDb) / 10.0);
     *(params.txParams->psd) *= propagationGainLinear;
-
     // apply the fast fading and the beamforming gain
     auto rxParams = m_spectrumLossModel->CalcRxPowerSpectralDensity(params.txParams,
                                                                     params.txMob,
@@ -120,13 +110,11 @@
     double noisePowerSpectralDensity = kT_W_Hz * noiseFigureLinear;
     Ptr<SpectrumValue> noisePsd = Create<SpectrumValue>(params.txParams->psd->GetSpectrumModel());
     (*noisePsd) = noisePowerSpectralDensity;
-
-    // compute the SNR
+    // log the average SNR
     NS_LOG_DEBUG("Average SNR " << 10 * log10(Sum(*rxPsd) / Sum(*noisePsd)) << " dB");
-
-    // print the SNR and pathloss values in the snr-trace.txt file
+    // print the SNR and pathloss values in the three-gpp-v2v-channel-example-output.txt file
     std::ofstream f;
-    f.open("example-output.txt", std::ios::out | std::ios::app);
+    f.open("three-gpp-v2v-channel-example-output.txt", std::ios::out | std::ios::app);
     f << Simulator::Now().GetSeconds() << " " // time [s]
       << params.txMob->GetPosition().x << " " << params.txMob->GetPosition().y << " "
       << params.rxMob->GetPosition().x << " " << params.rxMob->GetPosition().y << " "
@@ -170,28 +158,20 @@
     double vScatt = 0;                  // maximum speed of the vehicles in the scenario [m/s]
     double subCarrierSpacing = 60e3;    // subcarrier spacing in kHz
     uint32_t numRb = 275;               // number of resource blocks
+    uint32_t updatePeriodMs = 0;
 
     CommandLine cmd(__FILE__);
     cmd.AddValue("frequency", "operating frequency in Hz", frequency);
     cmd.AddValue("txPow", "tx power in dBm", txPow_dbm);
     cmd.AddValue("noiseFigure", "noise figure in dB", noiseFigure);
     cmd.AddValue("scenario", "3GPP propagation scenario, V2V-Urban or V2V-Highway", scenario);
+    cmd.AddValue("updatePeriodMs", "the channel update period", updatePeriodMs);
     cmd.Parse(argc, argv);
 
     // create the nodes
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the antenna objects and set their dimensions
     Ptr<PhasedArrayModel> txAntenna =
         CreateObjectWithAttributes<UniformPlanarArray>("NumColumns",
@@ -321,6 +301,7 @@
     channelModel->SetAttribute("Frequency", DoubleValue(frequency));
     channelModel->SetAttribute("ChannelConditionModel", PointerValue(m_condModel));
     channelModel->SetAttribute("vScatt", DoubleValue(vScatt));
+    channelModel->SetAttribute("UpdatePeriod", TimeValue(MilliSeconds(updatePeriodMs)));
 
     // create the spectrum propagation loss model
     m_spectrumLossModel = CreateObjectWithAttributes<ThreeGppSpectrumPropagationLossModel>(
@@ -330,8 +311,8 @@
     BuildingsHelper::Install(nodes);
 
     // set the beamforming vectors
-    DoBeamforming(txDev, txAntenna, rxDev);
-    DoBeamforming(rxDev, rxAntenna, txDev);
+    DoBeamforming(txMob, txAntenna, rxMob);
+    DoBeamforming(rxMob, rxAntenna, txMob);
 
     // create the tx power spectral density
     Bands rbs;
@@ -340,9 +321,9 @@
     {
         BandInfo rb;
         rb.fl = freqSubBand;
-        freqSubBand += subCarrierSpacing / 2;
+        freqSubBand += (subCarrierSpacing * 12) / 2;
         rb.fc = freqSubBand;
-        freqSubBand += subCarrierSpacing / 2;
+        freqSubBand += (subCarrierSpacing * 12) / 2;
         rb.fh = freqSubBand;
         rbs.push_back(rb);
     }
@@ -350,7 +331,7 @@
     Ptr<SpectrumValue> txPsd = Create<SpectrumValue>(spectrumModel);
     Ptr<SpectrumSignalParameters> txParams = Create<SpectrumSignalParameters>();
     double txPow_w = std::pow(10., (txPow_dbm - 30) / 10);
-    double txPowDens = (txPow_w / (numRb * subCarrierSpacing));
+    double txPowDens = (txPow_w / (numRb * subCarrierSpacing * 12));
     (*txPsd) = txPowDens;
     txParams->psd = txPsd->Copy();
 
@@ -362,13 +343,13 @@
 
     // initialize the output file
     std::ofstream f;
-    f.open("example-output.txt", std::ios::out);
+    f.open("three-gpp-v2v-channel-example-output.txt", std::ios::out);
     f << "Time[s] TxPosX[m] TxPosY[m] RxPosX[m] RxPosY[m] ChannelState SNR[dB] Pathloss[dB]"
       << std::endl;
     f.close();
 
     // print the list of buildings to file
-    PrintGnuplottableBuildingListToFile("buildings.txt");
+    PrintGnuplottableBuildingListToFile("three-gpp-v2v-channel-example-buildings.txt");
 
     Simulator::Run();
     Simulator::Destroy();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/channel-models/three-gpp-v2v-channel-example.py ns-3.47/examples/channel-models/three-gpp-v2v-channel-example.py
--- ns-3.46/examples/channel-models/three-gpp-v2v-channel-example.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/channel-models/three-gpp-v2v-channel-example.py	2026-02-16 16:08:49.557229800 -0800
@@ -15,7 +15,7 @@
 import pandas as pd
 
 # Load the data
-df = pd.read_csv("example-output.txt", sep=r"\s+", comment="#")
+df = pd.read_csv("three-gpp-v2v-channel-example-output.txt", sep=r"\s+", comment="#")
 df = df.iloc[::10, :]
 
 # Column indices (adjust if needed)
@@ -48,7 +48,9 @@
 ax_snr.grid(True)
 (snr_line,) = ax_snr.plot([], [], "k-")
 
-buildings = pd.read_csv("buildings.txt", sep=r"\s+", comment="#", header=None)
+buildings = pd.read_csv(
+    "three-gpp-v2v-channel-example-buildings.txt", sep=r"\s+", comment="#", header=None
+)
 building_patches = []
 for idx, row in buildings.iterrows():
     x0, y0, x1, y1 = row
@@ -73,7 +75,7 @@
 ani = animation.FuncAnimation(fig, update, frames=len(df), interval=0.001, blit=False)
 
 # Save animation
-ani.save("map.gif", writer="pillow")
+ani.save("three-gpp-v2v-channel-example-map.gif", writer="pillow")
 
 # Save final SNR plot separately
 plt.figure()
@@ -83,4 +85,4 @@
 plt.grid(True)
 plt.xlim(0, 40)
 plt.ylim(-20, 100)
-plt.savefig("snr.png")
+plt.savefig("three-gpp-v2v-channel-example-snr.png")
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/routing/global-injection-slash32.cc ns-3.47/examples/routing/global-injection-slash32.cc
--- ns-3.46/examples/routing/global-injection-slash32.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/routing/global-injection-slash32.cc	2026-02-16 16:08:49.558229700 -0800
@@ -11,9 +11,9 @@
 #include "ns3/core-module.h"
 #include "ns3/csma-net-device.h"
 #include "ns3/global-router-interface.h"
+#include "ns3/global-routing.h"
 #include "ns3/internet-module.h"
 #include "ns3/ipv4-global-routing-helper.h"
-#include "ns3/ipv4-global-routing.h"
 #include "ns3/ipv4-list-routing-helper.h"
 #include "ns3/ipv4-list-routing.h"
 #include "ns3/ipv4-routing-table-entry.h"
@@ -110,7 +110,7 @@
     // Populate routing tables for nodes nA and nB
     Ipv4GlobalRoutingHelper::PopulateRoutingTables();
     // Inject global routes from Node B, including transit network...
-    Ptr<GlobalRouter> globalRouterB = nB->GetObject<GlobalRouter>();
+    Ptr<Ipv4GlobalRouter> globalRouterB = nB->GetObject<Ipv4GlobalRouter>();
     globalRouterB->InjectRoute("10.1.1.4", "255.255.255.252");
     // ...and the host in network "C"
     globalRouterB->InjectRoute("192.168.1.1", "255.255.255.255");
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/stats/wifi-example-apps.h ns-3.47/examples/stats/wifi-example-apps.h
--- ns-3.46/examples/stats/wifi-example-apps.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/stats/wifi-example-apps.h	2026-02-16 16:08:49.559229600 -0800
@@ -13,6 +13,9 @@
  *
  */
 
+#ifndef WIFI_EXAMPLE_APPS_H
+#define WIFI_EXAMPLE_APPS_H
+
 #include "ns3/application.h"
 #include "ns3/core-module.h"
 #include "ns3/network-module.h"
@@ -115,3 +118,5 @@
     Ptr<CounterCalculator<>> m_calc;           //!< Counter of the number of received packets
     Ptr<TimeMinMaxAvgTotalCalculator> m_delay; //!< Delay calculator
 };
+
+#endif // WIFI_EXAMPLE_APPS_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/tcp/CMakeLists.txt ns-3.47/examples/tcp/CMakeLists.txt
--- ns-3.46/examples/tcp/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/tcp/CMakeLists.txt	2026-02-16 16:08:49.560229800 -0800
@@ -28,6 +28,19 @@
 )
 
 build_example(
+  NAME tcp-ecn-example
+  SOURCE_FILES tcp-ecn-example.cc
+  LIBRARIES_TO_LINK
+    ${libpoint-to-point}
+    ${libinternet}
+    ${libapplications}
+    ${libtraffic-control}
+    ${libnetwork}
+    ${libinternet-apps}
+    ${libflow-monitor}
+)
+
+build_example(
   NAME tcp-bbr-example
   SOURCE_FILES tcp-bbr-example.cc
   LIBRARIES_TO_LINK
@@ -112,3 +125,15 @@
     ${libapplications}
     ${libtraffic-control}
 )
+build_example(
+  NAME fack-example
+  SOURCE_FILES fack-example.cc
+  LIBRARIES_TO_LINK
+    ${libinternet}
+    ${libpoint-to-point}
+    ${libapplications}
+    ${libtraffic-control}
+    ${libnetwork}
+    ${libinternet-apps}
+    ${libflow-monitor}
+)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/tcp/examples-to-run.py ns-3.47/examples/tcp/examples-to-run.py
--- ns-3.46/examples/tcp/examples-to-run.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/tcp/examples-to-run.py	2026-02-16 16:08:49.560229800 -0800
@@ -31,6 +31,11 @@
         "True",
         "True",
     ),
+    (
+        "tcp-ecn-example --stopTime=20s",
+        "True",
+        "False",
+    ),
 ]
 
 # A list of Python examples to run in order to ensure that they remain
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/tcp/fack-example.cc ns-3.47/examples/tcp/fack-example.cc
--- ns-3.46/examples/tcp/fack-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/examples/tcp/fack-example.cc	2026-02-16 16:08:49.560229800 -0800
@@ -0,0 +1,427 @@
+/*
+ * Copyright (c) 2018-20, 2025 NITK Surathkal
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Authors: Aarti Nandagiri <aarti.nandagiri@gmail.com>
+ *          Vivek Jain <jain.vivek.anand@gmail.com>
+ *          Mohit P. Tahiliani <tahiliani@nitk.edu.in>
+ *
+ * Modified for FACK by: Jayesh Akot <akotjayesh@gmail.com>
+ */
+
+/**
+ * @file
+ * @ingroup examples-tcp
+ * @brief Example to verify TCP FACK performance based on Mathis & Mahdavi paper.
+ *
+ * The network topology used in this example is based on the Fig. 1 described in
+ * Mathis, M., & Mahdavi, J. (1996, August).
+ * Forward acknowledgement: Refining TCP congestion control.
+ *
+ * @code{.text}
+ *            10 Mbps           1.5 Mbps          10 Mbps
+ * Sender -------------- R1 -------------- R2 -------------- Receiver
+ *              2ms               5ms               33ms
+ * @endcode
+ *
+ * This program runs by default for 100 seconds and creates a new directory
+ * called 'fack-results' in the ns-3 root directory. The program creates one
+ * sub-directory called 'pcap' in 'fack-results' directory (if pcap generation
+ * is enabled) and six .dat files.
+ *
+ * - 'pcap' sub-directory contains six PCAP files:
+ *  - fack-0-0.pcap for the interface on Sender
+ *  - fack-1-0.pcap for the interface on Receiver
+ *  - fack-2-0.pcap for the first interface on R1
+ *  - fack-2-1.pcap for the second interface on R1
+ *  - fack-3-0.pcap for the first interface on R2
+ *  - fack-3-1.pcap for the second interface on R2
+ * - cwnd.dat file contains congestion window trace for the sender node
+ * - throughput.dat file contains sender side throughput trace (throughput is in Mbit/s)
+ * - queueSize.dat file contains queue length trace from the bottleneck link
+ * - lostSegments.dat file contains packet lost trace at queue
+ * - bytesInFlight.dat contains trace of inflight data(SND.NXT - SND.UNA).
+ * - fackAwnd.dat contains trace of the FACK-specific inflight estimate.
+ *
+ * This example allows the user to quantitatively verify two key mechanisms
+ * described in the Mathis & Mahdavi paper:
+ * -# The preservation of the TCP Self-Clock during recovery (Section 4.2).
+ * -# The "Overdamping" mechanism to drain queues after overshoot (Section 4.4).
+ *
+ * To observe these insights, you must run the simulation twice with different configurations:
+ *
+ * - **Step 1:** Run with FACK enabled (Default)
+ * @code ./ns3 run "fack-example --fack=true" @endcode
+ *
+ * - **Step 2:** Run with FACK disabled (Standard Reno+SACK behavior)
+ * @code ./ns3 run "fack-example --fack=false" @endcode
+ *
+ * - **Step 3:** Compare the output traces 'bytesInFlight.dat' and 'cwnd.dat' from both runs.
+ *
+ * **SPECIFIC OBSERVATIONS:**
+ *
+ * **A. Preservation of Self-Clock (Compare 'bytesInFlight.dat'):**
+ * - WITHOUT FACK: When multiple packet losses occur (approx t=0.5s), the
+ * inflight data drops significantly (often stalling near zero). This indicates
+ * a loss of the ACK clock, matching the behavior of "Reno+SACK" described
+ * in Section 4.2 and Figure 3 of the paper.
+ * - WITH FACK: The inflight data remains elevated even during recovery.
+ * This demonstrates that FACK successfully decouples congestion control
+ * from data recovery, using SACK blocks to inject new data and maintain
+ * the self-clock. This matches the behavior in Figure 4 of the paper.
+ *
+ * **B. Overdamping and Queue Draining (Compare 'cwnd.dat' vs 'bytesInFlight.dat'):**
+ * - In the FACK run (between t=1.0s and t=2.0s), observe that 'cwnd' drops
+ * to a low value (~10 packets) while 'bytesInFlight' remains high (~50 packets).
+ * - This divergence is the "Overdamping" mechanism (Section 4.4). FACK detects
+ * that the actual data in the network (inflight) exceeds the target window
+ * (cwnd) due to the initial Slow-Start overshoot.
+ * - Consequently, FACK inhibits transmission (visible as a throughput dip) to
+ * allow the excess queue to drain.
+ */
+#include "ns3/applications-module.h"
+#include "ns3/core-module.h"
+#include "ns3/flow-monitor-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/network-module.h"
+#include "ns3/point-to-point-module.h"
+#include "ns3/traffic-control-module.h"
+
+using namespace ns3;
+using namespace ns3::SystemPath;
+
+std::string g_dir;                      //!< Output directory
+std::ofstream g_throughput;             //!< Throughput output stream
+std::ofstream g_queueSize;              //!< Queue size output stream
+std::ofstream g_individualLostSegments; //!< Lost segments output stream
+
+uint32_t g_prev = 0;           //!< Previous throughput byte count
+Time g_prevTime;               //!< Previous throughput measurement time
+uint32_t g_segmentSize = 1448; //!< Segment size
+
+/**
+ * @brief Calculate and trace throughput
+ * @param monitor The flow monitor
+ */
+static void
+TraceThroughput(Ptr<FlowMonitor> monitor)
+{
+    FlowMonitor::FlowStatsContainer stats = monitor->GetFlowStats();
+    if (!stats.empty())
+    {
+        auto itr = stats.begin();
+        Time curTime = Now();
+        // Convert (curTime - prevTime) to microseconds so that throughput is in bits per
+        // microsecond (which is equivalent to Mbps)
+
+        g_throughput << curTime.GetSeconds() << "s "
+                     << 8 * (itr->second.txBytes - g_prev) /
+                            ((curTime - g_prevTime).ToDouble(Time::US))
+                     << " Mbps" << std::endl;
+        g_prevTime = curTime;
+        g_prev = itr->second.txBytes;
+    }
+    Simulator::Schedule(Seconds(0.2), &TraceThroughput, monitor);
+}
+
+/**
+ * @brief Check and trace the queue size
+ * @param qd The queue disc
+ */
+void
+CheckQueueSize(Ptr<QueueDisc> qd)
+{
+    uint32_t qsize = qd->GetCurrentSize().GetValue();
+    Simulator::Schedule(Seconds(0.2), &CheckQueueSize, qd);
+    g_queueSize << Simulator::Now().GetSeconds() << " " << qsize << std::endl;
+}
+
+/**
+ * @brief Trace packets dropped at Queue
+ * @param stream The output stream
+ * @param item The queue disc item dropped
+ */
+static void
+TracePacketDrop(Ptr<OutputStreamWrapper> stream, Ptr<const QueueDiscItem> item)
+{
+    Ptr<const Packet> p = item->GetPacket();
+
+    TcpHeader tcpHeader;
+    p->PeekHeader(tcpHeader);
+    uint32_t seqNum = tcpHeader.GetSequenceNumber().GetValue();
+
+    // The plots in the paper use segment index, not the raw sequence number.
+    // We calculate it by dividing by the segment size.
+    double segmentIndex = static_cast<double>(seqNum) / g_segmentSize;
+
+    *stream->GetStream() << Simulator::Now().GetSeconds() << " " << segmentIndex << std::endl;
+}
+
+/**
+ * @brief Trace congestion window
+ * @param stream The output stream
+ * @param oldval Old value
+ * @param newval New value
+ */
+static void
+CwndTracer(Ptr<OutputStreamWrapper> stream, uint32_t oldval, uint32_t newval)
+{
+    *stream->GetStream() << Simulator::Now().GetSeconds() << " " << newval / g_segmentSize
+                         << std::endl;
+}
+
+/**
+ * @brief Trace BytesInFlight in segments
+ * @param stream The output stream
+ * @param oldval Old value
+ * @param newval New value
+ */
+static void
+BytesInFlightTracer(Ptr<OutputStreamWrapper> stream, uint32_t oldval, uint32_t newval)
+{
+    uint32_t segs = newval / g_segmentSize;
+    *stream->GetStream() << Simulator::Now().GetSeconds() << " " << segs << std::endl;
+}
+
+/**
+ * @brief Trace FACK's inflight(AWND) in segments
+ * @param stream The output stream
+ * @param oldval Old value
+ * @param newval New value
+ */
+static void
+FackAwndTracer(Ptr<OutputStreamWrapper> stream, uint32_t oldval, uint32_t newval)
+{
+    uint32_t segs = newval / g_segmentSize;
+    *stream->GetStream() << Simulator::Now().GetSeconds() << " " << segs << std::endl;
+}
+
+/**
+ * @brief Bind the Congestion Window trace source.
+ * @param nodeId The node ID.
+ * @param socketId The socket ID.
+ */
+void
+TraceCwnd(uint32_t nodeId, uint32_t socketId)
+{
+    AsciiTraceHelper ascii;
+    Ptr<OutputStreamWrapper> stream = ascii.CreateFileStream(g_dir + "/cwnd.dat");
+    Config::ConnectWithoutContext("/NodeList/" + std::to_string(nodeId) +
+                                      "/$ns3::TcpL4Protocol/SocketList/" +
+                                      std::to_string(socketId) + "/CongestionWindow",
+                                  MakeBoundCallback(&CwndTracer, stream));
+}
+
+/**
+ * @brief Bind the BytesInFlight trace source.
+ * @param nodeId The node ID.
+ * @param socketId The socket ID.
+ */
+void
+TraceBytesInFlight(uint32_t nodeId, uint32_t socketId)
+{
+    AsciiTraceHelper ascii;
+    Ptr<OutputStreamWrapper> stream = ascii.CreateFileStream(g_dir + "/bytesInFlight.dat");
+    Config::ConnectWithoutContext("/NodeList/" + std::to_string(nodeId) +
+                                      "/$ns3::TcpL4Protocol/SocketList/" +
+                                      std::to_string(socketId) + "/BytesInFlight",
+                                  MakeBoundCallback(&BytesInFlightTracer, stream));
+}
+
+/**
+ * @brief Bind the FackAwnd trace source.
+ * @param nodeId The node ID.
+ * @param socketId The socket ID.
+ */
+void
+TraceFackAwnd(uint32_t nodeId, uint32_t socketId)
+{
+    AsciiTraceHelper ascii;
+    Ptr<OutputStreamWrapper> stream = ascii.CreateFileStream(g_dir + "/fackAwnd.dat");
+    Config::ConnectWithoutContext("/NodeList/" + std::to_string(nodeId) +
+                                      "/$ns3::TcpL4Protocol/SocketList/" +
+                                      std::to_string(socketId) + "/FackAwnd",
+                                  MakeBoundCallback(&FackAwndTracer, stream));
+}
+
+int
+main(int argc, char* argv[])
+{
+    // Naming the output directory using local system time
+    time_t rawtime;
+    struct tm* timeinfo;
+    char buffer[80];
+    time(&rawtime);
+    timeinfo = localtime(&rawtime);
+    strftime(buffer, sizeof(buffer), "%d-%m-%Y-%I-%M-%S", timeinfo);
+    std::string currentTime(buffer);
+
+    std::string tcpTypeId = "TcpLinuxReno";
+    std::string queueDisc = "FifoQueueDisc";
+    uint32_t delAckCount = 2;
+    bool bql = true;
+    bool enablePcap = false;
+    bool sack = true;
+    bool fack = true;
+    Time stopTime = Seconds(10);
+
+    queueDisc = std::string("ns3::") + queueDisc;
+
+    Config::SetDefault("ns3::TcpSocket::SndBufSize", UintegerValue(4194304));
+    Config::SetDefault("ns3::TcpSocket::RcvBufSize", UintegerValue(6291456));
+    Config::SetDefault("ns3::TcpSocket::InitialCwnd", UintegerValue(10));
+    Config::SetDefault("ns3::TcpSocket::DelAckCount", UintegerValue(delAckCount));
+    Config::SetDefault("ns3::TcpSocket::SegmentSize", UintegerValue(g_segmentSize));
+    Config::SetDefault("ns3::TcpSocket::InitialSlowStartThreshold",
+                       UintegerValue(35 * g_segmentSize));
+    Config::SetDefault("ns3::DropTailQueue<Packet>::MaxSize", QueueSizeValue(QueueSize("1p")));
+    Config::SetDefault(queueDisc + "::MaxSize", QueueSizeValue(QueueSize("10p")));
+    Config::SetDefault("ns3::TcpSocketBase::Sack", BooleanValue(sack));
+
+    CommandLine cmd(__FILE__);
+    cmd.AddValue("tcpTypeId", "Transport protocol to use: TcpLinuxReno", tcpTypeId);
+    cmd.AddValue("enablePcap", "Enable/Disable pcap file generation", enablePcap);
+    cmd.AddValue("stopTime",
+                 "Stop time for applications / simulation time will be stopTime + 1",
+                 stopTime);
+    cmd.AddValue("fack", "Enable/Disable FACK", fack);
+
+    cmd.Parse(argc, argv);
+
+    Config::SetDefault("ns3::TcpL4Protocol::SocketType", StringValue("ns3::" + tcpTypeId));
+    Config::SetDefault("ns3::TcpSocketBase::Fack", BooleanValue(fack));
+
+    // The maximum send buffer size is set to 4194304 bytes (4MB) and the
+    // maximum receive buffer size is set to 6291456 bytes (6MB) in the Linux
+    // kernel. The same buffer sizes are used as default in this example.
+
+    AsciiTraceHelper asciiTraceHelper;
+    Ptr<OutputStreamWrapper> streamWrapper;
+
+    NodeContainer sender;
+    NodeContainer receiver;
+    NodeContainer routers;
+    sender.Create(1);
+    receiver.Create(1);
+    routers.Create(2);
+
+    // Create the point-to-point link helpers
+    PointToPointHelper bottleneckLink;
+    bottleneckLink.SetDeviceAttribute("DataRate", StringValue("1.5Mbps"));
+    bottleneckLink.SetChannelAttribute("Delay", StringValue("5ms"));
+
+    PointToPointHelper senderToR1;
+    senderToR1.SetDeviceAttribute("DataRate", StringValue("10Mbps"));
+    senderToR1.SetChannelAttribute("Delay", StringValue("2ms"));
+
+    PointToPointHelper r2ToReceiver;
+    r2ToReceiver.SetDeviceAttribute("DataRate", StringValue("10Mbps"));
+    r2ToReceiver.SetChannelAttribute("Delay", StringValue("33ms"));
+
+    // Create NetDevice containers
+    NetDeviceContainer senderEdge = senderToR1.Install(sender.Get(0), routers.Get(0));
+    NetDeviceContainer r1r2 = bottleneckLink.Install(routers.Get(0), routers.Get(1));
+    NetDeviceContainer receiverEdge = r2ToReceiver.Install(routers.Get(1), receiver.Get(0));
+
+    // Install Stack
+    InternetStackHelper internet;
+    internet.Install(sender);
+    internet.Install(receiver);
+    internet.Install(routers);
+
+    // Configure the root queue discipline
+    TrafficControlHelper tch;
+    tch.SetRootQueueDisc(queueDisc);
+
+    if (bql)
+    {
+        tch.SetQueueLimits("ns3::DynamicQueueLimits", "HoldTime", StringValue("1000ms"));
+    }
+
+    tch.Install(senderEdge);
+    tch.Install(receiverEdge);
+
+    // Assign IP addresses
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.0.0.0", "255.255.255.0");
+
+    Ipv4InterfaceContainer i1i2 = ipv4.Assign(r1r2);
+
+    ipv4.NewNetwork();
+    Ipv4InterfaceContainer is1 = ipv4.Assign(senderEdge);
+
+    ipv4.NewNetwork();
+    Ipv4InterfaceContainer ir1 = ipv4.Assign(receiverEdge);
+
+    // Populate routing tables
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+    // Select sender side port
+    uint16_t port = 50001;
+
+    // Install application on the sender
+    BulkSendHelper source("ns3::TcpSocketFactory", InetSocketAddress(ir1.GetAddress(1), port));
+    source.SetAttribute("MaxBytes", UintegerValue(0));
+    ApplicationContainer sourceApps = source.Install(sender.Get(0));
+    sourceApps.Start(Seconds(0.1));
+    // Hook trace source after application starts
+    Simulator::Schedule(Seconds(0.1) + MilliSeconds(1), &TraceCwnd, 0, 0);
+    Simulator::Schedule(Seconds(0.1) + MilliSeconds(1), &TraceBytesInFlight, 0, 0);
+    Simulator::Schedule(Seconds(0.1) + MilliSeconds(1), &TraceFackAwnd, 0, 0);
+    sourceApps.Stop(stopTime);
+
+    // Install application on the receiver
+    PacketSinkHelper sink("ns3::TcpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), port));
+    ApplicationContainer sinkApps = sink.Install(receiver.Get(0));
+    sinkApps.Start(Seconds(0));
+    sinkApps.Stop(stopTime);
+
+    // Create a new directory to store the output of the program
+    g_dir = "fack-results/" + currentTime + "/";
+    MakeDirectories(g_dir);
+
+    // Open the lostSegments.dat file and write packet-loss events into it.
+    g_individualLostSegments.open(g_dir + "/lostSegments.dat", std::ios::out);
+    Ptr<OutputStreamWrapper> streamWrapperIndivLost =
+        asciiTraceHelper.CreateFileStream(g_dir + "/lostSegments.dat");
+
+    // Trace the queue occupancy on the second interface of R1
+    tch.Uninstall(routers.Get(0)->GetDevice(1));
+    QueueDiscContainer qd;
+    qd = tch.Install(routers.Get(0)->GetDevice(1));
+
+    // Get the actual QueueDisc Ptr and connect the "Drop" trace source
+    Ptr<QueueDisc> q = qd.Get(0);
+    q->TraceConnectWithoutContext("Drop",
+                                  MakeBoundCallback(&TracePacketDrop, streamWrapperIndivLost));
+
+    Simulator::ScheduleNow(&CheckQueueSize, qd.Get(0));
+    // Generate PCAP traces if it is enabled
+    if (enablePcap)
+    {
+        MakeDirectories(g_dir + "pcap/");
+        bottleneckLink.EnablePcapAll(g_dir + "/pcap/fack", true);
+    }
+
+    // Open files for writing throughput traces and queue size
+    g_throughput.open(g_dir + "/throughput.dat", std::ios::out);
+    g_queueSize.open(g_dir + "/queueSize.dat", std::ios::out);
+
+    NS_ASSERT_MSG(g_throughput.is_open(), "Throughput file was not opened correctly");
+    NS_ASSERT_MSG(g_queueSize.is_open(), "Queue size file was not opened correctly");
+
+    // Check for dropped packets using Flow Monitor
+    FlowMonitorHelper flowmon;
+    Ptr<FlowMonitor> monitor = flowmon.InstallAll();
+    Simulator::Schedule(Seconds(0 + 0.000001), &TraceThroughput, monitor);
+
+    Simulator::Stop(stopTime + TimeStep(1));
+    Simulator::Run();
+    Simulator::Destroy();
+
+    g_throughput.close();
+    g_queueSize.close();
+
+    return 0;
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/tcp/tcp-bbr-example.py ns-3.47/examples/tcp/tcp-bbr-example.py
--- ns-3.46/examples/tcp/tcp-bbr-example.py	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/examples/tcp/tcp-bbr-example.py	2026-02-16 16:08:49.560229800 -0800
@@ -0,0 +1,280 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (c) 2018-20 NITK Surathkal
+# Copyright (c) 2025 CSPIT, CHARUSAT
+#
+# Authors:
+#   Aarti Nandagiri <aarti.nandagiri@gmail.com>
+#   Vivek Jain <jain.vivek.anand@gmail.com>
+#   Mohit P. Tahiliani <tahiliani@nitk.edu.in>
+#   Urval Kheni <kheniurval777@gmail.com>
+#   Ritesh Patel <riteshpatel.ce@charusat.ac.in>
+
+"""
+Python port of tcp-bbr-example.cc
+
+Simulates the following topology:
+
+    Sender ---- R1 ---- R2 ---- Receiver
+     1000Mbps    10Mbps   1000Mbps
+       5ms        10ms       5ms
+
+Outputs:
+- cwnd.dat        (congestion window)
+- throughput.dat  (throughput)
+- queueSize.dat   (queue length)
+"""
+
+import argparse
+import os
+import time
+
+try:
+    from ns import ns
+except ModuleNotFoundError:
+    raise SystemExit(
+        "Error: ns3 Python module not found. "
+        "Bindings may not be enabled or PYTHONPATH is incorrect."
+    )
+
+# Global trace variables
+prev_tx = 0
+prev_time = None
+throughput_file = None
+queue_file = None
+cwnd_file = None
+outdir = ""
+
+
+def trace_throughput_callback():
+    """Throughput trace callback."""
+    global prev_tx, prev_time, throughput_file
+
+    stats = ns.cppyy.gbl.monitor_ptr.GetFlowStats()
+    if stats and not stats.empty():
+        flow_stats = stats.begin().__deref__().second
+        cur_time = ns.Now()
+
+        bits = 8 * (flow_stats.txBytes - prev_tx)
+        us = (cur_time - prev_time).ToDouble(ns.Time.US)
+
+        if us > 0:
+            throughput_file.write(f"{cur_time.GetSeconds():.6g}s {bits / us:.6g} Mbps\n")
+            throughput_file.flush()
+
+        prev_time = cur_time
+        prev_tx = flow_stats.txBytes
+
+
+def check_queue_size_callback():
+    """Queue length tracer."""
+    global queue_file
+
+    q = ns.cppyy.gbl.qdisc_ptr.GetCurrentSize().GetValue()
+    queue_file.write(f"{ns.Simulator.Now().GetSeconds():.6g} {q}\n")
+    queue_file.flush()
+
+
+def trace_cwnd_callback(oldval, newval):
+    """Congestion window tracer."""
+    global cwnd_file
+
+    cwnd_file.write(f"{ns.Simulator.Now().GetSeconds():.6g} {newval / 1448.0:.6g}\n")
+    cwnd_file.flush()
+
+
+# C++ helper functions for scheduling Python callbacks
+ns.cppyy.cppdef(r"""
+#include "ns3/simulator.h"
+#include "ns3/flow-monitor-helper.h"
+#include "ns3/queue-disc.h"
+#include "ns3/node.h"
+#include "ns3/config.h"
+
+using namespace ns3;
+
+Ptr<FlowMonitor> monitor_ptr;
+Ptr<QueueDisc> qdisc_ptr;
+
+// Helper to create event from Python callback without parameters
+EventImpl* pythonMakeEvent(void (*f)())
+{
+    return MakeEvent(f);
+}
+
+// Helper to connect cwnd callback and create event
+EventImpl* pythonConnectCwndTrace(void (*f)(uint32_t, uint32_t))
+{
+    Config::ConnectWithoutContext(
+        "/NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow",
+        MakeCallback(f));
+    return nullptr;
+}
+""")
+
+
+def main():
+    global throughput_file, queue_file, cwnd_file, outdir, prev_time
+    global prev_tx
+
+    parser = argparse.ArgumentParser(description="tcp-bbr-example (Python)")
+    parser.add_argument(
+        "--tcpTypeId",
+        default="TcpBbr",
+        help="Transport protocol: TcpNewReno, TcpBbr",
+    )
+    parser.add_argument("--delAckCount", type=int, default=2, help="Delayed ACK count")
+    parser.add_argument("--enablePcap", action="store_true", help="Enable pcap traces")
+    parser.add_argument("--stopTime", type=float, default=100.0, help="Simulation stop time")
+    args = parser.parse_args()
+
+    ts = time.strftime("%d-%m-%Y-%I-%M-%S")
+    outdir = f"bbr-results/{ts}/"
+    os.makedirs(outdir, exist_ok=True)
+
+    tcpType = args.tcpTypeId
+    queueDisc = "ns3::FifoQueueDisc"
+    delAck = args.delAckCount
+    enablePcap = args.enablePcap
+    stopTime = ns.Seconds(args.stopTime)
+
+    ns.Config.SetDefault(
+        "ns3::TcpL4Protocol::SocketType",
+        ns.StringValue("ns3::" + tcpType),
+    )
+    ns.Config.SetDefault("ns3::TcpSocket::SndBufSize", ns.UintegerValue(4194304))
+    ns.Config.SetDefault("ns3::TcpSocket::RcvBufSize", ns.UintegerValue(6291456))
+    ns.Config.SetDefault("ns3::TcpSocket::InitialCwnd", ns.UintegerValue(10))
+    ns.Config.SetDefault("ns3::TcpSocket::DelAckCount", ns.UintegerValue(delAck))
+    ns.Config.SetDefault("ns3::TcpSocket::SegmentSize", ns.UintegerValue(1448))
+    ns.Config.SetDefault(
+        "ns3::DropTailQueue<Packet>::MaxSize", ns.QueueSizeValue(ns.QueueSize("1p"))
+    )
+    ns.Config.SetDefault(
+        queueDisc + "::MaxSize",
+        ns.QueueSizeValue(ns.QueueSize("100p")),
+    )
+
+    sender = ns.NodeContainer()
+    receiver = ns.NodeContainer()
+    routers = ns.NodeContainer()
+    sender.Create(1)
+    receiver.Create(1)
+    routers.Create(2)
+
+    bottleneck = ns.PointToPointHelper()
+    bottleneck.SetDeviceAttribute("DataRate", ns.StringValue("10Mbps"))
+    bottleneck.SetChannelAttribute("Delay", ns.StringValue("10ms"))
+
+    edge = ns.PointToPointHelper()
+    edge.SetDeviceAttribute("DataRate", ns.StringValue("1000Mbps"))
+    edge.SetChannelAttribute("Delay", ns.StringValue("5ms"))
+
+    senderEdge = edge.Install(sender.Get(0), routers.Get(0))
+    r1r2 = bottleneck.Install(routers.Get(0), routers.Get(1))
+    receiverEdge = edge.Install(routers.Get(1), receiver.Get(0))
+
+    inet = ns.InternetStackHelper()
+    inet.Install(sender)
+    inet.Install(receiver)
+    inet.Install(routers)
+
+    tch = ns.TrafficControlHelper()
+    tch.SetRootQueueDisc(queueDisc)
+    tch.SetQueueLimits("ns3::DynamicQueueLimits", "HoldTime", ns.StringValue("1000ms"))
+    tch.Install(senderEdge)
+    tch.Install(receiverEdge)
+
+    ipv4 = ns.Ipv4AddressHelper()
+    ipv4.SetBase(ns.Ipv4Address("10.0.0.0"), ns.Ipv4Mask("255.255.255.0"))
+
+    _ = ipv4.Assign(r1r2)
+
+    ipv4.NewNetwork()
+    ipv4.Assign(senderEdge)
+
+    ipv4.NewNetwork()
+    ir1 = ipv4.Assign(receiverEdge)
+
+    ns.Ipv4GlobalRoutingHelper.PopulateRoutingTables()
+
+    port = 50001
+
+    source = ns.BulkSendHelper(
+        "ns3::TcpSocketFactory",
+        ns.InetSocketAddress(ir1.GetAddress(1), port).ConvertTo(),
+    )
+    source.SetAttribute("MaxBytes", ns.UintegerValue(0))
+    sourceApps = source.Install(sender.Get(0))
+    sourceApps.Start(ns.Seconds(0.1))
+    sourceApps.Stop(stopTime)
+
+    sink = ns.PacketSinkHelper(
+        "ns3::TcpSocketFactory",
+        ns.InetSocketAddress(ns.Ipv4Address.GetAny(), port).ConvertTo(),
+    )
+    sinkApps = sink.Install(receiver.Get(0))
+    sinkApps.Start(ns.Seconds(0))
+    sinkApps.Stop(stopTime)
+
+    if enablePcap:
+        os.makedirs(os.path.join(outdir, "pcap"), exist_ok=True)
+
+    throughput_file = open(os.path.join(outdir, "throughput.dat"), "w")
+    queue_file = open(os.path.join(outdir, "queueSize.dat"), "w")
+    cwnd_file = open(os.path.join(outdir, "cwnd.dat"), "w")
+
+    if enablePcap:
+        bottleneck.EnablePcapAll(os.path.join(outdir, "pcap", "bbr"), True)
+
+    flowmon = ns.FlowMonitorHelper()
+    monitor = flowmon.InstallAll()
+
+    tch.Uninstall(routers.Get(0).GetDevice(1))
+    qdc = tch.Install(routers.Get(0).GetDevice(1))
+
+    prev_time = ns.Now()
+    prev_tx = 0
+
+    ns.cppyy.gbl.monitor_ptr = monitor
+    ns.cppyy.gbl.qdisc_ptr = qdc.Get(0)
+
+    # Define wrapper functions that reschedule themselves
+    def throughput_tracer():
+        """Periodic throughput tracer that reschedules itself."""
+        trace_throughput_callback()
+        event = ns.cppyy.gbl.pythonMakeEvent(throughput_tracer)
+        ns.Simulator.Schedule(ns.Seconds(0.2), event)
+
+    def queue_size_tracer():
+        """Periodic queue size tracer that reschedules itself."""
+        check_queue_size_callback()
+        event = ns.cppyy.gbl.pythonMakeEvent(queue_size_tracer)
+        ns.Simulator.Schedule(ns.Seconds(0.2), event)
+
+    # Schedule initial events using pythonMakeEvent
+    event = ns.cppyy.gbl.pythonMakeEvent(throughput_tracer)
+    ns.Simulator.Schedule(ns.Seconds(0.000001), event)
+
+    event = ns.cppyy.gbl.pythonMakeEvent(queue_size_tracer)
+    ns.Simulator.ScheduleNow(event)
+
+    # Connect cwnd trace after socket is created
+    def connect_cwnd_trace():
+        """Connect the cwnd trace callback."""
+        ns.cppyy.gbl.pythonConnectCwndTrace(trace_cwnd_callback)
+
+    event = ns.cppyy.gbl.pythonMakeEvent(connect_cwnd_trace)
+    ns.Simulator.Schedule(ns.Seconds(0.1) + ns.MilliSeconds(1), event)
+
+    ns.Simulator.Stop(stopTime + ns.TimeStep(1))
+    ns.Simulator.Run()
+    ns.Simulator.Destroy()
+
+    throughput_file.close()
+    queue_file.close()
+    cwnd_file.close()
+
+
+if __name__ == "__main__":
+    main()
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/tcp/tcp-ecn-example.cc ns-3.47/examples/tcp/tcp-ecn-example.cc
--- ns-3.46/examples/tcp/tcp-ecn-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/examples/tcp/tcp-ecn-example.cc	2026-02-16 16:08:49.560229800 -0800
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2025 NITK Surathkal
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Authors: N Nagabhushanam    <thechosentwins2005@gmail.com>
+ *          Namburi Yaswanth   <yaswanthnamburi1010@gmail.com>
+ *          Vishruth S Kumar   <vishruthskumar@gmail.com>
+ *          Yashas             <yashas80dj@gmail.com>
+ *          Mohit P. Tahiliani <tahiliani@nitk.edu.in>
+ */
+
+// This program simulates the following topology:
+//
+//           1000 Mbps           10Mbps          1000 Mbps
+//  Sender -------------- R1 -------------- R2 -------------- Receiver
+//              5ms               10ms               5ms
+//
+// This topology is the same as the topology present in tcp-bbr-example.
+// The link between R1 and R2 is a bottleneck link with 10 Mbps. All other
+// links are 1000 Mbps.
+//
+// This program runs by default for 100 seconds and creates a new directory
+// called 'ecn-results' in the ns-3 root directory. The program creates one
+// sub-directory called 'pcap' in 'ecn-results' directory (if pcap generation
+// is enabled) and three .dat files.
+//
+// (1) 'pcap' sub-directory contains six PCAP files:
+//     * ecn-0-0.pcap for the interface on Sender
+//     * ecn-1-0.pcap for the interface on Receiver
+//     * ecn-2-0.pcap for the first interface on R1
+//     * ecn-2-1.pcap for the second interface on R1
+//     * ecn-3-0.pcap for the first interface on R2
+//     * ecn-3-1.pcap for the second interface on R2
+// (2) cwnd.dat file contains congestion window trace for the sender node
+// (3) throughput.dat file contains sender side throughput trace (throughput is in Mbit/s)
+// (4) queueSize.dat file contains queue length trace from the bottleneck link
+//
+// ABE (Alternative Backoff with ECN) modifies the traditional TCP response to
+// ECN (Explicit Congestion Notification) markings. While ECN signals
+// congestion before queue overflows, standard TCP still reduces its congestion
+// window aggressively (by 30% in TcpCubic) upon receiving these marks.
+//
+// ABE improves upon this by backing off less aggressively when ECN marks are
+// received, typically reducing the window (by 15% in TcpCubic instead). This results in
+// better throughput and faster recovery in high-bandwidth, ECN-enabled networks.
+
+#include "ns3/applications-module.h"
+#include "ns3/core-module.h"
+#include "ns3/flow-monitor-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/network-module.h"
+#include "ns3/point-to-point-module.h"
+#include "ns3/traffic-control-module.h"
+
+#include <filesystem>
+
+using namespace ns3;
+using namespace ns3::SystemPath;
+
+std::string dir;          //!< The directory where the output files will be stored
+std::ofstream throughput; //!< The stream where the throughput will be stored
+std::ofstream queueSize;  //!< The stream where the queue size will be stored
+
+uint32_t prev = 0; //!< Previous transmitted bytes
+Time prevTime;     //!< Previous time for throughput calculation
+
+/**
+ * Traces the throughput of the flow monitor.
+ * @param monitor The flow monitor to trace.
+ */
+static void
+TraceThroughput(Ptr<FlowMonitor> monitor)
+{
+    FlowMonitor::FlowStatsContainer stats = monitor->GetFlowStats();
+    if (!stats.empty())
+    {
+        auto itr = stats.begin();
+        Time curTime = Now();
+
+        // Convert (curTime - prevTime) to microseconds so that throughput is in bits per
+        // microsecond (which is equivalent to Mbps)
+        throughput << curTime.GetSeconds() << "s "
+                   << 8 * (itr->second.txBytes - prev) / ((curTime - prevTime).ToDouble(Time::US))
+                   << " Mbps" << std::endl;
+        prevTime = curTime;
+        prev = itr->second.txBytes;
+    }
+    Simulator::Schedule(Seconds(0.2), &TraceThroughput, monitor);
+}
+
+/**
+ * Checks the queue size of the queue discipline.
+ * @param qd The queue discipline to check.
+ */
+void
+CheckQueueSize(Ptr<QueueDisc> qd)
+{
+    uint32_t qsize = qd->GetCurrentSize().GetValue();
+    Simulator::Schedule(Seconds(0.2), &CheckQueueSize, qd);
+    queueSize << Simulator::Now().GetSeconds() << " " << qsize << std::endl;
+}
+
+/**
+ * Writes the cWnd to the output stream.
+ * @param stream The output stream to write to.
+ * @param oldval The old congestion window value.
+ * @param newval The new congestion window value.
+ */
+static void
+CwndTracer(Ptr<OutputStreamWrapper> stream, uint32_t oldval, uint32_t newval)
+{
+    *stream->GetStream() << Simulator::Now().GetSeconds() << " " << newval / 1448.0 << std::endl;
+}
+
+/**
+ * Traces the congestion window of the socket.
+ * @param nodeId The node ID of the socket.
+ * @param socketId The socket ID of the socket.
+ */
+void
+TraceCwnd(uint32_t nodeId, uint32_t socketId)
+{
+    AsciiTraceHelper ascii;
+    Ptr<OutputStreamWrapper> stream = ascii.CreateFileStream(dir + "/cwnd.dat");
+    Config::ConnectWithoutContext("/NodeList/" + std::to_string(nodeId) +
+                                      "/$ns3::TcpL4Protocol/SocketList/" +
+                                      std::to_string(socketId) + "/CongestionWindow",
+                                  MakeBoundCallback(&CwndTracer, stream));
+}
+
+int
+main(int argc, char* argv[])
+{
+    // Naming the output directory using local system time
+    time_t rawtime;
+    struct tm* timeinfo;
+    char buffer[80];
+    time(&rawtime);
+    timeinfo = localtime(&rawtime);
+    strftime(buffer, sizeof(buffer), "%d-%m-%Y-%I-%M-%S", timeinfo);
+    std::string currentTime(buffer);
+
+    std::string tcpTypeId = "TcpCubic";
+    uint32_t delAckCount = 2;
+    bool bql = true;
+    bool enablePcap = false;
+    bool useEcn = true;
+    bool useAbe = false;
+    Time stopTime = Seconds(100);
+
+    CommandLine cmd(__FILE__);
+    cmd.AddValue("tcpTypeId",
+                 "Transport protocol to use: TcpNewReno,TcpCubic, TcpLinuxReno",
+                 tcpTypeId);
+    cmd.AddValue("delAckCount", "Delayed ACK count", delAckCount);
+    cmd.AddValue("enablePcap", "Enable/Disable pcap file generation", enablePcap);
+    cmd.AddValue("useEcn", "Enable/Disable ECN", useEcn);
+    cmd.AddValue("useAbe", "Enable/Disable ABE", useAbe);
+    cmd.AddValue("stopTime",
+                 "Stop time for applications / simulation time will be stopTime + 1",
+                 stopTime);
+    cmd.Parse(argc, argv);
+
+    if (tcpTypeId.find("ns3::") == std::string::npos)
+    {
+        tcpTypeId = "ns3::" + tcpTypeId;
+    }
+
+    Config::SetDefault("ns3::TcpL4Protocol::SocketType", StringValue(tcpTypeId));
+
+    // The maximum send buffer size is set to 4194304 bytes (4MB) and the
+    // maximum receive buffer size is set to 6291456 bytes (6MB) in the Linux
+    // kernel. The same buffer sizes are used as default in this example.
+    Config::SetDefault("ns3::TcpSocket::SndBufSize", UintegerValue(4194304));
+    Config::SetDefault("ns3::TcpSocket::RcvBufSize", UintegerValue(6291456));
+    Config::SetDefault("ns3::TcpSocket::InitialCwnd", UintegerValue(10));
+    Config::SetDefault("ns3::TcpSocket::DelAckCount", UintegerValue(delAckCount));
+    Config::SetDefault("ns3::TcpSocket::SegmentSize", UintegerValue(1448));
+    Config::SetDefault("ns3::DropTailQueue<Packet>::MaxSize", QueueSizeValue(QueueSize("1p")));
+
+    // AQM
+    std::string queueDisc = "ns3::CoDelQueueDisc";
+    std::string bottleNeckLinkBw = "10Mbps";
+    std::string bottleNeckLinkDelay = "10ms";
+
+    // CoDel parameters
+    Config::SetDefault("ns3::CoDelQueueDisc::MaxSize", QueueSizeValue(QueueSize("1000p")));
+    Config::SetDefault("ns3::CoDelQueueDisc::Interval", TimeValue(MilliSeconds(100)));
+    Config::SetDefault("ns3::CoDelQueueDisc::Target", TimeValue(MilliSeconds(5)));
+    Config::SetDefault("ns3::CoDelQueueDisc::UseEcn", BooleanValue(true));
+
+    if (useAbe)
+    {
+        Config::SetDefault("ns3::TcpSocketBase::UseAbe", BooleanValue(true));
+    }
+    if (useEcn)
+    {
+        Config::SetDefault("ns3::TcpSocketBase::UseEcn", EnumValue(TcpSocketState::On));
+    }
+
+    NodeContainer sender;
+    NodeContainer receiver;
+    NodeContainer routers;
+    sender.Create(1);
+    receiver.Create(1);
+    routers.Create(2);
+
+    // Create the point-to-point link helpers
+    PointToPointHelper bottleneckLink;
+    bottleneckLink.SetDeviceAttribute("DataRate", StringValue("10Mbps"));
+    bottleneckLink.SetChannelAttribute("Delay", StringValue("10ms"));
+
+    PointToPointHelper edgeLink;
+    edgeLink.SetDeviceAttribute("DataRate", StringValue("1000Mbps"));
+    edgeLink.SetChannelAttribute("Delay", StringValue("5ms"));
+
+    // Create NetDevice containers
+    NetDeviceContainer senderEdge = edgeLink.Install(sender.Get(0), routers.Get(0));
+    NetDeviceContainer r1r2 = bottleneckLink.Install(routers.Get(0), routers.Get(1));
+    NetDeviceContainer receiverEdge = edgeLink.Install(routers.Get(1), receiver.Get(0));
+
+    // Install Stack
+    InternetStackHelper internet;
+    internet.Install(sender);
+    internet.Install(receiver);
+    internet.Install(routers);
+
+    // Configure the root queue discipline
+    TrafficControlHelper tch;
+    tch.SetRootQueueDisc(queueDisc);
+
+    if (bql)
+    {
+        tch.SetQueueLimits("ns3::DynamicQueueLimits", "HoldTime", StringValue("1000ms"));
+    }
+    tch.Install(senderEdge);
+    tch.Install(receiverEdge);
+
+    // Assign IP addresses
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.0.0.0", "255.255.255.0");
+
+    Ipv4InterfaceContainer i1i2 = ipv4.Assign(r1r2);
+
+    ipv4.NewNetwork();
+    Ipv4InterfaceContainer is1 = ipv4.Assign(senderEdge);
+
+    ipv4.NewNetwork();
+    Ipv4InterfaceContainer ir1 = ipv4.Assign(receiverEdge);
+
+    // Populate routing tables
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+    // Select sender side port
+    uint16_t port = 50001;
+
+    // Install application on the sender
+    BulkSendHelper source("ns3::TcpSocketFactory", InetSocketAddress(ir1.GetAddress(1), port));
+    source.SetAttribute("MaxBytes", UintegerValue(0));
+    ApplicationContainer sourceApps = source.Install(sender.Get(0));
+    sourceApps.Start(Seconds(0.1));
+    // Hook trace source after application starts
+    Simulator::Schedule(Seconds(0.1) + MilliSeconds(1), &TraceCwnd, 0, 0);
+    sourceApps.Stop(stopTime);
+
+    // Install application on the receiver
+    PacketSinkHelper sink("ns3::TcpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), port));
+    ApplicationContainer sinkApps = sink.Install(receiver.Get(0));
+    sinkApps.Start(Seconds(0));
+    sinkApps.Stop(stopTime);
+    // Create a new directory to store the output of the program
+    dir = "ecn-results/" + currentTime + "/";
+    MakeDirectories(dir);
+
+    // The plotting scripts are provided in the following repository, if needed:
+    // https://github.com/mohittahiliani/BBR-Validation/
+    //
+    // Download 'PlotScripts' directory (which is inside ns-3 scripts directory)
+    // from the link given above and place it in the ns-3 root directory.
+    // Uncomment the following three lines to copy plot scripts for
+    // Congestion Window, sender side throughput and queue occupancy on the
+    // bottleneck link into the output directory.
+    //
+    // std::filesystem::copy("PlotScripts/gnuplotScriptCwnd", dir);
+    // std::filesystem::copy("PlotScripts/gnuplotScriptThroughput", dir);
+    // std::filesystem::copy("PlotScripts/gnuplotScriptQueueSize", dir);
+
+    // Trace the queue occupancy on the second interface of R1
+    tch.Uninstall(routers.Get(0)->GetDevice(1));
+    QueueDiscContainer qd;
+    qd = tch.Install(routers.Get(0)->GetDevice(1));
+    Simulator::ScheduleNow(&CheckQueueSize, qd.Get(0));
+
+    // Generate PCAP traces if it is enabled
+    if (enablePcap)
+    {
+        MakeDirectories(dir + "pcap/");
+        bottleneckLink.EnablePcapAll(dir + "/pcap/ecn", true);
+    }
+
+    // Open files for writing throughput traces and queue size
+    throughput.open(dir + "/throughput.dat", std::ios::out);
+    queueSize.open(dir + "/queueSize.dat", std::ios::out);
+
+    NS_ASSERT_MSG(throughput.is_open(), "Throughput file was not opened correctly");
+    NS_ASSERT_MSG(queueSize.is_open(), "Queue size file was not opened correctly");
+
+    // Check for dropped packets using Flow Monitor
+    FlowMonitorHelper flowmon;
+    Ptr<FlowMonitor> monitor = flowmon.InstallAll();
+    Simulator::ScheduleNow(&TraceThroughput, monitor);
+
+    Simulator::Stop(stopTime + TimeStep(1));
+    Simulator::Run();
+    Simulator::Destroy();
+
+    throughput.close();
+    queueSize.close();
+
+    return 0;
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/tcp/tcp-linux-reno.cc ns-3.47/examples/tcp/tcp-linux-reno.cc
--- ns-3.46/examples/tcp/tcp-linux-reno.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/tcp/tcp-linux-reno.cc	2026-02-16 16:08:49.560229800 -0800
@@ -118,7 +118,7 @@
     uint32_t delAckCount = 1;
     std::string recovery = "ns3::TcpClassicRecovery";
 
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.AddValue("tcpTypeId",
                  "TCP variant to use (e.g., ns3::TcpNewReno, ns3::TcpLinuxReno, etc.)",
                  tcpTypeId);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/examples-to-run.py ns-3.47/examples/wireless/examples-to-run.py
--- ns-3.46/examples/wireless/examples-to-run.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/examples-to-run.py	2026-02-16 16:08:49.562229600 -0800
@@ -235,7 +235,7 @@
         "True",
     ),
     (
-        "wifi-simple-ht-hidden-stations --simulationTime=1s --enableRts=1 --nMpdus=32 --minExpectedThroughput=57 --maxExpectedThroughput=58",
+        "wifi-simple-ht-hidden-stations --simulationTime=1s --enableRts=1 --nMpdus=32 --minExpectedThroughput=56 --maxExpectedThroughput=57",
         "True",
         "True",
     ),
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/mixed-wired-wireless.py ns-3.47/examples/wireless/mixed-wired-wireless.py
--- ns-3.46/examples/wireless/mixed-wired-wireless.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/mixed-wired-wireless.py	2026-02-16 16:08:49.562229600 -0800
@@ -332,13 +332,11 @@
     )
     appSink = ns.NodeList.GetNode(lastNodeIndex)
 
-    ns.cppyy.cppdef(
-        """
+    ns.cppyy.cppdef("""
         Ipv4Address getIpv4AddressFromNode(Ptr<Node> node){
         return node->GetObject<Ipv4>()->GetAddress(1,0).GetLocal();
         }
-    """
-    )
+    """)
     # Let's fetch the IP address of the last node, which is on Ipv4Interface 1
     remoteAddr = ns.cppyy.gbl.getIpv4AddressFromNode(appSink)
     socketAddr = ns.InetSocketAddress(remoteAddr, port)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-adhoc.cc ns-3.47/examples/wireless/wifi-adhoc.cc
--- ns-3.46/examples/wireless/wifi-adhoc.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-adhoc.cc	2026-02-16 16:08:49.562229600 -0800
@@ -21,10 +21,20 @@
 #include "ns3/yans-wifi-channel.h"
 #include "ns3/yans-wifi-helper.h"
 
+/**
+ * @file
+ * @ingroup wifi
+ * Adhoc example
+ */
+
 using namespace ns3;
 
 NS_LOG_COMPONENT_DEFINE("Wifi-Adhoc");
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
 /**
  * WiFi adhoc experiment class.
  *
@@ -198,6 +208,8 @@
     return m_output;
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char* argv[])
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-ap.py ns-3.47/examples/wireless/wifi-ap.py
--- ns-3.46/examples/wireless/wifi-ap.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-ap.py	2026-02-16 16:08:49.562229600 -0800
@@ -66,8 +66,7 @@
 #   std::cout << " start="<<start<<" duration="<<duration<<std::endl;
 # }
 
-ns.cppyy.cppdef(
-    """
+ns.cppyy.cppdef("""
     using namespace ns3;
     void AdvancePosition(Ptr<Node> node){
         Ptr<MobilityModel> mob = node->GetObject<MobilityModel>();
@@ -77,8 +76,7 @@
             return;
         mob->SetPosition(pos);
         Simulator::Schedule(Seconds(1), AdvancePosition, node);
-    }"""
-)
+    }""")
 
 
 def main(argv):
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-clear-channel-cmu.cc ns-3.47/examples/wireless/wifi-clear-channel-cmu.cc
--- ns-3.46/examples/wireless/wifi-clear-channel-cmu.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-clear-channel-cmu.cc	2026-02-16 16:08:49.562229600 -0800
@@ -19,10 +19,20 @@
 #include "ns3/yans-wifi-channel.h"
 #include "ns3/yans-wifi-helper.h"
 
+/**
+ * @file
+ * @ingroup wifi
+ * Clear channel example
+ */
+
 using namespace ns3;
 
 NS_LOG_COMPONENT_DEFINE("ClearChannelCmu");
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
 /**
  * WiFi clear channel cmu experiment class.
  *
@@ -97,14 +107,14 @@
     m_output.SetStyle(Gnuplot2dDataset::LINES);
 }
 
-void
+[[maybe_unused]] void
 Experiment::SetPosition(Ptr<Node> node, Vector position)
 {
     Ptr<MobilityModel> mobility = node->GetObject<MobilityModel>();
     mobility->SetPosition(position);
 }
 
-Vector
+[[maybe_unused]] Vector
 Experiment::GetPosition(Ptr<Node> node)
 {
     Ptr<MobilityModel> mobility = node->GetObject<MobilityModel>();
@@ -211,6 +221,8 @@
     return m_pktsTotal;
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char* argv[])
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-mixed-network.cc ns-3.47/examples/wireless/wifi-mixed-network.cc
--- ns-3.46/examples/wireless/wifi-mixed-network.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-mixed-network.cc	2026-02-16 16:08:49.563229800 -0800
@@ -27,27 +27,33 @@
 #include "ns3/yans-wifi-channel.h"
 #include "ns3/yans-wifi-helper.h"
 
-// This example shows how to configure mixed networks (i.e. mixed b/g and HT/non-HT) and how are
-// performance in several scenarios.
-//
-// The example compares first g only and mixed b/g cases with various configurations depending on
-// the following parameters:
-// - protection mode that is configured on the AP;
-// - whether short PPDU format is supported by the 802.11b station;
-// - whether short slot time is supported by both the 802.11g station and the AP.
-//
-// The example then compares HT only and mixed HT/non-HT cases.
-//
-// The output results show that the presence of an 802.11b station strongly affects 802.11g
-// performance. Protection mechanisms ensure that the NAV value of 802.11b stations is set correctly
-// in case of 802.11g transmissions. In practice, those protection mechanism add a lot of overhead,
-// resulting in reduced performance. CTS-To-Self introduces less overhead than Rts-Cts, but is not
-// heard by hidden stations (and is thus generally only recommended as a protection mechanism for
-// access points). Since short slot time is disabled once an 802.11b station enters the network,
-// benefits from short slot time are only observed in a g only configuration.
-//
-// The user can also select the payload size and can choose either an UDP or a TCP connection.
-// Example: ./ns3 run "wifi-mixed-network --isUdp=1"
+/**
+ * @file
+ * @ingroup wifi
+ *
+ * This example shows how to configure mixed networks (i.e. mixed b/g and HT/non-HT) and how are
+ * performance in several scenarios.
+ *
+ * The example compares first g only and mixed b/g cases with various configurations depending on
+ * the following parameters:
+ * - protection mode that is configured on the AP;
+ * - whether short PPDU format is supported by the 802.11b station;
+ * - whether short slot time is supported by both the 802.11g station and the AP.
+ *
+ * The example then compares HT only and mixed HT/non-HT cases.
+ *
+ * The output results show that the presence of an 802.11b station strongly affects 802.11g
+ * performance. Protection mechanisms ensure that the NAV value of 802.11b stations is set correctly
+ * in case of 802.11g transmissions. In practice, those protection mechanism add a lot of overhead,
+ * resulting in reduced performance. CTS-To-Self introduces less overhead than Rts-Cts, but is not
+ * heard by hidden stations (and is thus generally only recommended as a protection mechanism for
+ * access points). Since short slot time is disabled once an 802.11b station enters the network,
+ * benefits from short slot time are only observed in a g only configuration.
+ *
+ * The user can also select the payload size and can choose either an UDP or a TCP connection.
+ *
+ * Example: `./ns3 run "wifi-mixed-network --isUdp=1"`
+ */
 
 using namespace ns3;
 
@@ -73,6 +79,11 @@
     Time simulationTime;           //!< Simulation time
 };
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
+/** Experiment runner, based on Parameters */
 class Experiment
 {
   public:
@@ -340,6 +351,8 @@
     return throughput;
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char* argv[])
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-multirate.cc ns-3.47/examples/wireless/wifi-multirate.cc
--- ns-3.46/examples/wireless/wifi-multirate.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-multirate.cc	2026-02-16 16:08:49.563229800 -0800
@@ -25,10 +25,20 @@
 #include "ns3/yans-wifi-channel.h"
 #include "ns3/yans-wifi-helper.h"
 
+/**
+ * @file
+ * @ingroup wifi
+ * WiFi multirate experiment.
+ */
+
 using namespace ns3;
 
 NS_LOG_COMPONENT_DEFINE("multirate");
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
 /**
  * WiFi multirate experiment class.
  *
@@ -429,7 +439,7 @@
  * @param server Server node.
  * @return a string with the nodes data and positions
  */
-static inline std::string
+inline std::string
 PrintPosition(Ptr<Node> client, Ptr<Node> server)
 {
     Vector serverPos = server->GetObject<MobilityModel>()->GetPosition();
@@ -680,6 +690,8 @@
     return true;
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char* argv[])
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-power-adaptation-distance.cc ns-3.47/examples/wireless/wifi-power-adaptation-distance.cc
--- ns-3.46/examples/wireless/wifi-power-adaptation-distance.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-power-adaptation-distance.cc	2026-02-16 16:08:49.563229800 -0800
@@ -456,7 +456,7 @@
     // Configure the AP node
     wifi.SetRemoteStationManager(manager,
                                  "DefaultTxPowerLevel",
-                                 UintegerValue(powerLevels - 1),
+                                 UintegerValue(WIFI_MIN_TX_PWR_LEVEL + powerLevels - 1),
                                  "RtsCtsThreshold",
                                  UintegerValue(rtsThreshold));
     wifiPhy.Set("TxPowerStart", DoubleValue(minPower));
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-simple-ht-hidden-stations.cc ns-3.47/examples/wireless/wifi-simple-ht-hidden-stations.cc
--- ns-3.46/examples/wireless/wifi-simple-ht-hidden-stations.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-simple-ht-hidden-stations.cc	2026-02-16 16:08:49.564229700 -0800
@@ -54,9 +54,6 @@
     double minExpectedThroughput{0};
     double maxExpectedThroughput{0};
 
-    RngSeedManager::SetSeed(1);
-    RngSeedManager::SetRun(7);
-
     CommandLine cmd(__FILE__);
     cmd.AddValue("nMpdus", "Number of aggregated MPDUs", nMpdus);
     cmd.AddValue("payloadSize", "Payload size in bytes", payloadSize);
@@ -127,9 +124,12 @@
     Config::Set("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/Mac/BE_MaxAmpduSize",
                 UintegerValue(maxAmpduSize));
 
-    int64_t streamNumber = 20;
-    streamNumber += WifiHelper::AssignStreams(apDevice, streamNumber);
-    streamNumber += WifiHelper::AssignStreams(staDevices, streamNumber);
+    int64_t streamNumber = 1;
+    int64_t streamIncrement = 1000; // large enough to avoid overlap from AssignStreams calls
+    WifiHelper::AssignStreams(apDevice, streamNumber);
+    streamNumber += streamIncrement;
+    WifiHelper::AssignStreams(staDevices, streamNumber);
+    streamNumber += streamIncrement;
 
     // Setting mobility model
     MobilityHelper mobility;
@@ -152,8 +152,10 @@
     InternetStackHelper stack;
     stack.Install(wifiApNode);
     stack.Install(wifiStaNodes);
-    streamNumber += stack.AssignStreams(wifiApNode, streamNumber);
-    streamNumber += stack.AssignStreams(wifiStaNodes, streamNumber);
+    stack.AssignStreams(wifiApNode, streamNumber);
+    streamNumber += streamIncrement;
+    stack.AssignStreams(wifiStaNodes, streamNumber);
+    streamNumber += streamIncrement;
 
     Ipv4AddressHelper address;
     address.SetBase("192.168.1.0", "255.255.255.0");
@@ -168,7 +170,8 @@
     ApplicationContainer serverApp = server.Install(wifiApNode);
     serverApp.Start(Seconds(0));
     serverApp.Stop(simulationTime + Seconds(1));
-    streamNumber += server.AssignStreams(wifiApNode, streamNumber);
+    server.AssignStreams(wifiApNode, streamNumber);
+    streamNumber += streamIncrement;
 
     UdpClientHelper client(ApInterface.GetAddress(0), port);
     client.SetAttribute("MaxPackets", UintegerValue(4294967295U));
@@ -179,7 +182,8 @@
     ApplicationContainer clientApp1 = client.Install(wifiStaNodes);
     clientApp1.Start(Seconds(1));
     clientApp1.Stop(simulationTime + Seconds(1));
-    streamNumber += client.AssignStreams(wifiStaNodes, streamNumber);
+    client.AssignStreams(wifiStaNodes, streamNumber);
+    streamNumber += streamIncrement;
 
     phy.EnablePcap("SimpleHtHiddenStations_Ap", apDevice.Get(0));
     phy.EnablePcap("SimpleHtHiddenStations_Sta1", staDevices.Get(0));
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-simple-interference.cc ns-3.47/examples/wireless/wifi-simple-interference.cc
--- ns-3.46/examples/wireless/wifi-simple-interference.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-simple-interference.cc	2026-02-16 16:08:49.564229700 -0800
@@ -46,7 +46,7 @@
 // For instance, for this configuration, the interfering frame arrives
 // at -90 dBm with a time offset of 3.2 microseconds:
 //
-// ./ns3 run "wifi-simple-interference --Irss=-90 --delta=3.2ns"
+// ./ns3 run "wifi-simple-interference --Irss=-90 --delta=3.2us"
 //
 // Note that all ns-3 attributes (not just the ones exposed in the below
 // script) can be changed at command line; see the documentation.
@@ -60,18 +60,22 @@
 // If you have tcpdump installed, you can try this:
 //
 // tcpdump -r wifi-simple-interference-0-0.pcap -nn -tt
-// reading from file wifi-simple-interference-0-0.pcap, link-type IEEE802_11_RADIO (802.11 plus BSD
-// radio information header) 10.008704 10008704us tsft 1.0 Mb/s 2437 MHz (0x00c0) -80dB signal -98dB
-// noise IP 10.1.1.2.49153 > 10.1.1.255.80: UDP, length 1000
+// reading from file wifi-simple-interference-0-0.pcap, link-type IEEE802_11_RADIO (802.11 plus
+// radiotap header) 10.008754 13308699848833236992us tsft fragmented 0.0 Mb/s 0 MHz 16dBm signal
+// 2dBm noise IP 10.0.0.2.49153 > 255.255.255.255.80: UDP, length 1000
+//
+// With a zero delta time offset, only the first packet will be decoded; the second packet
+// transmission must be delayed past the end of the first packet to receive it..
 //
 // Next, try this command and look at the tcpdump-- you should see two packets
 // that are no longer interfering:
-// ./ns3 run "wifi-simple-interference --delta=30000ns"
+// ./ns3 run "wifi-simple-interference --delta=9000us"
 
 #include "ns3/command-line.h"
 #include "ns3/config.h"
 #include "ns3/double.h"
 #include "ns3/internet-stack-helper.h"
+#include "ns3/ipv4-address-helper.h"
 #include "ns3/log.h"
 #include "ns3/mobility-helper.h"
 #include "ns3/mobility-model.h"
@@ -124,26 +128,11 @@
  *
  * @param socket The sending socket.
  * @param pktSize The packet size.
- * @param pktCount The packet counter.
- * @param pktInterval The interval between two packets.
  */
 static void
-GenerateTraffic(Ptr<Socket> socket, uint32_t pktSize, uint32_t pktCount, Time pktInterval)
+GenerateTraffic(Ptr<Socket> socket, uint32_t pktSize)
 {
-    if (pktCount > 0)
-    {
-        socket->Send(Create<Packet>(pktSize));
-        Simulator::Schedule(pktInterval,
-                            &GenerateTraffic,
-                            socket,
-                            pktSize,
-                            pktCount - 1,
-                            pktInterval);
-    }
-    else
-    {
-        socket->Close();
-    }
+    socket->Send(Create<Packet>(pktSize));
 }
 
 int
@@ -158,13 +147,9 @@
     bool verbose{false};
 
     // these are not command line arguments for this version
-    uint32_t numPackets{1};
-    Time interPacketInterval{"1s"};
     Time startTime{"10s"};
-    meter_u distanceToRx{100.0};
+    meter_u distanceToRx{100.0}; // If you change this, also change TxGain below
 
-    double offset{91}; // This is a magic number used to set the
-                       // transmit power, based on other configuration
     CommandLine cmd(__FILE__);
     cmd.AddValue("phyMode", "Wifi Phy mode", phyMode);
     cmd.AddValue("Prss", "Intended primary received signal strength (dBm)", Prss);
@@ -194,6 +179,9 @@
     // ns-3 supports RadioTap and Prism tracing extensions for 802.11b
     wifiPhy.SetPcapDataLinkType(WifiPhyHelper::DLT_IEEE802_11_RADIO);
 
+    // Disable preamble detection model to receive signals below -82 dBm
+    wifiPhy.DisablePreambleDetectionModel();
+
     YansWifiChannelHelper wifiChannel;
     wifiChannel.SetPropagationDelay("ns3::ConstantSpeedPropagationDelayModel");
     wifiChannel.AddPropagationLoss("ns3::LogDistancePropagationLossModel");
@@ -209,15 +197,23 @@
     // Set it to adhoc mode
     wifiMac.SetType("ns3::AdhocWifiMac");
     NetDeviceContainer devices = wifi.Install(wifiPhy, wifiMac, c.Get(0));
-    // This will disable these sending devices from detecting a signal
-    // so that they do not backoff
-    wifiPhy.Set("TxGain", DoubleValue(offset + Prss));
+    // Setting RxSensitivity to 0 dBm will disable the two sending devices from detecting
+    // received signals, so that they do not back off
+    wifiPhy.Set("RxSensitivity", DoubleValue(0));
+    // We use the TxGain parameter on each sender to control the received signal power.
+    // The transmit power is roughly 16 dBm.  The signal attenuation from both senders to
+    // the receiving device is 106.7 dB (100 meters at this frequency, based on the
+    // LogDistancePropagationLossModel).  We want the receive signal strength to be
+    // Prss dBm.  We therefore want to solve for the gain as follows:
+    // 16 dBm + TxGain - propagationLoss = Prss (dBm)
+    // Working backwards, TxGain = Prss (dBm) - 16 dB + 106.7 dB = Prss (dBm) + 90.7 dB
+    dB_u powerOffset{90.7};
+    wifiPhy.Set("TxGain", DoubleValue(Prss + powerOffset));
     devices.Add(wifi.Install(wifiPhy, wifiMac, c.Get(1)));
-    wifiPhy.Set("TxGain", DoubleValue(offset + Irss));
+    // Repeat for the interferer
+    wifiPhy.Set("TxGain", DoubleValue(Irss + powerOffset));
     devices.Add(wifi.Install(wifiPhy, wifiMac, c.Get(2)));
 
-    // Note that with FixedRssLossModel, the positions below are not
-    // used for received signal strength.
     MobilityHelper mobility;
     Ptr<ListPositionAllocator> positionAlloc = CreateObject<ListPositionAllocator>();
     positionAlloc->Add(Vector(0.0, 0.0, 0.0));
@@ -230,6 +226,11 @@
     InternetStackHelper internet;
     internet.Install(c);
 
+    Ipv4AddressHelper address;
+    address.SetBase("10.0.0.0", "255.255.255.0");
+    Ipv4InterfaceContainer ipInterfaces;
+    ipInterfaces = address.Assign(devices);
+
     TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
     Ptr<Socket> recvSink = Socket::CreateSocket(c.Get(0), tid);
     InetSocketAddress local = InetSocketAddress(Ipv4Address("10.1.1.1"), 80);
@@ -259,17 +260,13 @@
                                    startTime,
                                    &GenerateTraffic,
                                    source,
-                                   PpacketSize,
-                                   numPackets,
-                                   interPacketInterval);
+                                   PpacketSize);
 
     Simulator::ScheduleWithContext(interferer->GetNode()->GetId(),
                                    startTime + delta,
                                    &GenerateTraffic,
                                    interferer,
-                                   IpacketSize,
-                                   numPackets,
-                                   interPacketInterval);
+                                   IpacketSize);
 
     Simulator::Run();
     Simulator::Destroy();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/examples/wireless/wifi-sleep.cc ns-3.47/examples/wireless/wifi-sleep.cc
--- ns-3.46/examples/wireless/wifi-sleep.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/examples/wireless/wifi-sleep.cc	2026-02-16 16:08:49.564229700 -0800
@@ -107,7 +107,7 @@
     dBm_u txPowerStart{0.0};
     dBm_u txPowerEnd{15.0};
     uint32_t nTxPowerLevels{16};
-    uint32_t txPowerLevel{0};
+    uint32_t txPowerLevel{WIFI_MIN_TX_PWR_LEVEL};
     ampere_u idleCurrent{0.273};
     ampere_u txCurrent{0.380};
     bool verbose{false};
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/.github/workflows/per_commit.yml ns-3.47/.github/workflows/per_commit.yml
--- ns-3.46/.github/workflows/per_commit.yml	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/.github/workflows/per_commit.yml	1969-12-31 16:00:00.000000000 -0800
@@ -1,219 +0,0 @@
-name: "CI"
-
-on: [push]
-jobs:
-  Ubuntu:
-    runs-on: ubuntu-latest
-    outputs:
-      cache_misses: ${{ env.cache_misses }}
-    steps:
-      - uses: actions/checkout@v4
-      - name: Install required packages
-        run: |
-          sudo apt-get update
-          sudo apt-get -y install apt-utils
-          sudo apt-get -y install git gcc g++ cmake python3 make ninja-build
-          sudo apt-get -y install tcpdump libgsl-dev libxml2-dev
-          sudo apt-get -y install curl unzip tar
-          sudo apt-get -y install ccache
-      - name: Get timestamp
-        id: time
-        run: python3 -c "from datetime import datetime; print('time='+datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))" >> $GITHUB_ENV
-      - name: Restore ccache
-        id: ccache
-        uses: actions/cache@v4
-        with:
-          path: .ccache
-          key: ubuntu-ci-${{env.time}}
-          restore-keys: ubuntu-ci-
-      - name: Setup ccache
-        run: |
-          ccache --set-config=cache_dir="$GITHUB_WORKSPACE/.ccache"
-          ccache --set-config=max_size=400M
-          ccache --set-config=compression=true
-          ccache -z
-      - name: Configure CMake
-        run: |
-          ./ns3 configure -d release --enable-asserts --enable-examples --enable-tests --disable-werror -G"Ninja"
-      - name: Build ns-3
-        run: |
-          ./ns3 build
-      - name: Print ccache statistics
-        id: ccache_results
-        run: |
-          ccache -s
-          python3 -c "import re, subprocess;print('cache_misses=%d' % int(re.findall('cache_miss(.*)', subprocess.check_output(['ccache', '--print-stats']).decode())[0]))" >> $GITHUB_ENV
-      - name: Run tests and examples
-        if: env.cache_misses != '0'
-        run: python3 test.py --no-build --verbose-failed
-
-  Codecov:
-    runs-on: ubuntu-latest
-    needs: Ubuntu
-    if: needs.Ubuntu.outputs.cache_misses != '0'
-    steps:
-      - uses: actions/checkout@v4
-      - name: Install required packages
-        run: |
-          sudo apt-get update
-          sudo apt-get -y install apt-utils
-          sudo apt-get -y install git gcc g++ cmake python3 make ninja-build
-          sudo apt-get -y install tcpdump libgsl-dev libxml2-dev
-          sudo apt-get -y install curl unzip tar
-          sudo apt-get -y install ccache
-          sudo apt-get -y install lcov
-      - name: Get timestamp
-        id: time
-        run: python3 -c "from datetime import datetime; print('time='+datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))" >> $GITHUB_ENV
-      - name: Restore ccache
-        id: ccache
-        uses: actions/cache@v4
-        with:
-          path: .ccache
-          key: ubuntu-coverage-${{env.time}}
-          restore-keys: ubuntu-coverage-
-      - name: Setup ccache
-        run: |
-          ccache --set-config=cache_dir="$GITHUB_WORKSPACE/.ccache"
-          ccache --set-config=max_size=400M
-          ccache --set-config=compression=true
-          ccache -z
-      - name: Configure CMake
-        run: |
-          ./ns3 configure --enable-asserts --enable-examples --enable-tests --disable-werror --enable-gcov -G"Ninja"
-      - name: Build ns-3
-        run: |
-          ./ns3 build
-      - name: Print ccache statistics
-        id: ccache_results
-        run: |
-          ccache -s
-          python3 -c "import re, subprocess;print('cache_misses=%d' % int(re.findall('cache_miss(.*)', subprocess.check_output(['ccache', '--print-stats']).decode())[0]))" >> $GITHUB_ENV
-      - name: Generate coverage data and submit to codecov.io
-        if: env.cache_misses != '0'
-        run: |
-          ./ns3 build coverage_gcc
-          cd ./build/coverage
-          bash <(curl -s https://codecov.io/bash) -f ns3.info || echo "Codecov did not collect coverage reports"
-        env:
-          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-  Windows_MinGW:
-    runs-on: windows-latest
-    defaults:
-      run:
-        shell: msys2 {0}
-    steps:
-      - uses: actions/checkout@v4
-      - uses: msys2/setup-msys2@v2
-      - name: Install required msys2/mingw64 packages
-        run: |
-          pacman -S --noconfirm \
-          unzip \
-          tar \
-          mingw-w64-x86_64-curl \
-          mingw-w64-x86_64-binutils \
-          mingw-w64-x86_64-cmake \
-          mingw-w64-x86_64-gcc \
-          mingw-w64-x86_64-ninja \
-          mingw-w64-x86_64-python \
-          mingw-w64-x86_64-ccache \
-          mingw-w64-x86_64-gsl \
-          mingw-w64-x86_64-libxml2 \
-          mingw-w64-x86_64-lld \
-          mingw-w64-x86_64-sed
-          pacman --noconfirm -Scc
-      - name: Get timestamp
-        id: time
-        run: python3 -c "from datetime import datetime; print('time='+datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))" >> $GITHUB_ENV
-      - name: Restore ccache
-        id: ccache
-        uses: actions/cache@v4
-        with:
-          path: .ccache
-          key: msys2-${{env.time}}
-          restore-keys: msys2-
-      - name: Setup ccache
-        run: |
-          ccache --set-config=cache_dir="$GITHUB_WORKSPACE/.ccache"
-          ccache --set-config=max_size=400M
-          ccache --set-config=compression=true
-          ccache -z
-      - name: Configure CMake
-        run: |
-          python3 ns3 configure -d release --enable-asserts --enable-examples --enable-tests --disable-werror -G"Ninja"
-      - name: Build ns-3
-        run: |
-          python3 ns3 build
-      - name: Print ccache statistics
-        id: ccache_results
-        run: |
-          ccache -s
-          python3 -c "import re, subprocess;print('cache_misses=%d' % int(re.findall('cache_miss(.*)', subprocess.check_output(['ccache', '--print-stats']).decode())[0]))" >> $GITHUB_ENV
-      - name: Run tests and examples
-        if: env.cache_misses != '0'
-        run: python3 test.py --no-build --verbose-failed
-
-  Mac_OS_X:
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: [ macos-latest, macos-14 ]
-    steps:
-      - uses: actions/checkout@v4
-      - name: Install required packages
-        run: |
-          brew uninstall cmake ninja
-          brew install ninja cmake ccache libxml2 gsl open-mpi #qt5
-      - name: Get timestamp
-        id: time
-        run: python3 -c "from datetime import datetime; print('time='+datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))" >> $GITHUB_ENV
-      - name: Restore ccache
-        id: ccache
-        uses: actions/cache@v4
-        with:
-          path: .ccache
-          key: osx_brew-ci-${{env.time}}
-          restore-keys: osx_brew-ci-
-      - name: Setup ccache
-        run: |
-          export PATH=/usr/local/bin:$PATH #:/usr/local/opt/qt/bin
-          ccache --set-config=cache_dir="$GITHUB_WORKSPACE/.ccache"
-          ccache --set-config=max_size=400M
-          ccache --set-config=compression=true
-          ccache -z
-      - name: Configure CMake
-        run: |
-          export PATH=/usr/local/bin:$PATH #:/usr/local/opt/qt/bin
-          ./ns3 configure -d release --enable-asserts --enable-examples --enable-tests --disable-werror -G"Ninja"
-      - name: Build ns-3
-        run: |
-          export PATH="$PATH" #:/usr/local/opt/qt/bin
-          ./ns3 build
-      - name: Print ccache statistics
-        id: ccache_results
-        run: |
-          ccache -s
-          python3 -c "import re, subprocess;print('cache_misses=%d' % int(re.findall('cache_miss(.*)', subprocess.check_output(['ccache', '--print-stats']).decode())[0]))" >> $GITHUB_ENV
-      - name: Run tests and examples
-        if: env.cache_misses != '0'
-        run: ./test.py --no-build --verbose-failed
-  Windows_Visual_Studio:
-    runs-on: windows-latest
-    defaults:
-      run:
-        shell: cmd
-    steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-python@v5
-      - name: Configure CMake
-        run: |
-          %comspec% /k ""C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat""
-          python ns3 configure --enable-tests --enable-examples --enable-asserts -d release -- -G"Visual Studio 17 2022" -A x64 -T ClangCL
-      - name: Build ns-3
-        run: |
-          %comspec% /k ""C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat""
-          python ns3 build
-      - name: Run tests and examples
-        run: |
-          set PATH=%CD%\build;%PATH%
-          python .\test.py --no-build --verbose-failed
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/LICENSE ns-3.47/LICENSE
--- ns-3.46/LICENSE	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/LICENSE	2026-02-16 16:08:49.473229400 -0800
@@ -1,3 +1,8 @@
+This project contains contributions that are mostly licensed as GPLv2-only,
+but some code is licensed differently; the LICENSES directory contains
+the set of licenses present in the codebase.  The common denominator and
+requirement for contribution is compatibility with the GPLv2-only license.
+
 		    GNU GENERAL PUBLIC LICENSE
 		       Version 2, June 1991
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/ns3 ns-3.47/ns3
--- ns-3.46/ns3	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/ns3	2026-02-16 16:08:49.565229700 -0800
@@ -88,7 +88,7 @@
     # Instead of copying and pasting repeated arguments for each parser, we add them here
     for subparser in parsers:
         subparser_name = subparser.prog.replace("ns3", "").strip()
-        destination = ("%s_%s" % (subparser_name, dest)) if subparser_name else dest
+        destination = (f"{subparser_name}_{dest}") if subparser_name else dest
         subparser.add_argument(
             *arguments, help=help_msg, action=action, default=default_value, dest=destination
         )
@@ -96,13 +96,19 @@
 
 def parse_args(argv):
     py39args = {"exit_on_error": False}
+    py314args = {"color": False}
     if sys.version_info < (3, 9):
         py39args = {}
+    if sys.version_info < (3, 14):
+        py314args = {}
     parser = argparse.ArgumentParser(
-        description="ns-3 wrapper for the CMake build system", add_help=False, **py39args
+        description="ns-3 wrapper for the CMake build system",
+        add_help=False,
+        **py39args,
+        **py314args,
     )
 
-    sub_parser = parser.add_subparsers()
+    sub_parser = parser.add_subparsers(dest="subparser")
 
     parser.add_argument(
         "-h",
@@ -126,9 +132,7 @@
         metavar="compile_or_die",
     )
     parser_help = sub_parser.add_parser("help", help="Print a summary of available commands")
-    parser_help.add_argument(
-        "help", help="Print a summary of available commands", action="store_true", default=False
-    )
+
     # parser.add_argument('--docset',
     #               help=(
     #                   'Create Docset, without building. This requires the docsetutil tool from Xcode 9.2 or earlier.'
@@ -145,7 +149,7 @@
         formatter_class=argparse.RawTextHelpFormatter,
     )
     parser_build.add_argument(
-        "build",
+        "target",
         help=(
             "Build the entire project or the specified target and its dependencies.\n"
             "To get the list of targets, use:\n"
@@ -153,14 +157,11 @@
         ),
         action="store",
         nargs="*",
-        default=None,
-        metavar="target",
     )
 
     parser_configure = sub_parser.add_parser(
         "configure", help='Try "./ns3 configure --help" for more configuration options'
     )
-    parser_configure.add_argument("configure", action="store_true", default=False)
     parser_configure.add_argument(
         "-d",
         "--build-profile",
@@ -205,6 +206,7 @@
         ),
         ("build-version", "embedding git changes as a build version during build"),
         ("clang-tidy", "clang-tidy static analysis"),
+        ("clang-tidy-fix", "clang-tidy static analysis and apply automatic fixes"),
         ("dpdk", "the fd-net-device DPDK features"),
         ("eigen", "Eigen3 library support"),
         ("examples", "the ns-3 examples"),
@@ -321,18 +323,14 @@
     )
 
     parser_clean = sub_parser.add_parser("clean", help="Removes files created by ns3")
-    parser_clean.add_argument("clean", action="store_true", default=False)
 
     parser_distclean = sub_parser.add_parser(
         "distclean", help="Removes files created by ns3, tests and documentation"
     )
-    parser_distclean.add_argument("distclean", action="store_true", default=False)
 
     parser_install = sub_parser.add_parser("install", help="Install ns-3")
-    parser_install.add_argument("install", action="store_true", default=False)
 
     parser_uninstall = sub_parser.add_parser("uninstall", help="Uninstall ns-3")
-    parser_uninstall.add_argument("uninstall", action="store_true", default=False)
 
     parser_run = sub_parser.add_parser(
         "run",
@@ -340,7 +338,7 @@
         formatter_class=argparse.RawTextHelpFormatter,
     )
     parser_run.add_argument(
-        "run",
+        "target",
         help=(
             "Build and run the target executable.\n"
             "If --no-build is present, the build step is skipped.\n"
@@ -353,7 +351,6 @@
         ),
         default="",
         nargs="?",
-        metavar="target",
     )
     parser_run.add_argument(
         "--no-build", help="Skip build step.", action="store_true", default=False
@@ -426,20 +423,14 @@
     )
 
     parser_shell = sub_parser.add_parser(
-        "shell", help='Try "./ns3 shell --help" for more shell options'
-    )
-    parser_shell.add_argument(
-        "shell",
-        help="Export necessary environment variables and open a shell",
-        action="store_true",
-        default=False,
+        "shell", help="Export necessary environment variables and open a shell"
     )
 
     parser_docs = sub_parser.add_parser(
         "docs", help='Try "./ns3 docs --help" for more documentation options'
     )
     parser_docs.add_argument(
-        "docs",
+        "target",
         help="Build project documentation",
         choices=[
             "contributing",
@@ -545,8 +536,31 @@
         print("\n".join(textwrap.wrap(error_message, width=TERMINAL_WIDTH)))
         exit(1)
 
+    # Since Python 3.14, we cannot store_true positional arguments.
+    # To keep same behavior, we emulate what we had: one flag set to false for every subparser
+    # not set to anything. And build is an empty string.
+    def set_subparser_flags(parser, args):
+        sub_parser_actions = list(
+            filter(lambda x: isinstance(x, argparse._SubParsersAction), parser._actions)
+        )
+        sub_parsers = list(sub_parser_actions[0].choices.keys())
+        if args.subparser in sub_parsers:
+            sub_parsers.remove(args.subparser)
+
+        for parser in sub_parsers:
+            setattr(args, parser, "" if parser in ["build", "run", "show"] else False)
+
+        if args.subparser not in [None, "build", "run", "show"]:
+            setattr(args, args.subparser, True)
+
+        if args.subparser in ["build", "run", "docs"]:
+            setattr(args, args.subparser, args.target)
+        return args
+
+    args = set_subparser_flags(parser, args)
+
     # If run doesn't have a target, print the help message of the run parser
-    if "run" in args and args.run == "":
+    if args.subparser == "run" and args.run == "":
         parser_run.print_help()
         exit(-1)
 
@@ -792,23 +806,24 @@
                     current_cmake_generator = line.split("=")[-1]
 
     if not current_cmake_generator:
+        supported_cmake_generators = subprocess.check_output(["cmake", "--help"]).decode()
         # Search for available generators
-        cmake_generator_map = {
+        cmake_build_tool_to_generator_map = {
             "msbuild": "Visual Studio 17 2022",
             "ninja": "Ninja",
             "make": "Unix Makefiles",
             "xcodebuild": "Xcode",
         }
         available_generators = []
-        for generator in cmake_generator_map.keys():
-            if shutil.which(generator):
+        for build_tool, generator in cmake_build_tool_to_generator_map.items():
+            if shutil.which(build_tool) and generator in supported_cmake_generators:
                 available_generators.append(generator)
 
         # Select the first one
         if len(available_generators) == 0:
             raise Exception("No generator available.")
 
-        current_cmake_generator = cmake_generator_map[available_generators[0]]
+        current_cmake_generator = available_generators[0]
 
     return current_cmake_cache_folder, current_cmake_generator
 
@@ -1429,7 +1444,8 @@
 
 def run_step(args, target_to_run, target_args):
     libdir = "%s/lib" % out_dir
-
+    if not os.path.exists(libdir):
+        libdir += "64"
     custom_env = {
         "PATH": libdir,
         "PYTHONPATH": "%s/bindings/python" % out_dir,
@@ -1517,16 +1533,23 @@
 
         # running with command template?
         if args.command_template:
-            commands = (args.command_template % target_to_run).split()
-            check_program_installed(commands[0])
+            commands = args.command_template % f'"{target_to_run}"'
+            commands = list(re.findall(r'(?:".*?"|\S)+', commands))
+            commands = list(map(lambda x: x.replace('"', ""), commands))
             target_to_run = commands[0]
-            target_args = commands[1:] + target_args
+            commands = commands[1:]
+            check_program_installed(target_to_run)
+            target_args = commands + target_args
 
             # running mpi on the CI?
             if target_to_run in ["mpiexec", "mpirun"] and os.getenv("MPI_CI"):
                 if shutil.which("ompi_info"):
                     try:
-                        subprocess.check_call([target_to_run, "--allow-run-as-root", "dir"])
+                        subprocess.check_call(
+                            [target_to_run, "--allow-run-as-root", "cmake", "--version"],
+                            stdout=subprocess.DEVNULL,
+                            stderr=subprocess.DEVNULL,
+                        )
                         target_args = ["--allow-run-as-root"] + target_args
                     except subprocess.CalledProcessError:
                         pass
@@ -1970,6 +1993,19 @@
                 target_to_run = target_to_run[1:-1]
             target_to_run = target_to_run.split()
             target_to_run, target_args = target_to_run[0], target_to_run[1:]
+            # Remove version prefixes and build profile suffices if they exist
+            if os.path.basename(target_to_run).startswith("ns3"):
+                pardir = os.path.dirname(target_to_run)
+                components = os.path.basename(target_to_run).split("-")
+                if "ns3" in components[0]:
+                    components.pop(0)
+                if "dev" == components[0]:
+                    components.pop(0)
+                if "rc" in components[0]:
+                    components.pop(0)
+                if components[-1] in ["debug", "relwithdebinfo", "default", "optimized"]:
+                    components.pop(-1)
+                target_to_run = f"{pardir}/{'-'.join(components)}"
         else:
             raise Exception("You need to specify a program to run")
 
@@ -2052,6 +2088,24 @@
         elif ".py" in target_to_run and os.path.exists(target_to_run):
             # We let python scripts pass to be able to run ./utils/python-unit-tests.py
             pass
+        elif "clang-tidy" in target_to_run:
+            clang_tidy_versions = ["run-clang-tidy"]
+            clang_tidy_versions = list(map(lambda x: shutil.which(x), clang_tidy_versions))
+            clang_tidy_versions = list(filter(lambda x: x, clang_tidy_versions))
+            if not clang_tidy_versions:
+                raise Exception(
+                    "Clang-tidy not found; please install it, or modify the PATH environment variable"
+                )
+            target_to_run = clang_tidy_versions[-1]
+            target_args = [
+                "-p",
+                current_cmake_cache_folder,
+                "-quiet",
+                "-format",
+                "-warnings-as-errors=*",
+            ] + target_args
+            run_only = True
+            build_and_run = False
         else:
             raise Exception("Couldn't find the specified program: %s" % target_to_run)
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/RELEASE_NOTES.md ns-3.47/RELEASE_NOTES.md
--- ns-3.46/RELEASE_NOTES.md	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/RELEASE_NOTES.md	2026-02-16 16:16:25.802885300 -0800
@@ -12,6 +12,108 @@
 and references prefixed by '!' refer to a
 [GitLab.com merge request](https://gitlab.com/nsnam/ns-3-dev/-/merge_requests) number.
 
+## Release 3.47
+
+This release is available from:
+<https://www.nsnam.org/release/ns-3.47.tar.bz2>
+
+### Supported platforms
+
+This release is intended to work on systems with the following minimal
+requirements (Note: not all ns-3 features are available on all systems):
+
+- g++-11.1 or later, or LLVM/clang++-17 or later
+- Python 3.10 or later
+- CMake 3.20 or later
+- (macOS only) Xcode 16.2 or later
+- (Windows only) Msys2/MinGW64, Msys2/UCRT64 and ClangCL/MSVC toolchains, or WSL2
+
+This release of ns-3 updated the minimum supported version of Python from 3.8 to 3.10.
+
+Contributors or maintainers who require the use of cmake-format (CMake formatting utility) to pass
+the ns-3 CI checks should note that only Python 3.12 is currently supported for cmake-format.
+Outside of this utility, Python 3.10 through 3.14 should work with this release.
+
+The version of clang-format enforced by the check-style-clang-format.py script for
+this release is version 20 only.
+
+Version 21 of the clang-tidy linter is now supported and recommended, although
+versions 17 to 20 are still compatible.
+
+Python API requires [Cppyy](https://cppyy.readthedocs.io/en/latest/installation.html) and has only
+been tested on Linux. As of this release, the latest known version to work with ns-3 is cppyy==3.5.0.
+
+### New user-visible features
+
+Numbers preceded by '!' reference GitLab.com Merge Request numbers (for more information)
+Numbers preceded by '#' reference GitLab.com Issue numbers (for more information)
+
+- (applications) !2611 New IEEE 802.11ax evaluation methodology traffic generators for video, VoIP, virtual desktop, and real-time mobile gaming
+- (core) !2633 Added `SystemPath::FindSelf()` to retrieve the full path to the running executable.
+- (examples) !2686 New TCP BBR Python example program.
+- (internet) !2405 Added support for TCP FACK (Forward Acknowledgement).
+- (internet) !2530 Added IPv6 support to Global Routing.
+- (internet) !2434 Added `PrintRouting` functionality to `GlobalRouting` similar to `TraceRoute`.
+- (lr-wpan) !2592 Added PHY preamble and RSSI support.
+- (propagation) !2626 Added 3GPP outdoor-to-indoor (O2I) penetration losses for sub-6 GHz frequencies according to TS 38.901 Table 7.4.3-3, including vehicular O2I losses.
+- (spectrum) !2490 The ns-3 3GPP channel model now supports a spatial-consistency update technique
+  aligned with TR 38.901 Procedure A (Sec. 7.6.3.2).
+- (visualizer) !2663 Add Lr-Wpan NetDevices support to the Pyviz visualizer.
+
+### Bugs fixed
+
+- (core) #1252 - Handle >1023 character paths to self on Linux
+- (internet) !2624 - Fix TCP LEDBAT congestion window growth calculation
+- (network) #1226 - Fix Ipv6Prefix and Ipv4Mask default constructors
+- (network) !2728 Fix deserialization, TLV field alignment, and MCS padding issues in Radiotap header
+- (propagation) !2593 Fix 3GPP O2I loss distributions that used standard deviation instead of variance from TS 38.901 Table 7.4.3-2
+- (spectrum) #1314 - Fix missing complex conjugation of RX beamforming vector in 3GPP long-term channel component calculation.
+- (wifi) !2613 Fix incorrect aPSDUMaxLength value for 802.11be.
+- (wifi) !2614 Fix hardcoded threshold value in EHT PHY to determine per-20MHz CCA indication.
+- (wifi) !2614 Fix spurious CCA-BUSY notification after channel width has changed
+- (wifi) #1295 - Avoid floating point precision issues in wifi-error-rate-models-test.cc
+- (wifi) #1284 - Fix radiotap for received packets
+- (wifi) !2599 Fix misuse of TX power and power level in remote station manager
+- (visualizer) !2636 - Fix overlapping labels and simulation stop time in PyViz visualizer.
+
+## Release 3.46.1
+
+ns-3.46.1 is a small update to ns-3.46 to fix build issues discovered after release.
+There should be no model behavior or API changes compared with ns-3.46.
+
+This release is available from:
+<https://www.nsnam.org/release/ns-3.46.1.tar.bz2>
+
+### Supported platforms
+
+This release is intended to work on systems with the following minimal
+requirements (Note: not all ns-3 features are available on all systems):
+
+- g++-11.1 or later, or LLVM/clang++-17 or later
+- Python 3.8 or later
+- CMake 3.20 or later
+- (macOS only) Xcode 16.2 or later
+- (Windows only) Msys2/MinGW64, Msys2/UCRT64 and ClangCL/MSVC toolchains, or WSL2
+
+The version of clang-format enforced by the check-style-clang-format.py script for
+this release is version 20 only.
+
+Version 20 of the clang-tidy linter is now supported and recommended, although versions 15-19
+are still compatible.
+
+Python API requires [Cppyy](https://cppyy.readthedocs.io/en/latest/installation.html) and has only
+been tested on Linux. As of this release, the latest known version to work with ns-3 is cppyy==3.5.0.
+
+### New user-visible features
+
+- (doc) New module documentation for mobility and propagation modules
+
+### Bugs fixed
+
+- (build) The ns3 script was not compatible with Python 3.14
+- (build) A missing header include in test.cc was breaking the g++-12 build
+- (build) Fixed OpenMPI-based build on Alpine Linux
+
 ## Release 3.46
 
 This release is available from:
@@ -100,6 +202,7 @@
 - (lr-wpan) - !2429 - Documentation update and small reformat fixes.
 - (wifi) 320 MHz channel support was added to the 802.11be model
 - (wifi) Added the `ProtectSingleExchange` attribute to the `QosFrameExchangeManager` to choose whether the NAV protection should cover the entire TXOP or only the current frame exchange when the TXOP limit is non-zero. In that case, the Duration/ID field in frames establishing the protection is set to the time remaining until the end of the current frame exchange. It is also possible to select whether the NAV duration should be extended by an additional time to protect beyond end of the immediate frame exchange via the `SingleExchangeProtectionSurplus` attribute of the `QosFrameExchangeManager`.
+- (tcp) !2410 - Added support for ABE (Alternative Backoff with ECN)
 
 ### Bugs fixed
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/scratch/nested-subdir/lib/scratch-nested-subdir-library-header.h ns-3.47/scratch/nested-subdir/lib/scratch-nested-subdir-library-header.h
--- ns-3.46/scratch/nested-subdir/lib/scratch-nested-subdir-library-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/scratch/nested-subdir/lib/scratch-nested-subdir-library-header.h	2026-02-16 16:08:49.565229700 -0800
@@ -6,6 +6,9 @@
 // in a scratch subdirectory, one can create new programs that are implemented
 // in multiple files and headers.
 
+#ifndef SCRATCH_NESTED_SUBDIR_LIBRARY_HEADER_H
+#define SCRATCH_NESTED_SUBDIR_LIBRARY_HEADER_H
+
 #include <string>
 
 namespace ns3
@@ -19,3 +22,5 @@
 std::string ScratchNestedSubdirGetMessage();
 
 } // namespace ns3
+
+#endif // SCRATCH_NESTED_SUBDIR_LIBRARY_HEADER_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/scratch/subdir/scratch-subdir-additional-header.h ns-3.47/scratch/subdir/scratch-subdir-additional-header.h
--- ns-3.46/scratch/subdir/scratch-subdir-additional-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/scratch/subdir/scratch-subdir-additional-header.h	2026-02-16 16:08:49.565229700 -0800
@@ -6,6 +6,9 @@
 // in a scratch subdirectory, one can create new programs that are implemented
 // in multiple files and headers.
 
+#ifndef SCRATCH_SUBDIR_ADDITIONAL_HEADER_H
+#define SCRATCH_SUBDIR_ADDITIONAL_HEADER_H
+
 #include <string>
 
 namespace ns3
@@ -19,3 +22,5 @@
 std::string ScratchSubdirGetMessage();
 
 } // namespace ns3
+
+#endif // SCRATCH_SUBDIR_ADDITIONAL_HEADER_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/antenna/examples/adjacency-matrix-example.cc ns-3.47/src/antenna/examples/adjacency-matrix-example.cc
--- ns-3.46/src/antenna/examples/adjacency-matrix-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/antenna/examples/adjacency-matrix-example.cc	2026-02-16 16:08:49.566229800 -0800
@@ -7,6 +7,11 @@
  */
 
 /**
+ * @ingroup antenna
+ * @defgroup antenna-examples Antenna Examples
+ */
+
+/**
  * @file
  * @ingroup antenna-examples
  * Example program illustrating one application of symmetric adjacency matrices for routing
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/antenna/utils/symmetric-adjacency-matrix.h ns-3.47/src/antenna/utils/symmetric-adjacency-matrix.h
--- ns-3.46/src/antenna/utils/symmetric-adjacency-matrix.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/antenna/utils/symmetric-adjacency-matrix.h	2026-02-16 16:08:49.567229700 -0800
@@ -8,6 +8,7 @@
 #ifndef NS3_SYMMETRIC_ADJACENCY_MATRIX_H
 #define NS3_SYMMETRIC_ADJACENCY_MATRIX_H
 
+#include <algorithm>
 #include <vector>
 
 namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/aodv/model/aodv-routing-protocol.cc ns-3.47/src/aodv/model/aodv-routing-protocol.cc
--- ns-3.46/src/aodv/model/aodv-routing-protocol.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/aodv/model/aodv-routing-protocol.cc	2026-02-16 16:08:49.568229700 -0800
@@ -1638,7 +1638,7 @@
     RrepHeader rrepHeader;
     p->RemoveHeader(rrepHeader);
     Ipv4Address dst = rrepHeader.GetDst();
-    NS_LOG_LOGIC("RREP destination " << dst << " RREP origin " << rrepHeader.GetOrigin());
+    NS_LOG_LOGIC("Route destination " << dst << " Route origin " << rrepHeader.GetOrigin());
 
     uint8_t hop = rrepHeader.GetHopCount() + 1;
     rrepHeader.SetHopCount(hop);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/aodv/test/aodv-regression.cc ns-3.47/src/aodv/test/aodv-regression.cc
--- ns-3.46/src/aodv/test/aodv-regression.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/aodv/test/aodv-regression.cc	2026-02-16 16:08:49.568229700 -0800
@@ -162,6 +162,9 @@
 {
     // 1. Setup WiFi
     int64_t streamsUsed = 0;
+    int64_t totalStreamsUsed = 0;
+    int64_t streamNumber = 0;
+    int64_t streamIncrement = 1000;
     WifiMacHelper wifiMac;
     wifiMac.SetType("ns3::AdhocWifiMac");
     YansWifiPhyHelper wifiPhy;
@@ -182,27 +185,47 @@
     NetDeviceContainer devices = wifi.Install(wifiPhy, wifiMac, *m_nodes);
 
     // Assign fixed stream numbers to wifi and channel random variables
-    streamsUsed += WifiHelper::AssignStreams(devices, streamsUsed);
-    // Assign 6 streams per device
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (devices.GetN() * 2), "Stream assignment mismatch");
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
+    constexpr int expectedWifiStreamsPerDevice = 2;
+    streamsUsed = WifiHelper::AssignStreams(devices, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
+    // Assign 2 streams per device
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed,
+                          (devices.GetN() * expectedWifiStreamsPerDevice),
+                          "Stream assignment mismatch");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
     // Assign 0 streams per channel for this configuration
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (devices.GetN() * 2), "Stream assignment mismatch");
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed, 0, "No WifiChannel stream usage expected");
 
     // 2. Setup TCP/IP & AODV
     AodvHelper aodv; // Use default parameters here
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.SetRoutingHelper(aodv);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
-    // InternetStack uses m_size more streams
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (devices.GetN() * 5) + m_size, "Stream assignment mismatch");
-    streamsUsed += aodv.AssignStreams(*m_nodes, streamsUsed);
-    // AODV uses m_size more streams
+    streamsUsed = internetStack.AssignStreams(*m_nodes, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
+    constexpr int expectedArpL3ProtocolStreams = 1;
+    constexpr int expectedInternetStreamsPerDevice = expectedArpL3ProtocolStreams;
+    // InternetStack uses numDevices * expectedInternetStreamsPerDevice more streams
     NS_TEST_ASSERT_MSG_EQ(streamsUsed,
-                          ((devices.GetN() * 5) + (2 * m_size)),
+                          (devices.GetN() * expectedInternetStreamsPerDevice),
                           "Stream assignment mismatch");
 
+    constexpr int expectedAodvStreamsPerDevice = 1;
+    streamsUsed = aodv.AssignStreams(*m_nodes, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
+    // AODV uses numDevices * expectedAodvStreamsPerDevice
+    NS_TEST_ASSERT_MSG_EQ(
+        totalStreamsUsed,
+        (devices.GetN() * (expectedWifiStreamsPerDevice + expectedInternetStreamsPerDevice +
+                           expectedAodvStreamsPerDevice)),
+        "Stream assignment mismatch");
+
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     Ipv4InterfaceContainer interfaces = address.Assign(devices);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/aodv/test/bug-772.cc ns-3.47/src/aodv/test/bug-772.cc
--- ns-3.46/src/aodv/test/bug-772.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/aodv/test/bug-772.cc	2026-02-16 16:08:49.569229800 -0800
@@ -124,6 +124,9 @@
 Bug772ChainTest::CreateDevices()
 {
     int64_t streamsUsed = 0;
+    int64_t totalStreamsUsed = 0;
+    int64_t streamNumber = 0;
+    int64_t streamIncrement = 1000;
     // 1. Setup WiFi
     WifiMacHelper wifiMac;
     wifiMac.SetType("ns3::AdhocWifiMac");
@@ -150,28 +153,51 @@
     NetDeviceContainer devices = wifi.Install(wifiPhy, wifiMac, *m_nodes);
 
     // Assign fixed stream numbers to wifi and channel random variables
-    streamsUsed += WifiHelper::AssignStreams(devices, streamsUsed);
+    streamsUsed = WifiHelper::AssignStreams(devices, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
+    constexpr int expectedWifiStreamsPerDevice = 2;
     // Assign 6 streams per device
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (devices.GetN() * 2), "Stream assignment mismatch");
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed,
+                          (devices.GetN() * expectedWifiStreamsPerDevice),
+                          "Stream assignment mismatch");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
     // Assign 0 streams per channel for this configuration
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (devices.GetN() * 2), "Stream assignment mismatch");
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed, 0, "No WifiChannel stream usage expected");
 
     // 2. Setup TCP/IP & AODV
     AodvHelper aodv; // Use default parameters here
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.SetRoutingHelper(aodv);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
-    // Expect to use (3*m_size) more streams for internet stack random variables
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed,
-                          ((devices.GetN() * 3) + (3 * m_size)),
-                          "Stream assignment mismatch");
-    streamsUsed += aodv.AssignStreams(*m_nodes, streamsUsed);
-    // Expect to use m_size more streams for AODV
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed,
-                          ((devices.GetN() * 3) + (3 * m_size) + m_size),
-                          "Stream assignment mismatch");
+    streamsUsed = internetStack.AssignStreams(*m_nodes, streamNumber);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
+
+    constexpr int expectedArpL3ProtocolStreams = 1;
+    constexpr int expectedInternetStreamsPerDevice = expectedArpL3ProtocolStreams;
+
+    // Expect number of streams assigned to match random variables streams used by wifi devices plus
+    // internet stack
+    NS_TEST_ASSERT_MSG_EQ(
+        totalStreamsUsed,
+        (devices.GetN() * (expectedWifiStreamsPerDevice + expectedInternetStreamsPerDevice)),
+        "Stream assignment mismatch");
+    streamsUsed = aodv.AssignStreams(*m_nodes, streamsUsed);
+    totalStreamsUsed += streamsUsed;
+    streamNumber += streamIncrement;
+
+    // Expect number of streams assigned to match random variables streams used by wifi devices plus
+    // internet and aodv stack
+    constexpr int expectedAodvStreamsPerDevice = 1;
+    NS_TEST_ASSERT_MSG_EQ(
+        totalStreamsUsed,
+        (devices.GetN() * (expectedWifiStreamsPerDevice + expectedInternetStreamsPerDevice +
+                           expectedAodvStreamsPerDevice)),
+        "Stream assignment mismatch");
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     Ipv4InterfaceContainer interfaces = address.Assign(devices);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/aodv/test/loopback.cc ns-3.47/src/aodv/test/loopback.cc
--- ns-3.46/src/aodv/test/loopback.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/aodv/test/loopback.cc	2026-02-16 16:08:49.569229800 -0800
@@ -139,6 +139,7 @@
     // Setup TCP/IP & AODV
     AodvHelper aodv; // Use default parameters here
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.SetRoutingHelper(aodv);
     internetStack.Install(nodes);
     Ipv4AddressHelper address;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/CMakeLists.txt ns-3.47/src/applications/CMakeLists.txt
--- ns-3.46/src/applications/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/CMakeLists.txt	2026-02-16 16:08:49.569229800 -0800
@@ -12,11 +12,15 @@
     model/onoff-application.cc
     model/packet-loss-counter.cc
     model/packet-sink.cc
+    model/rta-tig-mobile-gaming.cc
     model/seq-ts-echo-header.cc
     model/seq-ts-header.cc
     model/seq-ts-size-header.cc
     model/sink-application.cc
     model/source-application.cc
+    model/tgax-video-traffic.cc
+    model/tgax-virtual-desktop.cc
+    model/tgax-voip-traffic.cc
     model/three-gpp-http-client.cc
     model/three-gpp-http-header.cc
     model/three-gpp-http-server.cc
@@ -38,11 +42,15 @@
     model/onoff-application.h
     model/packet-loss-counter.h
     model/packet-sink.h
+    model/rta-tig-mobile-gaming.h
     model/seq-ts-echo-header.h
     model/seq-ts-header.h
     model/seq-ts-size-header.h
     model/sink-application.h
     model/source-application.h
+    model/tgax-video-traffic.h
+    model/tgax-virtual-desktop.h
+    model/tgax-voip-traffic.h
     model/three-gpp-http-client.h
     model/three-gpp-http-header.h
     model/three-gpp-http-server.h
@@ -57,4 +65,9 @@
     test/three-gpp-http-client-server-test.cc
     test/bulk-send-application-test-suite.cc
     test/udp-client-server-test.cc
+    test/tgax-video-traffic-test-suite.cc
+    test/tgax-voip-traffic-test-suite.cc
+    test/tgax-virtual-desktop-test-suite.cc
+    test/rta-tig-mobile-gaming-test-suite.cc
+  GENERATE_EXPORT_HEADER
 )
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/doc/applications.rst ns-3.47/src/applications/doc/applications.rst
--- ns-3.46/src/applications/doc/applications.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/doc/applications.rst	2026-02-16 16:08:49.569229800 -0800
@@ -1,16 +1,16 @@
 .. include:: replace.txt
 
+Applications
+============
+
 3GPP HTTP applications
 ----------------------
 
-Model Description
-*****************
-
 The model is a part of the applications library. The HTTP model is based on a commonly
 used 3GPP model in standardization `[4]`_.
 
 Design
-======
+~~~~~~
 
 This traffic generator simulates web browsing traffic using the Hypertext
 Transfer Protocol (HTTP). It consists of one or more ``ThreeGppHttpClient``
@@ -61,7 +61,7 @@
 
 
 3GPP HTTP server description
-############################
+****************************
 
 3GPP HTTP server is a model application which simulates the traffic of a web server. This
 application works in conjunction with ``ThreeGppHttpClient`` applications.
@@ -91,7 +91,7 @@
 packets received by ``ThreeGppHttpClient`` applications.
 
 3GPP HTTP client description
-############################
+****************************
 
 3GPP HTTP client is a model application which simulates the traffic of a web browser. This
 application works in conjunction with an ThreeGppHttpServer application.
@@ -147,7 +147,7 @@
 
 
 References
-==========
+~~~~~~~~~~
 
 Many aspects of the traffic are randomly determined by ``ThreeGppHttpVariables``.
 A separate instance of this object is used by the HTTP server and client applications.
@@ -181,7 +181,7 @@
 \
 
 Usage
-*****
+~~~~~
 
 The three-gpp-http-example can be referenced to see basic usage of the HTTP applications.
 In summary, using the ``ThreeGppHttpServerHelper`` and ``ThreeGppHttpClientHelper`` allow the
@@ -198,15 +198,8 @@
 "RxMainObjectPacket", "RxMainObject", "RxEmbeddedObjectPacket",
 "RxEmbeddedObject", "Rx", "RxDelay", "RxRtt", "StateTransition").
 
-
-Building the 3GPP HTTP applications
-===================================
-
-Building the applications does not require any special steps to be taken. It suffices to enable
-the applications module.
-
 Examples
-========
+~~~~~~~~
 
 For an example demonstrating HTTP applications
 run::
@@ -217,7 +210,7 @@
 server and client receiving content packets by using LOG_INFO of ``ThreeGppHttpServer`` and ``ThreeGppHttpClient``.
 
 Tests
-=====
+~~~~~
 
 For testing HTTP applications, three-gpp-http-client-server-test is provided. Run::
 
@@ -231,5 +224,17 @@
 Test cases themselves are rather simple: test verifies that HTTP object packet bytes sent match
 total bytes received by the client, and that ``ThreeGppHttpHeader`` matches the expected packet.
 
+.. include:: tgax-voip-traffic.inc
+
+.. include:: tgax-video-traffic.inc
+
+.. include:: tgax-virtual-desktop-traffic.inc
+
+.. include:: rta-tig-mobile-gaming-traffic.inc
+
+References
+~~~~~~~~~~
 
+.. [TGAX_EVAL] `IEEE 802.11-14/0571r12, "11ax Evaluation Methodology," IEEE 802.11 TGAX <https://mentor.ieee.org/802.11/dcn/14/11-14-0571-02-00ax-evaluation-methodology.docx>`_.
 
+.. [RTA-TIG] `IEEE 802.11-18/2009r6, "Real Time Applications TIG Report," IEEE 802.11 RTA TIG, Section 4.1: Real-time Mobile Gaming <https://mentor.ieee.org/802.11/dcn/18/11-18-2009-06-0rta-rta-report-draft.docx>`_.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/doc/rta-tig-mobile-gaming-traffic.inc ns-3.47/src/applications/doc/rta-tig-mobile-gaming-traffic.inc
--- ns-3.46/src/applications/doc/rta-tig-mobile-gaming-traffic.inc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/doc/rta-tig-mobile-gaming-traffic.inc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,296 @@
+RTA TIG Mobile Gaming Traffic Model
+-----------------------------------
+
+This section describes the implementation of the |ns3| Real-Time Applications (RTA) Task
+Interest Group (TIG) mobile gaming traffic generator model, which is based on the
+IEEE 802.11-18/2009r6 RTA TIG Report [RTA-TIG]_.
+
+The RTA TIG mobile gaming traffic model is implemented in the ``src/applications/`` folder.
+
+Real-time mobile gaming is characterized by small packets (30-500 bytes) transmitted
+frequently (every 30-60ms) with strict latency requirements. The traffic is asymmetric
+and bidirectional, with different characteristics for uplink (player commands) and
+downlink (game state updates).
+
+The model uses the Largest Extreme Value (LEV) distribution for packet inter-arrival
+times and packet sizes during the gaming stage, with uniform distributions for initial
+and ending packets.
+
+Scope and Limitations
+~~~~~~~~~~~~~~~~~~~~~
+
+The mobile gaming model implements the traffic generation aspects of the IEEE 802.11-18/2009r6
+specification (Section 4.1.4).
+
+**Supported Features:**
+
+- Two synchronization mechanisms: Status Sync and Frame Lockstep Sync
+- Three-stage traffic model: Initial, Gaming, and Ending
+- Largest Extreme Value distribution for packet arrivals and sizes
+- Uniform distribution for initial and end packet sizes
+- UDP protocol (default, matching specification)
+- Configurable parameters for all four scenarios (DL/UL x Status-Sync/Lockstep)
+- Trace source with stage information
+
+**Known Limitations:**
+
+- Each application instance is unidirectional; bidirectional gaming simulation requires
+  two separate application instances
+- No preset configuration enum; users must manually configure parameters for non-default
+  scenarios
+
+Model Description
+~~~~~~~~~~~~~~~~~
+
+The ``RtaTigMobileGaming`` application is a source application that generates real-time
+mobile gaming traffic patterns according to the IEEE 802.11-18/2009r6 RTA TIG Report.
+
+**Traffic Stages:**
+
+The model implements a three-stage traffic pattern:
+
+1. **INITIAL Stage**: A single packet is sent with size drawn from a uniform distribution.
+   This represents the initial game setup/synchronization packet.
+
+2. **GAMING Stage**: Continuous packet transmission during active gameplay. Packet sizes
+   and inter-arrival times are drawn from Largest Extreme Value distributions.
+
+3. **ENDING Stage**: A single packet is sent when the application stops, with size drawn
+   from a uniform distribution. This represents the game termination packet.
+
+**Synchronization Mechanisms:**
+
+The specification defines two traffic models for different game synchronization approaches:
+
+*Status Sync Model:*
+
+Used by games that synchronize game state between clients and server. Characterized by
+smaller, more frequent packets.
+
+.. list-table:: Status Sync Traffic Parameters
+   :header-rows: 1
+
+   * - Component
+     - Distribution
+     - DL (a, b)
+     - UL (a, b)
+   * - Initial packet size
+     - Uniform
+     - [0, 20] bytes
+     - [0, 20] bytes
+   * - Packet arrival time
+     - Largest Extreme Value
+     - a=13ms, b=3.7ms
+     - a=15ms, b=5.7ms
+   * - Packet size
+     - Largest Extreme Value
+     - a=50, b=11 bytes
+     - a=38, b=3.7 bytes
+   * - End packet size
+     - Uniform
+     - [500, 600] bytes
+     - [400, 550] bytes
+
+*Frame Lockstep Sync Model:*
+
+Used by games that require frame-by-frame synchronization. Characterized by larger
+packets with slightly longer intervals.
+
+.. list-table:: Frame Lockstep Sync Traffic Parameters
+   :header-rows: 1
+
+   * - Component
+     - Distribution
+     - DL (a, b)
+     - UL (a, b)
+   * - Initial packet size
+     - Uniform
+     - [0, 80] bytes
+     - [0, 80] bytes
+   * - Packet arrival time
+     - Largest Extreme Value
+     - a=28ms, b=4.2ms
+     - a=22ms, b=3.4ms
+   * - Packet size
+     - Largest Extreme Value
+     - a=210, b=35 bytes
+     - a=92, b=38 bytes
+   * - End packet size
+     - Uniform
+     - [1400, 1500] bytes
+     - [500, 600] bytes
+
+In the Largest Extreme Value distribution, **a** is the location parameter and **b** is
+the scale parameter.
+
+Configuration and Usage
+***********************
+
+The mobile gaming traffic generator can be instantiated through the ``ApplicationHelper``.
+The default configuration matches the Status-Sync Downlink parameters.
+
+**Default Configuration (Status-Sync DL):**
+
+::
+
+  // Create nodes
+  NodeContainer nodes;
+  nodes.Create(2);
+
+  // Install Internet stack and assign IP addresses
+  InternetStackHelper internet;
+  internet.Install(nodes);
+
+  Ipv4AddressHelper ipv4;
+  ipv4.SetBase("10.0.0.0", "255.255.255.0");
+  ipv4.Assign(devices);
+
+  // Configure gaming application (defaults to status-sync DL)
+  ApplicationHelper sourceHelper(RtaTigMobileGaming::GetTypeId());
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.2"), 5000)));
+
+  auto sourceApps = sourceHelper.Install(nodes.Get(0));
+  sourceApps.Start(Seconds(1.0));
+  sourceApps.Stop(Seconds(11.0));
+
+  // Create receiver
+  PacketSinkHelper sinkHelper("ns3::UdpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), 5000));
+  auto sinkApps = sinkHelper.Install(nodes.Get(1));
+  sinkApps.Start(Seconds(0.0));
+  sinkApps.Stop(Seconds(12.0));
+
+**Lockstep DL Configuration:**
+
+Note:  The below would be better configured via a mode attribute such as LOCKSTEP_DL.
+
+::
+
+  ApplicationHelper sourceHelper(RtaTigMobileGaming::GetTypeId());
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.2"), 5000)));
+
+  // Configure lockstep DL parameters
+  auto ips = CreateObjectWithAttributes<UniformRandomVariable>(
+      "Min", DoubleValue(0), "Max", DoubleValue(80));
+  sourceHelper.SetAttribute("InitialPacketSize", PointerValue(ips));
+
+  auto eps = CreateObjectWithAttributes<UniformRandomVariable>(
+      "Min", DoubleValue(1400), "Max", DoubleValue(1500));
+  sourceHelper.SetAttribute("EndPacketSize", PointerValue(eps));
+
+  auto pal = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+      "Location", DoubleValue(28000),   // 28ms in microseconds
+      "Scale", DoubleValue(4200));      // 4.2ms in microseconds
+  sourceHelper.SetAttribute("PacketArrivalLev", PointerValue(pal));
+
+  auto psl = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+      "Location", DoubleValue(210),
+      "Scale", DoubleValue(35));
+  sourceHelper.SetAttribute("PacketSizeLev", PointerValue(psl));
+
+Attributes
+~~~~~~~~~~
+
+The mobile gaming traffic generator uses UDP by default, as specified for in-game data
+in the IEEE specification.
+
+The following attributes control the behavior of the traffic generator:
+
+**Protocol Configuration:**
+
+* ``Protocol``: The socket factory type to use. Default: ``ns3::UdpSocketFactory``
+* ``Remote``: The remote address and port for the gaming traffic destination
+
+**Initial and End Packet Parameters:**
+
+* ``InitialPacketSize``: A uniform random variable for initial packet size in bytes.
+  Default: Uniform[0, 20]
+* ``EndPacketSize``: A uniform random variable for end packet size in bytes.
+  Default: Uniform[500, 600]
+
+**Gaming Stage Parameters:**
+
+* ``PacketArrivalLev``: A Largest Extreme Value random variable for packet inter-arrival
+  times in microseconds. Default: LEV(Location=13000, Scale=3700) (13ms, 3.7ms)
+* ``PacketSizeLev``: A Largest Extreme Value random variable for gaming packet sizes
+  in bytes. Default: LEV(Location=50, Scale=11)
+
+Trace Sources
+~~~~~~~~~~~~~
+
+The mobile gaming traffic generator provides a trace source for monitoring simulation
+behavior:
+
+**TxWithStage:**
+
+::
+
+  typedef void (*TxTracedCallback)(Ptr<const Packet> packet, TrafficModelStage stage);
+
+This trace source is fired when a packet is transmitted. It provides the packet and
+the current traffic model stage (INITIAL, GAMING, or ENDING). This is useful for
+analyzing traffic patterns and verifying stage transitions.
+
+Examples
+~~~~~~~~
+
+The ``rta-tig-mobile-gaming-example.cc`` program demonstrates usage of the mobile gaming
+traffic generator. This example sets up a simple Wi-Fi network (AP and STA) with gaming
+traffic. The example supports all four traffic model presets.
+
+To run with Status-Sync Downlink (default):
+
+::
+
+  $ ./ns3 run rta-tig-mobile-gaming-example
+
+To run with Status-Sync Uplink:
+
+::
+
+  $ ./ns3 run "rta-tig-mobile-gaming-example --model=status-sync-ul"
+
+To run with Frame Lockstep Downlink:
+
+::
+
+  $ ./ns3 run "rta-tig-mobile-gaming-example --model=lockstep-dl"
+
+To run with Frame Lockstep Uplink:
+
+::
+
+  $ ./ns3 run "rta-tig-mobile-gaming-example --model=lockstep-ul"
+
+Tests
+~~~~~
+
+Unit tests for the mobile gaming traffic model are provided in
+``src/applications/test/rta-tig-mobile-gaming-test-suite.cc``. These tests verify:
+
+* Correct stage transitions (INITIAL -> GAMING -> ENDING)
+* Initial packet size within expected bounds
+* End packet size within expected bounds
+* Average gaming packet size matches LEV distribution mean
+* Average packet inter-arrival time matches LEV distribution mean
+* All transmitted bytes are received
+
+To run the mobile gaming traffic tests:
+
+::
+
+  $ ./test.py -s applications-rta-tig-mobile-gaming
+
+Verification
+~~~~~~~~~~~~
+
+The mobile gaming traffic model has been checked against the IEEE 802.11-18/2009r6
+specification by comparing:
+
+* Default parameter values against Table 4-3 (Status Sync) in the specification
+* Largest Extreme Value distribution behavior for packet sizes and arrivals
+* Stage transition logic
+
+The test suite demonstrates that the model correctly implements the three-stage traffic
+pattern and produces traffic characteristics consistent with real-time mobile gaming
+applications.
+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/doc/tgax-video-traffic.inc ns-3.47/src/applications/doc/tgax-video-traffic.inc
--- ns-3.46/src/applications/doc/tgax-video-traffic.inc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/doc/tgax-video-traffic.inc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,340 @@
+TGax Video Traffic Model
+------------------------
+
+This section describes the implementation of the |ns3| TGax video traffic generator model, which
+is based on the IEEE 802.11-14/0571r12 TGAX evaluation methodology [TGAX_EVAL]_. The model generates
+buffered video streaming traffic patterns (notionally generated by services such as YouTube
+or Netflix) suitable for evaluating IEEE 802.11 networks and other wireless systems.
+
+The TGax video traffic model is implemented in the ``src/applications/`` folder.
+
+The video traffic generator models the behavior of a video streaming source, producing
+traffic that simulates video frames being generated at the application layer, fragmented
+at the transport layer, and experiencing network latency as they traverse the Internet
+before reaching the access point for transmission to clients.
+
+The model implements a Weibull distribution for video frame size generation and a Gamma
+distribution for network latency modeling, as specified in the IEEE 802.11-14/0571r12
+evaluation methodology. The latter is disabled by default since the model is primarily intended
+for use with a link simulator that does not model queues and TCP behavior.
+
+Scope and Limitations
+~~~~~~~~~~~~~~~~~~~~~
+
+The TGax video model implements the buffered video streaming and multicast video streaming
+traffic models from the IEEE 802.11-14/0571r12 specification. The following describes
+what is covered and what is not:
+
+**Supported Features:**
+
+- Eight predefined traffic model profiles (BV1-BV6 for buffered video, MC1-MC2 for multicast)
+- Custom traffic model configuration with user-defined parameters
+- Weibull distribution for video frame size generation
+- Gamma distribution for network latency modeling (mean ~14.8ms)
+- TCP and UDP protocol support
+- Multicast video modes
+- Automatic TCP segment size handling for large video frames
+- Comprehensive trace sources for monitoring frame generation and packet transmission
+
+**Known Limitations:**
+
+- The Wireless Display (WD1-WD4) traffic models from the specification are not implemented
+- The Video Conferencing (VC) traffic model is not explicitly implemented (users can simulate
+  this by creating bidirectional BV1 streams)
+- TCP acknowledgment traffic follows ns-3's built-in TCP behavior rather than the explicit
+  40-byte/1ms delay model in the specification
+- Network latency is not regenerated if it would cause packets to arrive after simulation end
+
+Model Description
+~~~~~~~~~~~~~~~~~
+
+The ``TgaxVideoTraffic`` application is a source application that generates video streaming
+traffic patterns according to the IEEE 802.11-14/0571r12 evaluation methodology. The application
+models video streaming services such as YouTube and Netflix.
+
+**Traffic Generation Process:**
+
+The model follows a three-step process:
+
+1. **Frame Size Generation**: At the application layer, video frame sizes (in bytes) are
+   generated according to a Weibull distribution with parameters that vary by target bit rate.
+
+2. **Frame Transmission**: Video frames are sent to the transport layer. For TCP, frames
+   larger than the segment size (1500 bytes) are automatically fragmented.
+
+3. **Network Latency**: Each packet experiences network latency drawn from a Gamma distribution
+   with mean approximately 14.8ms, simulating the delay encountered as video traffic
+   traverses multiple hops in the Internet before reaching the access point.
+
+(note, if code review comment accepted, step 3 should be described as optional)
+
+**Predefined Traffic Models:**
+
+The implementation provides eight predefined traffic model profiles, matching those in the
+IEEE specification:
+
+*Buffered Video (BV) Models:*
+
+.. list-table:: Buffered Video Traffic Models
+   :header-rows: 1
+
+   * - Model ID
+     - Bit Rate
+     - Weibull Scale (lambda)
+     - Weibull Shape (k)
+   * - BV1
+     - 2 Mbps
+     - 6950
+     - 0.8099
+   * - BV2
+     - 4 Mbps
+     - 13900
+     - 0.8099
+   * - BV3
+     - 6 Mbps
+     - 20850
+     - 0.8099
+   * - BV4
+     - 8 Mbps
+     - 27800
+     - 0.8099
+   * - BV5
+     - 10 Mbps
+     - 34750
+     - 0.8099
+   * - BV6
+     - 15.6 Mbps
+     - 54210
+     - 0.8099
+
+*Multicast Video (MC) Models:*
+
+.. list-table:: Multicast Video Traffic Models
+   :header-rows: 1
+
+   * - Model ID
+     - Bit Rate
+     - Weibull Scale (lambda)
+     - Weibull Shape (k)
+   * - MC1
+     - 3 Mbps
+     - 10425
+     - 0.8099
+   * - MC2
+     - 6 Mbps
+     - 20850
+     - 0.8099
+
+*Custom model:*
+
+The default CUSTOM model has parameter defaults corresponding to BV1 model, but is intended
+to be used and reconfigured when users may want to use their own model parameters.
+
+**Network Latency Model:**
+
+The network latency experienced by each packet is modeled using a Gamma distribution:
+
+* **Shape parameter (k)**: 0.2463
+* **Scale parameter (theta)**: 60.227
+* **Mean latency**: k * theta = 14.834 ms
+
+This models the delay variation experienced as video packets traverse multiple network
+hops between the video server and the access point.
+
+Configuration and Usage
+***********************
+
+The video traffic generator is instantiated as an application through the ``ApplicationHelper``.
+Here is a basic example using UDP:
+
+::
+
+  // Create nodes
+  NodeContainer nodes;
+  nodes.Create(2);
+
+  // Install Internet stack and assign IP addresses
+  InternetStackHelper internet;
+  internet.Install(nodes);
+
+  Ipv4AddressHelper ipv4;
+  ipv4.SetBase("10.0.0.0", "255.255.255.0");
+  ipv4.Assign(devices);
+
+  // Configure video application with BV3 (6 Mbps) profile
+  ApplicationHelper sourceHelper(TgaxVideoTraffic::GetTypeId());
+  sourceHelper.SetAttribute("Protocol", TypeIdValue(UdpSocketFactory::GetTypeId()));
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.2"), 5000)));
+  sourceHelper.SetAttribute("TrafficModelClassIdentifier", StringValue("BV3"));
+
+  auto sourceApps = sourceHelper.Install(nodes.Get(0));
+  sourceApps.Start(Seconds(1.0));
+  sourceApps.Stop(Seconds(11.0));
+
+  // Create receiver
+  PacketSinkHelper sinkHelper("ns3::UdpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), 5000));
+  auto sinkApps = sinkHelper.Install(nodes.Get(1));
+  sinkApps.Start(Seconds(0.0));
+  sinkApps.Stop(Seconds(12.0));
+
+For custom traffic parameters:
+
+::
+
+  // Configure custom video parameters
+  ApplicationHelper sourceHelper(TgaxVideoTraffic::GetTypeId());
+  sourceHelper.SetAttribute("Protocol", TypeIdValue(UdpSocketFactory::GetTypeId()));
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.2"), 5000)));
+  sourceHelper.SetAttribute("TrafficModelClassIdentifier", StringValue("Custom"));
+  sourceHelper.SetAttribute("CustomVideoBitRate", DataRateValue(DataRate("5Mbps")));
+  sourceHelper.SetAttribute("CustomWeibullScale", DoubleValue(17375));
+  sourceHelper.SetAttribute("CustomWeibullShape", DoubleValue(0.8099));
+
+Attributes
+~~~~~~~~~~
+
+The video traffic generator supports UDP and TCP protocols through the ``Protocol`` attribute:
+
+* **ns3::UdpSocketFactory**: UDP socket (recommended for video streaming simulation)
+* **ns3::TcpSocketFactory**: TCP socket (realistic for actual video streaming behavior)
+
+The ``Remote`` address must be configured appropriately:
+
+* For UDP/TCP: Use ``InetSocketAddress`` for IPv4 or ``Inet6SocketAddress`` for IPv6
+
+The following additional attributes control the behavior of the video traffic generator:
+
+**Protocol Configuration:**
+
+* ``Protocol``: The socket factory type to use. Default: ``ns3::UdpSocketFactory``
+* ``Remote``: The remote address and port for the video destination
+
+**Traffic Model Selection:**
+
+* ``TrafficModelClassIdentifier``: The traffic model to use. Options:
+  BV1, BV2, BV3, BV4, BV5, BV6, MC1, MC2, or Custom. Default: Custom
+
+  The BV1-BV6 and MC1-MC2 options correspond to the predefined traffic models from the
+  IEEE 802.11-14/0571r12 specification. The Custom option is an implementation extension
+  (not part of the IEEE specification) that allows users to specify their own Weibull
+  distribution parameters for experimentation or interpolation between defined bit rates.
+
+**Custom Model Parameters (only used when TrafficModelClassIdentifier is Custom):**
+
+* ``CustomVideoBitRate``: The target video bit rate. Default: 2 Mbps
+* ``CustomWeibullScale``: Scale parameter (lambda) for the Weibull distribution. Default: 6950
+* ``CustomWeibullShape``: Shape parameter (k) for the Weibull distribution. Default: 0.8099
+
+Note: The default values for the Custom model match the BV1 parameters from the specification.
+
+**Network Latency Parameters:**
+
+* ``GammaShape``: Shape parameter (k) for the Gamma distribution for network latency. Default: 0.2463
+* ``GammaScale``: Scale parameter (theta) for the Gamma distribution. Default: 60.227
+
+Trace Sources
+~~~~~~~~~~~~~
+
+The video traffic generator provides two main trace sources for monitoring simulation behavior:
+
+**TxWithLatency:**
+
+::
+
+  typedef void (*TxTracedCallback)(Ptr<const Packet> packet, Time latency);
+
+This trace source is fired when a packet is transmitted. It provides both the packet being
+transmitted and the network latency value that was applied to that packet. This is useful
+for analyzing packet transmission patterns and the distribution of network latency.
+
+**VideoFrameGenerated:**
+
+::
+
+  typedef void (*FrameGeneratedCallback)(uint32_t frameSize);
+
+This trace source is fired when a video frame is generated at the application layer.
+It provides the size of the generated frame in bytes. This is useful for analyzing the
+frame size distribution and verifying that it matches the expected Weibull distribution.
+
+Examples
+~~~~~~~~
+
+The ``tgax-video-example.cc`` program demonstrates basic usage of the video traffic generator.
+This example sets up a simple Wi-Fi network (AP and STAs) with video streaming traffic
+flowing from the AP to the station(s). The example supports:
+
+* All predefined traffic models (BV1-BV6, MC1-MC2)
+* Both UDP and TCP protocols
+* Multicast delivery to multiple receivers
+
+To run with default settings (BV1, 2 Mbps over UDP):
+
+::
+
+  $ ./ns3 run tgax-video-example
+
+To run with a higher bit rate profile:
+
+::
+
+  $ ./ns3 run "tgax-video-example --model=BV3"
+
+To run with TCP:
+
+::
+
+  $ ./ns3 run "tgax-video-example --protocol=tcp"
+
+To run a multicast scenario:
+
+::
+
+  $ ./ns3 run "tgax-video-example --model=MC1"
+
+Tests
+~~~~~
+
+Unit tests for the video traffic model are provided in
+``src/applications/test/tgax-video-traffic-test-suite.cc``. These tests verify:
+
+* All predefined traffic models (BV1-BV6) work correctly with both TCP and UDP
+* Multicast models (MC1, MC2) deliver traffic to multiple receivers
+* Custom traffic model parameters are applied correctly
+* Measured bit rate matches expected bit rate (within 5% tolerance)
+* Inter-frame duration matches expected value
+* Average network latency matches specification (14.834ms)
+* All transmitted bytes are received
+
+To run the video traffic tests:
+
+::
+
+  $ ./test.py -s applications-tgax-video-traffic
+
+Verification
+~~~~~~~~~~~~
+
+The video traffic model has been checked against the IEEE 802.11-14/0571r12 specification
+by comparing:
+
+* All predefined model parameters (Weibull scale and shape) against Table 5 in the specification
+* Network latency Gamma distribution parameters against the specification
+* Measured bit rates against expected bit rates for each traffic model
+* Frame size distribution against expected Weibull properties
+* Network latency distribution against expected Gamma properties (mean ~14.8ms)
+
+The test suite demonstrates that the model correctly implements the buffered video
+streaming traffic model, produces appropriate frame sizes, and generates traffic
+patterns consistent with real video streaming applications.
+
+Video Conferencing Simulation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Although the Video Conferencing (VC) traffic model is not explicitly implemented as a
+separate class, users can simulate video conferencing scenarios by creating bidirectional
+BV1 streams. According to the specification:
+
+* Traffic from AP to station: Use the BV1 model (with network latency)
+* Traffic from station to AP: Use the BV1 model (without network latency, or set GammaScale to 0)
+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/doc/tgax-virtual-desktop-traffic.inc ns-3.47/src/applications/doc/tgax-virtual-desktop-traffic.inc
--- ns-3.46/src/applications/doc/tgax-virtual-desktop-traffic.inc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/doc/tgax-virtual-desktop-traffic.inc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,255 @@
+TGax Virtual Desktop Infrastructure Traffic Model
+-------------------------------------------------
+
+This section describes the implementation of the |ns3| TGax Virtual Desktop Infrastructure (VDI)
+traffic generator model, which is based on the IEEE 802.11-14/0571r12 TGAX evaluation
+methodology [TGAX_EVAL]_.
+The TGax VDI traffic model is implemented in the ``src/applications/`` folder.
+
+Virtual Desktop Infrastructure (VDI) traffic is generated from a server and, in practice,
+may travel over multiple hops before arriving at an access point (AP) for transmission to
+stations (STAs).  VDI traffic statistics are asymmetric (between downlink (server to client
+display) and uplink (client display to server)).
+
+The model uses an exponential distribution for packet inter-arrival times and a normal
+distribution (including a bimodal distribution) for packet sizes, as specified in the
+IEEE 802.11-14/0571r12 evaluation methodology.
+
+Scope and Limitations
+~~~~~~~~~~~~~~~~~~~~~
+
+The VDI model implements the traffic generation aspects of the IEEE 802.11-14/0571r12
+specification. The following describes what is covered and what is not:
+
+**Supported Features:**
+
+- Exponential distribution for packet inter-arrival times
+- Uniform distribution for initial packet arrival (0-20ms)
+- Normal distribution for packet sizes
+- Bimodal normal distribution support for downlink packet sizes
+- TCP protocol (default, matching specification)
+- Configurable parameters for both downlink and uplink scenarios
+- Trace source for monitoring packet transmission
+
+**Known Limitations:**
+
+- (code review comment to resolve) Bimodal distribution mode selection uses equal probability instead
+  of weighted selection (70.6%/29.4%).
+  future versions.
+- (code review comment to resolve) Each application instance is unidirectional; bidirectional VDI simulation requires two
+  separate application instances and the uplink's parameters must be manually configured.
+
+Model Description
+~~~~~~~~~~~~~~~~~
+
+The ``TgaxVirtualDesktop`` application is a source application that generates VDI traffic
+patterns according to the IEEE 802.11-14/0571r12 evaluation methodology.
+
+**Traffic Generation Process:**
+
+1. **Initial Packet Arrival**: The first packet is scheduled after a uniformly distributed
+   delay in the range [0, 20ms], simulating random timing between client packet arrival
+   and frame boundaries.
+
+2. **Subsequent Packet Arrivals**: After the initial packet, inter-arrival times follow
+   an exponential distribution with direction-specific mean values.
+
+3. **Packet Size Generation**: Packet sizes are drawn from a normal distribution. For
+   downlink traffic, a bimodal normal distribution is used to model the mix of small
+   control packets and large display data packets.
+
+**Direction-Specific Parameters:**
+
+The specification defines different parameters for downlink and uplink traffic:
+
+*Downlink (AP to STA - Display Data):*
+
+.. list-table:: Downlink VDI Parameters
+   :header-rows: 1
+
+   * - Parameter
+     - Distribution
+     - Value
+   * - Initial packet arrival
+     - Uniform
+     - [0, 20] ms
+   * - Packet inter-arrival
+     - Exponential
+     - Mean = 60.2269 ms
+   * - Packet size
+     - Bimodal Normal
+     - Mode 1: mu=41.0, sigma=3.2 (weight 53.7/76.1)
+   * -
+     -
+     - Mode 2: mu=1478.3, sigma=11.6 (weight 22.4/76.1)
+
+*Uplink (STA to AP - Navigation/Feedback):*
+
+.. list-table:: Uplink VDI Parameters
+   :header-rows: 1
+
+   * - Parameter
+     - Distribution
+     - Value
+   * - Initial packet arrival
+     - Uniform
+     - [0, 20] ms
+   * - Packet inter-arrival
+     - Exponential
+     - Mean = 48.2870 ms
+   * - Packet size
+     - Normal
+     - mu=50.598, sigma=5.0753
+
+Configuration and Usage
+***********************
+
+The VDI traffic generator can be instantiated through the ``ApplicationHelper``.
+The default configuration matches the downlink specification.
+
+**Downlink Configuration (Default):**
+
+::
+
+  // Create nodes
+  NodeContainer nodes;
+  nodes.Create(2);
+
+  // Install Internet stack and assign IP addresses
+  InternetStackHelper internet;
+  internet.Install(nodes);
+
+  Ipv4AddressHelper ipv4;
+  ipv4.SetBase("10.0.0.0", "255.255.255.0");
+  ipv4.Assign(devices);
+
+  // Configure VDI application (downlink - default parameters)
+  ApplicationHelper sourceHelper(TgaxVirtualDesktop::GetTypeId());
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.2"), 5000)));
+
+  auto sourceApps = sourceHelper.Install(nodes.Get(0));
+  sourceApps.Start(Seconds(1.0));
+  sourceApps.Stop(Seconds(11.0));
+
+  // Create receiver
+  PacketSinkHelper sinkHelper("ns3::TcpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), 5000));
+  auto sinkApps = sinkHelper.Install(nodes.Get(1));
+  sinkApps.Start(Seconds(0.0));
+  sinkApps.Stop(Seconds(12.0));
+
+**Uplink Configuration:**
+
+::
+
+  // Configure VDI application for uplink
+  ApplicationHelper sourceHelper(TgaxVirtualDesktop::GetTypeId());
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.1"), 5001)));
+
+  // Set uplink inter-arrival time (mean 48.287 ms)
+  auto ulInterArrival = CreateObjectWithAttributes<ExponentialRandomVariable>(
+      "Mean", DoubleValue(48287000.0));  // nanoseconds
+  sourceHelper.SetAttribute("InterPacketArrivals", PointerValue(ulInterArrival));
+
+  // Set uplink packet size (normal distribution, mu=50.598, sigma=5.0753)
+  sourceHelper.SetAttribute("ParametersPacketSize", StringValue("50.598 5.0753"));
+
+  auto sourceApps = sourceHelper.Install(nodes.Get(1));  // STA node
+  sourceApps.Start(Seconds(1.0));
+  sourceApps.Stop(Seconds(11.0));
+
+Attributes
+~~~~~~~~~~
+
+The VDI traffic generator uses TCP by default.
+
+The following attributes control the behavior of the VDI traffic generator:
+
+**Protocol Configuration:**
+
+* ``Protocol``: The socket factory type to use. Default: ``ns3::TcpSocketFactory``
+* ``Remote``: The remote address and port for the VDI destination
+
+**Timing Parameters:**
+
+* ``InitialPacketArrival``: A uniform random variable for the initial packet arrival time.
+  Default: Uniform ranging from 0 to 20 ms
+* ``InterPacketArrivals``: An exponential random variable for inter-packet arrival times.
+  Default: Exponential with mean 60.2269ms (downlink specification)
+
+**Packet Size Parameters:**
+
+* ``ParametersPacketSize``: The mean and standard deviation for each mode of the (potentially
+  multimodal) normal distribution used to generate packet sizes. Format: "mean1 std1;mean2 std2"
+  for bimodal, or "mean std" for unimodal. Default: "41.0 3.2;1478.3 11.6" (downlink bimodal)
+
+Trace Sources
+~~~~~~~~~~~~~
+
+The VDI traffic generator provides a trace source for monitoring simulation behavior:
+
+**Tx:**
+
+::
+
+  typedef void (*TxTracedCallback)(Ptr<const Packet> packet);
+
+This trace source is fired when a packet is transmitted. It provides the packet being
+transmitted. This is useful for analyzing packet transmission patterns and sizes.
+
+Examples
+~~~~~~~~
+
+The ``tgax-virtual-desktop-example.cc`` program demonstrates basic usage of the VDI traffic
+generator. This example sets up a simple Wi-Fi network (AP and STA) with VDI traffic. The
+example supports downlink, uplink, and bidirectional traffic configurations.
+
+To run with downlink traffic (default):
+
+::
+
+  $ ./ns3 run tgax-virtual-desktop-example
+
+To run with uplink traffic:
+
+::
+
+  $ ./ns3 run "tgax-virtual-desktop-example --direction=uplink"
+
+To run with bidirectional traffic:
+
+::
+
+  $ ./ns3 run "tgax-virtual-desktop-example --direction=bidirectional"
+
+Tests
+~~~~~
+
+Unit tests for the VDI traffic model are provided in
+``src/applications/test/tgax-virtual-desktop-test-suite.cc``. These tests verify:
+
+* Downlink VDI traffic with default parameters
+* Uplink VDI traffic with custom parameters
+* Initial packet arrival within expected bounds (0-20ms)
+* Average packet inter-arrival time matches expectation
+* Average packet size matches expectation
+* All transmitted bytes are received
+
+To run the VDI traffic tests:
+
+::
+
+  $ ./test.py -s applications-tgax-virtual-desktop
+
+Verification
+~~~~~~~~~~~~
+
+The VDI traffic model has been checked against the IEEE 802.11-14/0571r12 specification
+by comparing:
+
+* Initial packet arrival distribution (Uniform [0, 20ms])
+* Packet inter-arrival time distribution (Exponential with direction-specific means)
+* Packet size distribution parameters against specification
+
+The test suite demonstrates that the model correctly implements the exponential inter-arrival
+behavior and normal packet size distributions.
+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/doc/tgax-voip-traffic.inc ns-3.47/src/applications/doc/tgax-voip-traffic.inc
--- ns-3.46/src/applications/doc/tgax-voip-traffic.inc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/doc/tgax-voip-traffic.inc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,266 @@
+VoIP Traffic Model
+------------------
+
+This section describes the implementation of the |ns3| VoIP traffic generator model, which
+is based on the IEEE 802.11-14/0571r12 TGAX evaluation methodology [TGAX_EVAL]_. The model
+generates realistic voice-over-IP (VoIP) traffic patterns suitable for evaluating IEEE 802.11
+networks and other wireless systems.
+
+The VoIP traffic model is implemented in the ``src/applications/`` folder.
+
+The VoIP traffic generator models the behavior of a real VoIP source, producing traffic
+that alternates between periods of active talking and silence. This model is useful for
+simulating realistic VoIP scenarios in network simulations, particularly for evaluating
+the performance of wireless access technologies such as IEEE 802.11 (Wi-Fi).
+
+The model implements a two-state Markov chain to represent voice activity, where transitions
+occur between active (talking) and inactive (silence) states. During active states, the source
+generates packets at regular intervals (20 ms) with one payload size, and during silence states,
+packets are generated at longer intervals (160 ms) with a smaller payload size. Both states
+can last for exponentially distributed durations, giving the model stochastic properties that
+match real voice traffic characteristics.
+
+The VoIP model can be used with various socket types including IPv4/UDP, IPv6/UDP, and
+PacketSocket, making it flexible for different network configurations and simulation scenarios.
+
+Scope and Limitations
+~~~~~~~~~~~~~~~~~~~~~
+
+The VoIP model implements the core traffic generation aspects of the IEEE 802.11-14/0571r12
+specification. The following describes what is covered and what is not:
+
+**Supported Features:**
+
+- Two-state Markov model for voice activity (active/inactive states)
+- Configurable state transition probabilities
+- Exponential distribution for state durations
+- Variable packet payload sizes for active and inactive states
+- Laplacian distribution for downlink packet delay jitter
+- Integration with UDP, TCP, and PacketSocket protocols
+- Comprehensive trace sources for monitoring packet transmission and state changes
+
+**Known Limitations:**
+
+- Protocol header overhead (3 bytes for IPv4, 5 bytes for IPv6-- both values assuming IP header compression (not implemented in ns-3) is used) must be added to payload sizes
+- Jitter is applied uniformly to all packets (asymmetric uplink/downlink jitter not differentiated)
+- TCP transmission does not validate buffer availability; packets may be dropped if the TX buffer is full
+- The model does not enforce the implicit 12.2 kbps bit rate constraint in the IEEE document; users must configure appropriate parameters
+- Directional traffic (unidirectional source only, no bidirectional conversation modeling)
+
+Model Description
+~~~~~~~~~~~~~~~~~
+
+The ``TgaxVoipTraffic`` application is a source application that generates VoIP traffic patterns
+according to the IEEE 802.11-14/0571r12 evaluation methodology. The application uses a simple
+but effective two-state Markov model to generate realistic voice activity patterns.
+
+**State Machine:**
+
+The model maintains two states:
+
+* **ACTIVE_TALKING**: The source is actively transmitting voice data. In this state, packets
+  are generated at the voice encoder interval (default 20 ms) with a payload size configured
+  for active voice packets (default 33 bytes).
+
+* **INACTIVE_SILENCE**: The source is in a silent period. In this state, packets are generated
+  at the silence encoder interval (default 160 ms) with a smaller payload size (default 7 bytes).
+  Silence packets typically carry comfort noise or silence indication in real VoIP systems.
+
+**State Transitions:**
+
+The transition from one state to another follows a two-state Markov chain with configurable
+transition probabilities:
+
+* **VoiceToSilenceProbability** (default 0.016): Probability per state update interval to transition
+  from ACTIVE_TALKING to INACTIVE_SILENCE.
+
+* **SilenceToVoiceProbability** (default 0.016): Probability per state update interval to transition
+  from INACTIVE_SILENCE to ACTIVE_TALKING.
+
+With default parameters (both probabilities at 0.016), the model produces a voice activity factor
+of approximately 50%, meaning the source is in the active state about half the time.
+
+**State Duration:**
+
+Once a state is entered, the source remains in that state for an exponentially distributed duration:
+
+* **MeanActiveStateDuration** (default 1250 ms): Mean duration of active talking periods.
+* **MeanInactiveStateDuration** (default 1250 ms): Mean duration of silence periods.
+
+These exponential distributions produce realistic variability in conversation patterns, where some
+active periods are very short and others are quite long.
+
+**Packet Generation and Jitter:**
+
+Packets are generated at regular intervals determined by the current state (20 ms for active,
+160 ms for silence). Each generated packet receives a random delay jitter before transmission,
+drawn from a Laplacian distribution:
+
+* **MeanDelayJitter** (default 0 us): Mean of the Laplacian distribution.
+* **ScaleDelayJitter** (default 5110 us): Scale parameter of the Laplacian distribution.
+* **BoundDelayJitter** (default 80 ms): Bound on the jitter magnitude to ensure stability.
+
+The jitter model is applied only in the downlink direction, as specified in the IEEE 802.11-14/0571r12
+methodology.
+
+**Payload Sizes and Protocol Headers:**
+
+The model specifies default payload sizes based on the IEEE specification:
+
+* **ActivePacketPayloadSize** (default 33 bytes): Size of packets during active talking.
+* **SilencePacketPayloadSize** (default 7 bytes): Size of packets during silence periods.
+
+When using UDP over IPv4 or IPv6, users should account for compressed protocol headers:
+- IPv4 with compression: add 3 bytes to each payload size
+- IPv6 with compression: add 5 bytes to each payload size
+
+These header sizes are NOT automatically included; the user must adjust the payload size
+attributes accordingly if protocol header compression is desired in the simulation.
+
+Configuration and Usage
+***********************
+
+The VoIP traffic generator is instantiated as an application through the ``ApplicationHelper``.
+Here is a basic example using IPv4/UDP:
+
+::
+
+  // Create nodes
+  NodeContainer nodes;
+  nodes.Create(2);
+
+  // Install Internet stack and assign IP addresses
+  InternetStackHelper internet;
+  internet.Install(nodes);
+
+  Ipv4AddressHelper ipv4;
+  ipv4.SetBase("10.0.0.0", "255.255.255.0");
+  ipv4.Assign(devices);
+
+  // Configure VoIP application
+  ApplicationHelper sourceHelper(TgaxVoipTraffic::GetTypeId());
+  sourceHelper.SetAttribute("Protocol", StringValue("ns3::UdpSocketFactory"));
+  sourceHelper.SetAttribute("Remote", AddressValue(InetSocketAddress(Ipv4Address("10.0.0.2"), 5000)));
+  sourceHelper.SetAttribute("ActivePacketPayloadSize", UintegerValue(33 + 3)); // +3 for IPv4 header
+  sourceHelper.SetAttribute("SilencePacketPayloadSize", UintegerValue(7 + 3));
+
+  auto sourceApps = sourceHelper.Install(nodes.Get(0));
+  sourceApps.Start(Seconds(1.0));
+  sourceApps.Stop(Seconds(11.0));
+
+  // Create receiver
+  PacketSinkHelper sinkHelper("ns3::UdpSocketFactory", InetSocketAddress(Ipv4Address::GetAny(), 5000));
+  auto sinkApps = sinkHelper.Install(nodes.Get(1));
+  sinkApps.Start(Seconds(0.0));
+  sinkApps.Stop(Seconds(12.0));
+
+Attributes
+~~~~~~~~~~
+
+The VoIP traffic generator supports multiple socket types through the ``Protocol`` attribute:
+
+* **ns3::UdpSocketFactory**: UDP socket (recommended for most scenarios)
+* **ns3::TcpSocketFactory**: TCP socket (may experience packet drops if TX buffer is full)
+* **ns3::PacketSocketFactory**: Raw packet socket (useful for link-layer only simulations)
+
+For each socket type, the ``Remote`` address must be configured appropriately:
+
+* For UDP/TCP: Use ``InetSocketAddress`` for IPv4 or ``Inet6SocketAddress`` for IPv6
+* For PacketSocket: Use ``PacketSocketAddress`` with physical layer addressing
+
+The following additional attributes control the behavior of the VoIP traffic generator:
+
+**Protocol Configuration:**
+
+* ``Protocol``: The socket factory type to use. Default: ``ns3::PacketSocketFactory``
+* ``Remote``: The remote address and port for the VoIP destination
+
+**Packet Sizes:**
+
+* ``ActivePacketPayloadSize``: Payload size (in bytes) for packets during active talking states. Default: 33
+* ``SilencePacketPayloadSize``: Payload size (in bytes) for packets during silence states. Default: 7
+
+**State Machine Parameters:**
+
+* ``MeanActiveStateDuration``: Mean duration (Time) of active talking states. Default: 1250 ms
+* ``MeanInactiveStateDuration``: Mean duration (Time) of silence states. Default: 1250 ms
+* ``VoiceToSilenceProbability``: Transition probability from active to silence state per state update. Default: 0.016
+* ``SilenceToVoiceProbability``: Transition probability from silence to active state per state update. Default: 0.016
+
+**Encoder Parameters:**
+
+* ``VoiceEncoderInterval``: Time interval between generation of active voice packets. Default: 20 ms
+* ``SilenceEncoderInterval``: Time interval between generation of silence packets. Default: 160 ms
+
+**Jitter Model:**
+
+* ``MeanDelayJitter``: Mean of the Laplacian distribution for packet delay jitter. Default: 0 us
+* ``ScaleDelayJitter``: Scale parameter of the Laplacian distribution for jitter. Default: 5110 us
+* ``BoundDelayJitter``: Upper bound on the magnitude of applied jitter. Default: 80 ms
+
+Trace Sources
+~~~~~~~~~~~~~
+
+The VoIP traffic generator provides two main trace sources for monitoring simulation behavior:
+
+**TxWithJitter:**
+
+::
+
+  typedef void (*TxTracedCallback)(Ptr<const Packet> packet, Time jitter);
+
+This trace source is fired when a packet is transmitted. It provides both the packet being
+transmitted and the jitter value that was applied to that packet. This is useful for analyzing
+packet transmission patterns and the distribution of applied jitter.
+
+**StateUpdate:**
+
+::
+
+  typedef void (*StateUpdatedCallback)(VoiceActivityState state, Time duration);
+
+This trace source is fired when the voice activity state changes (from active to silence or vice versa).
+It provides the new state and the expected duration of that state. This is useful for analyzing voice
+activity patterns and verifying that the Markov model is producing the expected statistics.
+
+Examples
+~~~~~~~~
+
+The ``voip-example.cc`` program demonstrates basic usage of the VoIP traffic generator.
+This example sets up a simple two-node Wi-Fi network (AP and STA) with VoIP traffic flowing
+from the STA to the AP. The example supports three socket type configurations: IPv4/UDP,
+IPv6/UDP, and PacketSocket.
+
+Tests
+~~~~~
+
+Unit tests for the VoIP traffic model are provided in ``src/applications/test/tgax-voip-traffic-test-suite.cc``.
+These tests verify:
+
+* Correct packet sizes for active and silence states
+* Proper encoder frame intervals (20 ms for active, 160 ms for silence)
+* State duration statistics matching exponential distribution expectations
+* State transition correctness
+* Proper application of jitter to packets
+* Voice activity factor statistics matching theoretical expectations
+
+To run the VoIP traffic tests:
+
+::
+
+  $ ./test.py -s tgax-voip-traffic
+
+Verification
+~~~~~~~~~~~~
+
+The VoIP traffic model has been checked against the IEEE 802.11-14/0571r12 specification
+by comparing:
+
+* Default parameter values against those specified in the standard
+* Observed state durations against exponential distribution properties
+* Voice activity factor (ratio of active to total time) against theoretical expectations
+* Packet sizes and intervals against specification
+
+The test suite demonstrates that the model correctly implements the two-state Markov behavior,
+produces appropriate state durations, and generates traffic patterns consistent with real VoIP systems.
+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/examples/CMakeLists.txt ns-3.47/src/applications/examples/CMakeLists.txt
--- ns-3.46/src/applications/examples/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/examples/CMakeLists.txt	2026-02-16 16:08:49.569229800 -0800
@@ -7,3 +7,47 @@
     ${libinternet}
     ${libnetwork}
 )
+
+build_lib_example(
+  NAME tgax-video-example
+  SOURCE_FILES tgax-video-example.cc
+  LIBRARIES_TO_LINK
+    ${libapplications}
+    ${libwifi}
+    ${libmobility}
+    ${libinternet}
+    ${libnetwork}
+)
+
+build_lib_example(
+  NAME tgax-voip-example
+  SOURCE_FILES tgax-voip-example.cc
+  LIBRARIES_TO_LINK
+    ${libapplications}
+    ${libwifi}
+    ${libmobility}
+    ${libinternet}
+    ${libnetwork}
+)
+
+build_lib_example(
+  NAME tgax-virtual-desktop-example
+  SOURCE_FILES tgax-virtual-desktop-example.cc
+  LIBRARIES_TO_LINK
+    ${libapplications}
+    ${libwifi}
+    ${libmobility}
+    ${libinternet}
+    ${libnetwork}
+)
+
+build_lib_example(
+  NAME rta-tig-mobile-gaming-example
+  SOURCE_FILES rta-tig-mobile-gaming-example.cc
+  LIBRARIES_TO_LINK
+    ${libapplications}
+    ${libwifi}
+    ${libmobility}
+    ${libinternet}
+    ${libnetwork}
+)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/examples/rta-tig-mobile-gaming-example.cc ns-3.47/src/applications/examples/rta-tig-mobile-gaming-example.cc
--- ns-3.46/src/applications/examples/rta-tig-mobile-gaming-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/examples/rta-tig-mobile-gaming-example.cc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,353 @@
+/*
+ * Copyright (c) 2005,2006,2007 INRIA
+ * Copyright (c) 2016 Magister Solutions
+ * Copyright (c) 2025 Tom Henderson
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Borrows from examples/wireless/wifi-ap.cc for overall Wi-Fi network setup pattern
+ * Borrows from src/applications/examples/three-gpp-http-example.cc for overall simulation flow
+ */
+
+/**
+ * @file rta-tig-mobile-gaming-example.cc
+ * @brief A simple real-time mobile gaming traffic generator example over Wi-Fi
+ *
+ * This example demonstrates how to set up a basic ns-3 simulation with real-time
+ * mobile gaming traffic over a Wi-Fi network. Real-time mobile gaming is characterized
+ * by small packets (30-500 bytes) sent frequently (every 30-60ms) with low latency
+ * requirements.
+ *
+ * The simulation consists of:
+ * - A simple Wi-Fi network with one AP (Access Point) and one STA (Station)
+ * - Gaming traffic with three stages: Initial, Gaming, and Ending
+ * - Application-level tracing to observe gaming packets and stage transitions
+ *
+ * The traffic model follows IEEE 802.11-18/2009r6 RTA TIG Report (Section 4.1.4):
+ * - Two synchronization mechanisms: Status Sync and Frame Lockstep Sync
+ * - Packet sizes and arrivals follow Largest Extreme Value distribution
+ * - Initial and end packets follow Uniform distribution
+ *
+ * Traffic model presets:
+ * - Status Sync DL: a=13ms, b=3.7ms arrivals; a=50, b=11 bytes sizes
+ * - Status Sync UL: a=15ms, b=5.7ms arrivals; a=38, b=3.7 bytes sizes
+ * - Lockstep DL: a=28ms, b=4.2ms arrivals; a=210, b=35 bytes sizes
+ * - Lockstep UL: a=22ms, b=3.4ms arrivals; a=92, b=38 bytes sizes
+ *
+ * To run with default (status-sync DL): ./ns3 run rta-tig-mobile-gaming-example
+ * To run status-sync UL: ./ns3 run "rta-tig-mobile-gaming-example --model=status-sync-ul"
+ * To run lockstep DL: ./ns3 run "rta-tig-mobile-gaming-example --model=lockstep-dl"
+ * To run lockstep UL: ./ns3 run "rta-tig-mobile-gaming-example --model=lockstep-ul"
+ *
+ * To disable verbose logging: ./ns3 run "rta-tig-mobile-gaming-example --verbose=false"
+ */
+
+#include "ns3/applications-module.h"
+#include "ns3/core-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/mobility-module.h"
+#include "ns3/network-module.h"
+#include "ns3/wifi-module.h"
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("RtaTigMobileGamingExample");
+
+/**
+ * Callback invoked when a gaming packet is transmitted
+ * @param context The context string
+ * @param packet The transmitted packet
+ * @param stage The traffic model stage
+ */
+void GamingPacketSent(std::string context,
+                      Ptr<const Packet> packet,
+                      RtaTigMobileGaming::TrafficModelStage stage);
+
+/**
+ * Callback invoked when the PacketSink receives a packet
+ * @param context The context string
+ * @param packet The received packet
+ * @param address The sender's address
+ */
+void PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address);
+
+int
+main(int argc, char* argv[])
+{
+    Time duration{Seconds(10)};          // Simulation time in seconds
+    std::string model{"status-sync-dl"}; // Traffic model preset
+    bool verbose = true;                 // Enable/disable verbose logging
+
+    CommandLine cmd(__FILE__);
+    cmd.Usage("Real-time mobile gaming traffic example");
+    cmd.AddValue("duration", "Duration of traffic flow, in seconds", duration);
+    cmd.AddValue("model",
+                 "Traffic model preset (status-sync-dl, status-sync-ul, lockstep-dl, lockstep-ul). "
+                 "Default: status-sync-dl",
+                 model);
+    cmd.AddValue("verbose",
+                 "Enable verbose logging of RtaTigMobileGaming, PacketSink, and this program",
+                 verbose);
+    cmd.Parse(argc, argv);
+
+    if (verbose)
+    {
+        LogComponentEnable(
+            "RtaTigMobileGamingExample",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_ALL));
+        LogComponentEnable(
+            "RtaTigMobileGaming",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+        LogComponentEnable(
+            "PacketSink",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+    }
+
+    NodeContainer wifiNodes;
+    wifiNodes.Create(2); // Create 2 nodes: one will be AP, one will be STA
+
+    Ptr<Node> apNode = wifiNodes.Get(0);
+    Ptr<Node> staNode = wifiNodes.Get(1);
+
+    YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
+    YansWifiPhyHelper phy;
+    phy.SetChannel(channel.Create());
+
+    WifiMacHelper mac;
+    WifiHelper wifi;
+    wifi.SetStandard(WIFI_STANDARD_80211ax);
+
+    NetDeviceContainer apDevices;
+    NetDeviceContainer staDevices;
+
+    // Configure AP
+    Ssid ssid = Ssid("gaming-network");
+    mac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid));
+    apDevices = wifi.Install(phy, mac, apNode);
+
+    // Configure STA
+    mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid));
+    staDevices = wifi.Install(phy, mac, staNode);
+
+    MobilityHelper mobility;
+    mobility.SetPositionAllocator("ns3::GridPositionAllocator",
+                                  "MinX",
+                                  DoubleValue(0.0),
+                                  "MinY",
+                                  DoubleValue(0.0),
+                                  "DeltaX",
+                                  DoubleValue(5.0),
+                                  "DeltaY",
+                                  DoubleValue(5.0),
+                                  "GridWidth",
+                                  UintegerValue(2),
+                                  "LayoutType",
+                                  StringValue("RowFirst"));
+    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
+    mobility.Install(wifiNodes);
+
+    InternetStackHelper internet;
+    internet.Install(wifiNodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+
+    NetDeviceContainer allDevices;
+    allDevices.Add(apDevices);
+    allDevices.Add(staDevices);
+    Ipv4InterfaceContainer ipv4Interfaces = ipv4.Assign(allDevices);
+
+    NS_LOG_INFO("AP address: " << ipv4Interfaces.GetAddress(0));
+    NS_LOG_INFO("STA address: " << ipv4Interfaces.GetAddress(1));
+
+    // Gaming uses UDP as specified
+    std::string protocolFactory = "ns3::UdpSocketFactory";
+    uint16_t port = 5000;
+
+    ApplicationHelper sourceHelper(RtaTigMobileGaming::GetTypeId());
+    sourceHelper.SetAttribute("Protocol", TypeIdValue(TypeId::LookupByName(protocolFactory)));
+
+    // Configure traffic model parameters based on preset
+    // Parameters from IEEE 802.11-18/2009r6 Tables 4-3 and 4-4
+    Ptr<Node> sourceNode;
+    Ptr<Node> sinkNode;
+    std::string remoteAddress;
+
+    if (model == "status-sync-dl")
+    {
+        // Downlink: AP -> STA
+        sourceNode = apNode;
+        sinkNode = staNode;
+        remoteAddress = "10.1.1.2";
+
+        // Status-sync DL parameters (defaults)
+        // Initial: [0, 20], End: [500, 600]
+        // Arrivals: LEV(13ms, 3.7ms), Sizes: LEV(50, 11)
+        NS_LOG_INFO("Using Status-Sync Downlink model (default parameters)");
+    }
+    else if (model == "status-sync-ul")
+    {
+        // Uplink: STA -> AP
+        sourceNode = staNode;
+        sinkNode = apNode;
+        remoteAddress = "10.1.1.1";
+
+        // Status-sync UL parameters
+        auto ips = CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                                     DoubleValue(0),
+                                                                     "Max",
+                                                                     DoubleValue(20));
+        sourceHelper.SetAttribute("InitialPacketSize", PointerValue(ips));
+
+        auto eps = CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                                     DoubleValue(400),
+                                                                     "Max",
+                                                                     DoubleValue(550));
+        sourceHelper.SetAttribute("EndPacketSize", PointerValue(eps));
+
+        auto pal = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+            "Location",
+            DoubleValue(15000), // 15ms in us
+            "Scale",
+            DoubleValue(5700)); // 5.7ms in us
+        sourceHelper.SetAttribute("PacketArrivalLev", PointerValue(pal));
+
+        auto psl = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>("Location",
+                                                                                 DoubleValue(38),
+                                                                                 "Scale",
+                                                                                 DoubleValue(3.7));
+        sourceHelper.SetAttribute("PacketSizeLev", PointerValue(psl));
+
+        NS_LOG_INFO("Using Status-Sync Uplink model");
+    }
+    else if (model == "lockstep-dl")
+    {
+        // Downlink: AP -> STA
+        sourceNode = apNode;
+        sinkNode = staNode;
+        remoteAddress = "10.1.1.2";
+
+        // Lockstep DL parameters
+        auto ips = CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                                     DoubleValue(0),
+                                                                     "Max",
+                                                                     DoubleValue(80));
+        sourceHelper.SetAttribute("InitialPacketSize", PointerValue(ips));
+
+        auto eps = CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                                     DoubleValue(1400),
+                                                                     "Max",
+                                                                     DoubleValue(1500));
+        sourceHelper.SetAttribute("EndPacketSize", PointerValue(eps));
+
+        auto pal = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+            "Location",
+            DoubleValue(28000), // 28ms in us
+            "Scale",
+            DoubleValue(4200)); // 4.2ms in us
+        sourceHelper.SetAttribute("PacketArrivalLev", PointerValue(pal));
+
+        auto psl = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>("Location",
+                                                                                 DoubleValue(210),
+                                                                                 "Scale",
+                                                                                 DoubleValue(35));
+        sourceHelper.SetAttribute("PacketSizeLev", PointerValue(psl));
+
+        NS_LOG_INFO("Using Frame Lockstep Downlink model");
+    }
+    else if (model == "lockstep-ul")
+    {
+        // Uplink: STA -> AP
+        sourceNode = staNode;
+        sinkNode = apNode;
+        remoteAddress = "10.1.1.1";
+
+        // Lockstep UL parameters
+        auto ips = CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                                     DoubleValue(0),
+                                                                     "Max",
+                                                                     DoubleValue(80));
+        sourceHelper.SetAttribute("InitialPacketSize", PointerValue(ips));
+
+        auto eps = CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                                     DoubleValue(500),
+                                                                     "Max",
+                                                                     DoubleValue(600));
+        sourceHelper.SetAttribute("EndPacketSize", PointerValue(eps));
+
+        auto pal = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+            "Location",
+            DoubleValue(22000), // 22ms in us
+            "Scale",
+            DoubleValue(3400)); // 3.4ms in us
+        sourceHelper.SetAttribute("PacketArrivalLev", PointerValue(pal));
+
+        auto psl = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>("Location",
+                                                                                 DoubleValue(92),
+                                                                                 "Scale",
+                                                                                 DoubleValue(38));
+        sourceHelper.SetAttribute("PacketSizeLev", PointerValue(psl));
+
+        NS_LOG_INFO("Using Frame Lockstep Uplink model");
+    }
+    else
+    {
+        NS_FATAL_ERROR(
+            "Invalid model: "
+            << model
+            << ". Use 'status-sync-dl', 'status-sync-ul', 'lockstep-dl', or 'lockstep-ul'.");
+    }
+
+    Address remoteAddr = InetSocketAddress(Ipv4Address(remoteAddress.c_str()), port);
+    sourceHelper.SetAttribute("Remote", AddressValue(remoteAddr));
+
+    auto sourceApps = sourceHelper.Install(sourceNode);
+    sourceApps.Start(Seconds(1.0));
+    sourceApps.Stop(Seconds(1.0) + duration);
+
+    PacketSinkHelper sinkHelper(protocolFactory, InetSocketAddress(Ipv4Address::GetAny(), port));
+    auto sinkApps = sinkHelper.Install(sinkNode);
+    sinkApps.Start(Seconds(0.0));
+    sinkApps.Stop(Seconds(2.0) + duration);
+
+    // Connect to gaming TX with stage trace
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::RtaTigMobileGaming/TxWithStage",
+                    MakeCallback(&GamingPacketSent));
+
+    // Connect to RX trace
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&PacketReceived));
+
+    NS_LOG_INFO("Starting simulation for traffic duration: " << duration.As(Time::S));
+    NS_LOG_INFO("Traffic model: " << model);
+
+    Simulator::Stop(Seconds(2) + duration);
+    Simulator::Run();
+
+    // Get statistics from packet sink
+    Ptr<PacketSink> sink = DynamicCast<PacketSink>(sinkApps.Get(0));
+    if (sink)
+    {
+        uint32_t bytesRx = sink->GetTotalRx();
+        NS_LOG_INFO("Total bytes received: " << bytesRx);
+    }
+
+    Simulator::Destroy();
+
+    return 0;
+}
+
+void
+GamingPacketSent(std::string context,
+                 Ptr<const Packet> packet,
+                 RtaTigMobileGaming::TrafficModelStage stage)
+{
+    NS_LOG_INFO("Gaming TX [" << context << "]: Packet size (bytes): " << packet->GetSize()
+                              << " Stage: " << stage);
+}
+
+void
+PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address)
+{
+    NS_LOG_INFO("Packet RX [" << context << "]: Size(bytes): " << packet->GetSize());
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/examples/tgax-video-example.cc ns-3.47/src/applications/examples/tgax-video-example.cc
--- ns-3.46/src/applications/examples/tgax-video-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/examples/tgax-video-example.cc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,325 @@
+/*
+ * Copyright (c) 2005,2006,2007 INRIA
+ * Copyright (c) 2016 Magister Solutions
+ * Copyright (c) 2025 Tom Henderson
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Borrows from examples/wireless/wifi-ap.cc for overall Wi-Fi network setup pattern
+ * Borrows from src/applications/examples/three-gpp-http-example.cc for overall simulation flow
+ */
+
+/**
+ * @file tgax-video-example.cc
+ * @brief A simple buffered video streaming traffic generator example over Wi-Fi
+ *
+ * This example demonstrates how to set up a basic ns-3 simulation with video streaming
+ * traffic over a Wi-Fi network. It includes support for:
+ *
+ * 1. Buffered video streaming (BV1-BV6): Different bit rates from 2Mbps to 15.6Mbps
+ * 2. Multicast video streaming (MC1-MC2): 3Mbps and 6Mbps multicast
+ * 3. Custom video parameters: User-defined Weibull parameters and bit rate
+ *
+ * The simulation consists of:
+ * - A simple Wi-Fi network with one AP (Access Point) and one or more STAs (Stations)
+ * - Video traffic flowing from the AP to the STA(s)
+ * - Application-level tracing to observe video frames being generated and sent
+ *
+ * The video traffic model follows IEEE 802.11-14/0571r12 TGAX evaluation methodology:
+ * - Video frame sizes are drawn from a Weibull distribution
+ * - Network latency is modeled using a Gamma distribution (mean ~14.8ms)
+ * - Multiple predefined profiles for different video quality levels
+ *
+ * To run with default settings (BV1, 2Mbps): ./ns3 run tgax-video-example
+ * To run with higher bit rate: ./ns3 run "tgax-video-example --model=BV3"
+ * To run with multicast: ./ns3 run "tgax-video-example --model=MC1"
+ * To run with TCP: ./ns3 run "tgax-video-example --protocol=tcp"
+ *
+ * To disable verbose logging: ./ns3 run "tgax-video-example --verbose=false"
+ */
+
+#include "ns3/applications-module.h"
+#include "ns3/core-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/mobility-module.h"
+#include "ns3/network-module.h"
+#include "ns3/wifi-module.h"
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TgaxVideoExample");
+
+/**
+ * Callback invoked when a video packet is transmitted
+ * @param packet The transmitted packet
+ * @param latency The network latency applied to the packet
+ */
+void VideoPacketSent(Ptr<const Packet> packet, Time latency);
+
+/**
+ * Callback invoked when a video frame is generated
+ * @param frameSize The size of the generated frame in bytes
+ */
+void VideoFrameGenerated(uint32_t frameSize);
+
+/**
+ * Callback invoked when the PacketSink receives a packet
+ * @param context The context string
+ * @param packet The received packet
+ * @param address The sender's address
+ */
+void PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address);
+
+int
+main(int argc, char* argv[])
+{
+    Time duration{Seconds(10)};  // Simulation time in seconds
+    std::string model{"BV1"};    // Traffic model: BV1-BV6, MC1-MC2, or Custom
+    std::string protocol{"udp"}; // Protocol: udp or tcp
+    bool verbose = true;         // Enable/disable verbose logging
+
+    CommandLine cmd(__FILE__);
+    cmd.Usage("Buffered video streaming example");
+    cmd.AddValue("duration", "Duration of traffic flow, in seconds", duration);
+    cmd.AddValue("model",
+                 "Traffic model to use (BV1, BV2, BV3, BV4, BV5, BV6, MC1, MC2, or Custom). "
+                 "Default: BV1",
+                 model);
+    cmd.AddValue("protocol", "Protocol to use (udp or tcp). Default: udp", protocol);
+    cmd.AddValue("verbose",
+                 "Enable verbose logging of TgaxVideoTraffic, PacketSink, and this program",
+                 verbose);
+    cmd.Parse(argc, argv);
+
+    if (verbose)
+    {
+        LogComponentEnable(
+            "TgaxVideoExample",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_ALL));
+        LogComponentEnable(
+            "TgaxVideoTraffic",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+        LogComponentEnable(
+            "PacketSink",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+    }
+
+    // Determine if this is a multicast scenario
+    bool isMulticast = (model == "MC1" || model == "MC2");
+    uint32_t numStations = isMulticast ? 2 : 1;
+
+    NodeContainer wifiNodes;
+    wifiNodes.Create(1 + numStations); // AP + STAs
+
+    Ptr<Node> apNode = wifiNodes.Get(0);
+    NodeContainer staNodes;
+    for (uint32_t i = 0; i < numStations; ++i)
+    {
+        staNodes.Add(wifiNodes.Get(1 + i));
+    }
+
+    YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
+    YansWifiPhyHelper phy;
+    phy.SetChannel(channel.Create());
+
+    WifiMacHelper mac;
+    WifiHelper wifi;
+    wifi.SetStandard(WIFI_STANDARD_80211ax);
+
+    NetDeviceContainer apDevices;
+    NetDeviceContainer staDevices;
+
+    // Configure AP
+    Ssid ssid = Ssid("video-network");
+    mac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid));
+    apDevices = wifi.Install(phy, mac, apNode);
+
+    // Configure STAs
+    mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid));
+    staDevices = wifi.Install(phy, mac, staNodes);
+
+    MobilityHelper mobility;
+    mobility.SetPositionAllocator("ns3::GridPositionAllocator",
+                                  "MinX",
+                                  DoubleValue(0.0),
+                                  "MinY",
+                                  DoubleValue(0.0),
+                                  "DeltaX",
+                                  DoubleValue(5.0),
+                                  "DeltaY",
+                                  DoubleValue(5.0),
+                                  "GridWidth",
+                                  UintegerValue(3),
+                                  "LayoutType",
+                                  StringValue("RowFirst"));
+    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
+    mobility.Install(wifiNodes);
+
+    InternetStackHelper internet;
+    internet.Install(wifiNodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+
+    NetDeviceContainer allDevices;
+    allDevices.Add(apDevices);
+    allDevices.Add(staDevices);
+    Ipv4InterfaceContainer ipv4Interfaces = ipv4.Assign(allDevices);
+
+    NS_LOG_INFO("AP address: " << ipv4Interfaces.GetAddress(0));
+    for (uint32_t i = 0; i < numStations; ++i)
+    {
+        NS_LOG_INFO("STA " << i << " address: " << ipv4Interfaces.GetAddress(1 + i));
+    }
+
+    std::string protocolFactory;
+    if (protocol == "udp")
+    {
+        protocolFactory = "ns3::UdpSocketFactory";
+    }
+    else if (protocol == "tcp")
+    {
+        protocolFactory = "ns3::TcpSocketFactory";
+    }
+    else
+    {
+        NS_FATAL_ERROR("Invalid protocol: " << protocol << ". Use 'udp' or 'tcp'.");
+    }
+
+    uint16_t remotePort = 5000;
+    std::string remoteAddress;
+    if (isMulticast)
+    {
+        // For multicast, use a multicast address
+        remoteAddress = "239.192.100.1";
+
+        // Add multicast route on AP
+        Ipv4StaticRoutingHelper staticRouting;
+        Ptr<Ipv4> ipv4Proto = apNode->GetObject<Ipv4>();
+        Ptr<Ipv4StaticRouting> routing = staticRouting.GetStaticRouting(ipv4Proto);
+        routing->AddHostRouteTo(Ipv4Address(remoteAddress.c_str()),
+                                ipv4Proto->GetInterfaceForDevice(apNode->GetDevice(0)),
+                                0);
+    }
+    else
+    {
+        // For unicast, use the first STA's address
+        remoteAddress = "10.1.1.2";
+    }
+
+    ApplicationHelper sourceHelper(TgaxVideoTraffic::GetTypeId());
+
+    // Configure video parameters based on model
+    // Traffic models from IEEE 802.11-14/0571r12:
+    // BV1: 2Mbps, BV2: 4Mbps, BV3: 6Mbps, BV4: 8Mbps, BV5: 10Mbps, BV6: 15.6Mbps
+    // MC1: 3Mbps multicast, MC2: 6Mbps multicast
+    sourceHelper.SetAttribute("Protocol", TypeIdValue(TypeId::LookupByName(protocolFactory)));
+    sourceHelper.SetAttribute("TrafficModelClassIdentifier", StringValue(model));
+
+    Address remoteAddr = InetSocketAddress(Ipv4Address(remoteAddress.c_str()), remotePort);
+    sourceHelper.SetAttribute("Remote", AddressValue(remoteAddr));
+
+    // Install video source on AP node (downlink traffic)
+    auto sourceApps = sourceHelper.Install(apNode);
+    sourceApps.Start(Seconds(1.0));
+    sourceApps.Stop(Seconds(1.0) + duration);
+
+    // Install packet sinks on STAs
+    PacketSinkHelper sinkHelper(protocolFactory,
+                                InetSocketAddress(Ipv4Address::GetAny(), remotePort));
+    auto sinkApps = sinkHelper.Install(staNodes);
+    sinkApps.Start(Seconds(0.0));
+    sinkApps.Stop(Seconds(2.0) + duration);
+
+    NS_LOG_INFO("PacketSink application installed on STA node(s)");
+
+    // Connect to video TX with latency trace
+    Config::ConnectWithoutContext(
+        "/NodeList/*/ApplicationList/*/$ns3::TgaxVideoTraffic/TxWithLatency",
+        MakeCallback(&VideoPacketSent));
+
+    // Connect to video frame generation trace
+    Config::ConnectWithoutContext(
+        "/NodeList/*/ApplicationList/*/$ns3::TgaxVideoTraffic/VideoFrameGenerated",
+        MakeCallback(&VideoFrameGenerated));
+
+    // Connect to RX trace
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&PacketReceived));
+
+    // Get expected bit rate for the model
+    std::map<std::string, double> modelBitRates = {{"BV1", 2.0},
+                                                   {"BV2", 4.0},
+                                                   {"BV3", 6.0},
+                                                   {"BV4", 8.0},
+                                                   {"BV5", 10.0},
+                                                   {"BV6", 15.6},
+                                                   {"MC1", 3.0},
+                                                   {"MC2", 6.0},
+                                                   {"Custom", 2.0}};
+
+    double expectedBitRate = modelBitRates.count(model) ? modelBitRates[model] : 2.0;
+
+    NS_LOG_INFO("Starting simulation for traffic duration: " << duration.As(Time::S));
+    NS_LOG_INFO("Traffic model: " << model);
+    NS_LOG_INFO("Protocol: " << protocol);
+    NS_LOG_INFO("Expected bit rate: " << expectedBitRate << " Mbps");
+    if (isMulticast)
+    {
+        NS_LOG_INFO("Multicast mode: delivering to " << numStations << " receivers");
+    }
+
+    Simulator::Stop(Seconds(2) + duration);
+    Simulator::Run();
+
+    // Get statistics from packet sink applications
+    uint64_t totalBytesReceived = 0;
+    for (uint32_t i = 0; i < sinkApps.GetN(); ++i)
+    {
+        Ptr<PacketSink> sink = DynamicCast<PacketSink>(sinkApps.Get(i));
+        if (sink)
+        {
+            uint32_t bytesRx = sink->GetTotalRx();
+            NS_LOG_INFO("STA " << i << " received: " << bytesRx << " bytes");
+            totalBytesReceived += bytesRx;
+        }
+    }
+
+    // Calculate approximate expected bytes
+    // expectedBitRate is in Mbps, duration is in seconds
+    double expectedBytes = (expectedBitRate * 1e6 / 8) * duration.GetSeconds();
+    NS_LOG_INFO("Total bytes received: " << totalBytesReceived);
+    NS_LOG_INFO("Approximate expected bytes per receiver: " << expectedBytes);
+
+    // Calculate measured bit rate
+    double measuredBitRate =
+        static_cast<double>(totalBytesReceived * 8) / (duration.GetSeconds() * 1e6);
+    if (isMulticast)
+    {
+        measuredBitRate /= numStations; // Per-receiver rate
+    }
+    NS_LOG_INFO("Measured bit rate: " << measuredBitRate << " Mbps");
+
+    Simulator::Destroy();
+
+    return 0;
+}
+
+void
+VideoPacketSent(Ptr<const Packet> packet, Time latency)
+{
+    NS_LOG_INFO("Video TX: Packet size (bytes): " << packet->GetSize()
+                                                  << " Latency: " << latency.As(Time::US));
+}
+
+void
+VideoFrameGenerated(uint32_t frameSize)
+{
+    NS_LOG_INFO("Video Frame Generated: " << frameSize << " bytes");
+}
+
+void
+PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address)
+{
+    NS_LOG_INFO("Packet RX: Size(bytes): " << packet->GetSize());
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/examples/tgax-virtual-desktop-example.cc ns-3.47/src/applications/examples/tgax-virtual-desktop-example.cc
--- ns-3.46/src/applications/examples/tgax-virtual-desktop-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/examples/tgax-virtual-desktop-example.cc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2005,2006,2007 INRIA
+ * Copyright (c) 2016 Magister Solutions
+ * Copyright (c) 2025 Tom Henderson
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Borrows from examples/wireless/wifi-ap.cc for overall Wi-Fi network setup pattern
+ * Borrows from src/applications/examples/three-gpp-http-example.cc for overall simulation flow
+ */
+
+/**
+ * @file tgax-virtual-desktop-example.cc
+ * @brief A simple Virtual Desktop Infrastructure (VDI) traffic generator example over Wi-Fi
+ *
+ * This example demonstrates how to set up a basic ns-3 simulation with VDI traffic
+ * over a Wi-Fi network. VDI traffic models remote desktop applications where a server
+ * sends desktop display data to clients.
+ *
+ * The simulation consists of:
+ * - A simple Wi-Fi network with one AP (Access Point) and one STA (Station)
+ * - VDI traffic flowing from the AP (server) to the STA (client) for downlink
+ * - Optional uplink traffic from STA to AP for navigation/feedback
+ * - Application-level tracing to observe VDI packets being sent
+ *
+ * The VDI traffic model follows IEEE 802.11-14/0571r12 TGAX evaluation methodology:
+ * - Packet inter-arrival times follow an exponential distribution
+ * - Packet sizes follow a normal distribution (bimodal for downlink)
+ * - Initial packet arrival is uniformly distributed in [0, 20ms]
+ *
+ * Traffic direction parameters from the specification:
+ * - Downlink (AP to STA): Mean arrival 60.2269ms, bimodal packet size (41/1478 bytes)
+ * - Uplink (STA to AP): Mean arrival 48.2870ms, normal packet size (mean 50.598 bytes)
+ *
+ * To run downlink traffic (default): ./ns3 run tgax-virtual-desktop-example
+ * To run uplink traffic: ./ns3 run "tgax-virtual-desktop-example --direction=uplink"
+ * To run bidirectional: ./ns3 run "tgax-virtual-desktop-example --direction=bidirectional"
+ *
+ * To disable verbose logging: ./ns3 run "tgax-virtual-desktop-example --verbose=false"
+ */
+
+#include "ns3/applications-module.h"
+#include "ns3/core-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/mobility-module.h"
+#include "ns3/network-module.h"
+#include "ns3/wifi-module.h"
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TgaxVirtualDesktopExample");
+
+/**
+ * Callback invoked when a VDI packet is transmitted
+ * @param context The context string identifying the source
+ * @param packet The transmitted packet
+ */
+void VdiPacketSent(std::string context, Ptr<const Packet> packet);
+
+/**
+ * Callback invoked when the PacketSink receives a packet
+ * @param context The context string
+ * @param packet The received packet
+ * @param address The sender's address
+ */
+void PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address);
+
+int
+main(int argc, char* argv[])
+{
+    Time duration{Seconds(10)};        // Simulation time in seconds
+    std::string direction{"downlink"}; // Traffic direction: downlink, uplink, or bidirectional
+    bool verbose = true;               // Enable/disable verbose logging
+
+    CommandLine cmd(__FILE__);
+    cmd.Usage("Virtual Desktop Infrastructure (VDI) traffic example");
+    cmd.AddValue("duration", "Duration of traffic flow, in seconds", duration);
+    cmd.AddValue("direction",
+                 "Traffic direction (downlink, uplink, or bidirectional). Default: downlink",
+                 direction);
+    cmd.AddValue("verbose",
+                 "Enable verbose logging of TgaxVirtualDesktop, PacketSink, and this program",
+                 verbose);
+    cmd.Parse(argc, argv);
+
+    if (verbose)
+    {
+        LogComponentEnable(
+            "TgaxVirtualDesktopExample",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_ALL));
+        LogComponentEnable(
+            "TgaxVirtualDesktop",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+        LogComponentEnable(
+            "PacketSink",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+    }
+
+    NodeContainer wifiNodes;
+    wifiNodes.Create(2); // Create 2 nodes: one will be AP, one will be STA
+
+    Ptr<Node> apNode = wifiNodes.Get(0);
+    Ptr<Node> staNode = wifiNodes.Get(1);
+
+    YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
+    YansWifiPhyHelper phy;
+    phy.SetChannel(channel.Create());
+
+    WifiMacHelper mac;
+    WifiHelper wifi;
+    wifi.SetStandard(WIFI_STANDARD_80211ax);
+
+    NetDeviceContainer apDevices;
+    NetDeviceContainer staDevices;
+
+    // Configure AP
+    Ssid ssid = Ssid("vdi-network");
+    mac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid));
+    apDevices = wifi.Install(phy, mac, apNode);
+
+    // Configure STA
+    mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid));
+    staDevices = wifi.Install(phy, mac, staNode);
+
+    MobilityHelper mobility;
+    mobility.SetPositionAllocator("ns3::GridPositionAllocator",
+                                  "MinX",
+                                  DoubleValue(0.0),
+                                  "MinY",
+                                  DoubleValue(0.0),
+                                  "DeltaX",
+                                  DoubleValue(5.0),
+                                  "DeltaY",
+                                  DoubleValue(5.0),
+                                  "GridWidth",
+                                  UintegerValue(2),
+                                  "LayoutType",
+                                  StringValue("RowFirst"));
+    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
+    mobility.Install(wifiNodes);
+
+    InternetStackHelper internet;
+    internet.Install(wifiNodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+
+    NetDeviceContainer allDevices;
+    allDevices.Add(apDevices);
+    allDevices.Add(staDevices);
+    Ipv4InterfaceContainer ipv4Interfaces = ipv4.Assign(allDevices);
+
+    NS_LOG_INFO("AP address: " << ipv4Interfaces.GetAddress(0));
+    NS_LOG_INFO("STA address: " << ipv4Interfaces.GetAddress(1));
+
+    // VDI uses TCP as specified in the standard
+    std::string protocolFactory = "ns3::TcpSocketFactory";
+    uint16_t dlPort = 5000; // Downlink port
+    uint16_t ulPort = 5001; // Uplink port
+
+    ApplicationContainer sourceApps;
+    ApplicationContainer sinkApps;
+
+    // Configure downlink traffic (AP -> STA)
+    if (direction == "downlink" || direction == "bidirectional")
+    {
+        ApplicationHelper dlSourceHelper(TgaxVirtualDesktop::GetTypeId());
+        dlSourceHelper.SetAttribute("Protocol", TypeIdValue(TypeId::LookupByName(protocolFactory)));
+
+        // Downlink parameters from IEEE 802.11-14/0571r12:
+        // - Mean inter-arrival: 60.2269 ms
+        // - Packet size: Bimodal Normal (mu1=41.0, sigma1=3.2; mu2=1478.3, sigma2=11.6)
+        // Default attributes already match downlink specification
+
+        Address dlRemoteAddr = InetSocketAddress(ipv4Interfaces.GetAddress(1), dlPort);
+        dlSourceHelper.SetAttribute("Remote", AddressValue(dlRemoteAddr));
+
+        auto dlSourceApp = dlSourceHelper.Install(apNode);
+        dlSourceApp.Start(Seconds(1.0));
+        dlSourceApp.Stop(Seconds(1.0) + duration);
+        sourceApps.Add(dlSourceApp);
+
+        PacketSinkHelper dlSinkHelper(protocolFactory,
+                                      InetSocketAddress(Ipv4Address::GetAny(), dlPort));
+        auto dlSinkApp = dlSinkHelper.Install(staNode);
+        dlSinkApp.Start(Seconds(0.0));
+        dlSinkApp.Stop(Seconds(2.0) + duration);
+        sinkApps.Add(dlSinkApp);
+
+        NS_LOG_INFO("Downlink VDI traffic configured (AP -> STA)");
+        NS_LOG_INFO("  Mean inter-arrival: 60.2269 ms");
+        NS_LOG_INFO("  Packet size: Bimodal Normal (41.0/1478.3 bytes)");
+    }
+
+    // Configure uplink traffic (STA -> AP)
+    if (direction == "uplink" || direction == "bidirectional")
+    {
+        ApplicationHelper ulSourceHelper(TgaxVirtualDesktop::GetTypeId());
+        ulSourceHelper.SetAttribute("Protocol", TypeIdValue(TypeId::LookupByName(protocolFactory)));
+
+        // Uplink parameters from IEEE 802.11-14/0571r12:
+        // - Mean inter-arrival: 48.2870 ms
+        // - Packet size: Normal (mu=50.598, sigma=5.0753)
+        auto ulInterArrival = CreateObjectWithAttributes<ExponentialRandomVariable>(
+            "Mean",
+            DoubleValue(48287000.0)); // 48.287 ms in nanoseconds
+        ulSourceHelper.SetAttribute("InterPacketArrivals", PointerValue(ulInterArrival));
+        ulSourceHelper.SetAttribute("ParametersPacketSize", StringValue("50.598 5.0753"));
+
+        Address ulRemoteAddr = InetSocketAddress(ipv4Interfaces.GetAddress(0), ulPort);
+        ulSourceHelper.SetAttribute("Remote", AddressValue(ulRemoteAddr));
+
+        auto ulSourceApp = ulSourceHelper.Install(staNode);
+        ulSourceApp.Start(Seconds(1.0));
+        ulSourceApp.Stop(Seconds(1.0) + duration);
+        sourceApps.Add(ulSourceApp);
+
+        PacketSinkHelper ulSinkHelper(protocolFactory,
+                                      InetSocketAddress(Ipv4Address::GetAny(), ulPort));
+        auto ulSinkApp = ulSinkHelper.Install(apNode);
+        ulSinkApp.Start(Seconds(0.0));
+        ulSinkApp.Stop(Seconds(2.0) + duration);
+        sinkApps.Add(ulSinkApp);
+
+        NS_LOG_INFO("Uplink VDI traffic configured (STA -> AP)");
+        NS_LOG_INFO("  Mean inter-arrival: 48.2870 ms");
+        NS_LOG_INFO("  Packet size: Normal (mean 50.598 bytes)");
+    }
+
+    if (direction != "downlink" && direction != "uplink" && direction != "bidirectional")
+    {
+        NS_FATAL_ERROR("Invalid direction: " << direction
+                                             << ". Use 'downlink', 'uplink', or 'bidirectional'.");
+    }
+
+    // Connect to VDI TX trace
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::TgaxVirtualDesktop/Tx",
+                    MakeCallback(&VdiPacketSent));
+
+    // Connect to RX trace
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&PacketReceived));
+
+    NS_LOG_INFO("Starting simulation for traffic duration: " << duration.As(Time::S));
+    NS_LOG_INFO("Traffic direction: " << direction);
+
+    Simulator::Stop(Seconds(2) + duration);
+    Simulator::Run();
+
+    // Get statistics from packet sink applications
+    for (uint32_t i = 0; i < sinkApps.GetN(); ++i)
+    {
+        Ptr<PacketSink> sink = DynamicCast<PacketSink>(sinkApps.Get(i));
+        if (sink)
+        {
+            uint32_t bytesRx = sink->GetTotalRx();
+            NS_LOG_INFO("Sink " << i << " received: " << bytesRx << " bytes");
+        }
+    }
+
+    Simulator::Destroy();
+
+    return 0;
+}
+
+void
+VdiPacketSent(std::string context, Ptr<const Packet> packet)
+{
+    NS_LOG_INFO("VDI TX [" << context << "]: Packet size (bytes): " << packet->GetSize());
+}
+
+void
+PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address)
+{
+    NS_LOG_INFO("Packet RX [" << context << "]: Size(bytes): " << packet->GetSize());
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/examples/tgax-voip-example.cc ns-3.47/src/applications/examples/tgax-voip-example.cc
--- ns-3.46/src/applications/examples/tgax-voip-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/examples/tgax-voip-example.cc	2026-02-16 16:08:49.569229800 -0800
@@ -0,0 +1,347 @@
+/*
+ * Copyright (c) 2005,2006,2007 INRIA
+ * Copyright (c) 2016 Magister Solutions
+ * Copyright (c) 2025 Tom Henderson
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Borrows from examples/wireless/wifi-ap.cc for overall Wi-Fi network setup pattern
+ * Borrows from src/applications/examples/three-gpp-http-example.cc for overall simulation flow
+ */
+
+/**
+ * @file tgax-voip-example.cc
+ * @brief A simple VoIP traffic generator example over Wi-Fi
+ *
+ * This example demonstrates how to set up a basic ns-3 simulation with VoIP traffic
+ * over a Wi-Fi network. It includes three configurations:
+ *
+ * 1. IPv4/UDP: VoIP traffic over IPv4 using UDP sockets
+ * 2. IPv6/UDP: VoIP traffic over IPv6 using UDP sockets
+ * 3. PacketSocket: VoIP traffic using raw packet sockets
+ *
+ * The simulation consists of:
+ * - A simple Wi-Fi network with one AP (Access Point) and one STA (Station)
+ * - VoIP traffic flowing from the STA to the AP
+ * - Application-level tracing to observe VoIP packets being sent
+ * - State change tracing to see voice activity transitions
+ *
+ * To run with IPv4/UDP (default): ./ns3 run tgax-voip-example
+ * To run with IPv6/UDP: ./ns3 run "tgax-voip-example --socketType=ipv6"
+ * To run with PacketSocket: ./ns3 run "tgax-voip-example --socketType=packet"
+ *
+ * To disable verbose logging: ./ns3 run "tgax-voip-example --verbose=false"
+ */
+
+#include "ns3/applications-module.h"
+#include "ns3/core-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/mobility-module.h"
+#include "ns3/network-module.h"
+#include "ns3/wifi-module.h"
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TgaxVoipExample");
+
+/**
+ * Callback invoked when a VoIP packet is transmitted
+ * @param packet The transmitted packet
+ * @param jitter The jitter applied to the packet
+ */
+void VoipPacketSent(Ptr<const Packet> packet, Time jitter);
+/**
+ * Callback invoked when the VoIP application state changes (active/silence)
+ * @param state The new voice activity state
+ * @param duration The expected duration in this state
+ */
+void VoipStateChanged(TgaxVoipTraffic::VoiceActivityState state, Time duration);
+/**
+ * Callback invoked when the PacketSink receives a packet
+ * @param context The context string
+ * @param packet The received packet
+ * @param address The sender's address
+ */
+void PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address);
+
+int
+main(int argc, char* argv[])
+{
+    Time duration{Seconds(10)};     // Simulation time in seconds
+    std::string socketType{"ipv4"}; // Socket type: ipv4, ipv6, or packet
+    bool verbose = true;            // Enable/disable verbose logging
+
+    CommandLine cmd(__FILE__);
+    cmd.Usage("Basic VoIP example");
+    cmd.AddValue("duration", "Duration of traffic flow, in seconds", duration);
+    cmd.AddValue("socketType",
+                 "Socket type to use (ipv4, ipv6, or packet). Default: ipv4",
+                 socketType);
+    cmd.AddValue("verbose",
+                 "Enable verbose logging of TgaxVoipTraffic, PacketSink, and this program",
+                 verbose);
+    cmd.Parse(argc, argv);
+
+    if (verbose)
+    {
+        LogComponentEnable(
+            "TgaxVoipExample",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_ALL));
+        LogComponentEnable(
+            "TgaxVoipTraffic",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+        LogComponentEnable(
+            "PacketSink",
+            (LogLevel)(LOG_PREFIX_FUNC | LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_LEVEL_INFO));
+    }
+
+    NodeContainer wifiNodes;
+    wifiNodes.Create(2); // Create 2 nodes: one will be AP, one will be STA
+
+    Ptr<Node> apNode = wifiNodes.Get(0);
+    Ptr<Node> staNode = wifiNodes.Get(1);
+
+    YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
+    YansWifiPhyHelper phy;
+    phy.SetChannel(channel.Create());
+
+    WifiMacHelper mac;
+    WifiHelper wifi;
+    wifi.SetStandard(WIFI_STANDARD_80211ax);
+
+    NetDeviceContainer wifiDevices;
+
+    // Configure AP
+    Ssid ssid = Ssid("voip-network");
+    mac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid));
+    auto apDevices = wifi.Install(phy, mac, apNode);
+    wifiDevices.Add(apDevices);
+
+    // Configure STA
+    mac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid));
+    auto staDevices = wifi.Install(phy, mac, staNode);
+    wifiDevices.Add(staDevices);
+
+    MobilityHelper mobility;
+    mobility.SetPositionAllocator("ns3::GridPositionAllocator",
+                                  "MinX",
+                                  DoubleValue(0.0),
+                                  "MinY",
+                                  DoubleValue(0.0),
+                                  "DeltaX",
+                                  DoubleValue(5.0),
+                                  "DeltaY",
+                                  DoubleValue(5.0),
+                                  "GridWidth",
+                                  UintegerValue(2),
+                                  "LayoutType",
+                                  StringValue("RowFirst"));
+    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
+    mobility.Install(wifiNodes);
+
+    InternetStackHelper internet;
+    internet.Install(wifiNodes);
+
+    Ipv4AddressHelper ipv4;
+    Ipv4InterfaceContainer ipv4Interfaces;
+    Ipv6AddressHelper ipv6;
+    Ipv6InterfaceContainer ipv6Interfaces;
+
+    if (socketType == "ipv4")
+    {
+        // Assign IPv4 addresses (needed even for IPv6-only test for basic network function)
+        ipv4.SetBase("10.1.1.0", "255.255.255.0");
+        ipv4Interfaces = ipv4.Assign(wifiDevices);
+        NS_LOG_INFO("AP address:  10.1.1.1");
+        NS_LOG_INFO("STA address: 10.1.1.2");
+    }
+    else if (socketType == "ipv6")
+    {
+        // Assign IPv6 addresses
+        ipv6.SetBase("2001:db8::", Ipv6Prefix(64));
+        ipv6Interfaces = ipv6.Assign(wifiDevices);
+        NS_LOG_INFO("AP address:  2001:db8::1");
+        NS_LOG_INFO("STA address: 2001:db8::2");
+    }
+
+    std::string protocol;
+    std::string remoteAddress;
+    uint16_t remotePort = 5000;
+
+    if (socketType == "ipv4")
+    {
+        protocol = "ns3::UdpSocketFactory";
+        remoteAddress = "10.1.1.1"; // AP's IPv4 address
+    }
+    else if (socketType == "ipv6")
+    {
+        protocol = "ns3::UdpSocketFactory";
+        remoteAddress = "2001:db8::1"; // AP's IPv6 address
+    }
+    else if (socketType == "packet")
+    {
+        protocol = "ns3::PacketSocketFactory";
+    }
+    else
+    {
+        NS_FATAL_ERROR("Invalid socket type: " << socketType);
+    }
+
+    ApplicationHelper sourceHelper(TgaxVoipTraffic::GetTypeId());
+
+    // Configure VoIP parameters
+    // These are the IEEE 802.11ax TGAX defaults:
+    // - Active state: 33-byte payload at 20ms intervals
+    // - Silence state: 7-byte payload at 160ms intervals
+    // - State transitions: 2-state Markov model with 50% voice activity
+    // - Jitter: Laplacian distribution (downlink only)
+
+    // For IPv4/UDP, add 3 bytes for compressed protocol header to payload sizes
+    // For IPv6/UDP, add 5 bytes for compressed protocol header to payload sizes
+    // For PacketSocket, use default sizes
+    uint32_t activePayloadSize = 33;
+    uint32_t silencePayloadSize = 7;
+
+    if (socketType == "ipv4")
+    {
+        // IPv4 header compression adds 3 bytes
+        activePayloadSize += 3;
+        silencePayloadSize += 3;
+    }
+    else if (socketType == "ipv6")
+    {
+        // IPv6 header compression adds 5 bytes
+        activePayloadSize += 5;
+        silencePayloadSize += 5;
+    }
+
+    sourceHelper.SetAttribute("Protocol", StringValue(protocol));
+    sourceHelper.SetAttribute("ActivePacketPayloadSize", UintegerValue(activePayloadSize));
+    sourceHelper.SetAttribute("SilencePacketPayloadSize", UintegerValue(silencePayloadSize));
+
+    // Optional: Configure VoIP state machine parameters
+    // Uncomment to customize:
+    // sourceHelper.SetAttribute("MeanActiveStateDuration", TimeValue(MilliSeconds(1250)));
+    // sourceHelper.SetAttribute("MeanInactiveStateDuration", TimeValue(MilliSeconds(1250)));
+    // sourceHelper.SetAttribute("VoiceToSilenceProbability", DoubleValue(0.016));
+    // sourceHelper.SetAttribute("SilenceToVoiceProbability", DoubleValue(0.016));
+
+    // Set remote address and port based on socket type
+    Address remoteAddr;
+    if (socketType == "ipv4")
+    {
+        remoteAddr = InetSocketAddress(Ipv4Address(remoteAddress.c_str()), remotePort);
+    }
+    else if (socketType == "ipv6")
+    {
+        remoteAddr = Inet6SocketAddress(Ipv6Address(remoteAddress.c_str()), remotePort);
+    }
+    else // PacketSocket
+    {
+        PacketSocketAddress socketAddr;
+        socketAddr.SetSingleDevice(staDevices.Get(0)->GetIfIndex());
+        socketAddr.SetPhysicalAddress(apDevices.Get(0)->GetAddress());
+        socketAddr.SetProtocol(1);
+        remoteAddr = socketAddr;
+    }
+
+    sourceHelper.SetAttribute("Remote", AddressValue(remoteAddr));
+
+    // Install VoIP source on STA node
+    auto sourceApps = sourceHelper.Install(staNode);
+    sourceApps.Start(Seconds(1.0));
+    sourceApps.Stop(Seconds(1.0) + duration);
+
+    Address sinkAddr;
+    if (socketType == "ipv4")
+    {
+        sinkAddr = InetSocketAddress(Ipv4Address::GetAny(), remotePort);
+    }
+    else if (socketType == "ipv6")
+    {
+        sinkAddr = Inet6SocketAddress(Ipv6Address::GetAny(), remotePort);
+    }
+    else // PacketSocket
+    {
+        PacketSocketAddress socketAddr;
+        socketAddr.SetSingleDevice(apDevices.Get(0)->GetIfIndex());
+        socketAddr.SetPhysicalAddress(apDevices.Get(0)->GetAddress());
+        socketAddr.SetProtocol(1);
+        sinkAddr = socketAddr;
+    }
+
+    PacketSinkHelper sinkHelper(protocol, sinkAddr);
+    auto sinkApps = sinkHelper.Install(apNode);
+    sinkApps.Start(Seconds(0.0));
+    sinkApps.Stop(Seconds(2.0) + duration);
+
+    NS_LOG_INFO("PacketSink application installed on AP (Node 0)");
+
+    // Connect to VoIP TX with jitter trace
+    Config::ConnectWithoutContext(
+        "/NodeList/*/ApplicationList/*/$ns3::TgaxVoipTraffic/TxWithJitter",
+        MakeCallback(&VoipPacketSent));
+
+    // Connect to VoIP state change trace
+    Config::ConnectWithoutContext("/NodeList/*/ApplicationList/*/$ns3::TgaxVoipTraffic/StateUpdate",
+                                  MakeCallback(&VoipStateChanged));
+
+    // Connect to RX trace
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&PacketReceived));
+
+    NS_LOG_INFO("Starting simulation for traffic duration: " << duration.As(Time::S));
+    NS_LOG_INFO("Socket type: " << socketType);
+    NS_LOG_INFO("Active payload size: " << activePayloadSize << " bytes");
+    NS_LOG_INFO("Silence payload size: " << silencePayloadSize << " bytes");
+
+    Simulator::Stop(Seconds(2) + duration);
+    Simulator::Run();
+
+    // Get the packet sink application to retrieve statistics
+    Ptr<PacketSink> sink = DynamicCast<PacketSink>(sinkApps.Get(0));
+    if (sink)
+    {
+        uint32_t totalBytes = sink->GetTotalRx();
+        NS_LOG_INFO("Total bytes received: " << totalBytes);
+
+        // Calculate approximate statistics for default configuration
+        // Active packets: ~36 bytes every 20ms = 1800 bytes per second (1 second of talking)
+        // Silence packets: ~10 bytes every 160ms = 62.5 bytes per second (1 second of silence)
+        // With 50% voice activity: average = (1800 + 62.5) / 2 = ~931 bytes per second
+        // For default 10 seconds of simulation, this is 9310 bytes.  Users can observe
+        // variation of the actual bytes sent by varying the RngRun parameter to use
+        // different random variable run numbers.  Because the duration of this test is short
+        // by default (10 seconds), the difference between actual and expected will vary but
+        // will gradually converge as traffic duration time is increased.
+        // Note that if you change the default configuration of the model, the below estimate
+        // will not update accordingly.
+        NS_LOG_INFO("Approximate expected bytes (50% activity): " << 931 * duration.GetSeconds());
+    }
+
+    Simulator::Destroy();
+
+    return 0;
+}
+
+void
+VoipPacketSent(Ptr<const Packet> packet, Time jitter)
+{
+    NS_LOG_INFO("VoIP TX: " << " Packet size (bytes): " << packet->GetSize()
+                            << " Jitter: " << jitter.As(Time::US));
+}
+
+void
+VoipStateChanged(TgaxVoipTraffic::VoiceActivityState state, Time duration)
+{
+    std::string stateStr = (state == TgaxVoipTraffic::VoiceActivityState::ACTIVE_TALKING)
+                               ? "ACTIVE_TALKING"
+                               : "INACTIVE_SILENCE";
+    NS_LOG_INFO("VoIP State: " << stateStr << " Duration: " << duration.As(Time::MS));
+}
+
+void
+PacketReceived(std::string context, Ptr<const Packet> packet, const Address& address)
+{
+    NS_LOG_INFO("Packet RX: Size(bytes): " << packet->GetSize());
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/application-packet-probe.cc ns-3.47/src/applications/model/application-packet-probe.cc
--- ns-3.46/src/applications/model/application-packet-probe.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/application-packet-probe.cc	2026-02-16 16:08:49.570229800 -0800
@@ -47,7 +47,6 @@
 ApplicationPacketProbe::ApplicationPacketProbe()
 {
     NS_LOG_FUNCTION(this);
-    m_packet = nullptr;
 }
 
 ApplicationPacketProbe::~ApplicationPacketProbe()
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/bulk-send-application.cc ns-3.47/src/applications/model/bulk-send-application.cc
--- ns-3.46/src/applications/model/bulk-send-application.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/bulk-send-application.cc	2026-02-16 16:08:49.570229800 -0800
@@ -52,17 +52,13 @@
             .AddAttribute("Protocol",
                           "The type of protocol to use.",
                           TypeIdValue(TcpSocketFactory::GetTypeId()),
-                          MakeTypeIdAccessor(&BulkSendApplication::m_tid),
+                          MakeTypeIdAccessor(&BulkSendApplication::m_protocolTid),
                           MakeTypeIdChecker())
             .AddAttribute("EnableSeqTsSizeHeader",
                           "Add SeqTsSizeHeader to each packet",
                           BooleanValue(false),
                           MakeBooleanAccessor(&BulkSendApplication::m_enableSeqTsSizeHeader),
                           MakeBooleanChecker())
-            .AddTraceSource("Tx",
-                            "A new packet is sent",
-                            MakeTraceSourceAccessor(&BulkSendApplication::m_txTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource("TxWithSeqTsSize",
                             "A new packet is created with SeqTsSizeHeader",
                             MakeTraceSourceAccessor(&BulkSendApplication::m_txTraceWithSeqTsSize),
@@ -76,10 +72,6 @@
 }
 
 BulkSendApplication::BulkSendApplication()
-    : m_socket(nullptr),
-      m_connected(false),
-      m_totBytes(0),
-      m_unsentPacket(nullptr)
 {
     NS_LOG_FUNCTION(this);
 }
@@ -96,114 +88,46 @@
     m_maxBytes = maxBytes;
 }
 
-Ptr<Socket>
-BulkSendApplication::GetSocket() const
-{
-    NS_LOG_FUNCTION(this);
-    return m_socket;
-}
-
 void
-BulkSendApplication::DoDispose()
+BulkSendApplication::CancelEvents()
 {
     NS_LOG_FUNCTION(this);
-
-    m_socket = nullptr;
     m_unsentPacket = nullptr;
-    // chain up
-    Application::DoDispose();
 }
 
 // Application Methods
 void
-BulkSendApplication::StartApplication() // Called at time specified by Start
+BulkSendApplication::DoStartApplication() // Called at time specified by Start
 {
     NS_LOG_FUNCTION(this);
-    Address from;
 
-    // Create the socket if not already
-    if (!m_socket)
+    // Fatal error if socket type is not NS3_SOCK_STREAM or NS3_SOCK_SEQPACKET
+    if (m_socket->GetSocketType() != Socket::NS3_SOCK_STREAM &&
+        m_socket->GetSocketType() != Socket::NS3_SOCK_SEQPACKET)
     {
-        m_socket = Socket::CreateSocket(GetNode(), m_tid);
-        int ret = -1;
-
-        // Fatal error if socket type is not NS3_SOCK_STREAM or NS3_SOCK_SEQPACKET
-        if (m_socket->GetSocketType() != Socket::NS3_SOCK_STREAM &&
-            m_socket->GetSocketType() != Socket::NS3_SOCK_SEQPACKET)
-        {
-            NS_FATAL_ERROR("Using BulkSend with an incompatible socket type. "
-                           "BulkSend requires SOCK_STREAM or SOCK_SEQPACKET. "
-                           "In other words, use TCP instead of UDP.");
-        }
-
-        NS_ABORT_MSG_IF(m_peer.IsInvalid(), "'Remote' attribute not properly set");
-
-        if (!m_local.IsInvalid())
-        {
-            NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
-                             InetSocketAddress::IsMatchingType(m_local)) ||
-                                (InetSocketAddress::IsMatchingType(m_peer) &&
-                                 Inet6SocketAddress::IsMatchingType(m_local)),
-                            "Incompatible peer and local address IP version");
-            ret = m_socket->Bind(m_local);
-        }
-        else
-        {
-            if (Inet6SocketAddress::IsMatchingType(m_peer))
-            {
-                ret = m_socket->Bind6();
-            }
-            else if (InetSocketAddress::IsMatchingType(m_peer))
-            {
-                ret = m_socket->Bind();
-            }
-        }
+        NS_FATAL_ERROR("Using BulkSend with an incompatible socket type. "
+                       "BulkSend requires SOCK_STREAM or SOCK_SEQPACKET. "
+                       "In other words, use TCP instead of UDP.");
+    }
 
-        if (ret == -1)
-        {
-            NS_FATAL_ERROR("Failed to bind socket");
-        }
+    m_socket->ShutdownRecv();
+    m_socket->SetSendCallback(MakeCallback(&BulkSendApplication::DataSend, this));
 
-        if (InetSocketAddress::IsMatchingType(m_peer))
-        {
-            m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-        }
-        m_socket->Connect(m_peer);
-        m_socket->ShutdownRecv();
-        m_socket->SetConnectCallback(MakeCallback(&BulkSendApplication::ConnectionSucceeded, this),
-                                     MakeCallback(&BulkSendApplication::ConnectionFailed, this));
-        m_socket->SetSendCallback(MakeCallback(&BulkSendApplication::DataSend, this));
-        Ptr<TcpSocketBase> tcpSocket = DynamicCast<TcpSocketBase>(m_socket);
-        if (tcpSocket)
-        {
-            tcpSocket->TraceConnectWithoutContext(
-                "Retransmission",
-                MakeCallback(&BulkSendApplication::PacketRetransmitted, this));
-        }
+    if (auto tcpSocket = DynamicCast<TcpSocketBase>(m_socket))
+    {
+        tcpSocket->TraceConnectWithoutContext(
+            "Retransmission",
+            MakeCallback(&BulkSendApplication::PacketRetransmitted, this));
     }
+
     if (m_connected)
     {
+        Address from;
         m_socket->GetSockName(from);
         SendData(from, m_peer);
     }
 }
 
-void
-BulkSendApplication::StopApplication() // Called at time specified by Stop
-{
-    NS_LOG_FUNCTION(this);
-
-    if (m_socket)
-    {
-        m_socket->Close();
-        m_connected = false;
-    }
-    else
-    {
-        NS_LOG_WARN("BulkSendApplication found null socket to close in StopApplication");
-    }
-}
-
 // Private helpers
 
 void
@@ -292,11 +216,10 @@
 }
 
 void
-BulkSendApplication::ConnectionSucceeded(Ptr<Socket> socket)
+BulkSendApplication::DoConnectionSucceeded(Ptr<Socket> socket)
 {
     NS_LOG_FUNCTION(this << socket);
     NS_LOG_LOGIC("BulkSendApplication Connection succeeded");
-    m_connected = true;
     Address from;
     Address to;
     socket->GetSockName(from);
@@ -305,13 +228,6 @@
 }
 
 void
-BulkSendApplication::ConnectionFailed(Ptr<Socket> socket)
-{
-    NS_LOG_FUNCTION(this << socket);
-    NS_LOG_LOGIC("BulkSendApplication, Connection Failed");
-}
-
-void
 BulkSendApplication::DataSend(Ptr<Socket> socket, uint32_t)
 {
     NS_LOG_FUNCTION(this);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/bulk-send-application.h ns-3.47/src/applications/model/bulk-send-application.h
--- ns-3.46/src/applications/model/bulk-send-application.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/bulk-send-application.h	2026-02-16 16:08:49.570229800 -0800
@@ -19,7 +19,6 @@
 namespace ns3
 {
 
-class Socket;
 class TcpHeader;
 class TcpSocketBase;
 
@@ -89,18 +88,10 @@
      */
     void SetMaxBytes(uint64_t maxBytes);
 
-    /**
-     * @brief Get the socket this application is attached to.
-     * @return pointer to associated socket
-     */
-    Ptr<Socket> GetSocket() const;
-
-  protected:
-    void DoDispose() override;
-
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void CancelEvents() override;
 
     /**
      * @brief Send data until the L4 transmission buffer is full.
@@ -109,19 +100,13 @@
      */
     void SendData(const Address& from, const Address& to);
 
-    Ptr<Socket> m_socket;                //!< Associated socket
-    bool m_connected;                    //!< True if connected
     uint32_t m_sendSize;                 //!< Size of data to send each time
     uint64_t m_maxBytes;                 //!< Limit total number of bytes sent
-    uint64_t m_totBytes;                 //!< Total bytes sent so far
-    TypeId m_tid;                        //!< The type of protocol to use.
+    uint64_t m_totBytes{0};              //!< Total bytes sent so far
     uint32_t m_seq{0};                   //!< Sequence
     Ptr<Packet> m_unsentPacket;          //!< Variable to cache unsent packet
     bool m_enableSeqTsSizeHeader{false}; //!< Enable or disable the SeqTsSizeHeader
 
-    /// Traced Callback: sent packets
-    TracedCallback<Ptr<const Packet>> m_txTrace;
-
     /// Traced Callback: retransmitted packets
     TracedCallback<Ptr<const Packet>,
                    const TcpHeader&,
@@ -135,17 +120,6 @@
     TracedCallback<Ptr<const Packet>, const Address&, const Address&, const SeqTsSizeHeader&>
         m_txTraceWithSeqTsSize;
 
-  private:
-    /**
-     * @brief Connection Succeeded (called by Socket through a callback)
-     * @param socket the connected socket
-     */
-    void ConnectionSucceeded(Ptr<Socket> socket);
-    /**
-     * @brief Connection Failed (called by Socket through a callback)
-     * @param socket the connected socket
-     */
-    void ConnectionFailed(Ptr<Socket> socket);
     /**
      * @brief Send more data as soon as some has been transmitted.
      *
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/onoff-application.cc ns-3.47/src/applications/model/onoff-application.cc
--- ns-3.46/src/applications/model/onoff-application.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/onoff-application.cc	2026-02-16 16:08:49.570229800 -0800
@@ -77,7 +77,7 @@
                           "The type of protocol to use. This should be "
                           "a subclass of ns3::SocketFactory",
                           TypeIdValue(UdpSocketFactory::GetTypeId()),
-                          MakeTypeIdAccessor(&OnOffApplication::m_tid),
+                          MakeTypeIdAccessor(&OnOffApplication::m_protocolTid),
                           // This should check for SocketFactory as a parent
                           MakeTypeIdChecker())
             .AddAttribute("EnableSeqTsSizeHeader",
@@ -85,10 +85,6 @@
                           BooleanValue(false),
                           MakeBooleanAccessor(&OnOffApplication::m_enableSeqTsSizeHeader),
                           MakeBooleanChecker())
-            .AddTraceSource("Tx",
-                            "A new packet is created and is sent",
-                            MakeTraceSourceAccessor(&OnOffApplication::m_txTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource("TxWithAddresses",
                             "A new packet is created and is sent",
                             MakeTraceSourceAccessor(&OnOffApplication::m_txTraceWithAddresses),
@@ -105,12 +101,6 @@
 }
 
 OnOffApplication::OnOffApplication()
-    : m_socket(nullptr),
-      m_connected(false),
-      m_residualBits(0),
-      m_lastStartTime(),
-      m_totBytes(0),
-      m_unsentPacket(nullptr)
 {
     NS_LOG_FUNCTION(this);
 }
@@ -127,13 +117,6 @@
     m_maxBytes = maxBytes;
 }
 
-Ptr<Socket>
-OnOffApplication::GetSocket() const
-{
-    NS_LOG_FUNCTION(this);
-    return m_socket;
-}
-
 int64_t
 OnOffApplication::AssignStreams(int64_t stream)
 {
@@ -141,79 +124,21 @@
     auto currentStream = stream;
     m_onTime->SetStream(currentStream++);
     m_offTime->SetStream(currentStream++);
-    currentStream += Application::AssignStreams(currentStream);
+    currentStream += SourceApplication::AssignStreams(currentStream);
     return (currentStream - stream);
 }
 
-void
-OnOffApplication::DoDispose()
-{
-    NS_LOG_FUNCTION(this);
-
-    CancelEvents();
-    m_socket = nullptr;
-    m_unsentPacket = nullptr;
-    // chain up
-    Application::DoDispose();
-}
-
 // Application Methods
 void
-OnOffApplication::StartApplication() // Called at time specified by Start
+OnOffApplication::DoStartApplication() // Called at time specified by Start
 {
     NS_LOG_FUNCTION(this);
 
-    // Create the socket if not already
-    if (!m_socket)
-    {
-        m_socket = Socket::CreateSocket(GetNode(), m_tid);
-        int ret = -1;
-
-        NS_ABORT_MSG_IF(m_peer.IsInvalid(), "'Remote' attribute not properly set");
-
-        if (!m_local.IsInvalid())
-        {
-            NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
-                             InetSocketAddress::IsMatchingType(m_local)) ||
-                                (InetSocketAddress::IsMatchingType(m_peer) &&
-                                 Inet6SocketAddress::IsMatchingType(m_local)),
-                            "Incompatible peer and local address IP version");
-            ret = m_socket->Bind(m_local);
-        }
-        else
-        {
-            if (Inet6SocketAddress::IsMatchingType(m_peer))
-            {
-                ret = m_socket->Bind6();
-            }
-            else if (InetSocketAddress::IsMatchingType(m_peer) ||
-                     PacketSocketAddress::IsMatchingType(m_peer))
-            {
-                ret = m_socket->Bind();
-            }
-        }
-
-        if (ret == -1)
-        {
-            NS_FATAL_ERROR("Failed to bind socket");
-        }
-
-        m_socket->SetConnectCallback(MakeCallback(&OnOffApplication::ConnectionSucceeded, this),
-                                     MakeCallback(&OnOffApplication::ConnectionFailed, this));
+    m_socket->SetAllowBroadcast(true);
+    m_socket->ShutdownRecv();
 
-        if (InetSocketAddress::IsMatchingType(m_peer))
-        {
-            m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-        }
-        m_socket->Connect(m_peer);
-        m_socket->SetAllowBroadcast(true);
-        m_socket->ShutdownRecv();
-    }
     m_cbrRateFailSafe = m_cbrRate;
 
-    // Ensure no pending event
-    CancelEvents();
-
     // If we are not yet connected, there is nothing to do here,
     // the ConnectionComplete upcall will start timers at that time.
     // If we are already connected, CancelEvents did remove the events,
@@ -225,22 +150,6 @@
 }
 
 void
-OnOffApplication::StopApplication() // Called at time specified by Stop
-{
-    NS_LOG_FUNCTION(this);
-
-    CancelEvents();
-    if (m_socket)
-    {
-        m_socket->Close();
-    }
-    else
-    {
-        NS_LOG_WARN("OnOffApplication found null socket to close in StopApplication");
-    }
-}
-
-void
 OnOffApplication::CancelEvents()
 {
     NS_LOG_FUNCTION(this);
@@ -302,8 +211,10 @@
         m_sendEvent = Simulator::Schedule(nextTime, &OnOffApplication::SendPacket, this);
     }
     else
-    { // All done, cancel any pending events
-        StopApplication();
+    {
+        // All done, cancel any pending events and close connection
+        CancelEvents();
+        CloseSocket();
     }
 }
 
@@ -398,19 +309,10 @@
 }
 
 void
-OnOffApplication::ConnectionSucceeded(Ptr<Socket> socket)
+OnOffApplication::DoConnectionSucceeded(Ptr<Socket> socket)
 {
     NS_LOG_FUNCTION(this << socket);
-
     ScheduleStartEvent();
-    m_connected = true;
-}
-
-void
-OnOffApplication::ConnectionFailed(Ptr<Socket> socket)
-{
-    NS_LOG_FUNCTION(this << socket);
-    NS_FATAL_ERROR("Can't connect");
 }
 
 } // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/onoff-application.h ns-3.47/src/applications/model/onoff-application.h
--- ns-3.46/src/applications/model/onoff-application.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/onoff-application.h	2026-02-16 16:08:49.570229800 -0800
@@ -26,7 +26,6 @@
 {
 
 class RandomVariableStream;
-class Socket;
 
 /**
  * @ingroup applications
@@ -103,26 +102,12 @@
      */
     void SetMaxBytes(uint64_t maxBytes);
 
-    /**
-     * @brief Return a pointer to associated socket.
-     * @return pointer to associated socket
-     */
-    Ptr<Socket> GetSocket() const;
-
     int64_t AssignStreams(int64_t stream) override;
 
-  protected:
-    void DoDispose() override;
-
   private:
-    void StartApplication() override;
-    void StopApplication() override;
-
-    // helpers
-    /**
-     * @brief Cancel all pending events.
-     */
-    void CancelEvents();
+    void DoStartApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void CancelEvents() override;
 
     // Event handlers
     /**
@@ -137,28 +122,34 @@
      * @brief Send a packet
      */
     void SendPacket();
+    /**
+     * @brief Schedule the next packet transmission
+     */
+    void ScheduleNextTx();
+    /**
+     * @brief Schedule the next On period start
+     */
+    void ScheduleStartEvent();
+    /**
+     * @brief Schedule the next Off period start
+     */
+    void ScheduleStopEvent();
 
-    Ptr<Socket> m_socket;                //!< Associated socket
-    bool m_connected;                    //!< True if connected
     Ptr<RandomVariableStream> m_onTime;  //!< rng for On Time
     Ptr<RandomVariableStream> m_offTime; //!< rng for Off Time
     DataRate m_cbrRate;                  //!< Rate that data is generated
     DataRate m_cbrRateFailSafe;          //!< Rate that data is generated (check copy)
     uint32_t m_pktSize;                  //!< Size of packets
-    uint32_t m_residualBits;             //!< Number of generated, but not sent, bits
+    uint32_t m_residualBits{0};          //!< Number of generated, but not sent, bits
     Time m_lastStartTime;                //!< Time last packet sent
     uint64_t m_maxBytes;                 //!< Limit total number of bytes sent
-    uint64_t m_totBytes;                 //!< Total bytes sent so far
+    uint64_t m_totBytes{0};              //!< Total bytes sent so far
     EventId m_startStopEvent;            //!< Event id for next start or stop event
     EventId m_sendEvent;                 //!< Event id of pending "send packet" event
-    TypeId m_tid;                        //!< Type of the socket used
     uint32_t m_seq{0};                   //!< Sequence
     Ptr<Packet> m_unsentPacket;          //!< Unsent packet cached for future attempt
     bool m_enableSeqTsSizeHeader{false}; //!< Enable or disable the use of SeqTsSizeHeader
 
-    /// Traced Callback: transmitted packets.
-    TracedCallback<Ptr<const Packet>> m_txTrace;
-
     /// Callbacks for tracing the packet Tx events, includes source and destination addresses
     TracedCallback<Ptr<const Packet>, const Address&, const Address&> m_txTraceWithAddresses;
 
@@ -167,30 +158,6 @@
     TracedCallback<Ptr<const Packet>, const Address&, const Address&, const SeqTsSizeHeader&>
         m_txTraceWithSeqTsSize;
 
-  private:
-    /**
-     * @brief Schedule the next packet transmission
-     */
-    void ScheduleNextTx();
-    /**
-     * @brief Schedule the next On period start
-     */
-    void ScheduleStartEvent();
-    /**
-     * @brief Schedule the next Off period start
-     */
-    void ScheduleStopEvent();
-    /**
-     * @brief Handle a Connection Succeed event
-     * @param socket the connected socket
-     */
-    void ConnectionSucceeded(Ptr<Socket> socket);
-    /**
-     * @brief Handle a Connection Failed event
-     * @param socket the not connected socket
-     */
-    void ConnectionFailed(Ptr<Socket> socket);
-
     TracedValue<bool> m_state; //!< State of application (0-OFF, 1-ON)
 };
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/packet-loss-counter.cc ns-3.47/src/applications/model/packet-loss-counter.cc
--- ns-3.46/src/applications/model/packet-loss-counter.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/packet-loss-counter.cc	2026-02-16 16:08:49.570229800 -0800
@@ -19,10 +19,6 @@
 NS_LOG_COMPONENT_DEFINE("PacketLossCounter");
 
 PacketLossCounter::PacketLossCounter(uint8_t bitmapSize)
-    : m_lost(0),
-      m_bitMapSize(0),
-      m_lastMaxSeqNum(0),
-      m_receiveBitMap(nullptr)
 {
     NS_LOG_FUNCTION(this << bitmapSize);
     SetBitMapSize(bitmapSize);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/packet-loss-counter.h ns-3.47/src/applications/model/packet-loss-counter.h
--- ns-3.46/src/applications/model/packet-loss-counter.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/packet-loss-counter.h	2026-02-16 16:08:49.570229800 -0800
@@ -76,10 +76,10 @@
      */
     void SetBit(uint32_t seqNum, bool val);
 
-    uint32_t m_lost;          //!< Lost packets counter.
-    uint16_t m_bitMapSize;    //!< Window size.
-    uint32_t m_lastMaxSeqNum; //!< Last sequence number seen.
-    uint8_t* m_receiveBitMap; //!< Received packets in the window size.
+    uint32_t m_lost{0};                //!< Lost packets counter.
+    uint16_t m_bitMapSize{0};          //!< Window size.
+    uint32_t m_lastMaxSeqNum{0};       //!< Last sequence number seen.
+    uint8_t* m_receiveBitMap{nullptr}; //!< Received packets in the window size.
 };
 } // namespace ns3
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/packet-sink.cc ns-3.47/src/applications/model/packet-sink.cc
--- ns-3.46/src/applications/model/packet-sink.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/packet-sink.cc	2026-02-16 16:08:49.570229800 -0800
@@ -42,17 +42,13 @@
             .AddAttribute("Protocol",
                           "The type id of the protocol to use for the rx socket.",
                           TypeIdValue(UdpSocketFactory::GetTypeId()),
-                          MakeTypeIdAccessor(&PacketSink::m_tid),
+                          MakeTypeIdAccessor(&PacketSink::m_protocolTid),
                           MakeTypeIdChecker())
             .AddAttribute("EnableSeqTsSizeHeader",
                           "Enable optional header tracing of SeqTsSizeHeader",
                           BooleanValue(false),
                           MakeBooleanAccessor(&PacketSink::m_enableSeqTsSizeHeader),
                           MakeBooleanChecker())
-            .AddTraceSource("Rx",
-                            "A packet has been received",
-                            MakeTraceSourceAccessor(&PacketSink::m_rxTrace),
-                            "ns3::Packet::AddressTracedCallback")
             .AddTraceSource("RxWithAddresses",
                             "A packet has been received",
                             MakeTraceSourceAccessor(&PacketSink::m_rxTraceWithAddresses),
@@ -65,12 +61,6 @@
 }
 
 PacketSink::PacketSink()
-    : m_buffer{},
-      m_socket{nullptr},
-      m_socket6{nullptr},
-      m_socketList{},
-      m_totalRx{0},
-      m_enableSeqTsSizeHeader{false}
 {
     NS_LOG_FUNCTION(this);
 }
@@ -105,74 +95,63 @@
 PacketSink::DoDispose()
 {
     NS_LOG_FUNCTION(this);
-    m_socket = nullptr;
     m_socketList.clear();
 
     // chain up
-    Application::DoDispose();
+    SinkApplication::DoDispose();
 }
 
 // Application Methods
 void
-PacketSink::StartApplication() // Called at time specified by Start
+PacketSink::DoStartApplication() // Called at time specified by Start
 {
     NS_LOG_FUNCTION(this);
 
-    // Create the socket if not already
-    if (!m_socket)
+    auto local = m_local;
+    if (local.IsInvalid())
     {
-        m_socket = Socket::CreateSocket(GetNode(), m_tid);
-        auto local = m_local;
-        if (local.IsInvalid())
-        {
-            local = InetSocketAddress(Ipv4Address::GetAny(), m_port);
-            NS_LOG_INFO(this << " Binding on port " << m_port << " / " << local << ".");
-        }
-        else if (InetSocketAddress::IsMatchingType(local))
-        {
-            m_port = InetSocketAddress::ConvertFrom(local).GetPort();
-            const auto ipv4 = InetSocketAddress::ConvertFrom(local).GetIpv4();
-            NS_LOG_INFO(this << " Binding on " << ipv4 << " port " << m_port << " / " << local
-                             << ".");
-        }
-        else if (Inet6SocketAddress::IsMatchingType(local))
-        {
-            m_port = Inet6SocketAddress::ConvertFrom(local).GetPort();
-            const auto ipv6 = Inet6SocketAddress::ConvertFrom(local).GetIpv6();
-            NS_LOG_INFO(this << " Binding on " << ipv6 << " port " << m_port << " / " << local
-                             << ".");
-        }
-        if (m_socket->Bind(local) == -1)
+        local = InetSocketAddress(Ipv4Address::GetAny(), m_port);
+        NS_LOG_INFO(this << " Binding on port " << m_port << " / " << local << ".");
+    }
+    else if (InetSocketAddress::IsMatchingType(local))
+    {
+        m_port = InetSocketAddress::ConvertFrom(local).GetPort();
+        const auto ipv4 = InetSocketAddress::ConvertFrom(local).GetIpv4();
+        NS_LOG_INFO(this << " Binding on " << ipv4 << " port " << m_port << " / " << local << ".");
+    }
+    else if (Inet6SocketAddress::IsMatchingType(local))
+    {
+        m_port = Inet6SocketAddress::ConvertFrom(local).GetPort();
+        const auto ipv6 = Inet6SocketAddress::ConvertFrom(local).GetIpv6();
+        NS_LOG_INFO(this << " Binding on " << ipv6 << " port " << m_port << " / " << local << ".");
+    }
+    if (m_socket->Bind(local) == -1)
+    {
+        NS_FATAL_ERROR("Failed to bind socket");
+    }
+    m_socket->Listen();
+    m_socket->ShutdownSend();
+    if (addressUtils::IsMulticast(local))
+    {
+        if (auto udpSocket = DynamicCast<UdpSocket>(m_socket))
         {
-            NS_FATAL_ERROR("Failed to bind socket");
+            // equivalent to setsockopt (MCAST_JOIN_GROUP)
+            udpSocket->MulticastJoinGroup(0, local);
         }
-        m_socket->Listen();
-        m_socket->ShutdownSend();
-        if (addressUtils::IsMulticast(local))
+        else
         {
-            if (auto udpSocket = DynamicCast<UdpSocket>(m_socket))
-            {
-                // equivalent to setsockopt (MCAST_JOIN_GROUP)
-                udpSocket->MulticastJoinGroup(0, local);
-            }
-            else
-            {
-                NS_FATAL_ERROR("Error: joining multicast on a non-UDP socket");
-            }
+            NS_FATAL_ERROR("Error: joining multicast on a non-UDP socket");
         }
-        m_socket->SetRecvCallback(MakeCallback(&PacketSink::HandleRead, this));
-        m_socket->SetRecvPktInfo(true);
-        m_socket->SetAcceptCallback(MakeNullCallback<bool, Ptr<Socket>, const Address&>(),
-                                    MakeCallback(&PacketSink::HandleAccept, this));
-        m_socket->SetCloseCallbacks(MakeCallback(&PacketSink::HandlePeerClose, this),
-                                    MakeCallback(&PacketSink::HandlePeerError, this));
     }
+    m_socket->SetRecvCallback(MakeCallback(&PacketSink::HandleRead, this));
+    m_socket->SetRecvPktInfo(true);
+    m_socket->SetAcceptCallback(MakeNullCallback<bool, Ptr<Socket>, const Address&>(),
+                                MakeCallback(&PacketSink::HandleAccept, this));
+    m_socket->SetCloseCallbacks(MakeCallback(&PacketSink::HandlePeerClose, this),
+                                MakeCallback(&PacketSink::HandlePeerError, this));
 
-    if (m_local.IsInvalid() && !m_socket6)
-    {
-        // local address is not specified, so create another socket to also listen to all IPv6
-        // addresses
-        m_socket6 = Socket::CreateSocket(GetNode(), m_tid);
+    if (m_local.IsInvalid())
+    {
         auto local = Inet6SocketAddress(Ipv6Address::GetAny(), m_port);
         if (m_socket6->Bind(local) == -1)
         {
@@ -202,7 +181,7 @@
 }
 
 void
-PacketSink::StopApplication() // Called at time specified by Stop
+PacketSink::DoStopApplication() // Called at time specified by Stop
 {
     NS_LOG_FUNCTION(this);
     while (!m_socketList.empty()) // these are accepted sockets, close them
@@ -211,16 +190,6 @@
         m_socketList.pop_front();
         acceptedSocket->Close();
     }
-    if (m_socket)
-    {
-        m_socket->Close();
-        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-    }
-    if (m_socket6)
-    {
-        m_socket6->Close();
-        m_socket6->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-    }
 }
 
 void
@@ -252,7 +221,8 @@
                                    << " total Rx " << m_totalRx << " bytes");
         }
 
-        if (!m_rxTrace.IsEmpty() || !m_rxTraceWithAddresses.IsEmpty() ||
+        if (!m_rxTrace.IsEmpty() || !m_rxTraceWithoutAddress.IsEmpty() ||
+            !m_rxTraceWithAddresses.IsEmpty() ||
             (!m_rxTraceWithSeqTsSize.IsEmpty() && m_enableSeqTsSizeHeader))
         {
             Address localAddress;
@@ -270,6 +240,7 @@
             {
                 socket->GetSockName(localAddress);
             }
+            m_rxTraceWithoutAddress(packet);
             m_rxTrace(packet, from);
             m_rxTraceWithAddresses(packet, from, localAddress);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/packet-sink.h ns-3.47/src/applications/model/packet-sink.h
--- ns-3.46/src/applications/model/packet-sink.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/packet-sink.h	2026-02-16 16:08:49.570229800 -0800
@@ -23,7 +23,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 
 /**
@@ -102,8 +101,8 @@
     void DoDispose() override;
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void DoStopApplication() override;
 
     /**
      * @brief Handle a packet received by the application
@@ -159,12 +158,12 @@
             if (InetSocketAddress::IsMatchingType(x))
             {
                 InetSocketAddress a = InetSocketAddress::ConvertFrom(x);
-                return Ipv4AddressHash()(a.GetIpv4());
+                return std::hash<Ipv4Address>{}(a.GetIpv4());
             }
             else if (Inet6SocketAddress::IsMatchingType(x))
             {
                 Inet6SocketAddress a = Inet6SocketAddress::ConvertFrom(x);
-                return Ipv6AddressHash()(a.GetIpv6());
+                return std::hash<Ipv6Address>{}(a.GetIpv6());
             }
 
             NS_ABORT_MSG("PacketSink: unexpected address type, neither IPv4 nor IPv6");
@@ -174,20 +173,14 @@
 
     std::unordered_map<Address, Ptr<Packet>, AddressHash> m_buffer; //!< Buffer for received packets
 
-    Ptr<Socket> m_socket;  //!< Socket
-    Ptr<Socket> m_socket6; //!< IPv6 Socket (used if only port is specified)
-
     // In the case of TCP, each socket accept returns a new socket, so the
     // listening socket is stored separately from the accepted sockets
     std::list<Ptr<Socket>> m_socketList; //!< the accepted sockets
 
-    uint64_t m_totalRx; //!< Total bytes received
-    TypeId m_tid;       //!< Protocol TypeId
+    uint64_t m_totalRx{0}; //!< Total bytes received
 
-    bool m_enableSeqTsSizeHeader; //!< Enable or disable the export of SeqTsSize header
+    bool m_enableSeqTsSizeHeader{false}; //!< Enable or disable the export of SeqTsSize header
 
-    /// Traced Callback: received packets, source address.
-    TracedCallback<Ptr<const Packet>, const Address&> m_rxTrace;
     /// Callback for tracing the packet Rx events, includes source and destination addresses
     TracedCallback<Ptr<const Packet>, const Address&, const Address&> m_rxTraceWithAddresses;
     /// Callbacks for tracing the packet Rx events, includes source, destination addresses, and
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/rta-tig-mobile-gaming.cc ns-3.47/src/applications/model/rta-tig-mobile-gaming.cc
--- ns-3.46/src/applications/model/rta-tig-mobile-gaming.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/rta-tig-mobile-gaming.cc	2026-02-16 16:08:49.570229800 -0800
@@ -0,0 +1,309 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "rta-tig-mobile-gaming.h"
+
+#include "ns3/double.h"
+#include "ns3/enum.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/inet6-socket-address.h"
+#include "ns3/log.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/packet.h"
+#include "ns3/pointer.h"
+#include "ns3/random-variable-stream.h"
+#include "ns3/simulator.h"
+#include "ns3/socket-factory.h"
+#include "ns3/socket.h"
+#include "ns3/string.h"
+#include "ns3/udp-socket-factory.h"
+
+#include <cmath>
+
+namespace ns3
+{
+
+NS_LOG_COMPONENT_DEFINE("RtaTigMobileGaming");
+
+NS_OBJECT_ENSURE_REGISTERED(RtaTigMobileGaming);
+
+TypeId
+RtaTigMobileGaming::GetTypeId()
+{
+    static TypeId tid =
+        TypeId("ns3::RtaTigMobileGaming")
+            .SetParent<SourceApplication>()
+            .SetGroupName("Applications")
+            .AddConstructor<RtaTigMobileGaming>()
+            .AddAttribute("ModelPresets",
+                          "The model presets to use (Custom for custom settings)",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          EnumValue(RtaTigMobileGaming::ModelPresets::CUSTOM),
+                          MakeEnumAccessor<ModelPresets>(&RtaTigMobileGaming::m_modelPresets),
+                          MakeEnumChecker(RtaTigMobileGaming::ModelPresets::CUSTOM,
+                                          "Custom",
+                                          RtaTigMobileGaming::ModelPresets::STATUS_SYNC_DL,
+                                          "StatusSyncDl",
+                                          RtaTigMobileGaming::ModelPresets::STATUS_SYNC_UL,
+                                          "StatusSyncUl",
+                                          RtaTigMobileGaming::ModelPresets::LOCKSTEP_DL,
+                                          "LockstepDl",
+                                          RtaTigMobileGaming::ModelPresets::LOCKSTEP_UL,
+                                          "LockstepUl"))
+            .AddAttribute(
+                "Protocol",
+                "The type of protocol to use. This should be a subclass of ns3::SocketFactory",
+                TypeIdValue(UdpSocketFactory::GetTypeId()),
+                MakeTypeIdAccessor(&RtaTigMobileGaming::m_protocolTid),
+                MakeTypeIdChecker())
+            .AddAttribute(
+                "CustomInitialPacketSize",
+                "A uniform random variable to generate size in bytes for initial packet payload.",
+                StringValue("ns3::UniformRandomVariable[Min=0|Max=20]"),
+                MakePointerAccessor(&RtaTigMobileGaming::m_initialSizeUniform),
+                MakePointerChecker<UniformRandomVariable>())
+            .AddAttribute(
+                "CustomEndPacketSize",
+                "A uniform random variable to generate size in bytes for end packet payload.",
+                StringValue("ns3::UniformRandomVariable[Min=500|Max=600]"),
+                MakePointerAccessor(&RtaTigMobileGaming::m_endSizeUniform),
+                MakePointerChecker<UniformRandomVariable>())
+            .AddAttribute(
+                "CustomPacketSizeLev",
+                "A largest extreme value random variable to calculate packet sizes in bytes.",
+                StringValue("ns3::LargestExtremeValueRandomVariable[Location=50|Scale=11.0]"),
+                MakePointerAccessor(&RtaTigMobileGaming::m_levSizes),
+                MakePointerChecker<LargestExtremeValueRandomVariable>())
+            .AddAttribute(
+                "CustomPacketArrivalLev",
+                "A largest extreme value random variable to calculate packet packet arrivals in "
+                "microseconds.",
+                StringValue("ns3::LargestExtremeValueRandomVariable[Location=13000|Scale=3700]"),
+                MakePointerAccessor(&RtaTigMobileGaming::m_levArrivals),
+                MakePointerChecker<LargestExtremeValueRandomVariable>())
+            .AddTraceSource("TxWithStage",
+                            "A packet is sent, this trace also reports the current stage",
+                            MakeTraceSourceAccessor(&RtaTigMobileGaming::m_txStageTrace),
+                            "ns3::RtaTigMobileGaming::TxTracedCallback");
+    return tid;
+}
+
+RtaTigMobileGaming::RtaTigMobileGaming()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+RtaTigMobileGaming::~RtaTigMobileGaming()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+int64_t
+RtaTigMobileGaming::AssignStreams(int64_t stream)
+{
+    NS_LOG_FUNCTION(this << stream);
+    auto currentStream = stream;
+    m_initialSizeUniform->SetStream(currentStream++);
+    m_endSizeUniform->SetStream(currentStream++);
+    m_levArrivals->SetStream(currentStream++);
+    m_levSizes->SetStream(currentStream++);
+    return (currentStream - stream);
+}
+
+void
+RtaTigMobileGaming::DoInitialize()
+{
+    NS_LOG_FUNCTION(this);
+    SourceApplication::DoInitialize();
+
+    if (m_modelPresets != ModelPresets::CUSTOM)
+    {
+        // Load model presets
+        switch (m_modelPresets)
+        {
+        case ModelPresets::STATUS_SYNC_DL:
+            m_initialSizeUniform->SetAttribute("Min", DoubleValue(0));
+            m_initialSizeUniform->SetAttribute("Max", DoubleValue(20));
+            m_endSizeUniform->SetAttribute("Min", DoubleValue(500));
+            m_endSizeUniform->SetAttribute("Max", DoubleValue(600));
+            m_levSizes->SetAttribute("Location", DoubleValue(50.0));
+            m_levSizes->SetAttribute("Scale", DoubleValue(11.0));
+            m_levArrivals->SetAttribute("Location", DoubleValue(13000.0));
+            m_levArrivals->SetAttribute("Scale", DoubleValue(3700.0));
+            break;
+        case ModelPresets::STATUS_SYNC_UL:
+            m_initialSizeUniform->SetAttribute("Min", DoubleValue(0));
+            m_initialSizeUniform->SetAttribute("Max", DoubleValue(20));
+            m_endSizeUniform->SetAttribute("Min", DoubleValue(400));
+            m_endSizeUniform->SetAttribute("Max", DoubleValue(550));
+            m_levSizes->SetAttribute("Location", DoubleValue(38.0));
+            m_levSizes->SetAttribute("Scale", DoubleValue(3.7));
+            m_levArrivals->SetAttribute("Location", DoubleValue(15000.0));
+            m_levArrivals->SetAttribute("Scale", DoubleValue(5700.0));
+            break;
+        case ModelPresets::LOCKSTEP_DL:
+            m_initialSizeUniform->SetAttribute("Min", DoubleValue(0));
+            m_initialSizeUniform->SetAttribute("Max", DoubleValue(80));
+            m_endSizeUniform->SetAttribute("Min", DoubleValue(1400));
+            m_endSizeUniform->SetAttribute("Max", DoubleValue(1500));
+            m_levSizes->SetAttribute("Location", DoubleValue(210.0));
+            m_levSizes->SetAttribute("Scale", DoubleValue(35.0));
+            m_levArrivals->SetAttribute("Location", DoubleValue(28000.0));
+            m_levArrivals->SetAttribute("Scale", DoubleValue(4200.0));
+            break;
+        case ModelPresets::LOCKSTEP_UL:
+            m_initialSizeUniform->SetAttribute("Min", DoubleValue(0));
+            m_initialSizeUniform->SetAttribute("Max", DoubleValue(80));
+            m_endSizeUniform->SetAttribute("Min", DoubleValue(500));
+            m_endSizeUniform->SetAttribute("Max", DoubleValue(600));
+            m_levSizes->SetAttribute("Location", DoubleValue(92.0));
+            m_levSizes->SetAttribute("Scale", DoubleValue(38.0));
+            m_levArrivals->SetAttribute("Location", DoubleValue(22000.0));
+            m_levArrivals->SetAttribute("Scale", DoubleValue(3400.0));
+            break;
+        case ModelPresets::CUSTOM:
+        default:
+            break;
+        }
+    }
+}
+
+void
+RtaTigMobileGaming::DoStartApplication()
+{
+    NS_LOG_FUNCTION(this);
+
+    m_socket->SetAllowBroadcast(true);
+    m_socket->ShutdownRecv();
+
+    m_currentStage = TrafficModelStage::INITIAL;
+
+    if (m_connected)
+    {
+        ScheduleNext();
+    }
+}
+
+void
+RtaTigMobileGaming::DoStopApplication()
+{
+    NS_LOG_FUNCTION(this);
+    m_currentStage = TrafficModelStage::ENDING;
+}
+
+void
+RtaTigMobileGaming::CancelEvents()
+{
+    NS_LOG_FUNCTION(this);
+    if (m_currentStage == TrafficModelStage::ENDING)
+    {
+        // handled once ending packet is transmitted
+        return;
+    }
+    m_txEvent.Cancel();
+}
+
+void
+RtaTigMobileGaming::ScheduleNext()
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT(!m_txEvent.IsPending());
+    const auto delay = MicroSeconds(m_levArrivals->GetValue());
+    m_txEvent = Simulator::Schedule(delay, &RtaTigMobileGaming::SendPacket, this);
+}
+
+void
+RtaTigMobileGaming::SendPacket()
+{
+    NS_LOG_FUNCTION(this);
+
+    uint32_t packetSize;
+    const auto stage{m_currentStage};
+    switch (stage)
+    {
+    case TrafficModelStage::INITIAL:
+        packetSize = m_initialSizeUniform->GetInteger();
+        break;
+    case TrafficModelStage::ENDING:
+        packetSize = m_endSizeUniform->GetInteger();
+        break;
+    case TrafficModelStage::GAMING:
+    default:
+        packetSize = std::round(m_levSizes->GetValue());
+        break;
+    }
+    auto packet = Create<Packet>(packetSize);
+
+    const auto actualSize = static_cast<uint32_t>(m_socket->Send(packet));
+    NS_ABORT_MSG_IF(actualSize != packetSize,
+                    "Sent size " << actualSize << " does not match expected size " << packetSize);
+    m_txTrace(packet);
+    m_txStageTrace(packet, stage);
+
+    if (InetSocketAddress::IsMatchingType(m_peer))
+    {
+        NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " gaming traffic source sent "
+                               << packetSize << " bytes during stage " << stage << " to "
+                               << InetSocketAddress::ConvertFrom(m_peer).GetIpv4() << " port "
+                               << InetSocketAddress::ConvertFrom(m_peer).GetPort());
+    }
+    else if (Inet6SocketAddress::IsMatchingType(m_peer))
+    {
+        NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " gaming traffic source sent "
+                               << packetSize << " bytes during stage " << stage << " to "
+                               << Inet6SocketAddress::ConvertFrom(m_peer).GetIpv6() << " port "
+                               << Inet6SocketAddress::ConvertFrom(m_peer).GetPort());
+    }
+
+    if (m_currentStage == TrafficModelStage::ENDING)
+    {
+        m_currentStage = TrafficModelStage::INITIAL;
+        CancelEvents();
+        CloseSocket();
+    }
+    else
+    {
+        if (m_currentStage == TrafficModelStage::INITIAL)
+        {
+            m_currentStage = TrafficModelStage::GAMING;
+        }
+        ScheduleNext();
+    }
+}
+
+void
+RtaTigMobileGaming::DoConnectionSucceeded(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    ScheduleNext();
+}
+
+std::ostream&
+operator<<(std::ostream& os, RtaTigMobileGaming::TrafficModelStage stage)
+{
+    switch (stage)
+    {
+    case RtaTigMobileGaming::TrafficModelStage::INITIAL:
+        os << "Initial stage";
+        break;
+    case RtaTigMobileGaming::TrafficModelStage::GAMING:
+        os << "Gaming stage";
+        break;
+    case RtaTigMobileGaming::TrafficModelStage::ENDING:
+        os << "Ending stage";
+        break;
+    default:
+        NS_FATAL_ERROR("Unknown gaming traffic model stage");
+    }
+    return os;
+}
+
+} // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/rta-tig-mobile-gaming.h ns-3.47/src/applications/model/rta-tig-mobile-gaming.h
--- ns-3.46/src/applications/model/rta-tig-mobile-gaming.h	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/rta-tig-mobile-gaming.h	2026-02-16 16:08:49.570229800 -0800
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#ifndef RTA_TIG_MOBILE_GAMING_H
+#define RTA_TIG_MOBILE_GAMING_H
+
+#include "source-application.h"
+
+#include "ns3/address.h"
+#include "ns3/event-id.h"
+#include "ns3/ptr.h"
+
+namespace ns3
+{
+
+class UniformRandomVariable;
+class LargestExtremeValueRandomVariable;
+
+/**
+ * @ingroup applications
+ * @brief Generate RT mobile gaming traffic.
+ *
+ * This RT mobile gaming traffic generator follows requirements from IEEE 802.11 Real Time
+ * Applications TIG Report (Section 4.1.4: Traffic model) (see applications documentation for
+ * full citation).
+ *
+ * RT mobile gaming traffic typically consists in small packets (between 30 and 500 Bytes) for both
+ * uplink and downlink, where usually downlink packets are bigger than uplink ones. Packets are
+ * generated on average every 30-60ms for uplink and downlink, usually downlink packet interval is
+ * larger than uplink one. The bandwidth for RT mobile gaming traffic is between 100kbps and 1Mbps.
+ */
+class RtaTigMobileGaming : public SourceApplication
+{
+  public:
+    /**
+     * @brief Get the type ID.
+     * @return the object TypeId
+     */
+    static TypeId GetTypeId();
+
+    RtaTigMobileGaming();
+    ~RtaTigMobileGaming() override;
+
+    /// Model presets enumeration
+    enum class ModelPresets : uint8_t
+    {
+        CUSTOM = 0, //< Custom traffic model (by default, load parameters of DL status-sync model
+                    // presets)
+        STATUS_SYNC_DL, //< DL status-sync model presets
+        STATUS_SYNC_UL, //< UL status-sync model presets
+        LOCKSTEP_DL,    //< DL lockstep model presets
+        LOCKSTEP_UL     //< UL lockstep model presets
+    };
+
+    int64_t AssignStreams(int64_t stream) override;
+
+    /**
+     * Traffic model stages
+     */
+    enum class TrafficModelStage : uint8_t
+    {
+        INITIAL = 0,
+        GAMING = 1,
+        ENDING = 2
+    };
+
+  protected:
+    void DoInitialize() override;
+
+  private:
+    void DoStartApplication() override;
+    void DoStopApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void CancelEvents() override;
+
+    /**
+     * Schedule the next packet transmission
+     */
+    void ScheduleNext();
+
+    /**
+     * Transmit one initial, gaming or ending packet
+     */
+    void SendPacket();
+
+    ModelPresets m_modelPresets; //!< Model presets to use to configure the traffic generator
+
+    Ptr<UniformRandomVariable>
+        m_initialSizeUniform; //!< Uniform random variable to generate the initial packet size
+    Ptr<UniformRandomVariable>
+        m_endSizeUniform; //!< Uniform random variable to generate the end packet size
+
+    Ptr<LargestExtremeValueRandomVariable>
+        m_levArrivals; //!< Largest extreme value random variable to generate packet arrival times
+    Ptr<LargestExtremeValueRandomVariable>
+        m_levSizes; //!< Largest extreme value random variable to generate packet sizes
+
+    TrafficModelStage m_currentStage{TrafficModelStage::INITIAL}; //!< Hold the current stage
+
+    EventId m_txEvent; //!< Event ID of pending TX event scheduling
+
+    /**
+     * TracedCallback signature for packet and stage.
+     *
+     * @param [in] packet The packet.
+     * @param [in] stage The gaming traffic model stage corresponding to the packet.
+     */
+    typedef void (*TxTracedCallback)(Ptr<const Packet> packet, TrafficModelStage stage);
+
+    /// Traced Callback: transmitted packets and their stage.
+    TracedCallback<Ptr<const Packet>, TrafficModelStage> m_txStageTrace;
+};
+
+/**
+ * Serialize gaming traffic model stage to ostream in a human-readable form.
+ *
+ * @param os std::ostream
+ * @param stage the gaming traffic model stage
+ * @return std::ostream
+ */
+std::ostream& operator<<(std::ostream& os, RtaTigMobileGaming::TrafficModelStage stage);
+
+} // namespace ns3
+
+#endif /* RTA_TIG_MOBILE_GAMING_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/seq-ts-echo-header.cc ns-3.47/src/applications/model/seq-ts-echo-header.cc
--- ns-3.46/src/applications/model/seq-ts-echo-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/seq-ts-echo-header.cc	2026-02-16 16:08:49.570229800 -0800
@@ -22,9 +22,7 @@
 NS_OBJECT_ENSURE_REGISTERED(SeqTsEchoHeader);
 
 SeqTsEchoHeader::SeqTsEchoHeader()
-    : m_seq(0),
-      m_tsValue(Simulator::Now()),
-      m_tsEchoReply()
+    : m_tsValue(Simulator::Now())
 {
     NS_LOG_FUNCTION(this);
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/seq-ts-echo-header.h ns-3.47/src/applications/model/seq-ts-echo-header.h
--- ns-3.46/src/applications/model/seq-ts-echo-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/seq-ts-echo-header.h	2026-02-16 16:08:49.571229700 -0800
@@ -78,7 +78,7 @@
     uint32_t Deserialize(Buffer::Iterator start) override;
 
   private:
-    uint32_t m_seq;     //!< Sequence number
+    uint32_t m_seq{0};  //!< Sequence number
     Time m_tsValue;     //!< Sender's timestamp
     Time m_tsEchoReply; //!< Receiver's timestamp
 };
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/seq-ts-header.cc ns-3.47/src/applications/model/seq-ts-header.cc
--- ns-3.46/src/applications/model/seq-ts-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/seq-ts-header.cc	2026-02-16 16:08:49.571229700 -0800
@@ -21,8 +21,7 @@
 NS_OBJECT_ENSURE_REGISTERED(SeqTsHeader);
 
 SeqTsHeader::SeqTsHeader()
-    : m_seq(0),
-      m_ts(Simulator::Now().GetTimeStep())
+    : m_ts{Simulator::Now()}
 {
     NS_LOG_FUNCTION(this);
 }
@@ -45,7 +44,7 @@
 SeqTsHeader::GetTs() const
 {
     NS_LOG_FUNCTION(this);
-    return TimeStep(m_ts);
+    return m_ts;
 }
 
 TypeId
@@ -68,7 +67,7 @@
 SeqTsHeader::Print(std::ostream& os) const
 {
     NS_LOG_FUNCTION(this << &os);
-    os << "(seq=" << m_seq << " time=" << TimeStep(m_ts).As(Time::S) << ")";
+    os << "(seq=" << m_seq << " time=" << m_ts.As(Time::S) << ")";
 }
 
 uint32_t
@@ -84,7 +83,7 @@
     NS_LOG_FUNCTION(this << &start);
     Buffer::Iterator i = start;
     i.WriteHtonU32(m_seq);
-    i.WriteHtonU64(m_ts);
+    i.WriteHtonU64(m_ts.GetTimeStep());
 }
 
 uint32_t
@@ -93,7 +92,7 @@
     NS_LOG_FUNCTION(this << &start);
     Buffer::Iterator i = start;
     m_seq = i.ReadNtohU32();
-    m_ts = i.ReadNtohU64();
+    m_ts = TimeStep(i.ReadNtohU64());
     return GetSerializedSize();
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/seq-ts-header.h ns-3.47/src/applications/model/seq-ts-header.h
--- ns-3.46/src/applications/model/seq-ts-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/seq-ts-header.h	2026-02-16 16:08:49.571229700 -0800
@@ -61,8 +61,8 @@
     uint32_t Deserialize(Buffer::Iterator start) override;
 
   private:
-    uint32_t m_seq; //!< Sequence number
-    uint64_t m_ts;  //!< Timestamp
+    uint32_t m_seq{0}; //!< Sequence number
+    Time m_ts;         //!< Timestamp
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/sink-application.cc ns-3.47/src/applications/model/sink-application.cc
--- ns-3.46/src/applications/model/sink-application.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/sink-application.cc	2026-02-16 16:08:49.571229700 -0800
@@ -9,6 +9,7 @@
 #include "sink-application.h"
 
 #include "ns3/log.h"
+#include "ns3/socket.h"
 #include "ns3/uinteger.h"
 
 namespace ns3
@@ -37,7 +38,15 @@
                 "Port on which the application listens for incoming packets.",
                 UintegerValue(INVALID_PORT),
                 MakeUintegerAccessor(&SinkApplication::SetPort, &SinkApplication::GetPort),
-                MakeUintegerChecker<uint32_t>());
+                MakeUintegerChecker<uint32_t>())
+            .AddTraceSource("Rx",
+                            "A packet has been received",
+                            MakeTraceSourceAccessor(&SinkApplication::m_rxTrace),
+                            "ns3::Packet::AddressTracedCallback")
+            .AddTraceSource("RxWithoutAddress",
+                            "A packet has been received from a given address",
+                            MakeTraceSourceAccessor(&SinkApplication::m_rxTraceWithoutAddress),
+                            "ns3::Packet::TracedCallback");
     return tid;
 }
 
@@ -53,6 +62,15 @@
 }
 
 void
+SinkApplication::DoDispose()
+{
+    NS_LOG_FUNCTION(this);
+    m_socket = nullptr;
+    m_socket6 = nullptr;
+    Application::DoDispose();
+}
+
+void
 SinkApplication::SetLocal(const Address& addr)
 {
     NS_LOG_FUNCTION(this << addr);
@@ -82,4 +100,69 @@
     return m_port;
 }
 
+void
+SinkApplication::StartApplication()
+{
+    NS_LOG_FUNCTION(this);
+
+    // note: it is currently not possible to restart an application
+
+    m_socket = Socket::CreateSocket(GetNode(), m_protocolTid);
+    if (m_local.IsInvalid() && !m_socket6)
+    {
+        // local address is not specified, so create another socket to also listen to all IPv6
+        // addresses
+        m_socket6 = Socket::CreateSocket(GetNode(), m_protocolTid);
+    }
+
+    DoStartApplication();
+}
+
+void
+SinkApplication::StopApplication()
+{
+    NS_LOG_FUNCTION(this);
+    DoStopApplication();
+    CloseAllSockets();
+}
+
+bool
+SinkApplication::CloseAllSockets()
+{
+    NS_LOG_FUNCTION(this);
+    auto ret = CloseSocket(m_socket);
+    ret = CloseSocket(m_socket6) && ret;
+    return ret;
+}
+
+bool
+SinkApplication::CloseSocket(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    if (socket)
+    {
+        const auto ret = socket->Close();
+        socket->SetAcceptCallback(MakeNullCallback<bool, Ptr<Socket>, const Address&>(),
+                                  MakeNullCallback<void, Ptr<Socket>, const Address&>());
+        socket->SetCloseCallbacks(MakeNullCallback<void, Ptr<Socket>>(),
+                                  MakeNullCallback<void, Ptr<Socket>>());
+        socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
+        socket->SetSendCallback(MakeNullCallback<void, Ptr<Socket>, uint32_t>());
+        return (ret == 0);
+    }
+    return true;
+}
+
+void
+SinkApplication::DoStartApplication()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+void
+SinkApplication::DoStopApplication()
+{
+    NS_LOG_FUNCTION(this);
+}
+
 } // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/sink-application.h ns-3.47/src/applications/model/sink-application.h
--- ns-3.46/src/applications/model/sink-application.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/sink-application.h	2026-02-16 16:08:49.571229700 -0800
@@ -11,12 +11,17 @@
 
 #include "ns3/address.h"
 #include "ns3/application.h"
+#include "ns3/traced-callback.h"
 
 #include <limits>
 
 namespace ns3
 {
 
+class Packet;
+class Address;
+class Socket;
+
 /**
  * @ingroup applications
  * @brief Base class for sink applications.
@@ -55,10 +60,32 @@
     static constexpr uint32_t INVALID_PORT{std::numeric_limits<uint32_t>::max()}; //!< invalid port
 
   protected:
+    void DoDispose() override;
+
+    /**
+     * @brief Close all the sockets
+     * @return true if all sockets closed successfully, false otherwise
+     */
+    bool CloseAllSockets();
+
+    /// Callbacks for tracing the packet Rx events
+    ns3::TracedCallback<Ptr<const Packet>> m_rxTraceWithoutAddress;
+
+    /// Traced Callback: received packets, source address.
+    TracedCallback<Ptr<const Packet>, const Address&> m_rxTrace;
+
+    Ptr<Socket> m_socket;  //!< Socket (IPv4 or IPv6, depending on local address)
+    Ptr<Socket> m_socket6; //!< IPv6 Socket (used if only port is specified)
+
+    TypeId m_protocolTid; //!< Protocol TypeId value
+
     Address m_local; //!< Local address to bind to (address and port)
     uint32_t m_port; //!< Local port to bind to
 
   private:
+    void StartApplication() override;
+    void StopApplication() override;
+
     /**
      * @brief set the local address
      * @param addr local address
@@ -82,6 +109,23 @@
      * @return the server port
      */
     uint32_t GetPort() const;
+
+    /**
+     * @brief Close the socket
+     * @param socket the socket to close
+     * @return true if the socket closed successfully, false otherwise
+     */
+    bool CloseSocket(Ptr<Socket> socket);
+
+    /**
+     * @brief Application specific startup code for child subclasses
+     */
+    virtual void DoStartApplication();
+
+    /**
+     * @brief Application specific shutdown code for child subclasses
+     */
+    virtual void DoStopApplication();
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/source-application.cc ns-3.47/src/applications/model/source-application.cc
--- ns-3.46/src/applications/model/source-application.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/source-application.cc	2026-02-16 16:08:49.571229700 -0800
@@ -8,7 +8,11 @@
 
 #include "source-application.h"
 
+#include "ns3/inet-socket-address.h"
+#include "ns3/inet6-socket-address.h"
 #include "ns3/log.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/socket.h"
 #include "ns3/uinteger.h"
 
 namespace ns3
@@ -43,12 +47,24 @@
                           "All 8 bits of the TOS byte are set (including ECN bits).",
                           UintegerValue(0),
                           MakeUintegerAccessor(&SourceApplication::m_tos),
-                          MakeUintegerChecker<uint8_t>());
+                          MakeUintegerChecker<uint8_t>())
+            .AddTraceSource("Tx",
+                            "A packet is sent",
+                            MakeTraceSourceAccessor(&SourceApplication::m_txTrace),
+                            "ns3::Packet::TracedCallback")
+            .AddTraceSource("ConnectionSucceeded",
+                            "Succeeded to establish connection",
+                            MakeTraceSourceAccessor(&SourceApplication::m_connectionSuccess),
+                            "ns3::SourceApplication::ConnectionEventCallback")
+            .AddTraceSource("ConnectionFailed",
+                            "Failed to establish connection",
+                            MakeTraceSourceAccessor(&SourceApplication::m_connectionFailure),
+                            "ns3::SourceApplication::ConnectionEventCallback");
     return tid;
 }
 
-SourceApplication::SourceApplication()
-    : m_peer{}
+SourceApplication::SourceApplication(bool allowPacketSocket)
+    : m_allowPacketSocket{allowPacketSocket}
 {
     NS_LOG_FUNCTION(this);
 }
@@ -59,6 +75,15 @@
 }
 
 void
+SourceApplication::DoDispose()
+{
+    NS_LOG_FUNCTION(this);
+    CancelEvents();
+    m_socket = nullptr;
+    Application::DoDispose();
+}
+
+void
 SourceApplication::SetRemote(const Address& addr)
 {
     NS_LOG_FUNCTION(this << addr);
@@ -74,4 +99,128 @@
     return m_peer;
 }
 
+Ptr<Socket>
+SourceApplication::GetSocket() const
+{
+    return m_socket;
+}
+
+void
+SourceApplication::StartApplication()
+{
+    NS_LOG_FUNCTION(this);
+
+    // note: it is currently not possible to restart an application
+
+    NS_ABORT_MSG_IF(m_peer.IsInvalid(), "Remote address not properly set");
+    if (!m_local.IsInvalid())
+    {
+        NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
+                         InetSocketAddress::IsMatchingType(m_local)) ||
+                            (InetSocketAddress::IsMatchingType(m_peer) &&
+                             Inet6SocketAddress::IsMatchingType(m_local)),
+                        "Incompatible peer and local address IP version");
+    }
+
+    m_socket = Socket::CreateSocket(GetNode(), m_protocolTid);
+    m_socket->SetConnectCallback(MakeCallback(&SourceApplication::ConnectionSucceeded, this),
+                                 MakeCallback(&SourceApplication::ConnectionFailed, this));
+
+    int ret{-1};
+    if (InetSocketAddress::IsMatchingType(m_peer) ||
+        (m_allowPacketSocket && PacketSocketAddress::IsMatchingType(m_peer)))
+    {
+        ret = m_socket->Bind();
+    }
+    else if (Inet6SocketAddress::IsMatchingType(m_peer))
+    {
+        ret = m_socket->Bind6();
+    }
+    else
+    {
+        NS_FATAL_ERROR("Incompatible address type: " << m_peer);
+    }
+    if (ret == -1)
+    {
+        NS_FATAL_ERROR("Failed to bind socket");
+    }
+
+    if (InetSocketAddress::IsMatchingType(m_peer))
+    {
+        m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
+    }
+
+    m_socket->Connect(m_peer);
+
+    CancelEvents();
+
+    DoStartApplication();
+}
+
+void
+SourceApplication::StopApplication()
+{
+    NS_LOG_FUNCTION(this);
+    DoStopApplication();
+    CancelEvents();
+    CloseSocket();
+}
+
+bool
+SourceApplication::CloseSocket()
+{
+    m_connected = false;
+    if (m_socket)
+    {
+        const auto ret = m_socket->Close();
+        m_socket->SetConnectCallback(MakeNullCallback<void, Ptr<Socket>>(),
+                                     MakeNullCallback<void, Ptr<Socket>>());
+        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
+        return (ret == 0);
+    }
+    return true;
+}
+
+void
+SourceApplication::ConnectionSucceeded(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    m_connected = true;
+    DoConnectionSucceeded(socket);
+    m_connectionSuccess(socket, m_local, m_peer);
+}
+
+void
+SourceApplication::ConnectionFailed(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    m_connected = false;
+    DoConnectionFailed(socket);
+    m_connectionFailure(socket, m_local, m_peer);
+}
+
+void
+SourceApplication::DoStartApplication()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+void
+SourceApplication::DoStopApplication()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+void
+SourceApplication::DoConnectionSucceeded(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+}
+
+void
+SourceApplication::DoConnectionFailed(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+}
+
 } // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/source-application.h ns-3.47/src/applications/model/source-application.h
--- ns-3.46/src/applications/model/source-application.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/source-application.h	2026-02-16 16:08:49.571229700 -0800
@@ -11,10 +11,14 @@
 
 #include "ns3/address.h"
 #include "ns3/application.h"
+#include "ns3/traced-callback.h"
 
 namespace ns3
 {
 
+class Packet;
+class Socket;
+
 /**
  * @ingroup applications
  * @brief Base class for source applications.
@@ -40,7 +44,11 @@
      */
     static TypeId GetTypeId();
 
-    SourceApplication();
+    /**
+     * @brief Constructor
+     * @param allowPacketSocket flag whether the application should allow the use of packet sockets
+     */
+    explicit SourceApplication(bool allowPacketSocket = true);
     ~SourceApplication() override;
 
     /**
@@ -55,10 +63,95 @@
      */
     Address GetRemote() const;
 
+    /**
+     * @brief Get the socket this application is attached to.
+     * @return pointer to associated socket
+     */
+    Ptr<Socket> GetSocket() const;
+
   protected:
+    void DoDispose() override;
+
+    /**
+     * @brief Close the socket
+     * @return true if the socket was closed, false if there was no socket to close
+     */
+    bool CloseSocket();
+
+    /// Traced Callback: transmitted packets.
+    TracedCallback<Ptr<const Packet>> m_txTrace;
+
+    /**
+     * TracedCallback signature for connection success/failure event.
+     *
+     * @param [in] socket The socket for which connection succeeded/failed.
+     * @param [in] local The local address.
+     * @param [in] remote The remote address.
+     */
+    typedef void (*ConnectionEventCallback)(Ptr<Socket> socket,
+                                            const Address& local,
+                                            const Address& remote);
+
+    /// Traced Callback: connection success event.
+    TracedCallback<Ptr<Socket>, const Address&, const Address&> m_connectionSuccess;
+
+    /// Traced Callback: connection failure event.
+    TracedCallback<Ptr<Socket>, const Address&, const Address&> m_connectionFailure;
+
+    Ptr<Socket> m_socket; //!< Socket
+
+    TypeId m_protocolTid; //!< Protocol TypeId value
+
     Address m_peer;  //!< Peer address
     Address m_local; //!< Local address to bind to
     uint8_t m_tos;   //!< The packets Type of Service
+
+    bool m_connected{false}; //!< flag whether socket is connected
+
+  private:
+    void StartApplication() override;
+    void StopApplication() override;
+
+    /**
+     * @brief Handle a Connection Succeed event
+     * @param socket the connected socket
+     */
+    void ConnectionSucceeded(Ptr<Socket> socket);
+
+    /**
+     * @brief Handle a Connection Failed event
+     * @param socket the not connected socket
+     */
+    void ConnectionFailed(Ptr<Socket> socket);
+
+    /**
+     * @brief Application specific startup code for child subclasses
+     */
+    virtual void DoStartApplication();
+
+    /**
+     * @brief Application specific shutdown code for child subclasses
+     */
+    virtual void DoStopApplication();
+
+    /**
+     * @brief Application specific code for child subclasses upon a Connection Succeed event
+     * @param socket the connected socket
+     */
+    virtual void DoConnectionSucceeded(Ptr<Socket> socket);
+
+    /**
+     * @brief Application specific code for child subclasses upon a Connection Failed event
+     * @param socket the not connected socket
+     */
+    virtual void DoConnectionFailed(Ptr<Socket> socket);
+
+    /**
+     * @brief Cancel all pending events.
+     */
+    virtual void CancelEvents() = 0;
+
+    bool m_allowPacketSocket; //!< Allow use of packet socket
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/tgax-video-traffic.cc ns-3.47/src/applications/model/tgax-video-traffic.cc
--- ns-3.46/src/applications/model/tgax-video-traffic.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/tgax-video-traffic.cc	2026-02-16 16:08:49.571229700 -0800
@@ -0,0 +1,390 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "tgax-video-traffic.h"
+
+#include "ns3/double.h"
+#include "ns3/enum.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/inet6-socket-address.h"
+#include "ns3/log.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/packet.h"
+#include "ns3/random-variable-stream.h"
+#include "ns3/simulator.h"
+#include "ns3/socket-factory.h"
+#include "ns3/socket.h"
+#include "ns3/tcp-socket-factory.h"
+#include "ns3/udp-socket-factory.h"
+#include "ns3/uinteger.h"
+
+#include <limits>
+
+namespace ns3
+{
+
+NS_LOG_COMPONENT_DEFINE("TgaxVideoTraffic");
+
+NS_OBJECT_ENSURE_REGISTERED(TgaxVideoTraffic);
+
+// clang-format off
+const TgaxVideoTraffic::TrafficModels TgaxVideoTraffic::m_trafficModels {
+    {
+        {TrafficModelClassIdentifier::BUFFERED_VIDEO_1, {{"2Mbps"}, 6950, 0.8099}},
+        {TrafficModelClassIdentifier::BUFFERED_VIDEO_2, {{"4Mbps"}, 13900, 0.8099}},
+        {TrafficModelClassIdentifier::BUFFERED_VIDEO_3, {{"6Mbps"}, 20850, 0.8099}},
+        {TrafficModelClassIdentifier::BUFFERED_VIDEO_4, {{"8Mbps"}, 27800, 0.8099}},
+        {TrafficModelClassIdentifier::BUFFERED_VIDEO_5, {{"10Mbps"}, 34750, 0.8099}},
+        {TrafficModelClassIdentifier::BUFFERED_VIDEO_6, {{"15600Kbps"}, 54210, 0.8099}},
+        {TrafficModelClassIdentifier::MULTICAST_VIDEO_1, {{"3Mbps"}, 10425, 0.8099}},
+        {TrafficModelClassIdentifier::MULTICAST_VIDEO_2, {{"6Mbps"}, 20850, 0.8099}}
+    }
+};
+
+// clang-format on
+
+TypeId
+TgaxVideoTraffic::GetTypeId()
+{
+    static TypeId tid =
+        TypeId("ns3::TgaxVideoTraffic")
+            .SetParent<SourceApplication>()
+            .SetGroupName("Applications")
+            .AddConstructor<TgaxVideoTraffic>()
+            .AddAttribute(
+                "TrafficModelClassIdentifier",
+                "The Traffic Model Class Identifier to use (use Custom for custom settings)",
+                TypeId::ATTR_GET | TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                EnumValue(TgaxVideoTraffic::TrafficModelClassIdentifier::CUSTOM),
+                MakeEnumAccessor<TrafficModelClassIdentifier>(
+                    &TgaxVideoTraffic::m_trafficModelClassId),
+                MakeEnumChecker(TgaxVideoTraffic::TrafficModelClassIdentifier::CUSTOM,
+                                "Custom",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_1,
+                                "BV1",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_2,
+                                "BV2",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_3,
+                                "BV3",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_4,
+                                "BV4",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_5,
+                                "BV5",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_6,
+                                "BV6",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_1,
+                                "MC1",
+                                TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_2,
+                                "MC2"))
+            .AddAttribute(
+                "Protocol",
+                "The type of protocol to use. This should be a subclass of ns3::SocketFactory",
+                TypeId::ATTR_GET | TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                TypeIdValue(TcpSocketFactory::GetTypeId()),
+                MakeTypeIdAccessor(&TgaxVideoTraffic::m_protocolTid),
+                MakeTypeIdChecker())
+            .AddAttribute("CustomVideoBitRate",
+                          "The video bit rate (if TrafficModelClassIdentifier is Custom).",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          DataRateValue(DataRate("2Mbps")),
+                          MakeDataRateAccessor(&TgaxVideoTraffic::m_bitRate),
+                          MakeDataRateChecker())
+            .AddAttribute("CustomFrameSizeScale",
+                          "Scale parameter for the Weibull distribution to calculate the video "
+                          "frame size in bytes (if TrafficModelClassIdentifier is Custom).",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          DoubleValue(6950),
+                          MakeDoubleAccessor(&TgaxVideoTraffic::m_frameSizeBytesScale),
+                          MakeDoubleChecker<double>())
+            .AddAttribute("CustomFrameSizeShape",
+                          "Shape parameter for the Weibull distribution to calculate the video "
+                          "frame size in bytes (if TrafficModelClassIdentifier is Custom).",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          DoubleValue(0.8099),
+                          MakeDoubleAccessor(&TgaxVideoTraffic::m_frameSizeBytesShape),
+                          MakeDoubleChecker<double>())
+            .AddAttribute("LatencyShape",
+                          "Shape parameter for the Gamma distribution to calculate the network "
+                          "latency in milliseconds.",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          DoubleValue(0.2463),
+                          MakeDoubleAccessor(&TgaxVideoTraffic::m_latencyMsShape),
+                          MakeDoubleChecker<double>())
+            .AddAttribute("LatencyScale",
+                          "Rate parameter for the Gamma distribution to calculate the network "
+                          "latency in milliseconds. If set to 0 (default), no latency is added."
+                          "In the reference model, this is set to 60.227ms because it uses a link "
+                          "simulator that doesn't actually have queues and TCP models.",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          DoubleValue(0),
+                          MakeDoubleAccessor(&TgaxVideoTraffic::m_latencyMsScale),
+                          MakeDoubleChecker<double>())
+            .AddTraceSource(
+                "TxWithLatency",
+                "A video packet is sent, this trace also reports the latency applied to the packet",
+                MakeTraceSourceAccessor(&TgaxVideoTraffic::m_txLatencyTrace),
+                "ns3::TgaxVideoTraffic::TxTracedCallback")
+            .AddTraceSource("VideoFrameGenerated",
+                            "A video frame is generated, this trace reports the amount of payload "
+                            "bytes in the generated frame",
+                            MakeTraceSourceAccessor(&TgaxVideoTraffic::m_frameGeneratedTrace),
+                            "ns3::TracedValueCallback::Uint32");
+    return tid;
+}
+
+TgaxVideoTraffic::TgaxVideoTraffic()
+    : m_frameSizeBytes{CreateObject<WeibullRandomVariable>()},
+      m_latencyMs{CreateObject<GammaRandomVariable>()}
+{
+    NS_LOG_FUNCTION(this);
+}
+
+TgaxVideoTraffic::~TgaxVideoTraffic()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+int64_t
+TgaxVideoTraffic::AssignStreams(int64_t stream)
+{
+    NS_LOG_FUNCTION(this << stream);
+    auto currentStream = stream;
+    m_frameSizeBytes->SetStream(currentStream++);
+    m_latencyMs->SetStream(currentStream++);
+    return (currentStream - stream);
+}
+
+void
+TgaxVideoTraffic::DoInitialize()
+{
+    NS_LOG_FUNCTION(this);
+    SourceApplication::DoInitialize();
+
+    if (m_trafficModelClassId != TrafficModelClassIdentifier::CUSTOM)
+    {
+        const auto it = m_trafficModels.find(m_trafficModelClassId);
+        NS_ASSERT(it != m_trafficModels.cend());
+        NS_ABORT_MSG_IF(
+            ((m_trafficModelClassId == TrafficModelClassIdentifier::MULTICAST_VIDEO_1 ||
+              m_trafficModelClassId == TrafficModelClassIdentifier::MULTICAST_VIDEO_2) &&
+             m_protocolTid == TcpSocketFactory::GetTypeId()),
+            "Cannot use TCP protocol with multicast video traffic model");
+        m_bitRate = it->second.bitRate;
+        m_frameSizeBytesScale = it->second.frameSizeBytesScale;
+        m_frameSizeBytesShape = it->second.frameSizeBytesShape;
+    }
+
+    m_frameSizeBytes->SetAttribute("Scale", DoubleValue(m_frameSizeBytesScale));
+    m_frameSizeBytes->SetAttribute("Shape", DoubleValue(m_frameSizeBytesShape));
+    m_latencyMs->SetAttribute("Alpha", DoubleValue(m_latencyMsScale));
+    m_latencyMs->SetAttribute("Beta", DoubleValue(m_latencyMsShape));
+
+    auto averageFrameSize = static_cast<uint32_t>(m_frameSizeBytes->GetMean());
+    m_interArrival = m_bitRate.CalculateBytesTxTime(averageFrameSize);
+}
+
+void
+TgaxVideoTraffic::DoStartApplication()
+{
+    NS_LOG_FUNCTION(this);
+
+    if (m_protocolTid == TcpSocketFactory::GetTypeId())
+    {
+        UintegerValue uintegerValue;
+        m_socket->GetAttribute("SegmentSize", uintegerValue);
+        m_maxSize = uintegerValue.Get();
+    }
+
+    m_socket->SetDataSentCallback(MakeCallback(&TgaxVideoTraffic::TxDone, this));
+    m_socket->SetSendCallback(MakeCallback(&TgaxVideoTraffic::TxAvailable, this));
+    m_socket->SetAllowBroadcast(true);
+    m_socket->ShutdownRecv();
+
+    if (m_connected)
+    {
+        ScheduleNextFrame();
+    }
+}
+
+void
+TgaxVideoTraffic::CancelEvents()
+{
+    NS_LOG_FUNCTION(this);
+    m_generateFrameEvent.Cancel();
+    for (auto& sendEvent : m_sendEvents)
+    {
+        sendEvent.second.Cancel();
+    }
+    m_sendEvents.clear();
+    m_unsentPackets.clear();
+}
+
+void
+TgaxVideoTraffic::ScheduleNextFrame()
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT(m_generateFrameEvent.IsExpired());
+    NS_ASSERT(m_interArrival.IsStrictlyPositive());
+    m_generateFrameEvent =
+        Simulator::Schedule(m_interArrival, &TgaxVideoTraffic::GenerateVideoFrame, this);
+}
+
+void
+TgaxVideoTraffic::GenerateVideoFrame()
+{
+    NS_LOG_FUNCTION(this);
+
+    uint32_t frameSize{0};
+    while (frameSize == 0)
+    {
+        frameSize = static_cast<uint32_t>(m_frameSizeBytes->GetValue());
+    }
+
+    m_frameGeneratedTrace(frameSize);
+    m_generatedFrames.push_back(frameSize);
+
+    SendWithLatency();
+
+    ScheduleNextFrame();
+}
+
+uint32_t
+TgaxVideoTraffic::GetNextPayloadSize()
+{
+    auto frameSize = m_remainingSize;
+    if (frameSize == 0)
+    {
+        NS_ASSERT(!m_generatedFrames.empty());
+        frameSize = m_generatedFrames.front();
+        m_generatedFrames.pop_front();
+    }
+
+    if (const auto limit = m_maxSize.value_or(m_socket->GetTxAvailable()); frameSize > limit)
+    {
+        m_remainingSize = frameSize - limit;
+        frameSize = limit;
+    }
+    else
+    {
+        m_remainingSize = 0;
+    }
+
+    return frameSize;
+}
+
+void
+TgaxVideoTraffic::SendWithLatency()
+{
+    NS_LOG_FUNCTION(this);
+
+    auto size = GetNextPayloadSize();
+    NS_ASSERT(size > 0);
+
+    auto latency{Time::FromDouble(m_latencyMs->GetValue(), Time::MS)};
+    NS_LOG_INFO("At time " << Simulator::Now().As(Time::S)
+                           << " video traffic source schedule to sent " << size
+                           << " bytes after latency of " << latency.As(Time::US));
+
+    m_sendEvents[m_nextEventId] =
+        Simulator::Schedule(latency, &TgaxVideoTraffic::Send, this, m_nextEventId, size, latency);
+    ++m_nextEventId;
+}
+
+void
+TgaxVideoTraffic::Send(uint64_t eventId, uint32_t size, Time latency)
+{
+    NS_LOG_FUNCTION(this << eventId << size << latency << m_unsentPackets.size());
+
+    auto it = m_sendEvents.find(eventId);
+    NS_ASSERT(it != m_sendEvents.end());
+    NS_ASSERT(it->second.IsExpired());
+
+    auto packet = m_unsentPackets.empty() ? Create<Packet>(size) : m_unsentPackets.front().packet;
+    const auto actual = static_cast<unsigned int>(m_socket->Send(packet));
+    if (actual == size)
+    {
+        if (!m_unsentPackets.empty())
+        {
+            m_unsentPackets.pop_front();
+        }
+        m_txTrace(packet);
+        m_txLatencyTrace(packet, latency);
+        m_sendEvents.erase(it);
+        if (InetSocketAddress::IsMatchingType(m_peer))
+        {
+            NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " video traffic source sent "
+                                   << size << " bytes to "
+                                   << InetSocketAddress::ConvertFrom(m_peer).GetIpv4() << " port "
+                                   << InetSocketAddress::ConvertFrom(m_peer).GetPort());
+        }
+        else if (Inet6SocketAddress::IsMatchingType(m_peer))
+        {
+            NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " video traffic source sent "
+                                   << size << " bytes to "
+                                   << Inet6SocketAddress::ConvertFrom(m_peer).GetIpv6() << " port "
+                                   << Inet6SocketAddress::ConvertFrom(m_peer).GetPort());
+        }
+    }
+    else
+    {
+        NS_LOG_DEBUG("Unable to send packet; actual " << actual << " size " << size
+                                                      << "; caching for later attempt");
+        if (m_unsentPackets.empty())
+        {
+            m_unsentPackets.push_back({eventId, packet, latency});
+        }
+    }
+}
+
+void
+TgaxVideoTraffic::DoConnectionSucceeded(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    ScheduleNextFrame();
+}
+
+void
+TgaxVideoTraffic::TxDone(Ptr<Socket> socket, uint32_t size)
+{
+    NS_LOG_FUNCTION(this << socket << size);
+    if (m_unsentPackets.empty() && !m_generatedFrames.empty() && m_socket->GetTxAvailable() > 0)
+    {
+        SendWithLatency();
+    }
+}
+
+void
+TgaxVideoTraffic::TxAvailable(Ptr<Socket> socket, uint32_t available)
+{
+    NS_LOG_FUNCTION(this << socket << available);
+    if (m_unsentPackets.empty())
+    {
+        if (!m_generatedFrames.empty() && m_socket->GetTxAvailable() > 0)
+        {
+            SendWithLatency();
+        }
+        return;
+    }
+    const auto& unsentPacketInfo = m_unsentPackets.front();
+    const auto size = unsentPacketInfo.packet->GetSize();
+    if (available >= size)
+    {
+        // Do not add additional networking latency when this is another TX attempt
+        NS_LOG_DEBUG("Retry packet " << unsentPacketInfo.id << " with size " << size);
+        Send(unsentPacketInfo.id, size, unsentPacketInfo.latency);
+    }
+}
+
+} // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/tgax-video-traffic.h ns-3.47/src/applications/model/tgax-video-traffic.h
--- ns-3.46/src/applications/model/tgax-video-traffic.h	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/tgax-video-traffic.h	2026-02-16 16:08:49.571229700 -0800
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#ifndef TGAX_VIDEO_TRAFFIC_H
+#define TGAX_VIDEO_TRAFFIC_H
+
+#include "source-application.h"
+
+#include "ns3/applications-export.h"
+#include "ns3/data-rate.h"
+#include "ns3/event-id.h"
+#include "ns3/traced-callback.h"
+
+#include <deque>
+#include <map>
+#include <optional>
+
+namespace ns3
+{
+
+class WeibullRandomVariable;
+class GammaRandomVariable;
+
+/**
+ * @ingroup applications
+ * @brief Generate video traffic.
+ *
+ * This video traffic generator implements the Buffered Video Steaming model from IEEE
+ * 802.11-14/0571r12 - 11ax Evaluation Methodology (see applications documentation for
+ * full citation).
+ */
+class APPLICATIONS_EXPORT TgaxVideoTraffic : public SourceApplication
+{
+  public:
+    /**
+     * @brief Get the type ID.
+     * @return the object TypeId
+     */
+    static TypeId GetTypeId();
+
+    TgaxVideoTraffic();
+    ~TgaxVideoTraffic() override;
+
+    /// TrafficModelClassIdentifier enumeration
+    enum class TrafficModelClassIdentifier : uint8_t
+    {
+        CUSTOM = 0, //< Custom traffic model (by default, load parameters of Buffered Video Model 1)
+        BUFFERED_VIDEO_1,  //< Buffered Video Model 1
+        BUFFERED_VIDEO_2,  //< Buffered Video Model 2
+        BUFFERED_VIDEO_3,  //< Buffered Video Model 3
+        BUFFERED_VIDEO_4,  //< Buffered Video Model 4
+        BUFFERED_VIDEO_5,  //< Buffered Video Model 5
+        BUFFERED_VIDEO_6,  //< Buffered Video Model 6
+        MULTICAST_VIDEO_1, //< Multicast Video Model 1
+        MULTICAST_VIDEO_2  //< Multicast Video Model 2
+    };
+
+    /// List of parameters for a given traffic model
+    struct TrafficModelParameters
+    {
+        DataRate bitRate;           //!< video bit rate
+        double frameSizeBytesScale; //!< scale parameter for the Weibull distribution used to
+                                    //!< generate size of video frames in bytes (corresponds to
+                                    //!< lambda parameter in table 5 from IEEE 802.11-14/0571r12 -
+                                    //!< 11ax Evaluation Methodology)
+        double frameSizeBytesShape; //!< shape parameter for the Weibull distribution used to
+                                    //!< generate size of video frames in bytes (corresponds to k
+                                    //!< parameter in table 5 from IEEE 802.11-14/0571r12 - 11ax
+                                    //!< Evaluation Methodology)
+    };
+
+    /// Parameters for each traffic model
+    using TrafficModels = std::map<TrafficModelClassIdentifier, TrafficModelParameters>;
+
+    static const TrafficModels m_trafficModels; //!< Traffic models as defined in Table 5 from IEEE
+                                                //!< 802.11-14/0571r12 - 11ax Evaluation Methodology
+
+    int64_t AssignStreams(int64_t stream) override;
+
+  protected:
+    void DoInitialize() override;
+
+  private:
+    void DoStartApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void CancelEvents() override;
+
+    /**
+     * Schedule send of a packet with a random latency
+     */
+    void SendWithLatency();
+
+    /**
+     * Effectively send a packet once the latency has elapsed
+     * @param eventId the event ID
+     * @param size the size of the packet to send (in bytes)
+     * @param latency the latency applied to that packet
+     */
+    void Send(uint64_t eventId, uint32_t size, Time latency);
+
+    /**
+     * Schedule the next frame generation
+     */
+    void ScheduleNextFrame();
+
+    /**
+     * Generate new video frame
+     */
+    void GenerateVideoFrame();
+
+    /**
+     * @return the payload size of the next packet to transmit (in bytes)
+     */
+    uint32_t GetNextPayloadSize();
+
+    /**
+     * Handle a Data Sent event
+     *
+     * @param socket the connected socket
+     * @param size the amount of transmitted bytes
+     */
+    void TxDone(Ptr<Socket> socket, uint32_t size);
+
+    /**
+     * Handle a Send event
+     *
+     * @param socket the connected socket
+     * @param available the amount of available bytes for transmission
+     */
+    void TxAvailable(Ptr<Socket> socket, uint32_t available);
+
+    TrafficModelClassIdentifier m_trafficModelClassId; //!< The Traffic Model Class Identifier
+    DataRate m_bitRate;                                //!< Video bit rate (if model is custom)
+    double m_frameSizeBytesScale; //!< Scale parameter for the Weibull distribution used to generate
+                                  //!< size of video frames (if model is custom)
+    double m_frameSizeBytesShape; //!< Shape parameter for the Weibull distribution used to generate
+                                  //!< size of video frames (if model is custom)
+    double
+        m_latencyMsScale; //!< Scale parameter for the Gamma distribution used to generate latency
+    double
+        m_latencyMsShape; //!< Shape parameter for the Gamma distribution used to generate latency
+
+    Ptr<WeibullRandomVariable>
+        m_frameSizeBytes; //!< Weibull random variable to generate size of video frames (in bytes)
+    Ptr<GammaRandomVariable>
+        m_latencyMs; //!< Gamma random variable to generate latency (in milliseconds)
+
+    std::optional<uint32_t> m_maxSize; //!< Limit on the number of bytes that can be sent at once
+                                       //!< over the network, hence we limit at application level to
+                                       //!< apply the latency to each transmitted packet
+    uint32_t m_remainingSize{0}; //!< Number of bytes to send directly to the socket because current
+                                 //!< video frame is too large to be sent at once
+    Time m_interArrival; //!< Calculated inter arrival duration between two generated packets
+
+    EventId m_generateFrameEvent;           //!< Event ID of pending frame generation event
+    std::deque<uint32_t> m_generatedFrames; //!< Hold size of generated video frames
+
+    std::map<uint64_t, EventId> m_sendEvents; //!< Event IDs of pending TX events
+    uint64_t m_nextEventId{0};                //!< The next event ID
+
+    /// Structure to store information about packets that are not successfully transmitted
+    struct UnsentPacketInfo
+    {
+        uint64_t id;        //!< the associated TX event ID
+        Ptr<Packet> packet; //!< the packet to transmit
+        Time latency;       //!< the networking latency applied to the first transmission attempt
+    };
+
+    std::deque<UnsentPacketInfo> m_unsentPackets; //!< Hold unsent packet for later attempt
+
+    /**
+     * TracedCallback signature for packet and latency.
+     *
+     * @param [in] packet The packet.
+     * @param [in] latency The networking latency.
+     */
+    typedef void (*TxTracedCallback)(Ptr<const Packet> packet, Time latency);
+
+    /// Traced Callback: transmitted packets and their latencies.
+    TracedCallback<Ptr<const Packet>, Time> m_txLatencyTrace;
+
+    /// Traced Callback: generated frames (amount of payload bytes).
+    TracedCallback<uint32_t> m_frameGeneratedTrace;
+};
+
+} // namespace ns3
+
+#endif /* TGAX_VIDEO_TRAFFIC_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/tgax-virtual-desktop.cc ns-3.47/src/applications/model/tgax-virtual-desktop.cc
--- ns-3.46/src/applications/model/tgax-virtual-desktop.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/tgax-virtual-desktop.cc	2026-02-16 16:08:49.571229700 -0800
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "tgax-virtual-desktop.h"
+
+#include "ns3/attribute-container.h"
+#include "ns3/double.h"
+#include "ns3/enum.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/inet6-socket-address.h"
+#include "ns3/log.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/packet.h"
+#include "ns3/pair.h"
+#include "ns3/pointer.h"
+#include "ns3/random-variable-stream.h"
+#include "ns3/simulator.h"
+#include "ns3/socket-factory.h"
+#include "ns3/socket.h"
+#include "ns3/string.h"
+
+#include <cmath>
+
+namespace ns3
+{
+
+NS_LOG_COMPONENT_DEFINE("TgaxVirtualDesktop");
+
+NS_OBJECT_ENSURE_REGISTERED(TgaxVirtualDesktop);
+
+TypeId
+TgaxVirtualDesktop::GetTypeId()
+{
+    static TypeId tid =
+        TypeId("ns3::TgaxVirtualDesktop")
+            .SetParent<SourceApplication>()
+            .SetGroupName("Applications")
+            .AddConstructor<TgaxVirtualDesktop>()
+            .AddAttribute("ModelPresets",
+                          "The model presets to use (Custom for custom settings)",
+                          TypeId::ATTR_GET |
+                              TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                          EnumValue(TgaxVirtualDesktop::ModelPresets::CUSTOM),
+                          MakeEnumAccessor<ModelPresets>(&TgaxVirtualDesktop::m_modelPresets),
+                          MakeEnumChecker(TgaxVirtualDesktop::ModelPresets::CUSTOM,
+                                          "Custom",
+                                          TgaxVirtualDesktop::ModelPresets::DOWNLINK,
+                                          "Downlink",
+                                          TgaxVirtualDesktop::ModelPresets::UPLINK,
+                                          "Uplink"))
+            .AddAttribute(
+                "Protocol",
+                "The type of protocol to use. This should be a subclass of ns3::SocketFactory",
+                TypeIdValue(TypeId::LookupByName("ns3::TcpSocketFactory")),
+                MakeTypeIdAccessor(&TgaxVirtualDesktop::m_protocolTid),
+                MakeTypeIdChecker())
+            .AddAttribute(
+                "CustomInitialPacketArrival",
+                "A uniform random variable to generate the initial packet arrival in nanoseconds.",
+                StringValue("ns3::UniformRandomVariable[Min=0|Max=20000000]"),
+                MakePointerAccessor(&TgaxVirtualDesktop::m_initialArrivalUniform),
+                MakePointerChecker<UniformRandomVariable>())
+            .AddAttribute("CustomInterPacketArrivals",
+                          "An exponential random variable to generate the inter packet arrivals in "
+                          "nanoseconds.",
+                          StringValue("ns3::ExponentialRandomVariable[Mean=60226900]"),
+                          MakePointerAccessor(&TgaxVirtualDesktop::m_interArrivalExponential),
+                          MakePointerChecker<ExponentialRandomVariable>())
+            .AddAttribute(
+                "CustomParametersPacketSize",
+                "The mean value and standard deviation for each mode of the multimodal normal "
+                "distribution used to calculate packet sizes.",
+                StringValue("41.0 3.2;1478.3 11.6"),
+                MakeAttributeContainerAccessor<PairValue<DoubleValue, DoubleValue>, ';'>(
+                    &TgaxVirtualDesktop::SetParametersPacketSize),
+                MakeAttributeContainerChecker<PairValue<DoubleValue, DoubleValue>, ';'>(
+                    MakePairChecker<DoubleValue, DoubleValue>(MakeDoubleChecker<double>(),
+                                                              MakeDoubleChecker<double>())));
+    return tid;
+}
+
+TgaxVirtualDesktop::TgaxVirtualDesktop()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+TgaxVirtualDesktop::~TgaxVirtualDesktop()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+int64_t
+TgaxVirtualDesktop::AssignStreams(int64_t stream)
+{
+    NS_LOG_FUNCTION(this << stream);
+    auto currentStream = stream;
+    m_initialArrivalUniform->SetStream(currentStream++);
+    m_interArrivalExponential->SetStream(currentStream++);
+    if (m_pktSizeDistributions.size() > 1)
+    {
+        m_dlModeSelection->SetStream(currentStream++);
+    }
+    for (auto& pktSizeDistribution : m_pktSizeDistributions)
+    {
+        pktSizeDistribution->SetStream(currentStream++);
+    }
+    return (currentStream - stream);
+}
+
+void
+TgaxVirtualDesktop::SetParametersPacketSize(const std::vector<std::pair<double, double>>& params)
+{
+    m_pktSizeDistributions.clear();
+    NS_LOG_FUNCTION(this << params.size());
+    for (const auto& [mean, std] : params)
+    {
+        auto normal = CreateObject<NormalRandomVariable>();
+        normal->SetAttribute("Mean", DoubleValue(mean));
+        normal->SetAttribute("Variance", DoubleValue(std::pow(std, 2)));
+        m_pktSizeDistributions.push_back(normal);
+    }
+    if ((m_pktSizeDistributions.size() > 1) && !m_dlModeSelection)
+    {
+        m_dlModeSelection =
+            CreateObjectWithAttributes<BernoulliRandomVariable>("Probability",
+                                                                DoubleValue(22.4 / 76.1));
+    }
+}
+
+void
+TgaxVirtualDesktop::DoInitialize()
+{
+    NS_LOG_FUNCTION(this);
+    SourceApplication::DoInitialize();
+
+    if (m_modelPresets != ModelPresets::CUSTOM)
+    {
+        std::vector<std::pair<double, double>> params;
+        if (m_modelPresets == ModelPresets::DOWNLINK)
+        {
+            // Downlink model presets
+            params = {{41.0, 3.2}, {1478.3, 11.6}};
+            m_initialArrivalUniform->SetAttribute("Min", DoubleValue(0.0));
+            m_initialArrivalUniform->SetAttribute("Max", DoubleValue(20000000.0));
+            m_interArrivalExponential->SetAttribute("Mean", DoubleValue(60226900.0));
+        }
+        else if (m_modelPresets == ModelPresets::UPLINK)
+        {
+            // Uplink model presets
+            params = {{50.598, 5.0753}};
+            m_initialArrivalUniform->SetAttribute("Min", DoubleValue(0.0));
+            m_initialArrivalUniform->SetAttribute("Max", DoubleValue(20000000.0));
+            m_interArrivalExponential->SetAttribute("Mean", DoubleValue(48287000.0));
+        }
+        SetParametersPacketSize(params);
+    }
+}
+
+void
+TgaxVirtualDesktop::DoStartApplication()
+{
+    NS_LOG_FUNCTION(this);
+
+    m_socket->SetDataSentCallback(MakeCallback(&TgaxVirtualDesktop::TxDone, this));
+    m_socket->SetSendCallback(MakeCallback(&TgaxVirtualDesktop::TxAvailable, this));
+    m_socket->SetAllowBroadcast(true);
+    m_socket->ShutdownRecv();
+
+    if (m_connected)
+    {
+        ScheduleNext();
+    }
+}
+
+void
+TgaxVirtualDesktop::CancelEvents()
+{
+    NS_LOG_FUNCTION(this);
+    m_txEvent.Cancel();
+    if (m_unsentPacket)
+    {
+        NS_LOG_DEBUG("Discarding cached packet upon CancelEvents ()");
+    }
+    m_unsentPacket = nullptr;
+    m_initialPacket = true;
+}
+
+Time
+TgaxVirtualDesktop::GetInterArrival() const
+{
+    if (m_unsentPacket)
+    {
+        return Time();
+    }
+    if (m_initialPacket)
+    {
+        return NanoSeconds(m_initialArrivalUniform->GetValue());
+    }
+    return NanoSeconds(m_interArrivalExponential->GetValue());
+}
+
+uint32_t
+TgaxVirtualDesktop::GetPacketSize() const
+{
+    const auto mode = (m_pktSizeDistributions.size() > 1) ? m_dlModeSelection->GetValue() : 0;
+    return m_pktSizeDistributions.at(mode)->GetValue();
+}
+
+void
+TgaxVirtualDesktop::ScheduleNext()
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT(!m_txEvent.IsPending());
+    m_txEvent = Simulator::Schedule(GetInterArrival(), &TgaxVirtualDesktop::SendPacket, this);
+}
+
+void
+TgaxVirtualDesktop::SendPacket()
+{
+    NS_LOG_FUNCTION(this);
+
+    NS_ASSERT(m_txEvent.IsExpired());
+
+    Ptr<Packet> packet;
+    if (m_unsentPacket)
+    {
+        packet = m_unsentPacket;
+    }
+    else
+    {
+        packet = Create<Packet>(GetPacketSize());
+    }
+
+    unsigned int actualSize = m_socket->Send(packet);
+    if (actualSize == packet->GetSize())
+    {
+        m_txTrace(packet);
+        m_unsentPacket = nullptr;
+        Address localAddress;
+        m_socket->GetSockName(localAddress);
+        if (InetSocketAddress::IsMatchingType(m_peer))
+        {
+            NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " VDI traffic source sent "
+                                   << (m_initialPacket ? "initial packet of " : "packet of ")
+                                   << packet->GetSize() << " bytes to "
+                                   << InetSocketAddress::ConvertFrom(m_peer).GetIpv4() << " port "
+                                   << InetSocketAddress::ConvertFrom(m_peer).GetPort());
+        }
+        else if (Inet6SocketAddress::IsMatchingType(m_peer))
+        {
+            NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " VDI traffic source sent "
+                                   << (m_initialPacket ? "initial packet of " : "packet of ")
+                                   << packet->GetSize() << " bytes to "
+                                   << Inet6SocketAddress::ConvertFrom(m_peer).GetIpv6() << " port "
+                                   << Inet6SocketAddress::ConvertFrom(m_peer).GetPort());
+        }
+    }
+    else
+    {
+        NS_LOG_DEBUG("Unable to send VDI packet; actual " << actualSize << " size "
+                                                          << packet->GetSize()
+                                                          << "; caching for later attempt");
+        m_unsentPacket = packet;
+    }
+
+    if (m_initialPacket)
+    {
+        m_initialPacket = false;
+    }
+
+    ScheduleNext();
+}
+
+void
+TgaxVirtualDesktop::DoConnectionSucceeded(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    ScheduleNext();
+}
+
+void
+TgaxVirtualDesktop::TxDone(Ptr<Socket> socket, uint32_t size)
+{
+    NS_LOG_FUNCTION(this << socket << size);
+    if (m_unsentPacket)
+    {
+        ScheduleNext();
+    }
+}
+
+void
+TgaxVirtualDesktop::TxAvailable(Ptr<Socket> socket, uint32_t available)
+{
+    NS_LOG_FUNCTION(this << socket << available);
+    if (m_unsentPacket)
+    {
+        ScheduleNext();
+    }
+}
+
+} // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/tgax-virtual-desktop.h ns-3.47/src/applications/model/tgax-virtual-desktop.h
--- ns-3.46/src/applications/model/tgax-virtual-desktop.h	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/tgax-virtual-desktop.h	2026-02-16 16:08:49.571229700 -0800
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#ifndef TGAX_VIRTUAL_DESKTOP_H
+#define TGAX_VIRTUAL_DESKTOP_H
+
+#include "source-application.h"
+
+#include "ns3/address.h"
+#include "ns3/event-id.h"
+#include "ns3/ptr.h"
+
+#include <utility>
+#include <vector>
+
+namespace ns3
+{
+
+class ExponentialRandomVariable;
+class UniformRandomVariable;
+class BernoulliRandomVariable;
+class NormalRandomVariable;
+
+/**
+ * @ingroup applications
+ * @brief Generate Virtual Desktop Infrastructure (VDI) traffic.
+ *
+ * This VDI traffic generator follows requirements from IEEE 802.11-14/0571r12 - 11ax Evaluation
+ * Methodology (Appendix 2 – Traffic model descriptions: Virtual Desktop Infrastructure Traffic
+ * Model) (see applications documentation for full citation).
+ *
+ * In this model, desktop application packet arrival interval obeys an exponential distribution and
+ * packet size obeys a normal distribution.
+ */
+class TgaxVirtualDesktop : public SourceApplication
+{
+  public:
+    /**
+     * @brief Get the type ID.
+     * @return the object TypeId
+     */
+    static TypeId GetTypeId();
+
+    TgaxVirtualDesktop();
+    ~TgaxVirtualDesktop() override;
+
+    /// Model presets enumeration
+    enum class ModelPresets : uint8_t
+    {
+        CUSTOM = 0, //< Custom traffic model (by default, load parameters of DL model presets)
+        DOWNLINK,   //< DL model presets
+        UPLINK      //< UL model presets
+    };
+
+    int64_t AssignStreams(int64_t stream) override;
+
+  protected:
+    void DoInitialize() override;
+
+  private:
+    void DoStartApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void CancelEvents() override;
+
+    /**
+     * @brief Set the parameters of the normal random variable used to generate the VDI packet
+     * sizes.
+     *
+     * @param params a vector of pairs (mean, standard deviation) for each mode of the normal
+     * distribution used to generate the VDI packet sizes
+     */
+    void SetParametersPacketSize(const std::vector<std::pair<double, double>>& params);
+
+    /**
+     * @brief Get the duration to use to schedule the TX of the next VDI packet.
+     *
+     * @return Get the duration to use to schedule the TX of the next VDI packet
+     */
+    Time GetInterArrival() const;
+
+    /**
+     * @brief Get the size in bytes of the next VDI packet to send.
+     *
+     * @return Get the size in bytes of the next VDI packet to send
+     */
+    uint32_t GetPacketSize() const;
+
+    /**
+     * Schedule the next TX
+     */
+    void ScheduleNext();
+
+    /**
+     * Transmit the next VDI packet
+     */
+    void SendPacket();
+
+    /**
+     * Handle a Data Sent event
+     *
+     * @param socket the connected socket
+     * @param size the amount of transmitted bytes
+     */
+    void TxDone(Ptr<Socket> socket, uint32_t size);
+
+    /**
+     * Handle a Send event
+     *
+     * @param socket the connected socket
+     * @param available the amount of available bytes for transmission
+     */
+    void TxAvailable(Ptr<Socket> socket, uint32_t available);
+
+    bool m_initialPacket{true}; //!< True if the next packet to send is the initial packet
+
+    ModelPresets
+        m_modelPresets; //!< Model presets to use to configure the VDI traffic model parameters
+    Ptr<UniformRandomVariable> m_initialArrivalUniform; //!< Uniform random variable to generate
+                                                        //!< initial packet arrival in nanoseconds
+    Ptr<ExponentialRandomVariable>
+        m_interArrivalExponential; //!< Exponential random variable to generate packet arrival times
+                                   //!< in nanoseconds
+    Ptr<BernoulliRandomVariable> m_dlModeSelection; //!< Uniform random variable to select mode for
+                                                    //!< downlink bimodal distribution
+    std::vector<Ptr<NormalRandomVariable>>
+        m_pktSizeDistributions; //!< Single or multi modal normal random variables to generate
+                                //!< packet sizes in bytes
+
+    EventId m_txEvent;          //!< Event id of pending TX event
+    Ptr<Packet> m_unsentPacket; //!< Unsent packet cached for future attempt
+};
+
+} // namespace ns3
+
+#endif /* TGAX_VIRTUAL_DESKTOP_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/tgax-voip-traffic.cc ns-3.47/src/applications/model/tgax-voip-traffic.cc
--- ns-3.46/src/applications/model/tgax-voip-traffic.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/tgax-voip-traffic.cc	2026-02-16 16:08:49.571229700 -0800
@@ -0,0 +1,362 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "tgax-voip-traffic.h"
+
+#include "ns3/double.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/inet6-socket-address.h"
+#include "ns3/log.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/packet.h"
+#include "ns3/random-variable-stream.h"
+#include "ns3/simulator.h"
+#include "ns3/socket-factory.h"
+#include "ns3/socket.h"
+#include "ns3/uinteger.h"
+
+namespace ns3
+{
+
+NS_LOG_COMPONENT_DEFINE("TgaxVoipTraffic");
+
+NS_OBJECT_ENSURE_REGISTERED(TgaxVoipTraffic);
+
+TypeId
+TgaxVoipTraffic::GetTypeId()
+{
+    static TypeId tid =
+        TypeId("ns3::TgaxVoipTraffic")
+            .SetParent<SourceApplication>()
+            .SetGroupName("Applications")
+            .AddConstructor<TgaxVoipTraffic>()
+            .AddAttribute(
+                "Protocol",
+                "The type of protocol to use. This should be a subclass of ns3::SocketFactory",
+                TypeId::ATTR_GET | TypeId::ATTR_CONSTRUCT, // prevent setting after construction
+                TypeIdValue(TypeId::LookupByName("ns3::PacketSocketFactory")),
+                MakeTypeIdAccessor(&TgaxVoipTraffic::m_protocolTid),
+                MakeTypeIdChecker())
+            .AddAttribute(
+                "ActivePacketPayloadSize",
+                "Size in bytes for payload of packets generated during periods of active talking.",
+                UintegerValue(33),
+                MakeUintegerAccessor(&TgaxVoipTraffic::m_activePacketSize),
+                MakeUintegerChecker<uint32_t>())
+            .AddAttribute(
+                "SilencePacketPayloadSize",
+                "Size in bytes for payload of packets generated during periods of silence.",
+                UintegerValue(7),
+                MakeUintegerAccessor(&TgaxVoipTraffic::m_silencePacketSize),
+                MakeUintegerChecker<uint32_t>())
+            .AddAttribute("MeanActiveStateDuration",
+                          "Mean duration of active/talking state.",
+                          TimeValue(MilliSeconds(1250)),
+                          MakeTimeAccessor(&TgaxVoipTraffic::SetActiveExponentialMean),
+                          MakeTimeChecker())
+            .AddAttribute("MeanInactiveStateDuration",
+                          "Mean duration of inactive/silence state.",
+                          TimeValue(MilliSeconds(1250)),
+                          MakeTimeAccessor(&TgaxVoipTraffic::SetInactiveExponentialMean),
+                          MakeTimeChecker())
+            .AddAttribute("VoiceEncoderInterval",
+                          "Interval between generation of voice packets.",
+                          TimeValue(MilliSeconds(20)),
+                          MakeTimeAccessor(&TgaxVoipTraffic::m_voiceInterval),
+                          MakeTimeChecker())
+            .AddAttribute("SilenceEncoderInterval",
+                          "Interval between generation of silence packets. "
+                          "This implementation requires the value of this attribute to be a "
+                          "multiple of VoiceEncoderInterval",
+                          TimeValue(MilliSeconds(160)),
+                          MakeTimeAccessor(&TgaxVoipTraffic::m_silenceInterval),
+                          MakeTimeChecker())
+            .AddAttribute(
+                "VoiceToSilenceProbability",
+                "The probability to transition from active talking state to silence state.",
+                DoubleValue(0.016),
+                MakeDoubleAccessor(&TgaxVoipTraffic::m_activeToInactive),
+                MakeDoubleChecker<double>(0, 1))
+            .AddAttribute(
+                "SilenceToVoiceProbability",
+                "The probability to transition from silence state to active talking state.",
+                DoubleValue(0.016),
+                MakeDoubleAccessor(&TgaxVoipTraffic::m_inactiveToActive),
+                MakeDoubleChecker<double>(0, 1))
+            .AddAttribute("ScaleDelayJitter",
+                          "Scale of laplacian distribution used to calculate delay jitter for "
+                          "generated packets.",
+                          TimeValue(MicroSeconds(5110)),
+                          MakeTimeAccessor(&TgaxVoipTraffic::m_delayJitterScale),
+                          MakeTimeChecker())
+            .AddAttribute("BoundDelayJitter",
+                          "Bound of laplacian distribution used to calculate delay jitter for "
+                          "generated packets. For no jitter, set this attribute to zero.",
+                          TimeValue(MilliSeconds(80)),
+                          MakeTimeAccessor(&TgaxVoipTraffic::m_delayJitterBound),
+                          MakeTimeChecker())
+            .AddTraceSource(
+                "TxWithJitter",
+                "A VoIP packet is sent, this trace also reports the jitter applied to the packet",
+                MakeTraceSourceAccessor(&TgaxVoipTraffic::m_txJitterTrace),
+                "ns3::TgaxVoipTraffic::TxTracedCallback")
+            .AddTraceSource("StateUpdate",
+                            "Voice activity state updated",
+                            MakeTraceSourceAccessor(&TgaxVoipTraffic::m_stateUpdate),
+                            "ns3::TgaxVoipTraffic::StateUpdatedCallback");
+    return tid;
+}
+
+TgaxVoipTraffic::TgaxVoipTraffic()
+    : m_inactiveExponential{CreateObject<ExponentialRandomVariable>()},
+      m_activeExponential{CreateObject<ExponentialRandomVariable>()},
+      m_inactiveUniform{CreateObject<UniformRandomVariable>()},
+      m_activeUniform{CreateObject<UniformRandomVariable>()},
+      m_delayJitterLaplacian{CreateObject<LaplacianRandomVariable>()}
+{
+    NS_LOG_FUNCTION(this);
+}
+
+TgaxVoipTraffic::~TgaxVoipTraffic()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+int64_t
+TgaxVoipTraffic::AssignStreams(int64_t stream)
+{
+    NS_LOG_FUNCTION(this << stream);
+    auto currentStream = stream;
+    m_inactiveExponential->SetStream(currentStream++);
+    m_activeExponential->SetStream(currentStream++);
+    m_inactiveUniform->SetStream(currentStream++);
+    m_activeUniform->SetStream(currentStream++);
+    m_delayJitterLaplacian->SetStream(currentStream++);
+    return (currentStream - stream);
+}
+
+void
+TgaxVoipTraffic::DoInitialize()
+{
+    NS_LOG_FUNCTION(this);
+    SourceApplication::DoInitialize();
+    m_delayJitterLaplacian->SetAttribute("Scale",
+                                         DoubleValue(m_delayJitterScale.GetMicroSeconds()));
+    m_delayJitterLaplacian->SetAttribute("Bound",
+                                         DoubleValue(m_delayJitterBound.GetMicroSeconds()));
+}
+
+void
+TgaxVoipTraffic::SetActiveExponentialMean(Time mean)
+{
+    NS_LOG_FUNCTION(this << mean);
+    m_activeExponential->SetAttribute("Mean", DoubleValue(mean.GetMilliSeconds()));
+}
+
+void
+TgaxVoipTraffic::SetInactiveExponentialMean(Time mean)
+{
+    NS_LOG_FUNCTION(this << mean);
+    m_inactiveExponential->SetAttribute("Mean", DoubleValue(mean.GetMilliSeconds()));
+}
+
+void
+TgaxVoipTraffic::DoStartApplication()
+{
+    NS_LOG_FUNCTION(this);
+
+    m_socket->SetAllowBroadcast(true);
+    m_socket->ShutdownRecv();
+
+    if (m_connected)
+    {
+        ScheduleNext();
+    }
+}
+
+void
+TgaxVoipTraffic::CancelEvents()
+{
+    NS_LOG_FUNCTION(this);
+    for (auto& event : m_txPacketEvents)
+    {
+        event.second.Cancel();
+    }
+    m_txPacketEvents.clear();
+    m_stateUpdateEvent.Cancel();
+    m_remainingEncodingDuration = GetEncoderFrameDuration();
+}
+
+Time
+TgaxVoipTraffic::GetEncoderFrameDuration() const
+{
+    return (m_currentState == VoiceActivityState::ACTIVE_TALKING) ? m_voiceInterval
+                                                                  : m_silenceInterval;
+}
+
+Time
+TgaxVoipTraffic::GetStateUpdateInterval() const
+{
+    NS_ABORT_MSG_IF(
+        !(m_silenceInterval % m_voiceInterval).IsZero(),
+        "Silence encoder frame duration should be a multiple of voice encoder frame duration");
+    return m_voiceInterval;
+}
+
+void
+TgaxVoipTraffic::ScheduleNext()
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT(!m_stateUpdateEvent.IsPending());
+    m_stateUpdateEvent =
+        Simulator::Schedule(GetStateUpdateInterval(), &TgaxVoipTraffic::UpdateState, this);
+}
+
+void
+TgaxVoipTraffic::SendPacket(uint64_t eventId, Ptr<Packet> packet, Time jitter)
+{
+    const auto size = packet->GetSize();
+    NS_LOG_FUNCTION(this << eventId << size << jitter);
+
+    auto it = m_txPacketEvents.find(eventId);
+    NS_ASSERT(it != m_txPacketEvents.end());
+    NS_ASSERT(it->second.IsExpired());
+
+    const auto actualSize = static_cast<uint32_t>(m_socket->Send(packet));
+    NS_ABORT_IF(actualSize != size);
+    m_txTrace(packet);
+    m_txJitterTrace(packet, jitter);
+
+    if (InetSocketAddress::IsMatchingType(m_peer))
+    {
+        NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " voip traffic source sent "
+                               << size << " bytes to "
+                               << InetSocketAddress::ConvertFrom(m_peer).GetIpv4() << " port "
+                               << InetSocketAddress::ConvertFrom(m_peer).GetPort());
+    }
+    else if (Inet6SocketAddress::IsMatchingType(m_peer))
+    {
+        NS_LOG_INFO("At time " << Simulator::Now().As(Time::S) << " voip traffic source sent "
+                               << size << " bytes to "
+                               << Inet6SocketAddress::ConvertFrom(m_peer).GetIpv6() << " port "
+                               << Inet6SocketAddress::ConvertFrom(m_peer).GetPort());
+    }
+
+    m_txPacketEvents.erase(it);
+}
+
+void
+TgaxVoipTraffic::UpdateState()
+{
+    NS_LOG_FUNCTION(this);
+
+    m_remainingStateDuration -= GetStateUpdateInterval();
+    m_remainingEncodingDuration -= GetStateUpdateInterval();
+    NS_ASSERT(!m_remainingEncodingDuration.IsStrictlyNegative());
+
+    auto newState = m_currentState;
+    if (m_currentState == VoiceActivityState::INACTIVE_SILENCE)
+    {
+        if (m_inactiveUniform->GetValue() >= (1 - m_inactiveToActive))
+        {
+            newState = VoiceActivityState::ACTIVE_TALKING;
+        }
+    }
+    else
+    {
+        if (m_activeUniform->GetValue() >= (1 - m_activeToInactive))
+        {
+            newState = VoiceActivityState::INACTIVE_SILENCE;
+        }
+    }
+    m_pendingStateTransition = m_pendingStateTransition || (newState != m_currentState);
+
+    if (!m_remainingStateDuration.IsStrictlyPositive() && m_remainingEncodingDuration.IsZero())
+    {
+        if (m_pendingStateTransition)
+        {
+            newState = (m_currentState == VoiceActivityState::ACTIVE_TALKING)
+                           ? VoiceActivityState::INACTIVE_SILENCE
+                           : VoiceActivityState::ACTIVE_TALKING;
+        }
+        m_remainingStateDuration = (newState == VoiceActivityState::ACTIVE_TALKING)
+                                       ? MilliSeconds(m_activeExponential->GetValue())
+                                       : MilliSeconds(m_inactiveExponential->GetValue());
+        m_stateUpdate(newState, m_remainingStateDuration);
+        if ((newState != m_currentState))
+        {
+            NS_LOG_INFO("At time " << Simulator::Now().As(Time::S)
+                                   << " voip voice activity state changed from " << m_currentState
+                                   << " to " << newState << " for "
+                                   << m_remainingStateDuration.As(Time::MS));
+            m_currentState = newState;
+        }
+        else
+        {
+            NS_LOG_INFO("At time " << Simulator::Now().As(Time::S)
+                                   << " voip voice activity state unchanged to " << m_currentState
+                                   << " for " << m_remainingStateDuration.As(Time::MS));
+        }
+        m_pendingStateTransition = false;
+    }
+
+    if (m_remainingEncodingDuration.IsZero())
+    {
+        m_remainingEncodingDuration = GetEncoderFrameDuration();
+        auto delay = GetEncoderFrameDuration();
+        Time jitter{};
+        if (!m_delayJitterBound.IsZero())
+        {
+            jitter = MicroSeconds(static_cast<int64x64_t>(m_delayJitterLaplacian->GetValue()));
+            // Add the bound to always have a positive value as suggested in Robert Novak et al.,
+            // "Downlink VoIP Packet Delay Jitter Model".
+            delay += jitter + m_delayJitterBound;
+        }
+        const auto payloadSize = (m_currentState == VoiceActivityState::ACTIVE_TALKING)
+                                     ? m_activePacketSize
+                                     : m_silencePacketSize;
+        auto packet = Create<Packet>(payloadSize);
+        m_txPacketEvents[m_nextEventId] = Simulator::Schedule(delay,
+                                                              &TgaxVoipTraffic::SendPacket,
+                                                              this,
+                                                              m_nextEventId,
+                                                              packet,
+                                                              jitter);
+        ++m_nextEventId;
+    }
+
+    ScheduleNext();
+}
+
+void
+TgaxVoipTraffic::DoConnectionSucceeded(Ptr<Socket> socket)
+{
+    NS_LOG_FUNCTION(this << socket);
+    ScheduleNext();
+}
+
+std::ostream&
+operator<<(std::ostream& os, TgaxVoipTraffic::VoiceActivityState state)
+{
+    switch (state)
+    {
+    case TgaxVoipTraffic::VoiceActivityState::INACTIVE_SILENCE:
+        os << "Silence";
+        break;
+    case TgaxVoipTraffic::VoiceActivityState::ACTIVE_TALKING:
+        os << "Active talking";
+        break;
+    default:
+        NS_FATAL_ERROR("Unknown voice activity state");
+    }
+    return os;
+}
+
+} // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/tgax-voip-traffic.h ns-3.47/src/applications/model/tgax-voip-traffic.h
--- ns-3.46/src/applications/model/tgax-voip-traffic.h	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/model/tgax-voip-traffic.h	2026-02-16 16:08:49.571229700 -0800
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#ifndef TGAX_VOIP_TRAFFIC_H
+#define TGAX_VOIP_TRAFFIC_H
+
+#include "source-application.h"
+
+#include "ns3/event-id.h"
+#include "ns3/ptr.h"
+#include "ns3/traced-callback.h"
+
+#include <map>
+
+namespace ns3
+{
+
+class ExponentialRandomVariable;
+class UniformRandomVariable;
+class LaplacianRandomVariable;
+
+/**
+ * @ingroup applications
+ * @brief Generate VoIP traffic.
+ *
+ * This voip traffic generator follows requirements from IEEE 802.11-14/0571r12 - 11ax Evaluation
+ * Methodology (see applications documentation for full citation).
+ *
+ * The VoIP traffic alternates between periods of active talking and silence,
+ * with given probabilities to transition from one state to another.
+ * These state updates are assumed to be done at the speech encoder frame rate.
+ *
+ * Fixed-size VoIP packets are generated at every encoder frame interval,
+ * plus a random network packet arrival delay jitter (if BoundDelayJitter is non-zero).
+ * The size of these packets also depend on the current state.
+ *
+ * The VoIP model from the reference relies on UDP with compressed protocol headers.
+ * Since compressed protocol headers are not supported in the simulator, a packet socket
+ * is used instead, allowing user to tune payload sizes by adding up the size of the compressed
+ * headers.
+ *
+ * This model can also be used with usual UDP or TCP sockets. For the later, user should be warned
+ * that the model does not provide any mechanism when TX buffer is full.
+ */
+class TgaxVoipTraffic : public SourceApplication
+{
+  public:
+    /**
+     * @brief Get the type ID.
+     * @return the object TypeId
+     */
+    static TypeId GetTypeId();
+
+    TgaxVoipTraffic();
+    ~TgaxVoipTraffic() override;
+
+    /**
+     * Voice activity states
+     */
+    enum class VoiceActivityState : uint8_t
+    {
+        INACTIVE_SILENCE = 0, //< Inactive/silence state
+        ACTIVE_TALKING = 1,   //< Active/talking state
+    };
+
+    int64_t AssignStreams(int64_t stream) override;
+
+  protected:
+    void DoInitialize() override;
+
+  private:
+    void DoStartApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void CancelEvents() override;
+
+    /**
+     * Set the mean of the exponential distribution used to calculate durations of active/talking
+     * state
+     * @param mean the mean value
+     */
+    void SetActiveExponentialMean(Time mean);
+
+    /**
+     * Set the mean of the exponential distribution used to calculate durations of inactive/silent
+     * state
+     * @param mean the mean value
+     */
+    void SetInactiveExponentialMean(Time mean);
+
+    /**
+     * Update voice activity state
+     */
+    void UpdateState();
+
+    /**
+     * Schedule the next state update
+     */
+    void ScheduleNext();
+
+    /**
+     * Transmit one VoIP packet
+     * @param eventId the event ID
+     * @param packet the packet to transmit
+     * @param jitter the delay jitter applied to that packet
+     */
+    void SendPacket(uint64_t eventId, Ptr<Packet> packet, Time jitter);
+
+    /**
+     * Get the duration to encode a frame based on the current state
+     *
+     * @return the duration to encode a frame
+     */
+    Time GetEncoderFrameDuration() const;
+
+    /**
+     * Get the interval between two state updates
+     *
+     * @return the interval between two state updates
+     */
+    Time GetStateUpdateInterval() const;
+
+    uint32_t m_activePacketSize;  //!< Size in bytes for payload of active packets
+    uint32_t m_silencePacketSize; //!< Size in bytes for payload of silence packets
+    Time m_voiceInterval;         //!< Interval between generation of voice packets
+    Time m_silenceInterval;       //!< Interval between generation of silence packets
+    double m_activeToInactive;    //!< Probability to transition from active/talking state to
+                                  //!< inactive/silence state
+    double m_inactiveToActive;    //!< Probability to transition from inactive/silence state to
+                                  //!< active/talking state
+    Time m_delayJitterScale; //!< Scale of laplacian distribution used to calculate delay jitter
+    Time m_delayJitterBound; //!< Bound of laplacian distribution used to calculate delay jitter
+
+    Ptr<ExponentialRandomVariable>
+        m_inactiveExponential; //!< Exponential random variable to generate inactive/silent state
+                               //!< durations
+    Ptr<ExponentialRandomVariable> m_activeExponential; //!< Exponential random variable to generate
+                                                        //!< active/talking state durations
+    Ptr<UniformRandomVariable> m_inactiveUniform; //!< Uniform random variable to generate state
+                                                  //!< transitions from inactive state
+    Ptr<UniformRandomVariable> m_activeUniform;   //!< Uniform random variable to generate state
+                                                  //!< transitions from active state
+    Ptr<LaplacianRandomVariable>
+        m_delayJitterLaplacian; //!< Laplacian random variable to generate delay jitter
+
+    VoiceActivityState m_currentState{
+        VoiceActivityState::INACTIVE_SILENCE}; //!< Hold the current voice activity state
+    bool m_pendingStateTransition{false}; //!< Flag whether a state transition should once duration
+                                          //!< of current state has elapsed
+    Time m_remainingStateDuration;        //!< The remaining duration in the current state
+    Time m_remainingEncodingDuration;     //!< The remaining duration to encode the current frame
+
+    EventId m_stateUpdateEvent; //!< Event ID of pending state update event scheduling
+
+    std::map<uint64_t, EventId> m_txPacketEvents; //!< Event IDs of pending TX events
+    uint64_t m_nextEventId{0};                    //!< The next event ID
+
+    /**
+     * TracedCallback signature for packet and jitter.
+     *
+     * @param [in] packet The packet.
+     * @param [in] jitter The networking jitter.
+     */
+    typedef void (*TxTracedCallback)(Ptr<const Packet> packet, Time jitter);
+
+    /**
+     * TracedCallback signature for state change.
+     *
+     * @param [in] state The new state.
+     * @param [in] duration The duration of the state.
+     */
+    typedef void (*StateUpdatedCallback)(VoiceActivityState state, Time duration);
+
+    /// Traced Callback: transmitted packets and their jitters.
+    TracedCallback<Ptr<const Packet>, Time> m_txJitterTrace;
+
+    /// Traced Callback: voice activity state updated.
+    TracedCallback<VoiceActivityState, Time> m_stateUpdate;
+};
+
+/**
+ * Serialize voice activity state to ostream in a human-readable form.
+ *
+ * @param os std::ostream
+ * @param state the voice activity state
+ * @return std::ostream
+ */
+std::ostream& operator<<(std::ostream& os, TgaxVoipTraffic::VoiceActivityState state);
+
+} // namespace ns3
+
+#endif /* TGAX_VOIP_TRAFFIC_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/three-gpp-http-client.cc ns-3.47/src/applications/model/three-gpp-http-client.cc
--- ns-3.46/src/applications/model/three-gpp-http-client.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/three-gpp-http-client.cc	2026-02-16 16:08:49.571229700 -0800
@@ -30,19 +30,11 @@
 NS_OBJECT_ENSURE_REGISTERED(ThreeGppHttpClient);
 
 ThreeGppHttpClient::ThreeGppHttpClient()
-    : m_state{NOT_STARTED},
-      m_socket{nullptr},
-      m_objectBytesToBeReceived{0},
-      m_objectClientTs{},
-      m_objectServerTs{},
-      m_embeddedObjectsToBeRequested{0},
-      m_pageLoadStartTs{},
-      m_numberEmbeddedObjectsRequested{0},
-      m_numberBytesPage{0},
-      m_httpVariables{CreateObject<ThreeGppHttpVariables>()},
-      m_peerPort{}
+    : SourceApplication(false),
+      m_httpVariables{CreateObject<ThreeGppHttpVariables>()}
 {
     NS_LOG_FUNCTION(this);
+    m_protocolTid = TypeId::LookupByName("ns3::TcpSocketFactory");
 }
 
 // static
@@ -88,10 +80,6 @@
                             "Connection to the destination web server is closed.",
                             MakeTraceSourceAccessor(&ThreeGppHttpClient::m_connectionClosedTrace),
                             "ns3::ThreeGppHttpClient::TracedCallback")
-            .AddTraceSource("Tx",
-                            "General trace for sending a packet of any kind.",
-                            MakeTraceSourceAccessor(&ThreeGppHttpClient::m_txTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource(
                 "TxMainObjectRequest",
                 "Sent a request for a main object.",
@@ -169,12 +157,6 @@
     }
 }
 
-Ptr<Socket>
-ThreeGppHttpClient::GetSocket() const
-{
-    return m_socket;
-}
-
 ThreeGppHttpClient::State_t
 ThreeGppHttpClient::GetState() const
 {
@@ -220,14 +202,15 @@
 
     if (!Simulator::IsFinished())
     {
-        StopApplication();
+        CancelEvents();
+        CloseSocket();
     }
 
-    Application::DoDispose(); // Chain up.
+    SourceApplication::DoDispose(); // Chain up.
 }
 
 void
-ThreeGppHttpClient::StartApplication()
+ThreeGppHttpClient::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
 
@@ -243,20 +226,14 @@
 }
 
 void
-ThreeGppHttpClient::StopApplication()
+ThreeGppHttpClient::DoStopApplication()
 {
     NS_LOG_FUNCTION(this);
-
     SwitchToState(STOPPED);
-    CancelAllPendingEvents();
-    m_socket->Close();
-    m_socket->SetConnectCallback(MakeNullCallback<void, Ptr<Socket>>(),
-                                 MakeNullCallback<void, Ptr<Socket>>());
-    m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
 }
 
 void
-ThreeGppHttpClient::ConnectionSucceededCallback(Ptr<Socket> socket)
+ThreeGppHttpClient::DoConnectionSucceeded(Ptr<Socket> socket)
 {
     NS_LOG_FUNCTION(this << socket);
 
@@ -273,7 +250,7 @@
 }
 
 void
-ThreeGppHttpClient::ConnectionFailedCallback(Ptr<Socket> socket)
+ThreeGppHttpClient::DoConnectionFailed(Ptr<Socket> socket)
 {
     NS_LOG_FUNCTION(this << socket);
 
@@ -292,7 +269,7 @@
 {
     NS_LOG_FUNCTION(this << socket);
 
-    CancelAllPendingEvents();
+    CancelEvents();
 
     if (socket->GetErrno() != Socket::ERROR_NOTERROR)
     {
@@ -311,7 +288,7 @@
 {
     NS_LOG_FUNCTION(this << socket);
 
-    CancelAllPendingEvents();
+    CancelEvents();
     if (socket->GetErrno() != Socket::ERROR_NOTERROR)
     {
         NS_LOG_ERROR(this << " Connection has been terminated,"
@@ -380,57 +357,8 @@
         NS_FATAL_ERROR("Invalid state " << GetStateString() << " for OpenConnection().");
     }
 
-    m_socket = Socket::CreateSocket(GetNode(), TcpSocketFactory::GetTypeId());
-    NS_ABORT_MSG_IF(m_peer.IsInvalid(), "Remote address not properly set");
-    if (!m_local.IsInvalid())
-    {
-        NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
-                         InetSocketAddress::IsMatchingType(m_local)) ||
-                            (InetSocketAddress::IsMatchingType(m_peer) &&
-                             Inet6SocketAddress::IsMatchingType(m_local)),
-                        "Incompatible peer and local address IP version");
-    }
-    if (InetSocketAddress::IsMatchingType(m_peer))
-    {
-        const auto ret [[maybe_unused]] =
-            m_local.IsInvalid() ? m_socket->Bind() : m_socket->Bind(m_local);
-        NS_LOG_DEBUG(this << " Bind() return value= " << ret
-                          << " GetErrNo= " << m_socket->GetErrno() << ".");
-
-        const auto ipv4 = InetSocketAddress::ConvertFrom(m_peer).GetIpv4();
-        const auto port = InetSocketAddress::ConvertFrom(m_peer).GetPort();
-        NS_LOG_INFO(this << " Connecting to " << ipv4 << " port " << port << " / " << m_peer
-                         << ".");
-        m_socket->SetIpTos(m_tos);
-    }
-    else if (Inet6SocketAddress::IsMatchingType(m_peer))
-    {
-        const auto ret [[maybe_unused]] =
-            m_local.IsInvalid() ? m_socket->Bind6() : m_socket->Bind(m_local);
-        NS_LOG_DEBUG(this << " Bind6() return value= " << ret
-                          << " GetErrNo= " << m_socket->GetErrno() << ".");
-
-        const auto ipv6 = Inet6SocketAddress::ConvertFrom(m_peer).GetIpv6();
-        const auto port = Inet6SocketAddress::ConvertFrom(m_peer).GetPort();
-        NS_LOG_INFO(this << " Connecting to " << ipv6 << " port " << port << " / " << m_peer
-                         << ".");
-    }
-    else
-    {
-        NS_ASSERT_MSG(false, "Incompatible address type: " << m_peer);
-    }
-
-    const auto ret [[maybe_unused]] = m_socket->Connect(m_peer);
-    NS_LOG_DEBUG(this << " Connect() return value= " << ret << " GetErrNo= " << m_socket->GetErrno()
-                      << ".");
-
-    NS_ASSERT_MSG(m_socket, "Failed creating socket.");
-
     SwitchToState(CONNECTING);
 
-    m_socket->SetConnectCallback(
-        MakeCallback(&ThreeGppHttpClient::ConnectionSucceededCallback, this),
-        MakeCallback(&ThreeGppHttpClient::ConnectionFailedCallback, this));
     m_socket->SetCloseCallbacks(MakeCallback(&ThreeGppHttpClient::NormalCloseCallback, this),
                                 MakeCallback(&ThreeGppHttpClient::ErrorCloseCallback, this));
     m_socket->SetRecvCallback(MakeCallback(&ThreeGppHttpClient::ReceivedDataCallback, this));
@@ -772,7 +700,7 @@
 }
 
 void
-ThreeGppHttpClient::CancelAllPendingEvents()
+ThreeGppHttpClient::CancelEvents()
 {
     NS_LOG_FUNCTION(this);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/three-gpp-http-client.h ns-3.47/src/applications/model/three-gpp-http-client.h
--- ns-3.46/src/applications/model/three-gpp-http-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/three-gpp-http-client.h	2026-02-16 16:08:49.571229700 -0800
@@ -21,7 +21,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 class ThreeGppHttpVariables;
 
@@ -113,12 +112,6 @@
      */
     static TypeId GetTypeId();
 
-    /**
-     * Returns a pointer to the associated socket.
-     * @return Pointer to the associated socket.
-     */
-    Ptr<Socket> GetSocket() const;
-
     /// The possible states of the application.
     enum State_t
     {
@@ -183,8 +176,11 @@
     void DoDispose() override;
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void DoStopApplication() override;
+    void DoConnectionSucceeded(Ptr<Socket> socket) override;
+    void DoConnectionFailed(Ptr<Socket> socket) override;
+    void CancelEvents() override;
 
     /**
      * @brief set the remote port (temporary function until deprecated attributes are removed)
@@ -193,19 +189,6 @@
     void SetPort(uint16_t port);
 
     // SOCKET CALLBACK METHODS
-
-    /**
-     * Invoked when a connection is established successfully on #m_socket. This
-     * triggers a request for a main object.
-     * @param socket Pointer to the socket where the event originates from.
-     */
-    void ConnectionSucceededCallback(Ptr<Socket> socket);
-    /**
-     * Invoked when #m_socket cannot establish a connection with the web server.
-     * Simulation will stop and error will be raised.
-     * @param socket Pointer to the socket where the event originates from.
-     */
-    void ConnectionFailedCallback(Ptr<Socket> socket);
     /**
      * Invoked when connection between #m_socket and the web sever is terminated.
      * Error will be logged, but simulation continues.
@@ -341,12 +324,6 @@
      * The method is invoked after a complete web page has been received.
      */
     void EnterReadingTime();
-    /**
-     * Cancels #m_eventRequestMainObject, #m_eventRequestEmbeddedObject, and
-     * #m_eventParseMainObject. Invoked by StopApplication() and when connection
-     * has been terminated.
-     */
-    void CancelAllPendingEvents();
 
     /**
      * Change the state of the client. Fires the `StateTransition` trace source.
@@ -361,11 +338,9 @@
     void FinishReceivingPage();
 
     /// The current state of the client application. Begins with NOT_STARTED.
-    State_t m_state;
-    /// The socket for sending and receiving packets to/from the web server.
-    Ptr<Socket> m_socket;
+    State_t m_state{NOT_STARTED};
     /// According to the content length specified by the ThreeGppHttpHeader.
-    uint32_t m_objectBytesToBeReceived;
+    uint32_t m_objectBytesToBeReceived{0};
     /// The packet constructed of one or more parts with ThreeGppHttpHeader.
     Ptr<Packet> m_constructedPacket;
     /// The client time stamp of the ThreeGppHttpHeader from the last received packet.
@@ -373,13 +348,13 @@
     /// The server time stamp of the ThreeGppHttpHeader from the last received packet.
     Time m_objectServerTs;
     /// Determined after parsing the main object.
-    uint32_t m_embeddedObjectsToBeRequested;
+    uint32_t m_embeddedObjectsToBeRequested{0};
     /// The time stamp when the page started loading.
     Time m_pageLoadStartTs;
     /// Number of embedded objects to requested in the current page.
-    uint32_t m_numberEmbeddedObjectsRequested;
+    uint32_t m_numberEmbeddedObjectsRequested{0};
     /// Number of bytes received for the current page.
-    uint32_t m_numberBytesPage;
+    uint32_t m_numberBytesPage{0};
 
     // ATTRIBUTES
 
@@ -397,8 +372,6 @@
     ns3::TracedCallback<Ptr<const ThreeGppHttpClient>> m_connectionEstablishedTrace;
     /// The `ConnectionClosed` trace source.
     ns3::TracedCallback<Ptr<const ThreeGppHttpClient>> m_connectionClosedTrace;
-    /// The `Tx` trace source.
-    ns3::TracedCallback<Ptr<const Packet>> m_txTrace;
     /// The `TxMainObjectRequest` trace source.
     ns3::TracedCallback<Ptr<const Packet>> m_txMainObjectRequestTrace;
     /// The `TxEmbeddedObjectRequest` trace source.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/three-gpp-http-header.cc ns-3.47/src/applications/model/three-gpp-http-header.cc
--- ns-3.46/src/applications/model/three-gpp-http-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/three-gpp-http-header.cc	2026-02-16 16:08:49.571229700 -0800
@@ -22,11 +22,7 @@
 NS_OBJECT_ENSURE_REGISTERED(ThreeGppHttpHeader);
 
 ThreeGppHttpHeader::ThreeGppHttpHeader()
-    : Header(),
-      m_contentType(NOT_SET),
-      m_contentLength(0),
-      m_clientTs(0),
-      m_serverTs(0)
+    : Header()
 {
     NS_LOG_FUNCTION(this);
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/three-gpp-http-header.h ns-3.47/src/applications/model/three-gpp-http-header.h
--- ns-3.46/src/applications/model/three-gpp-http-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/three-gpp-http-header.h	2026-02-16 16:08:49.571229700 -0800
@@ -115,10 +115,10 @@
     Time GetServerTs() const;
 
   private:
-    uint16_t m_contentType;   //!<" Content type field in integer format.
-    uint32_t m_contentLength; //!<" Content length field (in bytes unit).
-    uint64_t m_clientTs;      //!<" Client time stamp field (in time step unit).
-    uint64_t m_serverTs;      //!<" Server time stamp field (in time step unit).
+    uint16_t m_contentType{0};   //!< Content type field in integer format.
+    uint32_t m_contentLength{0}; //!< Content length field (in bytes unit).
+    uint64_t m_clientTs{0};      //!< Client time stamp field (in time step unit).
+    uint64_t m_serverTs{0};      //!< Server time stamp field (in time step unit).
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/three-gpp-http-server.cc ns-3.47/src/applications/model/three-gpp-http-server.cc
--- ns-3.46/src/applications/model/three-gpp-http-server.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/three-gpp-http-server.cc	2026-02-16 16:08:49.571229700 -0800
@@ -34,15 +34,13 @@
 
 ThreeGppHttpServer::ThreeGppHttpServer()
     : SinkApplication(HTTP_DEFAULT_PORT),
-      m_state{NOT_STARTED},
-      m_initialSocket{nullptr},
       m_txBuffer{Create<ThreeGppHttpServerTxBuffer>()},
       m_httpVariables{CreateObject<ThreeGppHttpVariables>()},
-      m_optPort{},
       m_mtuSize{m_httpVariables->GetMtuSize()}
 {
     NS_LOG_FUNCTION(this);
     NS_LOG_INFO(this << " MTU size for this server application is " << m_mtuSize << " bytes.");
+    m_protocolTid = TypeId::LookupByName("ns3::TcpSocketFactory");
 }
 
 // static
@@ -109,10 +107,6 @@
                             "A packet has been sent.",
                             MakeTraceSourceAccessor(&ThreeGppHttpServer::m_txTrace),
                             "ns3::Packet::TracedCallback")
-            .AddTraceSource("Rx",
-                            "A packet has been received.",
-                            MakeTraceSourceAccessor(&ThreeGppHttpServer::m_rxTrace),
-                            "ns3::Packet::AddressTracedCallback")
             .AddTraceSource("RxWithAddresses",
                             "A packet has been received.",
                             MakeTraceSourceAccessor(&ThreeGppHttpServer::m_rxTraceWithAddresses),
@@ -172,7 +166,7 @@
 Ptr<Socket>
 ThreeGppHttpServer::GetSocket() const
 {
-    return m_initialSocket;
+    return m_socket;
 }
 
 ThreeGppHttpServer::State_t
@@ -212,14 +206,14 @@
 
     if (!Simulator::IsFinished())
     {
-        StopApplication();
+        CloseAllSockets();
     }
 
-    Application::DoDispose(); // Chain up.
+    SinkApplication::DoDispose(); // Chain up.
 }
 
 void
-ThreeGppHttpServer::StartApplication()
+ThreeGppHttpServer::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
 
@@ -229,68 +223,50 @@
     }
 
     m_httpVariables->Initialize();
-    if (!m_initialSocket)
+
+    m_socket->SetAttribute("SegmentSize", UintegerValue(m_mtuSize));
+
+    NS_ABORT_MSG_IF(m_local.IsInvalid(), "Local address not properly set");
+    if (InetSocketAddress::IsMatchingType(m_local))
     {
-        // Find the current default MTU value of TCP sockets.
-        Ptr<const ns3::AttributeValue> previousSocketMtu;
-        const TypeId tcpSocketTid = TcpSocket::GetTypeId();
-        for (uint32_t i = 0; i < tcpSocketTid.GetAttributeN(); i++)
-        {
-            TypeId::AttributeInformation attrInfo = tcpSocketTid.GetAttribute(i);
-            if (attrInfo.name == "SegmentSize")
-            {
-                previousSocketMtu = attrInfo.initialValue;
-            }
-        }
+        const auto ipv4 [[maybe_unused]] = InetSocketAddress::ConvertFrom(m_local).GetIpv4();
+        m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
+        NS_LOG_INFO(this << " Binding on " << ipv4 << " port " << m_port << " / " << m_local
+                         << ".");
+    }
+    else if (Inet6SocketAddress::IsMatchingType(m_local))
+    {
+        const auto ipv6 [[maybe_unused]] = Inet6SocketAddress::ConvertFrom(m_local).GetIpv6();
+        NS_LOG_INFO(this << " Binding on " << ipv6 << " port " << m_port << " / " << m_local
+                         << ".");
+    }
+    else
+    {
+        NS_ABORT_MSG("Incompatible local address");
+    }
 
-        // Creating a TCP socket to connect to the server.
-        m_initialSocket = Socket::CreateSocket(GetNode(), TcpSocketFactory::GetTypeId());
-        m_initialSocket->SetAttribute("SegmentSize", UintegerValue(m_mtuSize));
+    auto ret [[maybe_unused]] = m_socket->Bind(m_local);
+    NS_LOG_DEBUG(this << " Bind() return value= " << ret << " GetErrNo= " << m_socket->GetErrno()
+                      << ".");
 
-        NS_ABORT_MSG_IF(m_local.IsInvalid(), "Local address not properly set");
-        if (InetSocketAddress::IsMatchingType(m_local))
-        {
-            const auto ipv4 [[maybe_unused]] = InetSocketAddress::ConvertFrom(m_local).GetIpv4();
-            m_initialSocket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-            NS_LOG_INFO(this << " Binding on " << ipv4 << " port " << m_port << " / " << m_local
-                             << ".");
-        }
-        else if (Inet6SocketAddress::IsMatchingType(m_local))
-        {
-            const auto ipv6 [[maybe_unused]] = Inet6SocketAddress::ConvertFrom(m_local).GetIpv6();
-            NS_LOG_INFO(this << " Binding on " << ipv6 << " port " << m_port << " / " << m_local
-                             << ".");
-        }
-        else
-        {
-            NS_ABORT_MSG("Incompatible local address");
-        }
+    ret = m_socket->Listen();
+    NS_LOG_DEBUG(this << " Listen () return value= " << ret << " GetErrNo= " << m_socket->GetErrno()
+                      << ".");
 
-        auto ret [[maybe_unused]] = m_initialSocket->Bind(m_local);
-        NS_LOG_DEBUG(this << " Bind() return value= " << ret
-                          << " GetErrNo= " << m_initialSocket->GetErrno() << ".");
-
-        ret = m_initialSocket->Listen();
-        NS_LOG_DEBUG(this << " Listen () return value= " << ret
-                          << " GetErrNo= " << m_initialSocket->GetErrno() << ".");
-
-        NS_ASSERT_MSG(m_initialSocket, "Failed creating socket.");
-        m_initialSocket->SetAcceptCallback(
-            MakeCallback(&ThreeGppHttpServer::ConnectionRequestCallback, this),
-            MakeCallback(&ThreeGppHttpServer::NewConnectionCreatedCallback, this));
-        m_initialSocket->SetCloseCallbacks(
-            MakeCallback(&ThreeGppHttpServer::NormalCloseCallback, this),
-            MakeCallback(&ThreeGppHttpServer::ErrorCloseCallback, this));
-        m_initialSocket->SetRecvCallback(
-            MakeCallback(&ThreeGppHttpServer::ReceivedDataCallback, this));
-        m_initialSocket->SetSendCallback(MakeCallback(&ThreeGppHttpServer::SendCallback, this));
-    }
+    NS_ASSERT_MSG(m_socket, "Failed creating socket.");
+    m_socket->SetAcceptCallback(
+        MakeCallback(&ThreeGppHttpServer::ConnectionRequestCallback, this),
+        MakeCallback(&ThreeGppHttpServer::NewConnectionCreatedCallback, this));
+    m_socket->SetCloseCallbacks(MakeCallback(&ThreeGppHttpServer::NormalCloseCallback, this),
+                                MakeCallback(&ThreeGppHttpServer::ErrorCloseCallback, this));
+    m_socket->SetRecvCallback(MakeCallback(&ThreeGppHttpServer::ReceivedDataCallback, this));
+    m_socket->SetSendCallback(MakeCallback(&ThreeGppHttpServer::SendCallback, this));
 
     SwitchToState(STARTED);
 }
 
 void
-ThreeGppHttpServer::StopApplication()
+ThreeGppHttpServer::DoStopApplication()
 {
     NS_LOG_FUNCTION(this);
 
@@ -298,18 +274,6 @@
 
     // Close all accepted sockets.
     m_txBuffer->CloseAllSockets();
-
-    // Stop listening.
-    if (m_initialSocket)
-    {
-        m_initialSocket->Close();
-        m_initialSocket->SetAcceptCallback(MakeNullCallback<bool, Ptr<Socket>, const Address&>(),
-                                           MakeNullCallback<void, Ptr<Socket>, const Address&>());
-        m_initialSocket->SetCloseCallbacks(MakeNullCallback<void, Ptr<Socket>>(),
-                                           MakeNullCallback<void, Ptr<Socket>>());
-        m_initialSocket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-        m_initialSocket->SetSendCallback(MakeNullCallback<void, Ptr<Socket>, uint32_t>());
-    }
 }
 
 bool
@@ -350,7 +314,7 @@
 {
     NS_LOG_FUNCTION(this << socket);
 
-    if (socket == m_initialSocket)
+    if (socket == m_socket)
     {
         if (m_state == STARTED)
         {
@@ -386,7 +350,7 @@
 {
     NS_LOG_FUNCTION(this << socket);
 
-    if (socket == m_initialSocket)
+    if (socket == m_socket)
     {
         if (m_state == STARTED)
         {
@@ -436,6 +400,7 @@
         packet->PeekHeader(httpHeader);
 
         // Fire trace sources.
+        m_rxTraceWithoutAddress(packet);
         m_rxTrace(packet, from);
         m_rxTraceWithAddresses(packet, from, m_local);
         m_rxDelayTrace(Simulator::Now() - httpHeader.GetClientTs(), from);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/three-gpp-http-server.h ns-3.47/src/applications/model/three-gpp-http-server.h
--- ns-3.46/src/applications/model/three-gpp-http-server.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/three-gpp-http-server.h	2026-02-16 16:08:49.571229700 -0800
@@ -27,7 +27,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 class ThreeGppHttpVariables;
 class ThreeGppHttpServerTxBuffer;
@@ -143,15 +142,15 @@
     void DoDispose() override;
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void DoStopApplication() override;
     void SetLocal(const Address& addr) override;
     void SetPort(uint32_t port) override;
 
     // SOCKET CALLBACK METHODS
 
     /**
-     * Invoked when #m_initialSocket receives a connection request.
+     * Invoked when #m_socket receives a connection request.
      * @param socket Pointer to the socket where the event originates from.
      * @param address The address of the remote client where the connection
      *                request comes from.
@@ -179,7 +178,7 @@
      */
     void ErrorCloseCallback(Ptr<Socket> socket);
     /**
-     * Invoked when #m_initialSocket receives some packet data. It will check the
+     * Invoked when #m_socket receives some packet data. It will check the
      * packet for ThreeGppHttpHeader. It also fires the `Rx` trace source.
      *
      * Depending on the type of object requested, the method will trigger
@@ -250,9 +249,7 @@
     void SwitchToState(State_t state);
 
     /// The current state of the client application. Begins with NOT_STARTED.
-    State_t m_state;
-    /// The listening socket, for receiving connection requests from clients.
-    Ptr<Socket> m_initialSocket;
+    State_t m_state{NOT_STARTED};
     /// Pointer to the transmission buffer.
     Ptr<ThreeGppHttpServerTxBuffer> m_txBuffer;
 
@@ -280,8 +277,6 @@
     TracedCallback<uint32_t> m_embeddedObjectTrace;
     /// The `Tx` trace source.
     TracedCallback<Ptr<const Packet>> m_txTrace;
-    /// The `Rx` trace source.
-    TracedCallback<Ptr<const Packet>, const Address&> m_rxTrace;
     /// The `Rx` trace source with the local address.
     TracedCallback<Ptr<const Packet>, const Address&, const Address&> m_rxTraceWithAddresses;
     /// The `RxDelay` trace source.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-client.cc ns-3.47/src/applications/model/udp-client.cc
--- ns-3.46/src/applications/model/udp-client.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-client.cc	2026-02-16 16:08:49.572229900 -0800
@@ -75,10 +75,6 @@
                           UintegerValue(1024),
                           MakeUintegerAccessor(&UdpClient::m_size),
                           MakeUintegerChecker<uint32_t>(12, 65507))
-            .AddTraceSource("Tx",
-                            "A new packet is created and sent",
-                            MakeTraceSourceAccessor(&UdpClient::m_txTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource("TxWithAddresses",
                             "A new packet is created and sent",
                             MakeTraceSourceAccessor(&UdpClient::m_txTraceWithAddresses),
@@ -87,13 +83,10 @@
 }
 
 UdpClient::UdpClient()
-    : m_sent{0},
-      m_totalTx{0},
-      m_socket{nullptr},
-      m_peerPort{},
-      m_sendEvent{}
+    : SourceApplication(false)
 {
     NS_LOG_FUNCTION(this);
+    m_protocolTid = TypeId::LookupByName("ns3::UdpSocketFactory");
 }
 
 UdpClient::~UdpClient()
@@ -164,53 +157,11 @@
 }
 
 void
-UdpClient::StartApplication()
+UdpClient::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
 
-    if (!m_socket)
-    {
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket = Socket::CreateSocket(GetNode(), tid);
-        NS_ABORT_MSG_IF(m_peer.IsInvalid(), "Remote address not properly set");
-        if (!m_local.IsInvalid())
-        {
-            NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
-                             InetSocketAddress::IsMatchingType(m_local)) ||
-                                (InetSocketAddress::IsMatchingType(m_peer) &&
-                                 Inet6SocketAddress::IsMatchingType(m_local)),
-                            "Incompatible peer and local address IP version");
-            if (m_socket->Bind(m_local) == -1)
-            {
-                NS_FATAL_ERROR("Failed to bind socket");
-            }
-        }
-        else
-        {
-            if (InetSocketAddress::IsMatchingType(m_peer))
-            {
-                if (m_socket->Bind() == -1)
-                {
-                    NS_FATAL_ERROR("Failed to bind socket");
-                }
-            }
-            else if (Inet6SocketAddress::IsMatchingType(m_peer))
-            {
-                if (m_socket->Bind6() == -1)
-                {
-                    NS_FATAL_ERROR("Failed to bind socket");
-                }
-            }
-            else
-            {
-                NS_ASSERT_MSG(false, "Incompatible address type: " << m_peer);
-            }
-        }
-        m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-        m_socket->Connect(m_peer);
-        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-        m_socket->SetAllowBroadcast(true);
-    }
+    m_socket->SetAllowBroadcast(true);
 
 #ifdef NS3_LOG_ENABLE
     std::stringstream peerAddressStringStream;
@@ -228,14 +179,7 @@
     m_peerString = peerAddressStringStream.str();
 #endif // NS3_LOG_ENABLE
 
-    m_sendEvent = Simulator::Schedule(Seconds(0), &UdpClient::Send, this);
-}
-
-void
-UdpClient::StopApplication()
-{
-    NS_LOG_FUNCTION(this);
-    Simulator::Cancel(m_sendEvent);
+    m_sendEvent = Simulator::ScheduleNow(&UdpClient::Send, this);
 }
 
 void
@@ -281,6 +225,13 @@
     }
 }
 
+void
+UdpClient::CancelEvents()
+{
+    NS_LOG_FUNCTION(this);
+    Simulator::Cancel(m_sendEvent);
+}
+
 uint64_t
 UdpClient::GetTotalTx() const
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-client.h ns-3.47/src/applications/model/udp-client.h
--- ns-3.46/src/applications/model/udp-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-client.h	2026-02-16 16:08:49.572229900 -0800
@@ -24,7 +24,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 
 /**
@@ -63,8 +62,8 @@
     uint64_t GetTotalTx() const;
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void CancelEvents() override;
 
     /**
      * @brief Send a packet
@@ -89,9 +88,6 @@
      */
     Address GetRemote() const;
 
-    /// Traced Callback: transmitted packets.
-    TracedCallback<Ptr<const Packet>> m_txTrace;
-
     /// Callbacks for tracing the packet Tx events, includes source and destination addresses
     TracedCallback<Ptr<const Packet>, const Address&, const Address&> m_txTraceWithAddresses;
 
@@ -99,9 +95,8 @@
     Time m_interval;  //!< Packet inter-send time
     uint32_t m_size;  //!< Size of the sent packet (including the SeqTsHeader)
 
-    uint32_t m_sent;                    //!< Counter for sent packets
-    uint64_t m_totalTx;                 //!< Total bytes sent
-    Ptr<Socket> m_socket;               //!< Socket
+    uint32_t m_sent{0};                 //!< Counter for sent packets
+    uint64_t m_totalTx{0};              //!< Total bytes sent
     std::optional<uint16_t> m_peerPort; //!< Remote peer port (deprecated) // NS_DEPRECATED_3_44
     EventId m_sendEvent;                //!< Event to send the next packet
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-echo-client.cc ns-3.47/src/applications/model/udp-echo-client.cc
--- ns-3.46/src/applications/model/udp-echo-client.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-echo-client.cc	2026-02-16 16:08:49.572229900 -0800
@@ -67,10 +67,6 @@
                 UintegerValue(100),
                 MakeUintegerAccessor(&UdpEchoClient::SetDataSize, &UdpEchoClient::GetDataSize),
                 MakeUintegerChecker<uint32_t>())
-            .AddTraceSource("Tx",
-                            "A new packet is created and is sent",
-                            MakeTraceSourceAccessor(&UdpEchoClient::m_txTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource("Rx",
                             "A packet has been received",
                             MakeTraceSourceAccessor(&UdpEchoClient::m_rxTrace),
@@ -87,20 +83,15 @@
 }
 
 UdpEchoClient::UdpEchoClient()
-    : m_dataSize{0},
-      m_data{nullptr},
-      m_sent{0},
-      m_socket{nullptr},
-      m_peerPort{},
-      m_sendEvent{}
+    : SourceApplication(false)
 {
     NS_LOG_FUNCTION(this);
+    m_protocolTid = TypeId::LookupByName("ns3::UdpSocketFactory");
 }
 
 UdpEchoClient::~UdpEchoClient()
 {
     NS_LOG_FUNCTION(this);
-    m_socket = nullptr;
 
     delete[] m_data;
     m_data = nullptr;
@@ -170,70 +161,12 @@
 }
 
 void
-UdpEchoClient::StartApplication()
+UdpEchoClient::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
-
-    if (!m_socket)
-    {
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket = Socket::CreateSocket(GetNode(), tid);
-        NS_ABORT_MSG_IF(m_peer.IsInvalid(), "Remote address not properly set");
-        if (!m_local.IsInvalid())
-        {
-            NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
-                             InetSocketAddress::IsMatchingType(m_local)) ||
-                                (InetSocketAddress::IsMatchingType(m_peer) &&
-                                 Inet6SocketAddress::IsMatchingType(m_local)),
-                            "Incompatible peer and local address IP version");
-            if (m_socket->Bind(m_local) == -1)
-            {
-                NS_FATAL_ERROR("Failed to bind socket");
-            }
-        }
-        else
-        {
-            if (InetSocketAddress::IsMatchingType(m_peer))
-            {
-                if (m_socket->Bind() == -1)
-                {
-                    NS_FATAL_ERROR("Failed to bind socket");
-                }
-            }
-            else if (Inet6SocketAddress::IsMatchingType(m_peer))
-            {
-                if (m_socket->Bind6() == -1)
-                {
-                    NS_FATAL_ERROR("Failed to bind socket");
-                }
-            }
-            else
-            {
-                NS_ASSERT_MSG(false, "Incompatible address type: " << m_peer);
-            }
-        }
-        m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-        m_socket->Connect(m_peer);
-        m_socket->SetRecvCallback(MakeCallback(&UdpEchoClient::HandleRead, this));
-        m_socket->SetAllowBroadcast(true);
-    }
-
-    ScheduleTransmit(Seconds(0.));
-}
-
-void
-UdpEchoClient::StopApplication()
-{
-    NS_LOG_FUNCTION(this);
-
-    if (m_socket)
-    {
-        m_socket->Close();
-        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-        m_socket = nullptr;
-    }
-
-    Simulator::Cancel(m_sendEvent);
+    m_socket->SetRecvCallback(MakeCallback(&UdpEchoClient::HandleRead, this));
+    m_socket->SetAllowBroadcast(true);
+    ScheduleTransmit(Time(0));
 }
 
 void
@@ -255,7 +188,6 @@
 uint32_t
 UdpEchoClient::GetDataSize() const
 {
-    NS_LOG_FUNCTION(this);
     return m_size;
 }
 
@@ -434,4 +366,11 @@
     }
 }
 
+void
+UdpEchoClient::CancelEvents()
+{
+    NS_LOG_FUNCTION(this);
+    Simulator::Cancel(m_sendEvent);
+}
+
 } // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-echo-client.h ns-3.47/src/applications/model/udp-echo-client.h
--- ns-3.46/src/applications/model/udp-echo-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-echo-client.h	2026-02-16 16:08:49.572229900 -0800
@@ -20,7 +20,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 
 /**
@@ -122,8 +121,8 @@
     void SetFill(uint8_t* fill, uint32_t fillSize, uint32_t dataSize);
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void CancelEvents() override;
 
     /**
      * @brief Set the remote port (temporary function until deprecated attributes are removed)
@@ -166,17 +165,13 @@
     Time m_interval;  //!< Packet inter-send time
     uint32_t m_size;  //!< Size of the sent packet
 
-    uint32_t m_dataSize; //!< packet payload size (must be equal to m_size)
-    uint8_t* m_data;     //!< packet payload data
+    uint32_t m_dataSize{0};   //!< packet payload size (must be equal to m_size)
+    uint8_t* m_data{nullptr}; //!< packet payload data
 
-    uint32_t m_sent;                    //!< Counter for sent packets
-    Ptr<Socket> m_socket;               //!< Socket
+    uint32_t m_sent{0};                 //!< Counter for sent packets
     std::optional<uint16_t> m_peerPort; //!< Remote peer port (deprecated) // NS_DEPRECATED_3_44
     EventId m_sendEvent;                //!< Event to send the next packet
 
-    /// Callbacks for tracing the packet Tx events
-    TracedCallback<Ptr<const Packet>> m_txTrace;
-
     /// Callbacks for tracing the packet Rx events
     TracedCallback<Ptr<const Packet>> m_rxTrace;
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-echo-server.cc ns-3.47/src/applications/model/udp-echo-server.cc
--- ns-3.46/src/applications/model/udp-echo-server.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-echo-server.cc	2026-02-16 16:08:49.572229900 -0800
@@ -41,10 +41,6 @@
                           UintegerValue(0),
                           MakeUintegerAccessor(&UdpEchoServer::m_tos),
                           MakeUintegerChecker<uint8_t>())
-            .AddTraceSource("Rx",
-                            "A packet has been received",
-                            MakeTraceSourceAccessor(&UdpEchoServer::m_rxTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource("RxWithAddresses",
                             "A packet has been received",
                             MakeTraceSourceAccessor(&UdpEchoServer::m_rxTraceWithAddresses),
@@ -53,61 +49,49 @@
 }
 
 UdpEchoServer::UdpEchoServer()
-    : SinkApplication(DEFAULT_PORT),
-      m_socket{nullptr},
-      m_socket6{nullptr}
+    : SinkApplication(DEFAULT_PORT)
 {
     NS_LOG_FUNCTION(this);
+    m_protocolTid = TypeId::LookupByName("ns3::UdpSocketFactory");
 }
 
 UdpEchoServer::~UdpEchoServer()
 {
     NS_LOG_FUNCTION(this);
-    m_socket = nullptr;
-    m_socket6 = nullptr;
 }
 
 void
-UdpEchoServer::StartApplication()
+UdpEchoServer::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
 
-    if (!m_socket)
+    auto local = m_local;
+    if (local.IsInvalid())
+    {
+        local = InetSocketAddress(Ipv4Address::GetAny(), m_port);
+    }
+    if (m_socket->Bind(local) == -1)
     {
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket = Socket::CreateSocket(GetNode(), tid);
-        auto local = m_local;
-        if (local.IsInvalid())
+        NS_FATAL_ERROR("Failed to bind socket");
+    }
+    if (addressUtils::IsMulticast(m_local))
+    {
+        Ptr<UdpSocket> udpSocket = DynamicCast<UdpSocket>(m_socket);
+        if (udpSocket)
         {
-            local = InetSocketAddress(Ipv4Address::GetAny(), m_port);
+            // equivalent to setsockopt (MCAST_JOIN_GROUP)
+            udpSocket->MulticastJoinGroup(0, m_local);
         }
-        if (m_socket->Bind(local) == -1)
+        else
         {
-            NS_FATAL_ERROR("Failed to bind socket");
-        }
-        if (addressUtils::IsMulticast(m_local))
-        {
-            Ptr<UdpSocket> udpSocket = DynamicCast<UdpSocket>(m_socket);
-            if (udpSocket)
-            {
-                // equivalent to setsockopt (MCAST_JOIN_GROUP)
-                udpSocket->MulticastJoinGroup(0, m_local);
-            }
-            else
-            {
-                NS_FATAL_ERROR("Error: Failed to join multicast group");
-            }
+            NS_FATAL_ERROR("Error: Failed to join multicast group");
         }
-        m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-        m_socket->SetRecvCallback(MakeCallback(&UdpEchoServer::HandleRead, this));
     }
+    m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
+    m_socket->SetRecvCallback(MakeCallback(&UdpEchoServer::HandleRead, this));
 
-    if (m_local.IsInvalid() && !m_socket6)
+    if (m_local.IsInvalid())
     {
-        // local address is not specified, so create another socket to also listen to all IPv6
-        // addresses
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket6 = Socket::CreateSocket(GetNode(), tid);
         auto local = Inet6SocketAddress(Ipv6Address::GetAny(), m_port);
         if (m_socket6->Bind(local) == -1)
         {
@@ -131,23 +115,6 @@
 }
 
 void
-UdpEchoServer::StopApplication()
-{
-    NS_LOG_FUNCTION(this);
-
-    if (m_socket)
-    {
-        m_socket->Close();
-        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-    }
-    if (m_socket6)
-    {
-        m_socket6->Close();
-        m_socket6->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-    }
-}
-
-void
 UdpEchoServer::HandleRead(Ptr<Socket> socket)
 {
     NS_LOG_FUNCTION(this << socket);
@@ -157,7 +124,8 @@
     {
         Address localAddress;
         socket->GetSockName(localAddress);
-        m_rxTrace(packet);
+        m_rxTraceWithoutAddress(packet);
+        m_rxTrace(packet, from);
         m_rxTraceWithAddresses(packet, from, localAddress);
         if (InetSocketAddress::IsMatchingType(from))
         {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-echo-server.h ns-3.47/src/applications/model/udp-echo-server.h
--- ns-3.46/src/applications/model/udp-echo-server.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-echo-server.h	2026-02-16 16:08:49.572229900 -0800
@@ -16,7 +16,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 
 /**
@@ -45,8 +44,7 @@
     ~UdpEchoServer() override;
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
 
     /**
      * @brief Handle a packet reception.
@@ -57,12 +55,7 @@
      */
     void HandleRead(Ptr<Socket> socket);
 
-    uint8_t m_tos;         //!< The packets Type of Service
-    Ptr<Socket> m_socket;  //!< Socket
-    Ptr<Socket> m_socket6; //!< IPv6 Socket (used if only port is specified)
-
-    /// Callbacks for tracing the packet Rx events
-    TracedCallback<Ptr<const Packet>> m_rxTrace;
+    uint8_t m_tos; //!< The packets Type of Service
 
     /// Callbacks for tracing the packet Rx events, includes source and destination addresses
     TracedCallback<Ptr<const Packet>, const Address&, const Address&> m_rxTraceWithAddresses;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-server.cc ns-3.47/src/applications/model/udp-server.cc
--- ns-3.46/src/applications/model/udp-server.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-server.cc	2026-02-16 16:08:49.572229900 -0800
@@ -45,10 +45,6 @@
                           MakeUintegerAccessor(&UdpServer::GetPacketWindowSize,
                                                &UdpServer::SetPacketWindowSize),
                           MakeUintegerChecker<uint16_t>(8, 256))
-            .AddTraceSource("Rx",
-                            "A packet has been received",
-                            MakeTraceSourceAccessor(&UdpServer::m_rxTrace),
-                            "ns3::Packet::TracedCallback")
             .AddTraceSource("RxWithAddresses",
                             "A packet has been received",
                             MakeTraceSourceAccessor(&UdpServer::m_rxTraceWithAddresses),
@@ -57,13 +53,10 @@
 }
 
 UdpServer::UdpServer()
-    : SinkApplication(DEFAULT_PORT),
-      m_socket{nullptr},
-      m_socket6{nullptr},
-      m_received{0},
-      m_lossCounter{0}
+    : SinkApplication(DEFAULT_PORT)
 {
     NS_LOG_FUNCTION(this);
+    m_protocolTid = TypeId::LookupByName("ns3::UdpSocketFactory");
 }
 
 UdpServer::~UdpServer()
@@ -74,7 +67,6 @@
 uint16_t
 UdpServer::GetPacketWindowSize() const
 {
-    NS_LOG_FUNCTION(this);
     return m_lossCounter.GetBitMapSize();
 }
 
@@ -88,60 +80,46 @@
 uint32_t
 UdpServer::GetLost() const
 {
-    NS_LOG_FUNCTION(this);
     return m_lossCounter.GetLost();
 }
 
 uint64_t
 UdpServer::GetReceived() const
 {
-    NS_LOG_FUNCTION(this);
     return m_received;
 }
 
 void
-UdpServer::StartApplication()
+UdpServer::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
 
-    if (!m_socket)
+    auto local = m_local;
+    if (local.IsInvalid())
     {
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket = Socket::CreateSocket(GetNode(), tid);
-        auto local = m_local;
-        if (local.IsInvalid())
-        {
-            local = InetSocketAddress(Ipv4Address::GetAny(), m_port);
-            NS_LOG_INFO(this << " Binding on port " << m_port << " / " << local << ".");
-        }
-        else
-        {
-            if (InetSocketAddress::IsMatchingType(m_local))
-            {
-                const auto ipv4 = InetSocketAddress::ConvertFrom(m_local).GetIpv4();
-                NS_LOG_INFO(this << " Binding on " << ipv4 << " port " << m_port << " / " << m_local
-                                 << ".");
-            }
-            else if (Ipv6Address::IsMatchingType(m_local))
-            {
-                const auto ipv6 = Inet6SocketAddress::ConvertFrom(m_local).GetIpv6();
-                NS_LOG_INFO(this << " Binding on " << ipv6 << " port " << m_port << " / " << m_local
-                                 << ".");
-            }
-        }
-        if (m_socket->Bind(local) == -1)
-        {
-            NS_FATAL_ERROR("Failed to bind socket");
-        }
-        m_socket->SetRecvCallback(MakeCallback(&UdpServer::HandleRead, this));
+        local = InetSocketAddress(Ipv4Address::GetAny(), m_port);
+        NS_LOG_INFO(this << " Binding on port " << m_port << " / " << local << ".");
     }
+    else if (InetSocketAddress::IsMatchingType(m_local))
+    {
+        const auto ipv4 = InetSocketAddress::ConvertFrom(m_local).GetIpv4();
+        NS_LOG_INFO(this << " Binding on " << ipv4 << " port " << m_port << " / " << m_local
+                         << ".");
+    }
+    else if (Ipv6Address::IsMatchingType(m_local))
+    {
+        const auto ipv6 = Inet6SocketAddress::ConvertFrom(m_local).GetIpv6();
+        NS_LOG_INFO(this << " Binding on " << ipv6 << " port " << m_port << " / " << m_local
+                         << ".");
+    }
+    if (m_socket->Bind(local) == -1)
+    {
+        NS_FATAL_ERROR("Failed to bind socket");
+    }
+    m_socket->SetRecvCallback(MakeCallback(&UdpServer::HandleRead, this));
 
-    if (m_local.IsInvalid() && !m_socket6)
+    if (m_local.IsInvalid())
     {
-        // local address is not specified, so create another socket to also listen to all IPv6
-        // addresses
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket6 = Socket::CreateSocket(GetNode(), tid);
         auto local = Inet6SocketAddress(Ipv6Address::GetAny(), m_port);
         if (m_socket6->Bind(local) == -1)
         {
@@ -152,21 +130,6 @@
 }
 
 void
-UdpServer::StopApplication()
-{
-    NS_LOG_FUNCTION(this);
-
-    if (m_socket)
-    {
-        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-    }
-    if (m_socket6)
-    {
-        m_socket6->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-    }
-}
-
-void
 UdpServer::HandleRead(Ptr<Socket> socket)
 {
     NS_LOG_FUNCTION(this << socket);
@@ -175,7 +138,8 @@
     {
         Address localAddress;
         socket->GetSockName(localAddress);
-        m_rxTrace(packet);
+        m_rxTraceWithoutAddress(packet);
+        m_rxTrace(packet, from);
         m_rxTraceWithAddresses(packet, from, localAddress);
         if (packet->GetSize() > 0)
         {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-server.h ns-3.47/src/applications/model/udp-server.h
--- ns-3.46/src/applications/model/udp-server.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-server.h	2026-02-16 16:08:49.572229900 -0800
@@ -75,8 +75,7 @@
     void SetPacketWindowSize(uint16_t size);
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
 
     /**
      * @brief Handle a packet reception.
@@ -87,13 +86,8 @@
      */
     void HandleRead(Ptr<Socket> socket);
 
-    Ptr<Socket> m_socket;            //!< Socket
-    Ptr<Socket> m_socket6;           //!< IPv6 Socket (used if only port is specified)
-    uint64_t m_received;             //!< Number of received packets
-    PacketLossCounter m_lossCounter; //!< Lost packet counter
-
-    /// Callbacks for tracing the packet Rx events
-    TracedCallback<Ptr<const Packet>> m_rxTrace;
+    uint64_t m_received{0};             //!< Number of received packets
+    PacketLossCounter m_lossCounter{0}; //!< Lost packet counter
 
     /// Callbacks for tracing the packet Rx events, includes source and destination addresses
     TracedCallback<Ptr<const Packet>, const Address&, const Address&> m_rxTraceWithAddresses;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-trace-client.cc ns-3.47/src/applications/model/udp-trace-client.cc
--- ns-3.46/src/applications/model/udp-trace-client.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-trace-client.cc	2026-02-16 16:08:49.572229900 -0800
@@ -102,13 +102,10 @@
 }
 
 UdpTraceClient::UdpTraceClient()
-    : m_sent{0},
-      m_socket{nullptr},
-      m_peerPort{},
-      m_sendEvent{},
-      m_currentEntry{0}
+    : SourceApplication(false)
 {
     NS_LOG_FUNCTION(this);
+    m_protocolTid = TypeId::LookupByName("ns3::UdpSocketFactory");
 }
 
 UdpTraceClient::~UdpTraceClient()
@@ -195,9 +192,8 @@
 }
 
 uint16_t
-UdpTraceClient::GetMaxPacketSize()
+UdpTraceClient::GetMaxPacketSize() const
 {
-    NS_LOG_FUNCTION(this);
     return m_maxPacketSize;
 }
 
@@ -280,58 +276,15 @@
 }
 
 void
-UdpTraceClient::StartApplication()
+UdpTraceClient::DoStartApplication()
 {
     NS_LOG_FUNCTION(this);
-
-    if (!m_socket)
-    {
-        auto tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-        m_socket = Socket::CreateSocket(GetNode(), tid);
-        NS_ABORT_MSG_IF(m_peer.IsInvalid(), "Remote address not properly set");
-        if (!m_local.IsInvalid())
-        {
-            NS_ABORT_MSG_IF((Inet6SocketAddress::IsMatchingType(m_peer) &&
-                             InetSocketAddress::IsMatchingType(m_local)) ||
-                                (InetSocketAddress::IsMatchingType(m_peer) &&
-                                 Inet6SocketAddress::IsMatchingType(m_local)),
-                            "Incompatible peer and local address IP version");
-            if (m_socket->Bind(m_local) == -1)
-            {
-                NS_FATAL_ERROR("Failed to bind socket");
-            }
-        }
-        else
-        {
-            if (InetSocketAddress::IsMatchingType(m_peer))
-            {
-                if (m_socket->Bind() == -1)
-                {
-                    NS_FATAL_ERROR("Failed to bind socket");
-                }
-            }
-            else if (Inet6SocketAddress::IsMatchingType(m_peer))
-            {
-                if (m_socket->Bind6() == -1)
-                {
-                    NS_FATAL_ERROR("Failed to bind socket");
-                }
-            }
-            else
-            {
-                NS_ASSERT_MSG(false, "Incompatible address type: " << m_peer);
-            }
-        }
-        m_socket->SetIpTos(m_tos); // Affects only IPv4 sockets.
-        m_socket->Connect(m_peer);
-        m_socket->SetRecvCallback(MakeNullCallback<void, Ptr<Socket>>());
-        m_socket->SetAllowBroadcast(true);
-    }
+    m_socket->SetAllowBroadcast(true);
     m_sendEvent = Simulator::ScheduleNow(&UdpTraceClient::Send, this);
 }
 
 void
-UdpTraceClient::StopApplication()
+UdpTraceClient::CancelEvents()
 {
     NS_LOG_FUNCTION(this);
     Simulator::Cancel(m_sendEvent);
@@ -369,6 +322,7 @@
     }
 #endif
 
+    m_txTrace(p);
     if ((m_socket->Send(p)) >= 0)
     {
         ++m_sent;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/model/udp-trace-client.h ns-3.47/src/applications/model/udp-trace-client.h
--- ns-3.46/src/applications/model/udp-trace-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/applications/model/udp-trace-client.h	2026-02-16 16:08:49.572229900 -0800
@@ -22,7 +22,6 @@
 namespace ns3
 {
 
-class Socket;
 class Packet;
 
 /**
@@ -87,7 +86,7 @@
      * @brief Return the maximum packet size
      * @return the maximum packet size
      */
-    uint16_t GetMaxPacketSize();
+    uint16_t GetMaxPacketSize() const;
 
     /**
      * @brief Set the maximum packet size
@@ -102,8 +101,8 @@
     void SetTraceLoop(bool traceLoop);
 
   private:
-    void StartApplication() override;
-    void StopApplication() override;
+    void DoStartApplication() override;
+    void CancelEvents() override;
 
     /**
      * @brief Set the remote port (temporary function until deprecated attributes are removed)
@@ -156,13 +155,12 @@
         char frameType;      //!< Frame type (I, P or B)
     };
 
-    uint32_t m_sent;                    //!< Counter for sent packets
-    Ptr<Socket> m_socket;               //!< Socket
+    uint32_t m_sent{0};                 //!< Counter for sent packets
     std::optional<uint16_t> m_peerPort; //!< Remote peer port (deprecated) // NS_DEPRECATED_3_44
     EventId m_sendEvent;                //!< Event to send the next packet
 
     std::vector<TraceEntry> m_entries;    //!< Entries in the trace to send
-    uint32_t m_currentEntry;              //!< Current entry index
+    uint32_t m_currentEntry{0};           //!< Current entry index
     static TraceEntry g_defaultEntries[]; //!< Default trace to send
     uint16_t m_maxPacketSize; //!< Maximum packet size to send (including the SeqTsHeader)
     bool m_traceLoop;         //!< Loop through the trace file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/test/rta-tig-mobile-gaming-test-suite.cc ns-3.47/src/applications/test/rta-tig-mobile-gaming-test-suite.cc
--- ns-3.46/src/applications/test/rta-tig-mobile-gaming-test-suite.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/test/rta-tig-mobile-gaming-test-suite.cc	2026-02-16 16:08:49.572229900 -0800
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "ns3/application-container.h"
+#include "ns3/application-helper.h"
+#include "ns3/config.h"
+#include "ns3/double.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/internet-stack-helper.h"
+#include "ns3/ipv4-address-helper.h"
+#include "ns3/ipv4-address.h"
+#include "ns3/ipv4-interface-container.h"
+#include "ns3/ipv4-l3-protocol.h"
+#include "ns3/ipv4-list-routing-helper.h"
+#include "ns3/node-container.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-sink-helper.h"
+#include "ns3/packet-sink.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/pointer.h"
+#include "ns3/rng-seed-manager.h"
+#include "ns3/rta-tig-mobile-gaming.h"
+#include "ns3/simple-net-device-helper.h"
+#include "ns3/string.h"
+#include "ns3/test.h"
+#include "ns3/traced-callback.h"
+
+#include <algorithm>
+#include <numeric>
+#include <string>
+#include <vector>
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("RtaTigMobileGamingTest");
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * RT mobile gaming traffic test.
+ *
+ * The test consider traffic values for the two models presented in IEEE 802.11 Real Time
+ * Applications TIG Report (Section 4.1.4: Traffic model) and for both downlink and uplink.
+ *
+ * The test generates traffic between two nodes and keeps track of generated TX packets (size,
+ * timestamp and current stage). The test verifies the correct transition to stages, it checks the
+ * average sizes of generated packets for each stage match with the settings of the random
+ * variables, and it validates the average inter-arrival between generated gaming packets against
+ * the expected one based on the settings of the random variable.
+ */
+class RtaTigMobileGamingTestCase : public TestCase
+{
+  public:
+    /// Information about gaming parameters
+    struct GamingParams
+    {
+        uint32_t minInitialPacketSize{};  //!< Minimum size in bytes for initial packet payload
+        uint32_t maxInitialPacketSize{};  //!< Maximum size in bytes for initial packet payload
+        uint32_t minEndPacketSize{};      //!< Minimum size in bytes for end packet payload
+        uint32_t maxEndPacketSize{};      //!< Maximum size in bytes for end packet payload
+        uint32_t packetSizeLevLocation{}; //!< Location of largest extreme value distribution used
+                                          //!< to calculate packet sizes
+        double packetSizeLevScale{};      //!< Scale of largest extreme value distribution used to
+                                          //!< calculate packet sizes
+        Time packetArrivalLevLocation{}; //!< Location of largest extreme value distribution used to
+                                         //!< calculate packet arrivals
+        Time packetArrivalLevScale{};    //!< Scale of largest extreme value distribution used to
+                                         //!< calculate packet arrivals
+    };
+
+    /**
+     * Constructor
+     * @param name the name of the test to run
+     * @param params the VoIP parameters to use for the test, default parameters are used if not
+     * provided
+     */
+    RtaTigMobileGamingTestCase(const std::string& name, const GamingParams& params);
+
+  private:
+    void DoSetup() override;
+    void DoRun() override;
+
+    /**
+     * Record a packets successfully sent
+     * @param packet the transmitted packet
+     * @param stage the gaming traffic model stage of the packet
+     */
+    void PacketTx(Ptr<const Packet> packet, RtaTigMobileGaming::TrafficModelStage stage);
+
+    /**
+     * Record a packet successfully received
+     * @param context the context
+     * @param p the packet
+     * @param addr the sender's address
+     */
+    void ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr);
+
+    /// Information about transmitted packet
+    struct TxInfo
+    {
+        uint32_t length{0};  //!< length of the packet in bytes
+        Time tstamp{Time()}; //!< timestamp at which the packet is transmitted
+        RtaTigMobileGaming::TrafficModelStage stage{
+            RtaTigMobileGaming::TrafficModelStage::INITIAL}; //!< traffic model stage when the
+                                                             //!< packet is transmitted
+    };
+
+    std::vector<TxInfo> m_sent; //!< transmitted gaming packets
+    uint64_t m_received{0};     //!< number of bytes received
+
+    GamingParams m_params; //!< parameters of the model
+};
+
+RtaTigMobileGamingTestCase::RtaTigMobileGamingTestCase(const std::string& name,
+                                                       const GamingParams& params)
+    : TestCase{name},
+      m_params{params}
+{
+}
+
+void
+RtaTigMobileGamingTestCase::PacketTx(Ptr<const Packet> packet,
+                                     RtaTigMobileGaming::TrafficModelStage stage)
+{
+    const auto now = Simulator::Now();
+    NS_LOG_FUNCTION(this << packet << packet->GetSize() << now << stage);
+    m_sent.push_back({packet->GetSize(), now, stage});
+}
+
+void
+RtaTigMobileGamingTestCase::ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr)
+{
+    NS_LOG_FUNCTION(this << p << addr << p->GetSize());
+    m_received += p->GetSize();
+}
+
+void
+RtaTigMobileGamingTestCase::DoSetup()
+{
+    NS_LOG_FUNCTION(this);
+    // LogComponentEnable("RtaTigMobileGamingTest", LOG_LEVEL_ALL);
+    // LogComponentEnable("RtaTigMobileGaming", LOG_LEVEL_ALL);
+    // LogComponentEnable("PacketSink", LOG_LEVEL_ALL);
+
+    RngSeedManager::SetSeed(1);
+    RngSeedManager::SetRun(6);
+
+    const auto simulationTime = Seconds(60);
+    uint16_t port = 90;
+
+    auto sender = CreateObject<Node>();
+    auto receiver = CreateObject<Node>();
+
+    NodeContainer nodes;
+    nodes.Add(sender);
+    nodes.Add(receiver);
+
+    SimpleNetDeviceHelper simpleHelper;
+    auto devices = simpleHelper.Install(nodes);
+
+    InternetStackHelper internet;
+    internet.Install(nodes);
+
+    Ipv4AddressHelper ipv4Helper;
+    ipv4Helper.SetBase("10.11.12.0", "255.255.255.0");
+    auto interfaces = ipv4Helper.Assign(devices);
+
+    ApplicationHelper sourceHelper(RtaTigMobileGaming::GetTypeId());
+    auto remoteAddress = InetSocketAddress(interfaces.GetAddress(1), port);
+    sourceHelper.SetAttribute("Remote", AddressValue(remoteAddress));
+
+    auto ips = CreateObjectWithAttributes<UniformRandomVariable>(
+        "Min",
+        DoubleValue(m_params.minInitialPacketSize),
+        "Max",
+        DoubleValue(m_params.maxInitialPacketSize));
+    sourceHelper.SetAttribute("CustomInitialPacketSize", PointerValue(ips));
+
+    auto eps =
+        CreateObjectWithAttributes<UniformRandomVariable>("Min",
+                                                          DoubleValue(m_params.minEndPacketSize),
+                                                          "Max",
+                                                          DoubleValue(m_params.maxEndPacketSize));
+    sourceHelper.SetAttribute("CustomEndPacketSize", PointerValue(eps));
+
+    auto psl = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+        "Location",
+        DoubleValue(m_params.packetSizeLevLocation),
+        "Scale",
+        DoubleValue(m_params.packetSizeLevScale));
+    sourceHelper.SetAttribute("CustomPacketSizeLev", PointerValue(psl));
+
+    auto pal = CreateObjectWithAttributes<LargestExtremeValueRandomVariable>(
+        "Location",
+        DoubleValue(m_params.packetArrivalLevLocation.GetMicroSeconds()),
+        "Scale",
+        DoubleValue(m_params.packetArrivalLevScale.GetMicroSeconds()));
+    sourceHelper.SetAttribute("CustomPacketArrivalLev", PointerValue(pal));
+
+    auto sourceApp = sourceHelper.Install(sender);
+    const auto startAppTime = Seconds(1.0);
+    sourceApp.Start(startAppTime);
+    sourceApp.Stop(startAppTime + simulationTime);
+
+    PacketSinkHelper sinkHelper("ns3::UdpSocketFactory",
+                                InetSocketAddress(Ipv4Address::GetAny(), port));
+    auto sinkApp = sinkHelper.Install(receiver);
+    sinkApp.Start(Seconds(0.0));
+    sinkApp.Stop(Seconds(2.0) + simulationTime);
+
+    int64_t streamNumber = 100;
+    sourceHelper.AssignStreams(nodes, streamNumber);
+
+    Config::ConnectWithoutContext(
+        "/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::RtaTigMobileGaming/TxWithStage",
+        MakeCallback(&RtaTigMobileGamingTestCase::PacketTx, this));
+
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&RtaTigMobileGamingTestCase::ReceiveRx, this));
+}
+
+void
+RtaTigMobileGamingTestCase::DoRun()
+{
+    Simulator::Run();
+    Simulator::Destroy();
+
+    const auto totalTx =
+        std::accumulate(m_sent.cbegin(), m_sent.cend(), 0ULL, [](auto sum, const auto& tx) {
+            return sum + tx.length;
+        });
+    NS_TEST_ASSERT_MSG_EQ(totalTx, m_received, "Did not receive all transmitted gaming packets");
+
+    NS_TEST_ASSERT_MSG_EQ(m_sent.cbegin()->stage,
+                          RtaTigMobileGaming::TrafficModelStage::INITIAL,
+                          "First received packet is not an initial packet");
+    NS_TEST_ASSERT_MSG_EQ(m_sent.crbegin()->stage,
+                          RtaTigMobileGaming::TrafficModelStage::ENDING,
+                          "Last received packet is not an ending packet");
+    const auto allGamingPackets =
+        std::all_of(m_sent.cbegin() + 1, m_sent.cend() - 1, [](const auto& tx) {
+            return tx.stage == RtaTigMobileGaming::TrafficModelStage::GAMING;
+        });
+    NS_TEST_ASSERT_MSG_EQ(allGamingPackets, true, "Incorrectly reported stage during gaming stage");
+
+    NS_TEST_ASSERT_MSG_GT_OR_EQ(m_sent.cbegin()->length,
+                                m_params.minInitialPacketSize,
+                                "Size of initial packet is lower than expected");
+    NS_TEST_ASSERT_MSG_LT_OR_EQ(m_sent.cbegin()->length,
+                                m_params.maxInitialPacketSize,
+                                "Size of initial packet is higher than expected");
+
+    NS_TEST_ASSERT_MSG_GT_OR_EQ(m_sent.crbegin()->length,
+                                m_params.minEndPacketSize,
+                                "Size of end packet is lower than expected");
+    NS_TEST_ASSERT_MSG_LT_OR_EQ(m_sent.crbegin()->length,
+                                m_params.maxEndPacketSize,
+                                "Size of end packet is higher than expected");
+
+    const auto totalGamingTx =
+        std::accumulate(m_sent.cbegin() + 1, m_sent.cend() - 1, 0.0, [](auto sum, const auto& tx) {
+            return sum + tx.length;
+        });
+    const auto averageGamingPacketSize = totalGamingTx / (m_sent.size() - 2);
+    const auto expectedAverageGamingPacketSize =
+        LargestExtremeValueRandomVariable::GetMean(m_params.packetSizeLevLocation,
+                                                   m_params.packetSizeLevScale);
+    NS_TEST_EXPECT_MSG_EQ_TOL(averageGamingPacketSize,
+                              expectedAverageGamingPacketSize,
+                              0.5,
+                              "Unexpected average gaming packet size");
+
+    std::vector<Time> packetArrivals;
+    std::transform(m_sent.cbegin(),
+                   m_sent.cend() - 1,
+                   m_sent.cbegin() + 1,
+                   std::back_inserter(packetArrivals),
+                   [](const auto& lhs, const auto& rhs) { return (rhs.tstamp - lhs.tstamp); });
+    const auto totalPacketArrivals =
+        std::accumulate(packetArrivals.cbegin(),
+                        packetArrivals.cend(),
+                        Time(),
+                        [](auto sum, const auto t) { return sum + t; });
+    const auto averagePacketArrivalUs =
+        static_cast<double>(totalPacketArrivals.GetMicroSeconds()) / packetArrivals.size();
+    const auto expectedAveragePacketArrivalUs = LargestExtremeValueRandomVariable::GetMean(
+        m_params.packetArrivalLevLocation.GetMicroSeconds(),
+        m_params.packetArrivalLevScale.GetMicroSeconds());
+    NS_TEST_EXPECT_MSG_EQ_TOL(averagePacketArrivalUs,
+                              expectedAveragePacketArrivalUs,
+                              0.01 * expectedAveragePacketArrivalUs,
+                              "Unexpected average packet arrival");
+}
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * @brief RtaTigMobileGaming TestSuite
+ */
+class RtaTigMobileGamingTestSuite : public TestSuite
+{
+  public:
+    RtaTigMobileGamingTestSuite();
+};
+
+RtaTigMobileGamingTestSuite::RtaTigMobileGamingTestSuite()
+    : TestSuite("applications-rta-tig-mobile-gaming", Type::UNIT)
+{
+    AddTestCase(new RtaTigMobileGamingTestCase(
+                    "Check real-time mobile gaming traffic for DL status-sync",
+                    {0, 20, 500, 600, 50, 11.0, MilliSeconds(13), MicroSeconds(3700)}),
+                TestCase::Duration::QUICK);
+    AddTestCase(new RtaTigMobileGamingTestCase(
+                    "Check real-time mobile gaming traffic for UL status-sync",
+                    {0, 20, 400, 550, 38, 3.7, MilliSeconds(15), MicroSeconds(5700)}),
+                TestCase::Duration::QUICK);
+    AddTestCase(new RtaTigMobileGamingTestCase(
+                    "Check real-time mobile gaming traffic for DL lockstep",
+                    {0, 80, 1400, 1500, 210, 35.0, MilliSeconds(28), MicroSeconds(4200)}),
+                TestCase::Duration::QUICK);
+    AddTestCase(new RtaTigMobileGamingTestCase(
+                    "Check real-time mobile gaming traffic for UL lockstep",
+                    {0, 80, 500, 600, 92, 38.0, MilliSeconds(22), MicroSeconds(3400)}),
+                TestCase::Duration::QUICK);
+}
+
+static RtaTigMobileGamingTestSuite
+    g_RtaTigMobileGamingTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/test/tgax-video-traffic-test-suite.cc ns-3.47/src/applications/test/tgax-video-traffic-test-suite.cc
--- ns-3.46/src/applications/test/tgax-video-traffic-test-suite.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/test/tgax-video-traffic-test-suite.cc	2026-02-16 16:08:49.572229900 -0800
@@ -0,0 +1,466 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "ns3/application-container.h"
+#include "ns3/application-helper.h"
+#include "ns3/config.h"
+#include "ns3/enum.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/internet-stack-helper.h"
+#include "ns3/ipv4-address-helper.h"
+#include "ns3/ipv4-address.h"
+#include "ns3/ipv4-interface-container.h"
+#include "ns3/ipv4-l3-protocol.h"
+#include "ns3/ipv4-list-routing-helper.h"
+#include "ns3/ipv4-static-routing-helper.h"
+#include "ns3/ipv4-static-routing.h"
+#include "ns3/node-container.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-sink-helper.h"
+#include "ns3/packet-sink.h"
+#include "ns3/rng-seed-manager.h"
+#include "ns3/simple-net-device-helper.h"
+#include "ns3/string.h"
+#include "ns3/test.h"
+#include "ns3/tgax-video-traffic.h"
+#include "ns3/traced-callback.h"
+
+#include <numeric>
+#include <optional>
+#include <string>
+#include <vector>
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TgaxVideoTrafficTest");
+
+namespace
+{
+
+/**
+ * Get the node ID from the context string.
+ *
+ * @param context the context string
+ * @return the corresponding node ID
+ */
+uint32_t
+ConvertContextToNodeId(const std::string& context)
+{
+    auto sub = context.substr(10);
+    auto pos = sub.find("/Device");
+    return std::stoi(sub.substr(0, pos));
+}
+
+/**
+ * Get the string from the traffic model enum.
+ *
+ * @param model the traffic model enum
+ * @return the corresponding string
+ */
+std::string
+ModelToString(TgaxVideoTraffic::TrafficModelClassIdentifier model)
+{
+    switch (model)
+    {
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::CUSTOM:
+        return "Custom";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_1:
+        return "BV1";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_2:
+        return "BV2";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_3:
+        return "BV3";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_4:
+        return "BV4";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_5:
+        return "BV5";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_6:
+        return "BV6";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_1:
+        return "MC1";
+    case TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_2:
+        return "MC2";
+    default:
+        return "";
+    }
+}
+
+/**
+ * Get the expected average frame size.
+ *
+ * @param frameSizeBytesScale the scale of the Weibull distribution used to generate random frame
+ * sizes
+ * @param frameSizeBytesShape the shape of the Weibull distribution used to generate random frame
+ * sizes
+ * @return the expected average frame size
+ */
+uint32_t
+GetAverageframeSize(double frameSizeBytesScale, double frameSizeBytesShape)
+{
+    auto weibull = CreateObject<WeibullRandomVariable>();
+    return static_cast<uint32_t>(
+        WeibullRandomVariable::GetMean(frameSizeBytesScale, frameSizeBytesShape));
+}
+
+/**
+ * Get whether a model applies to multicast video traffic.
+ *
+ * @param model the traffic model
+ * @return true if the model is for multicast, false otherwise
+ */
+bool
+IsMulticast(TgaxVideoTraffic::TrafficModelClassIdentifier model)
+{
+    return ((model == TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_1) ||
+            (model == TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_2));
+}
+
+const auto simulationTime{Seconds(20)}; //!< The simulation time
+
+} // namespace
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * TGax Video traffic test, check for the expected inter frame interval, bit rate and packets inter
+ * arrivals.
+ */
+class TgaxVideoTrafficTestCase : public TestCase
+{
+  public:
+    /**
+     * Constructor
+     *
+     * @param model the traffic model to use for the test
+     * @param protocol the protocol to use for the test (Udp or Tcp)
+     * @param expectedBitRate the expected bit rate in Mbps at the end of the test
+     * @param parameters optional traffic model parameters (used for custom traffic model)
+     * @param txBufferSizeLimit optional limit for the TX buffer size of the TCP socket
+     */
+    TgaxVideoTrafficTestCase(
+        TgaxVideoTraffic::TrafficModelClassIdentifier model,
+        const std::string& protocol,
+        double expectedBitRate,
+        std::optional<TgaxVideoTraffic::TrafficModelParameters> parameters = {},
+        std::optional<uint32_t> txBufferSizeLimit = {});
+
+  private:
+    void DoSetup() override;
+    void DoRun() override;
+
+    /**
+     * Record a packets successfully sent
+     * @param packet the transmitted packet
+     * @param latency the latency applied to the packet
+     */
+    void PacketSent(Ptr<const Packet> packet, Time latency);
+
+    /**
+     * Record video frame generated
+     * @param size the size of the generated frame
+     */
+    void FrameGenerated(uint32_t size);
+
+    /**
+     * Record a packet successfully received
+     * @param context the context
+     * @param p the packet
+     * @param addr the sender's address
+     */
+    void ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr);
+
+    TgaxVideoTraffic::TrafficModelClassIdentifier
+        m_model;            //!< Selected buffered video traffic model
+    std::string m_protocol; //!< Udp or Tcp protocol
+    std::optional<TgaxVideoTraffic::TrafficModelParameters>
+        m_parameters; //!< Optional traffic model parameters (used for custom traffic model)
+    std::optional<uint32_t>
+        m_txBufferSizeLimit; //!< Optional limit for the TX buffer size of the TCP socket
+
+    double m_expectedBitRate; //!< Expected video bit rate
+
+    uint64_t m_sent{0};                  //!< number of bytes sent
+    std::vector<uint64_t> m_received;    //!< number of bytes received per receiver
+    std::vector<Time> m_generatedFrames; //!< Store time at which each frame has been generated
+    std::vector<Time> m_latencies;       //!< Store latency applied to each transmitted packet
+};
+
+TgaxVideoTrafficTestCase::TgaxVideoTrafficTestCase(
+    TgaxVideoTraffic::TrafficModelClassIdentifier model,
+    const std::string& protocol,
+    double expectedBitRate,
+    std::optional<TgaxVideoTraffic::TrafficModelParameters> parameters,
+    std::optional<uint32_t> txBufferSizeLimit)
+    : TestCase("Check video traffic with model " + ModelToString(model) + " and protocol " +
+               protocol),
+      m_model(model),
+      m_protocol(protocol),
+      m_parameters(parameters),
+      m_txBufferSizeLimit(txBufferSizeLimit),
+      m_expectedBitRate(expectedBitRate),
+      m_received(IsMulticast(m_model) ? 2 : 1, 0)
+{
+}
+
+void
+TgaxVideoTrafficTestCase::PacketSent(Ptr<const Packet> packet, Time latency)
+{
+    NS_LOG_FUNCTION(this << packet << packet->GetSize() << latency);
+    m_sent += packet->GetSize();
+    m_latencies.push_back(latency);
+}
+
+void
+TgaxVideoTrafficTestCase::FrameGenerated(uint32_t size)
+{
+    NS_LOG_FUNCTION(this << size);
+    m_generatedFrames.push_back(Simulator::Now());
+}
+
+void
+TgaxVideoTrafficTestCase::ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr)
+{
+    const auto receiver = ConvertContextToNodeId(context) - 1;
+    NS_LOG_FUNCTION(this << receiver << p << addr << p->GetSize());
+    m_received.at(receiver) += p->GetSize();
+}
+
+void
+TgaxVideoTrafficTestCase::DoSetup()
+{
+    NS_LOG_FUNCTION(this);
+
+    // LogComponentEnable("TgaxVideoTrafficTest", LOG_LEVEL_ALL);
+    // LogComponentEnable("TgaxVideoTraffic", LOG_LEVEL_ALL);
+    // LogComponentEnable("PacketSink", LOG_LEVEL_ALL);
+
+    RngSeedManager::SetSeed(6);
+    RngSeedManager::SetRun(8);
+
+    Config::SetDefault("ns3::TcpSocket::SegmentSize", UintegerValue(1500));
+    Config::SetDefault("ns3::TcpSocket::DelAckCount", UintegerValue(1));
+    if (m_txBufferSizeLimit)
+    {
+        Config::SetDefault("ns3::TcpSocket::SndBufSize", UintegerValue(*m_txBufferSizeLimit));
+    }
+
+    uint16_t port = 90;
+    const std::string multicastAddress = "239.192.100.1";
+
+    auto sender = CreateObject<Node>();
+    NodeContainer receivers(m_received.size());
+
+    NodeContainer nodes;
+    nodes.Add(sender);
+    nodes.Add(receivers);
+
+    SimpleNetDeviceHelper simpleHelper;
+    auto devices = simpleHelper.Install(nodes);
+
+    Ipv4StaticRoutingHelper staticRouting;
+    InternetStackHelper internet;
+    if (IsMulticast(m_model))
+    {
+        Ipv4ListRoutingHelper listRouting;
+        listRouting.Add(staticRouting, 0);
+        internet.SetRoutingHelper(listRouting);
+    }
+    internet.Install(nodes);
+
+    Ipv4AddressHelper ipv4Helper;
+    ipv4Helper.SetBase("10.11.12.0", "255.255.255.0");
+    auto interfaces = ipv4Helper.Assign(devices);
+
+    if (IsMulticast(m_model))
+    {
+        auto ipv4 = sender->GetObject<Ipv4>();
+        auto routing = staticRouting.GetStaticRouting(ipv4);
+        routing->AddHostRouteTo(multicastAddress.c_str(),
+                                ipv4->GetInterfaceForDevice(sender->GetDevice(0)),
+                                0);
+    }
+
+    ApplicationHelper sourceHelper(TgaxVideoTraffic::GetTypeId());
+    auto remoteAddress = InetSocketAddress(IsMulticast(m_model) ? multicastAddress.c_str()
+                                                                : interfaces.GetAddress(1),
+                                           port);
+    sourceHelper.SetAttribute("Remote", AddressValue(remoteAddress));
+    const std::string protocol = "ns3::" + m_protocol + "SocketFactory";
+    sourceHelper.SetAttribute("Protocol", StringValue(protocol));
+    sourceHelper.SetAttribute("TrafficModelClassIdentifier", EnumValue(m_model));
+    sourceHelper.SetAttribute("LatencyScale", DoubleValue(60.227));
+    if (m_parameters)
+    {
+        sourceHelper.SetAttribute("CustomVideoBitRate", DataRateValue(m_parameters->bitRate));
+        sourceHelper.SetAttribute("CustomFrameSizeScale",
+                                  DoubleValue(m_parameters->frameSizeBytesScale));
+        sourceHelper.SetAttribute("CustomFrameSizeShape",
+                                  DoubleValue(m_parameters->frameSizeBytesShape));
+    }
+    auto sourceApp = sourceHelper.Install(sender);
+    sourceApp.Start(Seconds(1.0));
+    sourceApp.Stop(Seconds(1.0) + simulationTime);
+
+    PacketSinkHelper sinkHelper(protocol, InetSocketAddress(Ipv4Address::GetAny(), port));
+    auto sinkApps = sinkHelper.Install(receivers);
+    sinkApps.Start(Seconds(0.0));
+    sinkApps.Stop(Seconds(2.0) + simulationTime);
+
+    int64_t streamNumber = 55;
+    sourceHelper.AssignStreams(nodes, streamNumber);
+
+    Config::ConnectWithoutContext(
+        "/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::TgaxVideoTraffic/TxWithLatency",
+        MakeCallback(&TgaxVideoTrafficTestCase::PacketSent, this));
+
+    Config::ConnectWithoutContext(
+        "/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::TgaxVideoTraffic/VideoFrameGenerated",
+        MakeCallback(&TgaxVideoTrafficTestCase::FrameGenerated, this));
+
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&TgaxVideoTrafficTestCase::ReceiveRx, this));
+}
+
+void
+TgaxVideoTrafficTestCase::DoRun()
+{
+    NS_LOG_FUNCTION(this);
+
+    Simulator::Run();
+    Simulator::Destroy();
+
+    for (std::size_t i = 0; i < m_received.size(); ++i)
+    {
+        NS_TEST_ASSERT_MSG_EQ(m_sent,
+                              m_received.at(i),
+                              "Did not receive all transmitted video frames for receiver " << i);
+        const auto measuredBitrate =
+            static_cast<double>(m_received.at(i) * 8) / simulationTime.GetMicroSeconds();
+        NS_TEST_EXPECT_MSG_EQ_TOL(measuredBitrate,
+                                  m_expectedBitRate,
+                                  0.05 * m_expectedBitRate,
+                                  "Unexpected video bit rate " << measuredBitrate
+                                                               << " for receiver " << i);
+    }
+
+    const auto averageFrameSize =
+        (m_parameters) ? GetAverageframeSize(m_parameters->frameSizeBytesScale,
+                                             m_parameters->frameSizeBytesShape)
+                       : GetAverageframeSize(
+                             TgaxVideoTraffic::m_trafficModels.at(m_model).frameSizeBytesScale,
+                             TgaxVideoTraffic::m_trafficModels.at(m_model).frameSizeBytesShape);
+    const auto expectedInterFrameDuration =
+        static_cast<double>(averageFrameSize * 8) / m_expectedBitRate;
+    std::vector<Time> interFrameDurations;
+    std::transform(m_generatedFrames.cbegin(),
+                   m_generatedFrames.cend() - 1,
+                   m_generatedFrames.cbegin() + 1,
+                   std::back_inserter(interFrameDurations),
+                   [](const auto& lhs, const auto& rhs) { return (rhs - lhs); });
+    const auto totalInterFrameDurations =
+        std::accumulate(interFrameDurations.cbegin(),
+                        interFrameDurations.cend(),
+                        Time(),
+                        [](auto sum, const auto t) { return sum + t; });
+    const auto interFrameDuration = totalInterFrameDurations / interFrameDurations.size();
+    NS_TEST_EXPECT_MSG_EQ_TOL(interFrameDuration.GetMicroSeconds(),
+                              expectedInterFrameDuration,
+                              1,
+                              "Unexpected frame interval");
+
+    const auto totalLatency = std::accumulate(m_latencies.cbegin(),
+                                              m_latencies.cend(),
+                                              Time(),
+                                              [](auto sum, auto latency) { return sum + latency; });
+    const auto avgLatency = totalLatency / m_latencies.size();
+    NS_TEST_EXPECT_MSG_EQ_TOL(avgLatency.GetMicroSeconds(),
+                              14834,
+                              200,
+                              "Unexpected average networking latency");
+}
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * @brief TgaxVideoTraffic TestSuite
+ */
+class TgaxVideoTrafficTestSuite : public TestSuite
+{
+  public:
+    TgaxVideoTrafficTestSuite();
+};
+
+TgaxVideoTrafficTestSuite::TgaxVideoTrafficTestSuite()
+    : TestSuite("applications-tgax-video-traffic", Type::UNIT)
+{
+    for (const auto& protocol : {"Tcp", "Udp"})
+    {
+        AddTestCase(new TgaxVideoTrafficTestCase(
+                        TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_1,
+                        protocol,
+                        2.0),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TgaxVideoTrafficTestCase(
+                        TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_2,
+                        protocol,
+                        4.0),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TgaxVideoTrafficTestCase(
+                        TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_3,
+                        protocol,
+                        6.0),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TgaxVideoTrafficTestCase(
+                        TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_4,
+                        protocol,
+                        8.0),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TgaxVideoTrafficTestCase(
+                        TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_5,
+                        protocol,
+                        10.0),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TgaxVideoTrafficTestCase(
+                        TgaxVideoTraffic::TrafficModelClassIdentifier::BUFFERED_VIDEO_6,
+                        protocol,
+                        15.6),
+                    TestCase::Duration::QUICK);
+    }
+    AddTestCase(new TgaxVideoTrafficTestCase(
+                    TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_1,
+                    "Udp",
+                    3.0),
+                TestCase::Duration::QUICK);
+    AddTestCase(new TgaxVideoTrafficTestCase(
+                    TgaxVideoTraffic::TrafficModelClassIdentifier::MULTICAST_VIDEO_2,
+                    "Udp",
+                    6.0),
+                TestCase::Duration::QUICK);
+    AddTestCase(new TgaxVideoTrafficTestCase(TgaxVideoTraffic::TrafficModelClassIdentifier::CUSTOM,
+                                             "Udp",
+                                             5.0,
+                                             {{DataRate("5Mbps"), 5000, 0.5}}),
+                TestCase::Duration::QUICK);
+    AddTestCase(new TgaxVideoTrafficTestCase(TgaxVideoTraffic::TrafficModelClassIdentifier::CUSTOM,
+                                             "Tcp",
+                                             20.0,
+                                             {{DataRate("20Mbps"), 15000, 1.0}}),
+                TestCase::Duration::QUICK);
+    AddTestCase(new TgaxVideoTrafficTestCase(TgaxVideoTraffic::TrafficModelClassIdentifier::CUSTOM,
+                                             "Tcp",
+                                             20.0,
+                                             {{DataRate("20Mbps"), 15000, 1.0}},
+                                             1500),
+                TestCase::Duration::QUICK);
+}
+
+static TgaxVideoTrafficTestSuite
+    g_TgaxVideoTrafficTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/test/tgax-virtual-desktop-test-suite.cc ns-3.47/src/applications/test/tgax-virtual-desktop-test-suite.cc
--- ns-3.46/src/applications/test/tgax-virtual-desktop-test-suite.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/test/tgax-virtual-desktop-test-suite.cc	2026-02-16 16:08:49.572229900 -0800
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "ns3/application-container.h"
+#include "ns3/application-helper.h"
+#include "ns3/config.h"
+#include "ns3/double.h"
+#include "ns3/internet-stack-helper.h"
+#include "ns3/ipv4-address-helper.h"
+#include "ns3/ipv4-address.h"
+#include "ns3/ipv4-interface-container.h"
+#include "ns3/ipv4-l3-protocol.h"
+#include "ns3/ipv4-list-routing-helper.h"
+#include "ns3/node-container.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-sink-helper.h"
+#include "ns3/packet-sink.h"
+#include "ns3/pointer.h"
+#include "ns3/rng-seed-manager.h"
+#include "ns3/simple-net-device-helper.h"
+#include "ns3/string.h"
+#include "ns3/test.h"
+#include "ns3/tgax-virtual-desktop.h"
+#include "ns3/traced-callback.h"
+
+#include <string>
+#include <vector>
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TgaxVirtualDesktopTest");
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * TGax VDI traffic test.
+ *
+ * The test consider traffic values for the model presented in IEEE 802.11-14/0571r12 - 11ax
+ * Evaluation Methodology (Appendix 2 – Traffic model descriptions: Virtual Desktop Infrastructure
+ * Traffic Model) for both downlink and uplink.
+ *
+ * The test generates traffic between two nodes and keeps track of generated TX packets (size,
+ * and timestamp). The test verifies average sizes and inter arrivals of generated packets.
+ */
+class TgaxVirtualDesktopTestCase : public TestCase
+{
+  public:
+    /// Information about VDI parameters
+    struct VdiParams
+    {
+        Time meanPacketArrivalTime{
+            NanoSeconds(60226900)}; //!< mean of the distribution used to generate packet arrival
+        std::string parametersPacketSize{
+            "41.0 3.2;1478.3 11.6"}; //!< parameters of the distribution used to generate the packet
+                                     //!< sizes
+    };
+
+    /**
+     * Constructor
+     * @param name the name of the test to run
+     * @param params the VDI parameters to use for the test
+     */
+    TgaxVirtualDesktopTestCase(const std::string& name, const VdiParams& params);
+
+  private:
+    void DoSetup() override;
+    void DoRun() override;
+
+    /**
+     * Record a transmitted VDI packet
+     * @param packet the transmitted packet
+     */
+    void PacketSent(Ptr<const Packet> packet);
+
+    /**
+     * Record a VDI packet successfully received
+     * @param context the context
+     * @param p the packet
+     * @param addr the sender's address
+     */
+    void ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr);
+
+    /// Information about a transmitted VDI packet
+    struct TxInfo
+    {
+        uint32_t size{}; //!< size of the packet in bytes
+        Time tstamp{};   //!< timestamp at which the packet is transmitted
+    };
+
+    std::vector<TxInfo> m_sent; //!< transmitted VDI packets
+    uint64_t m_received{0};     //!< number of bytes received
+
+    VdiParams m_params;  //!< VDI parameters
+    Time m_startAppTime; //!< Time at which the application is started
+};
+
+TgaxVirtualDesktopTestCase::TgaxVirtualDesktopTestCase(const std::string& name,
+                                                       const VdiParams& params)
+    : TestCase{name},
+      m_params{params},
+      m_startAppTime{Seconds(1.0)}
+{
+}
+
+void
+TgaxVirtualDesktopTestCase::PacketSent(Ptr<const Packet> packet)
+{
+    NS_LOG_FUNCTION(this << packet << packet->GetSize());
+    m_sent.push_back({packet->GetSize(), Simulator::Now()});
+}
+
+void
+TgaxVirtualDesktopTestCase::ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr)
+{
+    NS_LOG_FUNCTION(this << p << addr << p->GetSize());
+    m_received += p->GetSize();
+}
+
+void
+TgaxVirtualDesktopTestCase::DoSetup()
+{
+    NS_LOG_FUNCTION(this);
+
+    RngSeedManager::SetSeed(1);
+    RngSeedManager::SetRun(1);
+
+    const auto simulationTime = Seconds(600);
+    uint16_t port = 90;
+
+    auto sender = CreateObject<Node>();
+    auto receiver = CreateObject<Node>();
+
+    NodeContainer nodes;
+    nodes.Add(sender);
+    nodes.Add(receiver);
+
+    SimpleNetDeviceHelper simpleHelper;
+    auto devices = simpleHelper.Install(nodes);
+
+    InternetStackHelper internet;
+    internet.Install(nodes);
+
+    Ipv4AddressHelper ipv4Helper;
+    ipv4Helper.SetBase("10.11.12.0", "255.255.255.0");
+    auto interfaces = ipv4Helper.Assign(devices);
+
+    ApplicationHelper sourceHelper(TgaxVirtualDesktop::GetTypeId());
+    auto remoteAddress = InetSocketAddress(interfaces.GetAddress(1), port);
+    sourceHelper.SetAttribute("Remote", AddressValue(remoteAddress));
+    const auto protocol = "ns3::TcpSocketFactory";
+    sourceHelper.SetAttribute("Protocol", StringValue(protocol));
+
+    auto ipa = CreateObjectWithAttributes<ExponentialRandomVariable>(
+        "Mean",
+        DoubleValue(m_params.meanPacketArrivalTime.GetNanoSeconds()));
+    sourceHelper.SetAttribute("CustomInterPacketArrivals", PointerValue(ipa));
+
+    sourceHelper.SetAttribute("CustomParametersPacketSize",
+                              StringValue(m_params.parametersPacketSize));
+    auto sourceApp = sourceHelper.Install(sender);
+    sourceApp.Start(m_startAppTime);
+    sourceApp.Stop(m_startAppTime + simulationTime);
+
+    PacketSinkHelper sinkHelper(protocol, InetSocketAddress(Ipv4Address::GetAny(), port));
+    auto sinkApp = sinkHelper.Install(receiver);
+    sinkApp.Start(Seconds(0.0));
+    sinkApp.Stop(Seconds(2.0) + simulationTime);
+
+    int64_t streamNumber = 100;
+    sourceHelper.AssignStreams(nodes, streamNumber);
+
+    Config::ConnectWithoutContext(
+        "/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::TgaxVirtualDesktop/Tx",
+        MakeCallback(&TgaxVirtualDesktopTestCase::PacketSent, this));
+
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&TgaxVirtualDesktopTestCase::ReceiveRx, this));
+}
+
+void
+TgaxVirtualDesktopTestCase::DoRun()
+{
+    Simulator::Run();
+    Simulator::Destroy();
+
+    const auto totalTx =
+        std::accumulate(m_sent.cbegin(), m_sent.cend(), 0ULL, [](auto sum, const auto& tx) {
+            return sum + tx.size;
+        });
+    NS_TEST_ASSERT_MSG_EQ(totalTx, m_received, "Did not receive all transmitted VDI packets");
+
+    const auto delayConnectionEstablished = MilliSeconds(18);
+    NS_TEST_ASSERT_MSG_LT_OR_EQ(m_sent.cbegin()->tstamp - m_startAppTime -
+                                    delayConnectionEstablished,
+                                MilliSeconds(20),
+                                "Initial packet arrival larger than upper bound");
+
+    std::vector<Time> packetArrivals;
+    std::transform(m_sent.cbegin(),
+                   m_sent.cend() - 1,
+                   m_sent.cbegin() + 1,
+                   std::back_inserter(packetArrivals),
+                   [](const auto& lhs, const auto& rhs) { return (rhs.tstamp - lhs.tstamp); });
+    const auto totalPacketArrivals =
+        std::accumulate(packetArrivals.cbegin(),
+                        packetArrivals.cend(),
+                        Time(),
+                        [](auto sum, const auto t) { return sum + t; });
+    const auto averagePacketArrivalNs =
+        static_cast<double>(totalPacketArrivals.GetNanoSeconds()) / packetArrivals.size();
+    const auto expectedAveragePacketArrivalNs = m_params.meanPacketArrivalTime.GetNanoSeconds();
+    NS_TEST_EXPECT_MSG_EQ_TOL(averagePacketArrivalNs,
+                              expectedAveragePacketArrivalNs,
+                              0.01 * expectedAveragePacketArrivalNs,
+                              "Unexpected average packet arrival");
+
+    const auto averagePacketSize = static_cast<double>(totalTx) / m_sent.size();
+    const auto modalPos = m_params.parametersPacketSize.find(';');
+    double expectedAveragePacketSize{};
+    auto pos = m_params.parametersPacketSize.find(' ');
+    const auto mu1 = m_params.parametersPacketSize.substr(0, pos);
+    const auto mean1 = std::stod(mu1);
+    if (modalPos == std::string::npos)
+    {
+        // single mode
+        expectedAveragePacketSize = mean1;
+    }
+    else
+    {
+        // bimodal
+        auto mode2 = m_params.parametersPacketSize.substr(modalPos + 1,
+                                                          m_params.parametersPacketSize.size() - 1);
+        pos = mode2.find(' ');
+        const auto mu2 = mode2.substr(0, pos);
+        const auto mean2 = std::stod(mu2);
+        const auto bernoulliProb = 22.4 / 76.1;
+        expectedAveragePacketSize = (mean1 * (1 - bernoulliProb)) + (mean2 * bernoulliProb);
+    }
+
+    NS_TEST_EXPECT_MSG_EQ_TOL(averagePacketSize,
+                              expectedAveragePacketSize,
+                              0.015 * expectedAveragePacketSize,
+                              "Unexpected average packet size");
+}
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * @brief TgaxVirtualDesktop TestSuite
+ */
+class TgaxVirtualDesktopTestSuite : public TestSuite
+{
+  public:
+    TgaxVirtualDesktopTestSuite();
+};
+
+TgaxVirtualDesktopTestSuite::TgaxVirtualDesktopTestSuite()
+    : TestSuite("applications-tgax-virtual-desktop", Type::UNIT)
+{
+    AddTestCase(new TgaxVirtualDesktopTestCase("DL VDI traffic (default)", {}),
+                TestCase::Duration::QUICK);
+    AddTestCase(
+        new TgaxVirtualDesktopTestCase("UL VDI traffic", {MicroSeconds(48287), "50.598 5.0753"}),
+        TestCase::Duration::QUICK);
+}
+
+static TgaxVirtualDesktopTestSuite
+    g_TgaxVirtualDesktopTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/applications/test/tgax-voip-traffic-test-suite.cc ns-3.47/src/applications/test/tgax-voip-traffic-test-suite.cc
--- ns-3.46/src/applications/test/tgax-voip-traffic-test-suite.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/applications/test/tgax-voip-traffic-test-suite.cc	2026-02-16 16:08:49.572229900 -0800
@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
+ */
+
+#include "ns3/application-container.h"
+#include "ns3/application-helper.h"
+#include "ns3/config.h"
+#include "ns3/double.h"
+#include "ns3/internet-stack-helper.h"
+#include "ns3/node-container.h"
+#include "ns3/node.h"
+#include "ns3/nstime.h"
+#include "ns3/packet-sink-helper.h"
+#include "ns3/packet-sink.h"
+#include "ns3/packet-socket-address.h"
+#include "ns3/simple-net-device-helper.h"
+#include "ns3/test.h"
+#include "ns3/tgax-voip-traffic.h"
+#include "ns3/traced-callback.h"
+
+#include <algorithm>
+#include <map>
+#include <numeric>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TgaxVoipTrafficTest");
+
+namespace
+{
+const uint32_t voicePayloadSize = 33;        ///< payload size of voice packets in bytes
+const uint32_t silencePayloadSize = 7;       ///< payload size of silence packets in bytes
+const uint32_t compressedProtocolHeader = 3; ///< size of compressed protocol header (assumes IPv4)
+const double tol = 0.1;                      ///< some tolerance for floating point comparisons
+} // namespace
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * TGax voice-over-IP traffic test.
+ */
+class TgaxVoipTrafficTestCase : public TestCase
+{
+  public:
+    /// Information about VoIP parameters
+    struct VoipParams
+    {
+        Time meanActiveStateDuration;   //!< Mean duration of active/talking state
+        Time meanInactiveStateDuration; //!< Mean duration of inactive/silence state
+        double
+            voiceToSilenceProbability; //!< Probability to transition from active to inactive state
+        double
+            silenceToVoiceProbability; //!< Probability to transition from inactive to active state
+        Time scaleDelayJitter;         //!< Scale of laplacian distribution used for delay jitter
+        Time boundDelayJitter;         //!< Bound of laplacian distribution used for delay jitter
+    };
+
+    /**
+     * Constructor
+     * @param name the name of the test to run
+     * @param params the VoIP parameters to use for the test, default parameters are used if not
+     * provided
+     */
+    TgaxVoipTrafficTestCase(const std::string& name, std::optional<VoipParams> params = {});
+
+  private:
+    void DoSetup() override;
+    void DoRun() override;
+
+    /**
+     * Record a packets successfully sent
+     * @param packet the transmitted packet
+     * @param jitter the delay jitter applied to the packet
+     */
+    void PacketSent(Ptr<const Packet> packet, Time jitter);
+
+    /**
+     * Record a packet successfully received
+     * @param context the context
+     * @param p the packet
+     * @param addr the sender's address
+     */
+    void ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr);
+
+    /**
+     * Record a change in VoIP voice activity state
+     * @param state the new voice activity state
+     * @param duration the expected duration of the state
+     */
+    void StateUpdated(TgaxVoipTraffic::VoiceActivityState state, Time duration);
+
+    /// Information about transmitted packet
+    struct TxInfo
+    {
+        /**
+         * Constructor
+         * @param s the size of the packet in bytes
+         * @param t the timestamp at which the packet is transmitted
+         * @param j the delay jitter applied to the packet
+         */
+        TxInfo(uint32_t s, Time t, Time j)
+            : size{s},
+              tstamp{t},
+              jitter{j}
+        {
+        }
+
+        uint32_t size{}; //!< size of the packet in bytes
+        Time tstamp{};   //!< timestamp at which the packet is transmitted
+        Time jitter{};   //!< delay jitter applied to the packet
+    };
+
+    std::map<uint64_t, TxInfo> m_sent; //!< transmitted VoIP packets
+    uint64_t m_received{0};            //!< number of bytes received
+    std::vector<std::pair<TgaxVoipTraffic::VoiceActivityState, Time>>
+        m_states; //!< Hold voice activity states and the time at which it started
+
+    std::optional<VoipParams> m_params; //!< VoIP parameters
+};
+
+TgaxVoipTrafficTestCase::TgaxVoipTrafficTestCase(const std::string& name,
+                                                 std::optional<VoipParams> params)
+    : TestCase(name),
+      m_params(params)
+{
+}
+
+void
+TgaxVoipTrafficTestCase::PacketSent(Ptr<const Packet> packet, Time jitter)
+{
+    NS_LOG_FUNCTION(this << packet << packet->GetSize() << packet->GetUid() << jitter);
+    if (!m_sent.empty() && m_params && m_params->boundDelayJitter.IsZero())
+    {
+        NS_TEST_ASSERT_MSG_EQ(packet->GetUid(),
+                              (m_sent.rbegin()->first + 1),
+                              "Packets should arrive in order if there is no jitter");
+    }
+    m_sent.try_emplace(packet->GetUid(),
+                       packet->GetSize() - compressedProtocolHeader,
+                       Simulator::Now(),
+                       jitter);
+}
+
+void
+TgaxVoipTrafficTestCase::StateUpdated(TgaxVoipTraffic::VoiceActivityState state, Time duration)
+{
+    NS_LOG_FUNCTION(this << state << duration);
+    m_states.emplace_back(state, Simulator::Now());
+}
+
+void
+TgaxVoipTrafficTestCase::ReceiveRx(std::string context, Ptr<const Packet> p, const Address& addr)
+{
+    NS_LOG_FUNCTION(this << p << addr << p->GetSize());
+    m_received += (p->GetSize() - compressedProtocolHeader);
+}
+
+void
+TgaxVoipTrafficTestCase::DoSetup()
+{
+    NS_LOG_FUNCTION(this);
+
+    const auto simulationTime{Seconds(300)};
+
+    auto sender = CreateObject<Node>();
+    auto receiver = CreateObject<Node>();
+
+    NodeContainer nodes;
+    nodes.Add(sender);
+    nodes.Add(receiver);
+
+    SimpleNetDeviceHelper simpleHelper;
+    auto devices = simpleHelper.Install(nodes);
+
+    InternetStackHelper internet;
+    internet.Install(nodes);
+
+    PacketSocketAddress socketAddress;
+    socketAddress.SetSingleDevice(devices.Get(0)->GetIfIndex());
+    socketAddress.SetPhysicalAddress(devices.Get(1)->GetAddress());
+    socketAddress.SetProtocol(1);
+
+    ApplicationHelper sourceHelper(TgaxVoipTraffic::GetTypeId());
+    sourceHelper.SetAttribute("Remote", AddressValue(socketAddress));
+    sourceHelper.SetAttribute("ActivePacketPayloadSize",
+                              UintegerValue(voicePayloadSize + compressedProtocolHeader));
+    sourceHelper.SetAttribute("SilencePacketPayloadSize",
+                              UintegerValue(silencePayloadSize + compressedProtocolHeader));
+    if (m_params)
+    {
+        sourceHelper.SetAttribute("MeanActiveStateDuration",
+                                  TimeValue(m_params->meanActiveStateDuration));
+        sourceHelper.SetAttribute("MeanInactiveStateDuration",
+                                  TimeValue(m_params->meanInactiveStateDuration));
+        sourceHelper.SetAttribute("VoiceToSilenceProbability",
+                                  DoubleValue(m_params->voiceToSilenceProbability));
+        sourceHelper.SetAttribute("SilenceToVoiceProbability",
+                                  DoubleValue(m_params->silenceToVoiceProbability));
+        sourceHelper.SetAttribute("ScaleDelayJitter", TimeValue(m_params->scaleDelayJitter));
+        sourceHelper.SetAttribute("BoundDelayJitter", TimeValue(m_params->boundDelayJitter));
+    }
+    auto sourceApp = sourceHelper.Install(sender);
+    const auto startAppTime = Seconds(1.0);
+    sourceApp.Start(startAppTime);
+    sourceApp.Stop(startAppTime + simulationTime);
+    m_states.emplace_back(TgaxVoipTraffic::VoiceActivityState::INACTIVE_SILENCE, startAppTime);
+
+    PacketSinkHelper sinkHelper("ns3::PacketSocketFactory", socketAddress);
+    auto sinkApp = sinkHelper.Install(receiver);
+    sinkApp.Start(Seconds(0.0));
+    sinkApp.Stop(Seconds(2.0) + simulationTime);
+
+    int64_t streamNumber = 10;
+    sourceHelper.AssignStreams(nodes, streamNumber);
+
+    Config::ConnectWithoutContext(
+        "/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::TgaxVoipTraffic/StateUpdate",
+        MakeCallback(&TgaxVoipTrafficTestCase::StateUpdated, this));
+
+    Config::ConnectWithoutContext(
+        "/NodeList/*/$ns3::Node/ApplicationList/*/$ns3::TgaxVoipTraffic/TxWithJitter",
+        MakeCallback(&TgaxVoipTrafficTestCase::PacketSent, this));
+
+    Config::Connect("/NodeList/*/ApplicationList/*/$ns3::PacketSink/Rx",
+                    MakeCallback(&TgaxVoipTrafficTestCase::ReceiveRx, this));
+}
+
+void
+TgaxVoipTrafficTestCase::DoRun()
+{
+    Simulator::Run();
+    Simulator::Destroy();
+
+    const auto totalTx =
+        std::accumulate(m_sent.cbegin(), m_sent.cend(), 0ULL, [](auto sum, const auto& elem) {
+            return sum + elem.second.size;
+        });
+    NS_TEST_ASSERT_MSG_EQ(totalTx, m_received, "Did not receive all transmitted voip packets");
+
+    const auto offset = m_params
+                            ? m_params->boundDelayJitter
+                            : MilliSeconds(80); // an offset is applied in the model to guarantee
+                                                // positive time for scheduling packets
+    std::optional<TxInfo> prevTx{};
+    for (const auto& [id, info] : m_sent)
+    {
+        auto stateIt =
+            std::upper_bound(m_states.begin(),
+                             m_states.end(),
+                             info.tstamp - info.jitter - offset,
+                             [](Time value, const auto& state) { return value <= state.second; });
+        stateIt--;
+        auto expectedSize =
+            (stateIt->first == TgaxVoipTraffic::VoiceActivityState::INACTIVE_SILENCE)
+                ? silencePayloadSize
+                : voicePayloadSize;
+        NS_TEST_ASSERT_MSG_EQ(info.size, expectedSize, "Unexpected packet size");
+
+        if (!prevTx)
+        {
+            // first TX, no delta with previous
+            prevTx = info;
+            continue;
+        }
+
+        const auto interval = info.tstamp - prevTx->tstamp;
+        const auto jitterCorrection = info.jitter - prevTx->jitter;
+        const auto expectedInterval =
+            (stateIt->first == TgaxVoipTraffic::VoiceActivityState::INACTIVE_SILENCE)
+                ? MilliSeconds(160)
+                : MilliSeconds(20);
+        NS_TEST_ASSERT_MSG_EQ(interval,
+                              expectedInterval + jitterCorrection,
+                              "Unexpected encoder frame interval");
+
+        prevTx = info;
+    }
+
+    std::vector<Time> inactiveDurations;
+    std::transform(m_states.cbegin(),
+                   m_states.cend() - 1,
+                   m_states.cbegin() + 1,
+                   std::back_inserter(inactiveDurations),
+                   [](const auto& lhs, const auto& rhs) {
+                       return (lhs.first == TgaxVoipTraffic::VoiceActivityState::INACTIVE_SILENCE)
+                                  ? (rhs.second - lhs.second)
+                                  : Seconds(0);
+                   });
+    auto noZeroEnd = std::remove_if(inactiveDurations.begin(),
+                                    inactiveDurations.end(),
+                                    [&](const auto& t) { return t.IsZero(); });
+    inactiveDurations.erase(noZeroEnd, inactiveDurations.end());
+    const auto totalInactiveDuration =
+        std::accumulate(inactiveDurations.cbegin(),
+                        inactiveDurations.cend(),
+                        Time(),
+                        [](auto sum, const auto t) { return sum + t; });
+
+    std::vector<Time> activeDurations;
+    std::transform(m_states.cbegin(),
+                   m_states.cend() - 1,
+                   m_states.cbegin() + 1,
+                   std::back_inserter(activeDurations),
+                   [](const auto& lhs, const auto& rhs) {
+                       return (lhs.first == TgaxVoipTraffic::VoiceActivityState::ACTIVE_TALKING)
+                                  ? (rhs.second - lhs.second)
+                                  : Seconds(0);
+                   });
+    noZeroEnd = std::remove_if(activeDurations.begin(), activeDurations.end(), [&](const auto& t) {
+        return t.IsZero();
+    });
+    activeDurations.erase(noZeroEnd, activeDurations.end());
+    const auto totalActiveDuration =
+        std::accumulate(activeDurations.cbegin(),
+                        activeDurations.cend(),
+                        Time(),
+                        [](auto sum, const auto t) { return sum + t; });
+
+    const auto averageActiveDuration = totalActiveDuration / activeDurations.size();
+    const auto expectedAverageActiveStateDurationMs =
+        m_params ? m_params->meanActiveStateDuration.GetMilliSeconds() : 1250;
+    NS_TEST_EXPECT_MSG_EQ_TOL(averageActiveDuration.GetMilliSeconds(),
+                              expectedAverageActiveStateDurationMs,
+                              tol * expectedAverageActiveStateDurationMs,
+                              "Unexpected average active state duration");
+
+    const auto averageInactiveDuration = totalInactiveDuration / inactiveDurations.size();
+    const auto expectedAverageInactiveStateDurationMs =
+        m_params ? m_params->meanInactiveStateDuration.GetMilliSeconds() : 1250;
+    NS_TEST_EXPECT_MSG_EQ_TOL(averageInactiveDuration.GetMilliSeconds(),
+                              expectedAverageInactiveStateDurationMs,
+                              tol * expectedAverageInactiveStateDurationMs,
+                              "Unexpected average inactive state duration");
+
+    const auto totalDuration = totalInactiveDuration + totalActiveDuration;
+    const auto voiceActivityFactor = static_cast<double>(totalActiveDuration.GetMicroSeconds()) /
+                                     totalDuration.GetMicroSeconds();
+    const auto expectedVoiceActivityFactor =
+        m_params ? (m_params->silenceToVoiceProbability /
+                    (m_params->silenceToVoiceProbability + m_params->voiceToSilenceProbability))
+                 : 0.5; // default is 50%
+    NS_TEST_EXPECT_MSG_EQ_TOL(voiceActivityFactor,
+                              expectedVoiceActivityFactor,
+                              tol,
+                              "Unexpected voice activity factor");
+
+    const double totalJitterUs =
+        std::accumulate(m_sent.cbegin(), m_sent.cend(), 0.0, [](double sum, const auto& elem) {
+            return sum + elem.second.jitter.GetMicroSeconds();
+        });
+    const auto avgJitterMs = (totalJitterUs / m_sent.size()) / 1000;
+    NS_TEST_EXPECT_MSG_EQ_TOL(avgJitterMs, 0.0, tol, "Unexpected average jitter");
+}
+
+/**
+ * @ingroup applications-test
+ * @ingroup tests
+ *
+ * @brief TgaxVoipTraffic TestSuite
+ */
+class TgaxVoipTrafficTestSuite : public TestSuite
+{
+  public:
+    TgaxVoipTrafficTestSuite();
+};
+
+TgaxVoipTrafficTestSuite::TgaxVoipTrafficTestSuite()
+    : TestSuite("applications-tgax-voip-traffic", Type::UNIT)
+{
+    AddTestCase(new TgaxVoipTrafficTestCase("VoIP traffic with default parameters"),
+                TestCase::Duration::QUICK);
+    AddTestCase(new TgaxVoipTrafficTestCase("VoIP traffic without jitter",
+                                            TgaxVoipTrafficTestCase::VoipParams{MilliSeconds(1250),
+                                                                                MilliSeconds(1250),
+                                                                                0.016,
+                                                                                0.016,
+                                                                                Time(),
+                                                                                Time()}),
+                TestCase::Duration::QUICK);
+    AddTestCase(new TgaxVoipTrafficTestCase("VoIP traffic with custom parameters",
+                                            TgaxVoipTrafficTestCase::VoipParams{MilliSeconds(1000),
+                                                                                MilliSeconds(1500),
+                                                                                0.0200,
+                                                                                0.0133,
+                                                                                MicroSeconds(5000),
+                                                                                MilliSeconds(60)}),
+                TestCase::Duration::QUICK);
+}
+
+static TgaxVoipTrafficTestSuite
+    g_TgaxVoipTrafficTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/brite/CMakeLists.txt ns-3.47/src/brite/CMakeLists.txt
--- ns-3.46/src/brite/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/brite/CMakeLists.txt	2026-02-16 16:08:49.573229800 -0800
@@ -38,7 +38,10 @@
 endif()
 
 # Only process module if include folder and library have been found
-include_directories(${brite_INCLUDE_DIRS})
+include_directories(
+  SYSTEM
+  ${brite_INCLUDE_DIRS}
+)
 set(NS3_BRITE
     "ON"
     CACHE INTERNAL
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/buildings/doc/source/buildings-design.rst ns-3.47/src/buildings/doc/source/buildings-design.rst
--- ns-3.46/src/buildings/doc/source/buildings-design.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/buildings/doc/source/buildings-design.rst	2026-02-16 16:08:49.574229700 -0800
@@ -117,7 +117,7 @@
 External Wall Loss (EWL)
 -------------------------
 
-This component models the penetration loss through walls for indoor to outdoor communications and vice-versa. The values are taken from the [cost231]_ model.
+This component models the penetration loss through walls for indoor to outdoor communications and vice-versa. The values are taken from the COST231 model.
 
   * Wood ~ 4 dB
   * Concrete with windows (not metallized) ~ 7 dB
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/buildings/doc/source/buildings-user.rst ns-3.47/src/buildings/doc/source/buildings-user.rst
--- ns-3.46/src/buildings/doc/source/buildings-user.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/buildings/doc/source/buildings-user.rst	2026-02-16 16:08:49.574229700 -0800
@@ -16,7 +16,7 @@
 
 Add this at the beginning of your simulation program::
 
-   #include <ns3/buildings-module.h>
+  #include "ns3/buildings-module.h"
 
 
 Create a building
@@ -40,7 +40,7 @@
 
 This building has three floors and an internal 3 x 2  grid of rooms of equal size.
 
-The helper class GridBuildingAllocator is also available to easily
+The helper class ``GridBuildingAllocator`` is also available to easily
 create a set of buildings with identical characteristics placed on a
 rectangular grid. Here's an example of how to use it::
 
@@ -133,10 +133,11 @@
     mm0->SetPosition(Vector(5.0, 5.0, 1.5));
     mm1->SetPosition(Vector(30.0, 40.0, 1.5));
 
-Alternatively, you could use any existing PositionAllocator
+Alternatively, you could use any existing ``PositionAllocator``
 class. The coordinates of the node will determine whether it is placed
 outdoor or indoor and, if indoor, in which building and room it is placed.
 
+.. _building-positioning-methods:
 
 Building-specific positioning methods
 -------------------------------------
@@ -144,21 +145,24 @@
 The following position allocator classes are available to place node
 in special positions with respect to buildings:
 
- - ``RandomBuildingPositionAllocator``: Allocate each position by
-   randomly choosing a building from the list of all buildings, and
-   then randomly choosing a position inside the building.
-
- - ``RandomRoomPositionAllocator``: Allocate each position by randomly
-   choosing a room from the list of rooms in all buildings, and then
-   randomly choosing a position inside the room.
-
- - ``SameRoomPositionAllocator``: Walks a given NodeContainer
-   sequentially, and for each node allocate a new position randomly in
-   the same room of that node.
-
- - ``FixedRoomPositionAllocator``: Generate a random position
-   uniformly distributed in the volume of a chosen room inside a
-   chosen building.
+- ``RandomBuildingPositionAllocator``: Allocate each position by
+  randomly choosing a building from the list of all buildings, and
+  then randomly choosing a position inside the building.
+
+- ``RandomRoomPositionAllocator``: Allocate each position by randomly
+  choosing a room from the list of rooms in all buildings, and then
+  randomly choosing a position inside the room.
+
+- ``SameRoomPositionAllocator``: Walks a given NodeContainer
+  sequentially, and for each node allocate a new position randomly in
+  the same room of that node.
+
+- ``FixedRoomPositionAllocator``: Generate a random position
+  uniformly distributed in the volume of a chosen room inside a
+  chosen building.
+
+- ``OutdoorPositionAllocator``: Allocate positions outside of existing
+  buildings using rejection sampling.
 
 
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/buildings/helper/building-container.h ns-3.47/src/buildings/helper/building-container.h
--- ns-3.46/src/buildings/helper/building-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/buildings/helper/building-container.h	2026-02-16 16:08:49.575230000 -0800
@@ -67,11 +67,10 @@
      * for-loop to run through the Buildings
      *
      * @code
-     *   BuildingContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some Building method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some Building method
+     *   }
      * @endcode
      *
      * @returns an iterator which refers to the first Building in the container.
@@ -88,11 +87,10 @@
      * for-loop to run through the Buildings
      *
      * @code
-     *   BuildingContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some Building method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some Building method
+     *   }
      * @endcode
      *
      * @returns an iterator which indicates an ending condition for a loop.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/buildings/model/buildings-propagation-loss-model.cc ns-3.47/src/buildings/model/buildings-propagation-loss-model.cc
--- ns-3.46/src/buildings/model/buildings-propagation-loss-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/buildings/model/buildings-propagation-loss-model.cc	2026-02-16 16:08:49.575230000 -0800
@@ -193,8 +193,7 @@
         }
         else
         {
-            double sigma = std::sqrt((m_shadowingSigmaOutdoor * m_shadowingSigmaOutdoor) +
-                                     (m_shadowingSigmaExtWalls * m_shadowingSigmaExtWalls));
+            double sigma = std::hypot(m_shadowingSigmaOutdoor, m_shadowingSigmaExtWalls);
             return sigma;
         }
     }
@@ -204,8 +203,7 @@
     }
     else
     {
-        double sigma = std::sqrt((m_shadowingSigmaOutdoor * m_shadowingSigmaOutdoor) +
-                                 (m_shadowingSigmaExtWalls * m_shadowingSigmaExtWalls));
+        double sigma = std::hypot(m_shadowingSigmaOutdoor, m_shadowingSigmaExtWalls);
         return sigma;
     }
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/buildings/test/buildings-penetration-loss-pathloss-test.cc ns-3.47/src/buildings/test/buildings-penetration-loss-pathloss-test.cc
--- ns-3.46/src/buildings/test/buildings-penetration-loss-pathloss-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/buildings/test/buildings-penetration-loss-pathloss-test.cc	2026-02-16 16:08:49.576229800 -0800
@@ -16,29 +16,44 @@
 #include "ns3/log.h"
 #include "ns3/mobility-building-info.h"
 #include "ns3/mobility-helper.h"
+#include "ns3/rng-seed-manager.h"
 #include "ns3/simulator.h"
 #include "ns3/test.h"
 #include "ns3/three-gpp-propagation-loss-model.h"
 
+#include <numeric>
+
 using namespace ns3;
 
 NS_LOG_COMPONENT_DEFINE("BuildingsPenetrationLossesTest");
 
+using O2ILH = ChannelCondition::O2iLowHighConditionValue;
+
 /**
  * @ingroup propagation-tests
  *
- * Test case for the 3GPP channel O2I building penetration losses.
+ * Test case for the 3GPP channel building and vehicular O2I penetration losses.
  * It considers pre-determined scenarios and based on the outdoor/indoor
  * condition (O2O/O2I) checks whether the calculated received power
  * is aligned with the calculation in 3GPP TR 38.901.
  *
  * For O2O condition the calculation is done according to Table 7.4.1-1
  * and we check if the calculated received power is equal to the expected
- * value.
- * For the O2I condition, the calculation is done considering the building
- * penetration losses based on the material of the building walls.
- * In this case, we check if the calculated received power is less than the
- * received value (thus ensure that losses have been included).
+ * value. O2O condition is also tested when unblocked (LOS),
+ * or blocked (NLOS) by a building.
+
+ * For the O2I condition with frequencies equal or above 6GHz,
+ * the calculation is done considering the building penetration losses
+ * based on the material of the building walls, as specified
+ * in Table 7.4.3-1 and Table 7.4.3-2.
+ * For the O2I condition with frequencies below 6GHz, the calculation
+ * is done considering the backward compatibility model in Table 7.4.3-3.
+ * In both cases, we check if the calculated received power is within
+ * the expected normal distribution.
+ *
+ * All cases are tested with one node at 0 or 30 km/h, representing pedestrian or vehicles.
+ * In case a vehicle is part of a channel link, its O2I losses are applied to the channel,
+ * following procedure in 3GPP TR 38901 Section 7.4.3.2.
  */
 
 class BuildingsPenetrationLossesTestCase : public TestCase
@@ -65,24 +80,23 @@
      */
     struct TestVector
     {
-        Vector m_positionA; //!< the position of the first node
-        Vector m_positionB; //!< the position of the second node
-        double m_frequency; //!< carrier frequency in Hz
-        TypeId m_condModel; //!< the type ID of the channel condition model to be used
-        TypeId m_propModel; //!< the type ID of the propagation loss model to be used
-        double m_pt;        //!< transmitted power in dBm
-        double m_pr;        //!< received power in dBm
+        Vector m_positionA;      //!< the position of the first node
+        Vector m_positionB;      //!< the position of the second node
+        double m_ueVelocity;     //!< velocity in km/h
+        double m_frequency;      //!< carrier frequency in Hz
+        TypeId m_propModel;      //!< the type ID of the propagation loss model to be used
+        O2ILH m_o2iLossType;     //!< type of O2I loss (low/high)
+        double m_expectedMean;   //!< expected mean in dBm
+        double m_expectedStddev; //!< expected deviation in dBm
     };
 
-    TestVectors<TestVector> m_testVectors;         //!< array containing all the test vectors
+    std::vector<TestVector> m_testVectors;         //!< array containing all the test vectors
     Ptr<ThreeGppPropagationLossModel> m_propModel; //!< the propagation loss model
-    double m_tolerance;                            //!< tolerance
 };
 
 BuildingsPenetrationLossesTestCase::BuildingsPenetrationLossesTestCase()
     : TestCase("Test case for BuildingsPenetrationLosses"),
-      m_testVectors(),
-      m_tolerance(2e-3)
+      m_testVectors()
 {
 }
 
@@ -93,158 +107,181 @@
 void
 BuildingsPenetrationLossesTestCase::DoRun()
 {
+    const auto UMi = ThreeGppUmiStreetCanyonPropagationLossModel::GetTypeId();
+    const auto UMa = ThreeGppUmaPropagationLossModel::GetTypeId();
+    const auto RMa = ThreeGppRmaPropagationLossModel::GetTypeId();
     // create the test vector
-    TestVector testVector;
-
-    // tests for the RMa scenario
-    testVector.m_positionA = Vector(0, 0, 35.0); // O2I case
-    testVector.m_positionB = Vector(10, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppRmaPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -77.3784;
-    m_testVectors.Add(testVector);
-
-    testVector.m_positionA = Vector(0, 0, 35.0); // O2I case
-    testVector.m_positionB = Vector(100, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppRmaPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -87.2965;
-    m_testVectors.Add(testVector);
-
-    testVector.m_positionA = Vector(0, 0, 35.0); // O2O case
-    testVector.m_positionB = Vector(1000, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppRmaPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -108.5577;
-    m_testVectors.Add(testVector);
-
-    // tests for the UMa scenario
-    testVector.m_positionA = Vector(0, 0, 25.0); // O2I case
-    testVector.m_positionB = Vector(10, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppUmaPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -72.9380;
-    m_testVectors.Add(testVector);
-
-    testVector.m_positionA = Vector(0, 0, 25.0); // O2I case
-    testVector.m_positionB = Vector(100, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppUmaPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -86.2362;
-    m_testVectors.Add(testVector);
-
-    testVector.m_positionA = Vector(0, 0, 25.0); // O2O case
-    testVector.m_positionB = Vector(1000, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppUmaPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -109.7252;
-    m_testVectors.Add(testVector);
-
-    // tests for the UMi scenario
-    testVector.m_positionA = Vector(0, 0, 10.0); // O2I case
-    testVector.m_positionB = Vector(10, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppUmiStreetCanyonPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -69.8591;
-    m_testVectors.Add(testVector);
-
-    testVector.m_positionA = Vector(0, 0, 10.0); // O2I case
-    testVector.m_positionB = Vector(100, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppUmiStreetCanyonPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -88.4122;
-    m_testVectors.Add(testVector);
-
-    testVector.m_positionA = Vector(0, 0, 10.0); // O2O case
-    testVector.m_positionB = Vector(1000, 0, 1.5);
-    testVector.m_frequency = 5.0e9;
-    testVector.m_propModel = ThreeGppUmiStreetCanyonPropagationLossModel::GetTypeId();
-    testVector.m_pt = 0.0;
-    testVector.m_pr = -119.3114;
-    m_testVectors.Add(testVector);
-
-    // create the factory for the propagation loss model
-    ObjectFactory propModelFactory;
-
-    auto building = CreateObject<Building>();
-    building->SetExtWallsType(Building::ExtWallsType_t::ConcreteWithWindows);
-    building->SetNRoomsX(1);
-    building->SetNRoomsY(1);
-    building->SetNFloors(2);
-    building->SetBoundaries(Box(0.0, 100.0, 0.0, 10.0, 0.0, 5.0));
-
-    // create the two nodes
-    NodeContainer nodes;
-    nodes.Create(2);
-
-    // create the mobility models
-    Ptr<MobilityModel> a = CreateObject<ConstantPositionMobilityModel>();
-    Ptr<MobilityModel> b = CreateObject<ConstantPositionMobilityModel>();
-
-    // aggregate the nodes and the mobility models
-    nodes.Get(0)->AggregateObject(a);
-    nodes.Get(1)->AggregateObject(b);
-
-    Ptr<MobilityBuildingInfo> buildingInfoA = CreateObject<MobilityBuildingInfo>();
-    Ptr<MobilityBuildingInfo> buildingInfoB = CreateObject<MobilityBuildingInfo>();
-    a->AggregateObject(buildingInfoA); // operation usually done by BuildingsHelper::Install
-    buildingInfoA->MakeConsistent(a);
-
-    b->AggregateObject(buildingInfoB); // operation usually done by BuildingsHelper::Install
-    buildingInfoB->MakeConsistent(b);
-
-    Ptr<ChannelConditionModel> condModel = CreateObject<BuildingsChannelConditionModel>();
-
-    for (std::size_t i = 0; i < m_testVectors.GetN(); i++)
+    // clang-format off
+    m_testVectors = {
+        // PosA, PosB, B velocity, frequency, propagation model, O2I loss type, channel condition, mean and standard deviation of expected gaussian
+        // 6GHz, pedestrian, Low O2I loss
+        {{0, 0, 35.0}, {  10, 0, 1.5},  0, 6e9, RMa,  O2ILH::LOW,  -93.0, 4.5}, // RMa O2I
+        {{0, 0, 35.0}, { 100, 0, 1.5},  0, 6e9, RMa,  O2ILH::LOW, -112.0, 4.5}, // RMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 6e9, RMa,  O2ILH::LOW,  -97.0, 0.1}, // RMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 6e9, RMa,  O2ILH::LOW, -110.0, 0.1}, // RMa O2O blocked
+        {{0, 0, 25.0}, {  10, 0, 1.5},  0, 6e9, UMa,  O2ILH::LOW, -101.0, 5.3}, // UMa O2I
+        {{0, 0, 25.0}, { 100, 0, 1.5},  0, 6e9, UMa,  O2ILH::LOW, -125.0, 5.3}, // UMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 6e9, UMa,  O2ILH::LOW,  -96.0, 0.1}, // UMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 6e9, UMa,  O2ILH::LOW, -117.0, 0.1}, // UMa O2O blocked
+        {{0, 0, 10.0}, {  10, 0, 1.5},  0, 6e9, UMi,  O2ILH::LOW,  -96.0, 5.3}, // UMi O2I
+        {{0, 0, 10.0}, { 100, 0, 1.5},  0, 6e9, UMi,  O2ILH::LOW, -127.0, 5.0}, // UMi O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 6e9, UMi,  O2ILH::LOW,  -99.0, 0.1}, // UMi O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 6e9, UMi,  O2ILH::LOW, -119.0, 0.1}, // UMi O2O blocked
+        // 6GHz, pedestrian, High O2I loss
+        {{0, 0, 35.0}, {  10, 0, 1.5},  0, 6e9, RMa, O2ILH::HIGH,  -93.0, 4.5}, // RMa O2I
+        {{0, 0, 35.0}, { 100, 0, 1.5},  0, 6e9, RMa, O2ILH::HIGH, -112.0, 4.5}, // RMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 6e9, RMa, O2ILH::HIGH,  -97.0, 0.1}, // RMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 6e9, RMa, O2ILH::HIGH, -110.0, 0.1}, // RMa O2O blocked
+        {{0, 0, 25.0}, {  10, 0, 1.5},  0, 6e9, UMa, O2ILH::HIGH, -101.0, 5.3}, // UMa O2I
+        {{0, 0, 25.0}, { 100, 0, 1.5},  0, 6e9, UMa, O2ILH::HIGH, -125.0, 5.3}, // UMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 6e9, UMa, O2ILH::HIGH,  -96.0, 0.1}, // UMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 6e9, UMa, O2ILH::HIGH, -117.0, 0.1}, // UMa O2O blocked
+        {{0, 0, 10.0}, {  10, 0, 1.5},  0, 6e9, UMi, O2ILH::HIGH,  -95.0, 5.3}, // UMi O2I
+        {{0, 0, 10.0}, { 100, 0, 1.5},  0, 6e9, UMi, O2ILH::HIGH, -127.0, 5.3}, // UMi O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 6e9, UMi, O2ILH::HIGH,  -99.0, 0.1}, // UMi O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 6e9, UMi, O2ILH::HIGH, -119.0, 0.1}, // UMi O2O blocked
+        // 6GHz, vehicles, High O2I loss
+        {{0, 0, 35.0}, {  10, 0, 1.5}, 30, 6e9, RMa, O2ILH::HIGH,  -94.0, 4.5}, // RMa O2I
+        {{0, 0, 35.0}, { 100, 0, 1.5}, 30, 6e9, RMa, O2ILH::HIGH, -112.0, 4.5}, // RMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5}, 30, 6e9, RMa, O2ILH::HIGH,  -98.0, 5.3}, // RMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5}, 30, 6e9, RMa, O2ILH::HIGH, -110.0, 5.3}, // RMa O2O blocked
+        {{0, 0, 25.0}, {  10, 0, 1.5}, 30, 6e9, UMa, O2ILH::HIGH, -101.0, 5.3}, // UMa O2I
+        {{0, 0, 25.0}, { 100, 0, 1.5}, 30, 6e9, UMa, O2ILH::HIGH, -125.0, 5.3}, // UMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5}, 30, 6e9, UMa, O2ILH::HIGH,  -96.0, 5.3}, // UMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5}, 30, 6e9, UMa, O2ILH::HIGH, -117.0, 5.3}, // UMa O2O blocked
+        {{0, 0, 10.0}, {  10, 0, 1.5}, 30, 6e9, UMi, O2ILH::HIGH,  -95.0, 5.3}, // UMi O2I
+        {{0, 0, 10.0}, { 100, 0, 1.5}, 30, 6e9, UMi, O2ILH::HIGH, -127.0, 5.3}, // UMi O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5}, 30, 6e9, UMi, O2ILH::HIGH, -100.0, 5.3}, // UMi O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5}, 30, 6e9, UMi, O2ILH::HIGH, -119.0, 5.3}, // UMi O2O blocked
+        // Sub-6GHz, pedestrian, legacy O2I
+        {{0, 0, 35.0}, {  10, 0, 1.5},  0, 5e9, RMa, O2ILH::HIGH,  -97.0, 0.1}, // RMa O2I
+        {{0, 0, 35.0}, { 100, 0, 1.5},  0, 5e9, RMa, O2ILH::HIGH, -115.0, 0.1}, // RMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 5e9, RMa, O2ILH::HIGH,  -96.0, 0.1}, // RMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 5e9, RMa, O2ILH::HIGH, -108.0, 0.1}, // RMa O2O blocked
+        {{0, 0, 25.0}, {  10, 0, 1.5},  0, 5e9, UMa, O2ILH::HIGH, -108.0, 3.6}, // UMa O2I
+        {{0, 0, 25.0}, { 100, 0, 1.5},  0, 5e9, UMa, O2ILH::HIGH, -132.0, 3.6}, // UMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 5e9, UMa, O2ILH::HIGH,  -94.0, 0.1}, // UMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 5e9, UMa, O2ILH::HIGH, -117.0, 0.1}, // UMa O2O blocked
+        {{0, 0, 10.0}, {  10, 0, 1.5},  0, 5e9, UMi, O2ILH::HIGH, -103.0, 3.6}, // UMi O2I
+        {{0, 0, 10.0}, { 100, 0, 1.5},  0, 5e9, UMi, O2ILH::HIGH, -134.0, 3.6}, // UMi O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5},  0, 5e9, UMi, O2ILH::HIGH,  -98.0, 0.1}, // UMi O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5},  0, 5e9, UMi, O2ILH::HIGH, -119.0, 0.1}, // UMi O2O blocked
+        // Sub-6GHz, vehicles, legacy O2I
+        {{0, 0, 35.0}, {  10, 0, 1.5}, 30, 5e9, RMa, O2ILH::HIGH,  -97.0, 0.1}, // RMa O2I
+        {{0, 0, 35.0}, { 100, 0, 1.5}, 30, 5e9, RMa, O2ILH::HIGH, -115.0, 0.1}, // RMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5}, 30, 5e9, RMa, O2ILH::HIGH,  -96.0, 5.0}, // RMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5}, 30, 5e9, RMa, O2ILH::HIGH, -108.0, 5.0}, // RMa O2O blocked
+        {{0, 0, 25.0}, {  10, 0, 1.5}, 30, 5e9, UMa, O2ILH::HIGH, -108.0, 3.6}, // UMa O2I
+        {{0, 0, 25.0}, { 100, 0, 1.5}, 30, 5e9, UMa, O2ILH::HIGH, -132.0, 3.6}, // UMa O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5}, 30, 5e9, UMa, O2ILH::HIGH, -103.0, 5.0}, // UMa O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5}, 30, 5e9, UMa, O2ILH::HIGH, -117.0, 5.0}, // UMa O2O blocked
+        {{0, 0, 10.0}, {  10, 0, 1.5}, 30, 5e9, UMi, O2ILH::HIGH, -102.0, 3.6}, // UMi O2I
+        {{0, 0, 10.0}, { 100, 0, 1.5}, 30, 5e9, UMi, O2ILH::HIGH, -134.0, 3.6}, // UMi O2I
+        {{0, 0, 10.0}, { 50, 15, 1.5}, 30, 5e9, UMi, O2ILH::HIGH,  -98.0, 5.0}, // UMi O2O unblocked
+        {{0, 0, 10.0}, {1000, 0, 1.5}, 30, 5e9, UMi, O2ILH::HIGH, -119.0, 5.0}, // UMi O2O blocked
+    };
+    // clang-format on
+    RngSeedManager::SetSeed(1);
+    RngSeedManager::SetRun(1);
+    for (std::size_t i = 0; i < m_testVectors.size(); i++)
     {
-        TestVector testVector = m_testVectors.Get(i);
+        RngSeedManager::ResetNextStreamIndex();
+        // create the factory for the propagation loss model
+        ObjectFactory propModelFactory;
+
+        auto building = CreateObject<Building>();
+        building->SetExtWallsType(Building::ExtWallsType_t::ConcreteWithWindows);
+        building->SetNRoomsX(1);
+        building->SetNRoomsY(1);
+        building->SetNFloors(2);
+        building->SetBoundaries(Box(0.0, 100.0, 0.0, 10.0, 0.0, 5.0));
+
+        // create the two nodes
+        NodeContainer nodes;
+        nodes.Create(2);
+
+        // create the mobility models
+        Ptr<MobilityModel> a = CreateObject<ConstantPositionMobilityModel>();
+        Ptr<MobilityModel> b = CreateObject<ConstantVelocityMobilityModel>();
+        Ptr<ConstantVelocityMobilityModel> bcv = DynamicCast<ConstantVelocityMobilityModel>(b);
+
+        // aggregate the nodes and the mobility models
+        nodes.Get(0)->AggregateObject(a);
+        nodes.Get(1)->AggregateObject(b);
+
+        Ptr<MobilityBuildingInfo> buildingInfoA = CreateObject<MobilityBuildingInfo>();
+        Ptr<MobilityBuildingInfo> buildingInfoB = CreateObject<MobilityBuildingInfo>();
+        a->AggregateObject(buildingInfoA); // operation usually done by BuildingsHelper::Install
+        buildingInfoA->MakeConsistent(a);
+
+        b->AggregateObject(buildingInfoB); // operation usually done by BuildingsHelper::Install
+        buildingInfoB->MakeConsistent(b);
+
+        Ptr<ChannelConditionModel> condModel = CreateObject<BuildingsChannelConditionModel>();
+        int64_t streamOffset = 0;
+        streamOffset += condModel->AssignStreams(streamOffset);
+
+        // Configure test case setup
+        const auto& testVector = m_testVectors.at(i);
         a->SetPosition(testVector.m_positionA);
         b->SetPosition(testVector.m_positionB);
-
-        Ptr<ChannelCondition> cond = condModel->GetChannelCondition(a, b);
-
-        propModelFactory.SetTypeId(testVector.m_propModel);
-        m_propModel = propModelFactory.Create<ThreeGppPropagationLossModel>();
-        m_propModel->SetAttribute("Frequency", DoubleValue(testVector.m_frequency));
-        m_propModel->SetAttribute("ShadowingEnabled", BooleanValue(false));
-        m_propModel->SetChannelConditionModel(condModel);
-
+        bcv->SetVelocity({testVector.m_ueVelocity / 3.6, 0, 0});
         bool isAIndoor = buildingInfoA->IsIndoor();
         bool isBIndoor = buildingInfoB->IsIndoor();
 
+        Ptr<ChannelCondition> cond = condModel->GetChannelCondition(a, b);
+        cond->SetO2iLowHighCondition(testVector.m_o2iLossType);
         if (!isAIndoor && !isBIndoor) // a and b are outdoor
         {
-            cond->SetLosCondition(ChannelCondition::LosConditionValue::LOS);
             cond->SetO2iCondition(ChannelCondition::O2iConditionValue::O2O);
-
-            // check rcv power to be equal to the calculated value without losses
-            NS_TEST_EXPECT_MSG_EQ_TOL(m_propModel->CalcRxPower(testVector.m_pt, a, b),
-                                      testVector.m_pr,
-                                      m_tolerance,
-                                      "rcv power is not equal expected value");
         }
         else
         {
-            cond->SetLosCondition(ChannelCondition::LosConditionValue::LOS);
             cond->SetO2iCondition(ChannelCondition::O2iConditionValue::O2I);
-            cond->SetO2iLowHighCondition(ChannelCondition::O2iLowHighConditionValue::LOW);
+        }
 
-            // check rcv power to be lower than the calculated without losses
-            NS_TEST_EXPECT_MSG_LT(m_propModel->CalcRxPower(testVector.m_pt, a, b),
-                                  testVector.m_pr,
-                                  "rcv power is not less than calculated value");
+        propModelFactory.SetTypeId(testVector.m_propModel);
+        m_propModel = propModelFactory.Create<ThreeGppPropagationLossModel>();
+        m_propModel->SetAttribute("Frequency", DoubleValue(testVector.m_frequency));
+        m_propModel->SetAttribute("ShadowingEnabled", BooleanValue(false));
+        streamOffset += m_propModel->AssignStreams(streamOffset);
+        streamOffset += m_propModel->GetChannelConditionModel()->AssignStreams(streamOffset);
+        m_propModel->SetChannelConditionModel(condModel);
+        streamOffset += m_propModel->AssignStreams(streamOffset);
+        m_propModel->GetChannelConditionModel()->AssignStreams(streamOffset);
+        std::vector<double> samples(1000);
+        for (auto& sample : samples)
+        {
+            // Compute total loss
+            sample = m_propModel->CalcRxPower(0.0, a, b);
+            // Trigger another random variable usage
+            m_propModel->ClearO2iLossCacheMap();
         }
+
+        auto getMeanAndStddev = [](const std::vector<double> vec) {
+            // Calculate the mean
+            double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
+            double mean = sum / vec.size();
+
+            // Calculate variance and standard deviation
+            double sq_sum = std::inner_product(vec.begin(), vec.end(), vec.begin(), 0.0);
+            double stddev = std::sqrt((sq_sum / vec.size()) - mean * mean);
+            return std::make_pair(mean, stddev);
+        };
+
+        auto [mean, stddev] = getMeanAndStddev(samples);
+        NS_TEST_ASSERT_MSG_EQ_TOL(mean,
+                                  testVector.m_expectedMean,
+                                  -testVector.m_expectedMean * 0.1,
+                                  "Test vector " << i << ", mean outside expected range");
+        NS_TEST_ASSERT_MSG_EQ_TOL(stddev,
+                                  testVector.m_expectedStddev,
+                                  std::max(0.1, testVector.m_expectedStddev * 0.1),
+                                  "Test vector " << i
+                                                 << ", standard deviation outside expected range");
         m_propModel = nullptr;
+        Simulator::Destroy();
     }
-    Simulator::Destroy();
 }
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/click/CMakeLists.txt ns-3.47/src/click/CMakeLists.txt
--- ns-3.46/src/click/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/click/CMakeLists.txt	2026-02-16 16:08:49.576229800 -0800
@@ -41,7 +41,10 @@
   )
 endif()
 
-include_directories(${click_INCLUDE_DIRS})
+include_directories(
+  SYSTEM
+  ${click_INCLUDE_DIRS}
+)
 set(NS3_CLICK
     "ON"
     CACHE INTERNAL
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/config-store/model/display-functions.cc ns-3.47/src/config-store/model/display-functions.cc
--- ns-3.46/src/config-store/model/display-functions.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/config-store/model/display-functions.cc	2026-02-16 16:08:49.578230000 -0800
@@ -7,6 +7,8 @@
 
 #include "display-functions.h"
 
+#include "model-node-creator.h"
+#include "model-typeid-creator.h"
 #include "raw-text-config.h"
 
 #include "ns3/config.h"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/config-store/model/display-functions.h ns-3.47/src/config-store/model/display-functions.h
--- ns-3.46/src/config-store/model/display-functions.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/config-store/model/display-functions.h	2026-02-16 16:08:49.578230000 -0800
@@ -7,9 +7,6 @@
 #ifndef DISPLAY_FUNCTIONS_H
 #define DISPLAY_FUNCTIONS_H
 
-#include "model-node-creator.h"
-#include "model-typeid-creator.h"
-
 #include <gtk/gtk.h>
 
 namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/config-store/model/gtk-config-store.cc ns-3.47/src/config-store/model/gtk-config-store.cc
--- ns-3.46/src/config-store/model/gtk-config-store.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/config-store/model/gtk-config-store.cc	2026-02-16 16:08:49.578230000 -0800
@@ -8,7 +8,8 @@
 #include "gtk-config-store.h"
 
 #include "display-functions.h"
-#include "raw-text-config.h"
+#include "model-node-creator.h"
+#include "model-typeid-creator.h"
 
 #include "ns3/log.h"
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/config-store/model/model-node-creator.h ns-3.47/src/config-store/model/model-node-creator.h
--- ns-3.46/src/config-store/model/model-node-creator.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/config-store/model/model-node-creator.h	2026-02-16 16:08:49.578230000 -0800
@@ -5,6 +5,9 @@
  *          Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
  */
 
+#ifndef MODEL_NODE_CREATOR_H
+#define MODEL_NODE_CREATOR_H
+
 #include "attribute-iterator.h"
 
 #include <gtk/gtk.h>
@@ -90,3 +93,5 @@
     std::vector<GtkTreeIter*> m_iters; ///< attribute tree item
 };
 } // namespace ns3
+
+#endif // MODEL_NODE_CREATOR_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/config-store/model/model-typeid-creator.h ns-3.47/src/config-store/model/model-typeid-creator.h
--- ns-3.46/src/config-store/model/model-typeid-creator.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/config-store/model/model-typeid-creator.h	2026-02-16 16:08:49.578230000 -0800
@@ -4,6 +4,9 @@
  * Author: Moatamri Faker <faker.moatamri@sophia.inria.fr>
  */
 
+#ifndef MODEL_TYPEID_CREATOR_H
+#define MODEL_TYPEID_CREATOR_H
+
 #include "attribute-default-iterator.h"
 
 #include "ns3/type-id.h"
@@ -99,3 +102,5 @@
     std::vector<GtkTreeIter*> m_iters;
 };
 } // namespace ns3
+
+#endif // MODEL_TYPEID_CREATOR_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/assert-example.cc ns-3.47/src/core/examples/assert-example.cc
--- ns-3.46/src/core/examples/assert-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/assert-example.cc	2026-02-16 16:08:49.579229800 -0800
@@ -24,7 +24,7 @@
 int
 main(int argc, char** argv)
 {
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.Parse(argc, argv);
 
     std::cout << "NS_ASSERT_MSG example\n"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/empirical-random-variable-example.cc ns-3.47/src/core/examples/empirical-random-variable-example.cc
--- ns-3.46/src/core/examples/empirical-random-variable-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/empirical-random-variable-example.cc	2026-02-16 16:08:49.579229800 -0800
@@ -177,7 +177,7 @@
     long n = 1000000;
     bool disableAnti = false;
     bool single = false;
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.AddValue("count", "how many draws to make from the rng", n);
     cmd.AddValue("antithetic", "disable antithetic sampling", disableAnti);
     cmd.AddValue("single", "sample a single time", single);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/hash-example.cc ns-3.47/src/core/examples/hash-example.cc
--- ns-3.46/src/core/examples/hash-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/hash-example.cc	2026-02-16 16:08:49.579229800 -0800
@@ -6,7 +6,6 @@
 #include "ns3/hash.h"
 
 #include <algorithm> // find
-#include <climits>   // CHAR_BIT
 #include <fstream>
 #include <iomanip>
 #include <iostream>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/log-example.cc ns-3.47/src/core/examples/log-example.cc
--- ns-3.46/src/core/examples/log-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/log-example.cc	2026-02-16 16:08:49.579229800 -0800
@@ -96,7 +96,7 @@
 int
 main(int argc, char** argv)
 {
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.Parse(argc, argv);
 
     NS_LOG_DEBUG("Creating a Node");
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/sample-show-progress.cc ns-3.47/src/core/examples/sample-show-progress.cc
--- ns-3.46/src/core/examples/sample-show-progress.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/sample-show-progress.cc	2026-02-16 16:08:49.579229800 -0800
@@ -22,7 +22,6 @@
 #include "ns3/core-module.h"
 
 #include <chrono>
-#include <iomanip>
 #include <string>
 #include <thread>
 
@@ -39,7 +38,7 @@
  *
  * Inspired by PHOLD.
  */
-class Hold : public SimpleRefCount<Hold>
+class Hold
 {
   public:
     /**
@@ -120,7 +119,7 @@
               << "average event sleep time:  " << wait.As(Time::MS) << "\n"
               << "total simulation run time: " << stop.As(Time::S) << std::endl;
 
-    Ptr<Hold> h = Create<Hold>(wait, interval);
+    auto h = std::make_unique<Hold>(wait, interval);
     h->Event();
 
     Simulator::Stop(stop);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/sample-simulator.py ns-3.47/src/core/examples/sample-simulator.py
--- ns-3.46/src/core/examples/sample-simulator.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/sample-simulator.py	2026-02-16 16:08:49.579229800 -0800
@@ -36,8 +36,7 @@
     print("I should never be called... ")
 
 
-ns.cppyy.cppdef(
-    """
+ns.cppyy.cppdef("""
     #include "CPyCppyy/API.h"
 
     using namespace ns3;
@@ -99,8 +98,7 @@
     {
         return MakeEvent(&CancelledFunctionCpp);
     }
-   """
-)
+   """)
 
 
 def main(argv):
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/examples/system-path-examples.cc ns-3.47/src/core/examples/system-path-examples.cc
--- ns-3.46/src/core/examples/system-path-examples.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/examples/system-path-examples.cc	2026-02-16 16:08:49.579229800 -0800
@@ -6,9 +6,9 @@
  * Author: Peter D. Barnes, Jr. <pdbarnes@llnl.gov>
  */
 
-#include "ns3/core-module.h"
+#include "ns3/command-line.h"
+#include "ns3/system-path.h"
 
-#include <iomanip>
 #include <iostream>
 #include <string>
 
@@ -25,26 +25,30 @@
 int
 main(int argc, char* argv[])
 {
-    std::string path = "/usr/share/dict/";
+    std::string path = FindSelfDirectory();
 
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.Usage("SystemPath examples.\n");
 
     cmd.AddValue("path", "Path to demonstrate SystemPath functions.", path);
     cmd.Parse(argc, argv);
 
-    // Show initial values:
-    std::cout << std::endl;
-    std::cout << cmd.GetName() << ":" << std::endl;
-
-    std::cout << "FindSelfDirectory:   " << FindSelfDirectory() << std::endl;
-
-    std::cout << "Demonstration path:  " << path << std::endl;
-    std::cout << "Exists?              " << (Exists(path) ? "yes" : "no") << std::endl;
-
-    auto foo = Append(path, "foo");
-    std::cout << "Append 'foo':        " << foo << std::endl;
-    std::cout << "Exists?              " << (Exists(foo) ? "yes" : "no") << std::endl;
+    // Print system path information
+    std::cout << "Example name             : " << cmd.GetName() << "\n";
+    std::cout << "FindSelf                 : " << FindSelf() << "\n";
+    std::cout << "FindSelfDirectory        : " << FindSelfDirectory() << "\n";
+    std::cout << "Temporary directory name : " << MakeTemporaryDirectoryName() << "\n";
+    std::cout << "\n";
+
+    std::cout << "Path                     : " << path << "\n";
+    std::cout << "Exists                   : " << std::boolalpha << Exists(path) << "\n";
+    std::cout << "Valid system path        : " << CreateValidSystemPath(path) << "\n";
+    std::cout << "\n";
+
+    auto pathFoo = Append(path, "foo");
+    std::cout << "Append 'foo' to path     : " << pathFoo << "\n";
+    std::cout << "Exists                   : " << std::boolalpha << Exists(pathFoo) << "\n";
+    std::cout << "\n";
 
     std::cout << "Split path:\n";
     auto items = Split(path);
@@ -52,16 +56,17 @@
     {
         std::cout << "    '" << item << "'\n";
     }
-    std::cout << std::endl;
+    std::cout << "\n";
 
-    std::cout << "Successive Joins: \n";
+    std::cout << "Successive joins of the split path:\n";
     for (auto it = items.begin(); it != items.end(); ++it)
     {
         auto partial = Join(items.begin(), it);
         std::cout << "    '" << partial << "'\n";
     }
+    std::cout << "\n";
 
-    std::cout << "Files in the directory: \n";
+    std::cout << "Files in the path directory:\n";
     auto files = ReadFiles(path);
     for (const auto& item : files)
     {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/helper/csv-reader.cc ns-3.47/src/core/helper/csv-reader.cc
--- ns-3.46/src/core/helper/csv-reader.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/helper/csv-reader.cc	2026-02-16 16:08:49.579229800 -0800
@@ -10,13 +10,10 @@
 
 #include "ns3/log.h"
 
-#include <algorithm>
 #include <cctype>
 #include <fstream>
-#include <iterator>
 #include <limits>
 #include <sstream>
-#include <vector>
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/ascii-file.cc ns-3.47/src/core/model/ascii-file.cc
--- ns-3.46/src/core/model/ascii-file.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/ascii-file.cc	2026-02-16 16:08:49.580229800 -0800
@@ -11,7 +11,6 @@
 #include "ascii-file.h"
 
 #include "assert.h"
-#include "fatal-error.h"
 #include "fatal-impl.h"
 
 #include <iostream>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/attribute.cc ns-3.47/src/core/model/attribute.cc
--- ns-3.46/src/core/model/attribute.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/attribute.cc	2026-02-16 16:08:49.580229800 -0800
@@ -7,12 +7,9 @@
  */
 #include "attribute.h"
 
-#include "fatal-error.h"
 #include "log.h"
 #include "string.h"
 
-#include <sstream>
-
 /**
  * @file
  * @ingroup attributes
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/attribute-container.h ns-3.47/src/core/model/attribute-container.h
--- ns-3.46/src/core/model/attribute-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/attribute-container.h	2026-02-16 16:08:49.580229800 -0800
@@ -23,7 +23,7 @@
 namespace ns3
 {
 
-/*!
+/**
  * @ingroup attributes
  * @addtogroup attribute_AttributeContainer AttributeContainer Attribute
  * AttributeValue implementation for AttributeContainer
@@ -187,7 +187,7 @@
     container_type m_container; //!< Internal container
 };
 
-/*!
+/**
  * @ingroup attribute_AttributeContainer
  *
  * @class  ns3::AttributeContainerChecker "attribute-container.h"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/boolean.cc ns-3.47/src/core/model/boolean.cc
--- ns-3.46/src/core/model/boolean.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/boolean.cc	2026-02-16 16:08:49.580229800 -0800
@@ -7,7 +7,6 @@
  */
 #include "boolean.h"
 
-#include "fatal-error.h"
 #include "log.h"
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/calendar-scheduler.cc ns-3.47/src/core/model/calendar-scheduler.cc
--- ns-3.46/src/core/model/calendar-scheduler.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/calendar-scheduler.cc	2026-02-16 16:08:49.581229700 -0800
@@ -11,13 +11,11 @@
 
 #include "assert.h"
 #include "boolean.h"
-#include "event-impl.h"
 #include "log.h"
 #include "type-id.h"
 
 #include <list>
 #include <string>
-#include <utility>
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/callback.h ns-3.47/src/core/model/callback.h
--- ns-3.46/src/core/model/callback.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/callback.h	2026-02-16 16:08:49.581229700 -0800
@@ -339,6 +339,9 @@
  * @ingroup callbackimpl
  * Base class for Callback class.
  * Provides pimpl abstraction.
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 class CallbackBase
 {
@@ -416,6 +419,9 @@
  *
  * @tparam R \explicit The return type of the Callback.
  * @tparam UArgs \explicit The types of any arguments to the Callback.
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 template <typename R, typename... UArgs>
 class Callback : public CallbackBase
@@ -661,11 +667,13 @@
 }
 
 /**
+ * @ingroup callback
+ * Build Callbacks for class method members which take varying numbers
+ * of arguments and potentially returning a value.
  * @{
  */
 /**
- * Build Callbacks for class method members which take varying numbers
- * of arguments and potentially returning a value.
+ * Build Callbacks for class method members
  *
  * @tparam T   \deduced Type of the class having the member function.
  * @tparam OBJ \deduced Type of the class instance.
@@ -675,9 +683,7 @@
  * @param [in] memPtr Class method member pointer
  * @param [in] objPtr Class instance
  * @return A wrapper Callback
- *
- * Build Callbacks for class method members which take varying numbers of arguments
- * and potentially returning a value.
+ * @hidecaller
  */
 template <typename T, typename OBJ, typename R, typename... Args>
 Callback<R, Args...>
@@ -697,13 +703,14 @@
 
 /**
  * @ingroup callback
+ * Build Callbacks for functions which take varying numbers of arguments
+ * and potentially returning a value.
+ *
  * @tparam R   \deduced Return type of the callback function..
  * @tparam Args  \deduced Type list of any arguments to the member function.
  * @param [in] fnPtr Function pointer
  * @return A wrapper Callback
- *
- * Build Callbacks for functions which take varying numbers of arguments
- * and potentially returning a value.
+ * @hidecaller
  */
 template <typename R, typename... Args>
 Callback<R, Args...>
@@ -714,13 +721,13 @@
 
 /**
  * @ingroup callback
- * @tparam R   \deduced Return type of the callback function..
- * @tparam Args  \deduced Type list of any arguments to the member function.
- * @return A wrapper Callback
- *
  * Build null Callbacks which take no arguments,
  * for varying number of template arguments,
  * and potentially returning a value.
+ * @tparam R   \deduced Return type of the callback function..
+ * @tparam Args  \deduced Type list of any arguments to the member function.
+ * @return A wrapper Callback
+ * @hidecaller
  */
 template <typename R, typename... Args>
 Callback<R, Args...>
@@ -739,6 +746,7 @@
  * @param [in] fnPtr Function pointer
  * @param [in] bargs Bound arguments
  * @return A bound Callback
+ * @hidecaller
  */
 template <typename R, typename... Args, typename... BArgs>
 auto
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/command-line.cc ns-3.47/src/core/model/command-line.cc
--- ns-3.46/src/core/model/command-line.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/command-line.cc	2026-02-16 16:08:49.581229700 -0800
@@ -9,7 +9,6 @@
 #include "command-line.h"
 
 #include "config.h"
-#include "des-metrics.h"
 #include "environment-variable.h"
 #include "global-value.h"
 #include "log.h"
@@ -21,11 +20,16 @@
 #include "version.h"
 #endif
 
+#ifdef ENABLE_DES_METRICS
+#include "des-metrics.h"
+#endif
+
 #include <algorithm> // transform
 #include <cctype>    // tolower
 #include <cstdlib>   // exit
 #include <cstring>   // strlen
-#include <iomanip>   // setw, boolalpha
+#include <fstream>
+#include <iomanip> // setw, boolalpha
 #include <set>
 #include <sstream>
 
@@ -473,7 +477,7 @@
 
     std::fstream os(outf, std::fstream::out);
 
-    os << "/**\n \\file " << m_shortName << ".cc\n"
+    os << "/**\n @file " << m_shortName << ".cc\n"
        << "<h3>Usage</h3>\n"
        << "<code>$ ./ns3 run \"" << m_shortName << (!m_options.empty() ? " [Program Options]" : "")
        << (!nonOptions.empty() ? " [Program Arguments]" : "") << "\"</code>\n";
@@ -501,12 +505,12 @@
 
     if (!m_options.empty())
     {
-        listOptions("Program Options", m_options, "\\c --");
+        listOptions("Program Options", m_options, "@c --");
     }
 
     if (!nonOptions.empty())
     {
-        listOptions("Program Arguments", nonOptions, "\\c ");
+        listOptions("Program Arguments", nonOptions, "@c ");
     }
 
     os << "*/" << std::endl;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/command-line.h ns-3.47/src/core/model/command-line.h
--- ns-3.46/src/core/model/command-line.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/command-line.h	2026-02-16 16:08:49.581229700 -0800
@@ -120,7 +120,7 @@
  * @verbatim
    --SchedulerType=HeapScheduler \endverbatim
  *
- * A simple example of CommandLine is in `src/core/example/``command-line-example.cc`
+ * A simple example of CommandLine is in `src/core/example/command-line-example.cc`
  * See that file for an example of handling non-option arguments.
  *
  * The heart of that example is this code:
@@ -142,7 +142,7 @@
  *    cmd.Parse (argc, argv);
  * @endcode
  * after which it prints the values of each variable.  (The \c SetCbArg function
- * is not shown here; see `src/core/example/``command-line-example.cc`)
+ * is not shown here; see `src/core/example/command-line-example.cc`)
  *
  * Here is the output from a few runs of that program:
  *
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/config.h ns-3.47/src/core/model/config.h
--- ns-3.46/src/core/model/config.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/config.h	2026-02-16 16:08:49.581229700 -0800
@@ -76,6 +76,8 @@
  * This method overrides the initial value of the
  * matching attribute. This method cannot fail: it will
  * crash if the input attribute name or value is invalid.
+ *
+ * @hidecaller
  */
 void SetDefault(std::string name, const AttributeValue& value);
 /**
@@ -115,6 +117,8 @@
  * to them.  If no matching trace sources are found, this method will
  * throw a fatal error.  Use ConnectWithoutContextFailSafe if the absence
  * of matching trace sources should not be fatal.
+ *
+ * @hidecaller
  */
 void ConnectWithoutContext(std::string path, const CallbackBase& cb);
 /**
@@ -149,6 +153,8 @@
  * If no matching trace sources are found, this method will
  * throw a fatal error.  Use ConnectFailSafe if the absence
  * of matching trace sources should not be fatal.
+ *
+ * @hidecaller
  */
 void Connect(std::string path, const CallbackBase& cb);
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/default-simulator-impl.cc ns-3.47/src/core/model/default-simulator-impl.cc
--- ns-3.46/src/core/model/default-simulator-impl.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/default-simulator-impl.cc	2026-02-16 16:08:49.581229700 -0800
@@ -13,8 +13,6 @@
 #include "scheduler.h"
 #include "simulator.h"
 
-#include <cmath>
-
 /**
  * @file
  * @ingroup simulator
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/deprecated.h ns-3.47/src/core/model/deprecated.h
--- ns-3.46/src/core/model/deprecated.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/deprecated.h	2026-02-16 16:08:49.581229700 -0800
@@ -85,6 +85,13 @@
 
 /**
  * @ingroup deprecation
+ * @def NS_DEPRECATED_3_47
+ * Tag for things deprecated in version ns-3.47.
+ */
+#define NS_DEPRECATED_3_47(msg) NS_DEPRECATED("Deprecated in ns-3.47: " msg)
+
+/**
+ * @ingroup deprecation
  * @def NS_DEPRECATED_3_46
  * Tag for things deprecated in version ns-3.46.
  */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/des-metrics.h ns-3.47/src/core/model/des-metrics.h
--- ns-3.46/src/core/model/des-metrics.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/des-metrics.h	2026-02-16 16:08:49.582229900 -0800
@@ -85,7 +85,7 @@
  * Some useful shell pipelines:
  *
  * \li Run everything, retaining the results directory: <br/>
- *   @code  ./test.py --nobuild --retain \endcode
+ *   @code  ./test.py --no-build --retain \endcode
  * \li Example traces end up in \c testpy-output/, so move there: <br/>
  *   @code cd testpy-output/$(date +"%F")*_/  \endcode
  *   (Remove the `_', which is to work around a Doxygen limitation.)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/enum.h ns-3.47/src/core/model/enum.h
--- ns-3.46/src/core/model/enum.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/enum.h	2026-02-16 16:08:49.582229900 -0800
@@ -173,6 +173,8 @@
  * @param [in] v  The default enum value.
  * @param [in] n  The corresponding name.
  * @param [in] args Any additional arguments.
+ *
+ * @hidecaller
  */
 template <typename T, typename... Ts>
 Ptr<const AttributeChecker>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/environment-variable.cc ns-3.47/src/core/model/environment-variable.cc
--- ns-3.46/src/core/model/environment-variable.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/environment-variable.cc	2026-02-16 16:08:49.582229900 -0800
@@ -10,10 +10,8 @@
 
 #include "string.h"
 
-#include <cstdlib>  // std::getenv
-#include <cstdlib>  // Global functions setenv, unsetenv
-#include <cstring>  // strlen
-#include <iostream> // clog
+#include <cstdlib> // Global functions std::getenv, setenv, unsetenv
+#include <cstring> // strlen
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/environment-variable.h ns-3.47/src/core/model/environment-variable.h
--- ns-3.46/src/core/model/environment-variable.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/environment-variable.h	2026-02-16 16:08:49.582229900 -0800
@@ -123,7 +123,7 @@
          * \p delim character.  Keys may be assigned values by following
          * the key with the `=` character; any remaining text up to the next
          * delimiter will be taken as the value.  If no `=`
-         * is given the enpty string will be stored as the value.
+         * is given the empty string will be stored as the value.
          *
          * @param [in] envvar The environment variable.
          * @param [in] delim The delimiter between key,value pairs.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/event-id.h ns-3.47/src/core/model/event-id.h
--- ns-3.46/src/core/model/event-id.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/event-id.h	2026-02-16 16:08:49.582229900 -0800
@@ -9,6 +9,7 @@
 #define EVENT_ID_H
 
 #include "event-impl.h"
+#include "nstime.h"
 #include "ptr.h"
 
 #include <stdint.h>
@@ -110,19 +111,18 @@
     /**@}*/
 
     /**
+     * @name Comparison operators
+     * @{
+     */
+
+    /**
      * Test if two EventId's are equal.
      * @param [in] a The first EventId.
      * @param [in] b The second EventId.
      * @return \c true if the \pname{a} and \pname{b} represent the same event.
      */
     friend bool operator==(const EventId& a, const EventId& b);
-    /**
-     * Test if two EventId's are not equal.
-     * @param [in] a The first EventId.
-     * @param [in] b The second EventId.
-     * @return \c true if the \pname{a} and \pname{b} are not the same event.
-     */
-    friend bool operator!=(const EventId& a, const EventId& b);
+
     /**
      * Less than operator for two EventId's, based on time stamps.
      * @param [in] a The first EventId.
@@ -131,6 +131,27 @@
      */
     friend bool operator<(const EventId& a, const EventId& b);
 
+    /**
+     * Compare a Time to an EventId.
+     *
+     * This is useful when you have cached a previously scheduled event:
+     *
+     *     m_event = Schedule (...);
+     *
+     * and later you want to know the relationship between that event
+     * and some other Time `when`:
+     *
+     *     if (when < m_event) ...
+     *
+     * @param [in] time The Time operand.
+     * @param [in] event The EventId
+     * @returns \c true if \p time is before (less than) the
+     *          time stamp of the EventId.
+     */
+    friend bool operator<(const Time& time, const EventId& event);
+
+    /**@}*/ // Comparison operators
+
   private:
     Ptr<EventImpl> m_eventImpl; /**< The underlying event implementation. */
     uint64_t m_ts;              /**< The virtual time stamp. */
@@ -150,17 +171,22 @@
 }
 
 inline bool
-operator!=(const EventId& a, const EventId& b)
+operator<(const EventId& a, const EventId& b)
 {
-    return !(a == b);
+    return (a.GetTs() < b.GetTs());
 }
 
 inline bool
-operator<(const EventId& a, const EventId& b)
+operator<(const Time& time, const EventId& event)
 {
-    return (a.GetTs() < b.GetTs());
+    // Negative Time is less than any possible EventId, which are all >= 0.
+    if (time.IsStrictlyNegative())
+    {
+        return true;
+    }
+    // Time must be >= 0 so casting to unsigned is safe.
+    return static_cast<uint64_t>(time.GetTimeStep()) < event.GetTs();
 }
-
 } // namespace ns3
 
 #endif /* EVENT_ID_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/example-as-test.cc ns-3.47/src/core/model/example-as-test.cc
--- ns-3.46/src/core/model/example-as-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/example-as-test.cc	2026-02-16 16:08:49.582229900 -0800
@@ -9,13 +9,11 @@
 #include "example-as-test.h"
 
 #include "ascii-test.h"
-#include "assert.h"
 #include "environment-variable.h"
 #include "fatal-error.h"
 #include "log.h"
 
 #include <cstdlib> // itoa(), system ()
-#include <cstring>
 #include <sstream>
 #include <string>
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/global-value.cc ns-3.47/src/core/model/global-value.cc
--- ns-3.46/src/core/model/global-value.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/global-value.cc	2026-02-16 16:08:49.582229900 -0800
@@ -12,9 +12,6 @@
 #include "fatal-error.h"
 #include "log.h"
 #include "string.h"
-#include "uinteger.h"
-
-#include "ns3/core-config.h"
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/heap-scheduler.cc ns-3.47/src/core/model/heap-scheduler.cc
--- ns-3.46/src/core/model/heap-scheduler.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/heap-scheduler.cc	2026-02-16 16:08:49.583229800 -0800
@@ -11,7 +11,6 @@
 #include "heap-scheduler.h"
 
 #include "assert.h"
-#include "event-impl.h"
 #include "log.h"
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/int64x64-128.cc ns-3.47/src/core/model/int64x64-128.cc
--- ns-3.46/src/core/model/int64x64-128.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/int64x64-128.cc	2026-02-16 16:08:49.583229800 -0800
@@ -6,7 +6,6 @@
 
 #include "int64x64-128.h"
 
-#include "abort.h"
 #include "assert.h"
 #include "log.h"
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/integer.cc ns-3.47/src/core/model/integer.cc
--- ns-3.46/src/core/model/integer.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/integer.cc	2026-02-16 16:08:49.583229800 -0800
@@ -7,7 +7,6 @@
  */
 #include "integer.h"
 
-#include "fatal-error.h"
 #include "log.h"
 
 #include <sstream>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/length.cc ns-3.47/src/core/model/length.cc
--- ns-3.46/src/core/model/length.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/length.cc	2026-02-16 16:08:49.583229800 -0800
@@ -11,18 +11,14 @@
 #include "log.h"
 
 #include <algorithm>
-#include <array>
 #include <cctype>
 #include <cmath>
 #include <functional>
 #include <limits>
-#include <map>
 #include <ratio>
 #include <sstream>
 #include <string>
-#include <type_traits>
 #include <unordered_map>
-#include <vector>
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/list-scheduler.cc ns-3.47/src/core/model/list-scheduler.cc
--- ns-3.46/src/core/model/list-scheduler.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/list-scheduler.cc	2026-02-16 16:08:49.583229800 -0800
@@ -9,11 +9,9 @@
 #include "list-scheduler.h"
 
 #include "assert.h"
-#include "event-impl.h"
 #include "log.h"
 
 #include <string>
-#include <utility>
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/log.cc ns-3.47/src/core/model/log.cc
--- ns-3.46/src/core/model/log.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/log.cc	2026-02-16 16:08:49.584229700 -0800
@@ -12,15 +12,10 @@
 #include "fatal-error.h"
 #include "string.h"
 
-#include "ns3/core-config.h"
-
 #include <algorithm> // transform
-#include <cstring>   // strlen
+#include <ctype.h>   // toupper
 #include <iostream>
-#include <list>
-#include <locale> // toupper
 #include <map>
-#include <numeric> // accumulate
 #include <stdexcept>
 #include <utility>
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/log.h ns-3.47/src/core/model/log.h
--- ns-3.46/src/core/model/log.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/log.h	2026-02-16 16:08:49.584229700 -0800
@@ -25,6 +25,9 @@
  * @file
  * @ingroup logging
  * Debug message logging
+ *
+ * Included by graph was not generated because of its size.
+ * @hideincludedbygraph
  */
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/log-macros-enabled.h ns-3.47/src/core/model/log-macros-enabled.h
--- ns-3.46/src/core/model/log-macros-enabled.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/log-macros-enabled.h	2026-02-16 16:08:49.584229700 -0800
@@ -189,7 +189,7 @@
  *
  * Output the name of the function.
  *
- * This should be used only in static functions; most member functions
+ * This should be used only in static functions without arguments; most member functions
  * should instead use NS_LOG_FUNCTION().
  */
 #define NS_LOG_FUNCTION_NOARGS()                                                                   \
@@ -221,8 +221,10 @@
  * @endcode
  *
  * To facilitate function tracing, most functions should begin with
- * (at least) NS_LOG_FUNCTION(this).  Static functions should use
- * NS_LOG_FUNCTION_NOARGS() instead.
+ * (at least) NS_LOG_FUNCTION(this).
+ *
+ * Static functions should use NS_LOG_FUNCTION(args) when they have arguments,
+ * and NS_LOG_FUNCTION_NOARGS() when they have no arguments.
  *
  * @param [in] parameters The parameters to output.
  */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/map-scheduler.cc ns-3.47/src/core/model/map-scheduler.cc
--- ns-3.46/src/core/model/map-scheduler.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/map-scheduler.cc	2026-02-16 16:08:49.584229700 -0800
@@ -10,7 +10,6 @@
 #include "map-scheduler.h"
 
 #include "assert.h"
-#include "event-impl.h"
 #include "log.h"
 
 #include <string>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/names.h ns-3.47/src/core/model/names.h
--- ns-3.46/src/core/model/names.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/names.h	2026-02-16 16:08:49.584229700 -0800
@@ -328,6 +328,8 @@
      *
      * @returns A smart pointer to the named object converted to
      *          the requested type.
+     *
+     * @hidecaller
      */
     template <typename T>
     static Ptr<T> Find(std::string path);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/node-printer.cc ns-3.47/src/core/model/node-printer.cc
--- ns-3.46/src/core/model/node-printer.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/node-printer.cc	2026-02-16 16:08:49.584229700 -0800
@@ -11,8 +11,6 @@
 #include "log.h"
 #include "simulator.h" // GetContext()
 
-#include <iomanip>
-
 /**
  * @file
  * @ingroup simulator
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/nstime.h ns-3.47/src/core/model/nstime.h
--- ns-3.46/src/core/model/nstime.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/nstime.h	2026-02-16 16:08:49.584229700 -0800
@@ -11,7 +11,6 @@
 #include "assert.h"
 #include "attribute-helper.h"
 #include "attribute.h"
-#include "event-id.h"
 #include "int64x64.h"
 #include "type-name.h"
 
@@ -391,6 +390,11 @@
         return ToDouble(Time::MIN);
     }
 
+    /**
+     * @copydoc GetYears()
+     * @hidecaller
+     * @hideref
+     */
     inline double GetSeconds() const
     {
         return ToDouble(Time::S);
@@ -645,6 +649,8 @@
      *
      * @param [in] unit The unit to use.
      * @return The Time with embedded unit.
+     * @hidecaller
+     * @hideref
      */
     TimeWithUnit As(const Unit unit = Time::AUTO) const;
 
@@ -771,6 +777,15 @@
      */
     static bool StaticInit();
 
+    /**
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
+     *
+     * @param other Time to compare to this one
+     * @returns The result of the comparison.
+     */
+    constexpr std::strong_ordering operator<=>(const Time& other) const = default;
+
   private:
     /**
      * @cond HIDE_FROM_DOXYGEN
@@ -805,21 +820,6 @@
      */
     static void ConvertTimes(const Unit unit);
 
-    // Operator and related functions which need access
-
-    /**
-     * @name Comparison operators
-     * @{
-     */
-    friend bool operator==(const Time& lhs, const Time& rhs);
-    friend bool operator!=(const Time& lhs, const Time& rhs);
-    friend bool operator<=(const Time& lhs, const Time& rhs);
-    friend bool operator>=(const Time& lhs, const Time& rhs);
-    friend bool operator<(const Time& lhs, const Time& rhs);
-    friend bool operator>(const Time& lhs, const Time& rhs);
-    friend bool operator<(const Time& time, const EventId& event);
-    /**@}*/ // Comparison operators
-
     /**
      * @name Arithmetic operators
      * @{
@@ -891,107 +891,6 @@
 static bool g_TimeStaticInit [[maybe_unused]] = Time::StaticInit();
 
 /**
- * Equality operator for Time.
- * @param [in] lhs The first value
- * @param [in] rhs The second value
- * @returns \c true if the two input values are equal.
- */
-inline bool
-operator==(const Time& lhs, const Time& rhs)
-{
-    return lhs.m_data == rhs.m_data;
-}
-
-/**
- * Inequality operator for Time.
- * @param [in] lhs The first value
- * @param [in] rhs The second value
- * @returns \c true if the two input values not are equal.
- */
-inline bool
-operator!=(const Time& lhs, const Time& rhs)
-{
-    return lhs.m_data != rhs.m_data;
-}
-
-/**
- * Less than or equal operator for Time.
- * @param [in] lhs The first value
- * @param [in] rhs The second value
- * @returns \c true if the first input value is less than or equal to the second input value.
- */
-inline bool
-operator<=(const Time& lhs, const Time& rhs)
-{
-    return lhs.m_data <= rhs.m_data;
-}
-
-/**
- * Greater than or equal operator for Time.
- * @param [in] lhs The first value
- * @param [in] rhs The second value
- * @returns \c true if the first input value is greater than or equal to the second input value.
- */
-inline bool
-operator>=(const Time& lhs, const Time& rhs)
-{
-    return lhs.m_data >= rhs.m_data;
-}
-
-/**
- * Less than operator for Time.
- * @param [in] lhs The first value
- * @param [in] rhs The second value
- * @returns \c true if the first input value is less than the second input value.
- */
-inline bool
-operator<(const Time& lhs, const Time& rhs)
-{
-    return lhs.m_data < rhs.m_data;
-}
-
-/**
- * Greater than operator for Time.
- * @param [in] lhs The first value
- * @param [in] rhs The second value
- * @returns \c true if the first input value is greater than the second input value.
- */
-inline bool
-operator>(const Time& lhs, const Time& rhs)
-{
-    return lhs.m_data > rhs.m_data;
-}
-
-/**
- * Compare a Time to an EventId.
- *
- * This is useful when you have cached a previously scheduled event:
- *
- *     m_event = Schedule (...);
- *
- * and later you want to know the relationship between that event
- * and some other Time `when`:
- *
- *     if (when < m_event) ...
- *
- * @param [in] time The Time operand.
- * @param [in] event The EventId
- * @returns \c true if \p time is before (less than) the
- *          time stamp of the EventId.
- */
-inline bool
-operator<(const Time& time, const EventId& event)
-{
-    // Negative Time is less than any possible EventId, which are all >= 0.
-    if (time.m_data < 0)
-    {
-        return true;
-    }
-    // Time must be >= 0 so casting to unsigned is safe.
-    return static_cast<uint64_t>(time.m_data) < event.GetTs();
-}
-
-/**
  * Addition operator for Time.
  * @param [in] lhs The first value
  * @param [in] rhs The second value
@@ -1365,6 +1264,11 @@
     return Time::From(value, Time::MIN);
 }
 
+/**
+ * @copydoc Years()
+ * @hidecaller
+ * @hideref
+ */
 inline Time
 Seconds(double value)
 {
@@ -1377,6 +1281,11 @@
     return Time::From(value, Time::S);
 }
 
+/**
+ * @copydoc Years()
+ * @hidecaller
+ * @hideref
+ */
 inline Time
 MilliSeconds(uint64_t value)
 {
@@ -1389,6 +1298,11 @@
     return Time::From(value, Time::MS);
 }
 
+/**
+ * @copydoc Years()
+ * @hidecaller
+ * @hideref
+ */
 inline Time
 MicroSeconds(uint64_t value)
 {
@@ -1401,6 +1315,11 @@
     return Time::From(value, Time::US);
 }
 
+/**
+ * @copydoc Years()
+ * @hidecaller
+ * @hideref
+ */
 inline Time
 NanoSeconds(uint64_t value)
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/object-base.cc ns-3.47/src/core/model/object-base.cc
--- ns-3.46/src/core/model/object-base.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/object-base.cc	2026-02-16 16:08:49.584229700 -0800
@@ -7,15 +7,14 @@
  */
 #include "object-base.h"
 
-#include "assert.h"
+#include "abort.h"
 #include "attribute-construction-list.h"
 #include "environment-variable.h"
+#include "fatal-error.h"
 #include "log.h"
 #include "string.h"
 #include "trace-source-accessor.h"
 
-#include "ns3/core-config.h"
-
 /**
  * @file
  * @ingroup object
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/object.cc ns-3.47/src/core/model/object.cc
--- ns-3.46/src/core/model/object.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/object.cc	2026-02-16 16:08:49.585229900 -0800
@@ -12,12 +12,9 @@
 #include "assert.h"
 #include "attribute.h"
 #include "log.h"
-#include "object-factory.h"
-#include "string.h"
 
 #include <cstdlib>
 #include <cstring>
-#include <sstream>
 #include <vector>
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/object-factory.cc ns-3.47/src/core/model/object-factory.cc
--- ns-3.46/src/core/model/object-factory.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/object-factory.cc	2026-02-16 16:08:49.584229700 -0800
@@ -9,8 +9,6 @@
 
 #include "log.h"
 
-#include <sstream>
-
 /**
  * @file
  * @ingroup object
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/object-factory.h ns-3.47/src/core/model/object-factory.h
--- ns-3.46/src/core/model/object-factory.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/object-factory.h	2026-02-16 16:08:49.584229700 -0800
@@ -174,6 +174,8 @@
  *
  * The args sequence can be made of any number of pairs, each consisting of a
  * name (of std::string type) followed by a value (of const AttributeValue & type).
+ *
+ * @hidecaller
  */
 template <typename T, typename... Args>
 Ptr<T> CreateObjectWithAttributes(Args... args);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/object.h ns-3.47/src/core/model/object.h
--- ns-3.46/src/core/model/object.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/object.h	2026-02-16 16:08:49.585229900 -0800
@@ -73,6 +73,9 @@
  * all its aggregates. The DoDispose() method is always automatically
  * invoked from the Unref() method before destroying the Object,
  * even if the user did not call Dispose() directly.
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 class Object : public SimpleRefCount<Object, ObjectBase, ObjectDeleter>
 {
@@ -130,7 +133,7 @@
         std::vector<Ptr<Object>>::const_iterator m_uniAggrIter;
     };
 
-    /** Constructor. */
+    /** @hidecaller */
     Object();
     /** Destructor. */
     ~Object() override;
@@ -144,6 +147,7 @@
      * @tparam T \explicit The type of the aggregated Object to retrieve.
      * @returns A pointer to the requested Object, or zero
      *          if it could not be found.
+     * @hidecaller
      */
     template <typename T>
     inline Ptr<T> GetObject() const;
@@ -155,6 +159,7 @@
      * @param [in] tid The TypeId of the requested Object.
      * @returns A pointer to the requested Object with the specified TypeId,
      *          or zero if it could not be found.
+     * @hidecaller
      */
     template <typename T>
     Ptr<T> GetObject(TypeId tid) const;
@@ -301,6 +306,8 @@
      * DoDispose() method.
      *
      * It is safe to call GetObject() from within this method.
+     *
+     * @hidecaller
      */
     virtual void DoDispose();
     /**
@@ -336,12 +343,12 @@
      * This method invoke the copy constructor of the input object
      * and returns the new instance.
      */
-    /**@{*/
     template <typename T>
     friend Ptr<T> CopyObject(Ptr<T> object);
+
+    /** @copydoc CopyObject(Ptr<T>) */
     template <typename T>
     friend Ptr<T> CopyObject(Ptr<const T> object);
-    /**@}*/
 
     /**
      * Set the TypeId and construct all Attributes of an Object.
@@ -613,6 +620,7 @@
  * @tparam T \explicit The type of the derived object to construct.
  * @param [in] args Arguments to pass to the constructor.
  * @return The derived object.
+ * @hidecaller
  */
 template <typename T, typename... Args>
 Ptr<T>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/object-ptr-container.h ns-3.47/src/core/model/object-ptr-container.h
--- ns-3.46/src/core/model/object-ptr-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/object-ptr-container.h	2026-02-16 16:08:49.585229900 -0800
@@ -39,8 +39,8 @@
  * @see AttributeValue
  *
  * Call graph was not generated because of its size.
- * @hidecallergraph
  * @hidecallgraph
+ * @hidecaller
  */
 class ObjectPtrContainerValue : public AttributeValue
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/pointer.h ns-3.47/src/core/model/pointer.h
--- ns-3.46/src/core/model/pointer.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/pointer.h	2026-02-16 16:08:49.585229900 -0800
@@ -125,6 +125,7 @@
  * Create a PointerChecker for a type.
  * @tparam T \explicit The underlying type.
  * @returns The PointerChecker.
+ * @hidecaller
  */
 template <typename T>
 Ptr<AttributeChecker> MakePointerChecker();
@@ -257,12 +258,14 @@
  * \fn ns3::Ptr<const ns3::AttributeAccessor> ns3::MakePointerAccessor (T1 a1)
  * @copydoc ns3::MakeAccessorHelper(T1)
  * @see AttributeAccessor
+ * @hidecaller
  */
 /**
  * @ingroup attribute_Pointer
  * \fn ns3::Ptr<const ns3::AttributeAccessor> ns3::MakePointerAccessor (T1 a1, T2 a2)
  * @copydoc ns3::MakeAccessorHelper(T1,T2)
  * @see AttributeAccessor
+ * @hidecaller
  */
 
 template <typename T>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/priority-queue-scheduler.cc ns-3.47/src/core/model/priority-queue-scheduler.cc
--- ns-3.46/src/core/model/priority-queue-scheduler.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/priority-queue-scheduler.cc	2026-02-16 16:08:49.585229900 -0800
@@ -8,9 +8,6 @@
 
 #include "priority-queue-scheduler.h"
 
-#include "assert.h"
-#include "event-impl.h"
-#include "log-macros-disabled.h"
 #include "log.h"
 #include "scheduler.h"
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/ptr.h ns-3.47/src/core/model/ptr.h
--- ns-3.46/src/core/model/ptr.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/ptr.h	2026-02-16 16:08:49.585229900 -0800
@@ -61,6 +61,9 @@
  * (or ObjectBase) there is also a convenience wrapper Create<>()
  *
  * @tparam T \explicit The type of the underlying object.
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 template <typename T>
 class Ptr
@@ -95,6 +98,8 @@
      * @tparam U \deduced The actual type of the argument and return pointer.
      * @param [in] p Smart pointer
      * @return The pointer managed by this smart pointer.
+     * @hidecaller
+     * @hideref
      */
     template <typename U>
     friend U* PeekPointer(const Ptr<U>& p);
@@ -233,6 +238,8 @@
  * @tparam Ts \deduced Types of the constructor arguments.
  * @param  [in] args Constructor arguments.
  * @return A Ptr to the newly created \c T.
+ * @hidecaller
+ * @hiderefby
  */
 template <typename T, typename... Ts>
 Ptr<T> Create(Ts&&... args);
@@ -401,6 +408,14 @@
 struct EventMemberImplObjTraits;
 
 /**
+ * @ingroup events
+ * @defgroup makeeventmemptr MakeEvent from class methods.
+ *
+ * Create EventImpl instances from class member methods which take
+ * varying numbers of arguments.
+ */
+
+/**
  * @ingroup makeeventmemptr
  * Helper for the MakeEvent functions which take a class method.
  *
@@ -580,6 +595,11 @@
     return Ptr<T1>(const_cast<T1*>(PeekPointer(p)));
 }
 
+/**
+ * @copydoc ConstCast()
+ * @hidecaller
+ * @hiderefby
+ */
 template <typename T1, typename T2>
 Ptr<T1>
 DynamicCast(const Ptr<T2>& p)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/random-variable-stream.cc ns-3.47/src/core/model/random-variable-stream.cc
--- ns-3.46/src/core/model/random-variable-stream.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/random-variable-stream.cc	2026-02-16 16:08:49.585229900 -0800
@@ -22,9 +22,7 @@
 #include "rng-seed-manager.h"
 #include "rng-stream.h"
 #include "string.h"
-#include "uinteger.h"
 
-#include <algorithm> // upper_bound
 #include <cmath>
 #include <iostream>
 #include <numbers>
@@ -687,6 +685,12 @@
     NS_LOG_FUNCTION(this);
 }
 
+void
+NormalRandomVariable::SetStdDev(double stdDev)
+{
+    m_variance = stdDev * stdDev;
+}
+
 double
 NormalRandomVariable::GetMean() const
 {
@@ -700,6 +704,12 @@
 }
 
 double
+NormalRandomVariable::GetStdDev() const
+{
+    return sqrt(m_variance);
+}
+
+double
 NormalRandomVariable::GetBound() const
 {
     return m_bound;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/random-variable-stream.h ns-3.47/src/core/model/random-variable-stream.h
--- ns-3.46/src/core/model/random-variable-stream.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/random-variable-stream.h	2026-02-16 16:08:49.585229900 -0800
@@ -1007,6 +1007,13 @@
     double GetVariance() const;
 
     /**
+     * @brief Returns the standard deviation value for the normal distribution returned by this RNG
+     * stream.
+     * @return The standard deviation value for the normal distribution returned by this RNG stream.
+     */
+    double GetStdDev() const;
+
+    /**
      * @brief Returns the bound on values that can be returned by this RNG stream.
      * @return The bound on values that can be returned by this RNG stream.
      */
@@ -1025,6 +1032,14 @@
     /** @copydoc GetValue(double,double,double) */
     uint32_t GetInteger(uint32_t mean, uint32_t variance, uint32_t bound);
 
+    /**
+     * Set standard deviation of this normal distribution RNG stream.
+     * The deviation is squared and stored as variance, which can also be set
+     * via the variance attribute.
+     * @param [in] stdDev Standard deviation value for the normal distribution.
+     */
+    void SetStdDev(double stdDev);
+
     // Inherited
     double GetValue() override;
     using RandomVariableStream::GetInteger;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/realtime-simulator-impl.cc ns-3.47/src/core/model/realtime-simulator-impl.cc
--- ns-3.46/src/core/model/realtime-simulator-impl.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/realtime-simulator-impl.cc	2026-02-16 16:08:49.585229900 -0800
@@ -7,19 +7,16 @@
 #include "realtime-simulator-impl.h"
 
 #include "assert.h"
-#include "boolean.h"
 #include "enum.h"
 #include "event-impl.h"
 #include "fatal-error.h"
 #include "log.h"
-#include "pointer.h"
 #include "ptr.h"
 #include "scheduler.h"
 #include "simulator.h"
 #include "synchronizer.h"
 #include "wall-clock-synchronizer.h"
 
-#include <cmath>
 #include <mutex>
 #include <thread>
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/rng-seed-manager.cc ns-3.47/src/core/model/rng-seed-manager.cc
--- ns-3.46/src/core/model/rng-seed-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/rng-seed-manager.cc	2026-02-16 16:08:49.585229900 -0800
@@ -7,7 +7,6 @@
 
 #include "rng-seed-manager.h"
 
-#include "attribute-helper.h"
 #include "config.h"
 #include "global-value.h"
 #include "log.h"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/rng-seed-manager.h ns-3.47/src/core/model/rng-seed-manager.h
--- ns-3.46/src/core/model/rng-seed-manager.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/rng-seed-manager.h	2026-02-16 16:08:49.586229800 -0800
@@ -44,6 +44,8 @@
      * @note While the underlying RNG takes six integer values as a seed;
      * it is sufficient to set these all to the same integer, so we provide
      * a simpler interface here that just takes one integer.
+     *
+     * @hidecaller
      */
     static void SetSeed(uint32_t seed);
 
@@ -79,6 +81,7 @@
      * @endcode
      *
      * @param [in] run The run number.
+     * @hidecaller
      */
     static void SetRun(uint64_t run);
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/rng-stream.cc ns-3.47/src/core/model/rng-stream.cc
--- ns-3.46/src/core/model/rng-stream.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/rng-stream.cc	2026-02-16 16:08:49.586229800 -0800
@@ -13,7 +13,6 @@
 #include "fatal-error.h"
 #include "log.h"
 
-#include <cstdlib>
 #include <iostream>
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/scheduler.cc ns-3.47/src/core/model/scheduler.cc
--- ns-3.46/src/core/model/scheduler.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/scheduler.cc	2026-02-16 16:08:49.586229800 -0800
@@ -8,7 +8,6 @@
 
 #include "scheduler.h"
 
-#include "assert.h"
 #include "log.h"
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/simple-ref-count.h ns-3.47/src/core/model/simple-ref-count.h
--- ns-3.46/src/core/model/simple-ref-count.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/simple-ref-count.h	2026-02-16 16:08:49.586229800 -0800
@@ -28,6 +28,9 @@
 /**
  * @ingroup ptr
  * @brief Empty class, used as a default parent class for SimpleRefCount
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 class Empty
 {
@@ -64,6 +67,9 @@
  *      to the object it manages exist anymore.
  *
  * Interesting users of this class include ns3::Object as well as ns3::Packet.
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 template <typename T, typename PARENT = Empty, typename DELETER = DefaultDeleter<T>>
 class SimpleRefCount : public PARENT
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/simulator.cc ns-3.47/src/core/model/simulator.cc
--- ns-3.46/src/core/model/simulator.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/simulator.cc	2026-02-16 16:08:49.586229800 -0800
@@ -7,9 +7,8 @@
  */
 #include "simulator.h"
 
-#include "assert.h"
-#include "des-metrics.h"
 #include "event-impl.h"
+#include "fatal-error.h"
 #include "global-value.h"
 #include "log.h"
 #include "map-scheduler.h"
@@ -19,14 +18,9 @@
 #include "simulator-impl.h"
 #include "string.h"
 
-#include "ns3/core-config.h"
-
-#include <cmath>
-#include <fstream>
-#include <iomanip>
-#include <iostream>
-#include <list>
-#include <vector>
+#ifdef ENABLE_DES_METRICS
+#include "des-metrics.h"
+#endif
 
 /**
  * @file
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/simulator.h ns-3.47/src/core/model/simulator.h
--- ns-3.46/src/core/model/simulator.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/simulator.h	2026-02-16 16:08:49.586229800 -0800
@@ -111,6 +111,8 @@
      * After this method has been invoked, it is actually possible
      * to restart a new simulation with a set of calls to Simulator::Run,
      * Simulator::Schedule and Simulator::ScheduleWithContext.
+     *
+     * @hidecaller
      */
     static void Destroy();
 
@@ -134,6 +136,8 @@
      *   - The user called Simulator::Stop with a stop time and the
      *     expiration time of the next event to be processed
      *     is greater than or equal to the stop time.
+     *
+     * @hidecaller
      */
     static void Run();
 
@@ -144,6 +148,8 @@
      * If a running event invokes this method, it will be the last
      * event executed by the Simulator::Run method before
      * returning to the caller.
+     *
+     * @hidecaller
      */
     static void Stop();
 
@@ -156,6 +162,8 @@
      * current simulation time.
      * @param [in] delay The stop time, relative to the current time.
      * @return The stop EventId.
+     *
+     * @hidecaller
      */
     static EventId Stop(const Time& delay);
 
@@ -224,6 +232,7 @@
      * @param [in] f The function to invoke.
      * @param [in] args Arguments to pass to MakeEvent.
      * @returns The id for the scheduled event.
+     * @hidecaller
      */
     template <typename FUNC,
               std::enable_if_t<!std::is_convertible_v<FUNC, Ptr<EventImpl>>, int> = 0,
@@ -245,6 +254,7 @@
      * @param [in] f The function to invoke.
      * @param [in] args Arguments to pass to the invoked function.
      * @returns The id for the scheduled event.
+     * @hidecaller
      */
     template <typename... Us, typename... Ts>
     static EventId Schedule(const Time& delay, void (*f)(Us...), Ts&&... args);
@@ -270,6 +280,7 @@
      * @param [in] delay The relative expiration time of the event.
      * @param [in] f The function to invoke.
      * @param [in] args Arguments to pass to MakeEvent.
+     * @hidecaller
      */
     template <typename FUNC,
               std::enable_if_t<!std::is_convertible_v<FUNC, Ptr<EventImpl>>, int> = 0,
@@ -288,6 +299,7 @@
      * @param [in] delay The relative expiration time of the event.
      * @param [in] f The function to invoke.
      * @param [in] args Arguments to pass to the invoked function.
+     * @hidecaller
      */
     template <typename... Us, typename... Ts>
     static void ScheduleWithContext(uint32_t context,
@@ -313,6 +325,7 @@
      * @param [in] f The function to invoke.
      * @param [in] args Arguments to pass to the invoked function.
      * @return The EventId of the scheduled event.
+     * @hidecaller
      */
     template <typename FUNC,
               std::enable_if_t<!std::is_convertible_v<FUNC, Ptr<EventImpl>>, int> = 0,
@@ -330,6 +343,7 @@
      * @param [in] f The function to invoke.
      * @param [in] args Arguments to pass to MakeEvent.
      * @return The EventId of the scheduled event.
+     * @hidecaller
      */
     template <typename... Us, typename... Ts>
     static EventId ScheduleNow(void (*f)(Us...), Ts&&... args);
@@ -428,6 +442,7 @@
      * Return the current simulation virtual time.
      *
      * @returns The current virtual time.
+     * @hidecaller
      */
     static Time Now();
 
@@ -457,6 +472,7 @@
      * @param [in] delay Delay until the event expires.
      * @param [in] event The event to schedule.
      * @returns A unique identifier for the newly-scheduled event.
+     * @hidecaller
      */
     static EventId Schedule(const Time& delay, const Ptr<EventImpl>& event);
 
@@ -467,6 +483,7 @@
      * @param [in] delay Delay until the event expires.
      * @param [in] context Event context.
      * @param [in] event The event to schedule.
+     * @hidecaller
      */
     static void ScheduleWithContext(uint32_t context, const Time& delay, EventImpl* event);
 
@@ -484,6 +501,7 @@
      *
      * @param [in] event The event to schedule.
      * @returns A unique identifier for the newly-scheduled event.
+     * @hidecaller
      */
     static EventId ScheduleNow(const Ptr<EventImpl>& event);
 
@@ -537,6 +555,7 @@
  *   Simulator::Schedule (Seconds (2.0) - Now (), &my_function);
  * @endcode
  * @return The current simulation time.
+ * @hidecaller
  */
 Time Now();
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/system-path.cc ns-3.47/src/core/model/system-path.cc
--- ns-3.46/src/core/model/system-path.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/system-path.cc	2026-02-16 16:08:49.586229800 -0800
@@ -116,7 +116,7 @@
 }
 
 std::string
-FindSelfDirectory()
+FindSelf()
 {
     /**
      * This function returns the path to the running $PREFIX.
@@ -134,11 +134,12 @@
         ssize_t size = 1024;
         char* buffer = (char*)malloc(size);
         memset(buffer, 0, size);
-        int status;
+        int pathLength;
         while (true)
         {
-            status = readlink("/proc/self/exe", buffer, size);
-            if (status != 1 || (status == -1 && errno != ENAMETOOLONG))
+            pathLength = readlink("/proc/self/exe", buffer, size);
+            if ((pathLength > 0 && pathLength < size) ||
+                (pathLength == -1 && errno != ENAMETOOLONG))
             {
                 break;
             }
@@ -147,7 +148,7 @@
             buffer = (char*)malloc(size);
             memset(buffer, 0, size);
         }
-        if (status == -1)
+        if (pathLength == -1)
         {
             NS_FATAL_ERROR("Oops, could not find self directory.");
         }
@@ -159,15 +160,15 @@
         //  LPTSTR = char *
         DWORD size = 1024;
         auto lpFilename = (LPTSTR)malloc(sizeof(TCHAR) * size);
-        DWORD status = GetModuleFileName(nullptr, lpFilename, size);
-        while (status == size)
+        DWORD pathLength = GetModuleFileName(nullptr, lpFilename, size);
+        while (pathLength == size)
         {
             size = size * 2;
             free(lpFilename);
             lpFilename = (LPTSTR)malloc(sizeof(TCHAR) * size);
-            status = GetModuleFileName(nullptr, lpFilename, size);
+            pathLength = GetModuleFileName(nullptr, lpFilename, size);
         }
-        NS_ASSERT(status != 0);
+        NS_ASSERT(pathLength != 0);
         filename = lpFilename;
         free(lpFilename);
     }
@@ -202,7 +203,13 @@
         filename = buf;
     }
 #endif
-    return Dirname(filename);
+    return filename;
+}
+
+std::string
+FindSelfDirectory()
+{
+    return Dirname(FindSelf());
 }
 
 std::string
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/system-path.h ns-3.47/src/core/model/system-path.h
--- ns-3.46/src/core/model/system-path.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/system-path.h	2026-02-16 16:08:49.586229800 -0800
@@ -41,6 +41,17 @@
  * @ingroup systempath
  * Get the file system path to the current executable.
  *
+ * This path contains the currently-executing binary. For its directory,
+ * use FindSelfDirectory() instead.
+ *
+ * @return The path to the currently-executing binary
+ */
+std::string FindSelf();
+
+/**
+ * @ingroup systempath
+ * Get the file system path to the current executable.
+ *
  * This path is only equivalent to the current working directory when
  * the executable is executed in its parent directory.
  *
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/test.cc ns-3.47/src/core/model/test.cc
--- ns-3.46/src/core/model/test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/test.cc	2026-02-16 16:08:49.587229700 -0800
@@ -6,15 +6,19 @@
 
 #include "test.h"
 
-#include "abort.h"
 #include "assert.h"
 #include "config.h"
-#include "des-metrics.h"
+#include "fatal-error.h"
 #include "log.h"
 #include "rng-seed-manager.h"
 #include "singleton.h"
 #include "system-path.h"
 
+#ifdef ENABLE_DES_METRICS
+#include "des-metrics.h"
+#endif
+
+#include <algorithm>
 #include <cmath>
 #include <cstring>
 #include <list>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/test.h ns-3.47/src/core/model/test.h
--- ns-3.46/src/core/model/test.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/test.h	2026-02-16 16:08:49.587229700 -0800
@@ -1044,6 +1044,9 @@
  * method, and use the NS_TEST_* macros within DoRun.
  *
  * @see sample-test-suite.cc
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 class TestCase
 {
@@ -1062,11 +1065,17 @@
     virtual ~TestCase();
 
     // Delete copy constructor and assignment operator to avoid misuse
+
+    // Doxygen erroneously attributes all TestCases to this c'tor,
+    // hence we have to hide the caller graph
+    /** @hidecaller */
     TestCase(const TestCase&) = delete;
+
     TestCase& operator=(const TestCase&) = delete;
 
     /**
      * @return The name of this test
+     * @hidecaller
      */
     std::string GetName() const;
 
@@ -1075,6 +1084,7 @@
      * @brief Constructor.
      *
      * @param [in] name The name of the new TestCase created
+     * @hidecaller
      */
     TestCase(std::string name);
 
@@ -1084,6 +1094,7 @@
      * @param [in] testCase Pointer to the TestCase object to be added.
      * @param [in] duration Amount of time this test takes to execute
      *             (defaults to QUICK).
+     * @hidecaller
      */
     void AddTestCase(TestCase* testCase, Duration duration = Duration::QUICK);
 
@@ -1245,6 +1256,9 @@
  * @brief A suite of tests to run.
  *
  * @see sample-test-suite.cc
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 class TestSuite : public TestCase
 {
@@ -1267,6 +1281,7 @@
      *
      * @param [in] name The name of the test suite.
      * @param [in] type The TestType of the test suite (defaults to UNIT test).
+     * @hidecaller
      */
     TestSuite(std::string name, Type type = Type::UNIT);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/time.cc ns-3.47/src/core/model/time.cc
--- ns-3.46/src/core/model/time.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/time.cc	2026-02-16 16:08:49.587229700 -0800
@@ -11,8 +11,7 @@
 #include "log.h"
 #include "nstime.h"
 
-#include <cmath>   // pow
-#include <iomanip> // showpos
+#include <cmath> // pow
 #include <mutex>
 #include <sstream>
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/timer.cc ns-3.47/src/core/model/timer.cc
--- ns-3.46/src/core/model/timer.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/timer.cc	2026-02-16 16:08:49.587229700 -0800
@@ -8,7 +8,6 @@
 #include "timer.h"
 
 #include "log.h"
-#include "simulation-singleton.h"
 #include "simulator.h"
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/traced-callback.h ns-3.47/src/core/model/traced-callback.h
--- ns-3.46/src/core/model/traced-callback.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/traced-callback.h	2026-02-16 16:08:49.587229700 -0800
@@ -37,6 +37,9 @@
  * number of arguments.
  *
  * @tparam Ts \explicit Types of the functor arguments.
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 template <typename... Ts>
 class TracedCallback
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/traced-value.h ns-3.47/src/core/model/traced-value.h
--- ns-3.46/src/core/model/traced-value.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/traced-value.h	2026-02-16 16:08:49.587229700 -0800
@@ -152,7 +152,7 @@
     /**
      * Copy from a TracedValue of a compatible type.
      * @tparam U \deduced The underlying type of the other TracedValue.
-     * @param [in] other The other TracedValuet to copy.
+     * @param [in] other The other TracedValue to copy.
      */
     template <typename U>
     TracedValue(const TracedValue<U>& other)
@@ -327,6 +327,7 @@
  * @param [in] rhs The right-hand argument.
  * @returns The Boolean result of comparing the underlying values.
  */
+/**@{*/
 template <typename T, typename U>
 bool
 operator==(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -335,7 +336,6 @@
     return lhs.Get() == rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator==(const TracedValue<T>& lhs, const U& rhs)
@@ -344,7 +344,6 @@
     return lhs.Get() == rhs;
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator==(const U& lhs, const TracedValue<T>& rhs)
@@ -353,7 +352,6 @@
     return lhs == rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator!=(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -362,7 +360,6 @@
     return lhs.Get() != rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator!=(const TracedValue<T>& lhs, const U& rhs)
@@ -371,7 +368,6 @@
     return lhs.Get() != rhs;
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator!=(const U& lhs, const TracedValue<T>& rhs)
@@ -380,7 +376,6 @@
     return lhs != rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator<=(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -389,7 +384,6 @@
     return lhs.Get() <= rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator<=(const TracedValue<T>& lhs, const U& rhs)
@@ -398,7 +392,6 @@
     return lhs.Get() <= rhs;
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator<=(const U& lhs, const TracedValue<T>& rhs)
@@ -407,7 +400,6 @@
     return lhs <= rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator>=(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -416,7 +408,6 @@
     return lhs.Get() >= rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator>=(const TracedValue<T>& lhs, const U& rhs)
@@ -425,7 +416,6 @@
     return lhs.Get() >= rhs;
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator>=(const U& lhs, const TracedValue<T>& rhs)
@@ -434,7 +424,6 @@
     return lhs >= rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator<(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -443,7 +432,6 @@
     return lhs.Get() < rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator<(const TracedValue<T>& lhs, const U& rhs)
@@ -452,7 +440,6 @@
     return lhs.Get() < rhs;
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator<(const U& lhs, const TracedValue<T>& rhs)
@@ -461,7 +448,6 @@
     return lhs < rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator>(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -470,7 +456,6 @@
     return lhs.Get() > rhs.Get();
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator>(const TracedValue<T>& lhs, const U& rhs)
@@ -479,7 +464,6 @@
     return lhs.Get() > rhs;
 }
 
-/** @copydoc operator==(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 bool
 operator>(const U& lhs, const TracedValue<T>& rhs)
@@ -488,6 +472,8 @@
     return lhs > rhs.Get();
 }
 
+/**@}*/
+
 /**
  * Infix arithmetic operator for TracedValue.
  *
@@ -498,9 +484,9 @@
  * @tparam U \deduced The underlying type held by the right-hand argument.
  * @param [in] lhs The left-hand argument.
  * @param [in] rhs The right-hand argument.
- * @returns The result of doing the operator on
- *     the underlying values.
+ * @returns The result of doing the operator on the underlying values.
  */
+/**@{*/
 template <typename T, typename U>
 auto
 operator+(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -510,7 +496,6 @@
     return TracedValue<decltype(lhs.Get() + rhs.Get())>(lhs.Get() + rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator+(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() + rhs)>
@@ -519,7 +504,6 @@
     return TracedValue<decltype(lhs.Get() + rhs)>(lhs.Get() + rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator+(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs + rhs.Get())>
@@ -528,7 +512,6 @@
     return TracedValue<decltype(lhs + rhs.Get())>(lhs + rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator-(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -538,7 +521,6 @@
     return TracedValue<decltype(lhs.Get() - rhs.Get())>(lhs.Get() - rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator-(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() - rhs)>
@@ -547,7 +529,6 @@
     return TracedValue<decltype(lhs.Get() - rhs)>(lhs.Get() - rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator-(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs - rhs.Get())>
@@ -556,7 +537,6 @@
     return TracedValue<decltype(lhs - rhs.Get())>(lhs - rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator*(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -566,7 +546,6 @@
     return TracedValue<decltype(lhs.Get() * rhs.Get())>(lhs.Get() * rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator*(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() * rhs)>
@@ -575,7 +554,6 @@
     return TracedValue<decltype(lhs.Get() * rhs)>(lhs.Get() * rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator*(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs + rhs.Get())>
@@ -584,7 +562,6 @@
     return TracedValue<decltype(lhs + rhs.Get())>(lhs * rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator/(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -594,7 +571,6 @@
     return TracedValue<decltype(lhs.Get() / rhs.Get())>(lhs.Get() / rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator/(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() / rhs)>
@@ -603,7 +579,6 @@
     return TracedValue<decltype(lhs.Get() / rhs)>(lhs.Get() / rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator/(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs / rhs.Get())>
@@ -612,7 +587,6 @@
     return TracedValue<decltype(lhs / rhs.Get())>(lhs / rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator%(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -622,7 +596,6 @@
     return TracedValue<decltype(lhs.Get() % rhs.Get())>(lhs.Get() % rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator%(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() % rhs)>
@@ -631,7 +604,6 @@
     return TracedValue<decltype(lhs.Get() % rhs)>(lhs.Get() % rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator%(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs % rhs.Get())>
@@ -640,7 +612,6 @@
     return TracedValue<decltype(lhs % rhs.Get())>(lhs % rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator^(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -650,7 +621,6 @@
     return TracedValue<decltype(lhs.Get() ^ rhs.Get())>(lhs.Get() ^ rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator^(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() ^ rhs)>
@@ -659,7 +629,6 @@
     return TracedValue<decltype(lhs.Get() ^ rhs)>(lhs.Get() ^ rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator^(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs ^ rhs.Get())>
@@ -668,7 +637,6 @@
     return TracedValue<decltype(lhs ^ rhs.Get())>(lhs ^ rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator|(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -678,7 +646,6 @@
     return TracedValue<decltype(lhs.Get() | rhs.Get())>(lhs.Get() | rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator|(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() | rhs)>
@@ -687,7 +654,6 @@
     return TracedValue<decltype(lhs.Get() | rhs)>(lhs.Get() | rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator|(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs | rhs.Get())>
@@ -696,7 +662,6 @@
     return TracedValue<decltype(lhs | rhs.Get())>(lhs | rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator&(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -706,7 +671,6 @@
     return TracedValue<decltype(lhs.Get() & rhs.Get())>(lhs.Get() & rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator&(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() & rhs)>
@@ -715,7 +679,6 @@
     return TracedValue<decltype(lhs.Get() & rhs)>(lhs.Get() & rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator&(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs & rhs.Get())>
@@ -724,7 +687,6 @@
     return TracedValue<decltype(lhs & rhs.Get())>(lhs & rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator<<(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -734,7 +696,6 @@
     return TracedValue<decltype(lhs.Get() << rhs.Get())>(lhs.Get() << rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator<<(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() << rhs)>
@@ -743,7 +704,6 @@
     return TracedValue<decltype(lhs.Get() << rhs)>(lhs.Get() << rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator<<(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs << rhs.Get())>
@@ -752,7 +712,6 @@
     return TracedValue<decltype(lhs << rhs.Get())>(lhs << rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator>>(const TracedValue<T>& lhs, const TracedValue<U>& rhs)
@@ -762,7 +721,6 @@
     return TracedValue<decltype(lhs.Get() >> rhs.Get())>(lhs.Get() >> rhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator>>(const TracedValue<T>& lhs, const U& rhs) -> TracedValue<decltype(lhs.Get() >> rhs)>
@@ -771,7 +729,6 @@
     return TracedValue<decltype(lhs.Get() >> rhs)>(lhs.Get() >> rhs);
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs,const TracedValue<U>&rhs) */
 template <typename T, typename U>
 auto
 operator>>(const U& lhs, const TracedValue<T>& rhs) -> TracedValue<decltype(lhs >> rhs.Get())>
@@ -780,6 +737,8 @@
     return TracedValue<decltype(lhs >> rhs.Get())>(lhs >> rhs.Get());
 }
 
+/**@}*/
+
 /**
  * Operator assignment for TracedValue.
  *
@@ -791,9 +750,9 @@
  * @tparam U \deduced The underlying type held by the right-hand argument.
  * @param [in] lhs The left-hand argument.
  * @param [in] rhs The right-hand argument.
- * @returns The result of doing the operator on
- *     the underlying values.
+ * @returns The result of doing the operator on the underlying values.
  */
+/**@{*/
 template <typename T, typename U>
 TracedValue<T>&
 operator+=(TracedValue<T>& lhs, const U& rhs)
@@ -805,7 +764,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator-=(TracedValue<T>& lhs, const U& rhs)
@@ -817,7 +775,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator*=(TracedValue<T>& lhs, const U& rhs)
@@ -829,7 +786,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator/=(TracedValue<T>& lhs, const U& rhs)
@@ -841,7 +797,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator%=(TracedValue<T>& lhs, const U& rhs)
@@ -853,7 +808,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator<<=(TracedValue<T>& lhs, const U& rhs)
@@ -865,7 +819,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator>>=(TracedValue<T>& lhs, const U& rhs)
@@ -877,7 +830,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator&=(TracedValue<T>& lhs, const U& rhs)
@@ -889,7 +841,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator|=(TracedValue<T>& lhs, const U& rhs)
@@ -901,7 +852,6 @@
     return lhs;
 }
 
-/** @copydoc operator+=(TracedValue<T>&lhs,const U&rhs) */
 template <typename T, typename U>
 TracedValue<T>&
 operator^=(TracedValue<T>& lhs, const U& rhs)
@@ -913,14 +863,16 @@
     return lhs;
 }
 
+/**@}*/
+
 /**
  * Unary arithmetic operator for TracedValue.
  *
  * @tparam T \deduced The underlying type held by the TracedValue.
  * @param [in] lhs The TracedValue.
- * @returns The result of doing the operator on
- *     the underlying values.
+ * @returns The result of doing the operator on the underlying values.
  */
+/**@{*/
 template <typename T>
 TracedValue<T>
 operator+(const TracedValue<T>& lhs)
@@ -929,7 +881,6 @@
     return TracedValue<T>(+lhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs) */
 template <typename T>
 TracedValue<T>
 operator-(const TracedValue<T>& lhs)
@@ -938,7 +889,6 @@
     return TracedValue<T>(-lhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs) */
 template <typename T>
 TracedValue<T>
 operator~(const TracedValue<T>& lhs)
@@ -947,7 +897,6 @@
     return TracedValue<T>(~lhs.Get());
 }
 
-/** @copydoc operator+(const TracedValue<T>&lhs) */
 template <typename T>
 TracedValue<T>
 operator!(const TracedValue<T>& lhs)
@@ -956,6 +905,8 @@
     return TracedValue<T>(!lhs.Get());
 }
 
+/**@}*/
+
 /**@}*/ // \ingroup tracing
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/trace-source-accessor.h ns-3.47/src/core/model/trace-source-accessor.h
--- ns-3.46/src/core/model/trace-source-accessor.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/trace-source-accessor.h	2026-02-16 16:08:49.587229700 -0800
@@ -101,6 +101,7 @@
  *
  * @param [in] a The trace source
  * @returns The TraceSourceAccessor
+ * @hidecaller
  */
 template <typename T>
 Ptr<const TraceSourceAccessor> MakeTraceSourceAccessor(T a);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/type-id.cc ns-3.47/src/core/model/type-id.cc
--- ns-3.46/src/core/model/type-id.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/type-id.cc	2026-02-16 16:08:49.588230000 -0800
@@ -12,9 +12,7 @@
 #include "singleton.h"
 #include "trace-source-accessor.h"
 
-#include <iomanip>
 #include <map>
-#include <sstream>
 #include <vector>
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/type-id.h ns-3.47/src/core/model/type-id.h
--- ns-3.46/src/core/model/type-id.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/type-id.h	2026-02-16 16:08:49.588230000 -0800
@@ -13,6 +13,7 @@
 #include "attribute.h"
 #include "callback.h"
 #include "deprecated.h"
+#include "event-id.h"
 #include "hash.h"
 #include "trace-source-accessor.h"
 
@@ -132,6 +133,8 @@
      *
      * This method cannot fail: it will crash if the input
      * name is not a valid TypeId name.
+     *
+     * @hidecaller
      */
     static TypeId LookupByName(std::string name);
     /**
@@ -244,6 +247,7 @@
      * Get the name.
      *
      * @returns The name of this interface.
+     * @hidecaller
      */
     std::string GetName() const;
 
@@ -326,6 +330,8 @@
      *
      * Record in this TypeId which TypeId is the TypeId
      * of the base class of the subclass.
+     *
+     * @hidecaller
      */
     TypeId SetParent(TypeId tid);
     /**
@@ -336,6 +342,8 @@
      *
      * Record in this TypeId which TypeId is the TypeId
      * of the base class of the subclass.
+     *
+     * @hidecaller
      */
     template <typename T>
     TypeId SetParent();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/uinteger.cc ns-3.47/src/core/model/uinteger.cc
--- ns-3.46/src/core/model/uinteger.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/uinteger.cc	2026-02-16 16:08:49.588230000 -0800
@@ -7,7 +7,6 @@
  */
 #include "uinteger.h"
 
-#include "fatal-error.h"
 #include "log.h"
 
 #include <sstream>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/unix-fd-reader.cc ns-3.47/src/core/model/unix-fd-reader.cc
--- ns-3.46/src/core/model/unix-fd-reader.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/unix-fd-reader.cc	2026-02-16 16:08:49.588230000 -0800
@@ -9,7 +9,6 @@
 #include "fatal-error.h"
 #include "fd-reader.h"
 #include "log.h"
-#include "simple-ref-count.h"
 #include "simulator.h"
 
 #include <cerrno>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/vector.cc ns-3.47/src/core/model/vector.cc
--- ns-3.46/src/core/model/vector.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/vector.cc	2026-02-16 16:08:49.589229800 -0800
@@ -7,11 +7,9 @@
  */
 #include "vector.h"
 
-#include "fatal-error.h"
 #include "log.h"
 
 #include <cmath>
-#include <sstream>
 #include <tuple>
 
 /**
@@ -70,14 +68,14 @@
 Vector3D::GetLength() const
 {
     NS_LOG_FUNCTION(this);
-    return std::sqrt(x * x + y * y + z * z);
+    return std::hypot(x, y, z);
 }
 
 double
 Vector2D::GetLength() const
 {
     NS_LOG_FUNCTION(this);
-    return std::sqrt(x * x + y * y);
+    return std::hypot(x, y);
 }
 
 double
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/version.cache ns-3.47/src/core/model/version.cache
--- ns-3.46/src/core/model/version.cache	2025-10-01 18:47:01.000000000 -0700
+++ ns-3.47/src/core/model/version.cache	2026-02-16 16:16:45.552951000 -0800
@@ -1,10 +1,10 @@
-CLOSEST_TAG = '"ns-3.46"'
-VERSION_COMMIT_HASH = '"gea50b72ab"'
+CLOSEST_TAG = '"ns-3.47"'
+VERSION_COMMIT_HASH = '"ge2c9e30c6"'
 VERSION_DIRTY_FLAG = '0'
 VERSION_MAJOR = '3'
-VERSION_MINOR = '46'
+VERSION_MINOR = '47'
 VERSION_PATCH = '0'
 VERSION_RELEASE_CANDIDATE = '""'
-VERSION_TAG = '"ns-3.46"'
+VERSION_TAG = '"ns-3.47"'
 VERSION_TAG_DISTANCE = '0'
 VERSION_BUILD_PROFILE = 'default'
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/model/wall-clock-synchronizer.cc ns-3.47/src/core/model/wall-clock-synchronizer.cc
--- ns-3.46/src/core/model/wall-clock-synchronizer.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/model/wall-clock-synchronizer.cc	2026-02-16 16:08:49.589229800 -0800
@@ -10,7 +10,6 @@
 
 #include <chrono>
 #include <condition_variable>
-#include <ctime> // clock_t
 #include <mutex>
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/attribute-container-test-suite.cc ns-3.47/src/core/test/attribute-container-test-suite.cc
--- ns-3.46/src/core/test/attribute-container-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/attribute-container-test-suite.cc	2026-02-16 16:08:49.590229700 -0800
@@ -20,7 +20,6 @@
 #include "ns3/uinteger.h"
 
 #include <algorithm>
-#include <iterator>
 #include <list>
 #include <map>
 #include <utility>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/command-line-test-suite.cc ns-3.47/src/core/test/command-line-test-suite.cc
--- ns-3.46/src/core/test/command-line-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/command-line-test-suite.cc	2026-02-16 16:08:49.590229700 -0800
@@ -15,7 +15,6 @@
 #include "ns3/type-id.h"
 
 #include <cstdarg>
-#include <cstdlib>
 #include <sstream>
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/config-test-suite.cc ns-3.47/src/core/test/config-test-suite.cc
--- ns-3.46/src/core/test/config-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/config-test-suite.cc	2026-02-16 16:08:49.590229700 -0800
@@ -18,8 +18,6 @@
 #include "ns3/trace-source-accessor.h"
 #include "ns3/traced-value.h"
 
-#include <sstream>
-
 /**
  * @file
  * @ingroup core-tests
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/examples-as-tests-test-suite.cc ns-3.47/src/core/test/examples-as-tests-test-suite.cc
--- ns-3.46/src/core/test/examples-as-tests-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/examples-as-tests-test-suite.cc	2026-02-16 16:08:49.590229700 -0800
@@ -9,8 +9,6 @@
 #include "ns3/example-as-test.h"
 #include "ns3/system-path.h"
 
-#include <vector>
-
 using namespace ns3;
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/examples-to-run.py ns-3.47/src/core/test/examples-to-run.py
--- ns-3.46/src/core/test/examples-to-run.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/examples-to-run.py	2026-02-16 16:08:49.590229700 -0800
@@ -13,6 +13,7 @@
     ("main-ptr", "True", "True"),
     ("main-random-variable", "True", "False"),
     ("sample-random-variable", "True", "True"),
+    ("system-path-examples", "True", "True"),
     ("test-string-value-formatting", "True", "True"),
 ]
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/length-test-suite.cc ns-3.47/src/core/test/length-test-suite.cc
--- ns-3.46/src/core/test/length-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/length-test-suite.cc	2026-02-16 16:08:49.590229700 -0800
@@ -23,7 +23,6 @@
 #include <functional>
 #include <initializer_list>
 #include <iomanip>
-#include <limits>
 #include <map>
 #include <sstream>
 #include <string>
@@ -303,6 +302,10 @@
     void TestModReturnsNonZero();
     /** @} */
 
+    /**
+     * Call graph was not generated because of its size.
+     * @hidecallgraph
+     */
     void DoRun() override;
 };
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/matrix-array-test-suite.cc ns-3.47/src/core/test/matrix-array-test-suite.cc
--- ns-3.46/src/core/test/matrix-array-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/matrix-array-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -93,7 +93,6 @@
      */
     MatrixArrayTestCase<T>& operator=(MatrixArrayTestCase<T>&&) = default;
 
-  protected:
   private:
     void DoRun() override;
 };
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/one-uniform-random-variable-many-get-value-calls-test-suite.cc ns-3.47/src/core/test/one-uniform-random-variable-many-get-value-calls-test-suite.cc
--- ns-3.46/src/core/test/one-uniform-random-variable-many-get-value-calls-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/one-uniform-random-variable-many-get-value-calls-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -11,8 +11,6 @@
 #include "ns3/random-variable-stream.h"
 #include "ns3/test.h"
 
-#include <vector>
-
 /**
  * @file
  * @ingroup core-tests
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/pair-value-test-suite.cc ns-3.47/src/core/test/pair-value-test-suite.cc
--- ns-3.46/src/core/test/pair-value-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/pair-value-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -16,8 +16,6 @@
 #include "ns3/test.h"
 #include "ns3/type-id.h"
 
-#include <algorithm>
-#include <iterator>
 #include <sstream>
 #include <utility>
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/random-variable-stream-test-suite.cc ns-3.47/src/core/test/random-variable-stream-test-suite.cc
--- ns-3.46/src/core/test/random-variable-stream-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/random-variable-stream-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -22,7 +22,6 @@
 
 #include <cmath>
 #include <ctime>
-#include <fstream>
 #include <gsl/gsl_cdf.h>
 #include <gsl/gsl_histogram.h>
 #include <gsl/gsl_randist.h>
@@ -744,6 +743,23 @@
                               expectedMean,
                               expectedRms * TOLERANCE,
                               "Wrong mean value.");
+
+    // Repeat test when setting via stddev attribute instead of variance
+    x->SetStdDev(sqrt(variance));
+
+    // Calculate the mean of these values.
+    valueMean = Average(x);
+
+    // The expected value for the mean of the values returned by a
+    // normally distributed random variable is equal to mean.
+    expectedMean = mean;
+    expectedRms = mean / std::sqrt(variance * N_MEASUREMENTS);
+
+    // Test that values have approximately the right mean value.
+    NS_TEST_ASSERT_MSG_EQ_TOL(valueMean,
+                              expectedMean,
+                              expectedRms * TOLERANCE,
+                              "Wrong mean value.");
 }
 
 /**
@@ -2888,7 +2904,7 @@
 }
 
 /**
- * @ingroup rng-test
+ * @ingroup rng-tests
  * @ingroup tests
  *
  * @brief Test the Shuffle function
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/rng-test-suite.cc ns-3.47/src/core/test/rng-test-suite.cc
--- ns-3.46/src/core/test/rng-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/rng-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -7,9 +7,7 @@
 #include "ns3/rng-seed-manager.h"
 #include "ns3/test.h"
 
-#include <cmath>
 #include <ctime>
-#include <fstream>
 #include <gsl/gsl_cdf.h>
 #include <gsl/gsl_histogram.h>
 
@@ -23,6 +21,7 @@
 
 /**
  * @ingroup core-tests
+ * @ingroup randomvariable
  * @defgroup rng-tests Random number generators tests
  */
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/simulator-test-suite.cc ns-3.47/src/core/test/simulator-test-suite.cc
--- ns-3.46/src/core/test/simulator-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/simulator-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -206,6 +206,9 @@
  * @brief Check that all templates are instantiated correctly.
  *
  * This is a compilation test, it cannot fail at runtime.
+ *
+ * Call graph was not generated because of its size.
+ * @hidecallgraph
  */
 class SimulatorTemplateTestCase : public TestCase
 {
@@ -228,6 +231,10 @@
     /** @} */
 
   private:
+    /**
+     * Call graph was not generated because of its size.
+     * @hidecallgraph
+     */
     void DoRun() override;
 
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/threaded-test-suite.cc ns-3.47/src/core/test/threaded-test-suite.cc
--- ns-3.46/src/core/test/threaded-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/threaded-test-suite.cc	2026-02-16 16:08:49.591230000 -0800
@@ -15,7 +15,6 @@
 #include "ns3/test.h"
 
 #include <chrono> // seconds, milliseconds
-#include <ctime>
 #include <list>
 #include <thread> // sleep_for
 #include <utility>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/core/test/tuple-value-test-suite.cc ns-3.47/src/core/test/tuple-value-test-suite.cc
--- ns-3.46/src/core/test/tuple-value-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/core/test/tuple-value-test-suite.cc	2026-02-16 16:08:49.592229800 -0800
@@ -16,11 +16,7 @@
 #include "ns3/tuple.h"
 #include "ns3/uinteger.h"
 
-#include <algorithm>
-#include <iterator>
-#include <sstream>
 #include <tuple>
-#include <utility>
 
 using namespace ns3;
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/csma/doc/csma.rst ns-3.47/src/csma/doc/csma.rst
--- ns-3.46/src/csma/doc/csma.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/csma/doc/csma.rst	2026-02-16 16:08:49.592229800 -0800
@@ -193,7 +193,7 @@
 
 The CsmaNetDevice implements a random exponential backoff algorithm that is
 executed if the channel is determined to be busy (``TRANSMITTING`` or
-``PPROPAGATING``) when the device wants to start propagating. This results in a
+``PROPAGATING``) when the device wants to start propagating. This results in a
 random delay of up to pow (2, retries) - 1 microseconds before a retry is
 attempted. The default maximum number of retries is 1000.
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/csma/model/csma-net-device.h ns-3.47/src/csma/model/csma-net-device.h
--- ns-3.46/src/csma/model/csma-net-device.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/csma/model/csma-net-device.h	2026-02-16 16:08:49.593229800 -0800
@@ -80,6 +80,10 @@
      */
     ~CsmaNetDevice() override;
 
+    // Delete copy constructor and assignment operator to avoid misuse
+    CsmaNetDevice(const CsmaNetDevice&) = delete;
+    CsmaNetDevice& operator=(const CsmaNetDevice&) = delete;
+
     /**
      * Set the interframe gap used to separate packets.  The interframe gap
      * defines the minimum space required between packets sent by this device.
@@ -353,21 +357,6 @@
 
   private:
     /**
-     * Operator = is declared but not implemented.  This disables the assignment
-     * operator for CsmaNetDevice objects.
-     * @param o object to copy
-     * @returns the copied object
-     */
-    CsmaNetDevice& operator=(const CsmaNetDevice& o);
-
-    /**
-     * Copy constructor is declared but not implemented.  This disables the
-     * copy constructor for CsmaNetDevice objects.
-     * @param o object to copy
-     */
-    CsmaNetDevice(const CsmaNetDevice& o);
-
-    /**
      * Initialization function used during object construction.
      * @param sendEnable if device will be allowed to send
      * @param receiveEnable if device will be allowed to receive
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/csma-layout/examples/csma-star.cc ns-3.47/src/csma-layout/examples/csma-star.cc
--- ns-3.46/src/csma-layout/examples/csma-star.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/csma-layout/examples/csma-star.cc	2026-02-16 16:08:49.592229800 -0800
@@ -49,8 +49,8 @@
     //
     uint32_t nSpokes = 7;
     uint32_t useIpv6 = 0;
-    Ipv6Address ipv6AddressBase = Ipv6Address("2001::");
-    Ipv6Prefix ipv6AddressPrefix = Ipv6Prefix(64);
+    auto ipv6AddressBase = Ipv6Address("2001::");
+    auto ipv6AddressPrefix = Ipv6Prefix(64);
 
     CommandLine cmd(__FILE__);
     cmd.AddValue("nSpokes", "Number of spoke nodes to place in the star", nSpokes);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/dsr/model/dsr-fs-header.h ns-3.47/src/dsr/model/dsr-fs-header.h
--- ns-3.46/src/dsr/model/dsr-fs-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/dsr/model/dsr-fs-header.h	2026-02-16 16:08:49.595229900 -0800
@@ -308,6 +308,12 @@
     uint32_t Deserialize(Buffer::Iterator start) override;
 };
 
+/**
+ * @brief Output streamer for DsrRoutingHeader
+ * @param os The output stream
+ * @param dsr The DsrRoutingHeader
+ * @return The stream
+ */
 static inline std::ostream&
 operator<<(std::ostream& os, const DsrRoutingHeader& dsr)
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/dsr/model/dsr-network-queue.h ns-3.47/src/dsr/model/dsr-network-queue.h
--- ns-3.46/src/dsr/model/dsr-network-queue.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/dsr/model/dsr-network-queue.h	2026-02-16 16:08:49.595229900 -0800
@@ -33,6 +33,7 @@
 namespace dsr
 {
 
+/** Flag used to prioritize packets */
 enum DsrMessageType
 {
     DSR_CONTROL_PACKET = 1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/dsr/model/dsr-option-header.h ns-3.47/src/dsr/model/dsr-option-header.h
--- ns-3.46/src/dsr/model/dsr-option-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/dsr/model/dsr-option-header.h	2026-02-16 16:08:49.595229900 -0800
@@ -1281,10 +1281,16 @@
     Ipv4Address m_realDstAddress;
 };
 
+/**
+ * @brief Output streamer for DsrOptionSRHeader
+ * @param os The output stream
+ * @param dsr The DsrOptionSRHeader
+ * @return The stream
+ */
 [[maybe_unused]] static inline std::ostream&
-operator<<(std::ostream& os, const DsrOptionSRHeader& sr)
+operator<<(std::ostream& os, const DsrOptionSRHeader& dsr)
 {
-    sr.Print(os);
+    dsr.Print(os);
     return os;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/dsr/model/dsr-rcache.cc ns-3.47/src/dsr/model/dsr-rcache.cc
--- ns-3.46/src/dsr/model/dsr-rcache.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/dsr/model/dsr-rcache.cc	2026-02-16 16:08:49.596229800 -0800
@@ -46,6 +46,14 @@
 namespace dsr
 {
 
+/**
+ * @ingroup dsr
+ * @brief Compare route cache entries by hop count and priority,
+ *        equivalent to comparing the expire time.
+ * @param a The first route cache entry
+ * @param b The second route canche entry
+ * @returns `true` if `a < b`
+ */
 bool
 CompareRoutesBoth(const DsrRouteCacheEntry& a, const DsrRouteCacheEntry& b)
 {
@@ -55,6 +63,13 @@
             (a.GetExpireTime() > b.GetExpireTime()));
 }
 
+/**
+ * @ingroup dsr
+ * @brief Compare route cache entries by hop count only
+ * @param a The first route cache entry
+ * @param b The second route canche entry
+ * @returns `true` if `a < b`
+ */
 bool
 CompareRoutesHops(const DsrRouteCacheEntry& a, const DsrRouteCacheEntry& b)
 {
@@ -62,6 +77,13 @@
     return a.GetVector().size() < b.GetVector().size();
 }
 
+/**
+ * @ingroup dsr
+ * @brief Compare route cache entries by hop priority only.
+ * @param a The first route cache entry
+ * @param b The second route canche entry
+ * @returns `true` if `a < b`
+ */
 bool
 CompareRoutesExpire(const DsrRouteCacheEntry& a, const DsrRouteCacheEntry& b)
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/helper/energy-harvester-container.h ns-3.47/src/energy/helper/energy-harvester-container.h
--- ns-3.46/src/energy/helper/energy-harvester-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/helper/energy-harvester-container.h	2026-02-16 16:08:49.598230000 -0800
@@ -94,11 +94,10 @@
      * for-loop to run through the EnergyHarvesters.
      *
      * @code
-     *   EnergyHarvesterContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some EnergyHarvester method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some EnergyHarvester method
+     *   }
      * @endcode
      */
     Iterator Begin() const;
@@ -115,11 +114,10 @@
      * for-loop to run through the EnergyHarvesters.
      *
      * @code
-     *   EnergyHarvesterContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some EnergyHarvester method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some EnergyHarvester method
+     *   }
      * @endcode
      */
     Iterator End() const;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/helper/energy-source-container.h ns-3.47/src/energy/helper/energy-source-container.h
--- ns-3.46/src/energy/helper/energy-source-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/helper/energy-source-container.h	2026-02-16 16:08:49.598230000 -0800
@@ -92,11 +92,10 @@
      * for-loop to run through the EnergySources.
      *
      * @code
-     *   EnergySourceContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some EnergySource method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some EnergySource method
+     *   }
      * @endcode
      */
     Iterator Begin() const;
@@ -113,11 +112,10 @@
      * for-loop to run through the EnergySources.
      *
      * @code
-     *   EnergySourceContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some EnergySource method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some EnergySource method
+     *   }
      * @endcode
      */
     Iterator End() const;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/model/basic-energy-harvester.cc ns-3.47/src/energy/model/basic-energy-harvester.cc
--- ns-3.46/src/energy/model/basic-energy-harvester.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/model/basic-energy-harvester.cc	2026-02-16 16:08:49.598230000 -0800
@@ -9,6 +9,8 @@
 
 #include "basic-energy-harvester.h"
 
+#include "energy-source.h"
+
 #include "ns3/assert.h"
 #include "ns3/log.h"
 #include "ns3/pointer.h"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/model/device-energy-model-container.h ns-3.47/src/energy/model/device-energy-model-container.h
--- ns-3.46/src/energy/model/device-energy-model-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/model/device-energy-model-container.h	2026-02-16 16:08:49.599229800 -0800
@@ -87,11 +87,10 @@
      * for-loop to run through the DeviceEnergyModels.
      *
      * @code
-     *   DeviceEnergyModelContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some DeviceEnergyModel method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some DeviceEnergyModel method
+     *   }
      * @endcode
      */
     Iterator Begin() const;
@@ -108,11 +107,10 @@
      * for-loop to run through the DeviceEnergyModels.
      *
      * @code
-     *   DeviceEnergyModelContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some DeviceEnergyModel method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some DeviceEnergyModel method
+     *   }
      * @endcode
      */
     Iterator End() const;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/model/energy-harvester.cc ns-3.47/src/energy/model/energy-harvester.cc
--- ns-3.46/src/energy/model/energy-harvester.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/model/energy-harvester.cc	2026-02-16 16:08:49.599229800 -0800
@@ -9,7 +9,10 @@
 
 #include "energy-harvester.h"
 
+#include "energy-source.h"
+
 #include "ns3/log.h"
+#include "ns3/node.h"
 
 namespace ns3
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/model/energy-harvester.h ns-3.47/src/energy/model/energy-harvester.h
--- ns-3.46/src/energy/model/energy-harvester.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/model/energy-harvester.h	2026-02-16 16:08:49.599229800 -0800
@@ -10,17 +10,17 @@
 #ifndef ENERGY_HARVESTER_H
 #define ENERGY_HARVESTER_H
 
-#include <iostream>
-
-// include from ns-3
-#include "ns3/energy-source-container.h"
-#include "ns3/node.h"
 #include "ns3/object.h"
 #include "ns3/ptr.h"
 #include "ns3/type-id.h"
 
+#include <iostream>
+
 namespace ns3
 {
+
+class Node;
+
 namespace energy
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/model/energy-source.cc ns-3.47/src/energy/model/energy-source.cc
--- ns-3.46/src/energy/model/energy-source.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/model/energy-source.cc	2026-02-16 16:08:49.599229800 -0800
@@ -70,8 +70,7 @@
 {
     NS_LOG_FUNCTION(this << tid);
     DeviceEnergyModelContainer container;
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         if ((*i)->GetInstanceTypeId() == tid)
         {
@@ -86,8 +85,7 @@
 {
     NS_LOG_FUNCTION(this << name);
     DeviceEnergyModelContainer container;
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         if ((*i)->GetInstanceTypeId().GetName() == name)
         {
@@ -105,8 +103,7 @@
      * Device models are not aggregated to the node, hence we have to manually
      * call dispose method here.
      */
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         (*i)->Initialize();
     }
@@ -120,8 +117,7 @@
      * Device models are not aggregated to the node, hence we have to manually
      * call dispose method here.
      */
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         (*i)->Dispose();
     }
@@ -155,8 +151,7 @@
 {
     NS_LOG_FUNCTION(this);
     double totalCurrentA = 0.0;
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         totalCurrentA += (*i)->GetCurrentA();
     }
@@ -190,8 +185,7 @@
 {
     NS_LOG_FUNCTION(this);
     // notify all device energy models installed on node
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         (*i)->HandleEnergyDepletion();
     }
@@ -202,8 +196,7 @@
 {
     NS_LOG_FUNCTION(this);
     // notify all device energy models installed on node
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         (*i)->HandleEnergyRecharged();
     }
@@ -214,8 +207,7 @@
 {
     NS_LOG_FUNCTION(this);
     // notify all device energy models installed on node
-    DeviceEnergyModelContainer::Iterator i;
-    for (i = m_models.Begin(); i != m_models.End(); i++)
+    for (auto i = m_models.Begin(); i != m_models.End(); i++)
     {
         (*i)->HandleEnergyChanged();
     }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/energy/model/generic-battery-model.h ns-3.47/src/energy/model/generic-battery-model.h
--- ns-3.46/src/energy/model/generic-battery-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/energy/model/generic-battery-model.h	2026-02-16 16:08:49.599229800 -0800
@@ -18,6 +18,8 @@
 #include "ns3/nstime.h"
 #include "ns3/traced-value.h"
 
+#include <array>
+
 namespace ns3
 {
 namespace energy
@@ -77,61 +79,73 @@
  *
  *  Contains the values that form the battery presents available in this module.
  */
-static BatteryPresets g_batteryPreset[] = {{NIMH_NICD,
-                                            "Panasonic HHR650D | NiMH | 1.2V 6.5Ah | Size: D",
-                                            1.39,
-                                            7.0,
-                                            1.18,
-                                            6.25,
-                                            1.28,
-                                            1.3,
-                                            0.0046,
-                                            1.3,
-                                            1.0},
-                                           {LEADACID,
-                                            "CSB GP1272 | Lead Acid | 12V 7.2Ah",
-                                            12.8,
-                                            7.2,
-                                            11.5,
-                                            4.5,
-                                            12.5,
-                                            2,
-                                            0.056,
-                                            0.36,
-                                            8.0},
-                                           {LION_LIPO,
-                                            "Panasonic CGR18650DA | Li-Ion | 3.6V 2.45Ah | Size: A",
-                                            4.17,
-                                            2.33,
-                                            3.57,
-                                            2.14,
-                                            3.714,
-                                            1.74,
-                                            0.0830,
-                                            0.466,
-                                            3.0},
-                                           {LEADACID,
-                                            "Rs PRO LGP12100 | Lead Acid | 12V 100Ah",
-                                            12.60,
-                                            130,
-                                            12.44,
-                                            12.3,
-                                            12.52,
-                                            12,
-                                            0.00069,
-                                            5,
-                                            11},
-                                           {NIMH_NICD,
-                                            "PANASONIC N-700AAC | NiCd | 1.2V 700mAh | Size: AA",
-                                            1.38,
-                                            0.790,
-                                            1.17,
-                                            0.60,
-                                            1.25,
-                                            0.24,
-                                            0.016,
-                                            0.7,
-                                            0.8}};
+static const auto g_batteryPreset = std::to_array<BatteryPresets>({
+    {
+        NIMH_NICD,
+        "Panasonic HHR650D | NiMH | 1.2V 6.5Ah | Size: D",
+        1.39,
+        7.0,
+        1.18,
+        6.25,
+        1.28,
+        1.3,
+        0.0046,
+        1.3,
+        1.0,
+    },
+    {
+        LEADACID,
+        "CSB GP1272 | Lead Acid | 12V 7.2Ah",
+        12.8,
+        7.2,
+        11.5,
+        4.5,
+        12.5,
+        2,
+        0.056,
+        0.36,
+        8.0,
+    },
+    {
+        LION_LIPO,
+        "Panasonic CGR18650DA | Li-Ion | 3.6V 2.45Ah | Size: A",
+        4.17,
+        2.33,
+        3.57,
+        2.14,
+        3.714,
+        1.74,
+        0.0830,
+        0.466,
+        3.0,
+    },
+    {
+        LEADACID,
+        "Rs PRO LGP12100 | Lead Acid | 12V 100Ah",
+        12.60,
+        130,
+        12.44,
+        12.3,
+        12.52,
+        12,
+        0.00069,
+        5,
+        11,
+    },
+    {
+        NIMH_NICD,
+        "PANASONIC N-700AAC | NiCd | 1.2V 700mAh | Size: AA",
+        1.38,
+        0.790,
+        1.17,
+        0.60,
+        1.25,
+        0.24,
+        0.016,
+        0.7,
+        0.8,
+    },
+});
 
 /**
  * @ingroup energy
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/fd-net-device/examples/CMakeLists.txt ns-3.47/src/fd-net-device/examples/CMakeLists.txt
--- ns-3.46/src/fd-net-device/examples/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/fd-net-device/examples/CMakeLists.txt	2026-02-16 16:08:49.600230000 -0800
@@ -1,4 +1,7 @@
-include_directories(${DPDK_INCLUDE_DIRS})
+include_directories(
+  SYSTEM
+  ${DPDK_INCLUDE_DIRS}
+)
 
 build_lib_example(
   NAME dummy-network
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/fd-net-device/examples/fd-emu-send.cc ns-3.47/src/fd-net-device/examples/fd-emu-send.cc
--- ns-3.46/src/fd-net-device/examples/fd-emu-send.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/fd-net-device/examples/fd-emu-send.cc	2026-02-16 16:08:49.600230000 -0800
@@ -134,7 +134,7 @@
     std::string emuMode("netmap");
 #endif
 
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.AddValue("deviceName", "Device name", deviceName);
     cmd.AddValue("level", "Enable send (1) or write (0) level test", level);
     cmd.AddValue("emuMode", "Emulation mode in {raw, netmap}", emuMode);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/fd-net-device/examples/fd-emu-tc.cc ns-3.47/src/fd-net-device/examples/fd-emu-tc.cc
--- ns-3.46/src/fd-net-device/examples/fd-emu-tc.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/fd-net-device/examples/fd-emu-tc.cc	2026-02-16 16:08:49.600230000 -0800
@@ -84,7 +84,7 @@
     std::string emuMode("netmap");
 #endif
 
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.AddValue("deviceName0", "Device name", deviceName0);
     cmd.AddValue("deviceName1", "Device name", deviceName1);
     cmd.AddValue("ip0", "Local IP address", ip0);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/flow-monitor/model/flow-monitor.cc ns-3.47/src/flow-monitor/model/flow-monitor.cc
--- ns-3.46/src/flow-monitor/model/flow-monitor.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/flow-monitor/model/flow-monitor.cc	2026-02-16 16:08:49.602229800 -0800
@@ -13,7 +13,6 @@
 #include "ns3/simulator.h"
 
 #include <fstream>
-#include <limits>
 #include <sstream>
 
 #define PERIODIC_CHECK_INTERVAL (Seconds(1))
@@ -541,7 +540,7 @@
         flowStat.jitterSum = Seconds(0);
         flowStat.lastDelay = Seconds(0);
         flowStat.maxDelay = Seconds(0);
-        flowStat.minDelay = Seconds(std::numeric_limits<double>::max());
+        flowStat.minDelay = Time::Max();
         flowStat.txBytes = 0;
         flowStat.rxBytes = 0;
         flowStat.txPackets = 0;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/CMakeLists.txt ns-3.47/src/internet/CMakeLists.txt
--- ns-3.46/src/internet/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/CMakeLists.txt	2026-02-16 16:08:49.605229900 -0800
@@ -3,6 +3,7 @@
     helper/internet-trace-helper.cc
     helper/ipv4-address-helper.cc
     helper/ipv4-global-routing-helper.cc
+    helper/ipv6-global-routing-helper.cc
     helper/ipv4-interface-container.cc
     helper/ipv4-list-routing-helper.cc
     helper/ipv4-routing-helper.cc
@@ -31,7 +32,7 @@
     model/ipv4-address-generator.cc
     model/ipv4-end-point-demux.cc
     model/ipv4-end-point.cc
-    model/ipv4-global-routing.cc
+    model/global-routing.cc
     model/ipv4-header.cc
     model/ipv4-interface-address.cc
     model/ipv4-interface.cc
@@ -133,6 +134,7 @@
     helper/internet-trace-helper.h
     helper/ipv4-address-helper.h
     helper/ipv4-global-routing-helper.h
+    helper/ipv6-global-routing-helper.h
     helper/ipv4-interface-container.h
     helper/ipv4-list-routing-helper.h
     helper/ipv4-routing-helper.h
@@ -161,7 +163,7 @@
     model/ipv4-address-generator.h
     model/ipv4-end-point-demux.h
     model/ipv4-end-point.h
-    model/ipv4-global-routing.h
+    model/global-routing.h
     model/ipv4-header.h
     model/ipv4-interface-address.h
     model/ipv4-interface.h
@@ -267,7 +269,7 @@
     test/ipv4-deduplication-test.cc
     test/ipv4-forwarding-test.cc
     test/ipv4-fragmentation-test.cc
-    test/ipv4-global-routing-test-suite.cc
+    test/global-routing-test-suite.cc
     test/ipv4-header-test.cc
     test/ipv4-list-routing-test-suite.cc
     test/ipv4-packet-info-tag-test-suite.cc
@@ -289,6 +291,7 @@
     test/ipv6-test.cc
     test/neighbor-cache-test.cc
     test/rtt-test.cc
+    test/tcp-abe-test.cc
     test/tcp-advertised-window-test.cc
     test/tcp-bbr-test.cc
     test/tcp-bic-test.cc
@@ -301,6 +304,7 @@
     test/tcp-ecn-test.cc
     test/tcp-endpoint-bug2211.cc
     test/tcp-error-model.cc
+    test/tcp-fack-test.cc
     test/tcp-fast-retr-test.cc
     test/tcp-general-test.cc
     test/tcp-header-test.cc
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/doc/global-routing.rst ns-3.47/src/internet/doc/global-routing.rst
--- ns-3.46/src/internet/doc/global-routing.rst	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/doc/global-routing.rst	2026-02-16 16:08:49.607230000 -0800
@@ -0,0 +1,278 @@
+.. include:: replace.txt
+.. highlight:: cpp
+
+Global Routing
+==============
+
+This chapter defines the implementation of the ``GlobalRouting`` API present in the internet module. Global routing is not a dynamic routing protocol that
+sends packets over the simulated network.  Instead, it is a simulation
+process that walks the simulation topology (typically just before the
+simulation starts), runs a shortest path algorithm on the discovered
+topology, and populates each node's routing tables accordingly. No actual
+protocol overhead (on the simulated links) is incurred with this approach.
+
+The quagga (`<https://www.nongnu.org/quagga/>`_) OSPF implementation was used as the
+basis for the routing computation logic. The Link State information exported by each global router in
+this API conforms to the OSPFv2 specs (:rfc:`2328`) as used by the quagga implementation.
+
+Presently, global centralized IPv[4,6] unicast routing over both point-to-point and
+shared (CSMA) links is supported.
+
+The following is a high level overview of the classes that are used to implement the Global Routing API:
+
+* **User Interface**: ``Ipv4GlobalRoutingHelper`` and ``GlobalRouteManager``
+* **Route Computation**: ``SPFVertex``, ``GlobalRouteManagerLSDB`` and  ``GlobalRouteManagerImpl``
+* **Link State Advertisements**: ``GlobalRoutingLSA``, ``GlobalRoutingLinkRecord`` and ``GlobalRouter``
+* **RouteLookup and Forwarding**: ``GlobalRouting``
+
+Scope and Limitations
+---------------------
+The API has a few constraints:
+
+* **Wired only:** It is not intended for use in wireless networks. For wireless, OLSR dynamic routing is recommended, as described below.
+* **Unicast only:** It does not support multicast.
+* **Scalability:** Some users of this API in large topologies (e.g., 1000 nodes) have observed that the current implementation
+  is not very scalable.The global centralized routing is planned to be modified in the future to reduce computations and improve runtime performance.
+
+The following are the known limitations of the GlobalRouting implementation:
+
+#. When calculating routes for networks using GlobalRouting, if multiple exit interfaces are present from the root node to a network,
+   packets may travel an extra hop to reach their destination within that network.
+   To clarify, the following are the generalized conditions:
+
+  #. It is assumed that there is a network (e.g., 10.1.2.0/24) that has 2 (or more) routers (2 egress points, or more).
+  #. It is assumed that a node has 2 (or more) equal-cost paths toward the aforementioned network.
+  #. It is assumed that multiple IP addresses with that network prefix are present on one or more interfaces.
+
+This will result in packets reaching the internal interfaces of the "other" router(s) via one unnecessary hop,
+passing through the designated router, and discarding the (shorter) path that directly leads to the destination.
+
+ .. _Global-Routing-Limitation:
+
+ .. figure:: figures/global-routing-limitation.*
+
+#. Although adding multiple IP addresses on the same interface and Equal Cost Multi Path are both supported individually,
+the combination of both is not fully supported and may lead to unexpected results and,
+in some specific cases, routing loops.
+This limitation is tracked in issue #1242 of the issue tracker.
+
+The Following are the known limitations of the GlobalRouting implementation for Ipv6 ONLY:
+
+#. The GlobalRouting implementation for Ipv6 does not support non-Onlink Ipv6Addresses. The API will assert if it encounters such an address.
+#. The GlobalRouting implementation for Ipv6 does not support StrongEndSystem Models. Calls to GlobalRouting
+   via the Ipv6GlobalRoutingHelper will automatically disable StrongEndSystem model for all nodes in the system.
+   When using the GlobalRouteManager directly, make sure to disable StrongEndSystem model for all nodes in the system.
+
+Usage
+-----
+
+To use GlobalRouting as the routing protocol, an instance of Ipv[4,6]GlobalRoutingHelper needs
+to be set using InternetStackHelper::SetRoutingProtocol().  Example code of this is::
+
+  InternetStackHelper internet;
+  Ipv4GlobalRoutingHelper glbrouting;
+  internet.SetRoutingHelper(glbrouting);
+
+Further usage is typically based on the following helper code.
+
+Helpers
+~~~~~~~
+Users must include the ns3/ipv4-global-routing-helper.h or ns3/ipv6-global-routing-helper.h
+header file for IPv4 or IPv6, respectively.  After IP addresses are configured, the
+following function call will cause all of the nodes that have an Ipv4 interface
+to receive forwarding tables entered automatically by the GlobalRouteManager::
+
+  Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+Likewise, if IPv6 is used::
+
+  Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+.. note::
+    A reminder that the wifi NetDevice will work, but does not take any
+    wireless effects into account. For wireless, we recommend OLSR dynamic routing.
+
+.. note::
+    For IPv6, calls to GlobalRouting via the Ipv6GlobalRoutingHelper will enable
+    forwarding for all nodes in the simulation with GlobalRouting installed.
+    If more control is desired, it is recommended to use the GlobalRouteManager directly.
+
+
+It is possible to call this function again in the midst of a simulation using
+the following additional public function::
+
+  Ipv4GlobalRoutingHelper::RecomputeRoutingTables();
+  Ipv6GlobalRoutingHelper::RecomputeRoutingTables();
+
+which flushes the old tables, queries the nodes for new interface information,
+and rebuilds the routes.
+
+For instance, this scheduling call will cause the tables to be rebuilt
+at time 5 seconds::
+
+  Simulator::Schedule(Seconds(5),
+                      &Ipv4GlobalRoutingHelper::RecomputeRoutingTables);
+
+GlobalRouteManager
+~~~~~~~~~~~~~~~~~~
+
+GlobalRouteManager class can also be used to run the Global Routing API.
+
+Users must include ns3/global-route-manager.h header file.  After the
+IPv4 topology has been built and addresses assigned, users call
+ns3::GlobalRouteManager::PopulateRoutingTables (), prior to the
+ns3::Simulator::Run() call.
+
+It has the following functions:
+
+* DeleteGlobalRoutes() - Deletes all the routes in the routing table for each node in the simulation.
+* BuildGlobalRoutingDatabase() - Queries each of the globalRouter in the simulation and collects
+  the Link State Advertisements exported by them into a Link State Database with key as the LinkstateID.
+* InitializeRoutes() -For each node that is a global router (which is determined by the presence of an aggregated GlobalRouter interface),
+  run the Dijkstra SPF calculation on the database rooted at that router, and populate the node forwarding tables.
+
+Note: Calls to GlobalRouteManager will in turn call the Simulation Singleton Object GlobalRouteManagerImpl to run the actual routing logic.
+
+
+Default Usage
+~~~~~~~~~~~~~
+By default, when using the |ns3| helper API and the default InternetStackHelper,
+global routing capability will be added to the node, and global routing will be
+inserted as a routing protocol with lower priority than the static routes (i.e.,
+users can insert routes via Ipv[4,6]StaticRouting API and they will take precedence
+over routes found by global routing).
+
+The public API is very minimal. User scripts include the following::
+
+    #include "ns3/internet-module.h"
+
+If the default InternetStackHelper is used, then an instance of GlobalRouting and globalRouter
+will be aggregated to each node.
+
+Attributes
+~~~~~~~~~~
+
+There are two attributes that govern the behavior of global routing.
+
+#. Ipv4GlobalRouting::RandomEcmpRouting. If set to true, packets are randomly
+   routed across equal-cost multipath routes. If set to false (default), only one
+   route is consistently used.
+
+#. Ipv4GlobalRouting::RespondToInterfaceEvents. If set to true, dynamically
+   recompute the global routes upon Interface notification events (up/down, or
+   add/remove address). If set to false (default), routing may break unless the
+   user manually calls RecomputeRoutingTables() after such events. The default is
+   set to false to preserve legacy |ns3| program behavior.
+
+The following code shows how to configure these attributes::
+
+    Config::SetDefault("ns3::Ipv4GlobalRouting::RespondToInterfaceEvents", BooleanValue(true));
+    Config::SetDefault("ns3::Ipv4GlobalRouting::RandomEcmpRouting", BooleanValue(true));
+
+The helper also provides a ``PrintRoute()`` method that allows users to
+print the routing path calculated between two nodes; see the example
+program ``src/internet/examples/ipv4-global-routing-print-route.cc`` for a
+number of example statements showing how this method can be called.
+
+
+Traces
+~~~~~~
+
+Not Applicable
+
+
+
+Implementation Details
+----------------------
+This section is for those readers who care about how this is implemented.
+A singleton object (GlobalRouteManager) is responsible for populating the static routes on each node,
+using the public Ipv[4,6] API of that node.
+It queries each node in the topology for a “globalRouter” interface.
+If found, it uses the API of that interface to obtain a “link state advertisement (LSA)” for the router.
+Link State Advertisements are used in OSPF routing, and the formatting is followed.
+
+It is important to note that all of these computations are done before packets are flowing in the network.
+In particular, there are no overhead or control packets being exchanged when using this implementation.
+Instead, this global route manager just walks the list of nodes to build the necessary information
+and configure each node’s routing table.
+
+The GlobalRouteManager populates a link state database with LSAs gathered from the entire topology.
+Then, for each router in the topology, the GlobalRouteManager executes
+the OSPF shortest path first (SPF) computation on the database and populates the routing tables on each node.
+
+The quagga (https://www.nongnu.org/quagga/) OSPF implementation was used as the basis for the routing computation logic.
+One benefit of following an existing OSPF SPF implementation is that
+OSPF already has defined link state advertisements for all common types of network links:
+- Point-to-point (serial links).
+- Point-to-multipoint (Frame Relay, ad hoc wireless).
+- Non-broadcast multiple access (ATM).
+- Broadcast (Ethernet).
+
+Therefore, it is believed that enabling these other link types will be more
+straightforward now that the underlying OSPF SPF framework is in place.
+Currently, the implementation can handle IPv[4,6] point-to-point, numbered links,
+as well as shared broadcast (CSMA) links. Equal-cost multipath is also supported.
+Although wireless link types are supported by the implementation,
+it is noted that due to the nature of this implementation, any channel effects will not be considered,
+and the routing tables will assume that every node on the same shared channel is reachable from every other node
+(i.e., it will be treated like a broadcast CSMA link).
+
+The GlobalRouteManager first walks the list of nodes and aggregates a GlobalRouter interface to each one as follows::
+
+  typedef std::vector<Ptr<Node>>::iterator Iterator;
+  for (Iterator i = NodeList::Begin(); i != NodeList::End(); i++)
+  {
+  Ptr<Node> node = *i;
+  Ptr<GlobalRouter> globalRouter = CreateObject<GlobalRouter>(node);
+  node->AggregateObject(globalRouter);
+  }
+
+This interface is later queried and used to generate a Link State Advertisement for each router,
+and this link state database is fed into the OSPF shortest path computation logic.
+The Ipv4 API is finally used to populate the routes themselves.
+
+Examples and Tests
+------------------
+
+Currently no examples are available for the Global Routing API.
+
+Two TestSuites are available for the Global Routing API in internet/test:
+
+* ``ipv4-global-routing-test-suite``
+* ``global-route-manager-impl-test-suite``
+
+The TestSuites are located in the ns3/src/internet/test directory.
+The Following is a brief description of the TestCases Contained within:
+
+**global-route-manager-impl-test-suite**
+
+* ``LinkRoutesTestCase`` : Checks Default Routes for stub nodes and routes for p2p links
+* ``LanRoutesTestCase`` : Checks Network Routes for LAN Topologies. Checks that the Network LSA is handled by GlobalRouteManagerImpl
+* ``RandomEcmpTestCase`` : Checks the RandomEcmp Attribute. i.e if equal cost multipath routes are possible build them and use them at random
+* ``GlobalRoutingProtocolTestCase`` : Checks the normal operation of GlobalRouting, by looking at the output of the RouteInput() and RouteOutput() for several different paths. This mimics how normal calls to GlobalRouting are made.
+
+**ipv4-global-routing-test-suite**
+
+* ``Link test``: Point-to-point link between two nodes, verifying default stub-link routes.
+* ``LAN test``: Two nodes on a broadcast network, verifying direct LAN subnet routes.
+* ``Two link test``: Three nodes on two point-to-point links, checking default and subnet routes.
+* ``Two LANs test``: Three nodes on two broadcast networks, testing inter-LAN routing.
+* ``Bridge test``: Two Links connected through a bridge, verifying routing across the bridged segment.
+* ``Two Bridge test``: links connected by two bridges in series, testing multi-hop bridging.
+
+
+Validation
+----------
+
+No formal validation (comparison of the routing tables generated by this
+implementation against the results of a real routing protocol)
+has been performed.
+
+References
+----------
+
+[`1 <https://www.nongnu.org/quagga/>`_] The quagga routing suite was used as the basis for the routing computation logic.
+
+[2] :rfc:`2328` was used as the basis for the information exported by the global routers for a single area network configuration.
+
+
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/doc/routing-overview.rst ns-3.47/src/internet/doc/routing-overview.rst
--- ns-3.46/src/internet/doc/routing-overview.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/doc/routing-overview.rst	2026-02-16 16:08:49.607230000 -0800
@@ -90,7 +90,7 @@
 
 * classes Ipv4ListRouting and Ipv6ListRouting (used to store a prioritized list of routing protocols)
 * classes Ipv4StaticRouting and Ipv6StaticRouting (covering both unicast and multicast)
-* class Ipv4GlobalRouting (used to store routes computed by the global route
+* class Ipv[4,6]GlobalRouting (used to store routes computed by the global route
   manager, if that is used)
 * class Ipv4NixVectorRouting (a more efficient version of global routing that
   stores source routes in a packet header field)
@@ -158,160 +158,12 @@
 
 .. _Global-centralized-routing:
 
-Global centralized routing
-++++++++++++++++++++++++++
+Global Centralized Routing
++++++++++++++++++++++++++++
 
-Global centralized routing is sometimes called "God" routing; it is a special
-implementation that walks the simulation topology and runs a shortest path
-algorithm, and populates each node's routing tables. No actual protocol overhead
-(on the simulated links) is incurred with this approach. It does have a few
-constraints:
-
-* **Wired only:**  It is not intended for use in wireless networks.
-* **Unicast only:** It does not do multicast.
-* **Scalability:**  Some users of this on large topologies (e.g. 1000 nodes)
-  have noticed that the current implementation is not very scalable. The global
-  centralized routing will be modified in the future to reduce computations and
-  runtime performance.
-
-Presently, global centralized IPv4 unicast routing over both point-to-point and
-shared (CSMA) links is supported.
-
-By default, when using the |ns3| helper API and the default InternetStackHelper,
-global routing capability will be added to the node, and global routing will be
-inserted as a routing protocol with lower priority than the static routes (i.e.,
-users can insert routes via Ipv4StaticRouting API and they will take precedence
-over routes found by global routing).
-
-Global Unicast Routing API
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The public API is very minimal. User scripts include the following::
-
-    #include "ns3/internet-module.h"
-
-If the default InternetStackHelper is used, then an instance of global routing
-will be aggregated to each node.  After IP addresses are configured, the
-following function call will cause all of the nodes that have an Ipv4 interface
-to receive forwarding tables entered automatically by the GlobalRouteManager::
-
-  Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-*Note:* A reminder that the wifi NetDevice will work but does not take any
-wireless effects into account. For wireless, we recommend OLSR dynamic routing
-described below.
-
-It is possible to call this function again in the midst of a simulation using
-the following additional public function::
-
-  Ipv4GlobalRoutingHelper::RecomputeRoutingTables();
-
-which flushes the old tables, queries the nodes for new interface information,
-and rebuilds the routes.
-
-For instance, this scheduling call will cause the tables to be rebuilt
-at time 5 seconds::
-
-  Simulator::Schedule(Seconds(5),
-                      &Ipv4GlobalRoutingHelper::RecomputeRoutingTables);
-
-
-There are two attributes that govern the behavior. The first is
-Ipv4GlobalRouting::RandomEcmpRouting. If set to true, packets are randomly
-routed across equal-cost multipath routes. If set to false (default), only one
-route is consistently used. The second is
-Ipv4GlobalRouting::RespondToInterfaceEvents. If set to true, dynamically
-recompute the global routes upon Interface notification events (up/down, or
-add/remove address). If set to false (default), routing may break unless the
-user manually calls RecomputeRoutingTables() after such events. The default is
-set to false to preserve legacy |ns3| program behavior.
-
-Global Routing Implementation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-This section is for those readers who care about how this is implemented.  A
-singleton object (GlobalRouteManager) is responsible for populating the static
-routes on each node, using the public Ipv4 API of that node.  It queries each
-node in the topology for a "globalRouter" interface.  If found, it uses the API
-of that interface to obtain a "link state advertisement (LSA)" for the router.
-Link State Advertisements are used in OSPF routing, and we follow their
-formatting.
-
-It is important to note that all of these computations are done before
-packets are flowing in the network.  In particular, there are no
-overhead or control packets being exchanged when using this implementation.
-Instead, this global route manager just walks the list of nodes to
-build the necessary information and configure each node's routing table.
-
-The GlobalRouteManager populates a link state database with LSAs gathered from
-the entire topology. Then, for each router in the topology, the
-GlobalRouteManager executes the OSPF shortest path first (SPF) computation on
-the database, and populates the routing tables on each node.
-
-The quagga (`<https://www.nongnu.org/quagga/>`_) OSPF implementation was used as the
-basis for the routing computation logic. One benefit of following an existing
-OSPF SPF implementation is that OSPF already has defined link state
-advertisements for all common types of network links:
-
-* point-to-point (serial links)
-* point-to-multipoint (Frame Relay, ad hoc wireless)
-* non-broadcast multiple access (ATM)
-* broadcast (Ethernet)
-
-Therefore, we think that enabling these other link types will be more
-straightforward now that the underlying OSPF SPF framework is in place.
-
-Presently, we can handle IPv4 point-to-point, numbered links, as well as shared
-broadcast (CSMA) links.  Equal-cost multipath is also supported.  Although
-wireless link types are supported by the implementation, note that due
-to the nature of this implementation, any channel effects will not be
-considered and the routing tables will assume that every node on the
-same shared channel is reachable from every other node (i.e. it will
-be treated like a broadcast CSMA link).
-
-The GlobalRouteManager first walks the list of nodes and aggregates
-a GlobalRouter interface to each one as follows::
-
-  typedef std::vector<Ptr<Node>>::iterator Iterator;
-  for (Iterator i = NodeList::Begin(); i != NodeList::End(); i++)
-    {
-      Ptr<Node> node = *i;
-      Ptr<GlobalRouter> globalRouter = CreateObject<GlobalRouter>(node);
-      node->AggregateObject(globalRouter);
-    }
-
-This interface is later queried and used to generate a Link State
-Advertisement for each router, and this link state database is
-fed into the OSPF shortest path computation logic. The Ipv4 API
-is finally used to populate the routes themselves.
-
-Limitations
-~~~~~~~~~~~
-
-The following are the known limitations of the GlobalRouting implementation:
-
-#. When calculating routes for networks using GlobalRouting,
-   if multiple exit Interfaces are present from the root node to a network,
-   then Packets may travel an extra hop to reach their destination in that network.
-   Clarifying this further, the following is the generalized conditions:
-
-    #. Assume that there is a network (e.g., 10.1.2.0/24) and the network has 2 (or more) routers (2 egress points, or more).
-    #. Assume that a node has 2 (or more) equal-cost paths toward said network.
-    #. Assume that multiple IP addresses with that network prefix are present on one or more interfaces.
-
-   This will mean that, to reach the internal interfaces of the "other" router(s), the packets will
-   do one unnecessary hop, passing through the designated router, and discarding the (shorter) path
-   leading directly to the destination.
-
-   .. _Global-Routing-Limitation:
-
-   .. figure:: figures/global-routing-limitation.*
-
-#. Although adding Multiple IP addresses on the same interface
-   and Equal Cost Multi Path are both supported individually,
-   the combination of both is not fully supported and may lead to unexpected results
-   and in some specific cases routing loops.
-   This limitation is tracked in issue #1242 of the issue tracker.
+Global centralized routing is an offline computation of routing tables that
+builds and installs routing tables without any protocol overhead on the
+simulated links. It is described in detail in the :doc:`global-routing` chapter.
 
 RIP and RIPng
 +++++++++++++
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/doc/tcp.rst ns-3.47/src/internet/doc/tcp.rst
--- ns-3.46/src/internet/doc/tcp.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/doc/tcp.rst	2026-02-16 16:08:49.608229900 -0800
@@ -49,7 +49,7 @@
 HighSpeed TCP (YeAH), Illinois, H-TCP, Low Extra Delay Background Transport
 (LEDBAT), TCP Low Priority (TCP-LP), Data Center TCP (DCTCP) and Bottleneck
 Bandwidth and RTT (BBR) also supported. The model also supports Selective
-Acknowledgements (SACK), Proportional Rate Reduction (PRR) and Explicit
+Acknowledgements (SACK), Forward Acknowledgement (FACK), Proportional Rate Reduction (PRR) and Explicit
 Congestion Notification (ECN). Multipath-TCP is not yet supported in the |ns3|
 releases.
 
@@ -1407,6 +1407,7 @@
 * **tcp-cong-avoid-test:** TCP congestion avoidance for different packet sizes
 * **tcp-datasentcb:** Check TCP's 'data sent' callback
 * **tcp-endpoint-bug2211-test:** A test for an issue that was causing stack overflow
+* **tcp-fack-test:** Unit tests on FACK
 * **tcp-fast-retr-test:** Fast Retransmit testing
 * **tcp-header:** Unit tests on the TCP header
 * **tcp-highspeed-test:** Unit tests on the HighSpeed congestion control
@@ -1627,6 +1628,184 @@
 For an academic peer-reviewed paper on the SACK implementation in ns-3,
 please refer to https://dl.acm.org/citation.cfm?id=3067666.
 
+Forward Acknowledgement (FACK)
+++++++++++++++++++++++++++++++
+
+FACK is designed to be used with the TCP SACK option.
+It keeps count of the total number of bytes of outstanding data in the network. It
+achieves this by using the additional information provided by TCP SACK.
+
+FACK maintains two state variables: sndFack and retranData.
+
+sndFack is updated to reflect the highest sequence number that has been selectively acknowledged.
+In non-recovery state, the sndFack variable is updated from the acknowledgment
+number in the TCP header whereas during the recovery state, the sender utilizes
+information contained in TCP SACK options to update sndFack.
+
+retranData is the amount of outstanding retransmitted data in the
+network. Each time a segment is retransmitted, retranData is increased by the
+segment's size; when a retransmitted segment is determined to have left the
+network, retranData is decreased by the segment's size.
+
+awnd variable is defined to be the data sender’s estimate of the actual quantity
+of data outstanding in the network.
+
+Assuming that all the unacknowledged data has left the network:
+
+.. math::
+
+   awnd = sndNxt - sndFack
+
+sndNxt holds the sequence number of the first byte of unsent data.
+
+In recovery state, data which is retransmitted must also be included in the
+calculation of awnd.
+
+.. math::
+
+   awnd = sndNxt - sndFack + retranData
+
+Using this measure of outstanding data, the FACK algorithm can regulate the
+amount of data outstanding in the network to be within one MSS of the current
+value of cwnd.
+
+In the FACK version, the cwnd adjustment and retransmission are also triggered
+when the receiver reports that the reassembly queue is longer than 3 segments:
+
+.. code-block:: c++
+
+   if ((m_fackEnabled && fackDiff > m_tcb->m_segmentSize * 3) ||
+            ((m_dupAckCount == m_retxThresh) &&
+             (m_highRxAckMark >= m_recover || (!m_recoverActive))))
+        {
+            EnterRecovery(currentDelivered);
+            NS_ASSERT(m_tcb->m_congState == TcpSocketState::CA_RECOVERY);
+        }
+
+By default the FACK option is disabled. To enable FACK, the following
+configuration can be used:
+
+::
+
+  Config::SetDefault ("ns3::TcpSocketBase::Fack", BooleanValue (true));
+
+Note that FACK requires SACK to be enabled as well:
+
+::
+
+  Config::SetDefault ("ns3::TcpSocketBase::Sack", BooleanValue (true));
+
+
+The following unit tests have been written to verify the implementation of FACK:
+
+* This unit test creates a short packet flow and forces four consecutive lost
+  segments, and verifies that the snd.fack variable is updated to the highest sequence
+  number present in the incoming SACK blocks, and that its external calculation of
+  awnd matches the internal state variable.
+
+  The test monitors the sender's ACK processing and performs two key checks:
+
+  1. snd.fack: It parses incoming SACK blocks to determine the highest
+     acknowledged sequence number and verifies that the implementation updates
+     `snd.fack` correctly.
+
+  2. awnd: It recomputes the in-flight data window using the FACK formula
+     (`awnd = snd.nxt - snd.fack + retransmitted_data`) and confirms that this
+     matches the implementation's internal value.
+
+A successful test run ensures that the TCP FACK implementation accurately tracks
+the forward acknowledgment point and the amount of data in flight during
+recovery.
+
+Example and Performance Verification
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The example ``examples/tcp/fack-example.cc`` can be used to observe the
+FACK algorithm's influence on congestion control, specifically regarding the
+preservation of the "Self-Clock" and the "Overdamping" mechanism described
+in the Mathis & Mahdavi paper.
+
+The example simulates a bottleneck link where an initial Slow Start phase results
+in a significant packet overshoot (buffer overflow).
+
+**Reproducing the Data:**
+
+To generate the comparison data, the simulation should be run twice:
+
+1. **With FACK (Red Line):**
+   Ensure ``bool fack = true;`` in the example code.
+   Run the simulation and plot the ``fackAwnd.dat`` trace file using gnuplot. This file tracks
+   the FACK-specific `awnd` variable.
+
+2. **Without FACK (Blue Line):**
+   Modify the code to set ``bool fack = false;``.
+   Run the simulation and plot the ``bytesInFlight.dat`` trace file using gnuplot. This file
+   tracks the standard TCP `BytesInFlight` (SND.NXT - SND.UNA).
+
+**Plotting:**
+
+To generate the overlapping plot shown in **Figure 1**, you can use the following Gnuplot script. Save this as ``plot_inflight.gp`` and run it using ``gnuplot plot_inflight.gp``.
+
+.. code-block:: gnuplot
+
+   set terminal pngcairo enhanced color lw 1.5 font 'Times Roman'
+   set xrange [0:10]
+   set yrange [0:60]
+   set output "inflight.png"
+
+   set xlabel "Time (sec)"
+   set ylabel "Inflight (Packets)"
+   set key top right vertical
+
+   plot \
+     "bytesInFlight.dat" using 1:2 title "Without FACK" with lines lw 1.5 lc rgb "blue", \
+     "fackAwnd.dat" using 1:2 title "With FACK" with lines lw 1.5 lc rgb "red"
+
+.. note::
+   To generate the Congestion Window plot (**Figure 2**), use the same script logic but change the output filename to ``"cwnd.png"``, update the y-label to "Cwnd (Packets)", and replace the input files in the plot command with the saved CWND traces (e.g., ``cwnd_nofack.dat`` and ``cwnd_fack.dat``).
+
+**Interpretation of Results:**
+
+To fully appreciate the FACK algorithm's behavior, it is essential to analyze the relationship between the data outstanding in the network (`awnd`) and the sender's target window (`cwnd`). The following figures, generated from this example, illustrate the distinct phases of **Overshoot**, **Stall**, and **Overdamping**.
+
+.. _fig-fack-inflight:
+
+.. figure:: figures/fack-inflight.png
+   :align: center
+   :scale: 70 %
+   :alt: Comparison of FACK awnd vs Standard BytesInFlight
+
+   **Figure 1:** Comparison of Inflight Data. The Blue line represents the standard TCP `BytesInFlight`. The Red line represents the FACK-specific `awnd` (Active Window), which accurately tracks the ~50 packet backlog caused by the Slow-Start overshoot.
+
+.. _fig-fack-cwnd:
+
+.. figure:: figures/fack-cwnd.png
+   :align: center
+   :scale: 70 %
+   :alt: Congestion Window Trace
+
+   **Figure 2:** Congestion Window (`cwnd`). Note the sharp reduction to ~10 packets at t=1.0s. This low target `cwnd`, combined with the high `awnd` in Figure 1, triggers the Overdamping mechanism.
+
+
+As shown in **Figure 1** and **Figure 2**, distinct behaviors are observed during the recovery phase (1.0s - 3.0s):
+
+* **Blue Line (Without FACK):** The sharp drop in **Figure 1** indicates a loss of the TCP "Self-Clock." When losses occur, the standard algorithm cannot infer that packets have left the network, causing transmission to stall until the pipe drains.
+
+* **Red Line (With FACK):** The `awnd` in **Figure 1** remains high (~50 packets), significantly exceeding the Congestion Window (`cwnd`, approx. 10 packets, visible in **Figure 2**) during this interval. This demonstrates the **"Overdamping"** mechanism (Section 4.4 of the paper):
+
+    1.  **The Overshoot:** The initial Slow Start phase ramped up aggressively, pushing ~50 packets into the network before losses were detected (visible as the peak in the Red line).
+
+    2.  **The Detection:** FACK's `awnd` correctly measured this ~50-packet backlog. Simultaneously, the congestion control algorithm cut the `cwnd` to ~10 packets in response to the losses. This behavior matches the FACK paper’s statement:
+
+        *"In the case when cwnd has been halved immediately following a lost segment, awnd will be significantly larger than cwnd".*
+
+    3.  **The Damping Action:** Because `awnd` was significantly larger than `cwnd`, the FACK algorithm inhibited new transmissions. The gradual downward slope of the Red line represents the queue draining as packets left the network. This behavior is governed by the core FACK transmission rule (*while awnd < cwnd*) defined in Section 3, which forces the sender to stop transmitting whenever the `awnd` exceeds the target window (`cwnd`).
+
+This confirms that FACK successfully decouples data recovery from congestion control, maintaining an accurate picture of the network state even during heavy loss.
+
+More information (paper): https://dl.acm.org/citation.cfm?id=248181
+
+
 Loss Recovery Algorithms
 ++++++++++++++++++++++++
 The following loss recovery algorithms are supported in ns-3 TCP.  The current
@@ -1732,6 +1911,28 @@
 
 More information (RFC): https://tools.ietf.org/html/rfc6937
 
+Alternative Backoff with ECN (ABE)
+++++++++++++++++++++++++++++++++++
+
+RFC 8511 recommends using an alternate ``m_betaEcn`` for calculation of the
+ssthresh  when the congestion control algorithm detects congestion using ECN.
+There is an  attribute ``ns3::TcpSocketBase::UseAbe`` which can be set to true
+to enable this feature. When enabled, the congestion control algorithm
+(NewReno or CUBIC) will use the ``m_betaEcn`` value to calculate the ssthresh when
+congestion is detected using ECN. The default value of ``m_betaEcn`` is set to
+0.85 for CUBIC and 0.7 for NewReno as recommended by the RFC, but can be changed
+using the attribute ``ns3::TcpNewReno::BetaEcn`` for NewReno or using
+``ns3::TcpCubic::BetaEcn`` for CUBIC.
+
+For example, to enable ABE for NewReno, you can set the attribute as follows:
+::
+
+  Config::SetDefault("ns3::TcpSocketBase::UseAbe", BooleanValue(true));
+  Config::SetDefault("ns3::TcpNewReno::BetaEcn", DoubleValue(0.7));
+
+
+More information (RFC): https://tools.ietf.org/html/rfc8511
+
 Adding a new loss recovery algorithm in ns-3
 ++++++++++++++++++++++++++++++++++++++++++++
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/examples/CMakeLists.txt ns-3.47/src/internet/examples/CMakeLists.txt
--- ns-3.46/src/internet/examples/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/examples/CMakeLists.txt	2026-02-16 16:08:49.608229900 -0800
@@ -25,3 +25,14 @@
     ${libinternet}
     ${libnetwork}
 )
+
+build_lib_example(
+  NAME ipv4-global-routing-print-route
+  SOURCE_FILES ipv4-global-routing-print-route.cc
+  LIBRARIES_TO_LINK
+    ${libapplications}
+    ${libcsma}
+    ${libinternet}
+    ${libnetwork}
+    ${libpoint-to-point}
+)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/examples/ipv4-global-routing-print-route.cc ns-3.47/src/internet/examples/ipv4-global-routing-print-route.cc
--- ns-3.46/src/internet/examples/ipv4-global-routing-print-route.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/examples/ipv4-global-routing-print-route.cc	2026-02-16 16:08:49.608229900 -0800
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2025 Shashwat Patni
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Shashwat Patni <shashwatpatni25@gmail.com>
+ */
+
+#include "ns3/core-module.h"
+#include "ns3/internet-module.h"
+#include "ns3/network-module.h"
+#include "ns3/point-to-point-module.h"
+
+/**
+ * @file This example demonstrates how to use Ipv4GlobalRoutingHelper::PrintRoute()
+ *  and PrintRoutePathAt() and its overloads to print the route path as calculated
+ *  by the global routing protocol. The Print Format loosely follows that of traceroute.
+ *  Similar to traceroute, there is an option to disable the Reverse Node ID lookup.
+ *  We have two functions PrintRoute() and PrintRouteAt(). Each of these has 4 overloads.
+ *  Please refer to the doxygen documentation for more details on each of the functions and its
+ *  overloads.
+ *  The default behaviour is to also print the node id. This can be disabled by setting the
+ *  nodeIdLookup argument to false. This is similar to linux traceroute -n option.
+ *  We explain the use of each of the overloads for PrintRoute() using the below topology
+ *  The PrintRouteAt() variant behaves similarly with the added time argument.
+ *  @verbatim  Simple point to point links:
+      ________
+     /        \
+   n0 -- n1 -- n2 -- n3     n4----n5
+
+   n0 IP: 10.1.1.1, 10.1.4.1
+   n1 IP: 10.1.1.2, 10.1.2.1
+   n2 IP: 10.1.2.2, 10.1.3.1, 10.1.4.2
+   n3 IP: 10.1.3.2
+   n4 IP: 10.1.5.1
+   n5 IP: 10:1:5:2
+
+  Test 1:Route from n1 to 10.1.2.2
+  expected Output:
+  PrintRoute at Time: +0s from Node 1 to address 10.1.2.2, 64 hops Max.
+   1  10.1.2.2 (Node 2)
+
+  Test 2: Route from n1 to n3
+  expected Output:
+  PrintRoute at Time: +0s from Node 1 to Node 3, 64 hops Max.
+   1  10.1.2.2 (Node 2)
+   2  10.1.3.2 (Node 3)
+
+  Test 3: Route from n0 to 10.1.5.2
+  expected Output:
+  PrintRoute at Time: +0s from Node 0 to address 10.1.5.2, 64 hops Max.
+  There is no path from Node 0 to Node 5.
+
+  Test 4: Route from n0 to n3
+  expected Output:
+  PrintRoute at Time: +0s from Node 0 to Node 3, 64 hops Max.
+   1  10.1.4.2 (Node 2)
+   2  10.1.3.2 (Node 3)
+
+  Test 5: Route from n0 to 10.1.2.2
+  expected Output:
+  PrintRoute at Time: +0s from Node 0 to address 10.1.2.2, 64 hops Max.
+   1  10.1.2.2
+
+  Test 6: Route from n0 to n3
+  expected Output:
+  PrintRoute at Time: +0s from Node 0 to Node 3, 64 hops Max.
+   1  10.1.4.2
+   2  10.1.3.2
+
+  Test 7: Route from n1 to 10.1.2.2 at time +2s
+  expected Output:
+  PrintRoute at Time: +2s from Node 1 to address 10.1.2.2, 64 hops Max.
+   1  10.1.2.2 (Node 2)
+
+  Test 8: Route from n1 to n3 at time +4s
+  expected Output:
+  PrintRoute at Time: +4s from Node 1 to Node 3, 64 hops Max.
+   1  10.1.2.2 (Node 2)
+   2  10.1.3.2 (Node 3)
+   @endverbatim
+*/
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("Ipv4GlobalRoutingPrintRoute");
+
+int
+main(int argc, char* argv[])
+{
+    NodeContainer n;
+    n.Create(6);
+    PointToPointHelper pointToPoint;
+    // globalroutinghelper to install global routing on all nodes
+    Ipv4GlobalRoutingHelper globalRouting;
+    InternetStackHelper stack;
+    stack.SetRoutingHelper(globalRouting); // has effect on the next Install ()
+    stack.Install(n);
+
+    // NetDeviceContainers are the return type
+    auto devices01 = pointToPoint.Install(n.Get(0), n.Get(1));
+    auto devices12 = pointToPoint.Install(n.Get(1), n.Get(2));
+    auto devices23 = pointToPoint.Install(n.Get(2), n.Get(3));
+    auto devices02 = pointToPoint.Install(n.Get(0), n.Get(2));
+    auto devices45 = pointToPoint.Install(n.Get(4), n.Get(5));
+
+    // Assign IP Addresses
+    Ipv4AddressHelper address;
+    address.SetBase("10.1.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer interfaces01 = address.Assign(devices01);
+    address.SetBase("10.1.2.0", "255.255.255.0");
+    Ipv4InterfaceContainer interfaces12 = address.Assign(devices12);
+    address.SetBase("10.1.3.0", "255.255.255.0");
+    Ipv4InterfaceContainer interfaces23 = address.Assign(devices23);
+    address.SetBase("10.1.4.0", "255.255.255.0");
+    Ipv4InterfaceContainer interfaces02 = address.Assign(devices02);
+    address.SetBase("10.1.5.0", "255.255.255.0");
+    Ipv4InterfaceContainer interfaces45 = address.Assign(devices45);
+
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+    // create an output stream to be used in the examples
+    Ptr<OutputStreamWrapper> routingStream = Create<OutputStreamWrapper>(&std::cout);
+
+    //----------------------Now the examples-----------------------
+
+    // PrintRoute(): Route from:  Source node  to destination IP Address given an output stream
+    Ipv4GlobalRoutingHelper::PrintRoute(n.Get(1), interfaces12.GetAddress(1), routingStream);
+
+    // PrintRoute(): Route from : Source node to destination node given an output stream
+    Ipv4GlobalRoutingHelper::PrintRoute(n.Get(1), n.Get(3), routingStream);
+
+    // Variant with output stream as std::cout by default
+    Ipv4GlobalRoutingHelper::PrintRoute(n.Get(0), interfaces45.GetAddress(1));
+
+    Ipv4GlobalRoutingHelper::PrintRoute(n.Get(0), n.Get(3));
+
+    //  PrintRoute() with nodeIdLookup disabled
+    Ipv4GlobalRoutingHelper::PrintRoute(n.Get(0), interfaces12.GetAddress(1), false);
+    // also provide the output stream
+    Ipv4GlobalRoutingHelper::PrintRoute(n.Get(0), n.Get(3), routingStream, false);
+
+    // The At Variants of PrintRoute() behave similarly with the added time argument.
+    Ipv4GlobalRoutingHelper::PrintRouteAt(n.Get(1), interfaces12.GetAddress(1), Seconds(2));
+    Ipv4GlobalRoutingHelper::PrintRouteAt(n.Get(1), n.Get(3), Seconds(4));
+
+    // uncomment to see all routing tables
+    // Ipv4GlobalRoutingHelper::PrintRoutingTableAllAt(Seconds(3),routingStream);
+
+    Simulator::Stop(Seconds(10));
+    Simulator::Run();
+    Simulator::Destroy();
+    return 0;
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/internet-stack-helper.cc ns-3.47/src/internet/helper/internet-stack-helper.cc
--- ns-3.46/src/internet/helper/internet-stack-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/helper/internet-stack-helper.cc	2026-02-16 16:08:49.608229900 -0800
@@ -12,6 +12,8 @@
 #include "ipv4-global-routing-helper.h"
 #include "ipv4-list-routing-helper.h"
 #include "ipv4-static-routing-helper.h"
+#include "ipv6-global-routing-helper.h"
+#include "ipv6-list-routing-helper.h"
 #include "ipv6-static-routing-helper.h"
 
 #include "ns3/arp-l3-protocol.h"
@@ -19,8 +21,8 @@
 #include "ns3/callback.h"
 #include "ns3/config.h"
 #include "ns3/global-router-interface.h"
+#include "ns3/global-routing.h"
 #include "ns3/icmpv6-l4-protocol.h"
-#include "ns3/ipv4-global-routing.h"
 #include "ns3/ipv4.h"
 #include "ns3/ipv6-extension-demux.h"
 #include "ns3/ipv6-extension-header.h"
@@ -121,11 +123,15 @@
     Ipv4StaticRoutingHelper staticRouting;
     Ipv4GlobalRoutingHelper globalRouting;
     Ipv4ListRoutingHelper listRouting;
-    Ipv6StaticRoutingHelper staticRoutingv6;
+    Ipv6StaticRoutingHelper staticRouting6;
+    Ipv6GlobalRoutingHelper globalRouting6;
+    Ipv6ListRoutingHelper listRouting6;
     listRouting.Add(staticRouting, 0);
     listRouting.Add(globalRouting, -10);
     SetRoutingHelper(listRouting);
-    SetRoutingHelper(staticRoutingv6);
+    listRouting6.Add(staticRouting6, 0);
+    listRouting6.Add(globalRouting6, -10);
+    SetRoutingHelper(listRouting6);
 }
 
 InternetStackHelper::~InternetStackHelper()
@@ -215,10 +221,10 @@
     for (auto i = c.Begin(); i != c.End(); ++i)
     {
         Ptr<Node> node = *i;
-        Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
-        if (router)
+        Ptr<GlobalRouter<Ipv4Manager>> routerv4 = node->GetObject<GlobalRouter<Ipv4Manager>>();
+        if (routerv4)
         {
-            Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+            Ptr<Ipv4GlobalRouting> gr = routerv4->GetRoutingProtocol();
             if (gr)
             {
                 currentStream += gr->AssignStreams(currentStream);
@@ -229,7 +235,12 @@
         {
             Ptr<Ipv6Extension> fe = demux->GetExtension(Ipv6ExtensionFragment::EXT_NUMBER);
             NS_ASSERT(fe); // should always exist in the demux
-            currentStream += fe->AssignStreams(currentStream);
+            currentStream += demux->AssignStreams(currentStream);
+        }
+        Ptr<Ipv6ExtensionRoutingDemux> demuxRouter = node->GetObject<Ipv6ExtensionRoutingDemux>();
+        if (demuxRouter)
+        {
+            currentStream += demuxRouter->AssignStreams(currentStream);
         }
         Ptr<Ipv4> ipv4 = node->GetObject<Ipv4>();
         if (ipv4)
@@ -250,6 +261,22 @@
             }
         }
     }
+    // The below is intentionally left out of the above loop, to avoid some stream
+    // assignment perturbations on existing programs due to adding IPv6 global routing.
+    // Future extensions of this method should also copy this approach.
+    for (auto i = c.Begin(); i != c.End(); ++i)
+    {
+        Ptr<Node> node = *i;
+        Ptr<GlobalRouter<Ipv6Manager>> routerv6 = node->GetObject<GlobalRouter<Ipv6Manager>>();
+        if (routerv6)
+        {
+            Ptr<GlobalRouting<Ipv6RoutingProtocol>> gr = routerv6->GetRoutingProtocol();
+            if (gr)
+            {
+                currentStream += gr->AssignStreams(currentStream);
+            }
+        }
+    }
     return (currentStream - stream);
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv4-global-routing-helper.cc ns-3.47/src/internet/helper/ipv4-global-routing-helper.cc
--- ns-3.46/src/internet/helper/ipv4-global-routing-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/helper/ipv4-global-routing-helper.cc	2026-02-16 16:08:49.609229800 -0800
@@ -7,15 +7,16 @@
  */
 #include "ipv4-global-routing-helper.h"
 
+#include "ns3/global-route-manager.h"
 #include "ns3/global-router-interface.h"
-#include "ns3/ipv4-global-routing.h"
+#include "ns3/global-routing.h"
 #include "ns3/ipv4-list-routing.h"
 #include "ns3/log.h"
 
 namespace ns3
 {
 
-NS_LOG_COMPONENT_DEFINE("GlobalRoutingHelper");
+NS_LOG_COMPONENT_DEFINE("Ipv4GlobalRoutingHelper");
 
 Ipv4GlobalRoutingHelper::Ipv4GlobalRoutingHelper()
 {
@@ -36,11 +37,12 @@
 {
     NS_LOG_LOGIC("Adding GlobalRouter interface to node " << node->GetId());
 
-    Ptr<GlobalRouter> globalRouter = CreateObject<GlobalRouter>();
+    Ptr<GlobalRouter<Ipv4Manager>> globalRouter = CreateObject<GlobalRouter<Ipv4Manager>>();
     node->AggregateObject(globalRouter);
 
     NS_LOG_LOGIC("Adding GlobalRouting Protocol to node " << node->GetId());
-    Ptr<Ipv4GlobalRouting> globalRouting = CreateObject<Ipv4GlobalRouting>();
+    Ptr<GlobalRouting<Ipv4RoutingProtocol>> globalRouting =
+        CreateObject<GlobalRouting<Ipv4RoutingProtocol>>();
     globalRouter->SetRoutingProtocol(globalRouting);
 
     return globalRouting;
@@ -49,16 +51,124 @@
 void
 Ipv4GlobalRoutingHelper::PopulateRoutingTables()
 {
-    GlobalRouteManager::BuildGlobalRoutingDatabase();
-    GlobalRouteManager::InitializeRoutes();
+    Ipv4GlobalRouteManager::BuildGlobalRoutingDatabase();
+    Ipv4GlobalRouteManager::InitializeRoutes();
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRoute(Ptr<Node> sourceNode,
+                                    Ipv4Address dest,
+                                    Ptr<OutputStreamWrapper> stream,
+                                    bool nodeIdLookup,
+                                    Time::Unit unit)
+{
+    GlobalRouteManager<Ipv4Manager>::PrintRoute(sourceNode, dest, stream, nodeIdLookup, unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRoute(Ptr<Node> sourceNode,
+                                    Ipv4Address dest,
+                                    bool nodeIdLookup,
+                                    Time::Unit unit)
+{
+    GlobalRouteManager<Ipv4Manager>::PrintRoute(sourceNode, dest, nodeIdLookup, unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRoute(Ptr<Node> sourceNode,
+                                    Ptr<Node> dest,
+                                    Ptr<OutputStreamWrapper> stream,
+                                    bool nodeIdLookup,
+                                    Time::Unit unit)
+{
+    GlobalRouteManager<Ipv4Manager>::PrintRoute(sourceNode, dest, stream, nodeIdLookup, unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRoute(Ptr<Node> sourceNode,
+                                    Ptr<Node> dest,
+                                    bool nodeIdLookup,
+                                    Time::Unit unit)
+{
+    GlobalRouteManager<Ipv4Manager>::PrintRoute(sourceNode, dest, nodeIdLookup, unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRouteAt(Ptr<Node> sourceNode,
+                                      Ipv4Address dest,
+                                      Time printTime,
+                                      Ptr<OutputStreamWrapper> stream,
+                                      bool nodeIdLookup,
+                                      Time::Unit unit)
+{
+    Simulator::Schedule(
+        printTime,
+        static_cast<void (*)(Ptr<Node>, Ipv4Address, Ptr<OutputStreamWrapper>, bool, Time::Unit)>(
+            &Ipv4GlobalRoutingHelper::PrintRoute),
+        sourceNode,
+        dest,
+        stream,
+        nodeIdLookup,
+        unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRouteAt(Ptr<Node> sourceNode,
+                                      Ipv4Address dest,
+                                      Time printTime,
+                                      bool nodeIdLookup,
+                                      Time::Unit unit)
+{
+    Simulator::Schedule(printTime,
+                        static_cast<void (*)(Ptr<Node>, Ipv4Address, bool, Time::Unit)>(
+                            &Ipv4GlobalRoutingHelper::PrintRoute),
+                        sourceNode,
+                        dest,
+                        nodeIdLookup,
+                        unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRouteAt(Ptr<Node> sourceNode,
+                                      Ptr<Node> dest,
+                                      Time printTime,
+                                      Ptr<OutputStreamWrapper> stream,
+                                      bool nodeIdLookup,
+                                      Time::Unit unit)
+{
+    Simulator::Schedule(
+        printTime,
+        static_cast<void (*)(Ptr<Node>, Ptr<Node>, Ptr<OutputStreamWrapper>, bool, Time::Unit)>(
+            &Ipv4GlobalRoutingHelper::PrintRoute),
+        sourceNode,
+        dest,
+        stream,
+        nodeIdLookup,
+        unit);
+}
+
+void
+Ipv4GlobalRoutingHelper::PrintRouteAt(Ptr<Node> sourceNode,
+                                      Ptr<Node> dest,
+                                      Time printTime,
+                                      bool nodeIdLookup,
+                                      Time::Unit unit)
+{
+    Simulator::Schedule(printTime,
+                        static_cast<void (*)(Ptr<Node>, Ptr<Node>, bool, Time::Unit)>(
+                            &Ipv4GlobalRoutingHelper::PrintRoute),
+                        sourceNode,
+                        dest,
+                        nodeIdLookup,
+                        unit);
 }
 
 void
 Ipv4GlobalRoutingHelper::RecomputeRoutingTables()
 {
-    GlobalRouteManager::DeleteGlobalRoutes();
-    GlobalRouteManager::BuildGlobalRoutingDatabase();
-    GlobalRouteManager::InitializeRoutes();
+    Ipv4GlobalRouteManager::DeleteGlobalRoutes();
+    Ipv4GlobalRouteManager::BuildGlobalRoutingDatabase();
+    Ipv4GlobalRouteManager::InitializeRoutes();
 }
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv4-global-routing-helper.h ns-3.47/src/internet/helper/ipv4-global-routing-helper.h
--- ns-3.46/src/internet/helper/ipv4-global-routing-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/helper/ipv4-global-routing-helper.h	2026-02-16 16:08:49.609229800 -0800
@@ -65,6 +65,128 @@
      *
      */
     static void PopulateRoutingTables();
+
+    /**
+     * @brief prints the routing path for a source and destination to the standard cout output
+     * stream. If the routing path does not exist, it prints that the path does not exist between
+     * the nodes in the ostream. This is a scheduler for the PrintRoute call.
+     *
+     * @param sourceNode the source node
+     * @param dest the IPv4 destination address
+     * @param nodeIdLookup print the node id
+     * @param unit the time unit to be used in the report
+     *
+     * @details This method calls the PrintRoutingPath() method of the
+     * Ipv4GlobalRouting for the source and destination to provide
+     * the routing path at the specified time.
+     * Early return will be triggered if inputs are invalid. for example:
+     * if source or destination nodes do not exist, source or destination nodes lack IPv4 instances,
+     * or source node lacks a global routing instance.
+     * If the destination node has multiple IpAddresses, the routing path will be printed for the
+     * first Ip address of the destination Ipv4 Stack.
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           Ipv4Address dest,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     * @brief prints the routing path for a source and destination.
+     * @copydetails PrintRoute(Ptr<Node>, Ipv4Address, bool, Time::Unit)
+     * @param stream the output stream object to use
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           Ipv4Address dest,
+                           Ptr<OutputStreamWrapper> stream,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     * @copybrief PrintRoute(Ptr<Node>, Ipv4Address,  bool,
+     * Time::Unit)
+     * If the routing path does not exist, it prints that the path does not exist between
+     * the nodes in the ostream. This is a scheduler for the PrintRoute call.
+     *
+     * @param sourceNode the source node
+     * @param dest the destination node
+     * @param nodeIdLookup print the node id
+     * @param unit the time unit to be used in the report
+     *
+     * @details This method calls the PrintRoutingPath() method of the
+     * Ipv4GlobalRouting for the source and destination to provide
+     * the routing path at the specified time.
+     * Early return will be triggered if inputs are invalid. for example:
+     * if source or destination nodes do not exist, source or destination nodes lack IPv4 instances,
+     * or source node lacks a global routing instance.
+     * If the destination node has multiple IpAddresses, the routing path will be printed for the
+     * first Ip address of the destination Ipv4 Stack.
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           Ptr<Node> dest,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     * @copybrief PrintRoute(Ptr<Node>, Ipv4Address, Ptr<OutputStreamWrapper>, bool,
+     * Time::Unit)
+     * @copydetails PrintRoute(Ptr<Node>, Ptr<Node>, bool, Time::Unit)
+     * @param stream the output stream object to use
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           Ptr<Node> dest,
+                           Ptr<OutputStreamWrapper> stream,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     * @brief prints the routing path for the source and destination at a particular time to the
+     * standard cout output stream.
+     * @copydetails PrintRoute(Ptr<Node>, Ipv4Address, bool, Time::Unit)
+     * @param printTime the time at which the routing path should be printed.
+     */
+    static void PrintRouteAt(Ptr<Node> sourceNode,
+                             Ipv4Address dest,
+                             Time printTime,
+                             bool nodeIdLookup = true,
+                             Time::Unit unit = Time::S);
+
+    /**
+     * @brief Prints the routing path for the source and destination at a particular time.
+     * @copydetails PrintRoute(Ptr<Node>, Ipv4Address,bool, Time::Unit)
+     * @param printTime the time at which the routing path should be printed.
+     * @param stream the output stream object to use
+     */
+    static void PrintRouteAt(Ptr<Node> sourceNode,
+                             Ipv4Address dest,
+                             Time printTime,
+                             Ptr<OutputStreamWrapper> stream,
+                             bool nodeIdLookup = true,
+                             Time::Unit unit = Time::S);
+
+    /**
+     * @copybrief PrintRouteAt(Ptr<Node>, Ipv4Address, Time, bool, Time::Unit)
+     * @copydetails PrintRoute(Ptr<Node>, Ptr<Node>, bool, Time::Unit)
+     * @param printTime the time at which the routing path should be printed.
+     */
+    static void PrintRouteAt(Ptr<Node> sourceNode,
+                             Ptr<Node> dest,
+                             Time printTime,
+                             bool nodeIdLookup = true,
+                             Time::Unit unit = Time::S);
+
+    /**
+     * @copybrief PrintRouteAt(Ptr<Node>, Ipv4Address, Time, bool, Time::Unit)
+     * @copydetails PrintRoute(Ptr<Node>, Ptr<Node>, bool, Time::Unit)
+     * @param printTime the time at which the routing path should be printed.
+     * @param stream the output stream object to use.
+     */
+    static void PrintRouteAt(Ptr<Node> sourceNode,
+                             Ptr<Node> dest,
+                             Time printTime,
+                             Ptr<OutputStreamWrapper> stream,
+                             bool nodeIdLookup = true,
+                             Time::Unit unit = Time::S);
+
     /**
      * @brief Remove all routes that were previously installed in a prior call
      * to either PopulateRoutingTables() or RecomputeRoutingTables(), and
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv4-interface-container.h ns-3.47/src/internet/helper/ipv4-interface-container.h
--- ns-3.46/src/internet/helper/ipv4-interface-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/helper/ipv4-interface-container.h	2026-02-16 16:08:49.609229800 -0800
@@ -70,12 +70,11 @@
      * for-loop to run through the pairs
      *
      * @code
-     *   Ipv4InterfaceContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
      *       std::pair<Ptr<Ipv4>, uint32_t> pair = *i;
-     *       method (pair.first, pair.second);  // use the pair
-     *     }
+     *       method(pair.first, pair.second); // use the pair
+     *   }
      * @endcode
      *
      * @returns an iterator which refers to the first pair in the container.
@@ -92,12 +91,11 @@
      * for-loop to run through the Nodes
      *
      * @code
-     *   NodeContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
      *       std::pair<Ptr<Ipv4>, uint32_t> pair = *i;
-     *       method (pair.first, pair.second);  // use the pair
-     *     }
+     *       method(pair.first, pair.second); // use the pair
+     *   }
      * @endcode
      *
      * @returns an iterator which indicates an ending condition for a loop.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv6-address-helper.cc ns-3.47/src/internet/helper/ipv6-address-helper.cc
--- ns-3.46/src/internet/helper/ipv6-address-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/helper/ipv6-address-helper.cc	2026-02-16 16:08:49.609229800 -0800
@@ -140,7 +140,7 @@
         addrBuf[i] = netBuf[i] | hostBuf[i];
     }
 
-    Ipv6Address addr = Ipv6Address(addrBuf);
+    auto addr = Ipv6Address(addrBuf);
 
     // Remember: hostBuf[15] is the Least Significant Byte.
     uint16_t sum;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv6-global-routing-helper.cc ns-3.47/src/internet/helper/ipv6-global-routing-helper.cc
--- ns-3.46/src/internet/helper/ipv6-global-routing-helper.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/helper/ipv6-global-routing-helper.cc	2026-02-16 16:08:49.609229800 -0800
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2008 INRIA
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
+ */
+#include "ipv6-global-routing-helper.h"
+
+#include "ns3/global-router-interface.h"
+#include "ns3/global-routing.h"
+#include "ns3/ipv6-list-routing.h"
+#include "ns3/log.h"
+
+namespace ns3
+{
+
+NS_LOG_COMPONENT_DEFINE("Ipv6GlobalRoutingHelper");
+
+Ipv6GlobalRoutingHelper::Ipv6GlobalRoutingHelper()
+{
+}
+
+Ipv6GlobalRoutingHelper::Ipv6GlobalRoutingHelper(const Ipv6GlobalRoutingHelper& o)
+{
+}
+
+Ipv6GlobalRoutingHelper*
+Ipv6GlobalRoutingHelper::Copy() const
+{
+    return new Ipv6GlobalRoutingHelper(*this);
+}
+
+Ptr<Ipv6RoutingProtocol>
+Ipv6GlobalRoutingHelper::Create(Ptr<Node> node) const
+{
+    NS_LOG_LOGIC("Adding GlobalRouter interface to node " << node->GetId());
+
+    Ptr<GlobalRouter<Ipv6Manager>> globalRouter = CreateObject<GlobalRouter<Ipv6Manager>>();
+    node->AggregateObject(globalRouter);
+
+    NS_LOG_LOGIC("Adding GlobalRouting Protocol to node " << node->GetId());
+    Ptr<GlobalRouting<Ipv6RoutingProtocol>> globalRouting =
+        CreateObject<GlobalRouting<Ipv6RoutingProtocol>>();
+    globalRouter->SetRoutingProtocol(globalRouting);
+
+    return globalRouting;
+}
+
+void
+Ipv6GlobalRoutingHelper::PopulateRoutingTables()
+{
+    GlobalRouteManager<Ipv6Manager>::InitializeRouters();
+    GlobalRouteManager<Ipv6Manager>::BuildGlobalRoutingDatabase();
+    GlobalRouteManager<Ipv6Manager>::InitializeRoutes();
+}
+
+void
+Ipv6GlobalRoutingHelper::RecomputeRoutingTables()
+{
+    GlobalRouteManager<Ipv6Manager>::DeleteGlobalRoutes();
+    GlobalRouteManager<Ipv6Manager>::BuildGlobalRoutingDatabase();
+    GlobalRouteManager<Ipv6Manager>::InitializeRoutes();
+}
+
+} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv6-global-routing-helper.h ns-3.47/src/internet/helper/ipv6-global-routing-helper.h
--- ns-3.46/src/internet/helper/ipv6-global-routing-helper.h	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/helper/ipv6-global-routing-helper.h	2026-02-16 16:08:49.609229800 -0800
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2008 INRIA
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
+ */
+#ifndef IPV6_GLOBAL_ROUTING_HELPER_H
+#define IPV6_GLOBAL_ROUTING_HELPER_H
+
+#include "ipv6-routing-helper.h"
+
+#include "ns3/node-container.h"
+
+namespace ns3
+{
+
+/**
+ * @ingroup ipv4Helpers
+ *
+ * @brief Helper class that adds ns3::Ipv4GlobalRouting objects
+ */
+class Ipv6GlobalRoutingHelper : public Ipv6RoutingHelper
+{
+  public:
+    /**
+     * @brief Construct a GlobalRoutingHelper to make life easier for managing
+     * global routing tasks.
+     */
+    Ipv6GlobalRoutingHelper();
+
+    /**
+     * @brief Construct a GlobalRoutingHelper from another previously initialized
+     * instance (Copy Constructor).
+     * @param o object to be copied
+     */
+    Ipv6GlobalRoutingHelper(const Ipv6GlobalRoutingHelper& o);
+
+    // Delete assignment operator to avoid misuse
+    Ipv6GlobalRoutingHelper& operator=(const Ipv6GlobalRoutingHelper&) = delete;
+
+    /**
+     * @returns pointer to clone of this Ipv4GlobalRoutingHelper
+     *
+     * This method is mainly for internal use by the other helpers;
+     * clients are expected to free the dynamic memory allocated by this method
+     */
+    Ipv6GlobalRoutingHelper* Copy() const override;
+
+    /**
+     * @param node the node on which the routing protocol will run
+     * @returns a newly-created routing protocol
+     *
+     * This method will be called by ns3::InternetStackHelper::Install
+     */
+    Ptr<Ipv6RoutingProtocol> Create(Ptr<Node> node) const override;
+
+    /**
+     * @brief Build a routing database and initialize the routing tables of
+     * the nodes in the simulation.  Makes all nodes in the simulation into
+     * routers.
+     *
+     * All this function does is call the functions
+     * BuildGlobalRoutingDatabase () and  InitializeRoutes ().
+     *
+     */
+    static void PopulateRoutingTables();
+
+    /**
+     * @brief Remove all routes that were previously installed in a prior call
+     * to either PopulateRoutingTables() or RecomputeRoutingTables(), and
+     * add a new set of routes.
+     *
+     * This method does not change the set of nodes
+     * over which GlobalRouting is being used, but it will dynamically update
+     * its representation of the global topology before recomputing routes.
+     * Users must first call PopulateRoutingTables() and then may subsequently
+     * call RecomputeRoutingTables() at any later time in the simulation.
+     *
+     */
+    static void RecomputeRoutingTables();
+
+    /**
+     * @brief initialize all nodes as routers. this method queries all the nodes in the simulation
+     * and enables ipv6 forwarding on all of them.
+     */
+    void InitializeRouters();
+};
+
+} // namespace ns3
+
+#endif /* IPV6_GLOBAL_ROUTING_HELPER_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/helper/ipv6-interface-container.h ns-3.47/src/internet/helper/ipv6-interface-container.h
--- ns-3.46/src/internet/helper/ipv6-interface-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/helper/ipv6-interface-container.h	2026-02-16 16:08:49.609229800 -0800
@@ -105,12 +105,11 @@
      * for-loop to run through the pairs
      *
      * @code
-     *   Ipv6InterfaceContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
      *       std::pair<Ptr<Ipv6>, uint32_t> pair = *i;
-     *       method (pair.first, pair.second);  // use the pair
-     *     }
+     *       method(pair.first, pair.second); // use the pair
+     *   }
      * @endcode
      *
      * @returns an iterator which refers to the first pair in the container.
@@ -127,12 +126,11 @@
      * for-loop to run through the Nodes
      *
      * @code
-     *   NodeContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
      *       std::pair<Ptr<Ipv6>, uint32_t> pair = *i;
-     *       method (pair.first, pair.second);  // use the pair
-     *     }
+     *       method(pair.first, pair.second); // use the pair
+     *   }
      * @endcode
      *
      * @returns an iterator which indicates an ending condition for a loop.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/arp-header.cc ns-3.47/src/internet/model/arp-header.cc
--- ns-3.46/src/internet/model/arp-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/arp-header.cc	2026-02-16 16:08:49.610230000 -0800
@@ -212,10 +212,14 @@
     }
 
     m_type = static_cast<ArpType_e>(i.ReadNtohU16()); // Read OP
-    ReadFrom(i, m_macSource, hardwareAddressLen);     // Read SHA (size HLN)
-    ReadFrom(i, m_ipv4Source);                        // Read SPA (size PLN == 4)
-    ReadFrom(i, m_macDest, hardwareAddressLen);       // Read THA (size HLN)
-    ReadFrom(i, m_ipv4Dest);                          // Read TPA (size PLN == 4)
+
+    m_macSource.SetType("MacAddress", hardwareAddressLen);
+    m_macDest.SetType("MacAddress", hardwareAddressLen);
+
+    ReadFrom(i, m_macSource, hardwareAddressLen); // Read SHA (size HLN)
+    ReadFrom(i, m_ipv4Source);                    // Read SPA (size PLN == 4)
+    ReadFrom(i, m_macDest, hardwareAddressLen);   // Read THA (size HLN)
+    ReadFrom(i, m_ipv4Dest);                      // Read TPA (size PLN == 4)
     return GetSerializedSize();
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/candidate-queue.cc ns-3.47/src/internet/model/candidate-queue.cc
--- ns-3.46/src/internet/model/candidate-queue.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/candidate-queue.cc	2026-02-16 16:08:49.610230000 -0800
@@ -26,15 +26,16 @@
  * @param t the SPFVertex type
  * @returns the reference to the output stream
  */
+template <typename T>
 std::ostream&
-operator<<(std::ostream& os, const SPFVertex::VertexType& t)
+operator<<(std::ostream& os, const typename SPFVertex<T>::VertexType& t)
 {
     switch (t)
     {
-    case SPFVertex::VertexRouter:
+    case SPFVertex<T>::VertexRouter:
         os << "router";
         break;
-    case SPFVertex::VertexNetwork:
+    case SPFVertex<T>::VertexNetwork:
         os << "network";
         break;
     default:
@@ -44,10 +45,11 @@
     return os;
 }
 
+template <typename T>
 std::ostream&
-operator<<(std::ostream& os, const CandidateQueue& q)
+operator<<(std::ostream& os, const CandidateQueue<T>& q)
 {
-    const CandidateQueue::CandidateList_t& list = q.m_candidates;
+    const typename CandidateQueue<T>::CandidateList_t& list = q.m_candidates;
 
     os << "*** CandidateQueue Begin (<id, distance, LSA-type>) ***" << std::endl;
     for (auto iter = list.begin(); iter != list.end(); iter++)
@@ -59,32 +61,36 @@
     return os;
 }
 
-CandidateQueue::CandidateQueue()
+template <typename T>
+CandidateQueue<T>::CandidateQueue()
     : m_candidates()
 {
     NS_LOG_FUNCTION(this);
 }
 
-CandidateQueue::~CandidateQueue()
+template <typename T>
+CandidateQueue<T>::~CandidateQueue()
 {
     NS_LOG_FUNCTION(this);
     Clear();
 }
 
+template <typename T>
 void
-CandidateQueue::Clear()
+CandidateQueue<T>::Clear()
 {
     NS_LOG_FUNCTION(this);
     while (!m_candidates.empty())
     {
-        SPFVertex* p = Pop();
+        SPFVertex<T>* p = Pop();
         delete p;
         p = nullptr;
     }
 }
 
+template <typename T>
 void
-CandidateQueue::Push(SPFVertex* vNew)
+CandidateQueue<T>::Push(SPFVertex<T>* vNew)
 {
     NS_LOG_FUNCTION(this << vNew);
 
@@ -95,8 +101,9 @@
     m_candidates.insert(i, vNew);
 }
 
-SPFVertex*
-CandidateQueue::Pop()
+template <typename T>
+SPFVertex<T>*
+CandidateQueue<T>::Pop()
 {
     NS_LOG_FUNCTION(this);
     if (m_candidates.empty())
@@ -104,13 +111,14 @@
         return nullptr;
     }
 
-    SPFVertex* v = m_candidates.front();
+    SPFVertex<T>* v = m_candidates.front();
     m_candidates.pop_front();
     return v;
 }
 
-SPFVertex*
-CandidateQueue::Top() const
+template <typename T>
+SPFVertex<T>*
+CandidateQueue<T>::Top() const
 {
     NS_LOG_FUNCTION(this);
     if (m_candidates.empty())
@@ -121,29 +129,32 @@
     return m_candidates.front();
 }
 
+template <typename T>
 bool
-CandidateQueue::Empty() const
+CandidateQueue<T>::Empty() const
 {
     NS_LOG_FUNCTION(this);
     return m_candidates.empty();
 }
 
+template <typename T>
 uint32_t
-CandidateQueue::Size() const
+CandidateQueue<T>::Size() const
 {
     NS_LOG_FUNCTION(this);
     return m_candidates.size();
 }
 
-SPFVertex*
-CandidateQueue::Find(const Ipv4Address addr) const
+template <typename T>
+SPFVertex<T>*
+CandidateQueue<T>::Find(const IpAddress addr) const
 {
     NS_LOG_FUNCTION(this);
     auto i = m_candidates.begin();
 
     for (; i != m_candidates.end(); i++)
     {
-        SPFVertex* v = *i;
+        SPFVertex<T>* v = *i;
         if (v->GetVertexId() == addr)
         {
             return v;
@@ -153,8 +164,9 @@
     return nullptr;
 }
 
+template <typename T>
 void
-CandidateQueue::Reorder()
+CandidateQueue<T>::Reorder()
 {
     NS_LOG_FUNCTION(this);
 
@@ -170,8 +182,9 @@
  *
  * This ordering is necessary for implementing ECMP
  */
+template <typename T>
 bool
-CandidateQueue::CompareSPFVertex(const SPFVertex* v1, const SPFVertex* v2)
+CandidateQueue<T>::CompareSPFVertex(const SPFVertex<T>* v1, const SPFVertex<T>* v2)
 {
     NS_LOG_FUNCTION(&v1 << &v2);
 
@@ -182,13 +195,15 @@
     }
     else if (v1->GetDistanceFromRoot() == v2->GetDistanceFromRoot())
     {
-        if (v1->GetVertexType() == SPFVertex::VertexNetwork &&
-            v2->GetVertexType() == SPFVertex::VertexRouter)
+        if (v1->GetVertexType() == SPFVertex<T>::VertexNetwork &&
+            v2->GetVertexType() == SPFVertex<T>::VertexRouter)
         {
             result = true;
         }
     }
     return result;
 }
+template class CandidateQueue<Ipv4Manager>;
+template class CandidateQueue<Ipv6Manager>;
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/candidate-queue.h ns-3.47/src/internet/model/candidate-queue.h
--- ns-3.46/src/internet/model/candidate-queue.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/candidate-queue.h	2026-02-16 16:08:49.610230000 -0800
@@ -9,7 +9,10 @@
 #ifndef CANDIDATE_QUEUE_H
 #define CANDIDATE_QUEUE_H
 
+#include "global-route-manager.h"
+
 #include "ns3/ipv4-address.h"
+#include "ns3/ipv6-address.h"
 
 #include <list>
 #include <stdint.h>
@@ -17,8 +20,16 @@
 namespace ns3
 {
 
+template <typename T>
 class SPFVertex;
 
+template <typename>
+class CandidateQueue;
+
+// Forward declaration of operator<<
+template <typename T>
+std::ostream& operator<<(std::ostream& os, const CandidateQueue<T>& q);
+
 /**
  * @ingroup globalrouting
  *
@@ -38,8 +49,24 @@
  * requirement for a Reorder () operation led us to implement this simple
  * enhanced priority queue.
  */
+template <typename T>
 class CandidateQueue
 {
+    static_assert(std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+                  "T must be either Ipv4Manager or Ipv6Manager when calling CandidateQueue");
+
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4Manager and Ipv6Manager classes
+    using IpManager = typename std::conditional_t<IsIpv4, Ipv4Manager, Ipv6Manager>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
   public:
     /**
      * @brief Create an empty SPF Candidate Queue.
@@ -80,7 +107,7 @@
      * @see SPFVertex
      * @param vNew The Shortest Path First Vertex to add to the queue.
      */
-    void Push(SPFVertex* vNew);
+    void Push(SPFVertex<T>* vNew);
 
     /**
      * @brief Pop the Shortest Path First Vertex pointer at the top of the queue.
@@ -92,7 +119,7 @@
      * @see Top ()
      * @returns The Shortest Path First Vertex pointer at the top of the queue.
      */
-    SPFVertex* Pop();
+    SPFVertex<T>* Pop();
 
     /**
      * @brief Return the Shortest Path First Vertex pointer at the top of the
@@ -105,7 +132,7 @@
      * @see Pop ()
      * @returns The Shortest Path First Vertex pointer at the top of the queue.
      */
-    SPFVertex* Top() const;
+    SPFVertex<T>* Top() const;
 
     /**
      * @brief Test the Candidate Queue to determine if it is empty.
@@ -131,7 +158,7 @@
      * @param addr The IP address to search for.
      * @returns The SPFVertex* pointer corresponding to the given IP address.
      */
-    SPFVertex* Find(const Ipv4Address addr) const;
+    SPFVertex<T>* Find(const IpAddress addr) const;
 
     /**
      * @brief Reorders the Candidate Queue according to the priority scheme.
@@ -160,10 +187,10 @@
      * @param v2 second operand
      * @return True if v1 should be popped before v2; false otherwise
      */
-    static bool CompareSPFVertex(const SPFVertex* v1, const SPFVertex* v2);
+    static bool CompareSPFVertex(const SPFVertex<T>* v1, const SPFVertex<T>* v2);
 
-    typedef std::list<SPFVertex*> CandidateList_t; //!< container of SPFVertex pointers
-    CandidateList_t m_candidates;                  //!< SPFVertex candidates
+    typedef std::list<SPFVertex<T>*> CandidateList_t; //!< container of SPFVertex pointers
+    CandidateList_t m_candidates;                     //!< SPFVertex candidates
 
     /**
      * @brief Stream insertion operator.
@@ -172,7 +199,7 @@
      * @param q the CandidateQueue
      * @returns the reference to the output stream
      */
-    friend std::ostream& operator<<(std::ostream& os, const CandidateQueue& q);
+    friend std::ostream& operator<< <T>(std::ostream& os, const CandidateQueue& q);
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-route-manager.cc ns-3.47/src/internet/model/global-route-manager.cc
--- ns-3.46/src/internet/model/global-route-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-route-manager.cc	2026-02-16 16:08:49.611230000 -0800
@@ -14,6 +14,8 @@
 #include "ns3/log.h"
 #include "ns3/simulation-singleton.h"
 
+#include <iomanip>
+
 namespace ns3
 {
 
@@ -25,40 +27,130 @@
 //
 // ---------------------------------------------------------------------------
 
-uint32_t GlobalRouteManager::routerId = 0;
+template <typename T>
+uint32_t GlobalRouteManager<T>::routerId = 0; //!< Router ID counter
 
+template <typename T>
 void
-GlobalRouteManager::DeleteGlobalRoutes()
+GlobalRouteManager<T>::DeleteGlobalRoutes()
 {
     NS_LOG_FUNCTION_NOARGS();
-    SimulationSingleton<GlobalRouteManagerImpl>::Get()->DeleteGlobalRoutes();
+    SimulationSingleton<GlobalRouteManagerImpl<typename GlobalRouteManager<T>::IpManager>>::Get()
+        ->DeleteGlobalRoutes();
 }
 
+template <typename T>
 void
-GlobalRouteManager::BuildGlobalRoutingDatabase()
+GlobalRouteManager<T>::BuildGlobalRoutingDatabase()
 {
     NS_LOG_FUNCTION_NOARGS();
-    SimulationSingleton<GlobalRouteManagerImpl>::Get()->BuildGlobalRoutingDatabase();
+    SimulationSingleton<GlobalRouteManagerImpl<typename GlobalRouteManager<T>::IpManager>>::Get()
+        ->BuildGlobalRoutingDatabase();
 }
 
+template <typename T>
 void
-GlobalRouteManager::InitializeRoutes()
+GlobalRouteManager<T>::InitializeRoutes()
 {
     NS_LOG_FUNCTION_NOARGS();
-    SimulationSingleton<GlobalRouteManagerImpl>::Get()->InitializeRoutes();
+    SimulationSingleton<GlobalRouteManagerImpl<typename GlobalRouteManager<T>::IpManager>>::Get()
+        ->InitializeRoutes();
 }
 
+template <typename T>
 uint32_t
-GlobalRouteManager::AllocateRouterId()
+GlobalRouteManager<T>::AllocateRouterId()
 {
     NS_LOG_FUNCTION_NOARGS();
     return routerId++;
 }
 
+template <typename T>
 void
-GlobalRouteManager::ResetRouterId()
+GlobalRouteManager<T>::ResetRouterId()
 {
     routerId = 0;
 }
 
+template <typename T>
+void
+GlobalRouteManager<T>::PrintRoute(Ptr<Node> sourceNode,
+                                  IpAddress dest,
+                                  Ptr<OutputStreamWrapper> stream,
+                                  bool nodeIdLookup,
+                                  Time::Unit unit)
+{
+    std::ostream* os = stream->GetStream();
+    // Copy the current ostream state
+    std::ios oldState(nullptr);
+    oldState.copyfmt(*os);
+
+    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
+    *os << "PrintRoute at Time: " << Now().As(unit);
+    *os << " from Node " << sourceNode->GetId() << " to address " << dest;
+    SimulationSingleton<GlobalRouteManagerImpl<IpManager>>::Get()->PrintRoute(sourceNode,
+                                                                              dest,
+                                                                              stream,
+                                                                              nodeIdLookup,
+                                                                              unit);
+    (*os).copyfmt(oldState);
+}
+
+template <typename T>
+void
+GlobalRouteManager<T>::PrintRoute(Ptr<Node> sourceNode,
+                                  IpAddress dest,
+                                  bool nodeIdLookup,
+                                  Time::Unit unit)
+{
+    Ptr<OutputStreamWrapper> stream = Create<OutputStreamWrapper>(&std::cout);
+    GlobalRouteManager<T>::PrintRoute(sourceNode, dest, stream, nodeIdLookup, unit);
+}
+
+template <typename T>
+void
+GlobalRouteManager<T>::PrintRoute(Ptr<Node> sourceNode,
+                                  Ptr<Node> dest,
+                                  Ptr<OutputStreamWrapper> stream,
+                                  bool nodeIdLookup,
+                                  Time::Unit unit)
+{
+    std::ostream* os = stream->GetStream();
+    // Copy the current ostream state
+    std::ios oldState(nullptr);
+    oldState.copyfmt(*os);
+
+    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
+    *os << "PrintRoute at Time: " << Now().As(unit);
+    *os << " from Node " << sourceNode->GetId() << " to Node " << dest->GetId();
+    SimulationSingleton<GlobalRouteManagerImpl<IpManager>>::Get()->PrintRoute(sourceNode,
+                                                                              dest,
+                                                                              stream,
+                                                                              nodeIdLookup,
+                                                                              unit);
+    (*os).copyfmt(oldState);
+}
+
+template <typename T>
+void
+GlobalRouteManager<T>::PrintRoute(Ptr<Node> sourceNode,
+                                  Ptr<Node> dest,
+                                  bool nodeIdLookup,
+                                  Time::Unit unit)
+{
+    Ptr<OutputStreamWrapper> stream = Create<OutputStreamWrapper>(&std::cout);
+    GlobalRouteManager<T>::PrintRoute(sourceNode, dest, stream, nodeIdLookup, unit);
+}
+
+template <typename T>
+void
+GlobalRouteManager<T>::InitializeRouters()
+{
+    SimulationSingleton<GlobalRouteManagerImpl<typename GlobalRouteManager<T>::IpManager>>::Get()
+        ->InitializeRouters();
+}
+
+template class ns3::GlobalRouteManager<ns3::Ipv4Manager>;
+template class ns3::GlobalRouteManager<ns3::Ipv6Manager>;
+
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-route-manager.h ns-3.47/src/internet/model/global-route-manager.h
--- ns-3.46/src/internet/model/global-route-manager.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-route-manager.h	2026-02-16 16:08:49.611230000 -0800
@@ -10,12 +10,31 @@
 #ifndef GLOBAL_ROUTE_MANAGER_H
 #define GLOBAL_ROUTE_MANAGER_H
 
+#include "ipv6.h"
+
+#include "ns3/ipv4-routing-helper.h"
+#include "ns3/ipv6-routing-helper.h"
+
 #include <cstdint>
 
 namespace ns3
 {
 
 /**
+ * Empty struct used as a template parameter
+ */
+struct Ipv4Manager
+{
+};
+
+/**
+ * Empty struct used as a template parameter
+ */
+struct Ipv6Manager
+{
+};
+
+/**
  * @ingroup globalrouting
  *
  * @brief A global global router
@@ -28,12 +47,28 @@
  *
  * The design is guided by OSPFv2 \RFC{2328} section 16.1.1 and quagga ospfd.
  */
+
+template <typename T>
 class GlobalRouteManager
 {
+    static_assert(std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+                  "T must be either Ipv4Manager or Ipv6Manager when calling GlobalRouteManager");
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Manager and Ipv6Manager classes
+    using IpManager = typename std::conditional_t<IsIpv4, Ipv4Manager, Ipv6Manager>;
+
   public:
     // Delete copy constructor and assignment operator to avoid misuse
-    GlobalRouteManager(const GlobalRouteManager&) = delete;
-    GlobalRouteManager& operator=(const GlobalRouteManager&) = delete;
+    GlobalRouteManager(const GlobalRouteManager<T>&) = delete;
+    GlobalRouteManager& operator=(const GlobalRouteManager<T>&) = delete;
 
     /**
      * @brief Allocate a 32-bit router ID from monotonically increasing counter.
@@ -67,10 +102,78 @@
      */
     static void ResetRouterId();
 
+    /**
+     * @brief initialize all nodes as routers. this method queries all the nodes in the simulation
+     * and enables ipv6 forwarding on all of them.
+     */
+    static void InitializeRouters();
+
+    /**
+     * @brief prints the path from this node to the destination node at a particular time.
+     * @param sourceNode The source node.
+     * @param dest The IPv4 address of the destination node.
+     * @param stream The output stream to which the routing path will be written.
+     * @param nodeIdLookup Print the Node Id
+     * @param unit The time unit for timestamps in the printed output.
+     * @see Ipv4GlobalRoutingHelper::PrintRoute
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           IpAddress dest,
+                           Ptr<OutputStreamWrapper> stream,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     *@brief prints the path from this node to the destination node at a particular time.
+     * @param sourceNode The source node.
+     * @param dest The IP address of the destination node.
+     * @param nodeIdLookup Print the Node Id
+     * @param unit The time unit for timestamps in the printed output.
+     * @see Ipv4GlobalRoutingHelper::PrintRoute
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           IpAddress dest,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     *@brief prints the path from this node to the destination node at a particular time.
+     * @param sourceNode  The source node.
+     * @param dest The destination node.
+     * @param stream The output stream to which the routing path will be written.
+     * @param nodeIdLookup Print the Node Id
+     * @param unit The time unit for timestamps in the printed output.
+     * @see Ipv4GlobalRoutingHelper::PrintRoute
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           Ptr<Node> dest,
+                           Ptr<OutputStreamWrapper> stream,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
+    /**
+     *@brief prints the path from this node to the destination node at a particular time.
+     * @param sourceNode The source node.
+     * @param dest The destination node.
+     * @param nodeIdLookup Print the Node Id
+     * @param unit The time unit for timestamps in the printed output.
+     * @see Ipv4GlobalRoutingHelper::PrintRoute
+     */
+    static void PrintRoute(Ptr<Node> sourceNode,
+                           Ptr<Node> dest,
+                           bool nodeIdLookup = true,
+                           Time::Unit unit = Time::S);
+
   private:
     static uint32_t routerId; //!< Router ID counter
 };
 
+/**
+ * @ingroup globalrouting
+ * Create the typedef Ipv4GlobalRouting with T as Ipv4RoutingProtocol
+ */
+typedef GlobalRouteManager<Ipv4Manager> Ipv4GlobalRouteManager;
+
 } // namespace ns3
 
 #endif /* GLOBAL_ROUTE_MANAGER_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-route-manager-impl.cc ns-3.47/src/internet/model/global-route-manager-impl.cc
--- ns-3.46/src/internet/model/global-route-manager-impl.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-route-manager-impl.cc	2026-02-16 16:08:49.611230000 -0800
@@ -14,16 +14,22 @@
 
 #include "candidate-queue.h"
 #include "global-router-interface.h"
-#include "ipv4-global-routing.h"
+#include "global-routing.h"
+#include "ipv4-l3-protocol.h"
 #include "ipv4.h"
+#include "ipv6-l3-protocol.h"
+#include "ipv6-route.h"
 
 #include "ns3/assert.h"
+#include "ns3/boolean.h"
+#include "ns3/config.h"
 #include "ns3/fatal-error.h"
 #include "ns3/log.h"
 #include "ns3/node-list.h"
 #include "ns3/simulator.h"
 
 #include <algorithm>
+#include <iomanip>
 #include <iostream>
 #include <queue>
 #include <utility>
@@ -38,33 +44,27 @@
  * @brief Stream insertion operator.
  *
  * @param os the reference to the output stream
- * @param exit the exit node
+ * @param exit the exit gateway and egress interface index
  * @returns the reference to the output stream
  */
 std::ostream&
-operator<<(std::ostream& os, const SPFVertex::NodeExit_t& exit)
+operator<<(std::ostream& os, const std::pair<Ipv4Address, int>& exit)
 {
-    os << "(" << exit.first << " ," << exit.second << ")";
+    os << "(" << exit.first << ", " << exit.second << ")";
     return os;
 }
 
+/**
+ * @brief Stream insertion operator.
+ *
+ * @param os the reference to the output stream
+ * @param exit the exit gateway and egress interface index
+ * @returns the reference to the output stream
+ */
 std::ostream&
-operator<<(std::ostream& os, const SPFVertex::ListOfSPFVertex_t& vs)
+operator<<(std::ostream& os, const std::pair<Ipv6Address, int>& exit)
 {
-    os << "{";
-    for (auto iter = vs.begin(); iter != vs.end();)
-    {
-        os << (*iter)->m_vertexId;
-        if (++iter != vs.end())
-        {
-            os << ", ";
-        }
-        else
-        {
-            break;
-        }
-    }
-    os << "}";
+    os << "(" << exit.first << ", " << exit.second << ")";
     return os;
 }
 
@@ -73,14 +73,14 @@
 // SPFVertex Implementation
 //
 // ---------------------------------------------------------------------------
-
-SPFVertex::SPFVertex()
+template <typename T>
+SPFVertex<T>::SPFVertex()
     : m_vertexType(VertexUnknown),
       m_vertexId("255.255.255.255"),
       m_lsa(nullptr),
       m_distanceFromRoot(SPF_INFINITY),
       m_rootOif(SPF_INFINITY),
-      m_nextHop("0.0.0.0"),
+      m_nextHop(IpAddress::GetZero()),
       m_parents(),
       m_children(),
       m_vertexProcessed(false)
@@ -88,32 +88,34 @@
     NS_LOG_FUNCTION(this);
 }
 
-SPFVertex::SPFVertex(GlobalRoutingLSA* lsa)
+template <typename T>
+SPFVertex<T>::SPFVertex(GlobalRoutingLSA<IpManager>* lsa)
     : m_vertexId(lsa->GetLinkStateId()),
       m_lsa(lsa),
       m_distanceFromRoot(SPF_INFINITY),
       m_rootOif(SPF_INFINITY),
-      m_nextHop("0.0.0.0"),
+      m_nextHop(IpAddress::GetZero()),
       m_parents(),
       m_children(),
       m_vertexProcessed(false)
 {
     NS_LOG_FUNCTION(this << lsa);
 
-    if (lsa->GetLSType() == GlobalRoutingLSA::RouterLSA)
+    if (lsa->GetLSType() == GlobalRoutingLSA<IpManager>::RouterLSA)
     {
         NS_LOG_LOGIC("Setting m_vertexType to VertexRouter");
-        m_vertexType = SPFVertex::VertexRouter;
+        m_vertexType = SPFVertex<T>::VertexRouter;
         m_node = lsa->GetNode();
     }
-    else if (lsa->GetLSType() == GlobalRoutingLSA::NetworkLSA)
+    else if (lsa->GetLSType() == GlobalRoutingLSA<IpManager>::NetworkLSA)
     {
         NS_LOG_LOGIC("Setting m_vertexType to VertexNetwork");
-        m_vertexType = SPFVertex::VertexNetwork;
+        m_vertexType = SPFVertex<T>::VertexNetwork;
     }
 }
 
-SPFVertex::~SPFVertex()
+template <typename T>
+SPFVertex<T>::~SPFVertex()
 {
     NS_LOG_FUNCTION(this);
 
@@ -145,7 +147,7 @@
         //
         // Note that m_children.pop_front () is not necessary as this
         // p is removed from the children list when p is deleted
-        SPFVertex* p = m_children.front();
+        SPFVertex<T>* p = m_children.front();
         // 'p' == 0, this child is already deleted by its other parent
         if (p == nullptr)
         {
@@ -165,64 +167,73 @@
     NS_LOG_LOGIC("Vertex-" << m_vertexId << " completed deleted");
 }
 
+template <typename T>
 void
-SPFVertex::SetVertexType(SPFVertex::VertexType type)
+SPFVertex<T>::SetVertexType(SPFVertex<T>::VertexType type)
 {
     NS_LOG_FUNCTION(this << type);
     m_vertexType = type;
 }
 
-SPFVertex::VertexType
-SPFVertex::GetVertexType() const
+template <typename T>
+SPFVertex<T>::VertexType
+SPFVertex<T>::GetVertexType() const
 {
     NS_LOG_FUNCTION(this);
     return m_vertexType;
 }
 
+template <typename T>
 void
-SPFVertex::SetVertexId(Ipv4Address id)
+SPFVertex<T>::SetVertexId(IpAddress id)
 {
     NS_LOG_FUNCTION(this << id);
     m_vertexId = id;
 }
 
-Ipv4Address
-SPFVertex::GetVertexId() const
+template <typename T>
+SPFVertex<T>::IpAddress
+SPFVertex<T>::GetVertexId() const
 {
     NS_LOG_FUNCTION(this);
     return m_vertexId;
 }
 
+template <typename T>
 void
-SPFVertex::SetLSA(GlobalRoutingLSA* lsa)
+SPFVertex<T>::SetLSA(GlobalRoutingLSA<IpManager>* lsa)
 {
     NS_LOG_FUNCTION(this << lsa);
     m_lsa = lsa;
 }
 
-GlobalRoutingLSA*
-SPFVertex::GetLSA() const
+template <typename T>
+GlobalRoutingLSA<T>*
+SPFVertex<T>::GetLSA() const
 {
     NS_LOG_FUNCTION(this);
     return m_lsa;
 }
 
+template <typename T>
 void
-SPFVertex::SetDistanceFromRoot(uint32_t distance)
+SPFVertex<T>::SetDistanceFromRoot(uint32_t distance)
 {
     NS_LOG_FUNCTION(this << distance);
     m_distanceFromRoot = distance;
 }
 
+template <typename T>
 uint32_t
-SPFVertex::GetDistanceFromRoot() const
+SPFVertex<T>::GetDistanceFromRoot() const
 {
     NS_LOG_FUNCTION(this);
     return m_distanceFromRoot;
 }
 
+template <typename T>
 void
-SPFVertex::SetParent(SPFVertex* parent)
+SPFVertex<T>::SetParent(SPFVertex* parent)
 {
     NS_LOG_FUNCTION(this << parent);
 
@@ -231,8 +242,9 @@
     m_parents.push_back(parent);
 }
 
-SPFVertex*
-SPFVertex::GetParent(uint32_t i) const
+template <typename T>
+SPFVertex<T>*
+SPFVertex<T>::GetParent(uint32_t i) const
 {
     NS_LOG_FUNCTION(this << i);
 
@@ -250,8 +262,9 @@
     return *iter;
 }
 
+template <typename T>
 void
-SPFVertex::MergeParent(const SPFVertex* v)
+SPFVertex<T>::MergeParent(const SPFVertex* v)
 {
     NS_LOG_FUNCTION(this << v);
 
@@ -264,8 +277,9 @@
     NS_LOG_LOGIC("After merge, list of parents = " << m_parents);
 }
 
+template <typename T>
 void
-SPFVertex::SetRootExitDirection(Ipv4Address nextHop, int32_t id)
+SPFVertex<T>::SetRootExitDirection(IpAddress nextHop, int32_t id)
 {
     NS_LOG_FUNCTION(this << nextHop << id);
 
@@ -278,15 +292,17 @@
     m_rootOif = id;
 }
 
+template <typename T>
 void
-SPFVertex::SetRootExitDirection(SPFVertex::NodeExit_t exit)
+SPFVertex<T>::SetRootExitDirection(SPFVertex<T>::NodeExit_t exit)
 {
     NS_LOG_FUNCTION(this << exit);
     SetRootExitDirection(exit.first, exit.second);
 }
 
-SPFVertex::NodeExit_t
-SPFVertex::GetRootExitDirection(uint32_t i) const
+template <typename T>
+SPFVertex<T>::NodeExit_t
+SPFVertex<T>::GetRootExitDirection(uint32_t i) const
 {
     NS_LOG_FUNCTION(this << i);
 
@@ -301,8 +317,9 @@
     return *iter;
 }
 
-SPFVertex::NodeExit_t
-SPFVertex::GetRootExitDirection() const
+template <typename T>
+SPFVertex<T>::NodeExit_t
+SPFVertex<T>::GetRootExitDirection() const
 {
     NS_LOG_FUNCTION(this);
 
@@ -311,8 +328,9 @@
     return GetRootExitDirection(0);
 }
 
+template <typename T>
 void
-SPFVertex::MergeRootExitDirections(const SPFVertex* vertex)
+SPFVertex<T>::MergeRootExitDirections(const SPFVertex* vertex)
 {
     NS_LOG_FUNCTION(this << vertex);
 
@@ -325,8 +343,9 @@
     m_ecmpRootExits.unique();
 }
 
+template <typename T>
 void
-SPFVertex::InheritAllRootExitDirections(const SPFVertex* vertex)
+SPFVertex<T>::InheritAllRootExitDirections(const SPFVertex* vertex)
 {
     NS_LOG_FUNCTION(this << vertex);
 
@@ -342,22 +361,25 @@
                            vertex->m_ecmpRootExits.end());
 }
 
+template <typename T>
 uint32_t
-SPFVertex::GetNRootExitDirections() const
+SPFVertex<T>::GetNRootExitDirections() const
 {
     NS_LOG_FUNCTION(this);
     return m_ecmpRootExits.size();
 }
 
+template <typename T>
 uint32_t
-SPFVertex::GetNChildren() const
+SPFVertex<T>::GetNChildren() const
 {
     NS_LOG_FUNCTION(this);
     return m_children.size();
 }
 
-SPFVertex*
-SPFVertex::GetChild(uint32_t n) const
+template <typename T>
+SPFVertex<T>*
+SPFVertex<T>::GetChild(uint32_t n) const
 {
     NS_LOG_FUNCTION(this << n);
     uint32_t j = 0;
@@ -373,30 +395,34 @@
     return nullptr;
 }
 
+template <typename T>
 uint32_t
-SPFVertex::AddChild(SPFVertex* child)
+SPFVertex<T>::AddChild(SPFVertex* child)
 {
     NS_LOG_FUNCTION(this << child);
     m_children.push_back(child);
     return m_children.size();
 }
 
+template <typename T>
 void
-SPFVertex::SetVertexProcessed(bool value)
+SPFVertex<T>::SetVertexProcessed(bool value)
 {
     NS_LOG_FUNCTION(this << value);
     m_vertexProcessed = value;
 }
 
+template <typename T>
 bool
-SPFVertex::IsVertexProcessed() const
+SPFVertex<T>::IsVertexProcessed() const
 {
     NS_LOG_FUNCTION(this);
     return m_vertexProcessed;
 }
 
+template <typename T>
 void
-SPFVertex::ClearVertexProcessed()
+SPFVertex<T>::ClearVertexProcessed()
 {
     NS_LOG_FUNCTION(this);
     for (uint32_t i = 0; i < this->GetNChildren(); i++)
@@ -406,8 +432,9 @@
     this->SetVertexProcessed(false);
 }
 
+template <typename T>
 Ptr<Node>
-SPFVertex::GetNode() const
+SPFVertex<T>::GetNode() const
 {
     return m_node;
 }
@@ -417,49 +444,52 @@
 // GlobalRouteManagerLSDB Implementation
 //
 // ---------------------------------------------------------------------------
-
-GlobalRouteManagerLSDB::GlobalRouteManagerLSDB()
+template <typename T>
+GlobalRouteManagerLSDB<T>::GlobalRouteManagerLSDB()
     : m_database(),
       m_extdatabase()
 {
     NS_LOG_FUNCTION(this);
 }
 
-GlobalRouteManagerLSDB::~GlobalRouteManagerLSDB()
+template <typename T>
+GlobalRouteManagerLSDB<T>::~GlobalRouteManagerLSDB()
 {
     NS_LOG_FUNCTION(this);
     for (auto i = m_database.begin(); i != m_database.end(); i++)
     {
         NS_LOG_LOGIC("free LSA");
-        GlobalRoutingLSA* temp = i->second;
+        GlobalRoutingLSA<IpManager>* temp = i->second;
         delete temp;
     }
     for (uint32_t j = 0; j < m_extdatabase.size(); j++)
     {
         NS_LOG_LOGIC("free ASexternalLSA");
-        GlobalRoutingLSA* temp = m_extdatabase.at(j);
+        GlobalRoutingLSA<IpManager>* temp = m_extdatabase.at(j);
         delete temp;
     }
     NS_LOG_LOGIC("clear map");
     m_database.clear();
 }
 
+template <typename T>
 void
-GlobalRouteManagerLSDB::Initialize()
+GlobalRouteManagerLSDB<T>::Initialize()
 {
     NS_LOG_FUNCTION(this);
     for (auto i = m_database.begin(); i != m_database.end(); i++)
     {
-        GlobalRoutingLSA* temp = i->second;
-        temp->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+        GlobalRoutingLSA<IpManager>* temp = i->second;
+        temp->SetStatus(GlobalRoutingLSA<IpManager>::LSA_SPF_NOT_EXPLORED);
     }
 }
 
+template <typename T>
 void
-GlobalRouteManagerLSDB::Insert(Ipv4Address addr, GlobalRoutingLSA* lsa)
+GlobalRouteManagerLSDB<T>::Insert(IpAddress addr, GlobalRoutingLSA<IpManager>* lsa)
 {
     NS_LOG_FUNCTION(this << addr << lsa);
-    if (lsa->GetLSType() == GlobalRoutingLSA::ASExternalLSAs)
+    if (lsa->GetLSType() == GlobalRoutingLSA<IpManager>::ASExternalLSAs)
     {
         m_extdatabase.push_back(lsa);
     }
@@ -469,22 +499,25 @@
     }
 }
 
-GlobalRoutingLSA*
-GlobalRouteManagerLSDB::GetExtLSA(uint32_t index) const
+template <typename T>
+GlobalRoutingLSA<typename GlobalRouteManagerLSDB<T>::IpManager>*
+GlobalRouteManagerLSDB<T>::GetExtLSA(uint32_t index) const
 {
     NS_LOG_FUNCTION(this << index);
     return m_extdatabase.at(index);
 }
 
+template <typename T>
 uint32_t
-GlobalRouteManagerLSDB::GetNumExtLSAs() const
+GlobalRouteManagerLSDB<T>::GetNumExtLSAs() const
 {
     NS_LOG_FUNCTION(this);
     return m_extdatabase.size();
 }
 
-GlobalRoutingLSA*
-GlobalRouteManagerLSDB::GetLSA(Ipv4Address addr) const
+template <typename T>
+GlobalRoutingLSA<typename GlobalRouteManagerLSDB<T>::IpManager>*
+GlobalRouteManagerLSDB<T>::GetLSA(IpAddress addr) const
 {
     NS_LOG_FUNCTION(this << addr);
     //
@@ -500,8 +533,9 @@
     return nullptr;
 }
 
-GlobalRoutingLSA*
-GlobalRouteManagerLSDB::GetLSAByLinkData(Ipv4Address addr) const
+template <typename T>
+GlobalRoutingLSA<typename GlobalRouteManagerLSDB<T>::IpManager>*
+GlobalRouteManagerLSDB<T>::GetLSAByLinkData(IpAddress addr) const
 {
     NS_LOG_FUNCTION(this << addr);
     //
@@ -509,12 +543,12 @@
     //
     for (auto i = m_database.begin(); i != m_database.end(); i++)
     {
-        GlobalRoutingLSA* temp = i->second;
+        GlobalRoutingLSA<IpManager>* temp = i->second;
         // Iterate among temp's Link Records
         for (uint32_t j = 0; j < temp->GetNLinkRecords(); j++)
         {
-            GlobalRoutingLinkRecord* lr = temp->GetLinkRecord(j);
-            if (lr->GetLinkType() == GlobalRoutingLinkRecord::TransitNetwork &&
+            GlobalRoutingLinkRecord<IpManager>* lr = temp->GetLinkRecord(j);
+            if (lr->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::TransitNetwork &&
                 lr->GetLinkData() == addr)
             {
                 return temp;
@@ -530,14 +564,16 @@
 //
 // ---------------------------------------------------------------------------
 
-GlobalRouteManagerImpl::GlobalRouteManagerImpl()
+template <typename T>
+GlobalRouteManagerImpl<T>::GlobalRouteManagerImpl()
     : m_spfroot(nullptr)
 {
     NS_LOG_FUNCTION(this);
-    m_lsdb = new GlobalRouteManagerLSDB();
+    m_lsdb = new GlobalRouteManagerLSDB<IpManager>();
 }
 
-GlobalRouteManagerImpl::~GlobalRouteManagerImpl()
+template <typename T>
+GlobalRouteManagerImpl<T>::~GlobalRouteManagerImpl()
 {
     NS_LOG_FUNCTION(this);
     if (m_lsdb)
@@ -546,8 +582,9 @@
     }
 }
 
+template <typename T>
 void
-GlobalRouteManagerImpl::DebugUseLsdb(GlobalRouteManagerLSDB* lsdb)
+GlobalRouteManagerImpl<T>::DebugUseLsdb(GlobalRouteManagerLSDB<T>* lsdb)
 {
     NS_LOG_FUNCTION(this << lsdb);
     if (m_lsdb)
@@ -557,19 +594,20 @@
     m_lsdb = lsdb;
 }
 
+template <typename T>
 void
-GlobalRouteManagerImpl::DeleteGlobalRoutes()
+GlobalRouteManagerImpl<T>::DeleteGlobalRoutes()
 {
     NS_LOG_FUNCTION(this);
     for (auto i = NodeList::Begin(); i != NodeList::End(); i++)
     {
         Ptr<Node> node = *i;
-        Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
+        Ptr<GlobalRouter<IpManager>> router = node->GetObject<GlobalRouter<IpManager>>();
         if (!router)
         {
             continue;
         }
-        Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+        Ptr<GlobalRouting<IpRoutingProtocol>> gr = router->GetRoutingProtocol();
         uint32_t j = 0;
         uint32_t nRoutes = gr->GetNRoutes();
         NS_LOG_LOGIC("Deleting " << gr->GetNRoutes() << " routes from node " << node->GetId());
@@ -587,7 +625,7 @@
     {
         NS_LOG_LOGIC("Deleting LSDB, creating new one");
         delete m_lsdb;
-        m_lsdb = new GlobalRouteManagerLSDB();
+        m_lsdb = new GlobalRouteManagerLSDB<IpManager>();
     }
 }
 
@@ -600,8 +638,10 @@
 // add them to the Link State DataBase (LSDB) from which the routes will
 // ultimately be computed.
 //
+
+template <typename T>
 void
-GlobalRouteManagerImpl::BuildGlobalRoutingDatabase()
+GlobalRouteManagerImpl<T>::BuildGlobalRoutingDatabase()
 {
     NS_LOG_FUNCTION(this);
     //
@@ -612,7 +652,7 @@
     {
         Ptr<Node> node = *i;
 
-        Ptr<GlobalRouter> rtr = node->GetObject<GlobalRouter>();
+        Ptr<GlobalRouter<IpManager>> rtr = node->GetObject<GlobalRouter<IpManager>>();
         //
         // Ignore nodes that aren't participating in routing.
         //
@@ -628,13 +668,13 @@
         // DiscoverLSAs () will get zero as the number since no routes have been
         // found.
         //
-        Ptr<Ipv4GlobalRouting> grouting = rtr->GetRoutingProtocol();
+        Ptr<GlobalRouting<IpRoutingProtocol>> grouting = rtr->GetRoutingProtocol();
         uint32_t numLSAs = rtr->DiscoverLSAs();
         NS_LOG_LOGIC("Found " << numLSAs << " LSAs");
 
         for (uint32_t j = 0; j < numLSAs; ++j)
         {
-            auto lsa = new GlobalRoutingLSA();
+            auto lsa = new GlobalRoutingLSA<IpManager>();
             //
             // This is the call to actually fetch a Link State Advertisement from the
             // router.
@@ -682,8 +722,10 @@
 // algorithm then iterates again.  It terminates when the candidate
 // list becomes empty.
 //
+
+template <typename T>
 void
-GlobalRouteManagerImpl::InitializeRoutes()
+GlobalRouteManagerImpl<T>::InitializeRoutes()
 {
     NS_LOG_FUNCTION(this);
     //
@@ -697,7 +739,7 @@
         // Look for the GlobalRouter interface that indicates that the node is
         // participating in routing.
         //
-        Ptr<GlobalRouter> rtr = node->GetObject<GlobalRouter>();
+        Ptr<GlobalRouter<IpManager>> rtr = node->GetObject<GlobalRouter<IpManager>>();
 
         uint32_t systemId = Simulator::GetSystemId();
         // Ignore nodes that are not assigned to our systemId (distributed sim)
@@ -731,25 +773,27 @@
 // vertices not already on the list.  If a lower-cost path is found to a
 // vertex already on the candidate list, store the new (lower) cost.
 //
+
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFNext(SPFVertex* v, CandidateQueue& candidate)
+GlobalRouteManagerImpl<T>::SPFNext(SPFVertex<T>* v, CandidateQueue<T>& candidate)
 {
     NS_LOG_FUNCTION(this << v << &candidate);
 
-    SPFVertex* w = nullptr;
-    GlobalRoutingLSA* w_lsa = nullptr;
-    GlobalRoutingLinkRecord* l = nullptr;
+    SPFVertex<T>* w = nullptr;
+    GlobalRoutingLSA<IpManager>* w_lsa = nullptr;
+    GlobalRoutingLinkRecord<IpManager>* l = nullptr;
     uint32_t distance = 0;
     uint32_t numRecordsInVertex = 0;
     //
     // V points to a Router-LSA or Network-LSA
     // Loop over the links in router LSA or attached routers in Network LSA
     //
-    if (v->GetVertexType() == SPFVertex::VertexRouter)
+    if (v->GetVertexType() == SPFVertex<T>::VertexRouter)
     {
         numRecordsInVertex = v->GetLSA()->GetNLinkRecords();
     }
-    if (v->GetVertexType() == SPFVertex::VertexNetwork)
+    if (v->GetVertexType() == SPFVertex<T>::VertexNetwork)
     {
         numRecordsInVertex = v->GetLSA()->GetNAttachedRouters();
     }
@@ -758,7 +802,7 @@
     for (uint32_t i = 0; i < numRecordsInVertex; i++)
     {
         // Get w_lsa:  In case of V is Router-LSA
-        if (v->GetVertexType() == SPFVertex::VertexRouter)
+        if (v->GetVertexType() == SPFVertex<T>::VertexRouter)
         {
             NS_LOG_LOGIC("Examining link " << i << " of " << v->GetVertexId() << "'s "
                                            << v->GetLSA()->GetNLinkRecords() << " link records");
@@ -769,7 +813,7 @@
             //
             l = v->GetLSA()->GetLinkRecord(i);
             NS_ASSERT(l != nullptr);
-            if (l->GetLinkType() == GlobalRoutingLinkRecord::StubNetwork)
+            if (l->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::StubNetwork)
             {
                 NS_LOG_LOGIC("Found a Stub record to " << l->GetLinkId());
                 continue;
@@ -779,7 +823,7 @@
             // the vertex W's LSA (router-LSA or network-LSA) in Area A's link state
             // database.
             //
-            if (l->GetLinkType() == GlobalRoutingLinkRecord::PointToPoint)
+            if (l->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::PointToPoint)
             {
                 //
                 // Lookup the link state advertisement of the new link -- we call it <w> in
@@ -790,7 +834,7 @@
                 NS_LOG_LOGIC("Found a P2P record from " << v->GetVertexId() << " to "
                                                         << w_lsa->GetLinkStateId());
             }
-            else if (l->GetLinkType() == GlobalRoutingLinkRecord::TransitNetwork)
+            else if (l->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::TransitNetwork)
             {
                 w_lsa = m_lsdb->GetLSA(l->GetLinkId());
                 NS_ASSERT(w_lsa);
@@ -803,7 +847,7 @@
             }
         }
         // Get w_lsa:  In case of V is Network-LSA
-        if (v->GetVertexType() == SPFVertex::VertexNetwork)
+        if (v->GetVertexType() == SPFVertex<T>::VertexNetwork)
         {
             w_lsa = m_lsdb->GetLSAByLinkData(v->GetLSA()->GetAttachedRouter(i));
             if (!w_lsa)
@@ -822,7 +866,7 @@
         // If the link is to a router that is already in the shortest path first tree
         // then we have it covered -- ignore it.
         //
-        if (w_lsa->GetStatus() == GlobalRoutingLSA::LSA_SPF_IN_SPFTREE)
+        if (w_lsa->GetStatus() == GlobalRoutingLSA<IpManager>::LSA_SPF_IN_SPFTREE)
         {
             NS_LOG_LOGIC("Skipping ->  LSA " << w_lsa->GetLinkStateId() << " already in SPF tree");
             continue;
@@ -833,7 +877,7 @@
         // calculated) shortest path to vertex V and the advertised cost of the link
         // between vertices V and W.
         //
-        if (v->GetLSA()->GetLSType() == GlobalRoutingLSA::RouterLSA)
+        if (v->GetLSA()->GetLSType() == GlobalRoutingLSA<IpManager>::RouterLSA)
         {
             NS_ASSERT(l != nullptr);
             distance = v->GetDistanceFromRoot() + l->GetMetric();
@@ -846,7 +890,7 @@
         NS_LOG_LOGIC("Considering w_lsa " << w_lsa->GetLinkStateId());
 
         // Is there already vertex w in candidate list?
-        if (w_lsa->GetStatus() == GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED)
+        if (w_lsa->GetStatus() == GlobalRoutingLSA<IpManager>::LSA_SPF_NOT_EXPLORED)
         {
             // Calculate nexthop to w
             // We need to figure out how to actually get to the new router represented
@@ -855,10 +899,10 @@
             // used to forward the packets.
 
             // prepare vertex w
-            w = new SPFVertex(w_lsa);
+            w = new SPFVertex<T>(w_lsa);
             if (SPFNexthopCalculation(v, w, l, distance))
             {
-                w_lsa->SetStatus(GlobalRoutingLSA::LSA_SPF_CANDIDATE);
+                w_lsa->SetStatus(GlobalRoutingLSA<IpManager>::LSA_SPF_CANDIDATE);
                 //
                 // Push this new vertex onto the priority queue (ordered by distance from the
                 // root node).
@@ -873,7 +917,7 @@
                 NS_ASSERT_MSG(0, "SPFNexthopCalculation never return false, but it does now!");
             }
         }
-        else if (w_lsa->GetStatus() == GlobalRoutingLSA::LSA_SPF_CANDIDATE)
+        else if (w_lsa->GetStatus() == GlobalRoutingLSA<IpManager>::LSA_SPF_CANDIDATE)
         {
             //
             // We have already considered the link represented by <w>.  What wse have to
@@ -888,7 +932,7 @@
              * with the cost we just determined (w->distance) to see
              * if we've found a shorter path.
              */
-            SPFVertex* cw;
+            SPFVertex<T>* cw;
             cw = candidate.Find(w_lsa->GetLinkStateId());
             if (cw->GetDistanceFromRoot() < distance)
             {
@@ -919,7 +963,7 @@
                 // is very different from quagga (blame ns3::GlobalRouteManagerImpl)
 
                 // prepare vertex w
-                w = new SPFVertex(w_lsa);
+                w = new SPFVertex<T>(w_lsa);
                 SPFNexthopCalculation(v, w, l, distance);
                 cw->MergeRootExitDirections(w);
                 cw->MergeParent(w);
@@ -962,11 +1006,13 @@
 //
 // For now, this is greatly simplified from the quagga code
 //
+
+template <typename T>
 int
-GlobalRouteManagerImpl::SPFNexthopCalculation(SPFVertex* v,
-                                              SPFVertex* w,
-                                              GlobalRoutingLinkRecord* l,
-                                              uint32_t distance)
+GlobalRouteManagerImpl<T>::SPFNexthopCalculation(SPFVertex<T>* v,
+                                                 SPFVertex<T>* w,
+                                                 GlobalRoutingLinkRecord<IpManager>* l,
+                                                 uint32_t distance)
 {
     NS_LOG_FUNCTION(this << v << w << l << distance);
     //
@@ -1011,7 +1057,7 @@
         // node if this root node is a router.  We then need to see if this node <w>
         // is a router.
         //
-        if (w->GetVertexType() == SPFVertex::VertexRouter)
+        if (w->GetVertexType() == SPFVertex<T>::VertexRouter)
         {
             //
             // In the case of point-to-point links, the link data field (m_linkData) of a
@@ -1027,7 +1073,7 @@
             // SPFGetLink.
             //
             NS_ASSERT(l);
-            GlobalRoutingLinkRecord* linkRemote = nullptr;
+            GlobalRoutingLinkRecord<IpManager>* linkRemote = nullptr;
             linkRemote = SPFGetNextLink(w, v, linkRemote);
             //
             // At this point, <l> is the Global Router Link Record describing the point-
@@ -1040,13 +1086,29 @@
             // from the root node to the host represented by vertex <w>, you have to send
             // the packet to the next hop address specified in w->m_nextHop.
             //
-            Ipv4Address nextHop = linkRemote->GetLinkData();
+            IpAddress nextHop;
+            if constexpr (IsIpv4)
+            {
+                nextHop = linkRemote->GetLinkData();
+            }
+            else
+            {
+                nextHop = linkRemote->GetLinkLocData();
+            }
             //
             // Now find the outgoing interface corresponding to the point to point link
             // from the perspective of <v> -- remember that <l> is the link "from"
             // <v> "to" <w>.
             //
-            uint32_t outIf = FindOutgoingInterfaceId(l->GetLinkData());
+            uint32_t outIf;
+            if constexpr (IsIpv4)
+            {
+                outIf = FindOutgoingInterfaceId(l->GetLinkData());
+            }
+            else
+            {
+                outIf = FindOutgoingInterfaceId(l->GetLinkLocData());
+            }
 
             w->SetRootExitDirection(nextHop, outIf);
             w->SetDistanceFromRoot(distance);
@@ -1058,15 +1120,15 @@
         }
         else
         {
-            NS_ASSERT(w->GetVertexType() == SPFVertex::VertexNetwork);
+            NS_ASSERT(w->GetVertexType() == SPFVertex<T>::VertexNetwork);
             // W is a directly connected network; no next hop is required
-            GlobalRoutingLSA* w_lsa = w->GetLSA();
-            NS_ASSERT(w_lsa->GetLSType() == GlobalRoutingLSA::NetworkLSA);
+            GlobalRoutingLSA<IpManager>* w_lsa = w->GetLSA();
+            NS_ASSERT(w_lsa->GetLSType() == GlobalRoutingLSA<IpManager>::NetworkLSA);
             // Find outgoing interface ID for this network
             uint32_t outIf =
                 FindOutgoingInterfaceId(w_lsa->GetLinkStateId(), w_lsa->GetNetworkLSANetworkMask());
             // Set the next hop to 0.0.0.0 meaning "not exist"
-            Ipv4Address nextHop = Ipv4Address::GetZero();
+            IpAddress nextHop = IpAddress::GetZero();
             w->SetRootExitDirection(nextHop, outIf);
             w->SetDistanceFromRoot(distance);
             w->SetParent(v);
@@ -1076,7 +1138,7 @@
             return 1;
         }
     }
-    else if (v->GetVertexType() == SPFVertex::VertexNetwork)
+    else if (v->GetVertexType() == SPFVertex<T>::VertexNetwork)
     {
         // See if any of v's parents are the root
         if (v->GetParent() == m_spfroot)
@@ -1085,8 +1147,8 @@
             // directly connects the calculating router to the destination
             // router.  The list of next hops is then determined by
             // examining the destination's router-LSA...
-            NS_ASSERT(w->GetVertexType() == SPFVertex::VertexRouter);
-            GlobalRoutingLinkRecord* linkRemote = nullptr;
+            NS_ASSERT(w->GetVertexType() == SPFVertex<T>::VertexRouter);
+            GlobalRoutingLinkRecord<IpManager>* linkRemote = nullptr;
             while ((linkRemote = SPFGetNextLink(w, v, linkRemote)))
             {
                 /* ...For each link in the router-LSA that points back to the
@@ -1095,7 +1157,15 @@
                  * use can then be derived from the next hop IP address (or
                  * it can be inherited from the parent network).
                  */
-                Ipv4Address nextHop = linkRemote->GetLinkData();
+                IpAddress nextHop;
+                if constexpr (IsIpv4)
+                {
+                    nextHop = linkRemote->GetLinkData();
+                }
+                else
+                {
+                    nextHop = linkRemote->GetLinkLocData();
+                }
                 uint32_t outIf = v->GetRootExitDirection().second;
                 w->SetRootExitDirection(nextHop, outIf);
                 NS_LOG_LOGIC("Next hop from " << v->GetVertexId() << " to " << w->GetVertexId()
@@ -1146,16 +1216,19 @@
 // to <w>.  If prev_link is not NULL, we return a Global Router Link Record
 // representing a possible *second* link from <v> to <w>.
 //
-GlobalRoutingLinkRecord*
-GlobalRouteManagerImpl::SPFGetNextLink(SPFVertex* v,
-                                       SPFVertex* w,
-                                       GlobalRoutingLinkRecord* prev_link)
+
+template <typename T>
+
+GlobalRoutingLinkRecord<typename GlobalRouteManagerImpl<T>::IpManager>*
+GlobalRouteManagerImpl<T>::SPFGetNextLink(SPFVertex<T>* v,
+                                          SPFVertex<T>* w,
+                                          GlobalRoutingLinkRecord<IpManager>* prev_link)
 {
     NS_LOG_FUNCTION(this << v << w << prev_link);
 
     bool skip = true;
     bool found_prev_link = false;
-    GlobalRoutingLinkRecord* l;
+    GlobalRoutingLinkRecord<IpManager>* l;
     //
     // If prev_link is 0, we are really looking for the first link, not the next
     // link.
@@ -1223,8 +1296,10 @@
 //
 // Used for unit tests.
 //
+
+template <typename T>
 void
-GlobalRouteManagerImpl::DebugSPFCalculate(Ipv4Address root)
+GlobalRouteManagerImpl<T>::DebugSPFCalculate(IpAddress root)
 {
     NS_LOG_FUNCTION(this << root);
     SPFCalculate(root);
@@ -1236,19 +1311,21 @@
 // can safely be added to the next-hop router and SPF does not need
 // to be run
 //
+
+template <typename T>
 bool
-GlobalRouteManagerImpl::CheckForStubNode(Ipv4Address root)
+GlobalRouteManagerImpl<T>::CheckForStubNode(IpAddress root)
 {
     NS_LOG_FUNCTION(this << root);
-    GlobalRoutingLSA* rlsa = m_lsdb->GetLSA(root);
-    Ipv4Address myRouterId = rlsa->GetLinkStateId();
+    GlobalRoutingLSA<IpManager>* rlsa = m_lsdb->GetLSA(root);
+    IpAddress myRouterId = rlsa->GetLinkStateId();
     int transits = 0;
-    GlobalRoutingLinkRecord* transitLink = nullptr;
+    GlobalRoutingLinkRecord<IpManager>* transitLink = nullptr;
     for (uint32_t i = 0; i < rlsa->GetNLinkRecords(); i++)
     {
-        GlobalRoutingLinkRecord* l = rlsa->GetLinkRecord(i);
-        if (l->GetLinkType() == GlobalRoutingLinkRecord::TransitNetwork ||
-            l->GetLinkType() == GlobalRoutingLinkRecord::PointToPoint)
+        GlobalRoutingLinkRecord<IpManager>* l = rlsa->GetLinkRecord(i);
+        if (l->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::TransitNetwork ||
+            l->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::PointToPoint)
         {
             transits++;
             transitLink = l;
@@ -1264,7 +1341,7 @@
     }
     if (transits == 1)
     {
-        if (transitLink->GetLinkType() == GlobalRoutingLinkRecord::TransitNetwork)
+        if (transitLink->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::TransitNetwork)
         {
             // Install default route to next hop router
             // What is the next hop?  We need to check all neighbors on the link.
@@ -1275,20 +1352,20 @@
             NS_LOG_LOGIC("TBD: Would have inserted default for transit");
             return false;
         }
-        else if (transitLink->GetLinkType() == GlobalRoutingLinkRecord::PointToPoint)
+        else if (transitLink->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::PointToPoint)
         {
             // Install default route to next hop
             // The link record LinkID is the router ID of the peer.
             // The Link Data is the local IP interface address
-            GlobalRoutingLSA* w_lsa = m_lsdb->GetLSA(transitLink->GetLinkId());
+            GlobalRoutingLSA<IpManager>* w_lsa = m_lsdb->GetLSA(transitLink->GetLinkId());
             uint32_t nLinkRecords = w_lsa->GetNLinkRecords();
             for (uint32_t j = 0; j < nLinkRecords; ++j)
             {
                 //
                 // We are only concerned about point-to-point links
                 //
-                GlobalRoutingLinkRecord* lr = w_lsa->GetLinkRecord(j);
-                if (lr->GetLinkType() != GlobalRoutingLinkRecord::PointToPoint)
+                GlobalRoutingLinkRecord<IpManager>* lr = w_lsa->GetLinkRecord(j);
+                if (lr->GetLinkType() != GlobalRoutingLinkRecord<IpManager>::PointToPoint)
                 {
                     continue;
                 }
@@ -1296,18 +1373,34 @@
                 if (lr->GetLinkId() == myRouterId)
                 {
                     // Next hop is stored in the LinkID field of lr
-                    Ptr<GlobalRouter> router = rlsa->GetNode()->GetObject<GlobalRouter>();
+                    Ptr<GlobalRouter<IpManager>> router =
+                        rlsa->GetNode()->template GetObject<GlobalRouter<IpManager>>();
                     NS_ASSERT(router);
-                    Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+                    Ptr<GlobalRouting<IpRoutingProtocol>> gr = router->GetRoutingProtocol();
                     NS_ASSERT(gr);
-                    gr->AddNetworkRouteTo(Ipv4Address("0.0.0.0"),
-                                          Ipv4Mask("0.0.0.0"),
-                                          lr->GetLinkData(),
-                                          FindOutgoingInterfaceId(transitLink->GetLinkData()));
-                    NS_LOG_LOGIC("Inserting default route for node "
-                                 << myRouterId << " to next hop " << lr->GetLinkData()
-                                 << " via interface "
-                                 << FindOutgoingInterfaceId(transitLink->GetLinkData()));
+                    if constexpr (IsIpv4)
+                    {
+                        gr->AddNetworkRouteTo(IpAddress::GetZero(),
+                                              IpMaskOrPrefix::GetZero(),
+                                              lr->GetLinkData(),
+                                              FindOutgoingInterfaceId(transitLink->GetLinkData()));
+                        NS_LOG_LOGIC("Inserting default route for node "
+                                     << myRouterId << " to next hop " << lr->GetLinkData()
+                                     << " via interface "
+                                     << FindOutgoingInterfaceId(transitLink->GetLinkData()));
+                    }
+                    else
+                    {
+                        gr->AddNetworkRouteTo(
+                            IpAddress::GetZero(),
+                            IpMaskOrPrefix::GetZero(),
+                            lr->GetLinkLocData(),
+                            FindOutgoingInterfaceId(transitLink->GetLinkLocData()));
+                        NS_LOG_LOGIC("Inserting default route for node "
+                                     << myRouterId << " to next hop " << lr->GetLinkLocData()
+                                     << " via interface "
+                                     << FindOutgoingInterfaceId(transitLink->GetLinkLocData()));
+                    }
                     return true;
                 }
             }
@@ -1317,12 +1410,14 @@
 }
 
 // quagga ospf_spf_calculate
+
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFCalculate(Ipv4Address root)
+GlobalRouteManagerImpl<T>::SPFCalculate(IpAddress root)
 {
     NS_LOG_FUNCTION(this << root);
 
-    SPFVertex* v;
+    SPFVertex<T>* v;
     //
     // Initialize the Link State Database.
     //
@@ -1332,21 +1427,21 @@
     // of the queue being the closest vertex in terms of distance from the root
     // of the tree.  Initially, this queue is empty.
     //
-    CandidateQueue candidate;
+    CandidateQueue<T> candidate;
     NS_ASSERT(candidate.Size() == 0);
     //
     // Initialize the shortest-path tree to only contain the router doing the
     // calculation.  Each router (and corresponding network) is a vertex in the
     // shortest path first (SPF) tree.
     //
-    v = new SPFVertex(m_lsdb->GetLSA(root));
+    v = new SPFVertex<T>(m_lsdb->GetLSA(root));
     //
     // This vertex is the root of the SPF tree and it is distance 0 from the root.
     // We also mark this vertex as being in the SPF tree.
     //
     m_spfroot = v;
     v->SetDistanceFromRoot(0);
-    v->GetLSA()->SetStatus(GlobalRoutingLSA::LSA_SPF_IN_SPFTREE);
+    v->GetLSA()->SetStatus(GlobalRoutingLSA<IpManager>::LSA_SPF_IN_SPFTREE);
     NS_LOG_LOGIC("Starting SPFCalculate for node " << root);
 
     //
@@ -1407,7 +1502,7 @@
         // Update the status field of the vertex to indicate that it is in the SPF
         // tree.
         //
-        v->GetLSA()->SetStatus(GlobalRoutingLSA::LSA_SPF_IN_SPFTREE);
+        v->GetLSA()->SetStatus(GlobalRoutingLSA<IpManager>::LSA_SPF_IN_SPFTREE);
         //
         // The current vertex has a parent pointer.  By calling this rather oddly
         // named method (blame quagga) we add the current vertex to the list of
@@ -1444,11 +1539,11 @@
         // through its point-to-point links, adding a *host* route to the local IP
         // address (at the <v> side) for each of those links.
         //
-        if (v->GetVertexType() == SPFVertex::VertexRouter)
+        if (v->GetVertexType() == SPFVertex<T>::VertexRouter)
         {
             SPFIntraAddRouter(v);
         }
-        else if (v->GetVertexType() == SPFVertex::VertexNetwork)
+        else if (v->GetVertexType() == SPFVertex<T>::VertexNetwork)
         {
             SPFIntraAddTransit(v);
         }
@@ -1468,7 +1563,7 @@
     for (uint32_t i = 0; i < m_lsdb->GetNumExtLSAs(); i++)
     {
         m_spfroot->ClearVertexProcessed();
-        GlobalRoutingLSA* extlsa = m_lsdb->GetExtLSA(i);
+        GlobalRoutingLSA<IpManager>* extlsa = m_lsdb->GetExtLSA(i);
         NS_LOG_LOGIC("Processing External LSA with id " << extlsa->GetLinkStateId());
         ProcessASExternals(m_spfroot, extlsa);
     }
@@ -1482,16 +1577,17 @@
     m_spfroot = nullptr;
 }
 
+template <typename T>
 void
-GlobalRouteManagerImpl::ProcessASExternals(SPFVertex* v, GlobalRoutingLSA* extlsa)
+GlobalRouteManagerImpl<T>::ProcessASExternals(SPFVertex<T>* v, GlobalRoutingLSA<IpManager>* extlsa)
 {
     NS_LOG_FUNCTION(this << v << extlsa);
     NS_LOG_LOGIC("Processing external for destination "
                  << extlsa->GetLinkStateId() << ", for router " << v->GetVertexId()
                  << ", advertised by " << extlsa->GetAdvertisingRouter());
-    if (v->GetVertexType() == SPFVertex::VertexRouter)
+    if (v->GetVertexType() == SPFVertex<T>::VertexRouter)
     {
-        GlobalRoutingLSA* rlsa = v->GetLSA();
+        GlobalRoutingLSA<IpManager>* rlsa = v->GetLSA();
         NS_LOG_LOGIC("Processing router LSA with id " << rlsa->GetLinkStateId());
         if ((rlsa->GetLinkStateId()) == (extlsa->GetAdvertisingRouter()))
         {
@@ -1515,8 +1611,9 @@
 // SPFAddIntraAddStub()
 //
 
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFAddASExternal(GlobalRoutingLSA* extlsa, SPFVertex* v)
+GlobalRouteManagerImpl<T>::SPFAddASExternal(GlobalRoutingLSA<IpManager>* extlsa, SPFVertex<T>* v)
 {
     NS_LOG_FUNCTION(this << extlsa << v);
 
@@ -1532,7 +1629,7 @@
     NS_LOG_LOGIC("External is on remote host: " << extlsa->GetAdvertisingRouter()
                                                 << "; installing");
 
-    Ipv4Address routerId = m_spfroot->GetVertexId();
+    IpAddress routerId = m_spfroot->GetVertexId();
 
     NS_LOG_LOGIC("Vertex ID = " << routerId);
     //
@@ -1551,7 +1648,7 @@
     // to QI for that interface.  If there's no GlobalRouter interface, the node
     // in question cannot be the router we want, so we continue.
     //
-    Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
+    Ptr<GlobalRouter<T>> router = node->GetObject<GlobalRouter<T>>();
     NS_ASSERT_MSG(router, "No GlobalRouter interface on SPF root node " << node->GetId());
     //
     // If the router ID of the current node is equal to the router ID of the
@@ -1566,7 +1663,7 @@
         // for that interface.  If the node is acting as an IP version 4 router, it
         // should absolutely have an Ipv4 interface.
         //
-        Ptr<Ipv4> ipv4 = node->GetObject<Ipv4>();
+        Ptr<Ip> ipv4 = node->GetObject<Ip>();
         NS_ASSERT_MSG(ipv4,
                       "GlobalRouteManagerImpl::SPFIntraAddRouter (): "
                       "QI for <Ipv4> interface failed");
@@ -1579,9 +1676,17 @@
         NS_ASSERT_MSG(v->GetLSA(),
                       "GlobalRouteManagerImpl::SPFIntraAddRouter (): "
                       "Expected valid LSA in SPFVertex* v");
-        Ipv4Mask tempmask = extlsa->GetNetworkLSANetworkMask();
-        Ipv4Address tempip = extlsa->GetLinkStateId();
-        tempip = tempip.CombineMask(tempmask);
+        IpMaskOrPrefix tempmask = extlsa->GetNetworkLSANetworkMask();
+        IpAddress tempip = extlsa->GetLinkStateId();
+
+        if constexpr (IsIpv4)
+        {
+            tempip = tempip.CombineMask(tempmask);
+        }
+        else
+        {
+            tempip = tempip.CombinePrefix(tempmask);
+        }
 
         //
         // Here's why we did all of that work.  We're going to add a host route to the
@@ -1596,18 +1701,18 @@
         // Similarly, the vertex <v> has an m_rootOif (outbound interface index) to
         // which the packets should be send for forwarding.
         //
-        Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
+        Ptr<GlobalRouter<IpManager>> router = node->GetObject<GlobalRouter<IpManager>>();
 
         NS_ASSERT_MSG(router, "No GlobalRouter interface on node " << node->GetId());
 
-        Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+        Ptr<GlobalRouting<IpRoutingProtocol>> gr = router->GetRoutingProtocol();
         NS_ASSERT(gr);
         // walk through all next-hop-IPs and out-going-interfaces for reaching
         // the stub network gateway 'v' from the root node
         for (uint32_t i = 0; i < v->GetNRootExitDirections(); i++)
         {
-            SPFVertex::NodeExit_t exit = v->GetRootExitDirection(i);
-            Ipv4Address nextHop = exit.first;
+            typename SPFVertex<T>::NodeExit_t exit = v->GetRootExitDirection(i);
+            IpAddress nextHop = exit.first;
             int32_t outIf = exit.second;
             if (outIf >= 0)
             {
@@ -1634,21 +1739,23 @@
 // Processing logic from RFC 2328, page 166 and quagga ospf_spf_process_stubs ()
 // stub link records will exist for point-to-point interfaces and for
 // broadcast interfaces for which no neighboring router can be found
+
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFProcessStubs(SPFVertex* v)
+GlobalRouteManagerImpl<T>::SPFProcessStubs(SPFVertex<T>* v)
 {
     NS_LOG_FUNCTION(this << v);
     NS_LOG_LOGIC("Processing stubs for " << v->GetVertexId());
-    if (v->GetVertexType() == SPFVertex::VertexRouter)
+    if (v->GetVertexType() == SPFVertex<T>::VertexRouter)
     {
-        GlobalRoutingLSA* rlsa = v->GetLSA();
+        GlobalRoutingLSA<IpManager>* rlsa = v->GetLSA();
         NS_LOG_LOGIC("Processing router LSA with id " << rlsa->GetLinkStateId());
         for (uint32_t i = 0; i < rlsa->GetNLinkRecords(); i++)
         {
             NS_LOG_LOGIC("Examining link " << i << " of " << v->GetVertexId() << "'s "
                                            << v->GetLSA()->GetNLinkRecords() << " link records");
-            GlobalRoutingLinkRecord* l = v->GetLSA()->GetLinkRecord(i);
-            if (l->GetLinkType() == GlobalRoutingLinkRecord::StubNetwork)
+            GlobalRoutingLinkRecord<IpManager>* l = v->GetLSA()->GetLinkRecord(i);
+            if (l->GetLinkType() == GlobalRoutingLinkRecord<IpManager>::StubNetwork)
             {
                 NS_LOG_LOGIC("Found a Stub record to " << l->GetLinkId());
                 SPFIntraAddStub(l, v);
@@ -1667,8 +1774,10 @@
 }
 
 // RFC2328 16.1. second stage.
+
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFIntraAddStub(GlobalRoutingLinkRecord* l, SPFVertex* v)
+GlobalRouteManagerImpl<T>::SPFIntraAddStub(GlobalRoutingLinkRecord<IpManager>* l, SPFVertex<T>* v)
 {
     NS_LOG_FUNCTION(this << l << v);
 
@@ -1692,7 +1801,7 @@
     // going to use this ID to discover which node it is that we're actually going
     // to update.
     //
-    Ipv4Address routerId = m_spfroot->GetVertexId();
+    IpAddress routerId = m_spfroot->GetVertexId();
 
     NS_LOG_LOGIC("Vertex ID = " << routerId);
     //
@@ -1710,7 +1819,7 @@
     // to QI for that interface.  If there's no GlobalRouter interface, the node
     // in question cannot be the router we want, so we continue.
     //
-    Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
+    Ptr<GlobalRouter<T>> router = node->GetObject<GlobalRouter<T>>();
     NS_ASSERT_MSG(router, "No GlobalRouter interface on node " << node->GetId());
     //
     // If the router ID of the current node is equal to the router ID of the
@@ -1725,8 +1834,8 @@
         // for that interface.  If the node is acting as an IP version 4 router, it
         // should absolutely have an Ipv4 interface.
         //
-        Ptr<Ipv4> ipv4 = node->GetObject<Ipv4>();
-        NS_ASSERT_MSG(ipv4,
+        Ptr<Ip> ip = node->GetObject<Ip>();
+        NS_ASSERT_MSG(ip,
                       "GlobalRouteManagerImpl::SPFIntraAddRouter (): "
                       "QI for <Ipv4> interface failed");
         //
@@ -1738,9 +1847,28 @@
         NS_ASSERT_MSG(v->GetLSA(),
                       "GlobalRouteManagerImpl::SPFIntraAddRouter (): "
                       "Expected valid LSA in SPFVertex* v");
-        Ipv4Mask tempmask(l->GetLinkData().Get());
-        Ipv4Address tempip = l->GetLinkId();
-        tempip = tempip.CombineMask(tempmask);
+        IpMaskOrPrefix tempmask;
+        if constexpr (IsIpv4)
+        {
+            tempmask = Ipv4Mask(l->GetLinkData().Get());
+        }
+        else
+        {
+            // to get the Prefix from the Ipv6Address
+            uint8_t buf[16];
+            l->GetLinkData().GetBytes(buf);
+            tempmask = Ipv6Prefix(buf);
+        }
+
+        IpAddress tempip = l->GetLinkId();
+        if constexpr (IsIpv4)
+        {
+            tempip = tempip.CombineMask(tempmask);
+        }
+        else
+        {
+            tempip = tempip.CombinePrefix(tempmask);
+        }
         //
         // Here's why we did all of that work.  We're going to add a host route to the
         // host address found in the m_linkData field of the point-to-point link
@@ -1755,16 +1883,16 @@
         // which the packets should be send for forwarding.
         //
 
-        Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
+        Ptr<GlobalRouter<T>> router = node->GetObject<GlobalRouter<T>>();
 
-        Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+        Ptr<GlobalRouting<IpRoutingProtocol>> gr = router->GetRoutingProtocol();
         NS_ASSERT(gr);
         // walk through all next-hop-IPs and out-going-interfaces for reaching
         // the stub network gateway 'v' from the root node
         for (uint32_t i = 0; i < v->GetNRootExitDirections(); i++)
         {
-            SPFVertex::NodeExit_t exit = v->GetRootExitDirection(i);
-            Ipv4Address nextHop = exit.first;
+            typename SPFVertex<T>::NodeExit_t exit = v->GetRootExitDirection(i);
+            IpAddress nextHop = exit.first;
             int32_t outIf = exit.second;
             if (outIf >= 0)
             {
@@ -1794,8 +1922,10 @@
 // If no such interface is found, return -1 (note:  unit test framework
 // for routing assumes -1 to be a legal return value)
 //
+
+template <typename T>
 int32_t
-GlobalRouteManagerImpl::FindOutgoingInterfaceId(Ipv4Address a, Ipv4Mask amask)
+GlobalRouteManagerImpl<T>::FindOutgoingInterfaceId(IpAddress a, IpMaskOrPrefix amask)
 {
     NS_LOG_FUNCTION(this << a << amask);
     //
@@ -1806,7 +1936,7 @@
     // node in order to iterate the interfaces and find the one corresponding to
     // the address in question.
     //
-    Ipv4Address routerId = m_spfroot->GetVertexId();
+    IpAddress routerId = m_spfroot->GetVertexId();
     //
     // The node we need to add routes to is the node corresponding to the root vertex.
     // This is the node for which we are building the routing table.
@@ -1821,7 +1951,7 @@
         return -1;
     }
 
-    Ptr<GlobalRouter> rtr = node->GetObject<GlobalRouter>();
+    Ptr<GlobalRouter<IpManager>> rtr = node->GetObject<GlobalRouter<IpManager>>();
     NS_ASSERT_MSG(rtr, "No GlobalRouter interface on node " << node->GetId());
     //
     // If the node doesn't have a GlobalRouter interface it can't be the one
@@ -1836,8 +1966,8 @@
         // is participating in routing IP version 4 packets, it certainly must have
         // an Ipv4 interface.
         //
-        Ptr<Ipv4> ipv4 = node->GetObject<Ipv4>();
-        NS_ASSERT_MSG(ipv4,
+        Ptr<Ip> ip = node->GetObject<Ip>();
+        NS_ASSERT_MSG(ip,
                       "GlobalRouteManagerImpl::FindOutgoingInterfaceId (): "
                       "GetObject for <Ipv4> interface failed");
         //
@@ -1845,7 +1975,7 @@
         // we're looking for.  If we find one, return the corresponding interface
         // index, or -1 if not found.
         //
-        int32_t interface = ipv4->GetInterfaceForPrefix(a, amask);
+        int32_t interface = ip->GetInterfaceForPrefix(a, amask);
 
 #if 0
           if (interface < 0)
@@ -1877,8 +2007,10 @@
 // a destination IP address, reachable from the root, to which we add a host
 // route.
 //
+
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFIntraAddRouter(SPFVertex* v)
+GlobalRouteManagerImpl<T>::SPFIntraAddRouter(SPFVertex<T>* v)
 {
     NS_LOG_FUNCTION(this << v);
 
@@ -1890,7 +2022,7 @@
     // going to use this ID to discover which node it is that we're actually going
     // to update.
     //
-    Ipv4Address routerId = m_spfroot->GetVertexId();
+    IpAddress routerId = m_spfroot->GetVertexId();
 
     NS_LOG_LOGIC("Vertex ID = " << routerId);
     //
@@ -1909,7 +2041,7 @@
     // to GetObject for that interface.  If there's no GlobalRouter interface,
     // the node in question cannot be the router we want, so we continue.
     //
-    Ptr<GlobalRouter> rtr = node->GetObject<GlobalRouter>();
+    Ptr<GlobalRouter<T>> rtr = node->GetObject<GlobalRouter<T>>();
     NS_ASSERT_MSG(rtr, "No GlobalRouter interface on node " << node->GetId());
     //
     // If the router ID of the current node is equal to the router ID of the
@@ -1925,8 +2057,8 @@
         // GetObject for that interface.  If the node is acting as an IP version 4
         // router, it should absolutely have an Ipv4 interface.
         //
-        Ptr<Ipv4> ipv4 = node->GetObject<Ipv4>();
-        NS_ASSERT_MSG(ipv4,
+        Ptr<Ip> ip = node->GetObject<Ip>();
+        NS_ASSERT_MSG(ip,
                       "GlobalRouteManagerImpl::SPFIntraAddRouter (): "
                       "GetObject for <Ipv4> interface failed");
         //
@@ -1935,7 +2067,7 @@
         // Link Records corresponding to links off of that vertex / node.  We're going
         // to be interested in the records corresponding to point-to-point links.
         //
-        GlobalRoutingLSA* lsa = v->GetLSA();
+        GlobalRoutingLSA<IpManager>* lsa = v->GetLSA();
         NS_ASSERT_MSG(lsa,
                       "GlobalRouteManagerImpl::SPFIntraAddRouter (): "
                       "Expected valid LSA in SPFVertex* v");
@@ -1957,8 +2089,8 @@
             //
             // We are only concerned about point-to-point links
             //
-            GlobalRoutingLinkRecord* lr = lsa->GetLinkRecord(j);
-            if (lr->GetLinkType() != GlobalRoutingLinkRecord::PointToPoint)
+            GlobalRoutingLinkRecord<IpManager>* lr = lsa->GetLinkRecord(j);
+            if (lr->GetLinkType() != GlobalRoutingLinkRecord<IpManager>::PointToPoint)
             {
                 continue;
             }
@@ -1975,28 +2107,38 @@
             // Similarly, the vertex <v> has an m_rootOif (outbound interface index) to
             // which the packets should be send for forwarding.
             //
-            Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
+            Ptr<GlobalRouter<IpManager>> router = node->GetObject<GlobalRouter<IpManager>>();
             if (!router)
             {
                 continue;
             }
-            Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+            Ptr<GlobalRouting<IpRoutingProtocol>> gr = router->GetRoutingProtocol();
             NS_ASSERT(gr);
             // walk through all available exit directions due to ECMP,
             // and add host route for each of the exit direction toward
             // the vertex 'v'
             for (uint32_t i = 0; i < v->GetNRootExitDirections(); i++)
             {
-                SPFVertex::NodeExit_t exit = v->GetRootExitDirection(i);
-                Ipv4Address nextHop = exit.first;
+                typename SPFVertex<T>::NodeExit_t exit = v->GetRootExitDirection(i);
+                IpAddress nextHop = exit.first;
                 int32_t outIf = exit.second;
                 if (outIf >= 0)
                 {
-                    gr->AddHostRouteTo(lr->GetLinkData(), nextHop, outIf);
-                    NS_LOG_LOGIC("(Route " << i << ") Node " << node->GetId()
-                                           << " adding host route to " << lr->GetLinkData()
-                                           << " using next hop " << nextHop
-                                           << " and outgoing interface " << outIf);
+                    if (lr->GetLinkData() != IpAddress::GetZero())
+                    {
+                        gr->AddHostRouteTo(lr->GetLinkData(), nextHop, outIf);
+                        NS_LOG_LOGIC("(Route " << i << ") Node " << node->GetId()
+                                               << " adding host route to " << lr->GetLinkData()
+                                               << " using next hop " << nextHop
+                                               << " and outgoing interface " << outIf);
+                    }
+                    else
+                    {
+                        NS_LOG_LOGIC("The Link Data field of the link record is zero, This link "
+                                     "Record is for an interface with no globalUnicast address,  "
+                                     "not adding a hostroute to it");
+                        continue;
+                    }
                 }
                 else
                 {
@@ -2016,8 +2158,9 @@
     NS_FATAL_ERROR("SPFIntraAddRouter(): routerId and vertex ID do not match");
 }
 
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFIntraAddTransit(SPFVertex* v)
+GlobalRouteManagerImpl<T>::SPFIntraAddTransit(SPFVertex<T>* v)
 {
     NS_LOG_FUNCTION(this << v);
 
@@ -2029,7 +2172,7 @@
     // going to use this ID to discover which node it is that we're actually going
     // to update.
     //
-    Ipv4Address routerId = m_spfroot->GetVertexId();
+    IpAddress routerId = m_spfroot->GetVertexId();
 
     NS_LOG_LOGIC("Vertex ID = " << routerId);
     //
@@ -2048,7 +2191,7 @@
     // to GetObject for that interface.  If there's no GlobalRouter interface,
     // the node in question cannot be the router we want, so we continue.
     //
-    Ptr<GlobalRouter> rtr = node->GetObject<GlobalRouter>();
+    Ptr<GlobalRouter<T>> rtr = node->GetObject<GlobalRouter<T>>();
     NS_ASSERT_MSG(rtr, "No GlobalRouter interface on node " << node->GetId());
     //
     // If the router ID of the current node is equal to the router ID of the
@@ -2074,23 +2217,30 @@
         // Link Records corresponding to links off of that vertex / node.  We're going
         // to be interested in the records corresponding to point-to-point links.
         //
-        GlobalRoutingLSA* lsa = v->GetLSA();
+        GlobalRoutingLSA<T>* lsa = v->GetLSA();
         NS_ASSERT_MSG(lsa,
                       "GlobalRouteManagerImpl::SPFIntraAddTransit (): "
                       "Expected valid LSA in SPFVertex* v");
-        Ipv4Mask tempmask = lsa->GetNetworkLSANetworkMask();
-        Ipv4Address tempip = lsa->GetLinkStateId();
-        tempip = tempip.CombineMask(tempmask);
-        Ptr<GlobalRouter> router = node->GetObject<GlobalRouter>();
-        Ptr<Ipv4GlobalRouting> gr = router->GetRoutingProtocol();
+        IpMaskOrPrefix tempmask = lsa->GetNetworkLSANetworkMask();
+        IpAddress tempip = lsa->GetLinkStateId();
+        if constexpr (IsIpv4)
+        {
+            tempip = tempip.CombineMask(tempmask);
+        }
+        else
+        {
+            tempip = tempip.CombinePrefix(tempmask);
+        }
+        Ptr<GlobalRouter<T>> router = node->GetObject<GlobalRouter<T>>();
+        Ptr<GlobalRouting<IpRoutingProtocol>> gr = router->GetRoutingProtocol();
         NS_ASSERT(gr);
         // walk through all available exit directions due to ECMP,
         // and add host route for each of the exit direction toward
         // the vertex 'v'
         for (uint32_t i = 0; i < v->GetNRootExitDirections(); i++)
         {
-            SPFVertex::NodeExit_t exit = v->GetRootExitDirection(i);
-            Ipv4Address nextHop = exit.first;
+            typename SPFVertex<T>::NodeExit_t exit = v->GetRootExitDirection(i);
+            IpAddress nextHop = exit.first;
             int32_t outIf = exit.second;
 
             if (outIf >= 0)
@@ -2126,14 +2276,16 @@
 // Given a pointer to a vertex, it links back to the vertex's parent that it
 // already has set and adds itself to that vertex's list of children.
 //
+
+template <typename T>
 void
-GlobalRouteManagerImpl::SPFVertexAddParent(SPFVertex* v)
+GlobalRouteManagerImpl<T>::SPFVertexAddParent(SPFVertex<T>* v)
 {
     NS_LOG_FUNCTION(this << v);
 
     for (uint32_t i = 0;;)
     {
-        SPFVertex* parent;
+        SPFVertex<T>* parent;
         // check if all parents of vertex v
         if ((parent = v->GetParent(i++)) == nullptr)
         {
@@ -2143,4 +2295,387 @@
     }
 }
 
+template <typename T>
+Ptr<Node>
+GlobalRouteManagerImpl<T>::GetNodeByIp(const IpAddress& address)
+{
+    // iterate through the list of nodes
+    for (auto i = NodeList::Begin(); i != NodeList::End(); ++i)
+    {
+        Ptr<Node> node = *i;
+        Ptr<Ip> ip = node->GetObject<Ip>();
+
+        if (!ip)
+        {
+            continue;
+        }
+
+        int32_t interface = ip->GetInterfaceForAddress(address);
+        if (interface >= 0) // Address found on this node
+        {
+            return node;
+        }
+    }
+    return nullptr; // If no node with the given IP address was found
+}
+
+template <typename T>
+void
+GlobalRouteManagerImpl<T>::PrintRoute(Ptr<Node> sourceNode,
+                                      Ptr<Node> dest,
+                                      Ptr<OutputStreamWrapper> stream,
+                                      bool nodeIdLookup,
+                                      Time::Unit unit)
+{
+    // Get any Ip of destination other than the loopbackIp
+    Ptr<Ip> ip = dest->GetObject<Ip>();
+    NS_ASSERT_MSG(ip, "Ip not found on destination node " << dest->GetId());
+
+    uint32_t numInterfaces = ip->GetNInterfaces();
+
+    for (uint32_t i = 0; i < numInterfaces; i++)
+    {
+        uint32_t numAddresses = ip->GetNAddresses(i);
+        for (uint32_t j = 0; j < numAddresses; j++)
+        {
+            IpInterfaceAddress addr = ip->GetAddress(i, j);
+            IpAddress localAddr;
+            if constexpr (IsIpv4)
+            {
+                localAddr = addr.GetLocal();
+            }
+            else
+            {
+                localAddr = addr.GetAddress();
+            }
+            if (localAddr != IpAddress::GetLoopback())
+            {
+                PrintRoute(sourceNode, localAddr, stream, nodeIdLookup, unit);
+                return;
+            }
+        }
+    }
+    // If no IP address is associated with the destination node, abort the program
+    NS_ABORT_MSG("No IP address associated with destination Node");
+}
+
+template <typename T>
+Ptr<typename GlobalRouteManagerImpl<T>::IpGlobalRouting>
+GlobalRouteManagerImpl<T>::GetGlobalRoutingForNode(Ptr<Node> node)
+{
+    Ptr<Ip> ip = node->GetObject<Ip>();
+    auto globalRouting = DynamicCast<IpGlobalRouting>(ip->GetRoutingProtocol());
+
+    if (globalRouting)
+    {
+        return globalRouting;
+    }
+
+    auto list = DynamicCast<IpListRouting>(ip->GetRoutingProtocol());
+    if (!list)
+    {
+        return nullptr;
+    }
+
+    for (uint32_t i = 0; i < list->GetNRoutingProtocols(); i++)
+    {
+        int16_t priority = 0;
+        globalRouting = DynamicCast<IpGlobalRouting>(list->GetRoutingProtocol(i, priority));
+        if (globalRouting)
+        {
+            return globalRouting;
+        }
+    }
+
+    return nullptr;
+}
+
+template <typename T>
+bool
+GlobalRouteManagerImpl<T>::IsLocalDelivery(Ptr<Ip> ip, IpAddress dest)
+{
+    for (uint32_t i = 0; i < ip->GetNInterfaces(); i++)
+    {
+        for (uint32_t j = 0; j < ip->GetNAddresses(i); j++)
+        {
+            auto addr = ip->GetAddress(i, j);
+            IpAddress localAddr;
+            if constexpr (IsIpv4)
+            {
+                localAddr = addr.GetLocal();
+            }
+            else
+            {
+                localAddr = addr.GetAddress();
+            }
+            if (dest == localAddr)
+            {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+template <typename T>
+bool
+GlobalRouteManagerImpl<T>::ValidateSourceNodeHasIpAddress(Ptr<Ip> ip)
+{
+    uint32_t numInter = ip->GetNInterfaces();
+    if (numInter == 0)
+    {
+        NS_ABORT_MSG("No interfaces associated with source Node");
+        return false;
+    }
+
+    for (uint32_t i = 0; i < numInter; i++)
+    {
+        if (ip->GetNAddresses(i) > 0)
+        {
+            return true;
+        }
+    }
+
+    NS_ABORT_MSG("No IP address associated with source Node");
+    return false;
+}
+
+template <typename T>
+bool
+GlobalRouteManagerImpl<T>::IsOnSameSubnet(Ptr<Ip> ipCurrentNode, IpAddress dest)
+{
+    bool found = false;
+    uint32_t numInterfaces = ipCurrentNode->GetNInterfaces();
+    for (uint32_t i = 0; i < numInterfaces; i++)
+    {
+        uint32_t numAddresses = ipCurrentNode->GetNAddresses(i);
+        for (uint32_t j = 0; j < numAddresses; j++)
+        {
+            IpInterfaceAddress senderAddr = ipCurrentNode->GetAddress(i, j);
+            // Check if the destination is within the same subnet
+            if constexpr (IsIpv4)
+            {
+                IpMaskOrPrefix mask = senderAddr.GetMask();
+                if (mask.IsMatch(dest, senderAddr.GetLocal()))
+                {
+                    // next hop will be the destNode
+                    return true;
+                }
+            }
+            else
+            {
+                IpMaskOrPrefix prefix = senderAddr.GetPrefix();
+                if (prefix.IsMatch(dest, senderAddr.GetAddress()))
+                {
+                    // next hop will be the destNode
+                    return true;
+                }
+            }
+        }
+    }
+    return found;
+}
+
+template <typename T>
+void
+GlobalRouteManagerImpl<T>::PrintRoute(Ptr<Node> sourceNode,
+                                      IpAddress dest,
+                                      Ptr<OutputStreamWrapper> stream,
+                                      bool nodeIdLookup,
+                                      Time::Unit unit)
+{
+    NS_LOG_FUNCTION(this << sourceNode << dest);
+
+    Ptr<Node> destNode = GetNodeByIp(dest);
+    // check that given ip address exists
+    if (!destNode)
+    {
+        NS_ABORT_MSG("Destination node not found for IP address: " << dest);
+        return;
+    }
+
+    NS_ABORT_MSG_IF(!sourceNode, "No Source Node Provided");
+    Ptr<Ip> ip = sourceNode->GetObject<Ip>();
+    // check for ip stack
+    NS_ABORT_MSG_IF(!ip, "No Ip object found on source node " << sourceNode->GetId());
+
+    // check if source has ip address assigned to it
+    if (!ValidateSourceNodeHasIpAddress(ip))
+    {
+        NS_ABORT_MSG("No IP address associated with source Node");
+        return;
+    }
+
+    auto globalRouting = GetGlobalRoutingForNode(sourceNode);
+
+    // Final check: If still nullptr, GlobalRouting wasn't found anywhere
+    if (!globalRouting)
+    {
+        NS_ABORT_MSG("No global routing protocol found on source node " << sourceNode->GetId());
+        return;
+    }
+
+    // Set up the output stream
+    std::ostream* os = stream->GetStream();
+    uint32_t hopsRemaining = 64;
+    uint32_t currHop = 1;
+    // Print the maxHop. This is similar to TraceRoute
+    *os << ", " << hopsRemaining << " hops Max." << std::endl;
+
+    // first check if it is local delivery to one of the nodes on the source node itself
+    if (IsLocalDelivery(ip, dest))
+    {
+        NS_LOG_DEBUG("PrintRoute: Source and Destination are on the same Node "
+                     << sourceNode->GetId());
+        *os << "Source and Destination are on the same Node";
+        *os << std::endl << std::endl;
+        return;
+    }
+
+    // check if routes exist
+    if (!globalRouting->LookupGlobal(dest))
+    {
+        *os << "There is no path from Node " << sourceNode->GetId() << " to Node "
+            << destNode->GetId() << "." << std::endl
+            << std::endl;
+        return;
+    }
+
+    // we start searching for gateways
+    Ptr<Node> currentNode = sourceNode;
+    IpAddress currentNodeIp;
+    std::list<Ptr<Node>> visitedNodes;
+    visitedNodes.push_back(currentNode);
+    while (currentNode != destNode)
+    {
+        if (!hopsRemaining)
+        {
+            NS_LOG_WARN("Max Hop Limit reached");
+            return;
+        }
+
+        Ptr<Ip> ipCurrentNode = currentNode->GetObject<Ip>();
+        auto router = GetGlobalRoutingForNode(currentNode);
+
+        Ptr<IpRoute> gateway = router->LookupGlobal(dest);
+        // check if the gateway exists
+        if (!gateway)
+        {
+            NS_LOG_WARN("No next hop found");
+            return;
+        }
+
+        IpAddress gatewayAddress = gateway->GetGateway();
+
+        // check if the currentNode and the destination belong to the same network. Routing tables
+        // will have a routing table entry with 0.0.0.0 (or ::) as Gateway
+        if (gatewayAddress == IpAddress::GetZero())
+        {
+            // check if gateway is on the same subnet as the destination if it is not and still null
+            // then there is no next jump
+            bool found = IsOnSameSubnet(ipCurrentNode, dest);
+            if (!found)
+            {
+                *os << "Error: Did not find any addresses for  " << dest << " From "
+                    << currentNode->GetId() << std::endl;
+                return;
+            }
+            else
+            {
+                currentNode = destNode;
+                break;
+            }
+        }
+
+        Ptr<Node> nextNode = GetNodeByIp(gatewayAddress);
+
+        if (nextNode == currentNode)
+        {
+            *os << "Invalid route: Next hop points back to the current node (Node "
+                << currentNode->GetId() << ").\n";
+            NS_LOG_WARN("Invalid route: Next hop points back to the current node.");
+            return;
+        }
+
+        // check for loops
+        if (std::find(visitedNodes.begin(), visitedNodes.end(), nextNode) != visitedNodes.end())
+        {
+            *os << "Loop detected! Node " << nextNode->GetId() << " revisited.\n\n";
+            NS_LOG_WARN("Routing Loop detected");
+            return;
+        }
+        visitedNodes.push_back(nextNode);
+
+        Ptr<NetDevice> outdevice = gateway->GetOutputDevice();
+        Ptr<IpL3Protocol> ipL3 = currentNode->GetObject<IpL3Protocol>();
+        uint32_t interfaceIndex = ipCurrentNode->GetInterfaceForDevice(outdevice);
+        currentNodeIp = ipL3->SourceAddressSelection(interfaceIndex, gatewayAddress);
+
+        // print this iteration
+        std::ostringstream addr;
+        std::ostringstream node;
+        if (nextNode != destNode)
+        {
+            node << "(Node " << nextNode->GetId() << ")";
+            addr << gatewayAddress;
+            *os << std::right << std::setw(2) << currHop++ << "  " << addr.str();
+            if (nodeIdLookup)
+            {
+                *os << " " << node.str();
+            }
+            *os << std::endl;
+        }
+        // if the next node is the destination node, then print the Destination IP instead of the
+        // ingress IP of the destination.
+        else
+        {
+            node << "(Node " << destNode->GetId() << ")";
+            addr << dest;
+            *os << std::right << std::setw(2) << currHop++ << "  " << addr.str();
+            if (nodeIdLookup)
+            {
+                *os << " " << node.str();
+            }
+            *os << std::endl;
+        }
+        currentNode = nextNode;
+        currentNodeIp = gatewayAddress;
+        hopsRemaining--;
+    }
+    *os << std::endl;
+}
+
+template <typename T>
+void
+GlobalRouteManagerImpl<T>::InitializeRouters()
+{
+    NS_LOG_FUNCTION_NOARGS();
+    for (auto i = NodeList::Begin(); i != NodeList::End(); i++)
+    {
+        Ptr<Node> node = *i;
+        Ptr<Ip> ip = node->GetObject<Ip>();
+        if (!ip)
+        {
+            continue;
+        }
+        if constexpr (!IsIpv4)
+        {
+            ip->SetAttribute("StrongEndSystemModel", BooleanValue(false));
+        }
+
+        for (uint32_t i = 0; i < ip->GetNInterfaces(); i++)
+        {
+            ip->SetForwarding(i, true);
+        }
+    }
+}
+
+template class SPFVertex<Ipv4Manager>;
+template class GlobalRouteManagerLSDB<Ipv4Manager>;
+template class ns3::GlobalRouteManagerImpl<ns3::Ipv4Manager>;
+template class SPFVertex<Ipv6Manager>;
+template class GlobalRouteManagerLSDB<Ipv6Manager>;
+template class ns3::GlobalRouteManagerImpl<ns3::Ipv6Manager>;
+
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-route-manager-impl.h ns-3.47/src/internet/model/global-route-manager-impl.h
--- ns-3.46/src/internet/model/global-route-manager-impl.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-route-manager-impl.h	2026-02-16 16:08:49.611230000 -0800
@@ -11,15 +11,24 @@
 #define GLOBAL_ROUTE_MANAGER_IMPL_H
 
 #include "global-router-interface.h"
+#include "ipv4-l3-protocol.h"
+#include "ipv4-routing-protocol.h"
+#include "ipv6-l3-protocol.h"
+#include "ipv6-routing-protocol.h"
 
 #include "ns3/ipv4-address.h"
+#include "ns3/ipv4-routing-helper.h"
 #include "ns3/object.h"
 #include "ns3/ptr.h"
 
+#include <algorithm>
+#include <iomanip>
+#include <iostream>
 #include <list>
 #include <map>
 #include <queue>
 #include <stdint.h>
+#include <utility>
 #include <vector>
 
 namespace ns3
@@ -27,8 +36,10 @@
 
 const uint32_t SPF_INFINITY = 0xffffffff; //!< "infinite" distance between nodes
 
+template <typename T>
 class CandidateQueue;
-class Ipv4GlobalRouting;
+template <typename>
+class GlobalRouting;
 
 /**
  * @ingroup globalrouting
@@ -56,8 +67,41 @@
  * records that connect them provide the information required to construct the
  * required routes.
  */
+template <typename T>
 class SPFVertex
 {
+    static_assert(std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+                  "T must be either Ipv4Manager or Ipv6Manager In SPFVertex");
+
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4Manager and Ipv6Manager classes
+    using IpManager = typename std::conditional_t<IsIpv4, Ipv4Manager, Ipv6Manager>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
   public:
     /**
      * @brief Enumeration of the possible types of SPFVertex objects.
@@ -103,7 +147,7 @@
      * @see GlobalRoutingLSA
      * @param lsa The Link State Advertisement used for finding initial values.
      */
-    SPFVertex(GlobalRoutingLSA* lsa);
+    SPFVertex(GlobalRoutingLSA<IpManager>* lsa);
 
     /**
      * @brief Destroy an SPFVertex (Shortest Path First Vertex).
@@ -153,7 +197,7 @@
      *
      * @returns The Ipv4Address Vertex ID of the current SPFVertex object.
      */
-    Ipv4Address GetVertexId() const;
+    IpAddress GetVertexId() const;
 
     /**
      * @brief Set the Vertex ID field of a SPFVertex object.
@@ -169,7 +213,7 @@
      *
      * @param id The new Ipv4Address Vertex ID for the current SPFVertex object.
      */
-    void SetVertexId(Ipv4Address id);
+    void SetVertexId(IpAddress id);
 
     /**
      * @brief Get the Global Router Link State Advertisement returned by the
@@ -182,7 +226,7 @@
      * @returns A pointer to the GlobalRoutingLSA found by the router represented
      * by this SPFVertex object.
      */
-    GlobalRoutingLSA* GetLSA() const;
+    GlobalRoutingLSA<T>* GetLSA() const;
 
     /**
      * @brief Set the Global Router Link State Advertisement returned by the
@@ -197,7 +241,7 @@
      * must not delete the LSA after calling this method.
      * @param lsa A pointer to the GlobalRoutingLSA.
      */
-    void SetLSA(GlobalRoutingLSA* lsa);
+    void SetLSA(GlobalRoutingLSA<IpManager>* lsa);
 
     /**
      * @brief Get the distance from the root vertex to "this" SPFVertex object.
@@ -282,9 +326,9 @@
      * @param id The interface index to use when forwarding packets to the host or
      * network represented by "this" SPFVertex.
      */
-    void SetRootExitDirection(Ipv4Address nextHop, int32_t id = SPF_INFINITY);
+    void SetRootExitDirection(IpAddress nextHop, int32_t id = SPF_INFINITY);
 
-    typedef std::pair<Ipv4Address, int32_t>
+    typedef std::pair<IpAddress, int32_t>
         NodeExit_t; //!< IPv4 / interface container for exit nodes.
 
     /**
@@ -355,7 +399,7 @@
      * @param vertex From which the list of exit directions are obtain
      * and are merged into 'this' vertex
      */
-    void MergeRootExitDirections(const SPFVertex* vertex);
+    void MergeRootExitDirections(const SPFVertex<T>* vertex);
     /**
      * @brief Inherit all root exit directions from a given vertex to 'this' vertex
      * @param vertex The vertex from which all root exit directions are to be inherited
@@ -363,7 +407,7 @@
      * After the call of this method, the original root exit directions
      * in 'this' vertex are all lost.
      */
-    void InheritAllRootExitDirections(const SPFVertex* vertex);
+    void InheritAllRootExitDirections(const SPFVertex<T>* vertex);
     /**
      * @brief Get the number of exit directions from root for reaching 'this' vertex
      * @return The number of exit directions from root
@@ -409,7 +453,7 @@
      * @param parent A pointer to the SPFVertex that is the parent of "this"
      * SPFVertex* in the SPF tree.
      */
-    void SetParent(SPFVertex* parent);
+    void SetParent(SPFVertex<T>* parent);
     /**
      * @brief Merge the Parent list from the v into this vertex
      *
@@ -417,7 +461,7 @@
      * and then merged into the list of Parent of *this* vertex.
      * Note that the list in v remains intact
      */
-    void MergeParent(const SPFVertex* v);
+    void MergeParent(const SPFVertex<T>* v);
 
     /**
      * @brief Get the number of children of "this" SPFVertex.
@@ -466,7 +510,7 @@
      * @returns A pointer to the specified child SPFVertex (which resides in the
      * SPF tree).
      */
-    SPFVertex* GetChild(uint32_t n) const;
+    SPFVertex<T>* GetChild(uint32_t n) const;
 
     /**
      * @brief Get a borrowed SPFVertex pointer to the specified child of "this"
@@ -494,7 +538,7 @@
      * @returns The number of children of "this" SPFVertex after the addition of
      * the new child.
      */
-    uint32_t AddChild(SPFVertex* child);
+    uint32_t AddChild(SPFVertex<T>* child);
 
     /**
      * @brief Set the value of the VertexProcessed flag
@@ -530,28 +574,45 @@
 
   private:
     VertexType m_vertexType;                        //!< Vertex type
-    Ipv4Address m_vertexId;                         //!< Vertex ID
-    GlobalRoutingLSA* m_lsa;                        //!< Link State Advertisement
+    IpAddress m_vertexId;                           //!< Vertex ID
+    GlobalRoutingLSA<IpManager>* m_lsa;             //!< Link State Advertisement
     uint32_t m_distanceFromRoot;                    //!< Distance from root node
     int32_t m_rootOif;                              //!< root Output Interface
-    Ipv4Address m_nextHop;                          //!< next hop
+    IpAddress m_nextHop;                            //!< next hop
     typedef std::list<NodeExit_t> ListOfNodeExit_t; //!< container of Exit nodes
     ListOfNodeExit_t m_ecmpRootExits; //!< store the multiple root's exits for supporting ECMP
-    typedef std::list<SPFVertex*> ListOfSPFVertex_t; //!< container of SPFVertex items
-    ListOfSPFVertex_t m_parents;                     //!< parent list
-    ListOfSPFVertex_t m_children;                    //!< Children list
+    typedef std::list<SPFVertex<T>*> ListOfSPFVertex_t; //!< container of SPFVertex items
+    ListOfSPFVertex_t m_parents;                        //!< parent list
+    ListOfSPFVertex_t m_children;                       //!< Children list
     bool m_vertexProcessed; //!< Flag to note whether vertex has been processed in stage two of SPF
                             //!< computation
     Ptr<Node> m_node;       //!< node pointer corresponding to the this Vertex
 
     /**
      * @brief Stream insertion operator.
-     *
      * @param os the reference to the output stream
      * @param vs a list of SPFVertex items
      * @returns the reference to the output stream
      */
-    friend std::ostream& operator<<(std::ostream& os, const SPFVertex::ListOfSPFVertex_t& vs);
+    friend std::ostream& operator<<(std::ostream& os,
+                                    const typename SPFVertex<T>::ListOfSPFVertex_t& vs)
+    {
+        os << "{";
+        for (auto iter = vs.begin(); iter != vs.end();)
+        {
+            os << (*iter)->m_vertexId;
+            if (++iter != vs.end())
+            {
+                os << ", ";
+            }
+            else
+            {
+                break;
+            }
+        }
+        os << "}";
+        return os;
+    }
 };
 
 /**
@@ -567,8 +628,40 @@
  * This class implements a searchable database of LSAs gathered from every
  * router in the simulation.
  */
+template <typename T>
 class GlobalRouteManagerLSDB
 {
+    static_assert(std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+                  "T must be either Ipv4Manager or Ipv6Manager In GlobalRouteManagerLSDB");
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4Manager and Ipv6Manager classes
+    using IpManager = typename std::conditional_t<IsIpv4, Ipv4Manager, Ipv6Manager>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
   public:
     /**
      * @brief Construct an empty Global Router Manager Link State Database.
@@ -604,7 +697,7 @@
      * ID.
      * @param lsa A pointer to the Link State Advertisement for the router.
      */
-    void Insert(Ipv4Address addr, GlobalRoutingLSA* lsa);
+    void Insert(IpAddress addr, GlobalRoutingLSA<IpManager>* lsa);
 
     /**
      * @brief Look up the Link State Advertisement associated with the given
@@ -620,7 +713,7 @@
      * @returns A pointer to the Link State Advertisement for the router specified
      * by the IP address addr.
      */
-    GlobalRoutingLSA* GetLSA(Ipv4Address addr) const;
+    GlobalRoutingLSA<IpManager>* GetLSA(IpAddress addr) const;
     /**
      * @brief Look up the Link State Advertisement associated with the given
      * link state ID (address).  This is a variation of the GetLSA call
@@ -633,7 +726,7 @@
      * by the IP address addr.
      * ID.
      */
-    GlobalRoutingLSA* GetLSAByLinkData(Ipv4Address addr) const;
+    GlobalRoutingLSA<IpManager>* GetLSAByLinkData(IpAddress addr) const;
 
     /**
      * @brief Set all LSA flags to an initialized state, for SPF computation
@@ -659,7 +752,7 @@
      * @param index the index associated with the LSA.
      * @returns A pointer to the Link State Advertisement.
      */
-    GlobalRoutingLSA* GetExtLSA(uint32_t index) const;
+    GlobalRoutingLSA<IpManager>* GetExtLSA(uint32_t index) const;
     /**
      * @brief Get the number of External Link State Advertisements.
      *
@@ -669,13 +762,13 @@
     uint32_t GetNumExtLSAs() const;
 
   private:
-    typedef std::map<Ipv4Address, GlobalRoutingLSA*>
+    typedef std::map<IpAddress, GlobalRoutingLSA<IpManager>*>
         LSDBMap_t; //!< container of IPv4 addresses / Link State Advertisements
-    typedef std::pair<Ipv4Address, GlobalRoutingLSA*>
+    typedef std::pair<IpAddress, GlobalRoutingLSA<IpManager>*>
         LSDBPair_t; //!< pair of IPv4 addresses / Link State Advertisements
 
     LSDBMap_t m_database; //!< database of IPv4 addresses / Link State Advertisements
-    std::vector<GlobalRoutingLSA*>
+    std::vector<GlobalRoutingLSA<IpManager>*>
         m_extdatabase; //!< database of External Link State Advertisements
 };
 
@@ -690,8 +783,54 @@
  *
  * The design is guided by OSPFv2 \RFC{2328} section 16.1.1 and quagga ospfd.
  */
+template <typename T>
 class GlobalRouteManagerImpl
 {
+    static_assert(
+        std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+        "T must be either Ipv4Manager or Ipv6Manager when calling GlobalRouteManagerImpl");
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Manager and Ipv6Manager classes
+    using IpManager = typename std::conditional_t<IsIpv4, Ipv4Manager, Ipv6Manager>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
+    /// Alias for Ipv4ListRouting and Ipv6ListRouting classes
+    using IpListRouting = typename std::conditional_t<IsIpv4, Ipv4ListRouting, Ipv6ListRouting>;
+
+    /// Alias for Ipv4l3Protocol and Ipv6l3Protocol classes
+    using IpL3Protocol = typename std::conditional_t<IsIpv4, Ipv4L3Protocol, Ipv6L3Protocol>;
+
+    /// Alias for Ipv4RoutingProtocol and Ipv6RoutingProtocol classes
+    using IpRoutingProtocol =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingProtocol, Ipv6RoutingProtocol>;
+
+    /// Alias for Ipv4GlobalRouting and Ipv6GlobalRouting classes
+    using IpGlobalRouting = GlobalRouting<IpRoutingProtocol>;
+
   public:
     GlobalRouteManagerImpl();
     virtual ~GlobalRouteManagerImpl();
@@ -725,17 +864,54 @@
      * @brief Debugging routine; allow client code to supply a pre-built LSDB
      * @param lsdb the pre-built LSDB
      */
-    void DebugUseLsdb(GlobalRouteManagerLSDB* lsdb);
+    void DebugUseLsdb(GlobalRouteManagerLSDB<T>* lsdb);
 
     /**
      * @brief Debugging routine; call the core SPF from the unit tests
      * @param root the root node to start calculations
      */
-    void DebugSPFCalculate(Ipv4Address root);
+    void DebugSPFCalculate(IpAddress root);
+
+    /**
+     * @brief prints the path from this node to the destination node at a particular time.
+     * @param sourceNode the source node
+     * @param dest the destination node
+     * @param stream The output stream to which the routing path will be written.
+     * @param nodeIdLookup Print the Node Id
+     * @param unit The time unit for timestamps in the printed output.
+     * @see Ipv4GlobalRoutingHelper::PrintRoute
+     */
+    void PrintRoute(Ptr<Node> sourceNode,
+                    Ptr<Node> dest,
+                    Ptr<OutputStreamWrapper> stream,
+                    bool nodeIdLookup,
+                    Time::Unit unit);
+
+    /**
+     * @brief prints the path from this node to the destination node at a particular time.
+     * @param sourceNode the source node
+     * @param dest the destination nodes ipv4 address
+     * @param stream The output stream to which the routing path will be written.
+     * @param nodeIdLookup Print the node id
+     * @param unit The time unit for timestamps in the printed output.
+     * @see Ipv4GlobalRoutingHelper::PrintRoute
+     */
+    void PrintRoute(Ptr<Node> sourceNode,
+                    IpAddress dest,
+                    Ptr<OutputStreamWrapper> stream,
+                    bool nodeIdLookup,
+                    Time::Unit unit);
+
+    /**
+     * @brief initialize all nodes as routers. this method queries all the nodes in the simulation
+     * and enables ipv6 forwarding on all of them.
+     */
+    void InitializeRouters();
 
   private:
-    SPFVertex* m_spfroot;           //!< the root node
-    GlobalRouteManagerLSDB* m_lsdb; //!< the Link State DataBase (LSDB) of the Global Route Manager
+    SPFVertex<T>* m_spfroot; //!< the root node
+    GlobalRouteManagerLSDB<IpManager>*
+        m_lsdb; //!< the Link State DataBase (LSDB) of the Global Route Manager
 
     /**
      * @brief Test if a node is a stub, from an OSPF sense.
@@ -747,7 +923,7 @@
      * @param root the root node
      * @returns true if the node is a stub
      */
-    bool CheckForStubNode(Ipv4Address root);
+    bool CheckForStubNode(IpAddress root);
 
     /**
      * @brief Calculate the shortest path first (SPF) tree
@@ -755,7 +931,7 @@
      * Equivalent to quagga ospf_spf_calculate
      * @param root the root node
      */
-    void SPFCalculate(Ipv4Address root);
+    void SPFCalculate(IpAddress root);
 
     /**
      * @brief Process Stub nodes
@@ -766,7 +942,7 @@
      *
      * @param v vertex to be processed
      */
-    void SPFProcessStubs(SPFVertex* v);
+    void SPFProcessStubs(SPFVertex<T>* v);
 
     /**
      * @brief Process Autonomous Systems (AS) External LSA
@@ -774,7 +950,7 @@
      * @param v vertex to be processed
      * @param extlsa external LSA
      */
-    void ProcessASExternals(SPFVertex* v, GlobalRoutingLSA* extlsa);
+    void ProcessASExternals(SPFVertex<T>* v, GlobalRoutingLSA<IpManager>* extlsa);
 
     /**
      * @brief Examine the links in v's LSA and update the list of candidates with any
@@ -797,7 +973,7 @@
      * @param v the vertex
      * @param candidate the SPF candidate queue
      */
-    void SPFNext(SPFVertex* v, CandidateQueue& candidate);
+    void SPFNext(SPFVertex<T>* v, CandidateQueue<T>& candidate);
 
     /**
      * @brief Calculate nexthop from root through V (parent) to vertex W (destination)
@@ -812,9 +988,9 @@
      * @param distance the target distance
      * @returns 1 on success
      */
-    int SPFNexthopCalculation(SPFVertex* v,
-                              SPFVertex* w,
-                              GlobalRoutingLinkRecord* l,
+    int SPFNexthopCalculation(SPFVertex<T>* v,
+                              SPFVertex<T>* w,
+                              GlobalRoutingLinkRecord<IpManager>* l,
                               uint32_t distance);
 
     /**
@@ -831,7 +1007,7 @@
      *
      * @param v the vertex
      */
-    void SPFVertexAddParent(SPFVertex* v);
+    void SPFVertexAddParent(SPFVertex<T>* v);
 
     /**
      * @brief Search for a link between two vertices.
@@ -852,9 +1028,10 @@
      * @param prev_link the previous link in the list
      * @returns the link's record
      */
-    GlobalRoutingLinkRecord* SPFGetNextLink(SPFVertex* v,
-                                            SPFVertex* w,
-                                            GlobalRoutingLinkRecord* prev_link);
+    GlobalRoutingLinkRecord<IpManager>* SPFGetNextLink(
+        SPFVertex<T>* v,
+        SPFVertex<T>* w,
+        GlobalRoutingLinkRecord<IpManager>* prev_link);
 
     /**
      * @brief Add a host route to the routing tables
@@ -878,14 +1055,14 @@
      * @param v the vertex
      *
      */
-    void SPFIntraAddRouter(SPFVertex* v);
+    void SPFIntraAddRouter(SPFVertex<T>* v);
 
     /**
      * @brief Add a transit to the routing tables
      *
      * @param v the vertex
      */
-    void SPFIntraAddTransit(SPFVertex* v);
+    void SPFIntraAddTransit(SPFVertex<T>* v);
 
     /**
      * @brief Add a stub to the routing tables
@@ -893,7 +1070,7 @@
      * @param l the global routing link record
      * @param v the vertex
      */
-    void SPFIntraAddStub(GlobalRoutingLinkRecord* l, SPFVertex* v);
+    void SPFIntraAddStub(GlobalRoutingLinkRecord<IpManager>* l, SPFVertex<T>* v);
 
     /**
      * @brief Add an external route to the routing tables
@@ -901,7 +1078,7 @@
      * @param extlsa the external LSA
      * @param v the vertex
      */
-    void SPFAddASExternal(GlobalRoutingLSA* extlsa, SPFVertex* v);
+    void SPFAddASExternal(GlobalRoutingLSA<IpManager>* extlsa, SPFVertex<T>* v);
 
     /**
      * @brief Return the interface number corresponding to a given IP address and mask
@@ -915,7 +1092,46 @@
      * @param amask the target subnet mask
      * @return the outgoing interface number
      */
-    int32_t FindOutgoingInterfaceId(Ipv4Address a, Ipv4Mask amask = Ipv4Mask("255.255.255.255"));
+    int32_t FindOutgoingInterfaceId(IpAddress a, IpMaskOrPrefix amask = IpMaskOrPrefix::GetOnes());
+
+    /**
+     * @brief given IP it iterates through the node list to find the node associated with the ip
+     * @param source ip address associated with the node we want to find
+     * @returns the node pointer to the ip
+     */
+    Ptr<Node> GetNodeByIp(const IpAddress& source);
+
+    /**
+     * @brief Is used by PrintRoute() to get the global routing protocol associated with it or
+     * returns nullptr if not found.
+     * @param node the node pointer
+     * @returns the global routing protocol associated with the node
+     */
+    Ptr<IpGlobalRouting> GetGlobalRoutingForNode(Ptr<Node> node);
+
+    /**
+     * @brief Is used by PrintRoute() to check if the destination is on the source node itself
+     * @param ip the ip stack of the source node
+     * @param dest the destination
+     * @returns true if the destination is on the source node itself
+     */
+    bool IsLocalDelivery(Ptr<Ip> ip, IpAddress dest);
+
+    /**
+     * @brief Is used by PrintRoute() to check if the source node has an ipv4 address
+     * @param ip the ip stack of the source node
+     * @returns true if the source node has an ipv4 address
+     */
+    bool ValidateSourceNodeHasIpAddress(Ptr<Ip> ip);
+
+    /**
+     * @brief Is used by PrintRoute() to check if the destination is on the same subnet as the
+     * source node
+     * @param ipCurrentNode the current node
+     * @param dest the destination
+     * @returns true if the destination is on the same subnet as the source node
+     */
+    bool IsOnSameSubnet(Ptr<Ip> ipCurrentNode, IpAddress dest);
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-router-interface.cc ns-3.47/src/internet/model/global-router-interface.cc
--- ns-3.46/src/internet/model/global-router-interface.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-router-interface.cc	2026-02-16 16:08:49.611230000 -0800
@@ -8,7 +8,7 @@
 
 #include "global-router-interface.h"
 
-#include "ipv4-global-routing.h"
+#include "global-routing.h"
 #include "ipv4.h"
 #include "loopback-net-device.h"
 
@@ -20,6 +20,7 @@
 #include "ns3/net-device.h"
 #include "ns3/node-list.h"
 #include "ns3/node.h"
+#include "ns3/object-base.h"
 
 #include <vector>
 
@@ -34,100 +35,145 @@
 //
 // ---------------------------------------------------------------------------
 
-GlobalRoutingLinkRecord::GlobalRoutingLinkRecord()
-    : m_linkId("0.0.0.0"),
-      m_linkData("0.0.0.0"),
+template <typename T>
+GlobalRoutingLinkRecord<T>::GlobalRoutingLinkRecord()
+    : m_linkId(IpAddress::GetZero()),
+      m_linkData(IpAddress::GetZero()),
+      m_linkLocData(IpAddress::GetZero()),
       m_linkType(Unknown),
       m_metric(0)
 {
     NS_LOG_FUNCTION(this);
 }
 
-GlobalRoutingLinkRecord::GlobalRoutingLinkRecord(LinkType linkType,
-                                                 Ipv4Address linkId,
-                                                 Ipv4Address linkData,
-                                                 uint16_t metric)
+template <typename T>
+GlobalRoutingLinkRecord<T>::GlobalRoutingLinkRecord(LinkType linkType,
+                                                    IpAddress linkId,
+                                                    IpAddress linkData,
+                                                    uint16_t metric)
     : m_linkId(linkId),
       m_linkData(linkData),
+      m_linkLocData(IpAddress::GetZero()),
       m_linkType(linkType),
       m_metric(metric)
 {
     NS_LOG_FUNCTION(this << linkType << linkId << linkData << metric);
 }
 
-GlobalRoutingLinkRecord::~GlobalRoutingLinkRecord()
+template <typename T>
+GlobalRoutingLinkRecord<T>::GlobalRoutingLinkRecord(LinkType linkType,
+                                                    IpAddress linkId,
+                                                    IpAddress linkData,
+                                                    IpAddress linkLocData,
+                                                    uint16_t metric)
+    : m_linkId(linkId),
+      m_linkData(linkData),
+      m_linkLocData(linkLocData),
+      m_linkType(linkType),
+      m_metric(metric)
+{
+    NS_LOG_FUNCTION(this << m_linkId << m_linkData << m_linkType << m_linkLocData << m_metric);
+}
+
+template <typename T>
+GlobalRoutingLinkRecord<T>::~GlobalRoutingLinkRecord()
 {
     NS_LOG_FUNCTION(this);
 }
 
-Ipv4Address
-GlobalRoutingLinkRecord::GetLinkId() const
+template <typename T>
+GlobalRoutingLinkRecord<T>::IpAddress
+GlobalRoutingLinkRecord<T>::GetLinkId() const
 {
     NS_LOG_FUNCTION(this);
     return m_linkId;
 }
 
+template <typename T>
 void
-GlobalRoutingLinkRecord::SetLinkId(Ipv4Address addr)
+GlobalRoutingLinkRecord<T>::SetLinkId(IpAddress addr)
 {
     NS_LOG_FUNCTION(this << addr);
     m_linkId = addr;
 }
 
-Ipv4Address
-GlobalRoutingLinkRecord::GetLinkData() const
+template <typename T>
+GlobalRoutingLinkRecord<T>::IpAddress
+GlobalRoutingLinkRecord<T>::GetLinkData() const
 {
     NS_LOG_FUNCTION(this);
     return m_linkData;
 }
 
+template <typename T>
 void
-GlobalRoutingLinkRecord::SetLinkData(Ipv4Address addr)
+GlobalRoutingLinkRecord<T>::SetLinkData(GlobalRoutingLinkRecord<T>::IpAddress addr)
 {
     NS_LOG_FUNCTION(this << addr);
     m_linkData = addr;
 }
 
-GlobalRoutingLinkRecord::LinkType
-GlobalRoutingLinkRecord::GetLinkType() const
+template <typename T>
+GlobalRoutingLinkRecord<T>::LinkType
+GlobalRoutingLinkRecord<T>::GetLinkType() const
 {
     NS_LOG_FUNCTION(this);
     return m_linkType;
 }
 
+template <typename T>
 void
-GlobalRoutingLinkRecord::SetLinkType(GlobalRoutingLinkRecord::LinkType linkType)
+GlobalRoutingLinkRecord<T>::SetLinkType(GlobalRoutingLinkRecord::LinkType linkType)
 {
     NS_LOG_FUNCTION(this << linkType);
     m_linkType = linkType;
 }
 
+template <typename T>
 uint16_t
-GlobalRoutingLinkRecord::GetMetric() const
+GlobalRoutingLinkRecord<T>::GetMetric() const
 {
     NS_LOG_FUNCTION(this);
     return m_metric;
 }
 
+template <typename T>
 void
-GlobalRoutingLinkRecord::SetMetric(uint16_t metric)
+GlobalRoutingLinkRecord<T>::SetMetric(uint16_t metric)
 {
     NS_LOG_FUNCTION(this << metric);
     m_metric = metric;
 }
 
+template <typename T>
+void
+GlobalRoutingLinkRecord<T>::SetLinkLocData(IpAddress linkLocData)
+{
+    NS_LOG_FUNCTION(this << linkLocData);
+    m_linkLocData = linkLocData;
+}
+
+template <typename T>
+GlobalRoutingLinkRecord<T>::IpAddress
+GlobalRoutingLinkRecord<T>::GetLinkLocData()
+{
+    NS_LOG_FUNCTION(this);
+    return m_linkLocData;
+}
+
 // ---------------------------------------------------------------------------
 //
 // GlobalRoutingLSA Implementation
 //
 // ---------------------------------------------------------------------------
 
-GlobalRoutingLSA::GlobalRoutingLSA()
+template <typename T>
+GlobalRoutingLSA<T>::GlobalRoutingLSA()
     : m_lsType(GlobalRoutingLSA::Unknown),
-      m_linkStateId("0.0.0.0"),
-      m_advertisingRtr("0.0.0.0"),
+      m_linkStateId(IpAddress::GetZero()),
+      m_advertisingRtr(IpAddress::GetZero()),
       m_linkRecords(),
-      m_networkLSANetworkMask("0.0.0.0"),
+      m_networkLSANetworkMask(IpMaskOrPrefix::GetZero()),
       m_attachedRouters(),
       m_status(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED),
       m_node_id(0)
@@ -135,14 +181,15 @@
     NS_LOG_FUNCTION(this);
 }
 
-GlobalRoutingLSA::GlobalRoutingLSA(GlobalRoutingLSA::SPFStatus status,
-                                   Ipv4Address linkStateId,
-                                   Ipv4Address advertisingRtr)
+template <typename T>
+GlobalRoutingLSA<T>::GlobalRoutingLSA(GlobalRoutingLSA::SPFStatus status,
+                                      IpAddress linkStateId,
+                                      IpAddress advertisingRtr)
     : m_lsType(GlobalRoutingLSA::Unknown),
       m_linkStateId(linkStateId),
       m_advertisingRtr(advertisingRtr),
       m_linkRecords(),
-      m_networkLSANetworkMask("0.0.0.0"),
+      m_networkLSANetworkMask(IpMaskOrPrefix::GetZero()),
       m_attachedRouters(),
       m_status(status),
       m_node_id(0)
@@ -150,7 +197,8 @@
     NS_LOG_FUNCTION(this << status << linkStateId << advertisingRtr);
 }
 
-GlobalRoutingLSA::GlobalRoutingLSA(GlobalRoutingLSA& lsa)
+template <typename T>
+GlobalRoutingLSA<T>::GlobalRoutingLSA(GlobalRoutingLSA& lsa)
     : m_lsType(lsa.m_lsType),
       m_linkStateId(lsa.m_linkStateId),
       m_advertisingRtr(lsa.m_advertisingRtr),
@@ -163,8 +211,9 @@
     CopyLinkRecords(lsa);
 }
 
-GlobalRoutingLSA&
-GlobalRoutingLSA::operator=(const GlobalRoutingLSA& lsa)
+template <typename T>
+GlobalRoutingLSA<T>&
+GlobalRoutingLSA<T>::operator=(const GlobalRoutingLSA<T>& lsa)
 {
     NS_LOG_FUNCTION(this << &lsa);
     m_lsType = lsa.m_lsType;
@@ -178,18 +227,20 @@
     return *this;
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::CopyLinkRecords(const GlobalRoutingLSA& lsa)
+GlobalRoutingLSA<T>::CopyLinkRecords(const GlobalRoutingLSA& lsa)
 {
     NS_LOG_FUNCTION(this << &lsa);
     for (auto i = lsa.m_linkRecords.begin(); i != lsa.m_linkRecords.end(); i++)
     {
-        GlobalRoutingLinkRecord* pSrc = *i;
-        auto pDst = new GlobalRoutingLinkRecord;
+        GlobalRoutingLinkRecord<T>* pSrc = *i;
+        auto pDst = new GlobalRoutingLinkRecord<T>;
 
         pDst->SetLinkType(pSrc->GetLinkType());
         pDst->SetLinkId(pSrc->GetLinkId());
         pDst->SetLinkData(pSrc->GetLinkData());
+        pDst->SetLinkLocData(pSrc->GetLinkLocData());
         pDst->SetMetric(pSrc->GetMetric());
 
         m_linkRecords.push_back(pDst);
@@ -199,21 +250,23 @@
     m_attachedRouters = lsa.m_attachedRouters;
 }
 
-GlobalRoutingLSA::~GlobalRoutingLSA()
+template <typename T>
+GlobalRoutingLSA<T>::~GlobalRoutingLSA()
 {
     NS_LOG_FUNCTION(this);
     ClearLinkRecords();
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::ClearLinkRecords()
+GlobalRoutingLSA<T>::ClearLinkRecords()
 {
     NS_LOG_FUNCTION(this);
     for (auto i = m_linkRecords.begin(); i != m_linkRecords.end(); i++)
     {
         NS_LOG_LOGIC("Free link record");
 
-        GlobalRoutingLinkRecord* p = *i;
+        GlobalRoutingLinkRecord<T>* p = *i;
         delete p;
         p = nullptr;
 
@@ -223,23 +276,26 @@
     m_linkRecords.clear();
 }
 
+template <typename T>
 uint32_t
-GlobalRoutingLSA::AddLinkRecord(GlobalRoutingLinkRecord* lr)
+GlobalRoutingLSA<T>::AddLinkRecord(GlobalRoutingLinkRecord<T>* lr)
 {
     NS_LOG_FUNCTION(this << lr);
     m_linkRecords.push_back(lr);
     return m_linkRecords.size();
 }
 
+template <typename T>
 uint32_t
-GlobalRoutingLSA::GetNLinkRecords() const
+GlobalRoutingLSA<T>::GetNLinkRecords() const
 {
     NS_LOG_FUNCTION(this);
     return m_linkRecords.size();
 }
 
-GlobalRoutingLinkRecord*
-GlobalRoutingLSA::GetLinkRecord(uint32_t n) const
+template <typename T>
+GlobalRoutingLinkRecord<T>*
+GlobalRoutingLSA<T>::GetLinkRecord(uint32_t n) const
 {
     NS_LOG_FUNCTION(this << n);
     uint32_t j = 0;
@@ -254,93 +310,106 @@
     return nullptr;
 }
 
+template <typename T>
 bool
-GlobalRoutingLSA::IsEmpty() const
+GlobalRoutingLSA<T>::IsEmpty() const
 {
     NS_LOG_FUNCTION(this);
     return m_linkRecords.empty();
 }
 
-GlobalRoutingLSA::LSType
-GlobalRoutingLSA::GetLSType() const
+template <typename T>
+GlobalRoutingLSA<T>::LSType
+GlobalRoutingLSA<T>::GetLSType() const
 {
     NS_LOG_FUNCTION(this);
     return m_lsType;
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::SetLSType(GlobalRoutingLSA::LSType typ)
+GlobalRoutingLSA<T>::SetLSType(GlobalRoutingLSA::LSType typ)
 {
     NS_LOG_FUNCTION(this << typ);
     m_lsType = typ;
 }
 
-Ipv4Address
-GlobalRoutingLSA::GetLinkStateId() const
+template <typename T>
+GlobalRoutingLSA<T>::IpAddress
+GlobalRoutingLSA<T>::GetLinkStateId() const
 {
     NS_LOG_FUNCTION(this);
     return m_linkStateId;
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::SetLinkStateId(Ipv4Address addr)
+GlobalRoutingLSA<T>::SetLinkStateId(IpAddress addr)
 {
     NS_LOG_FUNCTION(this << addr);
     m_linkStateId = addr;
 }
 
-Ipv4Address
-GlobalRoutingLSA::GetAdvertisingRouter() const
+template <typename T>
+GlobalRoutingLSA<T>::IpAddress
+GlobalRoutingLSA<T>::GetAdvertisingRouter() const
 {
     NS_LOG_FUNCTION(this);
     return m_advertisingRtr;
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::SetAdvertisingRouter(Ipv4Address addr)
+GlobalRoutingLSA<T>::SetAdvertisingRouter(IpAddress addr)
 {
     NS_LOG_FUNCTION(this << addr);
     m_advertisingRtr = addr;
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::SetNetworkLSANetworkMask(Ipv4Mask mask)
+GlobalRoutingLSA<T>::SetNetworkLSANetworkMask(IpMaskOrPrefix mask)
 {
     NS_LOG_FUNCTION(this << mask);
     m_networkLSANetworkMask = mask;
 }
 
-Ipv4Mask
-GlobalRoutingLSA::GetNetworkLSANetworkMask() const
+template <typename T>
+GlobalRoutingLSA<T>::IpMaskOrPrefix
+GlobalRoutingLSA<T>::GetNetworkLSANetworkMask() const
 {
     NS_LOG_FUNCTION(this);
     return m_networkLSANetworkMask;
 }
 
-GlobalRoutingLSA::SPFStatus
-GlobalRoutingLSA::GetStatus() const
+template <typename T>
+GlobalRoutingLSA<T>::SPFStatus
+GlobalRoutingLSA<T>::GetStatus() const
 {
     NS_LOG_FUNCTION(this);
     return m_status;
 }
 
+template <typename T>
 uint32_t
-GlobalRoutingLSA::AddAttachedRouter(Ipv4Address addr)
+GlobalRoutingLSA<T>::AddAttachedRouter(IpAddress addr)
 {
     NS_LOG_FUNCTION(this << addr);
     m_attachedRouters.push_back(addr);
     return m_attachedRouters.size();
 }
 
+template <typename T>
 uint32_t
-GlobalRoutingLSA::GetNAttachedRouters() const
+GlobalRoutingLSA<T>::GetNAttachedRouters() const
 {
     NS_LOG_FUNCTION(this);
     return m_attachedRouters.size();
 }
 
-Ipv4Address
-GlobalRoutingLSA::GetAttachedRouter(uint32_t n) const
+template <typename T>
+GlobalRoutingLSA<T>::IpAddress
+GlobalRoutingLSA<T>::GetAttachedRouter(uint32_t n) const
 {
     NS_LOG_FUNCTION(this << n);
     uint32_t j = 0;
@@ -352,32 +421,36 @@
         }
     }
     NS_ASSERT_MSG(false, "GlobalRoutingLSA::GetAttachedRouter (): invalid index");
-    return Ipv4Address("0.0.0.0");
+    return IpAddress::GetZero();
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::SetStatus(GlobalRoutingLSA::SPFStatus status)
+GlobalRoutingLSA<T>::SetStatus(GlobalRoutingLSA::SPFStatus status)
 {
     NS_LOG_FUNCTION(this << status);
     m_status = status;
 }
 
+template <typename T>
 Ptr<Node>
-GlobalRoutingLSA::GetNode() const
+GlobalRoutingLSA<T>::GetNode() const
 {
     NS_LOG_FUNCTION(this);
     return NodeList::GetNode(m_node_id);
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::SetNode(Ptr<Node> node)
+GlobalRoutingLSA<T>::SetNode(Ptr<Node> node)
 {
     NS_LOG_FUNCTION(this << node);
     m_node_id = node->GetId();
 }
 
+template <typename T>
 void
-GlobalRoutingLSA::Print(std::ostream& os) const
+GlobalRoutingLSA<T>::Print(std::ostream& os) const
 {
     NS_LOG_FUNCTION(this << &os);
     os << std::endl;
@@ -408,18 +481,18 @@
     {
         for (auto i = m_linkRecords.begin(); i != m_linkRecords.end(); i++)
         {
-            GlobalRoutingLinkRecord* p = *i;
+            GlobalRoutingLinkRecord<T>* p = *i;
 
             os << "---------- RouterLSA Link Record ----------" << std::endl;
             os << "m_linkType = " << p->m_linkType;
-            if (p->m_linkType == GlobalRoutingLinkRecord::PointToPoint)
+            if (p->m_linkType == GlobalRoutingLinkRecord<T>::PointToPoint)
             {
                 os << " (GlobalRoutingLinkRecord::PointToPoint)" << std::endl;
                 os << "m_linkId = " << p->m_linkId << std::endl;
                 os << "m_linkData = " << p->m_linkData << std::endl;
                 os << "m_metric = " << p->m_metric << std::endl;
             }
-            else if (p->m_linkType == GlobalRoutingLinkRecord::TransitNetwork)
+            else if (p->m_linkType == GlobalRoutingLinkRecord<T>::TransitNetwork)
             {
                 os << " (GlobalRoutingLinkRecord::TransitNetwork)" << std::endl;
                 os << "m_linkId = " << p->m_linkId << " (Designated router for network)"
@@ -428,7 +501,7 @@
                    << std::endl;
                 os << "m_metric = " << p->m_metric << std::endl;
             }
-            else if (p->m_linkType == GlobalRoutingLinkRecord::StubNetwork)
+            else if (p->m_linkType == GlobalRoutingLinkRecord<T>::StubNetwork)
             {
                 os << " (GlobalRoutingLinkRecord::StubNetwork)" << std::endl;
                 os << "m_linkId = " << p->m_linkId << " (Network number of attached network)"
@@ -453,8 +526,7 @@
         os << "m_networkLSANetworkMask = " << m_networkLSANetworkMask << std::endl;
         for (auto i = m_attachedRouters.begin(); i != m_attachedRouters.end(); i++)
         {
-            Ipv4Address p = *i;
-            os << "attachedRouter = " << p << std::endl;
+            os << "attachedRouter = " << *i << std::endl;
         }
         os << "---------- End NetworkLSA Link Record ----------" << std::endl;
     }
@@ -471,8 +543,9 @@
     os << "========== End Global Routing LSA ==========" << std::endl;
 }
 
+template <typename T>
 std::ostream&
-operator<<(std::ostream& os, GlobalRoutingLSA& lsa)
+operator<<(std::ostream& os, GlobalRoutingLSA<T>& lsa)
 {
     lsa.Print(os);
     return os;
@@ -484,44 +557,70 @@
 //
 // ---------------------------------------------------------------------------
 
-NS_OBJECT_ENSURE_REGISTERED(GlobalRouter);
-
+template <typename T>
 TypeId
-GlobalRouter::GetTypeId()
+GlobalRouter<T>::GetTypeId()
 {
-    static TypeId tid = TypeId("ns3::GlobalRouter").SetParent<Object>().SetGroupName("Internet");
+    std::string name;
+    if constexpr (IsIpv4)
+    {
+        name = "Ipv4";
+    }
+    else
+    {
+        name = "Ipv6";
+    }
+    static TypeId tid = TypeId("ns3::" + name + "GlobalRouter")
+                            .SetParent<Object>()
+                            .SetGroupName("GlobalRouter")
+                            .template AddConstructor<GlobalRouter<T>>();
     return tid;
 }
 
-GlobalRouter::GlobalRouter()
+template <typename T>
+GlobalRouter<T>::GlobalRouter()
     : m_LSAs()
 {
     NS_LOG_FUNCTION(this);
-    m_routerId.Set(GlobalRouteManager::AllocateRouterId());
+    if constexpr (IsIpv4)
+    {
+        m_routerId.Set(Ipv4GlobalRouteManager::AllocateRouterId());
+    }
+    else
+    {
+        // looks ugly but gets the job done .
+        // Alternatively, convert uint32_t to a uint8_t buf[16] and then use  Ipv6Address(buf)
+        m_routerId = Ipv6Address::MakeIpv4MappedAddress(
+            Ipv4Address(GlobalRouteManager<Ipv6Manager>::AllocateRouterId()));
+    }
 }
 
-GlobalRouter::~GlobalRouter()
+template <typename T>
+GlobalRouter<T>::~GlobalRouter()
 {
     NS_LOG_FUNCTION(this);
     ClearLSAs();
 }
 
+template <typename T>
 void
-GlobalRouter::SetRoutingProtocol(Ptr<Ipv4GlobalRouting> routing)
+GlobalRouter<T>::SetRoutingProtocol(Ptr<GlobalRouting<IpRoutingProtocol>> routing)
 {
     NS_LOG_FUNCTION(this << routing);
     m_routingProtocol = routing;
 }
 
-Ptr<Ipv4GlobalRouting>
-GlobalRouter::GetRoutingProtocol()
+template <typename T>
+Ptr<GlobalRouting<typename GlobalRouter<T>::IpRoutingProtocol>>
+GlobalRouter<T>::GetRoutingProtocol()
 {
     NS_LOG_FUNCTION(this);
     return m_routingProtocol;
 }
 
+template <typename T>
 void
-GlobalRouter::DoDispose()
+GlobalRouter<T>::DoDispose()
 {
     NS_LOG_FUNCTION(this);
     m_routingProtocol = nullptr;
@@ -533,15 +632,16 @@
     Object::DoDispose();
 }
 
+template <typename T>
 void
-GlobalRouter::ClearLSAs()
+GlobalRouter<T>::ClearLSAs()
 {
     NS_LOG_FUNCTION(this);
     for (auto i = m_LSAs.begin(); i != m_LSAs.end(); i++)
     {
         NS_LOG_LOGIC("Free LSA");
 
-        GlobalRoutingLSA* p = *i;
+        GlobalRoutingLSA<T>* p = *i;
         delete p;
         p = nullptr;
 
@@ -551,8 +651,9 @@
     m_LSAs.clear();
 }
 
-Ipv4Address
-GlobalRouter::GetRouterId() const
+template <typename T>
+GlobalRouter<T>::IpAddress
+GlobalRouter<T>::GetRouterId() const
 {
     NS_LOG_FUNCTION(this);
     return m_routerId;
@@ -564,8 +665,9 @@
 // and build the Link State Advertisements that reflect them and their associated
 // networks.
 //
+template <typename T>
 uint32_t
-GlobalRouter::DiscoverLSAs()
+GlobalRouter<T>::DiscoverLSAs()
 {
     NS_LOG_FUNCTION(this);
     Ptr<Node> node = GetObject<Node>();
@@ -587,18 +689,18 @@
     // Ipv4 interface.  This is where the information regarding the attached
     // interfaces lives.  If we're a router, we had better have an Ipv4 interface.
     //
-    Ptr<Ipv4> ipv4Local = node->GetObject<Ipv4>();
-    NS_ABORT_MSG_UNLESS(ipv4Local,
+    Ptr<Ip> ipLocal = node->GetObject<Ip>();
+    NS_ABORT_MSG_UNLESS(ipLocal,
                         "GlobalRouter::DiscoverLSAs (): GetObject for <Ipv4> interface failed");
 
     //
     // Every router node originates a Router-LSA
     //
-    auto pLSA = new GlobalRoutingLSA;
-    pLSA->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto pLSA = new GlobalRoutingLSA<T>;
+    pLSA->SetLSType(GlobalRoutingLSA<T>::RouterLSA);
     pLSA->SetLinkStateId(m_routerId);
     pLSA->SetAdvertisingRouter(m_routerId);
-    pLSA->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+    pLSA->SetStatus(GlobalRoutingLSA<T>::LSA_SPF_NOT_EXPLORED);
     pLSA->SetNode(node);
 
     //
@@ -629,7 +731,7 @@
         //
         if (NetDeviceIsBridged(ndLocal))
         {
-            int32_t ifIndex = ipv4Local->GetInterfaceForDevice(ndLocal);
+            int32_t ifIndex = ipLocal->GetInterfaceForDevice(ndLocal);
             NS_ABORT_MSG_IF(
                 ifIndex != -1,
                 "GlobalRouter::DiscoverLSAs(): Bridge ports must not have an IPv4 interface index");
@@ -641,9 +743,9 @@
         // associated with a bridge.  We are only going to involve devices with
         // IP addresses in routing.
         //
-        int32_t interfaceNumber = ipv4Local->GetInterfaceForDevice(ndLocal);
+        int32_t interfaceNumber = ipLocal->GetInterfaceForDevice(ndLocal);
         if (interfaceNumber == -1 ||
-            !(ipv4Local->IsUp(interfaceNumber) && ipv4Local->IsForwarding(interfaceNumber)))
+            !(ipLocal->IsUp(interfaceNumber) && ipLocal->IsForwarding(interfaceNumber)))
         {
             NS_LOG_LOGIC("Net device "
                          << ndLocal
@@ -699,19 +801,29 @@
     //
     for (auto i = m_injectedRoutes.begin(); i != m_injectedRoutes.end(); i++)
     {
-        auto pLSA = new GlobalRoutingLSA;
-        pLSA->SetLSType(GlobalRoutingLSA::ASExternalLSAs);
+        auto pLSA = new GlobalRoutingLSA<T>;
+        pLSA->SetLSType(GlobalRoutingLSA<T>::ASExternalLSAs);
         pLSA->SetLinkStateId((*i)->GetDestNetwork());
         pLSA->SetAdvertisingRouter(m_routerId);
-        pLSA->SetNetworkLSANetworkMask((*i)->GetDestNetworkMask());
-        pLSA->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+        if constexpr (IsIpv4)
+        {
+            pLSA->SetNetworkLSANetworkMask((*i)->GetDestNetworkMask());
+        }
+        else
+        {
+            pLSA->SetNetworkLSANetworkMask((*i)->GetDestNetworkPrefix());
+        }
+        pLSA->SetStatus(GlobalRoutingLSA<T>::LSA_SPF_NOT_EXPLORED);
         m_LSAs.push_back(pLSA);
     }
     return m_LSAs.size();
 }
 
+template <typename T>
 void
-GlobalRouter::ProcessBroadcastLink(Ptr<NetDevice> nd, GlobalRoutingLSA* pLSA, NetDeviceContainer& c)
+GlobalRouter<T>::ProcessBroadcastLink(Ptr<NetDevice> nd,
+                                      GlobalRoutingLSA<T>* pLSA,
+                                      NetDeviceContainer& c)
 {
     NS_LOG_FUNCTION(this << nd << pLSA << &c);
 
@@ -725,14 +837,15 @@
     }
 }
 
+template <typename T>
 void
-GlobalRouter::ProcessSingleBroadcastLink(Ptr<NetDevice> nd,
-                                         GlobalRoutingLSA* pLSA,
-                                         NetDeviceContainer& c)
+GlobalRouter<T>::ProcessSingleBroadcastLink(Ptr<NetDevice> nd,
+                                            GlobalRoutingLSA<T>* pLSA,
+                                            NetDeviceContainer& c)
 {
     NS_LOG_FUNCTION(this << nd << pLSA << &c);
 
-    auto plr = new GlobalRoutingLinkRecord;
+    auto plr = new GlobalRoutingLinkRecord<T>;
     NS_ABORT_MSG_IF(plr == nullptr,
                     "GlobalRouter::ProcessSingleBroadcastLink(): Can't alloc link record");
 
@@ -745,24 +858,43 @@
     //
     Ptr<Node> node = nd->GetNode();
 
-    Ptr<Ipv4> ipv4Local = node->GetObject<Ipv4>();
+    Ptr<Ip> ipLocal = node->GetObject<Ip>();
     NS_ABORT_MSG_UNLESS(
-        ipv4Local,
+        ipLocal,
         "GlobalRouter::ProcessSingleBroadcastLink (): GetObject for <Ipv4> interface failed");
 
-    int32_t interfaceLocal = ipv4Local->GetInterfaceForDevice(nd);
+    int32_t interfaceLocal = ipLocal->GetInterfaceForDevice(nd);
     NS_ABORT_MSG_IF(
         interfaceLocal == -1,
         "GlobalRouter::ProcessSingleBroadcastLink(): No interface index associated with device");
 
-    if (ipv4Local->GetNAddresses(interfaceLocal) > 1)
+    if (ipLocal->GetNAddresses(interfaceLocal) > 1)
     {
         NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the primary one");
     }
-    Ipv4Address addrLocal = ipv4Local->GetAddress(interfaceLocal, 0).GetLocal();
-    Ipv4Mask maskLocal = ipv4Local->GetAddress(interfaceLocal, 0).GetMask();
+    IpAddress addrLocal;
+    IpAddress addrLinkLocal; // this is the link local associated with the link for ipv6
+    if constexpr (IsIpv4)
+    {
+        addrLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+    }
+    else
+    {
+        addrLinkLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+        addrLocal = ipLocal->GetAddress(interfaceLocal, 1).GetAddress();
+    }
+
+    IpMaskOrPrefix maskLocal;
+    if constexpr (IsIpv4)
+    {
+        maskLocal = ipLocal->GetAddress(interfaceLocal, 0).GetMask();
+    }
+    else
+    {
+        maskLocal = ipLocal->GetAddress(interfaceLocal, 1).GetPrefix();
+    }
     NS_LOG_LOGIC("Working with local address " << addrLocal);
-    uint16_t metricLocal = ipv4Local->GetMetric(interfaceLocal);
+    uint16_t metricLocal = ipLocal->GetMetric(interfaceLocal);
 
     //
     // Check to see if the net device is connected to a channel/network that has
@@ -777,19 +909,35 @@
         // This is a net device connected to a stub network
         //
         NS_LOG_LOGIC("Router-LSA Stub Network");
-        plr->SetLinkType(GlobalRoutingLinkRecord::StubNetwork);
+        plr->SetLinkType(GlobalRoutingLinkRecord<T>::StubNetwork);
 
         //
         // According to OSPF, the Link ID is the IP network number of
         // the attached network.
         //
-        plr->SetLinkId(addrLocal.CombineMask(maskLocal));
+        if constexpr (IsIpv4)
+        {
+            plr->SetLinkId(addrLocal.CombineMask(maskLocal));
+        }
+        else
+        {
+            plr->SetLinkId(addrLocal.CombinePrefix(maskLocal));
+        }
 
         //
         // and the Link Data is the network mask; converted to Ipv4Address
         //
-        Ipv4Address maskLocalAddr;
-        maskLocalAddr.Set(maskLocal.Get());
+        IpAddress maskLocalAddr;
+        if constexpr (IsIpv4)
+        {
+            maskLocalAddr.Set(maskLocal.Get());
+        }
+        else
+        {
+            uint8_t buf[16];
+            maskLocal.GetBytes(buf);
+            maskLocalAddr.Set(buf);
+        }
         plr->SetLinkData(maskLocalAddr);
         plr->SetMetric(metricLocal);
         pLSA->AddLinkRecord(plr);
@@ -802,7 +950,7 @@
         // a transit network.
         //
         NS_LOG_LOGIC("Router-LSA Transit Network");
-        plr->SetLinkType(GlobalRoutingLinkRecord::TransitNetwork);
+        plr->SetLinkType(GlobalRoutingLinkRecord<T>::TransitNetwork);
 
         //
         // By definition, the router with the lowest IP address is the
@@ -811,17 +959,43 @@
         // case.
         //
         ClearBridgesVisited();
-        Ipv4Address designatedRtr;
+        IpAddress designatedRtr;
         designatedRtr = FindDesignatedRouterForLink(nd);
 
         //
         // Let's double-check that any designated router we find out on our
         // network is really on our network.
         //
-        if (designatedRtr != "255.255.255.255")
+        bool isRtrAllOnes = false;
+        if constexpr (IsIpv4)
+        {
+            if (designatedRtr.IsBroadcast())
+            {
+                isRtrAllOnes = true;
+            }
+        }
+        else
         {
-            Ipv4Address networkHere = addrLocal.CombineMask(maskLocal);
-            Ipv4Address networkThere = designatedRtr.CombineMask(maskLocal);
+            if (designatedRtr == Ipv6Address::GetOnes())
+            {
+                isRtrAllOnes = true;
+            }
+        }
+
+        if (!isRtrAllOnes)
+        {
+            IpAddress networkHere;
+            IpAddress networkThere;
+            if constexpr (IsIpv4)
+            {
+                networkHere = addrLocal.CombineMask(maskLocal);
+                networkThere = designatedRtr.CombineMask(maskLocal);
+            }
+            else
+            {
+                networkHere = addrLocal.CombinePrefix(maskLocal);
+                networkThere = designatedRtr.CombinePrefix(maskLocal);
+            }
             NS_ABORT_MSG_UNLESS(
                 networkHere == networkThere,
                 "GlobalRouter::ProcessSingleBroadcastLink(): Network number confusion ("
@@ -839,16 +1013,18 @@
         // OSPF says that the Link Data is this router's own IP address.
         //
         plr->SetLinkData(addrLocal);
+        plr->SetLinkLocData(addrLinkLocal);
         plr->SetMetric(metricLocal);
         pLSA->AddLinkRecord(plr);
         plr = nullptr;
     }
 }
 
+template <typename T>
 void
-GlobalRouter::ProcessBridgedBroadcastLink(Ptr<NetDevice> nd,
-                                          GlobalRoutingLSA* pLSA,
-                                          NetDeviceContainer& c)
+GlobalRouter<T>::ProcessBridgedBroadcastLink(Ptr<NetDevice> nd,
+                                             GlobalRoutingLSA<T>* pLSA,
+                                             NetDeviceContainer& c)
 {
     NS_LOG_FUNCTION(this << nd << pLSA << &c);
     NS_ASSERT_MSG(nd->IsBridge(),
@@ -876,18 +1052,37 @@
   // associated to them.
   //
   Ptr<Node> node = nd->GetNode ();
-  Ptr<Ipv4> ipv4Local = node->GetObject<Ipv4> ();
+  Ptr<Ip> ipLocal = node->GetObject<Ip> ();
   NS_ABORT_MSG_UNLESS (ipv4Local, "GlobalRouter::ProcessBridgedBroadcastLink (): GetObject for <Ipv4> interface failed");
 
-  int32_t interfaceLocal = ipv4Local->GetInterfaceForDevice (nd);
+  int32_t interfaceLocal = ipLocal->GetInterfaceForDevice (nd);
   NS_ABORT_MSG_IF (interfaceLocal == -1, "GlobalRouter::ProcessBridgedBroadcastLink(): No interface index associated with device");
 
-  if (ipv4Local->GetNAddresses (interfaceLocal) > 1)
+  if (ipLocal->GetNAddresses (interfaceLocal) > 1)
     {
       NS_LOG_WARN ("Warning, interface has multiple IP addresses; using only the primary one");
     }
-  Ipv4Address addrLocal = ipv4Local->GetAddress (interfaceLocal, 0).GetLocal ();
-  Ipv4Mask maskLocal = ipv4Local->GetAddress (interfaceLocal, 0).GetMask ();
+   IpAddress addrLocal;
+    IpAddress addrLinkLocal; // this is the link local associated with the link for ipv6
+    if constexpr (IsIpv4)
+    {
+        addrLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+    }
+    else
+    {
+        addrLinkLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+        addrLocal = ipLocal->GetAddress(interfaceLocal, 1).GetAddress();
+    }
+
+    IpMaskOrPrefix maskLocal;
+    if constexpr (IsIpv4)
+    {
+        maskLocal = ipLocal->GetAddress(interfaceLocal, 0).GetMask();
+    }
+    else
+    {
+        maskLocal = ipLocal->GetAddress(interfaceLocal, 1).GetPrefix();
+    }
   NS_LOG_LOGIC ("Working with local address " << addrLocal);
   uint16_t metricLocal = ipv4Local->GetMetric (interfaceLocal);
 
@@ -901,7 +1096,7 @@
   //
 
   bool areTransitNetwork = false;
-  Ipv4Address designatedRtr ("255.255.255.255");
+  IpAddress designatedRtr;
 
   for (uint32_t i = 0; i < bnd->GetNBridgePorts (); ++i)
     {
@@ -925,21 +1120,48 @@
           // all.
           //
           ClearBridgesVisited ();
-          Ipv4Address designatedRtrTemp = FindDesignatedRouterForLink (ndTemp);
+          IpAddress designatedRtrTemp = FindDesignatedRouterForLink (ndTemp);
 
           //
           // Let's double-check that any designated router we find out on our
           // network is really on our network.
           //
-          if (designatedRtrTemp != "255.255.255.255")
+         bool isRtrAllOnes = false;
+        if constexpr (IsIpv4)
+        {
+            if (designatedRtrTemp.IsBroadcast())
             {
-              Ipv4Address networkHere = addrLocal.CombineMask (maskLocal);
-              Ipv4Address networkThere = designatedRtrTemp.CombineMask (maskLocal);
-              NS_ABORT_MSG_UNLESS (networkHere == networkThere,
-                                   "GlobalRouter::ProcessSingleBroadcastLink(): Network number confusion (" <<
-                                   addrLocal << "/" << maskLocal.GetPrefixLength () << ", " <<
-                                   designatedRtrTemp << "/" << maskLocal.GetPrefixLength () << ")");
+                isRtrAllOnes = true;
             }
+        }
+        else
+        {
+            if (designatedRtrTemp == Ipv6Address::GetOnes())
+            {
+                isRtrAllOnes = true;
+            }
+        }
+
+        if (!isRtrAllOnes)
+        {
+            IpAddress networkHere;
+            IpAddress networkThere;
+            if constexpr (IsIpv4)
+            {
+                networkHere = addrLocal.CombineMask(maskLocal);
+                networkThere = designatedRtrTemp.CombineMask(maskLocal);
+            }
+            else
+            {
+                networkHere = addrLocal.CombinePrefix(maskLocal);
+                networkThere = designatedRtrTemp.CombinePrefix(maskLocal);
+            }
+            NS_ABORT_MSG_UNLESS(
+                networkHere == networkThere,
+                "GlobalRouter::ProcessSingleBroadcastLink(): Network number confusion ("
+                    << addrLocal << "/" << maskLocal.GetPrefixLength() << ", " << designatedRtr
+                    << "/" << maskLocal.GetPrefixLength() << ")");
+        }
           if (designatedRtrTemp < designatedRtr)
             {
               designatedRtr = designatedRtrTemp;
@@ -966,14 +1188,29 @@
       // According to OSPF, the Link ID is the IP network number of
       // the attached network.
       //
-      plr->SetLinkId (addrLocal.CombineMask (maskLocal));
-
+       if constexpr (IsIpv4)
+        {
+            plr->SetLinkId(addrLocal.CombineMask(maskLocal));
+        }
+        else
+        {
+            plr->SetLinkId(addrLocal.CombinePrefix(maskLocal));
+        }
       //
       // and the Link Data is the network mask; converted to Ipv4Address
       //
-      Ipv4Address maskLocalAddr;
-      maskLocalAddr.Set (maskLocal.Get ());
-      plr->SetLinkData (maskLocalAddr);
+IpAddress maskLocalAddr;
+        if constexpr (IsIpv4)
+        {
+            maskLocalAddr.Set(maskLocal.Get());
+        }
+        else
+        {
+            uint8_t buf[16];
+            maskLocal.GetBytes(buf);
+            maskLocalAddr.Set(buf);
+        }
+        plr->SetLinkData(maskLocalAddr);
       plr->SetMetric (metricLocal);
       pLSA->AddLinkRecord (plr);
       plr = 0;
@@ -1004,6 +1241,7 @@
       // OSPF says that the Link Data is this router's own IP address.
       //
       plr->SetLinkData (addrLocal);
+      plr->SetLinkLocData (addrLinkLocal);
       plr->SetMetric (metricLocal);
       pLSA->AddLinkRecord (plr);
       plr = 0;
@@ -1011,8 +1249,9 @@
 #endif
 }
 
+template <typename T>
 void
-GlobalRouter::ProcessPointToPointLink(Ptr<NetDevice> ndLocal, GlobalRoutingLSA* pLSA)
+GlobalRouter<T>::ProcessPointToPointLink(Ptr<NetDevice> ndLocal, GlobalRoutingLSA<T>* pLSA)
 {
     NS_LOG_FUNCTION(this << ndLocal << pLSA);
 
@@ -1025,23 +1264,56 @@
     //
     Ptr<Node> nodeLocal = ndLocal->GetNode();
 
-    Ptr<Ipv4> ipv4Local = nodeLocal->GetObject<Ipv4>();
+    Ptr<Ip> ipLocal = nodeLocal->GetObject<Ip>();
     NS_ABORT_MSG_UNLESS(
-        ipv4Local,
+        ipLocal,
         "GlobalRouter::ProcessPointToPointLink (): GetObject for <Ipv4> interface failed");
 
-    int32_t interfaceLocal = ipv4Local->GetInterfaceForDevice(ndLocal);
+    int32_t interfaceLocal = ipLocal->GetInterfaceForDevice(ndLocal);
     NS_ABORT_MSG_IF(
         interfaceLocal == -1,
         "GlobalRouter::ProcessPointToPointLink (): No interface index associated with device");
 
-    if (ipv4Local->GetNAddresses(interfaceLocal) > 1)
+    if ((ipLocal->GetNAddresses(interfaceLocal) == 0))
     {
-        NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the primary one");
+        NS_LOG_LOGIC("Local interface " << interfaceLocal << " has no address");
+        return;
+    }
+
+    IpAddress addrLocal;
+    IpAddress addrLinkLocal; // this is the link local associated with the link for ipv6
+    if constexpr (IsIpv4)
+    {
+        if (ipLocal->GetNAddresses(interfaceLocal) > 1)
+        {
+            NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the primary one");
+        }
+        addrLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+    }
+    else
+    {
+        addrLinkLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+        if (ipLocal->GetNAddresses(interfaceLocal) >
+            1) // if there is a globalUnicast address for the interface
+        {
+            addrLocal = ipLocal->GetAddress(interfaceLocal, 1).GetAddress();
+        }
+    }
+    IpMaskOrPrefix maskLocal;
+    if constexpr (IsIpv4)
+    {
+        maskLocal = ipLocal->GetAddress(interfaceLocal, 0).GetMask();
+    }
+    else
+    {
+        if (ipLocal->GetNAddresses(interfaceLocal) >
+            1) // if there is a globalUnicast address for the remote interface
+        {
+            maskLocal = ipLocal->GetAddress(interfaceLocal, 1).GetPrefix();
+        }
     }
-    Ipv4Address addrLocal = ipv4Local->GetAddress(interfaceLocal, 0).GetLocal();
     NS_LOG_LOGIC("Working with local address " << addrLocal);
-    uint16_t metricLocal = ipv4Local->GetMetric(interfaceLocal);
+    uint16_t metricLocal = ipLocal->GetMetric(interfaceLocal);
 
     //
     // Now, we're going to walk over to the remote net device on the other end of
@@ -1063,9 +1335,9 @@
     // with bridging.
     //
     Ptr<Node> nodeRemote = ndRemote->GetNode();
-    Ptr<Ipv4> ipv4Remote = nodeRemote->GetObject<Ipv4>();
+    Ptr<Ip> ipRemote = nodeRemote->GetObject<Ip>();
     NS_ABORT_MSG_UNLESS(
-        ipv4Remote,
+        ipRemote,
         "GlobalRouter::ProcessPointToPointLink(): GetObject for remote <Ipv4> failed");
 
     //
@@ -1082,28 +1354,57 @@
     //
     // We're going to need the remote router ID, so we might as well get it now.
     //
-    Ipv4Address rtrIdRemote = rtrRemote->GetRouterId();
+    IpAddress rtrIdRemote = rtrRemote->GetRouterId();
     NS_LOG_LOGIC("Working with remote router " << rtrIdRemote);
 
     //
     // Now, just like we did above, we need to get the IP interface index for the
     // net device on the other end of the point-to-point channel.
     //
-    int32_t interfaceRemote = ipv4Remote->GetInterfaceForDevice(ndRemote);
+    int32_t interfaceRemote = ipRemote->GetInterfaceForDevice(ndRemote);
     NS_ABORT_MSG_IF(interfaceRemote == -1,
                     "GlobalRouter::ProcessPointToPointLinks(): No interface index associated with "
                     "remote device");
 
+    if ((ipRemote->GetNAddresses(interfaceRemote) == 0))
+    {
+        NS_LOG_LOGIC("Remote interface " << interfaceRemote << " has no address");
+    }
     //
     // Now that we have the Ipv4 interface, we can get the (remote) address and
     // mask we need.
     //
-    if (ipv4Remote->GetNAddresses(interfaceRemote) > 1)
+
+    IpAddress addrRemote;
+    if constexpr (IsIpv4)
     {
-        NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the primary one");
+        if (ipRemote->GetNAddresses(interfaceRemote) > 1)
+        {
+            NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the primary one");
+        }
+        addrRemote = ipRemote->GetAddress(interfaceRemote, 0).GetAddress();
+    }
+    else
+    {
+        if (ipRemote->GetNAddresses(interfaceRemote) >
+            1) // if there is a globalUnicast address for the interface
+        {
+            addrRemote = ipRemote->GetAddress(interfaceRemote, 1).GetAddress();
+        }
+    }
+    IpMaskOrPrefix maskRemote;
+    if constexpr (IsIpv4)
+    {
+        maskRemote = ipRemote->GetAddress(interfaceRemote, 0).GetMask();
+    }
+    else
+    {
+        if (ipRemote->GetNAddresses(interfaceRemote) >
+            1) // if there is a globalUnicast address for the remote interface
+        {
+            maskRemote = ipRemote->GetAddress(interfaceRemote, 1).GetPrefix();
+        }
     }
-    Ipv4Address addrRemote = ipv4Remote->GetAddress(interfaceRemote, 0).GetLocal();
-    Ipv4Mask maskRemote = ipv4Remote->GetAddress(interfaceRemote, 0).GetMask();
     NS_LOG_LOGIC("Working with remote address " << addrRemote);
 
     //
@@ -1111,36 +1412,61 @@
     // link records; the first is a point-to-point record describing the link and
     // the second is a stub network record with the network number.
     //
-    GlobalRoutingLinkRecord* plr;
-    if (ipv4Remote->IsUp(interfaceRemote))
+    GlobalRoutingLinkRecord<T>* plr;
+
+    if (ipRemote->IsUp(interfaceRemote))
     {
         NS_LOG_LOGIC("Remote side interface " << interfaceRemote << " is up-- add a type 1 link");
 
-        plr = new GlobalRoutingLinkRecord;
+        plr = new GlobalRoutingLinkRecord<T>;
         NS_ABORT_MSG_IF(plr == nullptr,
                         "GlobalRouter::ProcessPointToPointLink(): Can't alloc link record");
-        plr->SetLinkType(GlobalRoutingLinkRecord::PointToPoint);
+        plr->SetLinkType(GlobalRoutingLinkRecord<T>::PointToPoint);
         plr->SetLinkId(rtrIdRemote);
         plr->SetLinkData(addrLocal);
+        plr->SetLinkLocData(addrLinkLocal);
         plr->SetMetric(metricLocal);
         pLSA->AddLinkRecord(plr);
         plr = nullptr;
     }
 
     // Regardless of state of peer, add a type 3 link (RFC 2328: 12.4.1.1)
-    plr = new GlobalRoutingLinkRecord;
+    // the only exception is if the remote interface has a link local address only, then it behaves
+    // as a router and not a host
+    if constexpr (!IsIpv4)
+    {
+        if (ipRemote->GetNAddresses(interfaceRemote) == 1 &&
+            ipRemote->GetAddress(interfaceRemote, 0).GetAddress().IsLinkLocal())
+        {
+            NS_LOG_LOGIC("The remote interface only has a link local address, not adding a type 3 "
+                         "link record");
+            return;
+        }
+    }
+
+    plr = new GlobalRoutingLinkRecord<T>;
     NS_ABORT_MSG_IF(plr == nullptr,
                     "GlobalRouter::ProcessPointToPointLink(): Can't alloc link record");
-    plr->SetLinkType(GlobalRoutingLinkRecord::StubNetwork);
+    plr->SetLinkType(GlobalRoutingLinkRecord<T>::StubNetwork);
     plr->SetLinkId(addrRemote);
-    plr->SetLinkData(Ipv4Address(maskRemote.Get())); // Frown
+    if constexpr (IsIpv4)
+    {
+        plr->SetLinkData(Ipv4Address(maskRemote.Get())); // Frown
+    }
+    else
+    {
+        uint8_t buf[16];
+        maskRemote.GetBytes(buf);
+        plr->SetLinkData(Ipv6Address(buf)); // Frown
+    }
     plr->SetMetric(metricLocal);
     pLSA->AddLinkRecord(plr);
     plr = nullptr;
 }
 
+template <typename T>
 void
-GlobalRouter::BuildNetworkLSAs(NetDeviceContainer c)
+GlobalRouter<T>::BuildNetworkLSAs(NetDeviceContainer c)
 {
     NS_LOG_FUNCTION(this << &c);
 
@@ -1156,32 +1482,48 @@
         Ptr<NetDevice> ndLocal = c.Get(i);
         Ptr<Node> node = ndLocal->GetNode();
 
-        Ptr<Ipv4> ipv4Local = node->GetObject<Ipv4>();
+        Ptr<Ip> ipLocal = node->GetObject<Ip>();
         NS_ABORT_MSG_UNLESS(
-            ipv4Local,
+            ipLocal,
             "GlobalRouter::ProcessPointToPointLink (): GetObject for <Ipv4> interface failed");
 
-        int32_t interfaceLocal = ipv4Local->GetInterfaceForDevice(ndLocal);
+        int32_t interfaceLocal = ipLocal->GetInterfaceForDevice(ndLocal);
         NS_ABORT_MSG_IF(
             interfaceLocal == -1,
             "GlobalRouter::BuildNetworkLSAs (): No interface index associated with device");
 
-        if (ipv4Local->GetNAddresses(interfaceLocal) > 1)
+        if (ipLocal->GetNAddresses(interfaceLocal) > 1)
         {
             NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the primary one");
         }
-        Ipv4Address addrLocal = ipv4Local->GetAddress(interfaceLocal, 0).GetLocal();
-        Ipv4Mask maskLocal = ipv4Local->GetAddress(interfaceLocal, 0).GetMask();
+        IpAddress addrLocal;
+        if constexpr (IsIpv4)
+        {
+            addrLocal = ipLocal->GetAddress(interfaceLocal, 0).GetAddress();
+        }
+        else
+        {
+            addrLocal = ipLocal->GetAddress(interfaceLocal, 1).GetAddress();
+        }
+        IpMaskOrPrefix maskLocal;
+        if constexpr (IsIpv4)
+        {
+            maskLocal = ipLocal->GetAddress(interfaceLocal, 0).GetMask();
+        }
+        else
+        {
+            maskLocal = ipLocal->GetAddress(interfaceLocal, 1).GetPrefix();
+        }
 
-        auto pLSA = new GlobalRoutingLSA;
+        auto pLSA = new GlobalRoutingLSA<T>;
         NS_ABORT_MSG_IF(pLSA == nullptr,
                         "GlobalRouter::BuildNetworkLSAs(): Can't alloc link record");
 
-        pLSA->SetLSType(GlobalRoutingLSA::NetworkLSA);
+        pLSA->SetLSType(GlobalRoutingLSA<T>::NetworkLSA);
         pLSA->SetLinkStateId(addrLocal);
         pLSA->SetAdvertisingRouter(m_routerId);
         pLSA->SetNetworkLSANetworkMask(maskLocal);
-        pLSA->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+        pLSA->SetStatus(GlobalRoutingLSA<T>::LSA_SPF_NOT_EXPLORED);
         pLSA->SetNode(node);
 
         //
@@ -1223,25 +1565,33 @@
             // Does the attached node have an ipv4 interface for the device we're probing?
             // If not, it can't play router.
             //
-            Ptr<Ipv4> tempIpv4 = tempNode->GetObject<Ipv4>();
-            int32_t tempInterface = tempIpv4->GetInterfaceForDevice(tempNd);
+            Ptr<Ip> tempIp = tempNode->GetObject<Ip>();
+            int32_t tempInterface = tempIp->GetInterfaceForDevice(tempNd);
 
             if (tempInterface != -1)
             {
-                Ptr<Ipv4> tempIpv4 = tempNode->GetObject<Ipv4>();
-                NS_ASSERT(tempIpv4);
-                if (!tempIpv4->IsUp(tempInterface))
+                Ptr<Ip> tempIp = tempNode->GetObject<Ip>();
+                NS_ASSERT(tempIp);
+                if (!tempIp->IsUp(tempInterface))
                 {
                     NS_LOG_LOGIC("Remote side interface " << tempInterface << " not up");
                 }
                 else
                 {
-                    if (tempIpv4->GetNAddresses(tempInterface) > 1)
+                    if (tempIp->GetNAddresses(tempInterface) > 1)
                     {
                         NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the "
                                     "primary one");
                     }
-                    Ipv4Address tempAddr = tempIpv4->GetAddress(tempInterface, 0).GetLocal();
+                    IpAddress tempAddr;
+                    if constexpr (IsIpv4)
+                    {
+                        tempAddr = tempIp->GetAddress(tempInterface, 0).GetAddress();
+                    }
+                    else
+                    {
+                        tempAddr = tempIp->GetAddress(tempInterface, 1).GetAddress();
+                    }
                     NS_LOG_LOGIC("Adding " << tempAddr << " to Network LSA");
                     pLSA->AddAttachedRouter(tempAddr);
                 }
@@ -1259,8 +1609,9 @@
     }
 }
 
+template <typename T>
 NetDeviceContainer
-GlobalRouter::FindAllNonBridgedDevicesOnLink(Ptr<Channel> ch) const
+GlobalRouter<T>::FindAllNonBridgedDevicesOnLink(Ptr<Channel> ch) const
 {
     NS_LOG_FUNCTION(this << ch);
     NetDeviceContainer c;
@@ -1305,8 +1656,9 @@
 // will be us).  Of these, the router with the lowest IP address on the net device
 // connecting to the channel becomes the designated router for the link.
 //
-Ipv4Address
-GlobalRouter::FindDesignatedRouterForLink(Ptr<NetDevice> ndLocal) const
+template <typename T>
+GlobalRouter<T>::IpAddress
+GlobalRouter<T>::FindDesignatedRouterForLink(Ptr<NetDevice> ndLocal) const
 {
     NS_LOG_FUNCTION(this << ndLocal);
 
@@ -1317,7 +1669,15 @@
     NS_LOG_LOGIC("Looking for designated router off of net device " << ndLocal << " on node "
                                                                     << ndLocal->GetNode()->GetId());
 
-    Ipv4Address designatedRtr("255.255.255.255");
+    IpAddress designatedRtr;
+    if constexpr (IsIpv4)
+    {
+        designatedRtr = Ipv4Address::GetBroadcast();
+    }
+    else
+    {
+        designatedRtr = Ipv6Address::GetOnes();
+    }
 
     //
     // Look through all of the devices on the channel to which the net device
@@ -1364,24 +1724,32 @@
             //
             NS_LOG_LOGIC("Checking for router on bridge net device " << bnd);
             Ptr<GlobalRouter> rtr = nodeOther->GetObject<GlobalRouter>();
-            Ptr<Ipv4> ipv4 = nodeOther->GetObject<Ipv4>();
-            if (rtr && ipv4)
+            Ptr<Ip> ip = nodeOther->GetObject<Ip>();
+            if (rtr && ip)
             {
-                int32_t interfaceOther = ipv4->GetInterfaceForDevice(bnd);
+                int32_t interfaceOther = ip->GetInterfaceForDevice(bnd);
                 if (interfaceOther != -1)
                 {
                     NS_LOG_LOGIC("Found router on bridge net device " << bnd);
-                    if (!ipv4->IsUp(interfaceOther))
+                    if (!ip->IsUp(interfaceOther))
                     {
                         NS_LOG_LOGIC("Remote side interface " << interfaceOther << " not up");
                         continue;
                     }
-                    if (ipv4->GetNAddresses(interfaceOther) > 1)
+                    if (ip->GetNAddresses(interfaceOther) > 1)
                     {
                         NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the "
                                     "primary one");
                     }
-                    Ipv4Address addrOther = ipv4->GetAddress(interfaceOther, 0).GetLocal();
+                    IpAddress addrOther;
+                    if constexpr (IsIpv4)
+                    {
+                        addrOther = ip->GetAddress(interfaceOther, 0).GetAddress();
+                    }
+                    else
+                    {
+                        addrOther = ip->GetAddress(interfaceOther, 1).GetAddress();
+                    }
                     designatedRtr = addrOther < designatedRtr ? addrOther : designatedRtr;
                     NS_LOG_LOGIC("designated router now " << designatedRtr);
                 }
@@ -1412,7 +1780,7 @@
                 }
 
                 NS_LOG_LOGIC("Recursively looking for routers down bridge port " << ndBridged);
-                Ipv4Address addrOther = FindDesignatedRouterForLink(ndBridged);
+                IpAddress addrOther = FindDesignatedRouterForLink(ndBridged);
                 designatedRtr = addrOther < designatedRtr ? addrOther : designatedRtr;
                 NS_LOG_LOGIC("designated router now " << designatedRtr);
             }
@@ -1428,24 +1796,32 @@
             // an internet stack that includes the Ipv4 interface.  If it doesn't
             //
             Ptr<GlobalRouter> rtr = nodeOther->GetObject<GlobalRouter>();
-            Ptr<Ipv4> ipv4 = nodeOther->GetObject<Ipv4>();
-            if (rtr && ipv4)
+            Ptr<Ip> ip = nodeOther->GetObject<Ip>();
+            if (rtr && ip)
             {
-                int32_t interfaceOther = ipv4->GetInterfaceForDevice(ndOther);
+                int32_t interfaceOther = ip->GetInterfaceForDevice(ndOther);
                 if (interfaceOther != -1)
                 {
-                    if (!ipv4->IsUp(interfaceOther))
+                    if (!ip->IsUp(interfaceOther))
                     {
                         NS_LOG_LOGIC("Remote side interface " << interfaceOther << " not up");
                         continue;
                     }
                     NS_LOG_LOGIC("Found router on net device " << ndOther);
-                    if (ipv4->GetNAddresses(interfaceOther) > 1)
+                    if (ip->GetNAddresses(interfaceOther) > 1)
                     {
                         NS_LOG_WARN("Warning, interface has multiple IP addresses; using only the "
                                     "primary one");
                     }
-                    Ipv4Address addrOther = ipv4->GetAddress(interfaceOther, 0).GetLocal();
+                    IpAddress addrOther;
+                    if constexpr (IsIpv4)
+                    {
+                        addrOther = ip->GetAddress(interfaceOther, 0).GetAddress();
+                    }
+                    else
+                    {
+                        addrOther = ip->GetAddress(interfaceOther, 1).GetAddress();
+                    }
                     designatedRtr = addrOther < designatedRtr ? addrOther : designatedRtr;
                     NS_LOG_LOGIC("designated router now " << designatedRtr);
                 }
@@ -1461,8 +1837,9 @@
 // that has a GlobalRouter interface aggregated.  Life gets more complicated
 // when there is a bridged net device on the other side.
 //
+template <typename T>
 bool
-GlobalRouter::AnotherRouterOnLink(Ptr<NetDevice> nd) const
+GlobalRouter<T>::AnotherRouterOnLink(Ptr<NetDevice> nd) const
 {
     NS_LOG_FUNCTION(this << nd);
 
@@ -1564,8 +1941,9 @@
     return false;
 }
 
+template <typename T>
 uint32_t
-GlobalRouter::GetNumLSAs() const
+GlobalRouter<T>::GetNumLSAs() const
 {
     NS_LOG_FUNCTION(this);
     return m_LSAs.size();
@@ -1574,8 +1952,9 @@
 //
 // Get the nth link state advertisement from this router.
 //
+template <typename T>
 bool
-GlobalRouter::GetLSA(uint32_t n, GlobalRoutingLSA& lsa) const
+GlobalRouter<T>::GetLSA(uint32_t n, GlobalRoutingLSA<T>& lsa) const
 {
     NS_LOG_FUNCTION(this << n << &lsa);
     NS_ASSERT_MSG(lsa.IsEmpty(), "GlobalRouter::GetLSA (): Must pass empty LSA");
@@ -1591,7 +1970,7 @@
     {
         if (j == n)
         {
-            GlobalRoutingLSA* p = *i;
+            GlobalRoutingLSA<T>* p = *i;
             lsa = *p;
             return true;
         }
@@ -1600,20 +1979,22 @@
     return false;
 }
 
+template <typename T>
 void
-GlobalRouter::InjectRoute(Ipv4Address network, Ipv4Mask networkMask)
+GlobalRouter<T>::InjectRoute(IpAddress network, IpMaskOrPrefix networkMask)
 {
     NS_LOG_FUNCTION(this << network << networkMask);
-    auto route = new Ipv4RoutingTableEntry();
+    auto route = new IpRoutingTableEntry();
     //
     // Interface number does not matter here, using 1.
     //
-    *route = Ipv4RoutingTableEntry::CreateNetworkRouteTo(network, networkMask, 1);
+    *route = IpRoutingTableEntry::CreateNetworkRouteTo(network, networkMask, 1);
     m_injectedRoutes.push_back(route);
 }
 
-Ipv4RoutingTableEntry*
-GlobalRouter::GetInjectedRoute(uint32_t index)
+template <typename T>
+GlobalRouter<T>::IpRoutingTableEntry*
+GlobalRouter<T>::GetInjectedRoute(uint32_t index)
 {
     NS_LOG_FUNCTION(this << index);
     if (index < m_injectedRoutes.size())
@@ -1633,15 +2014,17 @@
     return nullptr;
 }
 
+template <typename T>
 uint32_t
-GlobalRouter::GetNInjectedRoutes()
+GlobalRouter<T>::GetNInjectedRoutes()
 {
     NS_LOG_FUNCTION(this);
     return m_injectedRoutes.size();
 }
 
+template <typename T>
 void
-GlobalRouter::RemoveInjectedRoute(uint32_t index)
+GlobalRouter<T>::RemoveInjectedRoute(uint32_t index)
 {
     NS_LOG_FUNCTION(this << index);
     NS_ASSERT(index < m_injectedRoutes.size());
@@ -1659,18 +2042,32 @@
     }
 }
 
+template <typename T>
 bool
-GlobalRouter::WithdrawRoute(Ipv4Address network, Ipv4Mask networkMask)
+GlobalRouter<T>::WithdrawRoute(IpAddress network, IpMaskOrPrefix networkMask)
 {
     NS_LOG_FUNCTION(this << network << networkMask);
     for (auto i = m_injectedRoutes.begin(); i != m_injectedRoutes.end(); i++)
     {
-        if ((*i)->GetDestNetwork() == network && (*i)->GetDestNetworkMask() == networkMask)
+        if constexpr (IsIpv4)
         {
-            NS_LOG_LOGIC("Withdrawing route to network/mask " << network << "/" << networkMask);
-            delete *i;
-            m_injectedRoutes.erase(i);
-            return true;
+            if ((*i)->GetDestNetwork() == network && (*i)->GetDestNetworkMask() == networkMask)
+            {
+                NS_LOG_LOGIC("Withdrawing route to network/mask " << network << "/" << networkMask);
+                delete *i;
+                m_injectedRoutes.erase(i);
+                return true;
+            }
+        }
+        else
+        {
+            if ((*i)->GetDestNetwork() == network && (*i)->GetDestNetworkPrefix() == networkMask)
+            {
+                NS_LOG_LOGIC("Withdrawing route to network/mask " << network << "/" << networkMask);
+                delete *i;
+                m_injectedRoutes.erase(i);
+                return true;
+            }
         }
     }
     return false;
@@ -1680,8 +2077,9 @@
 // Link through the given channel and find the net device that's on the
 // other end.  This only makes sense with a point-to-point channel.
 //
+template <typename T>
 Ptr<NetDevice>
-GlobalRouter::GetAdjacent(Ptr<NetDevice> nd, Ptr<Channel> ch) const
+GlobalRouter<T>::GetAdjacent(Ptr<NetDevice> nd, Ptr<Channel> ch) const
 {
     NS_LOG_FUNCTION(this << nd << ch);
     NS_ASSERT_MSG(ch->GetNDevices() == 2,
@@ -1714,8 +2112,9 @@
 //
 // Decide whether or not a given net device is being bridged by a BridgeNetDevice.
 //
+template <typename T>
 Ptr<BridgeNetDevice>
-GlobalRouter::NetDeviceIsBridged(Ptr<NetDevice> nd) const
+GlobalRouter<T>::NetDeviceIsBridged(Ptr<NetDevice> nd) const
 {
     NS_LOG_FUNCTION(this << nd);
 
@@ -1759,8 +2158,9 @@
 //
 // Start a new enumeration of an L2 broadcast domain by clearing m_bridgesVisited
 //
+template <typename T>
 void
-GlobalRouter::ClearBridgesVisited() const
+GlobalRouter<T>::ClearBridgesVisited() const
 {
     m_bridgesVisited.clear();
 }
@@ -1768,8 +2168,9 @@
 //
 // Check if we have already visited a given bridge net device by searching m_bridgesVisited
 //
+template <typename T>
 bool
-GlobalRouter::BridgeHasAlreadyBeenVisited(Ptr<BridgeNetDevice> bridgeNetDevice) const
+GlobalRouter<T>::BridgeHasAlreadyBeenVisited(Ptr<BridgeNetDevice> bridgeNetDevice) const
 {
     for (auto iter = m_bridgesVisited.begin(); iter != m_bridgesVisited.end(); ++iter)
     {
@@ -1785,11 +2186,28 @@
 //
 // Remember that we visited a bridge net device by adding it to m_bridgesVisited
 //
+template <typename T>
 void
-GlobalRouter::MarkBridgeAsVisited(Ptr<BridgeNetDevice> bridgeNetDevice) const
+GlobalRouter<T>::MarkBridgeAsVisited(Ptr<BridgeNetDevice> bridgeNetDevice) const
 {
     NS_LOG_FUNCTION(this << bridgeNetDevice);
     m_bridgesVisited.push_back(bridgeNetDevice);
 }
 
+/**Explicit initialize the template classes */
+/** @brief Stream insertion operator
+ *  @returns the reference to the output stream
+ */
+template std::ostream& operator<< <Ipv4Manager>(std::ostream&, GlobalRoutingLSA<Ipv4Manager>&);
+template class GlobalRoutingLinkRecord<Ipv4Manager>;
+template class GlobalRoutingLSA<Ipv4Manager>;
+/** @brief Stream insertion operator
+ *  @returns the reference to the output stream
+ */
+template std::ostream& operator<< <Ipv6Manager>(std::ostream&, GlobalRoutingLSA<Ipv6Manager>&);
+template class GlobalRoutingLinkRecord<Ipv6Manager>;
+template class GlobalRoutingLSA<Ipv6Manager>;
+NS_OBJECT_TEMPLATE_CLASS_DEFINE(GlobalRouter, Ipv4Manager);
+NS_OBJECT_TEMPLATE_CLASS_DEFINE(GlobalRouter, Ipv6Manager);
+
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-router-interface.h ns-3.47/src/internet/model/global-router-interface.h
--- ns-3.46/src/internet/model/global-router-interface.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-router-interface.h	2026-02-16 16:08:49.611230000 -0800
@@ -11,7 +11,12 @@
 #define GLOBAL_ROUTER_INTERFACE_H
 
 #include "global-route-manager.h"
+#include "ipv4-routing-protocol.h"
 #include "ipv4-routing-table-entry.h"
+#include "ipv6-l3-protocol.h"
+#include "ipv6-routing-protocol.h"
+#include "ipv6-routing-table-entry.h"
+#include "ipv6.h"
 
 #include "ns3/bridge-net-device.h"
 #include "ns3/channel.h"
@@ -26,9 +31,10 @@
 
 namespace ns3
 {
-
+template <typename T>
 class GlobalRouter;
-class Ipv4GlobalRouting;
+template <typename>
+class GlobalRouting;
 
 /**
  * @ingroup globalrouting
@@ -39,9 +45,40 @@
  * a Link State Advertisement.  Right now we will only see two types of link
  * records corresponding to a stub network and a point-to-point link (channel).
  */
+template <typename T>
 class GlobalRoutingLinkRecord
 {
+    static_assert(
+        std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+        "T must be either Ipv4Manager or Ipv6Manager when calling GlobalRoutingLinkRecord");
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
   public:
+    template <typename>
     friend class GlobalRoutingLSA; //!< Friend class.
 
     /**
@@ -81,8 +118,26 @@
      * @see SetLinkData
      */
     GlobalRoutingLinkRecord(LinkType linkType,
-                            Ipv4Address linkId,
-                            Ipv4Address linkData,
+                            IpAddress linkId,
+                            IpAddress linkData,
+                            uint16_t metric);
+
+    /**
+     * Construct an initialized Global Routing Link Record.
+     *
+     * @param linkType The type of link record to construct.
+     * @param linkId The link ID for the record.
+     * @param linkData The link data field for the record.
+     * @param linkLocData The link local data field for the record.
+     * @param metric The metric field for the record.
+     * @see LinkType
+     * @see SetLinkId
+     * @see SetLinkData
+     */
+    GlobalRoutingLinkRecord(LinkType linkType,
+                            IpAddress linkId,
+                            IpAddress linkData,
+                            IpAddress linkLocData,
                             uint16_t metric);
 
     /**
@@ -103,7 +158,7 @@
      *
      * @returns The Ipv4Address corresponding to the Link ID field of the record.
      */
-    Ipv4Address GetLinkId() const;
+    IpAddress GetLinkId() const;
 
     /**
      * @brief Set the Link ID field of the Global Routing Link Record.
@@ -116,7 +171,7 @@
      *
      * @param addr An Ipv4Address to store in the Link ID field of the record.
      */
-    void SetLinkId(Ipv4Address addr);
+    void SetLinkId(IpAddress addr);
 
     /**
      * @brief Get the Link Data field of the Global Routing Link Record.
@@ -129,7 +184,7 @@
      *
      * @returns The Ipv4Address corresponding to the Link Data field of the record.
      */
-    Ipv4Address GetLinkData() const;
+    IpAddress GetLinkData() const;
 
     /**
      * @brief Set the Link Data field of the Global Routing Link Record.
@@ -142,7 +197,7 @@
      *
      * @param addr An Ipv4Address to store in the Link Data field of the record.
      */
-    void SetLinkData(Ipv4Address addr);
+    void SetLinkData(IpAddress addr);
 
     /**
      * @brief Get the Link Type field of the Global Routing Link Record.
@@ -192,6 +247,30 @@
      */
     void SetMetric(uint16_t metric);
 
+    /**
+     * @brief Set the Link Local Data field of the Global Routing Link Record.
+     *
+     * For an OSPF type 1 link (PointToPoint) the Link Local Data must be the Link Local IP
+     * address of the node of the local side of the link.
+     *
+     * For an OSPF type 3 link (StubNetwork), the Link Local data field is not used.
+     *
+     * @param addr An Ipv6Address to store in the Link Data field of the record.
+     */
+    void SetLinkLocData(IpAddress addr);
+
+    /**
+     * @brief Set the Link Local Data field of the Global Routing Link Record.
+     *
+     * For an OSPF type 1 link (PointToPoint) the Link Local Data must be the Link Local IP
+     * address of the node of the local side of the link.
+     *
+     * For an OSPF type 3 link (StubNetwork), the Link Local data field is not used.
+     *
+     * @returns addr An Ipv6Address to store in the Link Data field of the record.
+     */
+    IpAddress GetLinkLocData();
+
   private:
     /**
      * m_linkId and m_linkData are defined by OSPF to have different meanings
@@ -203,7 +282,7 @@
      *
      * For Type 3 link (Stub), set m_linkId to neighbor's IP address
      */
-    Ipv4Address m_linkId;
+    IpAddress m_linkId;
 
     /**
      * m_linkId and m_linkData are defined by OSPF to have different meanings
@@ -214,7 +293,18 @@
      *
      * For Type 3 link (Stub), set m_linkData to mask
      */
-    Ipv4Address m_linkData; // for links to RouterLSA,
+    IpAddress m_linkData; // for links to RouterLSA,
+
+    /**
+     * m_linkLocData is not defined by the OSPF spec. This field is used to store the link local
+     * address associated with the link on the local side.
+     *
+     * For Type 1 link (PointToPoint), set m_linkLocData to Link local IP address on the local side
+     *
+     * For Type 3 link (Stub), this is not used.
+     */
+    IpAddress
+        m_linkLocData; // for the Link local address associated with the link on the local side
 
     /**
      * The type of the Global Routing Link Record.  Defined in the OSPF spec.
@@ -242,8 +332,37 @@
  * combined with a list of Link Records.  Since it's global, there's
  * no need for age or sequence number.  See \RFC{2328}, Appendix A.
  */
+template <typename T>
 class GlobalRoutingLSA
 {
+    static_assert(std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+                  "T must be either Ipv4Manager or Ipv6Manager when calling GlobalRoutingLSA");
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
   public:
     /**
      * @enum LSType
@@ -288,7 +407,7 @@
      * @param linkStateId The Ipv4Address for the link state ID field.
      * @param advertisingRtr The Ipv4Address for the advertising router field.
      */
-    GlobalRoutingLSA(SPFStatus status, Ipv4Address linkStateId, Ipv4Address advertisingRtr);
+    GlobalRoutingLSA(SPFStatus status, IpAddress linkStateId, IpAddress advertisingRtr);
 
     /**
      * @brief Copy constructor for a Global Routing Link State Advertisement.
@@ -339,7 +458,7 @@
      * @param lr The Global Routing Link Record to be added.
      * @returns The number of link records in the list.
      */
-    uint32_t AddLinkRecord(GlobalRoutingLinkRecord* lr);
+    uint32_t AddLinkRecord(GlobalRoutingLinkRecord<T>* lr);
 
     /**
      * @brief Return the number of Global Routing Link Records in the LSA.
@@ -354,7 +473,7 @@
      * @param n The LSA number desired.
      * @returns The number of link records in the list.
      */
-    GlobalRoutingLinkRecord* GetLinkRecord(uint32_t n) const;
+    GlobalRoutingLinkRecord<T>* GetLinkRecord(uint32_t n) const;
 
     /**
      * @brief Release all of the Global Routing Link Records present in the Global
@@ -396,7 +515,7 @@
      * @see GlobalRouting::GetRouterId ()
      * @returns The Ipv4Address stored as the link state ID.
      */
-    Ipv4Address GetLinkStateId() const;
+    IpAddress GetLinkStateId() const;
 
     /**
      * @brief Set the Link State ID is defined by the OSPF spec.  We always set it
@@ -405,7 +524,7 @@
      * @see RoutingEnvironment::AllocateRouterId ()
      * @see GlobalRouting::GetRouterId ()
      */
-    void SetLinkStateId(Ipv4Address addr);
+    void SetLinkStateId(IpAddress addr);
 
     /**
      * @brief Get the Advertising Router as defined by the OSPF spec.  We always
@@ -415,7 +534,7 @@
      * @see GlobalRouting::GetRouterId ()
      * @returns The Ipv4Address stored as the advertising router.
      */
-    Ipv4Address GetAdvertisingRouter() const;
+    IpAddress GetAdvertisingRouter() const;
 
     /**
      * @brief Set the Advertising Router as defined by the OSPF spec.  We always
@@ -425,14 +544,14 @@
      * @see RoutingEnvironment::AllocateRouterId ()
      * @see GlobalRouting::GetRouterId ()
      */
-    void SetAdvertisingRouter(Ipv4Address rtr);
+    void SetAdvertisingRouter(IpAddress rtr);
 
     /**
      * @brief For a Network LSA, set the Network Mask field that precedes
      * the list of attached routers.
      * @param mask the Network Mask field.
      */
-    void SetNetworkLSANetworkMask(Ipv4Mask mask);
+    void SetNetworkLSANetworkMask(IpMaskOrPrefix mask);
 
     /**
      * @brief For a Network LSA, get the Network Mask field that precedes
@@ -440,7 +559,7 @@
      *
      * @returns the NetworkLSANetworkMask
      */
-    Ipv4Mask GetNetworkLSANetworkMask() const;
+    IpMaskOrPrefix GetNetworkLSANetworkMask() const;
 
     /**
      * @brief Add an attached router to the list in the NetworkLSA
@@ -448,7 +567,7 @@
      * @param addr The Ipv4Address of the interface on the network link
      * @returns The number of addresses in the list.
      */
-    uint32_t AddAttachedRouter(Ipv4Address addr);
+    uint32_t AddAttachedRouter(IpAddress addr);
 
     /**
      * @brief Return the number of attached routers listed in the NetworkLSA
@@ -463,7 +582,7 @@
      * @param n The attached router number desired (number in the list).
      * @returns The Ipv4Address of the requested router
      */
-    Ipv4Address GetAttachedRouter(uint32_t n) const;
+    IpAddress GetAttachedRouter(uint32_t n) const;
 
     /**
      * @brief Get the SPF status of the advertisement.
@@ -505,7 +624,7 @@
      * @see RoutingEnvironment::AllocateRouterId ()
      * @see GlobalRouting::GetRouterId ()
      */
-    Ipv4Address m_linkStateId;
+    IpAddress m_linkStateId;
 
     /**
      * The Advertising Router is defined by the OSPF spec.  We always set it to
@@ -514,12 +633,12 @@
      * @see RoutingEnvironment::AllocateRouterId ()
      * @see GlobalRouting::GetRouterId ()
      */
-    Ipv4Address m_advertisingRtr;
+    IpAddress m_advertisingRtr;
 
     /**
      * A convenience typedef to avoid too much writers cramp.
      */
-    typedef std::list<GlobalRoutingLinkRecord*> ListOfLinkRecords_t;
+    typedef std::list<GlobalRoutingLinkRecord<T>*> ListOfLinkRecords_t;
 
     /**
      * Each Link State Advertisement contains a number of Link Records that
@@ -536,12 +655,12 @@
     /**
      * Each Network LSA contains the network mask of the attached network
      */
-    Ipv4Mask m_networkLSANetworkMask;
+    IpMaskOrPrefix m_networkLSANetworkMask;
 
     /**
      * A convenience typedef to avoid too much writers cramp.
      */
-    typedef std::list<Ipv4Address> ListOfAttachedRouters_t;
+    typedef std::list<IpAddress> ListOfAttachedRouters_t;
 
     /**
      * Each Network LSA contains a list of attached routers
@@ -570,7 +689,8 @@
  * @param lsa the LSA
  * @returns the reference to the output stream
  */
-std::ostream& operator<<(std::ostream& os, GlobalRoutingLSA& lsa);
+template <typename T>
+std::ostream& operator<<(std::ostream& os, GlobalRoutingLSA<T>& lsa);
 
 /**
  * @brief An interface aggregated to a node to provide global routing info
@@ -581,8 +701,42 @@
  * advertises its connections to neighboring routers.  We're basically
  * allowing the route manager to query for link state advertisements.
  */
+template <typename T>
 class GlobalRouter : public Object
 {
+    static_assert(std::is_same_v<T, Ipv4Manager> || std::is_same_v<T, Ipv6Manager>,
+                  "T must be either Ipv4Manager or Ipv6Manager when calling GlobalRouter");
+
+    /// Alias for determining whether the parent is Ipv4RoutingProtocol or Ipv6RoutingProtocol
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4Manager, T>;
+
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
+    /// Alias for Ipv4RoutingProtocol and Ipv6RoutingProtocol classes
+    using IpRoutingProtocol =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingProtocol, Ipv6RoutingProtocol>;
+
   public:
     /**
      * @brief Get the type ID.
@@ -603,13 +757,13 @@
      * @brief Set the specific Global Routing Protocol to be used
      * @param routing the routing protocol
      */
-    void SetRoutingProtocol(Ptr<Ipv4GlobalRouting> routing);
+    void SetRoutingProtocol(Ptr<GlobalRouting<IpRoutingProtocol>> routing);
 
     /**
      * @brief Get the specific Global Routing Protocol used
      * @returns the routing protocol
      */
-    Ptr<Ipv4GlobalRouting> GetRoutingProtocol();
+    Ptr<GlobalRouting<IpRoutingProtocol>> GetRoutingProtocol();
 
     /**
      * @brief Get the Router ID associated with this Global Router.
@@ -620,7 +774,7 @@
      * @see RoutingEnvironment::AllocateRouterId ()
      * @returns The Router ID associated with the Global Router.
      */
-    Ipv4Address GetRouterId() const;
+    IpAddress GetRouterId() const;
 
     /**
      * @brief Walk the connected channels, discover the adjacent routers and build
@@ -676,7 +830,7 @@
      * @param lsa The GlobalRoutingLSA class to receive the LSA information.
      * @returns The number of Global Router Link State Advertisements.
      */
-    bool GetLSA(uint32_t n, GlobalRoutingLSA& lsa) const;
+    bool GetLSA(uint32_t n, GlobalRoutingLSA<T>& lsa) const;
 
     /**
      * @brief Inject a route to be circulated to other routers as an external
@@ -685,7 +839,7 @@
      * @param network The Network to inject
      * @param networkMask The Network Mask to inject
      */
-    void InjectRoute(Ipv4Address network, Ipv4Mask networkMask);
+    void InjectRoute(IpAddress network, IpMaskOrPrefix networkMask);
 
     /**
      * @brief Get the number of injected routes that have been added
@@ -700,7 +854,7 @@
      * @return a pointer to that Ipv4RoutingTableEntry is returned
      *
      */
-    Ipv4RoutingTableEntry* GetInjectedRoute(uint32_t i);
+    IpRoutingTableEntry* GetInjectedRoute(uint32_t i);
 
     /**
      * @brief Withdraw a route from the global unicast routing table.
@@ -724,7 +878,7 @@
      *
      * @see GlobalRouter::RemoveInjectedRoute ()
      */
-    bool WithdrawRoute(Ipv4Address network, Ipv4Mask networkMask);
+    bool WithdrawRoute(IpAddress network, IpMaskOrPrefix networkMask);
 
   private:
     ~GlobalRouter() override;
@@ -756,7 +910,7 @@
      * @param ndLocal local NetDevice to scan
      * @returns the IP address of the designated router
      */
-    Ipv4Address FindDesignatedRouterForLink(Ptr<NetDevice> ndLocal) const;
+    IpAddress FindDesignatedRouterForLink(Ptr<NetDevice> ndLocal) const;
 
     /**
      * @brief Checks for the presence of another router on the NetDevice
@@ -777,7 +931,7 @@
      * @param pLSA the Global LSA
      * @param c the returned NetDevice container
      */
-    void ProcessBroadcastLink(Ptr<NetDevice> nd, GlobalRoutingLSA* pLSA, NetDeviceContainer& c);
+    void ProcessBroadcastLink(Ptr<NetDevice> nd, GlobalRoutingLSA<T>* pLSA, NetDeviceContainer& c);
 
     /**
      * @brief Process a single broadcast link
@@ -787,7 +941,7 @@
      * @param c the returned NetDevice container
      */
     void ProcessSingleBroadcastLink(Ptr<NetDevice> nd,
-                                    GlobalRoutingLSA* pLSA,
+                                    GlobalRoutingLSA<T>* pLSA,
                                     NetDeviceContainer& c);
 
     /**
@@ -798,7 +952,7 @@
      * @param c the returned NetDevice container
      */
     void ProcessBridgedBroadcastLink(Ptr<NetDevice> nd,
-                                     GlobalRoutingLSA* pLSA,
+                                     GlobalRoutingLSA<T>* pLSA,
                                      NetDeviceContainer& c);
 
     /**
@@ -807,7 +961,7 @@
      * @param ndLocal the NetDevice
      * @param pLSA the Global LSA
      */
-    void ProcessPointToPointLink(Ptr<NetDevice> ndLocal, GlobalRoutingLSA* pLSA);
+    void ProcessPointToPointLink(Ptr<NetDevice> ndLocal, GlobalRoutingLSA<T>* pLSA);
 
     /**
      * @brief Build one NetworkLSA for each net device talking to a network that we are the
@@ -841,17 +995,16 @@
      */
     Ptr<BridgeNetDevice> NetDeviceIsBridged(Ptr<NetDevice> nd) const;
 
-    typedef std::list<GlobalRoutingLSA*> ListOfLSAs_t; //!< container for the GlobalRoutingLSAs
-    ListOfLSAs_t m_LSAs;                               //!< database of GlobalRoutingLSAs
+    typedef std::list<GlobalRoutingLSA<T>*> ListOfLSAs_t; //!< container for the GlobalRoutingLSAs
+    ListOfLSAs_t m_LSAs;                                  //!< database of GlobalRoutingLSAs
 
-    Ipv4Address m_routerId;                   //!< router ID (its IPv4 address)
-    Ptr<Ipv4GlobalRouting> m_routingProtocol; //!< the Ipv4GlobalRouting in use
+    IpAddress m_routerId;                                    //!< router ID (its IPv4 address)
+    Ptr<GlobalRouting<IpRoutingProtocol>> m_routingProtocol; //!< the Ipv4GlobalRouting in use
 
-    typedef std::list<Ipv4RoutingTableEntry*>
-        InjectedRoutes; //!< container of Ipv4RoutingTableEntry
-    typedef std::list<Ipv4RoutingTableEntry*>::const_iterator
+    typedef std::list<IpRoutingTableEntry*> InjectedRoutes; //!< container of Ipv4RoutingTableEntry
+    typedef std::list<IpRoutingTableEntry*>::const_iterator
         InjectedRoutesCI; //!< Const Iterator to container of Ipv4RoutingTableEntry
-    typedef std::list<Ipv4RoutingTableEntry*>::iterator
+    typedef std::list<IpRoutingTableEntry*>::iterator
         InjectedRoutesI;             //!< Iterator to container of Ipv4RoutingTableEntry
     InjectedRoutes m_injectedRoutes; //!< Routes we are exporting
 
@@ -885,6 +1038,12 @@
     void DoDispose() override;
 };
 
+/**
+ * @ingroup globalrouting
+ * Create the typedef Ipv4GlobalRouting with T as Ipv4RoutingProtocol
+ */
+typedef GlobalRouter<Ipv4Manager> Ipv4GlobalRouter;
+
 } // namespace ns3
 
 #endif /* GLOBAL_ROUTER_INTERFACE_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-routing.cc ns-3.47/src/internet/model/global-routing.cc
--- ns-3.46/src/internet/model/global-routing.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/model/global-routing.cc	2026-02-16 16:08:49.611230000 -0800
@@ -0,0 +1,795 @@
+//
+// Copyright (c) 2008 University of Washington
+//
+// SPDX-License-Identifier: GPL-2.0-only
+//
+
+#include "global-routing.h"
+
+#include "global-route-manager.h"
+#include "ipv4-route.h"
+#include "ipv4-routing-table-entry.h"
+#include "ipv6-route.h"
+
+#include "ns3/boolean.h"
+#include "ns3/log.h"
+#include "ns3/names.h"
+#include "ns3/net-device.h"
+#include "ns3/node.h"
+#include "ns3/object.h"
+#include "ns3/packet.h"
+#include "ns3/simulator.h"
+
+#include <iomanip>
+#include <vector>
+
+namespace ns3
+{
+
+NS_LOG_COMPONENT_DEFINE("GlobalRouting");
+
+template <typename T>
+TypeId
+GlobalRouting<T>::GetTypeId()
+{
+    if (IsIpv4)
+    {
+        static TypeId tid =
+            TypeId("ns3::Ipv4GlobalRouting")
+                .SetParent<T>()
+                .SetGroupName("Internet")
+                .template AddConstructor<GlobalRouting<T>>()
+                .AddAttribute(
+                    "RandomEcmpRouting",
+                    "Set to true if packets are randomly routed among ECMP; set to false for "
+                    "using only one route consistently",
+                    BooleanValue(false),
+                    MakeBooleanAccessor(&GlobalRouting<T>::m_randomEcmpRouting),
+                    MakeBooleanChecker())
+                .AddAttribute(
+                    "RespondToInterfaceEvents",
+                    "Set to true if you want to dynamically recompute the global routes upon "
+                    "Interface notification events (up/down, or add/remove address)",
+                    BooleanValue(false),
+                    MakeBooleanAccessor(&GlobalRouting<T>::m_respondToInterfaceEvents),
+                    MakeBooleanChecker());
+        return tid;
+    }
+    else
+    {
+        static TypeId tid =
+            TypeId("ns3::Ipv6GlobalRouting")
+                .SetParent<T>()
+                .SetGroupName("Internet")
+                .template AddConstructor<GlobalRouting<T>>()
+                .AddAttribute(
+                    "RandomEcmpRouting",
+                    "Set to true if packets are randomly routed among ECMP; set to false for "
+                    "using only one route consistently",
+                    BooleanValue(false),
+                    MakeBooleanAccessor(&GlobalRouting<T>::m_randomEcmpRouting),
+                    MakeBooleanChecker())
+                .AddAttribute(
+                    "RespondToInterfaceEvents",
+                    "Set to true if you want to dynamically recompute the global routes upon "
+                    "Interface notification events (up/down, or add/remove address)",
+                    BooleanValue(false),
+                    MakeBooleanAccessor(&GlobalRouting<T>::m_respondToInterfaceEvents),
+                    MakeBooleanChecker());
+        return tid;
+    }
+}
+
+template <typename T>
+GlobalRouting<T>::GlobalRouting()
+    : m_randomEcmpRouting(false),
+      m_respondToInterfaceEvents(false)
+{
+    NS_LOG_FUNCTION(this);
+
+    m_rand = CreateObject<UniformRandomVariable>();
+}
+
+template <typename T>
+GlobalRouting<T>::~GlobalRouting()
+{
+    NS_LOG_FUNCTION(this);
+}
+
+template <typename T>
+void
+GlobalRouting<T>::AddHostRouteTo(IpAddress dest, IpAddress nextHop, uint32_t interface)
+{
+    NS_LOG_FUNCTION(this << dest << nextHop << interface);
+    auto route = new IpRoutingTableEntry();
+    *route = IpRoutingTableEntry::CreateHostRouteTo(dest, nextHop, interface);
+    for (auto routePointer : m_hostRoutes)
+    {
+        if (*routePointer == *route)
+        {
+            NS_LOG_LOGIC("Route already exists");
+            delete route;
+            return;
+        }
+    }
+    m_hostRoutes.push_back(route);
+}
+
+template <typename T>
+void
+GlobalRouting<T>::AddHostRouteTo(IpAddress dest, uint32_t interface)
+{
+    NS_LOG_FUNCTION(this << dest << interface);
+    auto route = new IpRoutingTableEntry();
+    *route = IpRoutingTableEntry::CreateHostRouteTo(dest, interface);
+    for (auto routePointer : m_hostRoutes)
+    {
+        if (*routePointer == *route)
+        {
+            NS_LOG_LOGIC("Route already exists");
+            delete route;
+            return;
+        }
+    }
+    m_hostRoutes.push_back(route);
+}
+
+template <typename T>
+void
+GlobalRouting<T>::AddNetworkRouteTo(IpAddress network,
+                                    IpMaskOrPrefix networkMask,
+                                    IpAddress nextHop,
+                                    uint32_t interface)
+{
+    NS_LOG_FUNCTION(this << network << networkMask << nextHop << interface);
+    auto route = new IpRoutingTableEntry();
+    *route = IpRoutingTableEntry::CreateNetworkRouteTo(network, networkMask, nextHop, interface);
+    for (auto routePointer : m_networkRoutes)
+    {
+        if (*routePointer == *route)
+        {
+            NS_LOG_LOGIC("Route already exists");
+            delete route;
+            return;
+        }
+    }
+    m_networkRoutes.push_back(route);
+}
+
+template <typename T>
+void
+GlobalRouting<T>::AddNetworkRouteTo(IpAddress network,
+                                    IpMaskOrPrefix networkMask,
+                                    uint32_t interface)
+{
+    NS_LOG_FUNCTION(this << network << networkMask << interface);
+    auto route = new IpRoutingTableEntry();
+    *route = IpRoutingTableEntry::CreateNetworkRouteTo(network, networkMask, interface);
+    for (auto routePointer : m_networkRoutes)
+    {
+        if (*routePointer == *route)
+        {
+            NS_LOG_LOGIC("Route already exists");
+            delete route;
+            return;
+        }
+    }
+    m_networkRoutes.push_back(route);
+}
+
+template <typename T>
+void
+GlobalRouting<T>::AddASExternalRouteTo(IpAddress network,
+                                       IpMaskOrPrefix networkMask,
+                                       IpAddress nextHop,
+                                       uint32_t interface)
+{
+    NS_LOG_FUNCTION(this << network << networkMask << nextHop << interface);
+    auto route = new IpRoutingTableEntry();
+    *route = IpRoutingTableEntry::CreateNetworkRouteTo(network, networkMask, nextHop, interface);
+    for (auto routePointer : m_ASexternalRoutes)
+    {
+        if (*routePointer == *route)
+        {
+            NS_LOG_LOGIC("Route already exists");
+            delete route;
+            return;
+        }
+    }
+    m_ASexternalRoutes.push_back(route);
+}
+
+template <typename T>
+Ptr<typename GlobalRouting<T>::IpRoute>
+GlobalRouting<T>::LookupGlobal(IpAddress dest, Ptr<NetDevice> oif)
+{
+    NS_LOG_FUNCTION(this << dest << oif);
+    NS_LOG_LOGIC("Looking for route for destination " << dest);
+    Ptr<IpRoute> rtentry = nullptr;
+    // store all available routes that bring packets to their destination
+    typedef std::vector<IpRoutingTableEntry*> RouteVec_t;
+    RouteVec_t allRoutes;
+
+    NS_LOG_LOGIC("Number of m_hostRoutes = " << m_hostRoutes.size());
+    for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i++)
+    {
+        NS_ASSERT((*i)->IsHost());
+        if ((*i)->GetDest() == dest)
+        {
+            if (oif)
+            {
+                if (oif != m_ip->GetNetDevice((*i)->GetInterface()))
+                {
+                    NS_LOG_LOGIC("Not on requested interface, skipping");
+                    continue;
+                }
+            }
+            allRoutes.push_back(*i);
+            NS_LOG_LOGIC(allRoutes.size() << "Found global host route" << *i);
+        }
+    }
+    if (allRoutes.empty()) // if no host route is found
+    {
+        NS_LOG_LOGIC("Number of m_networkRoutes" << m_networkRoutes.size());
+        // store the length of the longest mask.
+        uint16_t longest_mask = 0;
+        for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j++)
+        {
+            IpMaskOrPrefix mask;
+            if constexpr (IsIpv4)
+            {
+                mask = (*j)->GetDestNetworkMask();
+            }
+            else
+            {
+                mask = (*j)->GetDestNetworkPrefix();
+            }
+            uint16_t masklen = mask.GetPrefixLength();
+
+            IpAddress entry = (*j)->GetDestNetwork();
+            if (mask.IsMatch(dest, entry))
+            {
+                if (oif)
+                {
+                    if (oif != m_ip->GetNetDevice((*j)->GetInterface()))
+                    {
+                        NS_LOG_LOGIC("Not on requested interface, skipping");
+                        continue;
+                    }
+                }
+                NS_LOG_LOGIC(allRoutes.size() << "Found global network route" << *j);
+                if (masklen < longest_mask) // Not interested if got shorter mask
+                {
+                    NS_LOG_LOGIC("Previous match longer, skipping");
+                    continue;
+                }
+                else if (masklen == longest_mask)
+                {
+                    NS_LOG_LOGIC("Equal mask length, adding this to the list");
+                    allRoutes.push_back(*j);
+                }
+                else
+                {
+                    NS_LOG_LOGIC("Longer mask length found, clearing the list and adding");
+                    allRoutes.clear();
+                    allRoutes.push_back(*j);
+                }
+            }
+        }
+    }
+    if (allRoutes.empty()) // consider external if no host/network found
+    {
+        for (auto k = m_ASexternalRoutes.begin(); k != m_ASexternalRoutes.end(); k++)
+        {
+            IpMaskOrPrefix mask;
+            if constexpr (IsIpv4)
+            {
+                mask = (*k)->GetDestNetworkMask();
+            }
+            else
+            {
+                mask = (*k)->GetDestNetworkPrefix();
+            }
+
+            IpAddress entry = (*k)->GetDestNetwork();
+            if (mask.IsMatch(dest, entry))
+            {
+                NS_LOG_LOGIC("Found external route" << *k);
+                if (oif)
+                {
+                    if (oif != m_ip->GetNetDevice((*k)->GetInterface()))
+                    {
+                        NS_LOG_LOGIC("Not on requested interface, skipping");
+                        continue;
+                    }
+                }
+                allRoutes.push_back(*k);
+                break;
+            }
+        }
+    }
+    if (!allRoutes.empty()) // if route(s) is found
+    {
+        // pick up one of the routes uniformly at random if random
+        // ECMP routing is enabled, or always select the first route
+        // consistently if random ECMP routing is disabled
+        uint32_t selectIndex;
+        if (m_randomEcmpRouting)
+        {
+            selectIndex = m_rand->GetInteger(0, allRoutes.size() - 1);
+        }
+        else
+        {
+            selectIndex = 0;
+        }
+        IpRoutingTableEntry* route = allRoutes.at(selectIndex);
+        // create a Ipv4Route object from the selected routing table entry
+        rtentry = Create<IpRoute>();
+        rtentry->SetDestination(route->GetDest());
+        /// @todo handle multi-address case
+        if constexpr (IsIpv4)
+        {
+            rtentry->SetSource(m_ip->GetAddress(route->GetInterface(), 0).GetAddress());
+        }
+        else
+        {
+            rtentry->SetSource(m_ip->GetAddress(route->GetInterface(), 1).GetAddress());
+        }
+        rtentry->SetGateway(route->GetGateway());
+        uint32_t interfaceIdx = route->GetInterface();
+        rtentry->SetOutputDevice(m_ip->GetNetDevice(interfaceIdx));
+        return rtentry;
+    }
+    else
+    {
+        return nullptr;
+    }
+}
+
+template <typename T>
+uint32_t
+GlobalRouting<T>::GetNRoutes() const
+{
+    NS_LOG_FUNCTION(this);
+    uint32_t n = 0;
+    n += m_hostRoutes.size();
+    n += m_networkRoutes.size();
+    n += m_ASexternalRoutes.size();
+    return n;
+}
+
+template <typename T>
+GlobalRouting<T>::IpRoutingTableEntry*
+GlobalRouting<T>::GetRoute(uint32_t index) const
+{
+    NS_LOG_FUNCTION(this << index);
+    if (index < m_hostRoutes.size())
+    {
+        uint32_t tmp = 0;
+        for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i++)
+        {
+            if (tmp == index)
+            {
+                return *i;
+            }
+            tmp++;
+        }
+    }
+    index -= m_hostRoutes.size();
+    uint32_t tmp = 0;
+    if (index < m_networkRoutes.size())
+    {
+        for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j++)
+        {
+            if (tmp == index)
+            {
+                return *j;
+            }
+            tmp++;
+        }
+    }
+    index -= m_networkRoutes.size();
+    tmp = 0;
+    for (auto k = m_ASexternalRoutes.begin(); k != m_ASexternalRoutes.end(); k++)
+    {
+        if (tmp == index)
+        {
+            return *k;
+        }
+        tmp++;
+    }
+    NS_ASSERT(false);
+    // quiet compiler.
+    return nullptr;
+}
+
+template <typename T>
+void
+GlobalRouting<T>::RemoveRoute(uint32_t index)
+{
+    NS_LOG_FUNCTION(this << index);
+    if (index < m_hostRoutes.size())
+    {
+        uint32_t tmp = 0;
+        for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i++)
+        {
+            if (tmp == index)
+            {
+                NS_LOG_LOGIC("Removing route " << index << "; size = " << m_hostRoutes.size());
+                delete *i;
+                m_hostRoutes.erase(i);
+                NS_LOG_LOGIC("Done removing host route "
+                             << index << "; host route remaining size = " << m_hostRoutes.size());
+                return;
+            }
+            tmp++;
+        }
+    }
+    index -= m_hostRoutes.size();
+    uint32_t tmp = 0;
+    for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j++)
+    {
+        if (tmp == index)
+        {
+            NS_LOG_LOGIC("Removing route " << index << "; size = " << m_networkRoutes.size());
+            delete *j;
+            m_networkRoutes.erase(j);
+            NS_LOG_LOGIC("Done removing network route "
+                         << index << "; network route remaining size = " << m_networkRoutes.size());
+            return;
+        }
+        tmp++;
+    }
+    index -= m_networkRoutes.size();
+    tmp = 0;
+    for (auto k = m_ASexternalRoutes.begin(); k != m_ASexternalRoutes.end(); k++)
+    {
+        if (tmp == index)
+        {
+            NS_LOG_LOGIC("Removing route " << index << "; size = " << m_ASexternalRoutes.size());
+            delete *k;
+            m_ASexternalRoutes.erase(k);
+            NS_LOG_LOGIC("Done removing network route "
+                         << index << "; network route remaining size = " << m_networkRoutes.size());
+            return;
+        }
+        tmp++;
+    }
+    NS_ASSERT(false);
+}
+
+template <typename T>
+int64_t
+GlobalRouting<T>::AssignStreams(int64_t stream)
+{
+    NS_LOG_FUNCTION(this << stream);
+    m_rand->SetStream(stream);
+    return 1;
+}
+
+template <typename T>
+void
+GlobalRouting<T>::DoDispose()
+{
+    NS_LOG_FUNCTION(this);
+    for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i = m_hostRoutes.erase(i))
+    {
+        delete (*i);
+    }
+    for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j = m_networkRoutes.erase(j))
+    {
+        delete (*j);
+    }
+    for (auto l = m_ASexternalRoutes.begin(); l != m_ASexternalRoutes.end();
+         l = m_ASexternalRoutes.erase(l))
+    {
+        delete (*l);
+    }
+
+    IpRoutingProtocol::DoDispose();
+}
+
+// Formatted like output of "route -n" command
+template <typename T>
+void
+GlobalRouting<T>::PrintRoutingTable(Ptr<OutputStreamWrapper> stream, Time::Unit unit) const
+{
+    NS_LOG_FUNCTION(this << stream);
+    std::ostream* os = stream->GetStream();
+    // Copy the current ostream state
+    std::ios oldState(nullptr);
+    oldState.copyfmt(*os);
+
+    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
+
+    std::string name;
+    std::string header;
+    if constexpr (IsIpv4)
+    {
+        name = "Ipv4";
+        header = "Destination     Gateway         Genmask         Flags Metric Ref    Use Iface";
+    }
+    else
+    {
+        name = "Ipv6";
+        header = "Destination                    Next Hop                   Flag Met Ref Use Iface";
+    }
+
+    *os << "Node: " << m_ip->template GetObject<Node>()->GetId() << ", Time: " << Now().As(unit)
+        << ", Local time: " << m_ip->template GetObject<Node>()->GetLocalTime().As(unit) << ", "
+        << name << "GlobalRouting table" << std::endl
+        << header << std::endl;
+
+    if (GetNRoutes() > 0)
+    {
+        for (uint32_t j = 0; j < GetNRoutes(); j++)
+        {
+            std::ostringstream dest;
+            std::ostringstream gw;
+            std::ostringstream mask;
+            std::ostringstream flags;
+            IpRoutingTableEntry route = GetRoute(j);
+            if constexpr (IsIpv4)
+            {
+                dest << route.GetDest();
+                *os << std::setw(16) << dest.str();
+                gw << route.GetGateway();
+                *os << std::setw(16) << gw.str();
+                mask << route.GetDestNetworkMask();
+                *os << std::setw(16) << mask.str();
+            }
+            else
+            {
+                dest << route.GetDest() << "/"
+                     << int(route.GetDestNetworkPrefix().GetPrefixLength());
+                *os << std::setw(31) << dest.str();
+                gw << route.GetGateway();
+                *os << std::setw(27) << gw.str();
+            }
+            flags << "U";
+            if (route.IsHost())
+            {
+                flags << "H";
+            }
+            else if (route.IsGateway())
+            {
+                flags << "G";
+            }
+            *os << std::setw(6) << flags.str();
+            // Metric not implemented
+            *os << "-"
+                << "   ";
+            // Ref ct not implemented
+            *os << "-"
+                << "   ";
+            // Use not implemented
+            *os << "-"
+                << "   ";
+            if (!Names::FindName(m_ip->GetNetDevice(route.GetInterface())).empty())
+            {
+                *os << Names::FindName(m_ip->GetNetDevice(route.GetInterface()));
+            }
+            else
+            {
+                *os << route.GetInterface();
+            }
+            *os << std::endl;
+        }
+    }
+    *os << std::endl;
+    // Restore the previous ostream state
+    (*os).copyfmt(oldState);
+}
+
+template <typename T>
+Ptr<typename GlobalRouting<T>::IpRoute>
+GlobalRouting<T>::RouteOutput(Ptr<Packet> p,
+                              const IpHeader& header,
+                              Ptr<NetDevice> oif,
+                              Socket::SocketErrno& sockerr)
+{
+    NS_LOG_FUNCTION(this << p << &header << oif << &sockerr);
+    //
+    // First, see if this is a multicast packet we have a route for.  If we
+    // have a route, then send the packet down each of the specified interfaces.
+    //
+    if (header.GetDestination().IsMulticast())
+    {
+        NS_LOG_LOGIC("Multicast destination-- returning false");
+        return nullptr; // Let other routing protocols try to handle this
+    }
+    //
+    // See if this is a unicast packet we have a route for.
+    //
+    NS_LOG_LOGIC("Unicast destination- looking up");
+    Ptr<IpRoute> rtentry = LookupGlobal(header.GetDestination(), oif);
+    if (rtentry)
+    {
+        sockerr = Socket::ERROR_NOTERROR;
+    }
+    else
+    {
+        sockerr = Socket::ERROR_NOROUTETOHOST;
+    }
+    return rtentry;
+}
+
+template <typename T>
+bool
+GlobalRouting<T>::RouteInput(Ptr<const Packet> p,
+                             const IpHeader& header,
+                             Ptr<const NetDevice> idev,
+                             const UnicastForwardCallback& ucb,
+                             const MulticastForwardCallback& mcb,
+                             const LocalDeliverCallback& lcb,
+                             const ErrorCallback& ecb)
+{
+    NS_LOG_FUNCTION(this << p << header << header.GetSource() << header.GetDestination() << idev
+                         << &lcb << &ecb);
+    // Check if input device supports IP
+    NS_ASSERT(m_ip->GetInterfaceForDevice(idev) >= 0);
+    uint32_t iif = m_ip->GetInterfaceForDevice(idev);
+    if constexpr (IsIpv4)
+    {
+        if (m_ip->IsDestinationAddress(header.GetDestination(), iif))
+        {
+            if (!lcb.IsNull())
+            {
+                NS_LOG_LOGIC("Local delivery to " << header.GetDestination());
+                lcb(p, header, iif);
+                return true;
+            }
+            else
+            {
+                // The local delivery callback is null.  This may be a multicast
+                // or broadcast packet, so return false so that another
+                // multicast routing protocol can handle it.  It should be possible
+                // to extend this to explicitly check whether it is a unicast
+                // packet, and invoke the error callback if so
+                return false;
+            }
+        }
+    }
+    else
+    {
+        // for ipv6 the local delivery callback is never called. Local delivery is already handled
+        // in Ipv6l3Protocol::Receive() for now do nothing
+    }
+    // Check if input device supports IP forwarding
+    if (!m_ip->IsForwarding(iif))
+    {
+        NS_LOG_LOGIC("Forwarding disabled for this interface");
+        ecb(p, header, Socket::ERROR_NOROUTETOHOST);
+        return true;
+    }
+    // Next, try to find a route
+    NS_LOG_LOGIC("Unicast destination- looking up global route");
+    Ptr<IpRoute> rtentry = LookupGlobal(header.GetDestination());
+    if (rtentry)
+    {
+        NS_LOG_LOGIC("Found unicast destination- calling unicast callback");
+        if constexpr (IsIpv4)
+        {
+            ucb(rtentry, p, header);
+        }
+        else
+        {
+            ucb(idev, rtentry, p, header);
+        }
+        return true;
+    }
+    else
+    {
+        NS_LOG_LOGIC("Did not find unicast destination- returning false");
+        return false; // Let other routing protocols try to handle this
+                      // route request.
+    }
+}
+
+template <typename T>
+void
+GlobalRouting<T>::NotifyInterfaceUp(uint32_t i)
+{
+    NS_LOG_FUNCTION(this << i);
+    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
+    {
+        GlobalRouteManager<IpManager>::DeleteGlobalRoutes();
+        GlobalRouteManager<IpManager>::BuildGlobalRoutingDatabase();
+        GlobalRouteManager<IpManager>::InitializeRoutes();
+    }
+}
+
+template <typename T>
+void
+GlobalRouting<T>::NotifyInterfaceDown(uint32_t i)
+{
+    NS_LOG_FUNCTION(this << i);
+    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
+    {
+        GlobalRouteManager<IpManager>::DeleteGlobalRoutes();
+        GlobalRouteManager<IpManager>::BuildGlobalRoutingDatabase();
+        GlobalRouteManager<IpManager>::InitializeRoutes();
+    }
+}
+
+template <typename T>
+void
+GlobalRouting<T>::NotifyAddAddress(uint32_t interface, IpInterfaceAddress address)
+{
+    NS_LOG_FUNCTION(this << interface << address);
+    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
+    {
+        GlobalRouteManager<IpManager>::DeleteGlobalRoutes();
+        GlobalRouteManager<IpManager>::BuildGlobalRoutingDatabase();
+        GlobalRouteManager<IpManager>::InitializeRoutes();
+    }
+}
+
+template <typename T>
+void
+GlobalRouting<T>::NotifyRemoveAddress(uint32_t interface, IpInterfaceAddress address)
+{
+    NS_LOG_FUNCTION(this << interface << address);
+    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
+    {
+        GlobalRouteManager<IpManager>::DeleteGlobalRoutes();
+        GlobalRouteManager<IpManager>::BuildGlobalRoutingDatabase();
+        GlobalRouteManager<IpManager>::InitializeRoutes();
+    }
+}
+
+template <typename T>
+void
+GlobalRouting<T>::NotifyAddRoute(Ipv6Address dst,
+                                 Ipv6Prefix mask,
+                                 Ipv6Address nextHop,
+                                 uint32_t interface,
+                                 Ipv6Address prefixToUse)
+{
+}
+
+template <typename T>
+void
+GlobalRouting<T>::NotifyRemoveRoute(Ipv6Address dst,
+                                    Ipv6Prefix prefix,
+                                    Ipv6Address nextHop,
+                                    uint32_t ifIndex,
+                                    Ipv6Address origin)
+{
+    // no-op or real behavior
+}
+
+template <typename T>
+void
+GlobalRouting<T>::SetIpv4(Ptr<Ip> ipv4)
+{
+    NS_LOG_FUNCTION(this << ipv4);
+    NS_ASSERT(!m_ip && ipv4);
+
+    m_ip = ipv4;
+}
+
+template <typename T>
+void
+GlobalRouting<T>::SetIpv6(Ptr<Ip> ipv6)
+{
+    NS_LOG_FUNCTION(this << ipv6);
+    NS_ASSERT(!m_ip && ipv6);
+    m_ip = ipv6;
+}
+
+// template <typename T>
+// void
+// GlobalRouting<T>::InitializeRouters()
+//{
+//     NS_LOG_FUNCTION(this);
+//       for (uint32_t i = 0; i < m_ip->GetNInterfaces(); i++)
+//     {
+//         m_ip->SetForwarding(i, true);
+//     }
+// }
+
+NS_OBJECT_TEMPLATE_CLASS_DEFINE(GlobalRouting, Ipv4RoutingProtocol);
+NS_OBJECT_TEMPLATE_CLASS_DEFINE(GlobalRouting, Ipv6RoutingProtocol);
+
+} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/global-routing.h ns-3.47/src/internet/model/global-routing.h
--- ns-3.46/src/internet/model/global-routing.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/global-routing.h	2026-02-16 16:08:49.611230000 -0800
@@ -74,3 +74,487 @@
  * ported from the quagga routing suite (https://www.nongnu.org/quagga/).
  *
  */
+
+#ifndef GLOBAL_ROUTING_H
+#define GLOBAL_ROUTING_H
+
+#include "global-route-manager.h"
+#include "ipv4-header.h"
+#include "ipv4-routing-protocol.h"
+#include "ipv4.h"
+#include "ipv6-interface-address.h"
+#include "ipv6-routing-protocol.h"
+#include "ipv6-routing-table-entry.h"
+
+#include "ns3/ipv4-address.h"
+#include "ns3/ipv6-address.h"
+#include "ns3/ptr.h"
+#include "ns3/random-variable-stream.h"
+
+#include <list>
+#include <stdint.h>
+
+namespace ns3
+{
+
+class Packet;
+class NetDevice;
+class Ipv4Interface;
+class Ipv4Address;
+class Ipv4Header;
+class Ipv4RoutingTableEntry;
+class Ipv4MulticastRoutingTableEntry;
+class Node;
+template <typename>
+class GlobalRouteManagerImpl;
+
+/**
+ * @ingroup ipv4
+ *
+ * @brief Global routing protocol for IPv4 stacks.
+ *
+ * In ns-3 we have the concept of a pluggable routing protocol.  Routing
+ * protocols are added to a list maintained by the Ipv4L3Protocol.  Every
+ * stack gets one routing protocol for free -- the Ipv4StaticRouting routing
+ * protocol is added in the constructor of the Ipv4L3Protocol (this is the
+ * piece of code that implements the functionality of the IP layer).
+ *
+ * As an option to running a dynamic routing protocol, a GlobalRouteManager
+ * object has been created to allow users to build routes for all participating
+ * nodes.  One can think of this object as a "routing oracle"; it has
+ * an omniscient view of the topology, and can construct shortest path
+ * routes between all pairs of nodes.  These routes must be stored
+ * somewhere in the node, so therefore this class Ipv4GlobalRouting
+ * is used as one of the pluggable routing protocols.  It is kept distinct
+ * from Ipv4StaticRouting because these routes may be dynamically cleared
+ * and rebuilt in the middle of the simulation, while manually entered
+ * routes into the Ipv4StaticRouting may need to be kept distinct.
+ *
+ * This class deals with Ipv4 unicast routes only.
+ *
+ * @see Ipv4RoutingProtocol
+ * @see GlobalRouteManager
+ */
+template <typename T>
+class GlobalRouting : public std::enable_if_t<std::is_same_v<Ipv4RoutingProtocol, T> ||
+                                                  std::is_same_v<Ipv6RoutingProtocol, T>,
+                                              T>
+{
+    template <typename>
+    friend class GlobalRouteManagerImpl;
+
+    /// Alias for determining whether the parent is Ipv4RoutingHelper or Ipv6RoutingHelper
+    static constexpr bool IsIpv4 = std::is_same_v<Ipv4RoutingProtocol, T>;
+    /// Alias for Ipv4 and Ipv6 classes
+    using Ip = typename std::conditional_t<IsIpv4, Ipv4, Ipv6>;
+    /// Alias for Ipv4Address and Ipv6Address classes
+    using IpAddress = typename std::conditional_t<IsIpv4, Ipv4Address, Ipv6Address>;
+    /// Alias for Ipv4RoutingProtocol and Ipv6RoutingProtocol classes
+    using IpRoutingProtocol =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingProtocol, Ipv6RoutingProtocol>;
+
+    /// Alias for Ipv4Header and Ipv6Header classes
+    using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
+
+    /// Alias for Ipv4Route and Ipv6Route classes
+    using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
+
+    /// Alias for Ipv4Mask And Ipv6Prefix
+    using IpMaskOrPrefix = typename std::conditional_t<IsIpv4, Ipv4Mask, Ipv6Prefix>;
+
+    /// Alias for Ipv4RoutingTableEntry and Ipv6RoutingTableEntry classes
+    using IpRoutingTableEntry =
+        typename std::conditional_t<IsIpv4, Ipv4RoutingTableEntry, Ipv6RoutingTableEntry>;
+
+    /// Alias for Ipv4Manager and Ipv6Manager classes
+    using IpManager = typename std::conditional_t<IsIpv4, Ipv4Manager, Ipv6Manager>;
+
+    /// Alias for Ipv4InterfaceAddress and Ipv6InterfaceAddress classes
+    using IpInterfaceAddress =
+        typename std::conditional_t<IsIpv4, Ipv4InterfaceAddress, Ipv6InterfaceAddress>;
+
+  public:
+    /**
+     * @brief Get the type ID.
+     * @return the object TypeId
+     */
+    static TypeId GetTypeId();
+    /**
+     * @brief Construct an empty Ipv4GlobalRouting routing protocol,
+     *
+     * The Ipv4GlobalRouting class supports host and network unicast routes.
+     * This method initializes the lists containing these routes to empty.
+     *
+     * @see Ipv4GlobalRouting
+     */
+    GlobalRouting();
+    ~GlobalRouting();
+
+    // These methods inherited from base class
+
+    /* From Ipv4RoutingProtocol and Ipv6RoutingProtocol */
+
+    /**
+     * @brief Query routing cache for an existing route, for an outbound packet
+     * @param p packet to be routed.  Note that this method may modify the packet.
+     *          Callers may also pass in a null pointer.
+     * @param header input parameter (used to form key to search for the route)
+     * @param oif Output interface Netdevice.  May be zero, or may be bound via
+     *            socket options to a particular output interface.
+     * @param sockerr Output parameter; socket errno
+     *
+     * @returns a code that indicates what happened in the lookup
+     *
+     * @sa Ipv4RoutingProtocol::RouteOutput
+     * @sa Ipv6RoutingProtocol::RouteOutput
+     */
+    Ptr<IpRoute> RouteOutput(Ptr<Packet> p,
+                             const IpHeader& header,
+                             Ptr<NetDevice> oif,
+                             Socket::SocketErrno& sockerr);
+
+    /// Callback for IPv4 unicast packets to be forwarded
+    typedef Callback<void, Ptr<IpRoute>, Ptr<const Packet>, const IpHeader&>
+        UnicastForwardCallbackv4;
+
+    /// Callback for IPv6 unicast packets to be forwarded
+    typedef Callback<void, Ptr<const NetDevice>, Ptr<IpRoute>, Ptr<const Packet>, const IpHeader&>
+        UnicastForwardCallbackv6;
+
+    /// Callback for unicast packets to be forwarded
+    typedef typename std::conditional_t<IsIpv4, UnicastForwardCallbackv4, UnicastForwardCallbackv6>
+        UnicastForwardCallback;
+
+    /// Callback for IPv4 multicast packets to be forwarded
+    typedef Callback<void, Ptr<Ipv4MulticastRoute>, Ptr<const Packet>, const IpHeader&>
+        MulticastForwardCallbackv4;
+
+    /// Callback for IPv6 multicast packets to be forwarded
+    typedef Callback<void,
+                     Ptr<const NetDevice>,
+                     Ptr<Ipv6MulticastRoute>,
+                     Ptr<const Packet>,
+                     const IpHeader&>
+        MulticastForwardCallbackv6;
+
+    /// Callback for multicast packets to be forwarded
+    typedef
+        typename std::conditional_t<IsIpv4, MulticastForwardCallbackv4, MulticastForwardCallbackv6>
+            MulticastForwardCallback;
+
+    /// Callback for packets to be locally delivered
+    typedef Callback<void, Ptr<const Packet>, const IpHeader&, uint32_t> LocalDeliverCallback;
+
+    /// Callback for routing errors (e.g., no route found)
+    typedef Callback<void, Ptr<const Packet>, const IpHeader&, Socket::SocketErrno> ErrorCallback;
+
+    /**
+     * @brief Route an input packet (to be forwarded or locally delivered)
+     * @param p received packet
+     * @param header input parameter used to form a search key for a route
+     * @param idev Pointer to ingress network device
+     * @param ucb Callback for the case in which the packet is to be forwarded
+     *            as unicast
+     * @param mcb Callback for the case in which the packet is to be forwarded
+     *            as multicast
+     * @param lcb Callback for the case in which the packet is to be locally
+     *            delivered
+     * @param ecb Callback to call if there is an error in forwarding
+     *
+     * @returns true if GlobalRouting class takes responsibility for
+     *          forwarding or delivering the packet, false otherwise
+     *
+     * @sa Ipv4RoutingProtocol::RouteInput
+     * @sa Ipv6RoutingProtocol::RouteInput
+     */
+    bool RouteInput(Ptr<const Packet> p,
+                    const IpHeader& header,
+                    Ptr<const NetDevice> idev,
+                    const UnicastForwardCallback& ucb,
+                    const MulticastForwardCallback& mcb,
+                    const LocalDeliverCallback& lcb,
+                    const ErrorCallback& ecb);
+
+    /**
+     * @param interface the index of the interface we are being notified about
+     *
+     * @sa Ipv4RoutingProtocol::NotifyInterfaceUp
+     * @sa Ipv6RoutingProtocol::NotifyInterfaceUp
+     */
+    void NotifyInterfaceUp(uint32_t interface);
+    /**
+     * @param interface the index of the interface we are being notified about
+     *
+     * @sa Ipv4RoutingProtocol::NotifyInterfaceDown
+     * @sa Ipv6RoutingProtocol::NotifyInterfaceDown
+     */
+    void NotifyInterfaceDown(uint32_t interface);
+    /**
+     * @param interface the index of the interface we are being notified about
+     * @param address a new address being added to an interface
+     *
+     * @sa Ipv4RoutingProtocol::NotifyAddAddress
+     * @sa Ipv6RoutingProtocol::NotifyAddAddress
+     */
+    void NotifyAddAddress(uint32_t interface, IpInterfaceAddress address);
+    /**
+     * @param interface the index of the interface we are being notified about
+     * @param address a new address being added to an interface
+     *
+     * @sa Ipv4RoutingProtocol::NotifyRemoveAddress
+     * @sa Ipv6RoutingProtocol::NotifyRemoveAddress
+     */
+    void NotifyRemoveAddress(uint32_t interface, IpInterfaceAddress address);
+
+    /**
+     * @brief Notify a new route.
+     *
+     * Typically this is used to add another route from IPv6 stack (i.e. ICMPv6
+     * redirect case, ...).
+     * @param dst destination address
+     * @param mask destination mask
+     * @param nextHop nextHop for this destination
+     * @param interface output interface
+     * @param prefixToUse prefix to use as source with this route
+     */
+    virtual void NotifyAddRoute(Ipv6Address dst,
+                                Ipv6Prefix mask,
+                                Ipv6Address nextHop,
+                                uint32_t interface,
+                                Ipv6Address prefixToUse = Ipv6Address::GetZero());
+    /**
+     * @brief Notify route removing.
+     * @param dst destination address
+     * @param mask destination mask
+     * @param nextHop nextHop for this destination
+     * @param interface output interface
+     * @param prefixToUse prefix to use as source with this route
+     */
+    virtual void NotifyRemoveRoute(Ipv6Address dst,
+                                   Ipv6Prefix mask,
+                                   Ipv6Address nextHop,
+                                   uint32_t interface,
+                                   Ipv6Address prefixToUse = Ipv6Address::GetZero());
+
+    /* From IPv4RoutingProtocol */
+    /**
+     * @brief Typically, invoked directly or indirectly from ns3::Ipv4::SetRoutingProtocol
+     *
+     * @param ipv4 the ipv4 object this routing protocol is being associated with
+     *
+     * @sa Ipv4RoutingProtocol::SetIpv4
+     */
+    virtual void SetIpv4(Ptr<Ip> ipv4);
+
+    /* From IPv6RoutingProtocol */
+    /**
+     * @brief Typically, invoked directly or indirectly from ns3::Ipv6::SetRoutingProtocol
+     *
+     * @param ipv6 the ipv6 object this routing protocol is being associated with
+     *
+     * @sa Ipv6RoutingProtocol::SetIpv6
+     */
+    virtual void SetIpv6(Ptr<Ip> ipv6);
+
+    /**
+     * @brief Print the Routing Table entries
+     *
+     * @param stream The ostream the Routing table is printed to
+     * @param unit The time unit to be used in the report
+     *
+     * @sa Ipv4RoutingProtocol::PrintRoutingTable
+     * @sa Ipv6RoutingProtocol::PrintRoutingTable
+     */
+    void PrintRoutingTable(Ptr<OutputStreamWrapper> stream, Time::Unit unit = Time::S) const;
+
+    /**
+     * @brief Add a host route to the global routing table.
+     *
+     * @param dest The Ipv4Address destination for this route.
+     * @param nextHop The Ipv4Address of the next hop in the route.
+     * @param interface The network interface index used to send packets to the
+     * destination.
+     *
+     * @see Ipv4Address
+     */
+    void AddHostRouteTo(IpAddress dest, IpAddress nextHop, uint32_t interface);
+    /**
+     * @brief Add a host route to the global routing table.
+     *
+     * @param dest The Ipv4Address destination for this route.
+     * @param interface The network interface index used to send packets to the
+     * destination.
+     *
+     * @see Ipv4Address
+     */
+    void AddHostRouteTo(IpAddress dest, uint32_t interface);
+
+    /**
+     * @brief Add a network route to the global routing table.
+     *
+     * @param network The Ipv4Address network for this route.
+     * @param networkMask The Ipv4Mask to extract the network.
+     * @param nextHop The next hop in the route to the destination network.
+     * @param interface The network interface index used to send packets to the
+     * destination.
+     *
+     * @see Ipv4Address
+     */
+    void AddNetworkRouteTo(IpAddress network,
+                           IpMaskOrPrefix networkMask,
+                           IpAddress nextHop,
+                           uint32_t interface);
+
+    /**
+     * @brief Add a network route to the global routing table.
+     *
+     * @param network The Ipv4Address network for this route.
+     * @param networkMask The Ipv4Mask to extract the network.
+     * @param interface The network interface index used to send packets to the
+     * destination.
+     *
+     * @see Ipv4Address
+     */
+    void AddNetworkRouteTo(IpAddress network, IpMaskOrPrefix networkMask, uint32_t interface);
+
+    /**
+     * @brief Add an external route to the global routing table.
+     *
+     * @param network The Ipv4Address network for this route.
+     * @param networkMask The Ipv4Mask to extract the network.
+     * @param nextHop The next hop Ipv4Address
+     * @param interface The network interface index used to send packets to the
+     * destination.
+     */
+    void AddASExternalRouteTo(IpAddress network,
+                              IpMaskOrPrefix networkMask,
+                              IpAddress nextHop,
+                              uint32_t interface);
+
+    /**
+     * @brief Get the number of individual unicast routes that have been added
+     * to the routing table.
+     *
+     * @warning The default route counts as one of the routes.
+     * @returns the number of routes
+     */
+    uint32_t GetNRoutes() const;
+
+    /**
+     * @brief Get a route from the global unicast routing table.
+     *
+     * Externally, the unicast global routing table appears simply as a table with
+     * n entries.  The one subtlety of note is that if a default route has been set
+     * it will appear as the zeroth entry in the table.  This means that if you
+     * add only a default route, the table will have one entry that can be accessed
+     * either by explicitly calling GetDefaultRoute () or by calling GetRoute (0).
+     *
+     * Similarly, if the default route has been set, calling RemoveRoute (0) will
+     * remove the default route.
+     *
+     * @param i The index (into the routing table) of the route to retrieve.  If
+     * the default route has been set, it will occupy index zero.
+     * @return If route is set, a pointer to that Ipv4RoutingTableEntry is returned, otherwise
+     * a zero pointer is returned.
+     *
+     * @see Ipv4RoutingTableEntry
+     * @see Ipv4GlobalRouting::RemoveRoute
+     */
+    IpRoutingTableEntry* GetRoute(uint32_t i) const;
+
+    /**
+     * @brief Remove a route from the global unicast routing table.
+     *
+     * Externally, the unicast global routing table appears simply as a table with
+     * n entries.  The one subtlety of note is that if a default route has been set
+     * it will appear as the zeroth entry in the table.  This means that if the
+     * default route has been set, calling RemoveRoute (0) will remove the
+     * default route.
+     *
+     * @param i The index (into the routing table) of the route to remove.  If
+     * the default route has been set, it will occupy index zero.
+     *
+     * @see Ipv4RoutingTableEntry
+     * @see Ipv4GlobalRouting::GetRoute
+     * @see Ipv4GlobalRouting::AddRoute
+     */
+    void RemoveRoute(uint32_t i);
+
+    /**
+     * Assign a fixed random variable stream number to the random variables
+     * used by this model.  Return the number of streams (possibly zero) that
+     * have been assigned.
+     *
+     * @param stream first stream index to use
+     * @return the number of stream indices assigned by this model
+     */
+    int64_t AssignStreams(int64_t stream);
+
+  protected:
+    /**
+     * @sa Ipv4RoutingProtocol::DoDispose
+     * @sa Ipv6RoutingProtocol::DoDispose
+     */
+    void DoDispose();
+
+  private:
+    /// Set to true if packets are randomly routed among ECMP; set to false for using only one route
+    /// consistently
+    bool m_randomEcmpRouting;
+    /// Set to true if this interface should respond to interface events by globally recomputing
+    /// routes
+    bool m_respondToInterfaceEvents;
+    /// A uniform random number generator for randomly routing packets among ECMP
+    Ptr<UniformRandomVariable> m_rand;
+
+    /// container of Ipv4RoutingTableEntry (routes to hosts)
+    typedef std::list<IpRoutingTableEntry*> HostRoutes;
+    /// const iterator of container of Ipv4RoutingTableEntry (routes to hosts)
+    typedef std::list<IpRoutingTableEntry*>::const_iterator HostRoutesCI;
+    /// iterator of container of Ipv4RoutingTableEntry (routes to hosts)
+    typedef std::list<IpRoutingTableEntry*>::iterator HostRoutesI;
+
+    /// container of Ipv4RoutingTableEntry (routes to networks)
+    typedef std::list<IpRoutingTableEntry*> NetworkRoutes;
+    /// const iterator of container of Ipv4RoutingTableEntry (routes to networks)
+    typedef std::list<IpRoutingTableEntry*>::const_iterator NetworkRoutesCI;
+    /// iterator of container of Ipv4RoutingTableEntry (routes to networks)
+    typedef std::list<IpRoutingTableEntry*>::iterator NetworkRoutesI;
+
+    /// container of Ipv4RoutingTableEntry (routes to external AS)
+    typedef std::list<IpRoutingTableEntry*> ASExternalRoutes;
+    /// const iterator of container of Ipv4RoutingTableEntry (routes to external AS)
+    typedef std::list<IpRoutingTableEntry*>::const_iterator ASExternalRoutesCI;
+    /// iterator of container of Ipv4RoutingTableEntry (routes to external AS)
+    typedef std::list<IpRoutingTableEntry*>::iterator ASExternalRoutesI;
+
+    /**
+     * @brief Lookup in the forwarding table for destination.
+     * @param dest destination address
+     * @param oif output interface if any (put 0 otherwise)
+     * @return Ipv4Route to route the packet to reach dest address
+     */
+    Ptr<IpRoute> LookupGlobal(IpAddress dest, Ptr<NetDevice> oif = nullptr);
+
+    HostRoutes m_hostRoutes;             //!< Routes to hosts
+    NetworkRoutes m_networkRoutes;       //!< Routes to networks
+    ASExternalRoutes m_ASexternalRoutes; //!< External routes imported
+
+    Ptr<Ip> m_ip; //!< associated IPv4 instance
+};
+
+/**
+ * @ingroup ipv4
+ * Create the typedef Ipv4GlobalRouting with T as Ipv4RoutingProtocol
+ */
+typedef GlobalRouting<Ipv4RoutingProtocol> Ipv4GlobalRouting;
+
+/**
+ * @ingroup ipv6
+ * Create the typedef Ipv6GlobalRouting with T as Ipv6RoutingProtocol
+ */
+typedef GlobalRouting<Ipv6RoutingProtocol> Ipv6GlobalRouting;
+
+} // Namespace ns3
+
+#endif /* GLOBAL_ROUTING_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/icmpv6-header.cc ns-3.47/src/internet/model/icmpv6-header.cc
--- ns-3.46/src/internet/model/icmpv6-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/icmpv6-header.cc	2026-02-16 16:08:49.612229800 -0800
@@ -856,8 +856,8 @@
 }
 
 Icmpv6Redirection::Icmpv6Redirection()
-    : m_target(Ipv6Address("")),
-      m_destination(Ipv6Address("")),
+    : m_target(Ipv6Address::GetAny()),
+      m_destination(Ipv6Address::GetAny()),
       m_reserved(0)
 {
     NS_LOG_FUNCTION(this);
@@ -2044,6 +2044,7 @@
     NS_ASSERT(GetLength() * 8 <= 32 + 2);
     i.Read(mac, (GetLength() * 8) - 2);
 
+    m_addr.SetType("MacAddress", (GetLength() * 8) - 2);
     m_addr.CopyFrom(mac, (GetLength() * 8) - 2);
 
     return GetSerializedSize();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/icmpv6-l4-protocol.cc ns-3.47/src/internet/model/icmpv6-l4-protocol.cc
--- ns-3.46/src/internet/model/icmpv6-l4-protocol.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/icmpv6-l4-protocol.cc	2026-02-16 16:08:49.612229800 -0800
@@ -250,7 +250,6 @@
 Icmpv6L4Protocol::DoDAD(Ipv6Address target, Ptr<Ipv6Interface> interface)
 {
     NS_LOG_FUNCTION(this << target << interface);
-    Ipv6Address addr;
     Ptr<Ipv6L3Protocol> ipv6 = m_node->GetObject<Ipv6L3Protocol>();
 
     NS_ASSERT(ipv6);
@@ -498,7 +497,6 @@
                              Ptr<Ipv6Interface> interface)
 {
     NS_LOG_FUNCTION(this << lla << src << dst << interface);
-    Address hardwareAddress;
     NdiscCache::Entry* entry = nullptr;
     Ptr<NdiscCache> cache = FindCache(interface->GetDevice());
 
@@ -601,7 +599,6 @@
     Ptr<Ipv6L3Protocol> ipv6 = m_node->GetObject<Ipv6L3Protocol>();
     Icmpv6RS rsHeader;
     packet->RemoveHeader(rsHeader);
-    Address hardwareAddress;
     Icmpv6OptionLinkLayerAddress lla(true);
     NdiscCache::Entry* entry = nullptr;
     Ptr<NdiscCache> cache = FindCache(interface->GetDevice());
@@ -843,7 +840,6 @@
     packet->RemoveHeader(naHeader);
     Ipv6Address target = naHeader.GetIpv6Target();
 
-    Address hardwareAddress;
     NdiscCache::Entry* entry = nullptr;
     Ptr<NdiscCache> cache = FindCache(interface->GetDevice());
     std::list<NdiscCache::Ipv6PayloadHeaderPair> waiting;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-global-routing.cc ns-3.47/src/internet/model/ipv4-global-routing.cc
--- ns-3.46/src/internet/model/ipv4-global-routing.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-global-routing.cc	1969-12-31 16:00:00.000000000 -0800
@@ -1,638 +0,0 @@
-//
-// Copyright (c) 2008 University of Washington
-//
-// SPDX-License-Identifier: GPL-2.0-only
-//
-
-#include "ipv4-global-routing.h"
-
-#include "global-route-manager.h"
-#include "ipv4-route.h"
-#include "ipv4-routing-table-entry.h"
-
-#include "ns3/boolean.h"
-#include "ns3/log.h"
-#include "ns3/names.h"
-#include "ns3/net-device.h"
-#include "ns3/node.h"
-#include "ns3/object.h"
-#include "ns3/packet.h"
-#include "ns3/simulator.h"
-
-#include <iomanip>
-#include <vector>
-
-namespace ns3
-{
-
-NS_LOG_COMPONENT_DEFINE("Ipv4GlobalRouting");
-
-NS_OBJECT_ENSURE_REGISTERED(Ipv4GlobalRouting);
-
-TypeId
-Ipv4GlobalRouting::GetTypeId()
-{
-    static TypeId tid =
-        TypeId("ns3::Ipv4GlobalRouting")
-            .SetParent<Object>()
-            .SetGroupName("Internet")
-            .AddAttribute("RandomEcmpRouting",
-                          "Set to true if packets are randomly routed among ECMP; set to false for "
-                          "using only one route consistently",
-                          BooleanValue(false),
-                          MakeBooleanAccessor(&Ipv4GlobalRouting::m_randomEcmpRouting),
-                          MakeBooleanChecker())
-            .AddAttribute("RespondToInterfaceEvents",
-                          "Set to true if you want to dynamically recompute the global routes upon "
-                          "Interface notification events (up/down, or add/remove address)",
-                          BooleanValue(false),
-                          MakeBooleanAccessor(&Ipv4GlobalRouting::m_respondToInterfaceEvents),
-                          MakeBooleanChecker());
-    return tid;
-}
-
-Ipv4GlobalRouting::Ipv4GlobalRouting()
-    : m_randomEcmpRouting(false),
-      m_respondToInterfaceEvents(false)
-{
-    NS_LOG_FUNCTION(this);
-
-    m_rand = CreateObject<UniformRandomVariable>();
-}
-
-Ipv4GlobalRouting::~Ipv4GlobalRouting()
-{
-    NS_LOG_FUNCTION(this);
-}
-
-void
-Ipv4GlobalRouting::AddHostRouteTo(Ipv4Address dest, Ipv4Address nextHop, uint32_t interface)
-{
-    NS_LOG_FUNCTION(this << dest << nextHop << interface);
-    auto route = new Ipv4RoutingTableEntry();
-    *route = Ipv4RoutingTableEntry::CreateHostRouteTo(dest, nextHop, interface);
-    for (auto routePointer : m_hostRoutes)
-    {
-        if (*routePointer == *route)
-        {
-            NS_LOG_LOGIC("Route already exists");
-            delete route;
-            return;
-        }
-    }
-    m_hostRoutes.push_back(route);
-}
-
-void
-Ipv4GlobalRouting::AddHostRouteTo(Ipv4Address dest, uint32_t interface)
-{
-    NS_LOG_FUNCTION(this << dest << interface);
-    auto route = new Ipv4RoutingTableEntry();
-    *route = Ipv4RoutingTableEntry::CreateHostRouteTo(dest, interface);
-    for (auto routePointer : m_hostRoutes)
-    {
-        if (*routePointer == *route)
-        {
-            NS_LOG_LOGIC("Route already exists");
-            delete route;
-            return;
-        }
-    }
-    m_hostRoutes.push_back(route);
-}
-
-void
-Ipv4GlobalRouting::AddNetworkRouteTo(Ipv4Address network,
-                                     Ipv4Mask networkMask,
-                                     Ipv4Address nextHop,
-                                     uint32_t interface)
-{
-    NS_LOG_FUNCTION(this << network << networkMask << nextHop << interface);
-    auto route = new Ipv4RoutingTableEntry();
-    *route = Ipv4RoutingTableEntry::CreateNetworkRouteTo(network, networkMask, nextHop, interface);
-    for (auto routePointer : m_networkRoutes)
-    {
-        if (*routePointer == *route)
-        {
-            NS_LOG_LOGIC("Route already exists");
-            delete route;
-            return;
-        }
-    }
-    m_networkRoutes.push_back(route);
-}
-
-void
-Ipv4GlobalRouting::AddNetworkRouteTo(Ipv4Address network, Ipv4Mask networkMask, uint32_t interface)
-{
-    NS_LOG_FUNCTION(this << network << networkMask << interface);
-    auto route = new Ipv4RoutingTableEntry();
-    *route = Ipv4RoutingTableEntry::CreateNetworkRouteTo(network, networkMask, interface);
-    for (auto routePointer : m_networkRoutes)
-    {
-        if (*routePointer == *route)
-        {
-            NS_LOG_LOGIC("Route already exists");
-            delete route;
-            return;
-        }
-    }
-    m_networkRoutes.push_back(route);
-}
-
-void
-Ipv4GlobalRouting::AddASExternalRouteTo(Ipv4Address network,
-                                        Ipv4Mask networkMask,
-                                        Ipv4Address nextHop,
-                                        uint32_t interface)
-{
-    NS_LOG_FUNCTION(this << network << networkMask << nextHop << interface);
-    auto route = new Ipv4RoutingTableEntry();
-    *route = Ipv4RoutingTableEntry::CreateNetworkRouteTo(network, networkMask, nextHop, interface);
-    for (auto routePointer : m_ASexternalRoutes)
-    {
-        if (*routePointer == *route)
-        {
-            NS_LOG_LOGIC("Route already exists");
-            delete route;
-            return;
-        }
-    }
-    m_ASexternalRoutes.push_back(route);
-}
-
-Ptr<Ipv4Route>
-Ipv4GlobalRouting::LookupGlobal(Ipv4Address dest, Ptr<NetDevice> oif)
-{
-    NS_LOG_FUNCTION(this << dest << oif);
-    NS_LOG_LOGIC("Looking for route for destination " << dest);
-    Ptr<Ipv4Route> rtentry = nullptr;
-    // store all available routes that bring packets to their destination
-    typedef std::vector<Ipv4RoutingTableEntry*> RouteVec_t;
-    RouteVec_t allRoutes;
-
-    NS_LOG_LOGIC("Number of m_hostRoutes = " << m_hostRoutes.size());
-    for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i++)
-    {
-        NS_ASSERT((*i)->IsHost());
-        if ((*i)->GetDest() == dest)
-        {
-            if (oif)
-            {
-                if (oif != m_ipv4->GetNetDevice((*i)->GetInterface()))
-                {
-                    NS_LOG_LOGIC("Not on requested interface, skipping");
-                    continue;
-                }
-            }
-            allRoutes.push_back(*i);
-            NS_LOG_LOGIC(allRoutes.size() << "Found global host route" << *i);
-        }
-    }
-    if (allRoutes.empty()) // if no host route is found
-    {
-        NS_LOG_LOGIC("Number of m_networkRoutes" << m_networkRoutes.size());
-        // store the length of the longest mask.
-        uint16_t longest_mask = 0;
-        for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j++)
-        {
-            Ipv4Mask mask = (*j)->GetDestNetworkMask();
-            uint16_t masklen = mask.GetPrefixLength();
-
-            Ipv4Address entry = (*j)->GetDestNetwork();
-            if (mask.IsMatch(dest, entry))
-            {
-                if (oif)
-                {
-                    if (oif != m_ipv4->GetNetDevice((*j)->GetInterface()))
-                    {
-                        NS_LOG_LOGIC("Not on requested interface, skipping");
-                        continue;
-                    }
-                }
-                NS_LOG_LOGIC(allRoutes.size() << "Found global network route" << *j);
-                if (masklen < longest_mask) // Not interested if got shorter mask
-                {
-                    NS_LOG_LOGIC("Previous match longer, skipping");
-                    continue;
-                }
-                else if (masklen == longest_mask)
-                {
-                    NS_LOG_LOGIC("Equal mask length, adding this to the list");
-                    allRoutes.push_back(*j);
-                }
-                else
-                {
-                    NS_LOG_LOGIC("Longer mask length found, clearing the list and adding");
-                    allRoutes.clear();
-                    allRoutes.push_back(*j);
-                }
-            }
-        }
-    }
-    if (allRoutes.empty()) // consider external if no host/network found
-    {
-        for (auto k = m_ASexternalRoutes.begin(); k != m_ASexternalRoutes.end(); k++)
-        {
-            Ipv4Mask mask = (*k)->GetDestNetworkMask();
-            Ipv4Address entry = (*k)->GetDestNetwork();
-            if (mask.IsMatch(dest, entry))
-            {
-                NS_LOG_LOGIC("Found external route" << *k);
-                if (oif)
-                {
-                    if (oif != m_ipv4->GetNetDevice((*k)->GetInterface()))
-                    {
-                        NS_LOG_LOGIC("Not on requested interface, skipping");
-                        continue;
-                    }
-                }
-                allRoutes.push_back(*k);
-                break;
-            }
-        }
-    }
-    if (!allRoutes.empty()) // if route(s) is found
-    {
-        // pick up one of the routes uniformly at random if random
-        // ECMP routing is enabled, or always select the first route
-        // consistently if random ECMP routing is disabled
-        uint32_t selectIndex;
-        if (m_randomEcmpRouting)
-        {
-            selectIndex = m_rand->GetInteger(0, allRoutes.size() - 1);
-        }
-        else
-        {
-            selectIndex = 0;
-        }
-        Ipv4RoutingTableEntry* route = allRoutes.at(selectIndex);
-        // create a Ipv4Route object from the selected routing table entry
-        rtentry = Create<Ipv4Route>();
-        rtentry->SetDestination(route->GetDest());
-        /// @todo handle multi-address case
-        rtentry->SetSource(m_ipv4->GetAddress(route->GetInterface(), 0).GetLocal());
-        rtentry->SetGateway(route->GetGateway());
-        uint32_t interfaceIdx = route->GetInterface();
-        rtentry->SetOutputDevice(m_ipv4->GetNetDevice(interfaceIdx));
-        return rtentry;
-    }
-    else
-    {
-        return nullptr;
-    }
-}
-
-uint32_t
-Ipv4GlobalRouting::GetNRoutes() const
-{
-    NS_LOG_FUNCTION(this);
-    uint32_t n = 0;
-    n += m_hostRoutes.size();
-    n += m_networkRoutes.size();
-    n += m_ASexternalRoutes.size();
-    return n;
-}
-
-Ipv4RoutingTableEntry*
-Ipv4GlobalRouting::GetRoute(uint32_t index) const
-{
-    NS_LOG_FUNCTION(this << index);
-    if (index < m_hostRoutes.size())
-    {
-        uint32_t tmp = 0;
-        for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i++)
-        {
-            if (tmp == index)
-            {
-                return *i;
-            }
-            tmp++;
-        }
-    }
-    index -= m_hostRoutes.size();
-    uint32_t tmp = 0;
-    if (index < m_networkRoutes.size())
-    {
-        for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j++)
-        {
-            if (tmp == index)
-            {
-                return *j;
-            }
-            tmp++;
-        }
-    }
-    index -= m_networkRoutes.size();
-    tmp = 0;
-    for (auto k = m_ASexternalRoutes.begin(); k != m_ASexternalRoutes.end(); k++)
-    {
-        if (tmp == index)
-        {
-            return *k;
-        }
-        tmp++;
-    }
-    NS_ASSERT(false);
-    // quiet compiler.
-    return nullptr;
-}
-
-void
-Ipv4GlobalRouting::RemoveRoute(uint32_t index)
-{
-    NS_LOG_FUNCTION(this << index);
-    if (index < m_hostRoutes.size())
-    {
-        uint32_t tmp = 0;
-        for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i++)
-        {
-            if (tmp == index)
-            {
-                NS_LOG_LOGIC("Removing route " << index << "; size = " << m_hostRoutes.size());
-                delete *i;
-                m_hostRoutes.erase(i);
-                NS_LOG_LOGIC("Done removing host route "
-                             << index << "; host route remaining size = " << m_hostRoutes.size());
-                return;
-            }
-            tmp++;
-        }
-    }
-    index -= m_hostRoutes.size();
-    uint32_t tmp = 0;
-    for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j++)
-    {
-        if (tmp == index)
-        {
-            NS_LOG_LOGIC("Removing route " << index << "; size = " << m_networkRoutes.size());
-            delete *j;
-            m_networkRoutes.erase(j);
-            NS_LOG_LOGIC("Done removing network route "
-                         << index << "; network route remaining size = " << m_networkRoutes.size());
-            return;
-        }
-        tmp++;
-    }
-    index -= m_networkRoutes.size();
-    tmp = 0;
-    for (auto k = m_ASexternalRoutes.begin(); k != m_ASexternalRoutes.end(); k++)
-    {
-        if (tmp == index)
-        {
-            NS_LOG_LOGIC("Removing route " << index << "; size = " << m_ASexternalRoutes.size());
-            delete *k;
-            m_ASexternalRoutes.erase(k);
-            NS_LOG_LOGIC("Done removing network route "
-                         << index << "; network route remaining size = " << m_networkRoutes.size());
-            return;
-        }
-        tmp++;
-    }
-    NS_ASSERT(false);
-}
-
-int64_t
-Ipv4GlobalRouting::AssignStreams(int64_t stream)
-{
-    NS_LOG_FUNCTION(this << stream);
-    m_rand->SetStream(stream);
-    return 1;
-}
-
-void
-Ipv4GlobalRouting::DoDispose()
-{
-    NS_LOG_FUNCTION(this);
-    for (auto i = m_hostRoutes.begin(); i != m_hostRoutes.end(); i = m_hostRoutes.erase(i))
-    {
-        delete (*i);
-    }
-    for (auto j = m_networkRoutes.begin(); j != m_networkRoutes.end(); j = m_networkRoutes.erase(j))
-    {
-        delete (*j);
-    }
-    for (auto l = m_ASexternalRoutes.begin(); l != m_ASexternalRoutes.end();
-         l = m_ASexternalRoutes.erase(l))
-    {
-        delete (*l);
-    }
-
-    Ipv4RoutingProtocol::DoDispose();
-}
-
-// Formatted like output of "route -n" command
-void
-Ipv4GlobalRouting::PrintRoutingTable(Ptr<OutputStreamWrapper> stream, Time::Unit unit) const
-{
-    NS_LOG_FUNCTION(this << stream);
-    std::ostream* os = stream->GetStream();
-    // Copy the current ostream state
-    std::ios oldState(nullptr);
-    oldState.copyfmt(*os);
-
-    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
-
-    *os << "Node: " << m_ipv4->GetObject<Node>()->GetId() << ", Time: " << Now().As(unit)
-        << ", Local time: " << m_ipv4->GetObject<Node>()->GetLocalTime().As(unit)
-        << ", Ipv4GlobalRouting table" << std::endl;
-
-    if (GetNRoutes() > 0)
-    {
-        *os << "Destination     Gateway         Genmask         Flags Metric Ref    Use Iface"
-            << std::endl;
-        for (uint32_t j = 0; j < GetNRoutes(); j++)
-        {
-            std::ostringstream dest;
-            std::ostringstream gw;
-            std::ostringstream mask;
-            std::ostringstream flags;
-            Ipv4RoutingTableEntry route = GetRoute(j);
-            dest << route.GetDest();
-            *os << std::setw(16) << dest.str();
-            gw << route.GetGateway();
-            *os << std::setw(16) << gw.str();
-            mask << route.GetDestNetworkMask();
-            *os << std::setw(16) << mask.str();
-            flags << "U";
-            if (route.IsHost())
-            {
-                flags << "H";
-            }
-            else if (route.IsGateway())
-            {
-                flags << "G";
-            }
-            *os << std::setw(6) << flags.str();
-            // Metric not implemented
-            *os << "-"
-                << "      ";
-            // Ref ct not implemented
-            *os << "-"
-                << "      ";
-            // Use not implemented
-            *os << "-"
-                << "   ";
-            if (!Names::FindName(m_ipv4->GetNetDevice(route.GetInterface())).empty())
-            {
-                *os << Names::FindName(m_ipv4->GetNetDevice(route.GetInterface()));
-            }
-            else
-            {
-                *os << route.GetInterface();
-            }
-            *os << std::endl;
-        }
-    }
-    *os << std::endl;
-    // Restore the previous ostream state
-    (*os).copyfmt(oldState);
-}
-
-Ptr<Ipv4Route>
-Ipv4GlobalRouting::RouteOutput(Ptr<Packet> p,
-                               const Ipv4Header& header,
-                               Ptr<NetDevice> oif,
-                               Socket::SocketErrno& sockerr)
-{
-    NS_LOG_FUNCTION(this << p << &header << oif << &sockerr);
-    //
-    // First, see if this is a multicast packet we have a route for.  If we
-    // have a route, then send the packet down each of the specified interfaces.
-    //
-    if (header.GetDestination().IsMulticast())
-    {
-        NS_LOG_LOGIC("Multicast destination-- returning false");
-        return nullptr; // Let other routing protocols try to handle this
-    }
-    //
-    // See if this is a unicast packet we have a route for.
-    //
-    NS_LOG_LOGIC("Unicast destination- looking up");
-    Ptr<Ipv4Route> rtentry = LookupGlobal(header.GetDestination(), oif);
-    if (rtentry)
-    {
-        sockerr = Socket::ERROR_NOTERROR;
-    }
-    else
-    {
-        sockerr = Socket::ERROR_NOROUTETOHOST;
-    }
-    return rtentry;
-}
-
-bool
-Ipv4GlobalRouting::RouteInput(Ptr<const Packet> p,
-                              const Ipv4Header& header,
-                              Ptr<const NetDevice> idev,
-                              const UnicastForwardCallback& ucb,
-                              const MulticastForwardCallback& mcb,
-                              const LocalDeliverCallback& lcb,
-                              const ErrorCallback& ecb)
-{
-    NS_LOG_FUNCTION(this << p << header << header.GetSource() << header.GetDestination() << idev
-                         << &lcb << &ecb);
-    // Check if input device supports IP
-    NS_ASSERT(m_ipv4->GetInterfaceForDevice(idev) >= 0);
-    uint32_t iif = m_ipv4->GetInterfaceForDevice(idev);
-
-    if (m_ipv4->IsDestinationAddress(header.GetDestination(), iif))
-    {
-        if (!lcb.IsNull())
-        {
-            NS_LOG_LOGIC("Local delivery to " << header.GetDestination());
-            lcb(p, header, iif);
-            return true;
-        }
-        else
-        {
-            // The local delivery callback is null.  This may be a multicast
-            // or broadcast packet, so return false so that another
-            // multicast routing protocol can handle it.  It should be possible
-            // to extend this to explicitly check whether it is a unicast
-            // packet, and invoke the error callback if so
-            return false;
-        }
-    }
-
-    // Check if input device supports IP forwarding
-    if (!m_ipv4->IsForwarding(iif))
-    {
-        NS_LOG_LOGIC("Forwarding disabled for this interface");
-        ecb(p, header, Socket::ERROR_NOROUTETOHOST);
-        return true;
-    }
-    // Next, try to find a route
-    NS_LOG_LOGIC("Unicast destination- looking up global route");
-    Ptr<Ipv4Route> rtentry = LookupGlobal(header.GetDestination());
-    if (rtentry)
-    {
-        NS_LOG_LOGIC("Found unicast destination- calling unicast callback");
-        ucb(rtentry, p, header);
-        return true;
-    }
-    else
-    {
-        NS_LOG_LOGIC("Did not find unicast destination- returning false");
-        return false; // Let other routing protocols try to handle this
-                      // route request.
-    }
-}
-
-void
-Ipv4GlobalRouting::NotifyInterfaceUp(uint32_t i)
-{
-    NS_LOG_FUNCTION(this << i);
-    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
-    {
-        GlobalRouteManager::DeleteGlobalRoutes();
-        GlobalRouteManager::BuildGlobalRoutingDatabase();
-        GlobalRouteManager::InitializeRoutes();
-    }
-}
-
-void
-Ipv4GlobalRouting::NotifyInterfaceDown(uint32_t i)
-{
-    NS_LOG_FUNCTION(this << i);
-    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
-    {
-        GlobalRouteManager::DeleteGlobalRoutes();
-        GlobalRouteManager::BuildGlobalRoutingDatabase();
-        GlobalRouteManager::InitializeRoutes();
-    }
-}
-
-void
-Ipv4GlobalRouting::NotifyAddAddress(uint32_t interface, Ipv4InterfaceAddress address)
-{
-    NS_LOG_FUNCTION(this << interface << address);
-    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
-    {
-        GlobalRouteManager::DeleteGlobalRoutes();
-        GlobalRouteManager::BuildGlobalRoutingDatabase();
-        GlobalRouteManager::InitializeRoutes();
-    }
-}
-
-void
-Ipv4GlobalRouting::NotifyRemoveAddress(uint32_t interface, Ipv4InterfaceAddress address)
-{
-    NS_LOG_FUNCTION(this << interface << address);
-    if (m_respondToInterfaceEvents && Simulator::Now().GetSeconds() > 0) // avoid startup events
-    {
-        GlobalRouteManager::DeleteGlobalRoutes();
-        GlobalRouteManager::BuildGlobalRoutingDatabase();
-        GlobalRouteManager::InitializeRoutes();
-    }
-}
-
-void
-Ipv4GlobalRouting::SetIpv4(Ptr<Ipv4> ipv4)
-{
-    NS_LOG_FUNCTION(this << ipv4);
-    NS_ASSERT(!m_ipv4 && ipv4);
-    m_ipv4 = ipv4;
-}
-
-} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-global-routing.h ns-3.47/src/internet/model/ipv4-global-routing.h
--- ns-3.46/src/internet/model/ipv4-global-routing.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-global-routing.h	1969-12-31 16:00:00.000000000 -0800
@@ -1,274 +0,0 @@
-//
-// Copyright (c) 2008 University of Washington
-//
-// SPDX-License-Identifier: GPL-2.0-only
-//
-
-#ifndef IPV4_GLOBAL_ROUTING_H
-#define IPV4_GLOBAL_ROUTING_H
-
-#include "ipv4-header.h"
-#include "ipv4-routing-protocol.h"
-#include "ipv4.h"
-
-#include "ns3/ipv4-address.h"
-#include "ns3/ptr.h"
-#include "ns3/random-variable-stream.h"
-
-#include <list>
-#include <stdint.h>
-
-namespace ns3
-{
-
-class Packet;
-class NetDevice;
-class Ipv4Interface;
-class Ipv4Address;
-class Ipv4Header;
-class Ipv4RoutingTableEntry;
-class Ipv4MulticastRoutingTableEntry;
-class Node;
-
-/**
- * @ingroup ipv4
- *
- * @brief Global routing protocol for IPv4 stacks.
- *
- * In ns-3 we have the concept of a pluggable routing protocol.  Routing
- * protocols are added to a list maintained by the Ipv4L3Protocol.  Every
- * stack gets one routing protocol for free -- the Ipv4StaticRouting routing
- * protocol is added in the constructor of the Ipv4L3Protocol (this is the
- * piece of code that implements the functionality of the IP layer).
- *
- * As an option to running a dynamic routing protocol, a GlobalRouteManager
- * object has been created to allow users to build routes for all participating
- * nodes.  One can think of this object as a "routing oracle"; it has
- * an omniscient view of the topology, and can construct shortest path
- * routes between all pairs of nodes.  These routes must be stored
- * somewhere in the node, so therefore this class Ipv4GlobalRouting
- * is used as one of the pluggable routing protocols.  It is kept distinct
- * from Ipv4StaticRouting because these routes may be dynamically cleared
- * and rebuilt in the middle of the simulation, while manually entered
- * routes into the Ipv4StaticRouting may need to be kept distinct.
- *
- * This class deals with Ipv4 unicast routes only.
- *
- * @see Ipv4RoutingProtocol
- * @see GlobalRouteManager
- */
-class Ipv4GlobalRouting : public Ipv4RoutingProtocol
-{
-  public:
-    /**
-     * @brief Get the type ID.
-     * @return the object TypeId
-     */
-    static TypeId GetTypeId();
-    /**
-     * @brief Construct an empty Ipv4GlobalRouting routing protocol,
-     *
-     * The Ipv4GlobalRouting class supports host and network unicast routes.
-     * This method initializes the lists containing these routes to empty.
-     *
-     * @see Ipv4GlobalRouting
-     */
-    Ipv4GlobalRouting();
-    ~Ipv4GlobalRouting() override;
-
-    // These methods inherited from base class
-    Ptr<Ipv4Route> RouteOutput(Ptr<Packet> p,
-                               const Ipv4Header& header,
-                               Ptr<NetDevice> oif,
-                               Socket::SocketErrno& sockerr) override;
-
-    bool RouteInput(Ptr<const Packet> p,
-                    const Ipv4Header& header,
-                    Ptr<const NetDevice> idev,
-                    const UnicastForwardCallback& ucb,
-                    const MulticastForwardCallback& mcb,
-                    const LocalDeliverCallback& lcb,
-                    const ErrorCallback& ecb) override;
-    void NotifyInterfaceUp(uint32_t interface) override;
-    void NotifyInterfaceDown(uint32_t interface) override;
-    void NotifyAddAddress(uint32_t interface, Ipv4InterfaceAddress address) override;
-    void NotifyRemoveAddress(uint32_t interface, Ipv4InterfaceAddress address) override;
-    void SetIpv4(Ptr<Ipv4> ipv4) override;
-    void PrintRoutingTable(Ptr<OutputStreamWrapper> stream,
-                           Time::Unit unit = Time::S) const override;
-
-    /**
-     * @brief Add a host route to the global routing table.
-     *
-     * @param dest The Ipv4Address destination for this route.
-     * @param nextHop The Ipv4Address of the next hop in the route.
-     * @param interface The network interface index used to send packets to the
-     * destination.
-     *
-     * @see Ipv4Address
-     */
-    void AddHostRouteTo(Ipv4Address dest, Ipv4Address nextHop, uint32_t interface);
-    /**
-     * @brief Add a host route to the global routing table.
-     *
-     * @param dest The Ipv4Address destination for this route.
-     * @param interface The network interface index used to send packets to the
-     * destination.
-     *
-     * @see Ipv4Address
-     */
-    void AddHostRouteTo(Ipv4Address dest, uint32_t interface);
-
-    /**
-     * @brief Add a network route to the global routing table.
-     *
-     * @param network The Ipv4Address network for this route.
-     * @param networkMask The Ipv4Mask to extract the network.
-     * @param nextHop The next hop in the route to the destination network.
-     * @param interface The network interface index used to send packets to the
-     * destination.
-     *
-     * @see Ipv4Address
-     */
-    void AddNetworkRouteTo(Ipv4Address network,
-                           Ipv4Mask networkMask,
-                           Ipv4Address nextHop,
-                           uint32_t interface);
-
-    /**
-     * @brief Add a network route to the global routing table.
-     *
-     * @param network The Ipv4Address network for this route.
-     * @param networkMask The Ipv4Mask to extract the network.
-     * @param interface The network interface index used to send packets to the
-     * destination.
-     *
-     * @see Ipv4Address
-     */
-    void AddNetworkRouteTo(Ipv4Address network, Ipv4Mask networkMask, uint32_t interface);
-
-    /**
-     * @brief Add an external route to the global routing table.
-     *
-     * @param network The Ipv4Address network for this route.
-     * @param networkMask The Ipv4Mask to extract the network.
-     * @param nextHop The next hop Ipv4Address
-     * @param interface The network interface index used to send packets to the
-     * destination.
-     */
-    void AddASExternalRouteTo(Ipv4Address network,
-                              Ipv4Mask networkMask,
-                              Ipv4Address nextHop,
-                              uint32_t interface);
-
-    /**
-     * @brief Get the number of individual unicast routes that have been added
-     * to the routing table.
-     *
-     * @warning The default route counts as one of the routes.
-     * @returns the number of routes
-     */
-    uint32_t GetNRoutes() const;
-
-    /**
-     * @brief Get a route from the global unicast routing table.
-     *
-     * Externally, the unicast global routing table appears simply as a table with
-     * n entries.  The one subtlety of note is that if a default route has been set
-     * it will appear as the zeroth entry in the table.  This means that if you
-     * add only a default route, the table will have one entry that can be accessed
-     * either by explicitly calling GetDefaultRoute () or by calling GetRoute (0).
-     *
-     * Similarly, if the default route has been set, calling RemoveRoute (0) will
-     * remove the default route.
-     *
-     * @param i The index (into the routing table) of the route to retrieve.  If
-     * the default route has been set, it will occupy index zero.
-     * @return If route is set, a pointer to that Ipv4RoutingTableEntry is returned, otherwise
-     * a zero pointer is returned.
-     *
-     * @see Ipv4RoutingTableEntry
-     * @see Ipv4GlobalRouting::RemoveRoute
-     */
-    Ipv4RoutingTableEntry* GetRoute(uint32_t i) const;
-
-    /**
-     * @brief Remove a route from the global unicast routing table.
-     *
-     * Externally, the unicast global routing table appears simply as a table with
-     * n entries.  The one subtlety of note is that if a default route has been set
-     * it will appear as the zeroth entry in the table.  This means that if the
-     * default route has been set, calling RemoveRoute (0) will remove the
-     * default route.
-     *
-     * @param i The index (into the routing table) of the route to remove.  If
-     * the default route has been set, it will occupy index zero.
-     *
-     * @see Ipv4RoutingTableEntry
-     * @see Ipv4GlobalRouting::GetRoute
-     * @see Ipv4GlobalRouting::AddRoute
-     */
-    void RemoveRoute(uint32_t i);
-
-    /**
-     * Assign a fixed random variable stream number to the random variables
-     * used by this model.  Return the number of streams (possibly zero) that
-     * have been assigned.
-     *
-     * @param stream first stream index to use
-     * @return the number of stream indices assigned by this model
-     */
-    int64_t AssignStreams(int64_t stream);
-
-  protected:
-    void DoDispose() override;
-
-  private:
-    /// Set to true if packets are randomly routed among ECMP; set to false for using only one route
-    /// consistently
-    bool m_randomEcmpRouting;
-    /// Set to true if this interface should respond to interface events by globally recomputing
-    /// routes
-    bool m_respondToInterfaceEvents;
-    /// A uniform random number generator for randomly routing packets among ECMP
-    Ptr<UniformRandomVariable> m_rand;
-
-    /// container of Ipv4RoutingTableEntry (routes to hosts)
-    typedef std::list<Ipv4RoutingTableEntry*> HostRoutes;
-    /// const iterator of container of Ipv4RoutingTableEntry (routes to hosts)
-    typedef std::list<Ipv4RoutingTableEntry*>::const_iterator HostRoutesCI;
-    /// iterator of container of Ipv4RoutingTableEntry (routes to hosts)
-    typedef std::list<Ipv4RoutingTableEntry*>::iterator HostRoutesI;
-
-    /// container of Ipv4RoutingTableEntry (routes to networks)
-    typedef std::list<Ipv4RoutingTableEntry*> NetworkRoutes;
-    /// const iterator of container of Ipv4RoutingTableEntry (routes to networks)
-    typedef std::list<Ipv4RoutingTableEntry*>::const_iterator NetworkRoutesCI;
-    /// iterator of container of Ipv4RoutingTableEntry (routes to networks)
-    typedef std::list<Ipv4RoutingTableEntry*>::iterator NetworkRoutesI;
-
-    /// container of Ipv4RoutingTableEntry (routes to external AS)
-    typedef std::list<Ipv4RoutingTableEntry*> ASExternalRoutes;
-    /// const iterator of container of Ipv4RoutingTableEntry (routes to external AS)
-    typedef std::list<Ipv4RoutingTableEntry*>::const_iterator ASExternalRoutesCI;
-    /// iterator of container of Ipv4RoutingTableEntry (routes to external AS)
-    typedef std::list<Ipv4RoutingTableEntry*>::iterator ASExternalRoutesI;
-
-    /**
-     * @brief Lookup in the forwarding table for destination.
-     * @param dest destination address
-     * @param oif output interface if any (put 0 otherwise)
-     * @return Ipv4Route to route the packet to reach dest address
-     */
-    Ptr<Ipv4Route> LookupGlobal(Ipv4Address dest, Ptr<NetDevice> oif = nullptr);
-
-    HostRoutes m_hostRoutes;             //!< Routes to hosts
-    NetworkRoutes m_networkRoutes;       //!< Routes to networks
-    ASExternalRoutes m_ASexternalRoutes; //!< External routes imported
-
-    Ptr<Ipv4> m_ipv4; //!< associated IPv4 instance
-};
-
-} // Namespace ns3
-
-#endif /* IPV4_GLOBAL_ROUTING_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-header.h ns-3.47/src/internet/model/ipv4-header.h
--- ns-3.46/src/internet/model/ipv4-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-header.h	2026-02-16 16:08:49.613230000 -0800
@@ -208,6 +208,7 @@
     Ipv4Address GetSource() const;
     /**
      * @returns the destination address of this packet
+     * @hidecaller
      */
     Ipv4Address GetDestination() const;
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-interface-address.cc ns-3.47/src/internet/model/ipv4-interface-address.cc
--- ns-3.46/src/internet/model/ipv4-interface-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-interface-address.cc	2026-02-16 16:08:49.613230000 -0800
@@ -34,13 +34,11 @@
         m_scope = HOST;
     }
     m_mask = mask;
-    m_broadcast = Ipv4Address(local.Get() | (~mask.Get()));
 }
 
 Ipv4InterfaceAddress::Ipv4InterfaceAddress(const Ipv4InterfaceAddress& o)
     : m_local(o.m_local),
       m_mask(o.m_mask),
-      m_broadcast(o.m_broadcast),
       m_scope(o.m_scope),
       m_secondary(o.m_secondary)
 {
@@ -87,18 +85,11 @@
     return m_mask;
 }
 
-void
-Ipv4InterfaceAddress::SetBroadcast(Ipv4Address broadcast)
-{
-    NS_LOG_FUNCTION(this << broadcast);
-    m_broadcast = broadcast;
-}
-
 Ipv4Address
 Ipv4InterfaceAddress::GetBroadcast() const
 {
     NS_LOG_FUNCTION(this);
-    return m_broadcast;
+    return Ipv4Address(m_local.Get() | (~m_mask.Get()));
 }
 
 void
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-interface-address.h ns-3.47/src/internet/model/ipv4-interface-address.h
--- ns-3.46/src/internet/model/ipv4-interface-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-interface-address.h	2026-02-16 16:08:49.613230000 -0800
@@ -72,7 +72,7 @@
      * @brief Set local address
      * @param address the address
      *
-     * @note Functially identical to `Ipv4InterfaceAddress::SetLocal`.
+     * @note Functionally identical to `Ipv4InterfaceAddress::SetLocal`.
      *       This function is consistent with `Ipv6InterfaceAddress::SetAddress`.
      */
     void SetAddress(Ipv4Address address);
@@ -101,16 +101,13 @@
      * @param mask the network mask
      */
     void SetMask(Ipv4Mask mask);
+
     /**
      * @brief Get the network mask
      * @returns the network mask
      */
     Ipv4Mask GetMask() const;
-    /**
-     * @brief Set the broadcast address
-     * @param broadcast the broadcast address
-     */
-    void SetBroadcast(Ipv4Address broadcast);
+
     /**
      * @brief Get the broadcast address
      * @returns the broadcast address
@@ -157,8 +154,7 @@
     Ipv4Address m_local; //!< Interface address
     // Note:  m_peer may be added in future when necessary
     // Ipv4Address m_peer;   // Peer destination address (in Linux:  m_address)
-    Ipv4Mask m_mask;         //!< Network mask
-    Ipv4Address m_broadcast; //!< Broadcast address
+    Ipv4Mask m_mask; //!< Network mask
 
     InterfaceAddressScope_e m_scope; //!< Address scope
     bool m_secondary;                //!< For use in multihoming
@@ -194,15 +190,15 @@
 inline bool
 operator==(const Ipv4InterfaceAddress& a, const Ipv4InterfaceAddress& b)
 {
-    return (a.m_local == b.m_local && a.m_mask == b.m_mask && a.m_broadcast == b.m_broadcast &&
-            a.m_scope == b.m_scope && a.m_secondary == b.m_secondary);
+    return (a.m_local == b.m_local && a.m_mask == b.m_mask && a.m_scope == b.m_scope &&
+            a.m_secondary == b.m_secondary);
 }
 
 inline bool
 operator!=(const Ipv4InterfaceAddress& a, const Ipv4InterfaceAddress& b)
 {
-    return (a.m_local != b.m_local || a.m_mask != b.m_mask || a.m_broadcast != b.m_broadcast ||
-            a.m_scope != b.m_scope || a.m_secondary != b.m_secondary);
+    return (a.m_local != b.m_local || a.m_mask != b.m_mask || a.m_scope != b.m_scope ||
+            a.m_secondary != b.m_secondary);
 }
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-l3-protocol.cc ns-3.47/src/internet/model/ipv4-l3-protocol.cc
--- ns-3.46/src/internet/model/ipv4-l3-protocol.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-l3-protocol.cc	2026-02-16 16:08:49.613230000 -0800
@@ -755,8 +755,6 @@
 
     // Handle a few cases:
     // 1) packet is passed in with a route entry
-    // 1a) packet is passed in with a route entry but route->GetGateway is not set (e.g., on-demand)
-    // 1b) packet is passed in with a route entry and valid gateway
     // 2) packet is passed without a route and packet is destined to limited broadcast address
     // 3) packet is passed without a route and packet is destined to a subnet-directed broadcast
     // address 4) packet is passed without a route, packet is not broadcast (e.g., a raw socket
@@ -765,18 +763,6 @@
     // 1) packet is passed in with route entry
     if (route)
     {
-        // 1a) route->GetGateway is not set (e.g., on-demand)
-        if (!route->GetGateway().IsInitialized())
-        {
-            // This could arise because the synchronous RouteOutput() call
-            // returned to the transport protocol with a source address but
-            // there was no next hop available yet (since a route may need
-            // to be queried).
-            NS_FATAL_ERROR("Ipv4L3Protocol::Send case 1a: packet passed with a route but the "
-                           "Gateway address is uninitialized. This case not yet implemented.");
-        }
-
-        // 1b) with a valid gateway
         NS_LOG_LOGIC("Ipv4L3Protocol::Send case 1b:  passed in with route and valid gateway");
         int32_t interface = GetInterfaceForDevice(route->GetOutputDevice());
         m_sendOutgoingTrace(ipHeader, packet, interface);
@@ -1527,12 +1513,12 @@
     key.first = addressCombination;
     key.second = idProto;
 
-    Ptr<Fragments> fragments;
+    std::shared_ptr<Fragments> fragments;
 
     auto it = m_fragments.find(key);
     if (it == m_fragments.end())
     {
-        fragments = Create<Fragments>();
+        fragments = std::make_shared<Fragments>();
         m_fragments.insert(std::make_pair(key, fragments));
 
         auto iter = SetTimeout(key, ipHeader, iif);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv4-l3-protocol.h ns-3.47/src/internet/model/ipv4-l3-protocol.h
--- ns-3.46/src/internet/model/ipv4-l3-protocol.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv4-l3-protocol.h	2026-02-16 16:08:49.613230000 -0800
@@ -535,7 +535,7 @@
     /**
      * @brief A Set of Fragment belonging to the same packet (src, dst, identification and proto)
      */
-    class Fragments : public SimpleRefCount<Fragments>
+    class Fragments
     {
       public:
         /**
@@ -599,7 +599,7 @@
     };
 
     /// Container of fragments, stored as pairs(src+dst addr, src+dst port) / fragment
-    typedef std::map<FragmentKey_t, Ptr<Fragments>> MapFragments_t;
+    typedef std::map<FragmentKey_t, std::shared_ptr<Fragments>> MapFragments_t;
 
     MapFragments_t m_fragments;       //!< Fragmented packets.
     Time m_fragmentExpirationTimeout; //!< Expiration timeout
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-address-generator.cc ns-3.47/src/internet/model/ipv6-address-generator.cc
--- ns-3.46/src/internet/model/ipv6-address-generator.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-address-generator.cc	2026-02-16 16:08:49.615229800 -0800
@@ -421,7 +421,7 @@
     {
         ad[j] |= m_netTable[index].addr[j];
     }
-    Ipv6Address addr = Ipv6Address(ad);
+    auto addr = Ipv6Address(ad);
 
     for (int32_t j = 15; j >= 0; j--)
     {
@@ -601,8 +601,8 @@
     {
         NS_LOG_LOGIC("examine entry: " << Ipv6Address((*i).addrLow) << " to "
                                        << Ipv6Address((*i).addrHigh));
-        Ipv6Address low = Ipv6Address((*i).addrLow);
-        Ipv6Address high = Ipv6Address((*i).addrHigh);
+        auto low = Ipv6Address((*i).addrLow);
+        auto high = Ipv6Address((*i).addrHigh);
 
         if (address == low.CombinePrefix(prefix) || address == high.CombinePrefix(prefix))
         {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-extension.cc ns-3.47/src/internet/model/ipv6-extension.cc
--- ns-3.46/src/internet/model/ipv6-extension.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-extension.cc	2026-02-16 16:08:49.615229800 -0800
@@ -392,7 +392,7 @@
     Ipv6Address src = ipv6Header.GetSource();
 
     FragmentKey_t fragmentKey = FragmentKey_t(src, identification);
-    Ptr<Fragments> fragments;
+    std::shared_ptr<Fragments> fragments;
 
     Ipv6Header ipHeader = ipv6Header;
     ipHeader.SetNextHeader(fragmentHeader.GetNextHeader());
@@ -400,7 +400,7 @@
     auto it = m_fragments.find(fragmentKey);
     if (it == m_fragments.end())
     {
-        fragments = Create<Fragments>();
+        fragments = std::make_shared<Fragments>();
         m_fragments.insert(std::make_pair(fragmentKey, fragments));
         NS_LOG_DEBUG("Insert new fragment key: src: "
                      << src << " IP hdr id " << identification << " m_fragments.size() "
@@ -630,13 +630,12 @@
 Ipv6ExtensionFragment::HandleFragmentsTimeout(FragmentKey_t fragmentKey, Ipv6Header ipHeader)
 {
     NS_LOG_FUNCTION(this << fragmentKey.first << fragmentKey.second << ipHeader);
-    Ptr<Fragments> fragments;
 
     auto it = m_fragments.find(fragmentKey);
     NS_ASSERT_MSG(it != m_fragments.end(),
                   "IPv6 Fragment timeout reached for non-existent fragment");
-    fragments = it->second;
 
+    std::shared_ptr<Fragments> fragments = it->second;
     Ptr<Packet> packet = fragments->GetPartialPacket();
 
     // if we have at least 8 bytes, we can send an ICMP.
@@ -1026,6 +1025,17 @@
     m_extensionsRouting.remove(extensionRouting);
 }
 
+int64_t
+Ipv6ExtensionRoutingDemux::AssignStreams(int64_t stream)
+{
+    int64_t currentStream = stream;
+    for (auto& extension : m_extensionsRouting)
+    {
+        currentStream += extension->AssignStreams(currentStream);
+    }
+    return currentStream - stream;
+}
+
 NS_OBJECT_ENSURE_REGISTERED(Ipv6ExtensionLooseRouting);
 
 TypeId
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-extension-demux.cc ns-3.47/src/internet/model/ipv6-extension-demux.cc
--- ns-3.46/src/internet/model/ipv6-extension-demux.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-extension-demux.cc	2026-02-16 16:08:49.615229800 -0800
@@ -88,4 +88,14 @@
     m_extensions.remove(extension);
 }
 
+int64_t
+Ipv6ExtensionDemux::AssignStreams(int64_t stream)
+{
+    int64_t currentStream = stream;
+    for (auto& extension : m_extensions)
+    {
+        currentStream += extension->AssignStreams(currentStream);
+    }
+    return currentStream - stream;
+}
 } /* namespace ns3 */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-extension-demux.h ns-3.47/src/internet/model/ipv6-extension-demux.h
--- ns-3.46/src/internet/model/ipv6-extension-demux.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-extension-demux.h	2026-02-16 16:08:49.615229800 -0800
@@ -69,6 +69,14 @@
      */
     void Remove(Ptr<Ipv6Extension> extension);
 
+    /**
+     * Assigns stream numbers to the registered extensions of this IPv6 demux object
+     *
+     * @param stream the first stream number to assign
+     * @returns the number of assigned streams
+     */
+    int64_t AssignStreams(int64_t stream);
+
   protected:
     /**
      * @brief Dispose object.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-extension.h ns-3.47/src/internet/model/ipv6-extension.h
--- ns-3.46/src/internet/model/ipv6-extension.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-extension.h	2026-02-16 16:08:49.615229800 -0800
@@ -330,7 +330,7 @@
      *
      * @brief This class stores the fragments of a packet waiting to be rebuilt.
      */
-    class Fragments : public SimpleRefCount<Fragments>
+    class Fragments
     {
       public:
         /**
@@ -436,7 +436,7 @@
     /**
      * @brief Container for the packet fragments.
      */
-    typedef std::map<FragmentKey_t, Ptr<Fragments>> MapFragments_t;
+    typedef std::map<FragmentKey_t, std::shared_ptr<Fragments>> MapFragments_t;
 
     /**
      * @brief The hash of fragmented packets.
@@ -579,6 +579,14 @@
      */
     void Remove(Ptr<Ipv6ExtensionRouting> extensionRouting);
 
+    /**
+     * Assigns stream numbers to the registered extensions of this IPv6 routing demux object
+     *
+     * @param stream the first stream number to assign
+     * @returns the number of assigned streams
+     */
+    int64_t AssignStreams(int64_t stream);
+
   protected:
     /**
      * @brief Dispose this object.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-interface.cc ns-3.47/src/internet/model/ipv6-interface.cc
--- ns-3.46/src/internet/model/ipv6-interface.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-interface.cc	2026-02-16 16:08:49.615229800 -0800
@@ -176,6 +176,7 @@
         return;
     }
     DoSetup();
+
     m_ifup = true;
 
     Ptr<Ipv6> ip = m_node->GetObject<Ipv6>();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-l3-protocol.cc ns-3.47/src/internet/model/ipv6-l3-protocol.cc
--- ns-3.46/src/internet/model/ipv6-l3-protocol.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-l3-protocol.cc	2026-02-16 16:08:49.616230000 -0800
@@ -1428,7 +1428,6 @@
     Ptr<IpL4Protocol> protocol = nullptr;
     Ptr<Ipv6ExtensionDemux> ipv6ExtensionDemux = m_node->GetObject<Ipv6ExtensionDemux>();
     Ptr<Ipv6Extension> ipv6Extension = nullptr;
-    Ipv6Address src = ip.GetSource();
     Ipv6Address dst = ip.GetDestination();
     uint8_t nextHeader = ip.GetNextHeader();
     uint8_t nextHeaderPosition = 0;
@@ -1515,7 +1514,6 @@
             else
             {
                 p->RemoveAtStart(nextHeaderPosition);
-                /* protocol->Receive (p, src, dst, incomingInterface); */
 
                 // Options have been processed, and the packet is good.
                 // We need to adjust the IP header fields
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-list-routing.cc ns-3.47/src/internet/model/ipv6-list-routing.cc
--- ns-3.46/src/internet/model/ipv6-list-routing.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-list-routing.cc	2026-02-16 16:08:49.616230000 -0800
@@ -102,7 +102,6 @@
     NS_ASSERT(m_ipv6);
     // Check if input device supports IP
     NS_ASSERT(m_ipv6->GetInterfaceForDevice(idev) >= 0);
-    Ipv6Address dst = header.GetDestination();
 
     // Check if input device supports IP forwarding
     uint32_t iif = m_ipv6->GetInterfaceForDevice(idev);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-routing-table-entry.cc ns-3.47/src/internet/model/ipv6-routing-table-entry.cc
--- ns-3.46/src/internet/model/ipv6-routing-table-entry.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-routing-table-entry.cc	2026-02-16 16:08:49.617230000 -0800
@@ -263,6 +263,13 @@
     return os;
 }
 
+bool
+operator==(const Ipv6RoutingTableEntry a, const Ipv6RoutingTableEntry b)
+{
+    return (a.GetDest() == b.GetDest() && a.GetDestNetworkPrefix() == b.GetDestNetworkPrefix() &&
+            a.GetGateway() == b.GetGateway() && a.GetInterface() == b.GetInterface());
+}
+
 Ipv6MulticastRoutingTableEntry::Ipv6MulticastRoutingTableEntry()
 {
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-routing-table-entry.h ns-3.47/src/internet/model/ipv6-routing-table-entry.h
--- ns-3.46/src/internet/model/ipv6-routing-table-entry.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-routing-table-entry.h	2026-02-16 16:08:49.617230000 -0800
@@ -280,6 +280,15 @@
 std::ostream& operator<<(std::ostream& os, const Ipv6RoutingTableEntry& route);
 
 /**
+ * @brief Equality operator.
+ *
+ * @param a lhs
+ * @param b rhs
+ * @returns true if operands are equal, false otherwise
+ */
+bool operator==(const Ipv6RoutingTableEntry a, const Ipv6RoutingTableEntry b);
+
+/**
  * @ingroup ipv6Routing
  *
  * @brief A record of an IPv6 multicast route.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ipv6-static-routing.cc ns-3.47/src/internet/model/ipv6-static-routing.cc
--- ns-3.46/src/internet/model/ipv6-static-routing.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ipv6-static-routing.cc	2026-02-16 16:08:49.617230000 -0800
@@ -254,8 +254,8 @@
 {
     NS_LOG_FUNCTION(this << outputInterface);
     auto route = new Ipv6RoutingTableEntry();
-    Ipv6Address network = Ipv6Address("ff00::"); /* RFC 3513 */
-    Ipv6Prefix networkMask = Ipv6Prefix(8);
+    auto network = Ipv6Address("ff00::"); /* RFC 3513 */
+    auto networkMask = Ipv6Prefix(8);
     *route = Ipv6RoutingTableEntry::CreateNetworkRouteTo(network, networkMask, outputInterface);
     m_networkRoutes.emplace_back(route, 0);
 }
@@ -559,7 +559,6 @@
         uint32_t metric = it->second;
         Ipv6Prefix mask = j->GetDestNetworkPrefix();
         uint16_t maskLen = mask.GetPrefixLength();
-        Ipv6Address entry = j->GetDestNetwork();
 
         if (maskLen)
         {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/ripng.cc ns-3.47/src/internet/model/ripng.cc
--- ns-3.46/src/internet/model/ripng.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/ripng.cc	2026-02-16 16:08:49.618229900 -0800
@@ -1046,7 +1046,7 @@
 
     for (auto iter = rtes.begin(); iter != rtes.end(); iter++)
     {
-        Ipv6Prefix rtePrefix = Ipv6Prefix(iter->GetPrefixLen());
+        auto rtePrefix = Ipv6Prefix(iter->GetPrefixLen());
         Ipv6Address rteAddr = iter->GetPrefix().CombinePrefix(rtePrefix);
 
         NS_LOG_LOGIC("Processing RTE " << *iter);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-congestion-ops.cc ns-3.47/src/internet/model/tcp-congestion-ops.cc
--- ns-3.46/src/internet/model/tcp-congestion-ops.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-congestion-ops.cc	2026-02-16 16:08:49.619230000 -0800
@@ -86,7 +86,17 @@
     static TypeId tid = TypeId("ns3::TcpNewReno")
                             .SetParent<TcpCongestionOps>()
                             .SetGroupName("Internet")
-                            .AddConstructor<TcpNewReno>();
+                            .AddConstructor<TcpNewReno>()
+                            .AddAttribute("BetaLoss",
+                                          "Beta for multiplicative decrease",
+                                          DoubleValue(0.5),
+                                          MakeDoubleAccessor(&TcpNewReno::m_betaLoss),
+                                          MakeDoubleChecker<double>(0.0, 1.0))
+                            .AddAttribute("BetaEcn",
+                                          "Beta for multiplicative decrease for ABE",
+                                          DoubleValue(0.7), // According to RFC 8511 (ABE)
+                                          MakeDoubleAccessor(&TcpNewReno::m_betaEcn),
+                                          MakeDoubleChecker<double>(0.0, 1.0));
     return tid;
 }
 
@@ -234,7 +244,13 @@
 {
     NS_LOG_FUNCTION(this << state << bytesInFlight);
 
-    return std::max(2 * state->m_segmentSize, bytesInFlight / 2);
+    if (state->m_abeEnabled && state->m_ecnState == TcpSocketState::ECN_ECE_RCVD)
+    {
+        return std::max<uint32_t>(2 * state->m_segmentSize,
+                                  bytesInFlight * m_betaEcn); // According to RFC 8511 (ABE)
+    }
+
+    return std::max<uint32_t>(2 * state->m_segmentSize, bytesInFlight * m_betaLoss);
 }
 
 Ptr<TcpCongestionOps>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-congestion-ops.h ns-3.47/src/internet/model/tcp-congestion-ops.h
--- ns-3.46/src/internet/model/tcp-congestion-ops.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-congestion-ops.h	2026-02-16 16:08:49.619230000 -0800
@@ -234,6 +234,10 @@
   protected:
     virtual uint32_t SlowStart(Ptr<TcpSocketState> tcb, uint32_t segmentsAcked);
     virtual void CongestionAvoidance(Ptr<TcpSocketState> tcb, uint32_t segmentsAcked);
+
+  private:
+    double m_betaLoss{0}; //!< Beta for New-Reno multiplicative decrease
+    double m_betaEcn{0};  //!< Beta for New-Reno multiplicative decrease with ABE
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-cubic.cc ns-3.47/src/internet/model/tcp-cubic.cc
--- ns-3.46/src/internet/model/tcp-cubic.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-cubic.cc	2026-02-16 16:08:49.619230000 -0800
@@ -43,7 +43,12 @@
                           "Beta for multiplicative decrease",
                           DoubleValue(0.7),
                           MakeDoubleAccessor(&TcpCubic::m_beta),
-                          MakeDoubleChecker<double>(0.0))
+                          MakeDoubleChecker<double>(0.0, 1.0))
+            .AddAttribute("BetaEcn",
+                          "Beta for multiplicative decrease for ABE",
+                          DoubleValue(0.85), // According to RFC 8511 (ABE)
+                          MakeDoubleAccessor(&TcpCubic::m_betaEcn),
+                          MakeDoubleChecker<double>(0.0, 1.0))
             .AddAttribute("HyStart",
                           "Enable (true) or disable (false) hybrid slow start algorithm",
                           BooleanValue(true),
@@ -129,6 +134,7 @@
     : TcpCongestionOps(sock),
       m_fastConvergence(sock.m_fastConvergence),
       m_beta(sock.m_beta),
+      m_betaEcn(sock.m_betaEcn),
       m_hystart(sock.m_hystart),
       m_hystartDetect(sock.m_hystartDetect),
       m_hystartLowWindow(sock.m_hystartLowWindow),
@@ -467,8 +473,16 @@
 
     m_epochStart = Time::Min(); // end of epoch
 
-    /* Formula taken from the Linux kernel */
-    uint32_t ssThresh = std::max(static_cast<uint32_t>(segCwnd * m_beta), 2U) * tcb->m_segmentSize;
+    uint32_t ssThresh;
+    if (tcb->m_abeEnabled && tcb->m_ecnState == TcpSocketState::ECN_ECE_RCVD)
+    {
+        ssThresh = std::max(static_cast<uint32_t>(segCwnd * m_betaEcn), 2U) *
+                   tcb->m_segmentSize; // According to RFC 8511 (ABE)
+    }
+    else
+    { /* Formula taken from the Linux kernel */
+        ssThresh = std::max(static_cast<uint32_t>(segCwnd * m_beta), 2U) * tcb->m_segmentSize;
+    }
 
     NS_LOG_DEBUG("SsThresh = " << ssThresh);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-cubic.h ns-3.47/src/internet/model/tcp-cubic.h
--- ns-3.46/src/internet/model/tcp-cubic.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-cubic.h	2026-02-16 16:08:49.619230000 -0800
@@ -93,10 +93,10 @@
     void Init(Ptr<TcpSocketState> tcb) override;
 
   private:
-    bool m_fastConvergence; //!< Enable or disable fast convergence algorithm
-    bool m_tcpFriendliness; //!< Enable or disable TCP-friendliness heuristic
-    double m_beta;          //!< Beta for cubic multiplicative increase
-
+    bool m_fastConvergence;                //!< Enable or disable fast convergence algorithm
+    bool m_tcpFriendliness;                //!< Enable or disable TCP-friendliness heuristic
+    double m_beta;                         //!< Beta for cubic multiplicative decrease
+    double m_betaEcn;                      //!< Beta for cubic multiplicative decrease with ABE
     bool m_hystart;                        //!< Enable or disable HyStart algorithm
     HybridSSDetectionMode m_hystartDetect; //!< Detect way for HyStart algorithm
     uint32_t m_hystartLowWindow;           //!< Lower bound cWnd for hybrid slow start (segments)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-illinois.h ns-3.47/src/internet/model/tcp-illinois.h
--- ns-3.46/src/internet/model/tcp-illinois.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-illinois.h	2026-02-16 16:08:49.619230000 -0800
@@ -156,7 +156,6 @@
      */
     void PktsAcked(Ptr<TcpSocketState> tcb, uint32_t segmentsAcked, const Time& rtt) override;
 
-  protected:
   private:
     /**
      * @brief Recalculate alpha and beta every RTT
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-ledbat.cc ns-3.47/src/internet/model/tcp-ledbat.cc
--- ns-3.46/src/internet/model/tcp-ledbat.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-ledbat.cc	2026-02-16 16:08:49.619230000 -0800
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: GPL-2.0-only
  *
  * Author: Ankit Deepak <adadeepak8@gmail.com>
+ * Modified by: S B L Prateek <sblprateek@gmail.com>
  *
  */
 
@@ -37,17 +38,17 @@
                           "Number of Base delay samples",
                           UintegerValue(10),
                           MakeUintegerAccessor(&TcpLedbat::m_baseHistoLen),
-                          MakeUintegerChecker<uint32_t>())
+                          MakeUintegerChecker<uint32_t>(1))
             .AddAttribute("NoiseFilterLen",
                           "Number of Current delay samples",
                           UintegerValue(4),
                           MakeUintegerAccessor(&TcpLedbat::m_noiseFilterLen),
-                          MakeUintegerChecker<uint32_t>())
+                          MakeUintegerChecker<uint32_t>(1))
             .AddAttribute("Gain",
                           "Offset Gain",
                           DoubleValue(1.0),
                           MakeDoubleAccessor(&TcpLedbat::m_gain),
-                          MakeDoubleChecker<double>())
+                          MakeDoubleChecker<double>(1e-6))
             .AddAttribute("SSParam",
                           "Possibility of Slow Start",
                           EnumValue(DO_SLOWSTART),
@@ -57,7 +58,12 @@
                           "Minimum cWnd for Ledbat",
                           UintegerValue(2),
                           MakeUintegerAccessor(&TcpLedbat::m_minCwnd),
-                          MakeUintegerChecker<uint32_t>());
+                          MakeUintegerChecker<uint32_t>(1))
+            .AddAttribute("AllowedIncrease",
+                          "Allowed Increase",
+                          DoubleValue(1.0),
+                          MakeDoubleAccessor(&TcpLedbat::m_allowedIncrease),
+                          MakeDoubleChecker<double>(1e-6));
     return tid;
 }
 
@@ -80,17 +86,11 @@
     : TcpNewReno()
 {
     NS_LOG_FUNCTION(this);
-    m_target = MilliSeconds(100);
-    m_gain = 1;
-    m_doSs = DO_SLOWSTART;
-    m_baseHistoLen = 10;
-    m_noiseFilterLen = 4;
     InitCircBuf(m_baseHistory);
     InitCircBuf(m_noiseFilter);
-    m_lastRollover = 0;
+    m_lastRollover = Seconds(0);
     m_sndCwndCnt = 0;
     m_flag = LEDBAT_CAN_SS;
-    m_minCwnd = 2;
 }
 
 void
@@ -116,6 +116,7 @@
     m_sndCwndCnt = sock.m_sndCwndCnt;
     m_flag = sock.m_flag;
     m_minCwnd = sock.m_minCwnd;
+    m_allowedIncrease = sock.m_allowedIncrease;
 }
 
 TcpLedbat::~TcpLedbat()
@@ -193,31 +194,35 @@
             segmentsAcked); // letting it fall to TCP behaviour if no timestamps
         return;
     }
-    int64_t queue_delay;
+    uint32_t queueDelay;
     double offset;
-    uint32_t cwnd = (tcb->m_cWnd.Get());
-    uint32_t max_cwnd;
-    uint64_t current_delay = CurrentDelay(&TcpLedbat::MinCircBuf);
-    uint64_t base_delay = BaseDelay();
+    uint32_t cwnd = tcb->m_cWnd.Get();
+    uint32_t maxCwnd;
+    uint32_t currentDelay = CurrentDelay(&TcpLedbat::MinCircBuf);
+    uint32_t baseDelay = BaseDelay();
 
-    if (current_delay > base_delay)
+    if (currentDelay > baseDelay)
     {
-        queue_delay = static_cast<int64_t>(current_delay - base_delay);
-        offset = m_target.GetMilliSeconds() - queue_delay;
+        queueDelay = currentDelay - baseDelay;
+        offset = m_target.GetMilliSeconds() - queueDelay;
     }
     else
     {
-        queue_delay = static_cast<int64_t>(base_delay - current_delay);
-        offset = m_target.GetMilliSeconds() + queue_delay;
+        queueDelay = baseDelay - currentDelay;
+        offset = m_target.GetMilliSeconds() + queueDelay;
     }
     offset *= m_gain;
-    m_sndCwndCnt = static_cast<int32_t>(offset * segmentsAcked * tcb->m_segmentSize);
-    double inc = (m_sndCwndCnt * 1.0) / (m_target.GetMilliSeconds() * tcb->m_cWnd.Get());
+    m_sndCwndCnt = offset * segmentsAcked * tcb->m_segmentSize;
+    double inc = m_sndCwndCnt / (m_target.GetMilliSeconds() * tcb->m_cWnd.Get());
     cwnd += (inc * tcb->m_segmentSize);
 
-    max_cwnd = static_cast<uint32_t>(tcb->m_highTxMark.Get() - tcb->m_lastAckedSeq) +
-               segmentsAcked * tcb->m_segmentSize;
-    cwnd = std::min(cwnd, max_cwnd);
+    // Since m_bytesInFlight reflects the state after processing the current ACK,
+    // adding segmentsAcked * m_segmentSize reconstructs the flight size before this
+    // ACK arrived, as required by the RFC definition of flight size.
+    uint32_t flightSizeBeforeAck =
+        tcb->m_bytesInFlight.Get() + (segmentsAcked * tcb->m_segmentSize);
+    maxCwnd = flightSizeBeforeAck + static_cast<uint32_t>(m_allowedIncrease * tcb->m_segmentSize);
+    cwnd = std::min(cwnd, maxCwnd);
     cwnd = std::max(cwnd, m_minCwnd * tcb->m_segmentSize);
     tcb->m_cWnd = cwnd;
 
@@ -241,21 +246,15 @@
     cb.buffer.push_back(owd);
     if (cb.buffer[cb.min] > owd)
     {
-        cb.min = static_cast<uint32_t>(cb.buffer.size() - 1);
+        cb.min = cb.buffer.size() - 1;
     }
     if (cb.buffer.size() >= maxlen)
     {
         NS_LOG_LOGIC("Queue full" << maxlen);
         cb.buffer.erase(cb.buffer.begin());
-        cb.min = 0;
+        auto bufferStart = cb.buffer.begin();
+        cb.min = std::distance(bufferStart, std::min_element(bufferStart, cb.buffer.end()));
         NS_LOG_LOGIC("Current min element" << cb.buffer[cb.min]);
-        for (uint32_t i = 1; i < maxlen - 1; i++)
-        {
-            if (cb.buffer[i] < cb.buffer[cb.min])
-            {
-                cb.min = i;
-            }
-        }
     }
 }
 
@@ -268,16 +267,16 @@
         AddDelay(m_baseHistory, owd, m_baseHistoLen);
         return;
     }
-    uint64_t timestamp = static_cast<uint64_t>(Simulator::Now().GetSeconds());
+    Time timestamp = Simulator::Now();
 
-    if (timestamp - m_lastRollover > 60)
+    if ((timestamp - m_lastRollover) > Seconds(60))
     {
         m_lastRollover = timestamp;
         AddDelay(m_baseHistory, owd, m_baseHistoLen);
     }
     else
     {
-        auto last = static_cast<uint32_t>(m_baseHistory.buffer.size() - 1);
+        size_t last = m_baseHistory.buffer.size() - 1;
         if (owd < m_baseHistory.buffer[last])
         {
             m_baseHistory.buffer[last] = owd;
@@ -301,12 +300,11 @@
     {
         m_flag |= LEDBAT_VALID_OWD;
     }
-    if (rtt.IsPositive())
+    if (rtt.IsPositive() && tcb->m_rcvTimestampValue >= tcb->m_rcvTimestampEchoReply)
     {
-        AddDelay(m_noiseFilter,
-                 tcb->m_rcvTimestampValue - tcb->m_rcvTimestampEchoReply,
-                 m_noiseFilterLen);
-        UpdateBaseDelay(tcb->m_rcvTimestampValue - tcb->m_rcvTimestampEchoReply);
+        uint32_t owd = tcb->m_rcvTimestampValue - tcb->m_rcvTimestampEchoReply;
+        AddDelay(m_noiseFilter, owd, m_noiseFilterLen);
+        UpdateBaseDelay(owd);
     }
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-ledbat.h ns-3.47/src/internet/model/tcp-ledbat.h
--- ns-3.46/src/internet/model/tcp-ledbat.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-ledbat.h	2026-02-16 16:08:49.619230000 -0800
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: GPL-2.0-only
  *
  * Author: Ankit Deepak <adadeepak8@gmail.com>
+ * Modified by: S B L Prateek <sblprateek@gmail.com>
  *
  */
 
@@ -120,7 +121,7 @@
     struct OwdCircBuf
     {
         std::vector<uint32_t> buffer; //!< Vector to store the delay
-        uint32_t min;                 //!< The index of minimum value
+        size_t min;                   //!< The index of minimum value
     };
 
     /**
@@ -177,12 +178,13 @@
     SlowStartType m_doSs;      //!< Permissible Slow Start State
     uint32_t m_baseHistoLen;   //!< Length of base delay history buffer
     uint32_t m_noiseFilterLen; //!< Length of current delay buffer
-    uint64_t m_lastRollover;   //!< Timestamp of last added delay
-    int32_t m_sndCwndCnt;      //!< The congestion window addition parameter
+    Time m_lastRollover;       //!< Timestamp of last added delay
+    double m_sndCwndCnt;       //!< The congestion window addition parameter
     OwdCircBuf m_baseHistory;  //!< Buffer to store the base delay
     OwdCircBuf m_noiseFilter;  //!< Buffer to store the current delay
     uint32_t m_flag;           //!< LEDBAT Flag
     uint32_t m_minCwnd;        //!< Minimum cWnd value mentioned in RFC 6817
+    double m_allowedIncrease;  //!< ALLOWED INCREASE value mentioned in RFC 6817
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-linux-reno.cc ns-3.47/src/internet/model/tcp-linux-reno.cc
--- ns-3.46/src/internet/model/tcp-linux-reno.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-linux-reno.cc	2026-02-16 16:08:49.620230000 -0800
@@ -25,7 +25,18 @@
     static TypeId tid = TypeId("ns3::TcpLinuxReno")
                             .SetParent<TcpCongestionOps>()
                             .SetGroupName("Internet")
-                            .AddConstructor<TcpLinuxReno>();
+                            .AddConstructor<TcpLinuxReno>()
+                            .AddAttribute("BetaLoss",
+                                          "Beta for multiplicative decrease",
+                                          DoubleValue(0.5),
+                                          MakeDoubleAccessor(&TcpLinuxReno::m_betaLoss),
+                                          MakeDoubleChecker<double>(0.0, 1.0))
+                            .AddAttribute("BetaEcn",
+                                          "Beta for multiplicative decrease for ABE",
+                                          DoubleValue(0.7), // According to RFC 8511 (ABE)
+                                          MakeDoubleAccessor(&TcpLinuxReno::m_betaEcn),
+                                          MakeDoubleChecker<double>(0.0, 1.0));
+
     return tid;
 }
 
@@ -136,8 +147,13 @@
 {
     NS_LOG_FUNCTION(this << state << bytesInFlight);
 
-    // In Linux, it is written as:  return max(tp->snd_cwnd >> 1U, 2U);
-    return std::max<uint32_t>(2 * state->m_segmentSize, state->m_cWnd / 2);
+    if (state->m_abeEnabled && state->m_ecnState == TcpSocketState::ECN_ECE_RCVD)
+    {
+        return std::max<uint32_t>(2 * state->m_segmentSize,
+                                  state->m_cWnd * m_betaEcn); // According to RFC 8511 (ABE)
+    }
+
+    return std::max<uint32_t>(2 * state->m_segmentSize, state->m_cWnd * m_betaLoss);
 }
 
 Ptr<TcpCongestionOps>
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-linux-reno.h ns-3.47/src/internet/model/tcp-linux-reno.h
--- ns-3.46/src/internet/model/tcp-linux-reno.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-linux-reno.h	2026-02-16 16:08:49.620230000 -0800
@@ -84,6 +84,9 @@
     // tcp_reno_cong_avoid()
     bool m_suppressIncreaseIfCwndLimited{
         true}; //!< Suppress window increase if TCP is not cwnd limited
+
+    double m_betaLoss; //!< Beta for Linux-Reno multiplicative decrease
+    double m_betaEcn;  //!< Beta for Linux-Reno multiplicative decrease with ABE
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-option.h ns-3.47/src/internet/model/tcp-option.h
--- ns-3.46/src/internet/model/tcp-option.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-option.h	2026-02-16 16:08:49.620230000 -0800
@@ -71,7 +71,7 @@
     virtual uint32_t Deserialize(Buffer::Iterator start) = 0;
 
     /**
-     * @brief Get the `kind' (as in \RFC{793}) of this option
+     * @brief Get the `kind` (as in \RFC{793}) of this option
      * @return the Option Kind
      */
     virtual uint8_t GetKind() const = 0;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-socket-base.cc ns-3.47/src/internet/model/tcp-socket-base.cc
--- ns-3.46/src/internet/model/tcp-socket-base.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-socket-base.cc	2026-02-16 16:08:49.621230000 -0800
@@ -139,6 +139,11 @@
                           BooleanValue(true),
                           MakeBooleanAccessor(&TcpSocketBase::m_timestampEnabled),
                           MakeBooleanChecker())
+            .AddAttribute("Fack",
+                          "Enable or disable FACK option",
+                          BooleanValue(false),
+                          MakeBooleanAccessor(&TcpSocketBase::m_fackEnabled),
+                          MakeBooleanChecker())
             .AddAttribute(
                 "MinRto",
                 "Minimum retransmit timeout value",
@@ -194,6 +199,11 @@
                                           "On",
                                           TcpSocketState::AcceptOnly,
                                           "AcceptOnly"))
+            .AddAttribute("UseAbe",
+                          "Parameter to set ABE functionality",
+                          BooleanValue(false),
+                          MakeBooleanAccessor(&TcpSocketBase::m_useAbe),
+                          MakeBooleanChecker())
             .AddTraceSource("RTO",
                             "Retransmission timeout",
                             MakeTraceSourceAccessor(&TcpSocketBase::m_rto),
@@ -238,6 +248,10 @@
                             "Socket estimation of bytes in flight",
                             MakeTraceSourceAccessor(&TcpSocketBase::m_bytesInFlightTrace),
                             "ns3::TracedValueCallback::Uint32")
+            .AddTraceSource("FackAwnd",
+                            "Socket estimation of bytes in flight by FACK",
+                            MakeTraceSourceAccessor(&TcpSocketBase::m_fackAwndTrace),
+                            "ns3::TracedValueCallback::Uint32")
             .AddTraceSource("HighestRxSequence",
                             "Highest sequence number received from peer",
                             MakeTraceSourceAccessor(&TcpSocketBase::m_highRxMark),
@@ -293,11 +307,15 @@
     : TcpSocket()
 {
     NS_LOG_FUNCTION(this);
+
     m_txBuffer = CreateObject<TcpTxBuffer>();
     m_txBuffer->SetRWndCallback(MakeCallback(&TcpSocketBase::GetRWnd, this));
     m_tcb = CreateObject<TcpSocketState>();
     m_rateOps = CreateObject<TcpRateLinux>();
 
+    m_sndFack = 0;
+    m_outstandingRetransBytes = 0;
+
     m_tcb->m_rxBuffer = CreateObject<TcpRxBuffer>();
 
     m_tcb->m_pacingRate = m_tcb->m_maxPacingRate;
@@ -310,47 +328,59 @@
     ok = m_tcb->TraceConnectWithoutContext(
         "PacingRate",
         MakeCallback(&TcpSocketBase::UpdatePacingRateTrace, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source PacingRate");
 
     ok = m_tcb->TraceConnectWithoutContext("CongestionWindow",
                                            MakeCallback(&TcpSocketBase::UpdateCwnd, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source CongestionWindow");
 
     ok = m_tcb->TraceConnectWithoutContext("CongestionWindowInflated",
                                            MakeCallback(&TcpSocketBase::UpdateCwndInfl, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source CongestionWindowInflated");
 
     ok = m_tcb->TraceConnectWithoutContext("SlowStartThreshold",
                                            MakeCallback(&TcpSocketBase::UpdateSsThresh, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source SlowStartThreshold");
 
     ok = m_tcb->TraceConnectWithoutContext("CongState",
                                            MakeCallback(&TcpSocketBase::UpdateCongState, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source CongState");
 
     ok = m_tcb->TraceConnectWithoutContext("EcnState",
                                            MakeCallback(&TcpSocketBase::UpdateEcnState, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source EcnState");
 
     ok =
         m_tcb->TraceConnectWithoutContext("NextTxSequence",
                                           MakeCallback(&TcpSocketBase::UpdateNextTxSequence, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source NextTxSequence");
 
     ok = m_tcb->TraceConnectWithoutContext("HighestSequence",
                                            MakeCallback(&TcpSocketBase::UpdateHighTxMark, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source HighestSequence");
 
     ok = m_tcb->TraceConnectWithoutContext("BytesInFlight",
                                            MakeCallback(&TcpSocketBase::UpdateBytesInFlight, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source BytesInFlight");
+
+    ok = m_tcb->TraceConnectWithoutContext("FackAwnd",
+                                           MakeCallback(&TcpSocketBase::UpdateFackAwnd, this));
+    NS_ASSERT_MSG(ok, "Could not connect trace source FackAwnd");
 
     ok = m_tcb->TraceConnectWithoutContext("RTT", MakeCallback(&TcpSocketBase::UpdateRtt, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source RTT");
 
     ok = m_tcb->TraceConnectWithoutContext("LastRTT",
                                            MakeCallback(&TcpSocketBase::UpdateLastRtt, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source LastRTT");
+}
+
+void
+TcpSocketBase::NotifyConstructionCompleted()
+{
+    NS_LOG_FUNCTION(this);
+
+    SetUseAbe(m_useAbe);
 }
 
 TcpSocketBase::TcpSocketBase(const TcpSocketBase& sock)
@@ -393,6 +423,7 @@
       m_sndWindShift(sock.m_sndWindShift),
       m_timestampEnabled(sock.m_timestampEnabled),
       m_timestampToEcho(sock.m_timestampToEcho),
+      m_fackEnabled(sock.m_fackEnabled),
       m_recover(sock.m_recover),
       m_recoverActive(sock.m_recoverActive),
       m_retxThresh(sock.m_retxThresh),
@@ -448,51 +479,57 @@
         m_tcb->m_sendEmptyPacketCallback = MakeCallback(&TcpSocketBase::SendEmptyPacket, this);
     }
 
+    m_sndFack = sock.m_sndFack;
+    m_outstandingRetransBytes = sock.m_outstandingRetransBytes;
+
     bool ok;
 
     ok = m_tcb->TraceConnectWithoutContext(
         "PacingRate",
         MakeCallback(&TcpSocketBase::UpdatePacingRateTrace, this));
+    NS_ASSERT_MSG(ok, "Could not connect trace source PacingRate");
 
     ok = m_tcb->TraceConnectWithoutContext("CongestionWindow",
                                            MakeCallback(&TcpSocketBase::UpdateCwnd, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source CongestionWindow");
 
     ok = m_tcb->TraceConnectWithoutContext("CongestionWindowInflated",
                                            MakeCallback(&TcpSocketBase::UpdateCwndInfl, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source CongestionWindowInflated");
 
     ok = m_tcb->TraceConnectWithoutContext("SlowStartThreshold",
                                            MakeCallback(&TcpSocketBase::UpdateSsThresh, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source SlowStartThreshold");
 
     ok = m_tcb->TraceConnectWithoutContext("CongState",
                                            MakeCallback(&TcpSocketBase::UpdateCongState, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source CongState");
 
     ok = m_tcb->TraceConnectWithoutContext("EcnState",
                                            MakeCallback(&TcpSocketBase::UpdateEcnState, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source EcnState");
 
     ok =
         m_tcb->TraceConnectWithoutContext("NextTxSequence",
                                           MakeCallback(&TcpSocketBase::UpdateNextTxSequence, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source NextTxSequence");
 
     ok = m_tcb->TraceConnectWithoutContext("HighestSequence",
                                            MakeCallback(&TcpSocketBase::UpdateHighTxMark, this));
-    NS_ASSERT(ok == true);
-
+    NS_ASSERT_MSG(ok, "Could not connect trace source HighestSequence");
     ok = m_tcb->TraceConnectWithoutContext("BytesInFlight",
                                            MakeCallback(&TcpSocketBase::UpdateBytesInFlight, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source BytesInFlight");
+
+    ok = m_tcb->TraceConnectWithoutContext("FackAwnd",
+                                           MakeCallback(&TcpSocketBase::UpdateFackAwnd, this));
 
     ok = m_tcb->TraceConnectWithoutContext("RTT", MakeCallback(&TcpSocketBase::UpdateRtt, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source RTT");
 
     ok = m_tcb->TraceConnectWithoutContext("LastRTT",
                                            MakeCallback(&TcpSocketBase::UpdateLastRtt, this));
-    NS_ASSERT(ok == true);
+    NS_ASSERT_MSG(ok, "Could not connect trace source LastRTT");
 }
 
 TcpSocketBase::~TcpSocketBase()
@@ -1791,6 +1828,16 @@
         // CA_RECOVERY and reducing sending rate again.
         NS_ASSERT((m_dupAckCount <= m_retxThresh) || m_recoverActive);
 
+        uint32_t fackDiff = 0;
+        if (m_fackEnabled)
+        {
+            uint32_t headSeq = m_txBuffer->HeadSequence().GetValue();
+            if (m_sndFack > headSeq)
+            {
+                fackDiff = m_sndFack - headSeq;
+            }
+        }
+
         // RFC 6675, Section 5, continuing:
         // ... and take the following steps:
         // (1) If DupAcks >= DupThresh, go to step (4).
@@ -1802,8 +1849,12 @@
         //     bandwidth-greedy application in high speed and reliable network
         //     (such as datacenter network) whose sending rate is constrained by
         //     TCP socket buffer size at receiver side.
-        if ((m_dupAckCount == m_retxThresh) &&
-            ((m_highRxAckMark >= m_recover) || (!m_recoverActive)))
+
+        // Check FACK recovery condition
+
+        if ((m_fackEnabled && fackDiff > m_tcb->m_segmentSize * 3) ||
+            ((m_dupAckCount == m_retxThresh) &&
+             (m_highRxAckMark >= m_recover || (!m_recoverActive))))
         {
             EnterRecovery(currentDelivered);
             NS_ASSERT(m_tcb->m_congState == TcpSocketState::CA_RECOVERY);
@@ -1996,6 +2047,19 @@
      * reaching this code if SYN or FIN is set, and e) is not supported.
      */
 
+    if (m_fackEnabled && ackNumber == m_txBuffer->HeadSequence() &&
+        m_tcb->m_congState == TcpSocketState::CA_RECOVERY)
+    {
+        if (m_outstandingRetransBytes > m_tcb->m_segmentSize)
+        {
+            m_outstandingRetransBytes -= m_tcb->m_segmentSize;
+        }
+        else
+        {
+            m_outstandingRetransBytes = 0;
+        }
+    }
+
     bool isDupack = m_sackEnabled ? scoreboardUpdated
                                   : (ackNumber == oldHeadSequence &&
                                      ackNumber < m_tcb->m_highTxMark && !receivedData);
@@ -3565,9 +3629,35 @@
 uint32_t
 TcpSocketBase::AvailableWindow() const
 {
-    uint32_t win = Window();             // Number of bytes allowed to be outstanding
-    uint32_t inflight = BytesInFlight(); // Number of outstanding bytes
-    return (inflight > win) ? 0 : win - inflight;
+    uint32_t win = Window(); // Number of bytes allowed to be outstanding
+
+    if (m_sackEnabled && m_fackEnabled && win >= m_tcb->m_ssThresh)
+    {
+        // Update awnd (Data sender's estimate of the actual quantity of data outstanding in the
+        // network)
+        NS_LOG_DEBUG("FACK is enabled and win >= ssthresh (" << m_tcb->m_ssThresh << ")");
+
+        uint32_t sndNxt = (m_tcb->m_highTxMark.Get().GetValue());
+        uint32_t retranData = m_txBuffer->GetRetransmitsCount();
+        uint32_t awnd = sndNxt - m_sndFack + retranData;
+        m_tcb->m_fackAwnd = awnd;
+
+        NS_LOG_DEBUG("SndNxt: " << sndNxt << ", SndFack: " << m_sndFack << ", AWND: " << awnd
+                                << ", RetranData :" << retranData);
+
+        uint32_t awndDiff = (win > awnd) ? (win - awnd) : 0;
+        NS_LOG_DEBUG("AWND: " << awnd << ", win: " << win << ", AWND_DIFF: " << awndDiff);
+        return awndDiff;
+    }
+
+    uint32_t inflight = BytesInFlight();
+
+    if (inflight >= win)
+    {
+        return 0;
+    }
+
+    return win - inflight;
 }
 
 uint16_t
@@ -3813,6 +3903,13 @@
     // Reset the data retransmission count. We got a new ACK!
     m_dataRetrCount = m_dataRetries;
 
+    // Update m_sndFack if possible
+    if (m_fackEnabled && ack.GetValue() > m_sndFack)
+    {
+        NS_LOG_INFO(" m_sndFack " << m_sndFack << " updated by normal ack to " << ack.GetValue());
+        m_sndFack = ack.GetValue();
+    }
+
     if (m_state != SYN_RCVD && resetRTO)
     { // Set RTO unless the ACK is received in SYN_RCVD state
         NS_LOG_LOGIC(
@@ -4160,6 +4257,18 @@
     return m_txBuffer->MaxBufferSize();
 }
 
+uint32_t
+TcpSocketBase::GetSndFack() const
+{
+    return m_sndFack;
+}
+
+bool
+TcpSocketBase::GetFackEnabled() const
+{
+    return m_fackEnabled;
+}
+
 void
 TcpSocketBase::SetRcvBufSize(uint32_t size)
 {
@@ -4397,6 +4506,21 @@
     NS_LOG_FUNCTION(this << option);
 
     Ptr<const TcpOptionSack> s = DynamicCast<const TcpOptionSack>(option);
+
+    // Update m_sndFack with the highest sequence number acknowledged from the SACK blocks
+    if (m_fackEnabled)
+    {
+        for (const auto& [leftEdge, rightEdge] : s->GetSackList())
+        {
+            if (rightEdge.GetValue() > m_sndFack)
+            {
+                NS_LOG_INFO(" m_sndFack updated from " << m_sndFack << " to "
+                                                       << rightEdge.GetValue());
+                m_sndFack = rightEdge.GetValue();
+            }
+        }
+    }
+
     return m_txBuffer->Update(s->GetSackList(), MakeCallback(&TcpRateOps::SkbDelivered, m_rateOps));
 }
 
@@ -4642,6 +4766,12 @@
 }
 
 void
+TcpSocketBase::UpdateFackAwnd(uint32_t oldValue, uint32_t newValue) const
+{
+    m_fackAwndTrace(oldValue, newValue);
+}
+
+void
 TcpSocketBase::UpdateRtt(Time oldValue, Time newValue) const
 {
     m_srttTrace(oldValue, newValue);
@@ -4806,6 +4936,24 @@
     m_tcb->m_useEcn = useEcn;
 }
 
+void
+TcpSocketBase::SetUseAbe(bool useAbe)
+{
+    NS_LOG_FUNCTION(this << useAbe);
+    if (m_tcb->m_useEcn == TcpSocketState::Off && useAbe)
+    {
+        NS_LOG_INFO("Enabling ECN along with ABE");
+        m_tcb->m_useEcn = TcpSocketState::On;
+    }
+    m_tcb->m_abeEnabled = useAbe;
+}
+
+bool
+TcpSocketBase::GetUseAbe() const
+{
+    return m_tcb->m_abeEnabled;
+}
+
 uint32_t
 TcpSocketBase::GetRWnd() const
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-socket-base.h ns-3.47/src/internet/model/tcp-socket-base.h
--- ns-3.46/src/internet/model/tcp-socket-base.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-socket-base.h	2026-02-16 16:08:49.621230000 -0800
@@ -370,6 +370,11 @@
     TracedCallback<uint32_t, uint32_t> m_bytesInFlightTrace;
 
     /**
+     * @brief Callback pointer for fackAwnd trace chaining
+     */
+    TracedCallback<uint32_t, uint32_t> m_fackAwndTrace;
+
+    /**
      * @brief Callback pointer for RTT trace chaining
      */
     TracedCallback<Time, Time> m_srttTrace;
@@ -445,6 +450,13 @@
     void UpdateBytesInFlight(uint32_t oldValue, uint32_t newValue) const;
 
     /**
+     * @brief Callback function to hook to TcpSocketState awnd(FACK's inflight)
+     * @param oldValue old awnd value
+     * @param newValue new awnd value
+     */
+    void UpdateFackAwnd(uint32_t oldValue, uint32_t newValue) const;
+
+    /**
      * @brief Callback function to hook to TcpSocketState rtt
      * @param oldValue old rtt value
      * @param newValue new rtt value
@@ -579,7 +591,18 @@
      * @param useEcn Mode of ECN to use.
      */
     void SetUseEcn(TcpSocketState::UseEcn_t useEcn);
-
+    /**
+     * @brief Set ABE mode of use on the socket
+     *
+     * @param useAbe Mode of ABE to use.
+     */
+    void SetUseAbe(bool useAbe);
+    /**
+     * @brief Get ABE mode of use on the socket
+     *
+     * @return true if ABE is enabled, false otherwise.
+     */
+    bool GetUseAbe() const;
     /**
      * @brief Enable or disable pacing
      * @param pacing Boolean to enable or disable pacing
@@ -661,6 +684,26 @@
                                            const Address& peerAddr,
                                            const Ptr<const TcpSocketBase> socket);
 
+    // Variables for FACK
+    uint32_t m_sndFack;                  //!< Sequence number of the forward most acknowledgement
+    uint32_t m_outstandingRetransBytes;  //!< Number of outstanding retransmitted bytes
+    TracedValue<uint32_t> m_fackAwnd{0}; //!< Tracks the inflight data calculated by FACK algorithm.
+
+    /**
+     * @brief Get the current FACK sequence number.
+     *
+     * @return The sequence number up to which data is considered
+     *         cumulatively acknowledged by FACK.
+     */
+    uint32_t GetSndFack() const;
+
+    /**
+     * @brief Check whether Forward Acknowledgment (FACK) is enabled.
+     *
+     * @return true if FACK is enabled, false otherwise.
+     */
+    bool GetFackEnabled() const;
+
   protected:
     // Implementing ns3::TcpSocket -- Attribute get/set
     // inherited, no need to doc
@@ -691,6 +734,7 @@
     Time GetPersistTimeout() const override;
     bool SetAllowBroadcast(bool allowBroadcast) override;
     bool GetAllowBroadcast() const override;
+    void NotifyConstructionCompleted() override;
 
     // Helper functions: Connection set up
 
@@ -1394,6 +1438,10 @@
     bool m_shutdownSend{false};  //!< Send no longer allowed
     bool m_shutdownRecv{false};  //!< Receive no longer allowed
     bool m_connected{false};     //!< Connection established
+    bool m_useAbe{false};        //!< ABE mode
+                                 //!< It will override the UseEcn attribute
+                                 //!< if it is 'Off' and set it to 'On', but
+                                 //!< will leave it untouched if it is 'AcceptOnly'
     double m_msl{0.0};           //!< Max segment lifetime
 
     // Window management
@@ -1413,6 +1461,8 @@
     bool m_timestampEnabled{true};  //!< Timestamp option enabled
     uint32_t m_timestampToEcho{0};  //!< Timestamp to echo
 
+    bool m_fackEnabled{false}; //!< flag for enabling FACK
+
     EventId m_sendPendingDataEvent{}; //!< micro-delay event to send pending data
 
     // Fast Retransmit and Recovery
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-socket-state.cc ns-3.47/src/internet/model/tcp-socket-state.cc
--- ns-3.46/src/internet/model/tcp-socket-state.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-socket-state.cc	2026-02-16 16:08:49.621230000 -0800
@@ -79,6 +79,11 @@
                             "The TCP connection's congestion window",
                             MakeTraceSourceAccessor(&TcpSocketState::m_bytesInFlight),
                             "ns3::TracedValueCallback::Uint32")
+            .AddTraceSource(
+                "FackAwnd",
+                "Estimate of amount of outstanding data in the network (if FACK option is enabled)",
+                MakeTraceSourceAccessor(&TcpSocketState::m_fackAwnd),
+                "ns3::TracedValueCallback::Uint32")
             .AddTraceSource("RTT",
                             "Smoothed RTT",
                             MakeTraceSourceAccessor(&TcpSocketState::m_srtt),
@@ -117,6 +122,7 @@
       m_lastRtt(other.m_lastRtt),
       m_ecnMode(other.m_ecnMode),
       m_useEcn(other.m_useEcn),
+      m_abeEnabled(other.m_abeEnabled),
       m_ectCodePoint(other.m_ectCodePoint),
       m_lastAckedSackedBytes(other.m_lastAckedSackedBytes)
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-socket-state.h ns-3.47/src/internet/model/tcp-socket-state.h
--- ns-3.46/src/internet/model/tcp-socket-state.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-socket-state.h	2026-02-16 16:08:49.622230000 -0800
@@ -197,15 +197,17 @@
     Time m_minRtt{Time::Max()}; //!< Minimum RTT observed throughout the connection
 
     TracedValue<uint32_t> m_bytesInFlight{0}; //!< Bytes in flight
-    bool m_isCwndLimited{false};              //!< Whether throughput is limited by cwnd
-    TracedValue<Time> m_srtt;                 //!< Smoothed RTT
-    TracedValue<Time> m_lastRtt;              //!< RTT of the last (S)ACKed packet
+    TracedValue<uint32_t> m_fackAwnd{0};      //!< inflight data by FACK
 
-    Ptr<TcpRxBuffer> m_rxBuffer; //!< Rx buffer (reordering buffer)
+    bool m_isCwndLimited{false}; //!< Whether throughput is limited by cwnd
+    TracedValue<Time> m_srtt;    //!< Smoothed RTT
+    TracedValue<Time> m_lastRtt; //!< RTT of the last (S)ACKed packet
 
-    EcnMode_t m_ecnMode{ClassicEcn}; //!< ECN mode
-    UseEcn_t m_useEcn{Off};          //!< Socket ECN capability
+    Ptr<TcpRxBuffer> m_rxBuffer; //!< Rx buffer (reordering buffer)
 
+    EcnMode_t m_ecnMode{ClassicEcn};     //!< ECN mode
+    UseEcn_t m_useEcn{Off};              //!< Socket ECN capability
+    bool m_abeEnabled{false};            //!< ABE mode
     EcnCodePoint_t m_ectCodePoint{Ect0}; //!< ECT code point to use
 
     uint32_t m_lastAckedSackedBytes{
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-vegas.h ns-3.47/src/internet/model/tcp-vegas.h
--- ns-3.46/src/internet/model/tcp-vegas.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-vegas.h	2026-02-16 16:08:49.622230000 -0800
@@ -122,7 +122,6 @@
 
     Ptr<TcpCongestionOps> Fork() override;
 
-  protected:
   private:
     /**
      * @brief Enable Vegas algorithm to start taking Vegas samples
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-veno.h ns-3.47/src/internet/model/tcp-veno.h
--- ns-3.46/src/internet/model/tcp-veno.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-veno.h	2026-02-16 16:08:49.622230000 -0800
@@ -128,7 +128,6 @@
 
     Ptr<TcpCongestionOps> Fork() override;
 
-  protected:
   private:
     /**
      * @brief Enable Veno algorithm to start Veno sampling
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/model/tcp-yeah.h ns-3.47/src/internet/model/tcp-yeah.h
--- ns-3.46/src/internet/model/tcp-yeah.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/model/tcp-yeah.h	2026-02-16 16:08:49.622230000 -0800
@@ -129,7 +129,6 @@
 
     Ptr<TcpCongestionOps> Fork() override;
 
-  protected:
   private:
     /**
      * @brief Enable YeAH algorithm to start taking YeAH samples
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/global-route-manager-impl-test-suite.cc ns-3.47/src/internet/test/global-route-manager-impl-test-suite.cc
--- ns-3.46/src/internet/test/global-route-manager-impl-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/global-route-manager-impl-test-suite.cc	2026-02-16 16:08:49.623230000 -0800
@@ -15,10 +15,11 @@
 #include "ns3/candidate-queue.h"
 #include "ns3/config.h"
 #include "ns3/global-route-manager-impl.h"
+#include "ns3/global-routing.h"
 #include "ns3/internet-module.h"
 #include "ns3/internet-stack-helper.h"
 #include "ns3/ipv4-global-routing-helper.h"
-#include "ns3/ipv4-global-routing.h"
+#include "ns3/ipv6-global-routing-helper.h"
 #include "ns3/network-module.h"
 #include "ns3/node-container.h"
 #include "ns3/rng-seed-manager.h"
@@ -76,7 +77,24 @@
      *@brief Builds the LSAs for the topology. These LSAs are manually created and inserted into the
      * GlobalRouteManagerLSDB.Each node exports a router LSA.
      */
-    void BuildLsa();
+    void BuildLsav4();
+
+    /**
+     *@brief Builds the LSAs for the topology. These LSAs are manually created and inserted into the
+     * GlobalRouteManagerLSDB.Each node exports a router LSA.
+     */
+    void BuildLsav6();
+
+    /**
+     *  @brief Checks the Routing Table Entries for the expected output.
+     * @param globalroutingprotocol The routing protocol for the node whose routing table is to be
+     * checked.
+     * @param dests The expected destinations.
+     * @param gws The expected gateways.
+     */
+    void CheckRoutesv4(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
+                       std::vector<Ipv4Address>& dests,
+                       std::vector<Ipv4Address>& gws);
 
     /**
      *  @brief Checks the Routing Table Entries for the expected output.
@@ -85,12 +103,13 @@
      * @param dests The expected destinations.
      * @param gws The expected gateways.
      */
-    void CheckRoutes(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
-                     std::vector<Ipv4Address>& dests,
-                     std::vector<Ipv4Address>& gws);
+    void CheckRoutesv6(Ptr<Ipv6GlobalRouting>& globalroutingprotocol,
+                       std::vector<Ipv6Address>& dests,
+                       std::vector<Ipv6Address>& gws);
 
-    NodeContainer nodes;                   //!< NodeContainer to hold the nodes in the topology
-    std::vector<GlobalRoutingLSA*> m_lsas; //!< The LSAs for the topology
+    NodeContainer nodes; //!< NodeContainer to hold the nodes in the topology
+    std::vector<GlobalRoutingLSA<Ipv4Manager>*> m_lsasv4; //!< The LSAs for the topology
+    std::vector<GlobalRoutingLSA<Ipv6Manager>*> m_lsasv6; //!< The LSAs for the topology
 };
 
 LinkRoutesTestCase::LinkRoutesTestCase()
@@ -99,9 +118,9 @@
 }
 
 void
-LinkRoutesTestCase::CheckRoutes(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
-                                std::vector<Ipv4Address>& dests,
-                                std::vector<Ipv4Address>& gws)
+LinkRoutesTestCase::CheckRoutesv4(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
+                                  std::vector<Ipv4Address>& dests,
+                                  std::vector<Ipv4Address>& gws)
 {
     // check each individual Routing Table Entry for its destination and gateway
     for (uint32_t i = 0; i < globalroutingprotocol->GetNRoutes(); i++)
@@ -114,6 +133,21 @@
 }
 
 void
+LinkRoutesTestCase::CheckRoutesv6(Ptr<Ipv6GlobalRouting>& globalroutingprotocol,
+                                  std::vector<Ipv6Address>& dests,
+                                  std::vector<Ipv6Address>& gws)
+{
+    // check each individual Routing Table Entry for its destination and gateway
+    for (uint32_t i = 0; i < globalroutingprotocol->GetNRoutes(); i++)
+    {
+        Ipv6RoutingTableEntry* route = globalroutingprotocol->GetRoute(i);
+        NS_LOG_DEBUG("dest " << route->GetDest() << " gw " << route->GetGateway());
+        NS_TEST_ASSERT_MSG_EQ(route->GetDest(), dests[i], "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), gws[i], "Error-- wrong gateway");
+    }
+}
+
+void
 LinkRoutesTestCase::DoSetup()
 {
     // Simple p2p links. n0,n1 and n3 are stub nodes
@@ -136,9 +170,11 @@
 
     nodes.Create(4);
 
-    Ipv4GlobalRoutingHelper globalhelper;
+    Ipv4GlobalRoutingHelper globalhelperv4;
+    Ipv6GlobalRoutingHelper globalhelperv6;
     InternetStackHelper stack;
-    stack.SetRoutingHelper(globalhelper);
+    stack.SetRoutingHelper(globalhelperv4);
+    stack.SetRoutingHelper(globalhelperv6);
     stack.Install(nodes);
 
     SimpleNetDeviceHelper devHelper;
@@ -161,87 +197,105 @@
     Ipv4InterfaceContainer i12 = address.Assign(d12);
     address.SetBase("10.1.3.0", "255.255.255.252");
     Ipv4InterfaceContainer i23 = address.Assign(d23);
+
+    Ipv6AddressHelper addressv6;
+    addressv6.SetBase("2001:1::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i02v6 = addressv6.Assign(d02);
+    addressv6.SetBase("2001:2::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i12v6 = addressv6.Assign(d12);
+    addressv6.SetBase("2001:3::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i23v6 = addressv6.Assign(d23);
 }
 
 void
-LinkRoutesTestCase::BuildLsa()
+LinkRoutesTestCase::BuildLsav4()
 {
     // Manually build the link state database; four routers (0-3), 3 point-to-point
     // links
 
     // Router 0
-    auto lr0 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.2",  // link id -> router ID 0.0.0.2
-                                           "10.1.1.1", // link data -> Local IP address of router 0
-                                           1);         // metric
-
-    auto lr1 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.1.2", // link id ->adjacent neighbor's IP address
-                                           "255.255.255.252",
-                                           1);
+    auto lr0 = new GlobalRoutingLinkRecord<Ipv4Manager>(
+        GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+        "0.0.0.2",  // link id -> router ID 0.0.0.2
+        "10.1.1.1", // link data -> Local IP address of router 0
+        1);         // metric
+
+    auto lr1 = new GlobalRoutingLinkRecord<Ipv4Manager>(
+        GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+        "10.1.1.2", // link id ->adjacent neighbor's IP address
+        "255.255.255.252",
+        1);
 
-    auto lsa0 = new GlobalRoutingLSA();
-    lsa0->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lsa0 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa0->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa0->SetLinkStateId("0.0.0.0");
     lsa0->SetAdvertisingRouter("0.0.0.0");
     lsa0->SetNode(nodes.Get(0));
     lsa0->AddLinkRecord(lr0);
     lsa0->AddLinkRecord(lr1);
-    m_lsas.push_back(lsa0);
+    m_lsasv4.push_back(lsa0);
 
     // Router 1
-    auto lr2 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.2",
-                                           "10.1.2.1",
-                                           1);
-
-    auto lr3 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.2.2",
-                                           "255.255.255.252",
-                                           1);
+    auto lr2 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.2",
+                                                 "10.1.2.1",
+                                                 1);
+
+    auto lr3 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.2.2",
+                                                 "255.255.255.252",
+                                                 1);
 
-    auto lsa1 = new GlobalRoutingLSA();
-    lsa1->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lsa1 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa1->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa1->SetLinkStateId("0.0.0.1");
     lsa1->SetAdvertisingRouter("0.0.0.1");
     lsa1->AddLinkRecord(lr2);
     lsa1->AddLinkRecord(lr3);
     lsa1->SetNode(nodes.Get(1));
-    m_lsas.push_back(lsa1);
+    m_lsasv4.push_back(lsa1);
 
     // Router 2
-    auto lr4 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.0",
-                                           "10.1.1.2",
-                                           1);
-
-    auto lr5 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.1.1",
-                                           "255.255.255.252",
-                                           1);
-
-    auto lr6 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.1",
-                                           "10.1.2.2",
-                                           1);
-
-    auto lr7 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.2.2",
-                                           "255.255.255.252",
-                                           1);
-
-    auto lr8 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.3",
-                                           "10.1.3.1",
-                                           1);
-
-    auto lr9 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.3.2",
-                                           "255.255.255.252",
-                                           1);
+    auto lr4 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.0",
+                                                 "10.1.1.2",
+                                                 1);
+
+    auto lr5 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.1.1",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lr6 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.1",
+                                                 "10.1.2.2",
+                                                 1);
+
+    auto lr7 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.2.2",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lr8 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.3",
+                                                 "10.1.3.1",
+                                                 1);
+
+    auto lr9 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.3.2",
+                                                 "255.255.255.252",
+                                                 1);
 
-    auto lsa2 = new GlobalRoutingLSA();
-    lsa2->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lsa2 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa2->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa2->SetLinkStateId("0.0.0.2");
     lsa2->SetAdvertisingRouter("0.0.0.2");
     lsa2->AddLinkRecord(lr4);
@@ -251,27 +305,166 @@
     lsa2->AddLinkRecord(lr8);
     lsa2->AddLinkRecord(lr9);
     lsa2->SetNode(nodes.Get(2));
-    m_lsas.push_back(lsa2);
+    m_lsasv4.push_back(lsa2);
 
     // Router 3
-    auto lr10 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                            "0.0.0.2",
-                                            "10.1.3.2",
-                                            1);
-
-    auto lr11 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                            "10.1.3.1",
-                                            "255.255.255.252",
-                                            1);
+    auto lr10 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.2",
+                                                 "10.1.3.2",
+                                                 1);
+
+    auto lr11 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.3.1",
+                                                 "255.255.255.252",
+                                                 1);
 
-    auto lsa3 = new GlobalRoutingLSA();
-    lsa3->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lsa3 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa3->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa3->SetLinkStateId("0.0.0.3");
     lsa3->SetAdvertisingRouter("0.0.0.3");
     lsa3->AddLinkRecord(lr10);
     lsa3->AddLinkRecord(lr11);
     lsa3->SetNode(nodes.Get(3));
-    m_lsas.push_back(lsa3);
+    m_lsasv4.push_back(lsa3);
+}
+
+void
+LinkRoutesTestCase::BuildLsav6()
+{
+    // Manually build the link state database; four routers (0-3), 3 point-to-point
+    // links
+
+    // Router 0
+    auto lr0 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+        "::ffff:0.0.0.2",        // link id -> router ID 0.0.0.2
+        "2001:1::200:ff:fe00:1", // link data -> Local GlobalUnicast IP address of router 0
+        "fe80::200:ff:fe00:1",   // Local Link Local IP address of router 0
+        1);                      // metric
+
+    // only need to do this once for prefix of 64 bits
+    uint8_t buf[16];
+    auto prefix = Ipv6Prefix(64);
+    prefix.GetBytes(buf); // frown
+
+    auto lr1 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+        "2001:1::200:ff:fe00:2", // link id ->adjacent neighbor's IP address
+        Ipv6Address(buf), // Link Data -> Prefix converted to Ipv6Address associated with neighbours
+                          // Ip address
+        1);
+
+    auto lsa0 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa0->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa0->SetLinkStateId("::ffff:0.0.0.0");
+    lsa0->SetAdvertisingRouter("::ffff:0.0.0.0");
+    lsa0->SetNode(nodes.Get(0));
+    lsa0->AddLinkRecord(lr0);
+    lsa0->AddLinkRecord(lr1);
+    m_lsasv6.push_back(lsa0);
+
+    // Router 1
+    auto lr2 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+        "::ffff:0.0.0.2",        // link id -> router ID
+        "2001:2::200:ff:fe00:3", // link data -> Local Global Unicast IP address of router 1
+        "fe80::200:ff:fe00:3",   // link loc data -> Local Link Local IP address of router 1
+        1);
+
+    auto lr3 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+        "2001:2::200:ff:fe00:4", // link id ->adjacent neighbor's IP address
+        Ipv6Address(buf), // Link Data -> Prefix converted to Ipv6Address associated with neighbours
+                          // Ip address
+        1);
+
+    auto lsa1 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa1->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa1->SetLinkStateId("::ffff:0.0.0.1");
+    lsa1->SetAdvertisingRouter("::ffff:0.0.0.1");
+    lsa1->AddLinkRecord(lr2);
+    lsa1->AddLinkRecord(lr3);
+    lsa1->SetNode(nodes.Get(1));
+    m_lsasv6.push_back(lsa1);
+
+    // Router 2
+    auto lr4 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+        "::ffff:0.0.0.0",        // link id -> router ID
+        "2001:1::200:ff:fe00:2", // link data -> Local Global Unicast IP address of router 2
+        "fe80::200:ff:fe00:2",   // link loc data -> Local Link Local IP address of router 2
+        1);
+
+    auto lr5 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+        "2001:1::200:ff:fe00:1", // link id ->adjacent neighbor's IP address
+        Ipv6Address(buf), // Link Data -> Prefix converted to Ipv6Address associated with neighbours
+                          // Ip address
+        1);
+
+    auto lr6 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.1",
+                                                 "2001:2::200:ff:fe00:4",
+                                                 "fe80::200:ff:fe00:4",
+                                                 1);
+
+    auto lr7 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:2::200:ff:fe00:3",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lr8 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.3",
+                                                 "2001:3::200:ff:fe00:5",
+                                                 "fe80::200:ff:fe00:5",
+                                                 1);
+
+    auto lr9 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:3::200:ff:fe00:6",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lsa2 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa2->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa2->SetLinkStateId("::ffff:0.0.0.2");
+    lsa2->SetAdvertisingRouter("::ffff:0.0.0.2");
+    lsa2->AddLinkRecord(lr4);
+    lsa2->AddLinkRecord(lr5);
+    lsa2->AddLinkRecord(lr6);
+    lsa2->AddLinkRecord(lr7);
+    lsa2->AddLinkRecord(lr8);
+    lsa2->AddLinkRecord(lr9);
+    lsa2->SetNode(nodes.Get(2));
+    m_lsasv6.push_back(lsa2);
+
+    // Router 3
+    auto lr10 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.2",
+                                                 "2001:3::200:ff:fe00:6",
+                                                 "fe80::200:ff:fe00:6",
+                                                 1);
+
+    auto lr11 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:3::200:ff:fe00:5",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lsa3 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa3->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa3->SetLinkStateId("::ffff:0.0.0.3");
+    lsa3->SetAdvertisingRouter("::ffff:0.0.0.3");
+    lsa3->AddLinkRecord(lr10);
+    lsa3->AddLinkRecord(lr11);
+    lsa3->SetNode(nodes.Get(3));
+    m_lsasv6.push_back(lsa3);
 }
 
 void
@@ -285,38 +478,65 @@
     //  This is a manual setup of the LSAs, which would normally be done by the
     //  GlobalRoutingHelper.
 
-    BuildLsa();
+    BuildLsav4();
 
     // Test the database
-    auto srmlsdb = new GlobalRouteManagerLSDB();
-    srmlsdb->Insert(m_lsas[0]->GetLinkStateId(), m_lsas[0]);
-    srmlsdb->Insert(m_lsas[1]->GetLinkStateId(), m_lsas[1]);
-    srmlsdb->Insert(m_lsas[2]->GetLinkStateId(), m_lsas[2]);
-    srmlsdb->Insert(m_lsas[3]->GetLinkStateId(), m_lsas[3]);
+    auto srmlsdb = new GlobalRouteManagerLSDB<Ipv4Manager>();
+    srmlsdb->Insert(m_lsasv4[0]->GetLinkStateId(), m_lsasv4[0]);
+    srmlsdb->Insert(m_lsasv4[1]->GetLinkStateId(), m_lsasv4[1]);
+    srmlsdb->Insert(m_lsasv4[2]->GetLinkStateId(), m_lsasv4[2]);
+    srmlsdb->Insert(m_lsasv4[3]->GetLinkStateId(), m_lsasv4[3]);
 
     NS_TEST_ASSERT_MSG_EQ(
-        m_lsas[2],
-        srmlsdb->GetLSA(m_lsas[2]->GetLinkStateId()),
+        m_lsasv4[2],
+        srmlsdb->GetLSA(m_lsasv4[2]->GetLinkStateId()),
         "The Ipv4Address is not stored as the link state ID"); // LSAs are mapped by router id as
                                                                // key here we check that they are
                                                                // indeed getting the right lsa for
                                                                // the right key
 
     // next, calculate routes based on the manually created LSDB
-    auto srm = new GlobalRouteManagerImpl();
+    auto srm = new GlobalRouteManagerImpl<Ipv4Manager>();
     srm->DebugUseLsdb(srmlsdb); // manually add in an LSDB
 
-    srm->DebugSPFCalculate(m_lsas[0]->GetLinkStateId()); // fill routing table for node n0
+    srm->DebugSPFCalculate(m_lsasv4[0]->GetLinkStateId()); // fill routing table for node n0
 
-    srm->DebugSPFCalculate(m_lsas[1]->GetLinkStateId()); // fill routing table for node n1
+    srm->DebugSPFCalculate(m_lsasv4[1]->GetLinkStateId()); // fill routing table for node n1
 
-    srm->DebugSPFCalculate(m_lsas[2]->GetLinkStateId()); // fill routing table for node n2
+    srm->DebugSPFCalculate(m_lsasv4[2]->GetLinkStateId()); // fill routing table for node n2
 
-    srm->DebugSPFCalculate(m_lsas[3]->GetLinkStateId()); // fill routing table for node n3
+    srm->DebugSPFCalculate(m_lsasv4[3]->GetLinkStateId()); // fill routing table for node n3
 
-    Simulator::Run();
+    BuildLsav6();
+
+    // Test the database
+    auto srmlsdbv6 = new GlobalRouteManagerLSDB<Ipv6Manager>();
+    srmlsdbv6->Insert(m_lsasv6[0]->GetLinkStateId(), m_lsasv6[0]);
+    srmlsdbv6->Insert(m_lsasv6[1]->GetLinkStateId(), m_lsasv6[1]);
+    srmlsdbv6->Insert(m_lsasv6[2]->GetLinkStateId(), m_lsasv6[2]);
+    srmlsdbv6->Insert(m_lsasv6[3]->GetLinkStateId(), m_lsasv6[3]);
+
+    NS_TEST_ASSERT_MSG_EQ(
+        m_lsasv6[2],
+        srmlsdbv6->GetLSA(m_lsasv6[2]->GetLinkStateId()),
+        "The Ipv6Address is not stored as the link state ID"); // LSAs are mapped by router id as
+                                                               // key here we check that they are
+                                                               // indeed getting the right lsa for
+                                                               // the right key
+
+    // next, calculate routes based on the manually created LSDB
+    auto srmv6 = new GlobalRouteManagerImpl<Ipv6Manager>();
+    srmv6->DebugUseLsdb(srmlsdbv6); // manually add in an LSDB
+
+    srmv6->DebugSPFCalculate(m_lsasv6[0]->GetLinkStateId()); // fill ipv6 routing table for node n0
+
+    srmv6->DebugSPFCalculate(m_lsasv6[1]->GetLinkStateId()); // fill ipv6 routing table for node n1
 
-    //-----------------Now the tests------------------
+    srmv6->DebugSPFCalculate(m_lsasv6[2]->GetLinkStateId()); // fill ipv6 routing table for node n2
+
+    srmv6->DebugSPFCalculate(m_lsasv6[3]->GetLinkStateId()); // fill ipv6 routing table for node n3
+
+    //-----------------Now the tests for Ipv4------------------
     // Test 1: Check if the SPF calculate Sets default routes for Stub nodes
     Ptr<Ipv4L3Protocol> ip0 = nodes.Get(0)->GetObject<Ipv4L3Protocol>();
     NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object at node 0");
@@ -367,7 +587,66 @@
     expectedgws.emplace_back("10.1.2.1");
     expectedgws.emplace_back("10.1.3.2");
 
-    CheckRoutes(globalRouting2, expecteddests, expectedgws);
+    CheckRoutesv4(globalRouting2, expecteddests, expectedgws);
+
+    //-----------------Now the tests for Ipv6------------------
+    // Test 1: Check if the SPF calculate Sets default routes for Stub nodes
+    Ptr<Ipv6L3Protocol> ip0v6 = nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ip0v6, nullptr, "Error-- no Ipv6 object at node 0");
+    Ptr<Ipv6RoutingProtocol> routing0v6 = ip0v6->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routing0v6, nullptr, "Error-- no Ipv6 routing protocol object at node 0");
+    Ptr<Ipv6GlobalRouting> globalRouting0v6 = routing0v6->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRouting0v6,
+                          nullptr,
+                          "Error-- no Ipv6GlobalRouting object at node 0");
+
+    // Check that the right number of entries are in the routing table
+    uint32_t nRoutes0v6 = globalRouting0v6->GetNRoutes();
+    NS_TEST_ASSERT_MSG_EQ(nRoutes0v6, 1, "Error-- default route not found for stub node");
+
+    Ipv6RoutingTableEntry* routev6 = nullptr;
+    routev6 = globalRouting0v6->GetRoute(0);
+    // the only route is the default route on this node
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                          Ipv6Address("::"),
+                          "Error-- wrong destination for default route");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:2"),
+                          "Error-- wrong gateway");
+
+    // Test 2: Check if SPFCalculate sets the correct routes for node 2
+    Ptr<Ipv6L3Protocol> ip2v6 = nodes.Get(2)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ip2v6, nullptr, "Error-- no Ipv6 object at node 2");
+    Ptr<Ipv6RoutingProtocol> routing2v6 = ip2v6->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routing2v6, nullptr, "Error-- no Ipv6 routing protocol object at node 2");
+    Ptr<Ipv6GlobalRouting> globalRouting2v6 = routing2v6->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRouting2v6,
+                          nullptr,
+                          "Error-- no Ipv6GlobalRouting object at node 2");
+
+    // check that the correct number of routes were built
+    uint32_t nRoutes2v6 = globalRouting2v6->GetNRoutes();
+    NS_LOG_DEBUG("LinkRoutesTest nRoutes2 " << nRoutes2v6);
+    NS_TEST_ASSERT_MSG_EQ(nRoutes2v6, 6, "Error--- Incorrect number of routes found on node 2");
+
+    // check that all the routes in the routing table are correct for node 2
+    std::vector<Ipv6Address> expecteddestsv6;
+    std::vector<Ipv6Address> expectedgwsv6;
+    expecteddestsv6.emplace_back("2001:1::200:ff:fe00:1");
+    expecteddestsv6.emplace_back("2001:2::200:ff:fe00:3");
+    expecteddestsv6.emplace_back("2001:3::200:ff:fe00:6");
+    expecteddestsv6.emplace_back("2001:1::");
+    expecteddestsv6.emplace_back("2001:2::");
+    expecteddestsv6.emplace_back("2001:3::");
+
+    expectedgwsv6.emplace_back("fe80::200:ff:fe00:1");
+    expectedgwsv6.emplace_back("fe80::200:ff:fe00:3");
+    expectedgwsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedgwsv6.emplace_back("fe80::200:ff:fe00:1");
+    expectedgwsv6.emplace_back("fe80::200:ff:fe00:3");
+    expectedgwsv6.emplace_back("fe80::200:ff:fe00:6");
+
+    CheckRoutesv6(globalRouting2v6, expecteddestsv6, expectedgwsv6);
 
     Simulator::Run();
 
@@ -377,9 +656,11 @@
     // This delete clears the srm, which deletes the LSDB, which clears
     // all of the LSAs, which each destroys the attached LinkRecords.
     delete srm;
+    delete srmv6;
     // reset the router ID counter to zero so that it does not affect other tests
     //  that may run after this one in the same program run.
-    GlobalRouteManager::ResetRouterId();
+    GlobalRouteManager<Ipv4Manager>::ResetRouterId();
+    GlobalRouteManager<Ipv6Manager>::ResetRouterId();
 }
 
 /**
@@ -397,14 +678,22 @@
 
   private:
     /**
-     *@brief Builds the LSAs for the topology. These LSAs are manually created and inserted into the
-     * GlobalRouteManagerLSDB.Each node exports a router LSA. In addition,the designated router
-     * also Exports the Network LSA.
+     *@brief Builds the ipv4 LSAs for the topology. These LSAs are manually created and inserted
+     *into the GlobalRouteManagerLSDB.Each node exports a router LSA. In addition,the designated
+     *router also Exports the Network LSA.
+     */
+    void BuildLsav4();
+
+    /**
+     *@brief Builds the ipv6 LSAs for the topology. These LSAs are manually created and inserted
+     *into the GlobalRouteManagerLSDB.Each node exports a router LSA. In addition,the designated
+     *router also Exports the Network LSA.
      */
-    void BuildLsa();
+    void BuildLsav6();
 
-    NodeContainer nodes;                   //!< NodeContainer to hold the nodes in the topology
-    std::vector<GlobalRoutingLSA*> m_lsas; //!< The LSAs for the topology
+    NodeContainer nodes; //!< NodeContainer to hold the nodes in the topology
+    std::vector<GlobalRoutingLSA<Ipv4Manager>*> m_lsasv4; //!< The ipv4 LSAs for the topology
+    std::vector<GlobalRoutingLSA<Ipv6Manager>*> m_lsasv6; //!< The ipv6 LSAs for the topology
 };
 
 LanRoutesTestCase::LanRoutesTestCase()
@@ -427,9 +716,11 @@
     //       n1:10.1.1.2/29
     //       n2:10.1.1.3/29
     nodes.Create(3);
-    Ipv4GlobalRoutingHelper globalhelper;
+    Ipv4GlobalRoutingHelper globalhelperv4;
+    Ipv6GlobalRoutingHelper globalhelperv6;
     InternetStackHelper stack;
-    stack.SetRoutingHelper(globalhelper);
+    stack.SetRoutingHelper(globalhelperv4);
+    stack.SetRoutingHelper(globalhelperv6);
     stack.Install(nodes);
     SimpleNetDeviceHelper devHelper;
     Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
@@ -441,85 +732,175 @@
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.248");
     Ipv4InterfaceContainer i012 = address.Assign(d012);
+
+    Ipv6AddressHelper addressv6;
+    addressv6.SetBase("2001:1::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i012v6 = addressv6.Assign(d012);
 }
 
 void
-LanRoutesTestCase::BuildLsa()
+LanRoutesTestCase::BuildLsav4()
 {
     // we manually create the link state database, we could have used BuildRoutingTables() but this
     // way testing the GlobalRouteManagerImpl without the involvement of GLobalRouter makes it
     // easier to debug each individually
 
     // router 0
-    auto lr0 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::TransitNetwork,
-                                           "10.1.1.1",
-                                           "10.1.1.1",
-                                           1);
-    auto lsa0 = new GlobalRoutingLSA();
-    lsa0->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lr0 = new GlobalRoutingLinkRecord<Ipv4Manager>(
+        GlobalRoutingLinkRecord<Ipv4Manager>::TransitNetwork,
+        "10.1.1.1",
+        "10.1.1.1",
+        1);
+    auto lsa0 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa0->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa0->AddLinkRecord(lr0);
     lsa0->SetLinkStateId("0.0.0.0");
     lsa0->SetAdvertisingRouter("0.0.0.0");
     lsa0->SetNode(nodes.Get(0));
-    m_lsas.push_back(lsa0);
+    m_lsasv4.push_back(lsa0);
 
     // router 1
-    auto lr1 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::TransitNetwork,
-                                           "10.1.1.1",
-                                           "10.1.1.2",
-                                           1);
-    auto lsa1 = new GlobalRoutingLSA();
-    lsa1->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lr1 = new GlobalRoutingLinkRecord<Ipv4Manager>(
+        GlobalRoutingLinkRecord<Ipv4Manager>::TransitNetwork,
+        "10.1.1.1",
+        "10.1.1.2",
+        1);
+    auto lsa1 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa1->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa1->AddLinkRecord(lr1);
     lsa1->SetLinkStateId("0.0.0.1");
     lsa1->SetAdvertisingRouter("0.0.0.1");
     lsa1->SetNode(nodes.Get(1));
-    m_lsas.push_back(lsa1);
+    m_lsasv4.push_back(lsa1);
 
     // router 2
-    auto lr2 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::TransitNetwork,
-                                           "10.1.1.1",
-                                           "10.1.1.3",
-                                           1);
-    auto lsa2 = new GlobalRoutingLSA();
-    lsa2->SetLSType(GlobalRoutingLSA::RouterLSA);
+    auto lr2 = new GlobalRoutingLinkRecord<Ipv4Manager>(
+        GlobalRoutingLinkRecord<Ipv4Manager>::TransitNetwork,
+        "10.1.1.1",
+        "10.1.1.3",
+        1);
+    auto lsa2 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa2->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
     lsa2->AddLinkRecord(lr2);
     lsa2->SetLinkStateId("0.0.0.2");
     lsa2->SetAdvertisingRouter("0.0.0.2");
     lsa2->SetNode(nodes.Get(2));
-    m_lsas.push_back(lsa2);
+    m_lsasv4.push_back(lsa2);
 
     // router0 is the designated router for the LAN. it also exports the network LSA
-    auto lsa0network = new GlobalRoutingLSA();
-    lsa0network->SetLSType(GlobalRoutingLSA::NetworkLSA);
+    auto lsa0network = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa0network->SetLSType(GlobalRoutingLSA<Ipv4Manager>::NetworkLSA);
     lsa0network->SetLinkStateId("10.1.1.1");
     lsa0network->SetAdvertisingRouter("0.0.0.0");
     lsa0network->AddAttachedRouter("10.1.1.1");
     lsa0network->AddAttachedRouter("10.1.1.2");
     lsa0network->AddAttachedRouter("10.1.1.3");
     lsa0network->SetNetworkLSANetworkMask("255.255.255.248");
-    m_lsas.push_back(lsa0network); // note the index of the network lsa
+    m_lsasv4.push_back(lsa0network); // note the index of the network lsa
+}
+
+void
+LanRoutesTestCase::BuildLsav6()
+{
+    // we manually create the link state database, we could have used BuildRoutingTables() but this
+    // way testing the GlobalRouteManagerImpl without the involvement of GLobalRouter makes it
+    // easier to debug each individually
+
+    // router 0
+    auto lr0 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::TransitNetwork,
+        "2001:1::200:ff:fe00:1", // link id ->  Link ID is the ip address of the designated router
+        "2001:1::200:ff:fe00:1", // link data is this routers own Global Unicast ip address
+        "fe80::200:ff:fe00:1",   // the link local data is the link local address of this router for
+                                 // this link
+        1);
+    auto lsa0 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa0->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa0->AddLinkRecord(lr0);
+    lsa0->SetLinkStateId("::ffff:0.0.0.0");
+    lsa0->SetAdvertisingRouter("::ffff:0.0.0.0");
+    lsa0->SetNode(nodes.Get(0));
+    m_lsasv6.push_back(lsa0);
+
+    // router 1
+    auto lr1 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::TransitNetwork,
+        "2001:1::200:ff:fe00:1", // link id ->  Link ID is the ip address of the designated router
+        "2001:1::200:ff:fe00:2", // link data is this routers own Global Unicast ip address
+        "fe80::200:ff:fe00:2",   // the link local data is the link local address of this router for
+                                 // this link
+        1);
+    auto lsa1 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa1->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa1->AddLinkRecord(lr1);
+    lsa1->SetLinkStateId("::ffff:0.0.0.1");
+    lsa1->SetAdvertisingRouter("::ffff:0.0.0.1");
+    lsa1->SetNode(nodes.Get(1));
+    m_lsasv6.push_back(lsa1);
+
+    // router 2
+    auto lr2 = new GlobalRoutingLinkRecord<Ipv6Manager>(
+        GlobalRoutingLinkRecord<Ipv6Manager>::TransitNetwork,
+        "2001:1::200:ff:fe00:1",
+        "2001:1::200:ff:fe00:3",
+        "fe80::200:ff:fe00:3",
+        1);
+    auto lsa2 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa2->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa2->AddLinkRecord(lr2);
+    lsa2->SetLinkStateId("::ffff:0.0.0.2");
+    lsa2->SetAdvertisingRouter("::ffff:0.0.0.2");
+    lsa2->SetNode(nodes.Get(2));
+    m_lsasv6.push_back(lsa2);
+
+    // router0 is the designated router for the LAN. it also exports the network LSA
+    auto lsa0network = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa0network->SetLSType(GlobalRoutingLSA<Ipv6Manager>::NetworkLSA);
+    lsa0network->SetLinkStateId("2001:1::200:ff:fe00:1");
+    lsa0network->SetAdvertisingRouter("::ffff:0.0.0.0");
+    lsa0network->AddAttachedRouter("2001:1::200:ff:fe00:1");
+    lsa0network->AddAttachedRouter("2001:1::200:ff:fe00:2");
+    lsa0network->AddAttachedRouter("2001:1::200:ff:fe00:3");
+
+    lsa0network->SetNetworkLSANetworkMask(Ipv6Prefix(64));
+    m_lsasv6.push_back(lsa0network); // note the index of the network lsa
 }
 
 void
 LanRoutesTestCase::DoRun()
 {
-    BuildLsa();
+    BuildLsav4();
     // insert the LSAs into the GlobalRouteManagerLSDB
-    auto srmlsdb = new GlobalRouteManagerLSDB();
+    auto srmlsdb = new GlobalRouteManagerLSDB<Ipv4Manager>();
 
-    srmlsdb->Insert(m_lsas[0]->GetLinkStateId(), m_lsas[0]);
-    srmlsdb->Insert(m_lsas[1]->GetLinkStateId(), m_lsas[1]);
-    srmlsdb->Insert(m_lsas[2]->GetLinkStateId(), m_lsas[2]);
-    srmlsdb->Insert(m_lsas[3]->GetLinkStateId(), m_lsas[3]);
+    srmlsdb->Insert(m_lsasv4[0]->GetLinkStateId(), m_lsasv4[0]);
+    srmlsdb->Insert(m_lsasv4[1]->GetLinkStateId(), m_lsasv4[1]);
+    srmlsdb->Insert(m_lsasv4[2]->GetLinkStateId(), m_lsasv4[2]);
+    srmlsdb->Insert(m_lsasv4[3]->GetLinkStateId(), m_lsasv4[3]);
 
     // create the GlobalRouteManagerImpl
-    auto srm = new GlobalRouteManagerImpl();
-    srm->DebugUseLsdb(srmlsdb);
+    auto srmv4 = new GlobalRouteManagerImpl<Ipv4Manager>();
+    srmv4->DebugUseLsdb(srmlsdb);
 
-    srm->DebugSPFCalculate(m_lsas[0]->GetLinkStateId()); // fill the routing table for node 0
+    srmv4->DebugSPFCalculate(m_lsasv4[0]->GetLinkStateId()); // fill the routing table for node 0
 
-    // now the tests-----------------------
+    // now for ipv6
+    BuildLsav6();
+    // insert the LSAs into the GlobalRouteManagerLSDB
+    auto srmlsdbv6 = new GlobalRouteManagerLSDB<Ipv6Manager>();
+
+    srmlsdbv6->Insert(m_lsasv6[0]->GetLinkStateId(), m_lsasv6[0]);
+    srmlsdbv6->Insert(m_lsasv6[1]->GetLinkStateId(), m_lsasv6[1]);
+    srmlsdbv6->Insert(m_lsasv6[2]->GetLinkStateId(), m_lsasv6[2]);
+    srmlsdbv6->Insert(m_lsasv6[3]->GetLinkStateId(), m_lsasv6[3]);
+
+    // create the GlobalRouteManagerImpl
+    auto srmv6 = new GlobalRouteManagerImpl<Ipv6Manager>();
+    srmv6->DebugUseLsdb(srmlsdbv6);
+
+    srmv6->DebugSPFCalculate(m_lsasv6[0]->GetLinkStateId()); // fill the routing table for node 0
+
+    // now the tests for IPv4-----------------------
 
     Ptr<Ipv4L3Protocol> ip0 = nodes.Get(0)->GetObject<Ipv4L3Protocol>();
     NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object at node 0");
@@ -539,14 +920,38 @@
                           Ipv4Address("0.0.0.0"),
                           "Error-- wrong gateway for network route");
 
+    // now the tests for IPv6-----------------------
+
+    Ptr<Ipv6L3Protocol> ip0v6 = nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ip0v6, nullptr, "Error-- no Ipv6 object at node 0");
+    Ptr<Ipv6RoutingProtocol> routing0v6 = ip0v6->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routing0v6, nullptr, "Error-- no Ipv6 routing protocol object at node 0");
+    Ptr<Ipv6GlobalRouting> globalRouting0v6 = routing0v6->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRouting0v6,
+                          nullptr,
+                          "Error-- no Ipv6GlobalRouting object at node 0");
+
+    // The only route to check is the network route
+    uint32_t nRoutes0v6 = globalRouting0v6->GetNRoutes();
+    NS_TEST_ASSERT_MSG_EQ(nRoutes0v6, 1, "Error-- Network route not found for node 0");
+    Ipv6RoutingTableEntry* routev6 = globalRouting0v6->GetRoute(0);
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                          Ipv6Address("2001:1::"),
+                          "Error-- wrong destination for network route");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("::"),
+                          "Error-- wrong gateway for network route");
+
     Simulator::Run();
 
     Simulator::Stop(Seconds(3));
     Simulator::Destroy();
     // This delete clears the srm, which deletes the LSDB, which clears
     // all of the LSAs, which each destroys the attached LinkRecords.
-    delete srm;
-    GlobalRouteManager::ResetRouterId();
+    delete srmv4;
+    delete srmv6;
+    GlobalRouteManager<Ipv4Manager>::ResetRouterId();
+    GlobalRouteManager<Ipv6Manager>::ResetRouterId();
 }
 
 /**
@@ -568,14 +973,27 @@
      *@brief Builds the LSAs for the topology. These LSAs are manually created and inserted into the
      *       GlobalRouteManagerLSDB.Each node exports a router LSA.
      */
-    void BuildLsa();
+    void BuildLsav4();
+
+    /**
+     *@brief Builds the LSAs for the topology. These LSAs are manually created and inserted into the
+     *       GlobalRouteManagerLSDB.Each node exports a router LSA.
+     */
+    void BuildLsav6();
 
     /**
      * @brief Helper function that checks the output of the routing path
      * and increments the corresponding route counter.
      * @param route The routing path to check
      */
-    void IncrementCount(Ptr<Ipv4Route>& route);
+    void IncrementCountv4(Ptr<Ipv4Route>& route);
+
+    /**
+     * @brief Helper function that checks the output of the routing path
+     * and increments the corresponding route counter.
+     * @param route The routing path to check
+     */
+    void IncrementCountv6(Ptr<Ipv6Route>& route);
     /**
      * @brief function that checks the routing table entries for the expected output.
      * @param globalroutingprotocol The routing protocol for the node whose routing table is to be
@@ -583,13 +1001,27 @@
      * @param dests The expected destinations.
      * @param gws The expected gateways.
      */
-    void CheckRoutes(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
-                     std::vector<Ipv4Address>& dests,
-                     std::vector<Ipv4Address>& gws);
-    uint32_t route1 = 0; //!< Counter to keep track of the number of times route1 is used
-    uint32_t route2 = 0; //!< Counter to keep track of the number of times route2 is used
-    NodeContainer nodes; //!< NodeContainer to hold the nodes in the topology
-    std::vector<GlobalRoutingLSA*> m_lsas; //!< The LSAs for the topology
+    void CheckRoutesv4(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
+                       std::vector<Ipv4Address>& dests,
+                       std::vector<Ipv4Address>& gws);
+
+    /**
+     * @brief function that checks the routing table entries for the expected output.
+     * @param globalroutingprotocol The routing protocol for the node whose routing table is to be
+     * checked.
+     * @param dests The expected destinations.
+     * @param gws The expected gateways.
+     */
+    void CheckRoutesv6(Ptr<Ipv6GlobalRouting>& globalroutingprotocol,
+                       std::vector<Ipv6Address>& dests,
+                       std::vector<Ipv6Address>& gws);
+    uint32_t route1v4 = 0; //!< Counter to keep track of the number of times route1 is used for ipv4
+    uint32_t route2v4 = 0; //!< Counter to keep track of the number of times route2 is used for ipv4
+    uint32_t route1v6 = 0; //!< Counter to keep track of the number of times route1 is used for ipv6
+    uint32_t route2v6 = 0; //!< Counter to keep track of the number of times route2 is used for ipv6
+    NodeContainer nodes;   //!< NodeContainer to hold the nodes in the topology
+    std::vector<GlobalRoutingLSA<Ipv4Manager>*> m_lsasv4; //!< The LSAs for the topology
+    std::vector<GlobalRoutingLSA<Ipv6Manager>*> m_lsasv6; //!< The LSAs for the topology
 };
 
 RandomEcmpTestCase::RandomEcmpTestCase()
@@ -601,6 +1033,8 @@
 RandomEcmpTestCase::DoSetup()
 {
     Config::SetDefault("ns3::Ipv4GlobalRouting::RandomEcmpRouting", BooleanValue(true));
+    Config::SetDefault("ns3::Ipv6GlobalRouting::RandomEcmpRouting", BooleanValue(true));
+
     /*
     //   Creating a Simple topology with 4 nodes and 3 links
     //
@@ -622,8 +1056,10 @@
     nodes.Create(4);
 
     Ipv4GlobalRoutingHelper globalhelper;
+    Ipv6GlobalRoutingHelper globalhelperv6;
     InternetStackHelper stack;
     stack.SetRoutingHelper(globalhelper);
+    stack.SetRoutingHelper(globalhelperv6);
     stack.Install(nodes);
     SimpleNetDeviceHelper devHelper;
     devHelper.SetNetDevicePointToPointMode(true);
@@ -653,12 +1089,22 @@
 
     address.SetBase("10.1.4.0", "255.255.255.252");
     Ipv4InterfaceContainer i23 = address.Assign(d23);
+
+    Ipv6AddressHelper addressv6;
+    addressv6.SetBase("2001:1::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i01v6 = addressv6.Assign(d01);
+    addressv6.SetBase("2001:2::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i02v6 = addressv6.Assign(d02);
+    addressv6.SetBase("2001:3::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i13v6 = addressv6.Assign(d13);
+    addressv6.SetBase("2001:4::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i23v6 = addressv6.Assign(d23);
 }
 
 void
-RandomEcmpTestCase::CheckRoutes(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
-                                std::vector<Ipv4Address>& dests,
-                                std::vector<Ipv4Address>& gws)
+RandomEcmpTestCase::CheckRoutesv4(Ptr<Ipv4GlobalRouting>& globalroutingprotocol,
+                                  std::vector<Ipv4Address>& dests,
+                                  std::vector<Ipv4Address>& gws)
 {
     // check each individual routes destination and gateway
     for (uint32_t i = 0; i < globalroutingprotocol->GetNRoutes(); i++)
@@ -671,47 +1117,79 @@
 }
 
 void
-RandomEcmpTestCase::IncrementCount(Ptr<Ipv4Route>& route)
+RandomEcmpTestCase::CheckRoutesv6(Ptr<Ipv6GlobalRouting>& globalroutingprotocol,
+                                  std::vector<Ipv6Address>& dests,
+                                  std::vector<Ipv6Address>& gws)
+{
+    // check each individual routes destination and gateway
+    for (uint32_t i = 0; i < globalroutingprotocol->GetNRoutes(); i++)
+    {
+        Ipv6RoutingTableEntry* route = globalroutingprotocol->GetRoute(i);
+        NS_LOG_DEBUG("dest " << route->GetDest() << " gw " << route->GetGateway());
+        NS_TEST_ASSERT_MSG_EQ(route->GetDest(), dests[i], "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), gws[i], "Error-- wrong gateway");
+    }
+}
+
+void
+RandomEcmpTestCase::IncrementCountv4(Ptr<Ipv4Route>& route)
 {
     if (route->GetGateway() == Ipv4Address("10.1.1.2"))
     {
-        route1++;
+        route1v4++;
     }
     else if (route->GetGateway() == Ipv4Address("10.1.2.2"))
     {
-        route2++;
+        route2v4++;
+    }
+}
+
+void
+RandomEcmpTestCase::IncrementCountv6(Ptr<Ipv6Route>& route)
+{
+    if (route->GetGateway() == Ipv6Address("fe80::200:ff:fe00:2"))
+    {
+        route1v6++;
+    }
+    else if (route->GetGateway() == Ipv6Address("fe80::200:ff:fe00:6"))
+    {
+        route2v6++;
     }
 }
 
 void
-RandomEcmpTestCase::BuildLsa()
+RandomEcmpTestCase::BuildLsav4()
 {
     // we manually create the link state database, we could have used BuildRoutingTables() but this
     // way testing the GlobalRouteManagerImpl without the involvement of GLobalRouter makes it
     // easier to debug each individually
 
     // router 0
-    auto lr0 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.1",
-                                           "10.1.1.1",
-                                           1);
-    auto lr1 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.1.2",
-                                           "255.255.255.252",
-                                           1);
-
-    auto lr2 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.2",
-                                           "10.1.2.1",
-                                           1);
-    auto lr3 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.2.2",
-                                           "255.255.255.252",
-                                           1);
-
-    auto lsa0 = new GlobalRoutingLSA();
-    lsa0->SetLSType(GlobalRoutingLSA::RouterLSA);
-    lsa0->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+    auto lr0 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.1",
+                                                 "10.1.1.1",
+                                                 1);
+    auto lr1 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.1.2",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lr2 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.2",
+                                                 "10.1.2.1",
+                                                 1);
+    auto lr3 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.2.2",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lsa0 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa0->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
+    lsa0->SetStatus(GlobalRoutingLSA<Ipv4Manager>::LSA_SPF_NOT_EXPLORED);
     lsa0->SetLinkStateId("0.0.0.0");
     lsa0->SetAdvertisingRouter("0.0.0.0");
     lsa0->SetNode(nodes.Get(0));
@@ -719,30 +1197,34 @@
     lsa0->AddLinkRecord(lr1);
     lsa0->AddLinkRecord(lr2);
     lsa0->AddLinkRecord(lr3);
-    m_lsas.push_back(lsa0);
+    m_lsasv4.push_back(lsa0);
 
     // router 1
-    auto lr4 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.0",
-                                           "10.1.1.2",
-                                           1);
-    auto lr5 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.1.1",
-                                           "255.255.255.252",
-                                           1);
-
-    auto lr6 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.3",
-                                           "10.1.3.1",
-                                           1);
-    auto lr7 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.3.2",
-                                           "255.255.255.252",
-                                           1);
-
-    auto lsa1 = new GlobalRoutingLSA();
-    lsa1->SetLSType(GlobalRoutingLSA::RouterLSA);
-    lsa1->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+    auto lr4 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.0",
+                                                 "10.1.1.2",
+                                                 1);
+    auto lr5 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.1.1",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lr6 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.3",
+                                                 "10.1.3.1",
+                                                 1);
+    auto lr7 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.3.2",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lsa1 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa1->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
+    lsa1->SetStatus(GlobalRoutingLSA<Ipv4Manager>::LSA_SPF_NOT_EXPLORED);
     lsa1->SetLinkStateId("0.0.0.1");
     lsa1->SetAdvertisingRouter("0.0.0.1");
     lsa1->SetNode(nodes.Get(1));
@@ -750,29 +1232,33 @@
     lsa1->AddLinkRecord(lr5);
     lsa1->AddLinkRecord(lr6);
     lsa1->AddLinkRecord(lr7);
-    m_lsas.push_back(lsa1);
+    m_lsasv4.push_back(lsa1);
 
     // router 2
-    auto lr8 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                           "0.0.0.0",
-                                           "10.1.2.2",
-                                           1);
-    auto lr9 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                           "10.1.2.1",
-                                           "255.255.255.252",
-                                           1);
-    auto lr10 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                            "0.0.0.3",
-                                            "10.1.4.1",
-                                            1);
-    auto lr11 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                            "10.1.4.2",
-                                            "255.255.255.252",
-                                            1);
-
-    auto lsa2 = new GlobalRoutingLSA();
-    lsa2->SetLSType(GlobalRoutingLSA::RouterLSA);
-    lsa2->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+    auto lr8 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.0",
+                                                 "10.1.2.2",
+                                                 1);
+    auto lr9 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.2.1",
+                                                 "255.255.255.252",
+                                                 1);
+    auto lr10 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.3",
+                                                 "10.1.4.1",
+                                                 1);
+    auto lr11 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.4.2",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lsa2 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa2->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
+    lsa2->SetStatus(GlobalRoutingLSA<Ipv4Manager>::LSA_SPF_NOT_EXPLORED);
     lsa2->SetLinkStateId("0.0.0.2");
     lsa2->SetAdvertisingRouter("0.0.0.2");
     lsa2->SetNode(nodes.Get(2));
@@ -780,29 +1266,33 @@
     lsa2->AddLinkRecord(lr9);
     lsa2->AddLinkRecord(lr10);
     lsa2->AddLinkRecord(lr11);
-    m_lsas.push_back(lsa2);
+    m_lsasv4.push_back(lsa2);
 
     // router 3
-    auto lr12 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                            "0.0.0.1",
-                                            "10.1.3.2",
-                                            1);
-    auto lr13 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                            "10.1.3.1",
-                                            "255.255.255.252",
-                                            1);
-    auto lr14 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::PointToPoint,
-                                            "0.0.0.2",
-                                            "10.1.4.2",
-                                            1);
-    auto lr15 = new GlobalRoutingLinkRecord(GlobalRoutingLinkRecord::StubNetwork,
-                                            "10.1.4.1",
-                                            "255.255.255.252",
-                                            1);
-
-    auto lsa3 = new GlobalRoutingLSA();
-    lsa3->SetLSType(GlobalRoutingLSA::RouterLSA);
-    lsa3->SetStatus(GlobalRoutingLSA::LSA_SPF_NOT_EXPLORED);
+    auto lr12 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.1",
+                                                 "10.1.3.2",
+                                                 1);
+    auto lr13 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.3.1",
+                                                 "255.255.255.252",
+                                                 1);
+    auto lr14 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::PointToPoint,
+                                                 "0.0.0.2",
+                                                 "10.1.4.2",
+                                                 1);
+    auto lr15 =
+        new GlobalRoutingLinkRecord<Ipv4Manager>(GlobalRoutingLinkRecord<Ipv4Manager>::StubNetwork,
+                                                 "10.1.4.1",
+                                                 "255.255.255.252",
+                                                 1);
+
+    auto lsa3 = new GlobalRoutingLSA<Ipv4Manager>();
+    lsa3->SetLSType(GlobalRoutingLSA<Ipv4Manager>::RouterLSA);
+    lsa3->SetStatus(GlobalRoutingLSA<Ipv4Manager>::LSA_SPF_NOT_EXPLORED);
     lsa3->SetLinkStateId("0.0.0.3");
     lsa3->SetAdvertisingRouter("0.0.0.3");
     lsa3->SetNode(nodes.Get(3));
@@ -810,7 +1300,165 @@
     lsa3->AddLinkRecord(lr13);
     lsa3->AddLinkRecord(lr14);
     lsa3->AddLinkRecord(lr15);
-    m_lsas.push_back(lsa3);
+    m_lsasv4.push_back(lsa3);
+}
+
+void
+RandomEcmpTestCase::BuildLsav6()
+{
+    // we manually create the link state database, we could have used BuildRoutingTables() but this
+    // way testing the GlobalRouteManagerImpl without the involvement of GLobalRouter makes it
+    // easier to debug each individually
+
+    // router 0
+    auto lr0 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.1",
+                                                 "2001:1::200:ff:fe00:1",
+                                                 "fe80::200:ff:fe00:1",
+                                                 1);
+    // only need to do this once for prefix of 64 bits
+    uint8_t buf[16];
+    auto prefix = Ipv6Prefix(64);
+    prefix.GetBytes(buf); // frown
+
+    auto lr1 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:1::200:ff:fe00:2",
+                                                 Ipv6Address(buf),
+                                                 1);
+    auto lr2 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.2",
+                                                 "2001:2::200:ff:fe00:5",
+                                                 "fe80::200:ff:fe00:5",
+                                                 1);
+    auto lr3 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:2::200:ff:fe00:6",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lsa0 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa0->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa0->SetStatus(GlobalRoutingLSA<Ipv6Manager>::LSA_SPF_NOT_EXPLORED);
+    lsa0->SetLinkStateId("::ffff:0.0.0.0");
+    lsa0->SetAdvertisingRouter("::ffff:0.0.0.0");
+    lsa0->SetNode(nodes.Get(0));
+    lsa0->AddLinkRecord(lr0);
+    lsa0->AddLinkRecord(lr1);
+    lsa0->AddLinkRecord(lr2);
+    lsa0->AddLinkRecord(lr3);
+    m_lsasv6.push_back(lsa0);
+
+    // router 1
+    auto lr4 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.0",
+                                                 "2001:1::200:ff:fe00:2",
+                                                 "fe80::200:ff:fe00:2",
+                                                 1);
+    auto lr5 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:1::200:ff:fe00:1",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lr6 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.3",
+                                                 "2001:3::200:ff:fe00:7",
+                                                 "fe80::200:ff:fe00:7",
+                                                 1);
+    auto lr7 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:3::200:ff:fe00:8",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lsa1 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa1->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa1->SetStatus(GlobalRoutingLSA<Ipv6Manager>::LSA_SPF_NOT_EXPLORED);
+    lsa1->SetLinkStateId("::ffff:0.0.0.1");
+    lsa1->SetAdvertisingRouter("::ffff:0.0.0.1");
+    lsa1->SetNode(nodes.Get(1));
+    lsa1->AddLinkRecord(lr4);
+    lsa1->AddLinkRecord(lr5);
+    lsa1->AddLinkRecord(lr6);
+    lsa1->AddLinkRecord(lr7);
+    m_lsasv6.push_back(lsa1);
+
+    // router 2
+    auto lr8 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.0",
+                                                 "2001:2::200:ff:fe00:6",
+                                                 "fe80::200:ff:fe00:6",
+                                                 1);
+    auto lr9 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:2::200:ff:fe00:5",
+                                                 Ipv6Address(buf),
+                                                 1);
+    auto lr10 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.3",
+                                                 "2001:4::200:ff:fe00:3",
+                                                 "fe80::200:ff:fe00:3",
+                                                 1);
+    auto lr11 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:4::200:ff:fe00:4",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lsa2 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa2->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa2->SetStatus(GlobalRoutingLSA<Ipv6Manager>::LSA_SPF_NOT_EXPLORED);
+    lsa2->SetLinkStateId("::ffff:0.0.0.2");
+    lsa2->SetAdvertisingRouter("::ffff:0.0.0.2");
+    lsa2->SetNode(nodes.Get(2));
+    lsa2->AddLinkRecord(lr8);
+    lsa2->AddLinkRecord(lr9);
+    lsa2->AddLinkRecord(lr10);
+    lsa2->AddLinkRecord(lr11);
+    m_lsasv6.push_back(lsa2);
+
+    // router 3
+    auto lr12 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.1",
+                                                 "2001:3::200:ff:fe00:8",
+                                                 "fe80::200:ff:fe00:8",
+                                                 1);
+    auto lr13 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:3::200:ff:fe00:7",
+                                                 Ipv6Address(buf),
+                                                 1);
+    auto lr14 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::PointToPoint,
+                                                 "::ffff:0.0.0.2",
+                                                 "2001:4::200:ff:fe00:4",
+                                                 "fe80::200:ff:fe00:4",
+                                                 1);
+    auto lr15 =
+        new GlobalRoutingLinkRecord<Ipv6Manager>(GlobalRoutingLinkRecord<Ipv6Manager>::StubNetwork,
+                                                 "2001:4::200:ff:fe00:3",
+                                                 Ipv6Address(buf),
+                                                 1);
+
+    auto lsa3 = new GlobalRoutingLSA<Ipv6Manager>();
+    lsa3->SetLSType(GlobalRoutingLSA<Ipv6Manager>::RouterLSA);
+    lsa3->SetStatus(GlobalRoutingLSA<Ipv6Manager>::LSA_SPF_NOT_EXPLORED);
+    lsa3->SetLinkStateId("::ffff:0.0.0.3");
+    lsa3->SetAdvertisingRouter("::ffff:0.0.0.3");
+    lsa3->SetNode(nodes.Get(3));
+    lsa3->AddLinkRecord(lr12);
+    lsa3->AddLinkRecord(lr13);
+    lsa3->AddLinkRecord(lr14);
+    lsa3->AddLinkRecord(lr15);
+    m_lsasv6.push_back(lsa3);
 }
 
 void
@@ -823,21 +1471,37 @@
     RngSeedManager::SetSeed(1);
     RngSeedManager::SetRun(1);
 
-    BuildLsa();
+    BuildLsav4();
 
     // insert the LSAs into the GlobalRouteManagerLSDB
-    auto srmlsdb = new GlobalRouteManagerLSDB();
-    srmlsdb->Insert(m_lsas[0]->GetLinkStateId(), m_lsas[0]);
-    srmlsdb->Insert(m_lsas[1]->GetLinkStateId(), m_lsas[1]);
-    srmlsdb->Insert(m_lsas[2]->GetLinkStateId(), m_lsas[2]);
-    srmlsdb->Insert(m_lsas[3]->GetLinkStateId(), m_lsas[3]);
+    auto srmlsdbv4 = new GlobalRouteManagerLSDB<Ipv4Manager>();
+    srmlsdbv4->Insert(m_lsasv4[0]->GetLinkStateId(), m_lsasv4[0]);
+    srmlsdbv4->Insert(m_lsasv4[1]->GetLinkStateId(), m_lsasv4[1]);
+    srmlsdbv4->Insert(m_lsasv4[2]->GetLinkStateId(), m_lsasv4[2]);
+    srmlsdbv4->Insert(m_lsasv4[3]->GetLinkStateId(), m_lsasv4[3]);
 
     // create the GlobalRouteManagerImpl
-    auto srm = new GlobalRouteManagerImpl();
-    srm->DebugUseLsdb(srmlsdb);
+    auto srmv4 = new GlobalRouteManagerImpl<Ipv4Manager>();
+    srmv4->DebugUseLsdb(srmlsdbv4);
 
     // we manually call the DebugSPFCalculate to fill the routing tables for node 0
-    srm->DebugSPFCalculate(m_lsas[0]->GetLinkStateId());
+    srmv4->DebugSPFCalculate(m_lsasv4[0]->GetLinkStateId());
+
+    // now for ipv6
+    BuildLsav6();
+
+    // insert the LSAs into the GlobalRouteManagerLSDB
+    auto srmlsdbv6 = new GlobalRouteManagerLSDB<Ipv6Manager>();
+    srmlsdbv6->Insert(m_lsasv6[0]->GetLinkStateId(), m_lsasv6[0]);
+    srmlsdbv6->Insert(m_lsasv6[1]->GetLinkStateId(), m_lsasv6[1]);
+    srmlsdbv6->Insert(m_lsasv6[2]->GetLinkStateId(), m_lsasv6[2]);
+    srmlsdbv6->Insert(m_lsasv6[3]->GetLinkStateId(), m_lsasv6[3]);
+
+    // create the GlobalRouteManagerImpl
+    auto srmv6 = new GlobalRouteManagerImpl<Ipv6Manager>();
+    srmv6->DebugUseLsdb(srmlsdbv6);
+    // we manually call the DebugSPFCalculate to fill the routing tables for node 0
+    srmv6->DebugSPFCalculate(m_lsasv6[0]->GetLinkStateId());
 
     // now the tests-----------------------
 
@@ -851,6 +1515,7 @@
     // assign streams to the UniformRandomVariable
     globalRouting0->AssignStreams(0);
     // check that the correct number of routes were built
+
     uint32_t nRoutes0 = globalRouting0->GetNRoutes();
     NS_TEST_ASSERT_MSG_EQ(nRoutes0, 14, "Error-- incorrect number of routes found on node 0");
 
@@ -894,7 +1559,7 @@
     expectedNextHops.emplace_back("10.1.2.2");
 
     // Test 1: Check the Routing Table of Node 0
-    CheckRoutes(globalRouting0, expectedDests, expectedNextHops);
+    CheckRoutesv4(globalRouting0, expectedDests, expectedNextHops);
 
     // Test 2: Check that the equal cost routes are being used at least once.
     // we need to call RouteOutput() at node 0 and check the output
@@ -903,29 +1568,114 @@
 
     Socket::SocketErrno errno_;
     Ptr<NetDevice> oif(nullptr);
-    Ptr<Packet> packet = Create<Packet>();
+    Ptr<Packet> packetv6 = Create<Packet>();
     Ipv4Header ipHeader;
     ipHeader.SetSource(Ipv4Address("10.1.1.1"));
     ipHeader.SetDestination(Ipv4Address("10.1.4.2"));
 
     for (uint32_t i = 0; i < 10; i++)
     {
-        Ptr<Ipv4Route> route = globalRouting0->RouteOutput(packet, ipHeader, oif, errno_);
-        IncrementCount(route);
+        Ptr<Ipv4Route> route = globalRouting0->RouteOutput(packetv6, ipHeader, oif, errno_);
+        IncrementCountv4(route);
+    }
+
+    // now the tests for IPv6-----------------------
+
+    Ptr<Ipv6L3Protocol> ip0v6 = nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ip0v6, nullptr, "Error-- no Ipv6 object at node 0");
+    Ptr<Ipv6RoutingProtocol> routing0v6 = ip0v6->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routing0v6, nullptr, "Error-- no Ipv6 routing protocol object at node 0");
+    Ptr<Ipv6GlobalRouting> globalRouting0v6 = routing0v6->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRouting0v6,
+                          nullptr,
+                          "Error-- no Ipv6GlobalRouting object at node 0");
+
+    // assign streams to the UniformRandomVariable
+    globalRouting0v6->AssignStreams(0);
+    // check that the correct number of routes were built
+    uint32_t nRoutes0v6 = globalRouting0v6->GetNRoutes();
+    NS_TEST_ASSERT_MSG_EQ(nRoutes0v6, 14, "Error-- incorrect number of routes found on node 0");
+
+    // check that the routes are correct
+    std::vector<Ipv6Address> expectedDestsv6;
+    std::vector<Ipv6Address> expectedNextHopsv6;
+
+    // add all the expected destinations and gateways. This is pretty verbose
+    // Please Note: The order of the following line is important because we check the routing table
+    // entries in order. The test will fail if we reorder the vector entries.
+
+    // For Routing table of Node 0 we check the following Destinations and Gateways
+    expectedDestsv6.emplace_back("2001:1::200:ff:fe00:2");
+    expectedDestsv6.emplace_back("2001:3::200:ff:fe00:7");
+    expectedDestsv6.emplace_back("2001:2::200:ff:fe00:6");
+    expectedDestsv6.emplace_back("2001:4::200:ff:fe00:3");
+    expectedDestsv6.emplace_back("2001:3::200:ff:fe00:8");
+    expectedDestsv6.emplace_back("2001:3::200:ff:fe00:8");
+    expectedDestsv6.emplace_back("2001:4::200:ff:fe00:4");
+    expectedDestsv6.emplace_back("2001:4::200:ff:fe00:4");
+    expectedDestsv6.emplace_back("2001:1::");
+    expectedDestsv6.emplace_back("2001:3::");
+    expectedDestsv6.emplace_back("2001:3::");
+    expectedDestsv6.emplace_back("2001:4::");
+    expectedDestsv6.emplace_back("2001:4::");
+    expectedDestsv6.emplace_back("2001:2::");
+
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:2");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    expectedNextHopsv6.emplace_back("fe80::200:ff:fe00:6");
+    // Test 1: Check the Routing Table of Node 0
+    CheckRoutesv6(globalRouting0v6, expectedDestsv6, expectedNextHopsv6);
+
+    // Test 2: Check that the equal cost routes are being used at least once.
+    // we need to call RouteOutput() at node 0 and check the output
+    // route from the table. The thing to check here is that different equal cost routes are
+    // returned across different calls to this method.
+
+    Ptr<Packet> packet = Create<Packet>();
+    Ipv6Header ipHeaderv6;
+    ipHeaderv6.SetSource(Ipv6Address("2001:1::200:ff:fe00:1"));
+    ipHeaderv6.SetDestination(Ipv6Address("2001:4::200:ff:fe00:4"));
+
+    for (uint32_t i = 0; i < 10; i++)
+    {
+        Ptr<Ipv6Route> route = globalRouting0v6->RouteOutput(packet, ipHeaderv6, oif, errno_);
+        IncrementCountv6(route);
     }
 
     Simulator::Run();
 
     NS_TEST_ASSERT_MSG_GT_OR_EQ(
-        route1,
+        route1v4,
         1,
         "The routing path for node 0 to node 3 does not match the expected output, "
-        "Equal Cost MultiPath (ECMP) routes are not being used correctly");
+        "Equal Cost MultiPath (ECMP) routes are not being used correctly for ipv4");
     NS_TEST_ASSERT_MSG_GT_OR_EQ(
-        route2,
+        route2v4,
         1,
         "The routing path for node 0 to node 3 does not match the expected output, "
-        "Equal Cost MultiPath (ECMP) routes are not being used correctly");
+        "Equal Cost MultiPath (ECMP) routes are not being used correctly for ipv4");
+
+    NS_TEST_ASSERT_MSG_GT_OR_EQ(
+        route1v6,
+        1,
+        "The routing path for node 0 to node 3 does not match the expected output, "
+        "Equal Cost MultiPath (ECMP) routes are not being used correctly for ipv6");
+    NS_TEST_ASSERT_MSG_GT_OR_EQ(
+        route2v6,
+        1,
+        "The routing path for node 0 to node 3 does not match the expected output, "
+        "Equal Cost MultiPath (ECMP) routes are not being used correctly for ipv6");
 
     // end of test--------------------
 
@@ -937,7 +1687,8 @@
     RngSeedManager::SetRun(oldrun);
     // This delete clears the srm, which deletes the LSDB, which clears
     // all of the LSAs, which each destroys the attached LinkRecords.
-    delete srm;
+    delete srmv4;
+    delete srmv6;
 }
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/global-routing-test-suite.cc ns-3.47/src/internet/test/global-routing-test-suite.cc
--- ns-3.46/src/internet/test/global-routing-test-suite.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/test/global-routing-test-suite.cc	2026-02-16 16:08:49.623230000 -0800
@@ -0,0 +1,2974 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#include "ns3/boolean.h"
+#include "ns3/bridge-helper.h"
+#include "ns3/config.h"
+#include "ns3/global-route-manager.h"
+#include "ns3/global-routing.h"
+#include "ns3/inet-socket-address.h"
+#include "ns3/internet-stack-helper.h"
+#include "ns3/ipv4-address-helper.h"
+#include "ns3/ipv4-global-routing-helper.h"
+#include "ns3/ipv4-interface.h"
+#include "ns3/ipv4-l3-protocol.h"
+#include "ns3/ipv4-packet-info-tag.h"
+#include "ns3/ipv4-routing-protocol.h"
+#include "ns3/ipv4-routing-table-entry.h"
+#include "ns3/ipv4-static-routing-helper.h"
+#include "ns3/ipv6-address-helper.h"
+#include "ns3/ipv6-global-routing-helper.h"
+#include "ns3/ipv6-packet-info-tag.h"
+#include "ns3/ipv6-route.h"
+#include "ns3/log.h"
+#include "ns3/neighbor-cache-helper.h"
+#include "ns3/node-container.h"
+#include "ns3/node.h"
+#include "ns3/packet.h"
+#include "ns3/pointer.h"
+#include "ns3/simple-channel.h"
+#include "ns3/simple-net-device-helper.h"
+#include "ns3/simple-net-device.h"
+#include "ns3/simulator.h"
+#include "ns3/socket-factory.h"
+#include "ns3/string.h"
+#include "ns3/test.h"
+#include "ns3/udp-socket-factory.h"
+#include "ns3/uinteger.h"
+
+#include <vector>
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("GlobalRoutingTestSuite");
+
+// This test suite tests the operation of global routing on a few sample
+// networks to ensure that routes are built correctly
+//
+//  Link test:
+//      n0 <--------> n1  (point-to-point link)
+//  10.1.1.1        10.1.1.2
+//      Expected routes:
+//         n0:  route to 0.0.0.0 gw 10.1.1.2
+//         n1:  route to 0.0.0.0 gw 10.1.1.1
+//      Note:  These default routes to 0.0.0.0 are generated by the extension
+//             in the global route manager to install default routes via the
+//             peer node on a point-to-point link, when the node is on a
+//             stub link
+//
+//  LAN test:
+//      n0 <--------> n1  (broadcast link on subnet 10.1.1.0/24)
+//      Expected routes:
+//         n0:  route to 10.1.1.0 gw 0.0.0.0
+//         n1:  route to 10.1.1.0 gw 0.0.0.0
+//  Two link test:
+//      n0 <--------> n1 <--------> n2  (point-to-point links)
+//  10.1.1.1       10.1.1.2/     10.1.2.2
+//                 10.1.2.1
+//      Expected routes:
+//         n0:  route to 0.0.0.0 gw 10.1.1.2
+//         n1:  route to 10.1.1.1 gw 10.1.1.1
+//              route to 10.1.2.2 gw 10.1.2.2
+//              route to 10.1.1.0 gw 10.1.1.1
+//              route to 10.1.2.0 gw 10.1.2.2
+//         n2:  route to 0.0.0.0 gw 10.1.2.1
+//      Note:  These default routes to 0.0.0.0 are generated by the extension
+//             in the global route manager to install default routes via the
+//             peer node on a point-to-point link, when the node is on a
+//             stub link
+//  Two LANs test:
+//      n0 <--------> n1 <--------> n2  (broadcast links)
+//      Expected routes:
+//         n0:  route to 10.1.1.0 gw 0.0.0.0
+//              route to 0.0.0.0 gw 10.1.1.2
+//         n1:  route to 10.1.1.1 gw 10.1.1.1
+//              route to 10.1.2.2 gw 10.1.2.2
+//              route to 10.1.1.0 gw 10.1.1.1
+//              route to 10.1.2.0 gw 10.1.2.2
+//         n2:  route to 0.0.0.0 gw 10.1.2.1
+//  Bridge test:
+//      n0 <--------> n1 <---> Bridge-n2 <---> n3 <-------> n4 (broadcast links)
+//         10.1.1.0/24        10.1.2.0/24        10.1.3.0/24
+//      Expected routes:
+//         n0:  route to 10.1.1.0 gw 0.0.0.0
+//              route to 10.1.2.0 gw 10.1.1.2
+//              route to 10.1.3.0 gw 10.1.1.2
+//         n1:  route to 10.1.1.0 gw 0.0.0.0
+//              route to 10.1.2.0 gw 0.0.0.0
+//              route to 10.1.3.0 gw 10.1.2.2
+//         n3:  route to 10.1.1.0 gw 10.1.2.1
+//              route to 10.1.2.0 gw 0.0.0.0
+//              route to 10.1.3.0 gw 0.0.0.0
+//         n4:  route to 10.1.3.0 gw 0.0.0.0
+//              route to 10.1.2.0 gw 10.1.3.1
+//              route to 10.1.1.0 gw 10.1.3.1
+//  Two Bridge test:
+//      n0 <------> n1 <---> Bridge-n2 <---> Bridge-n3 <---> n4 (broadcast links)
+//         10.1.1.0/24        10.1.2.0/24
+//      Expected routes:
+//         n0:  route to 10.1.1.0 gw 0.0.0.0
+//              route to 10.1.2.0 gw 10.1.1.2
+//         n4:  route to 10.1.2.0 gw 0.0.0.0
+//              route to 10.1.1.0 gw 10.1.2.1
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief GlobalRouting Link test
+ */
+class LinkTest : public TestCase
+{
+  public:
+    void DoSetup() override;
+    void DoRun() override;
+    LinkTest();
+
+  private:
+    NodeContainer m_nodes; //!< Nodes used in the test.
+};
+
+LinkTest::LinkTest()
+    : TestCase("Global routing on point-to-point link")
+{
+}
+
+void
+LinkTest::DoSetup()
+{
+    m_nodes.Create(2);
+    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper;
+    simpleHelper.SetNetDevicePointToPointMode(true);
+    NetDeviceContainer net = simpleHelper.Install(m_nodes, channel);
+
+    InternetStackHelper internet;
+    // By default, InternetStackHelper adds a static and global routing
+    // implementation.  We just want the global for this test.
+    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    internet.SetRoutingHelper(ipv4RoutingHelper);
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+    internet.Install(m_nodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.252");
+    Ipv4InterfaceContainer iv4 = ipv4.Assign(net);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase(Ipv6Address("2001:0::"), Ipv6Prefix(64));
+    Ipv6InterfaceContainer iv6 = ipv6.Assign(net);
+}
+
+void
+LinkTest::DoRun()
+{
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    //----------Ipv4 Tests-------------------------
+    Ptr<Ipv4L3Protocol> ipv40 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv40, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4L3Protocol> ipv41 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv41, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routing0 = ipv40->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingV40 = routing0->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingV40, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv4RoutingProtocol> routingV41 = ipv41->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingV41 = routingV41->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingV41, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesV40 = globalRoutingV40->GetNRoutes();
+    NS_LOG_DEBUG("LinkTest nRoutes0 " << nRoutesV40);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesV40, 1, "Error-- not one route");
+    Ipv4RoutingTableEntry* routeV4 = globalRoutingV40->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routeV4->GetDest() << " gw " << routeV4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routeV4->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routeV4->GetGateway(), Ipv4Address("10.1.1.2"), "Error-- wrong gateway");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesV41 = globalRoutingV41->GetNRoutes();
+    NS_TEST_ASSERT_MSG_EQ(nRoutesV41, 1, "Error-- not one route");
+    NS_LOG_DEBUG("LinkTest nRoutes1 " << nRoutesV41);
+    routeV4 = globalRoutingV41->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routeV4->GetDest() << " gw " << routeV4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routeV4->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routeV4->GetGateway(), Ipv4Address("10.1.1.1"), "Error-- wrong gateway");
+
+    //-------------Ipv6 Tests---------------------
+    Ptr<Ipv6L3Protocol> ipv60 = m_nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv60, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6L3Protocol> ipv61 = m_nodes.Get(1)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv61, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6RoutingProtocol> routingV60 = ipv60->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingV60 = routingV60->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingV60, nullptr, "Error-- no Ipv6GlobalRouting object");
+    Ptr<Ipv6RoutingProtocol> routingV61 = ipv61->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingV61 = routingV61->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingV61, nullptr, "Error-- no Ipv6GlobalRouting object");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesV60 = globalRoutingV60->GetNRoutes();
+    NS_LOG_DEBUG("LinkTest nRoutes0 " << nRoutesV60);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesV60, 1, "Error-- not one route");
+    Ipv6RoutingTableEntry* routeV6 = globalRoutingV60->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routeV6->GetDest() << " gw " << routeV6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routeV6->GetDest(), Ipv6Address("::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routeV6->GetDestNetworkPrefix(),
+                          Ipv6Prefix::GetZero(),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routeV6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:2"),
+                          "Error-- wrong gateway");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesV61 = globalRoutingV61->GetNRoutes();
+    NS_TEST_ASSERT_MSG_EQ(nRoutesV61, 1, "Error-- not one route");
+    NS_LOG_DEBUG("LinkTest nRoutes1 " << nRoutesV61);
+    routeV6 = globalRoutingV61->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routeV6->GetDest() << " gw " << routeV6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routeV6->GetDest(), Ipv6Address("::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routeV6->GetDestNetworkPrefix(),
+                          Ipv6Prefix::GetZero(),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routeV6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:1"),
+                          "Error-- wrong gateway");
+
+    Simulator::Run();
+    Simulator::Stop(Seconds(10));
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief GlobalRouting LAN test
+ */
+class LanTest : public TestCase
+{
+  public:
+    void DoSetup() override;
+    void DoRun() override;
+    LanTest();
+
+  private:
+    NodeContainer m_nodes; //!< Nodes used in the test.
+};
+
+LanTest::LanTest()
+    : TestCase("Global routing on broadcast link")
+{
+}
+
+void
+LanTest::DoSetup()
+{
+    m_nodes.Create(2);
+
+    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper;
+    NetDeviceContainer net = simpleHelper.Install(m_nodes, channel);
+
+    InternetStackHelper internet;
+    // By default, InternetStackHelper adds a static and global routing
+    // implementation.  We just want the global for this test.
+    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    internet.SetRoutingHelper(ipv4RoutingHelper);
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+    internet.Install(m_nodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer i = ipv4.Assign(net);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase(Ipv6Address("2001:0::"), Ipv6Prefix(64));
+    Ipv6InterfaceContainer iv6 = ipv6.Assign(net);
+}
+
+void
+LanTest::DoRun()
+{
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    //----------Ipv4 Tests-------------------------------
+    Ptr<Ipv4L3Protocol> ipv40 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv40, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4L3Protocol> ipv41 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv41, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv40 = ipv40->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv40 = routingv40->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv40, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv4RoutingProtocol> routingv41 = ipv41->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv41 = routingv41->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv41, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv40 = globalRoutingv40->GetNRoutes();
+    NS_LOG_DEBUG("LanTest nRoutes0 " << nRoutesv40);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv40, 1, "Error-- more than one entry");
+    for (uint32_t i = 0; i < globalRoutingv40->GetNRoutes(); i++)
+    {
+        Ipv4RoutingTableEntry* routev4 = globalRoutingv40->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv41 = globalRoutingv41->GetNRoutes();
+    NS_LOG_DEBUG("LanTest nRoutes1 " << nRoutesv41);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv41, 1, "Error-- more than one entry");
+    for (uint32_t i = 0; i < globalRoutingv40->GetNRoutes(); i++)
+    {
+        Ipv4RoutingTableEntry* routev4 = globalRoutingv41->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+
+    //-------------Ipv6 Tests----------------------
+    Ptr<Ipv6L3Protocol> ipv60 = m_nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv60, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6L3Protocol> ipv61 = m_nodes.Get(1)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv61, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6RoutingProtocol> routingv60 = ipv60->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv60 = routingv60->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv60, nullptr, "Error-- no Ipv6GlobalRouting object");
+    Ptr<Ipv6RoutingProtocol> routingv61 = ipv61->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv61 = routingv61->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv61, nullptr, "Error-- no Ipv6GlobalRouting object");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv60 = globalRoutingv60->GetNRoutes();
+    NS_LOG_DEBUG("LanTest nRoutes0 " << nRoutesv60);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv60, 1, "Error-- more than one entry");
+    for (uint32_t i = 0; i < globalRoutingv60->GetNRoutes(); i++)
+    {
+        Ipv6RoutingTableEntry* routev6 = globalRoutingv60->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << routev6->GetDestNetworkPrefix()
+                                   << " gw " << routev6->GetGateway());
+    }
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv61 = globalRoutingv61->GetNRoutes();
+    NS_LOG_DEBUG("LanTest nRoutes1 " << nRoutesv61);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv61, 1, "Error-- more than one entry");
+    for (uint32_t i = 0; i < globalRoutingv60->GetNRoutes(); i++)
+    {
+        Ipv6RoutingTableEntry* routev6 = globalRoutingv61->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << routev6->GetDestNetworkPrefix()
+                                   << " gw " << routev6->GetGateway());
+    }
+
+    Simulator::Run();
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief GlobalRouting Two Link test
+ */
+class TwoLinkTest : public TestCase
+{
+  public:
+    void DoSetup() override;
+    void DoRun() override;
+    TwoLinkTest();
+
+  private:
+    NodeContainer m_nodes; //!< Nodes used in the test.
+};
+
+TwoLinkTest::TwoLinkTest()
+    : TestCase("Global routing across two hops (point-to-point links)")
+{
+}
+
+void
+TwoLinkTest::DoSetup()
+{
+    m_nodes.Create(3);
+
+    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper;
+    simpleHelper.SetNetDevicePointToPointMode(true);
+    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
+    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
+
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper2;
+    simpleHelper2.SetNetDevicePointToPointMode(true);
+    NetDeviceContainer net2 = simpleHelper.Install(m_nodes.Get(1), channel2);
+    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
+
+    InternetStackHelper internet;
+    // By default, InternetStackHelper adds a static and global routing
+    // implementation.  We just want the global for this test.
+    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    internet.SetRoutingHelper(ipv4RoutingHelper);
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+
+    internet.Install(m_nodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.252");
+    Ipv4InterfaceContainer i = ipv4.Assign(net);
+    ipv4.SetBase("10.1.2.0", "255.255.255.252");
+    Ipv4InterfaceContainer i2 = ipv4.Assign(net2);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase(Ipv6Address("2001:0::"), Ipv6Prefix(64));
+    ipv6.Assign(net);
+    ipv6.SetBase(Ipv6Address("2001:1::"), Ipv6Prefix(64));
+    ipv6.Assign(net2);
+}
+
+void
+TwoLinkTest::DoRun()
+{
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    //-----Ipv4 Tests-----------------
+    Ptr<Ipv4L3Protocol> ipv40 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv40, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4L3Protocol> ipv41 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv41, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4L3Protocol> ipv42 = m_nodes.Get(2)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv42, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv40 = ipv40->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv40 = routingv40->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv40, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv4RoutingProtocol> routingv41 = ipv41->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv41 = routingv41->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv41, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv4RoutingProtocol> routingv42 = ipv42->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv42 = routingv42->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv42, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // node n0
+    // Test that the right number of routes found
+    uint32_t nRoutesv40 = globalRoutingv40->GetNRoutes();
+    NS_LOG_DEBUG("TwoLinkTest nRoutes0 " << nRoutesv40);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv40, 1, "Error-- wrong number of links");
+
+    Ipv4RoutingTableEntry* routev4 = globalRoutingv40->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.1.2"), "Error-- wrong gateway");
+
+    // node n1
+    // Test that the right number of routes found
+    uint32_t nRoutesv41 = globalRoutingv41->GetNRoutes();
+    NS_LOG_DEBUG("TwoLinkTest nRoutes1 " << nRoutesv41);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv41, 4, "Error-- not one route");
+
+    routev4 = globalRoutingv41->GetRoute(0);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev4->GetDest() << " gw "
+                                           << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.1.1"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.1.1"), "Error-- wrong gateway");
+    routev4 = globalRoutingv41->GetRoute(1);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev4->GetDest() << " gw "
+                                           << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.2.2"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.2.2"), "Error-- wrong gateway");
+    routev4 = globalRoutingv41->GetRoute(2);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev4->GetDest() << " gw "
+                                           << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.1.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.1.1"), "Error-- wrong gateway");
+    routev4 = globalRoutingv41->GetRoute(3);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev4->GetDest() << " gw "
+                                           << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.2.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.2.2"), "Error-- wrong gateway");
+
+    // node n2
+    // Test that the right number of routes found
+    uint32_t nRoutesv42 = globalRoutingv42->GetNRoutes();
+    NS_LOG_DEBUG("TwoLinkTest nRoutes2 " << nRoutesv42);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv42, 1, "Error-- wrong number of links");
+
+    routev4 = globalRoutingv42->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.2.1"), "Error-- wrong gateway");
+
+    //--------------Ipv6 Tests-------------
+    Ptr<Ipv6L3Protocol> ipv60 = m_nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv60, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6L3Protocol> ipv61 = m_nodes.Get(1)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv61, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6L3Protocol> ipv62 = m_nodes.Get(2)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv62, nullptr, "Error-- no Ipv6 object");
+    Ptr<Ipv6RoutingProtocol> routingv60 = ipv60->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv60 = routingv60->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv60, nullptr, "Error-- no Ipv6GlobalRouting object");
+    Ptr<Ipv6RoutingProtocol> routingv61 = ipv61->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv61 = routingv61->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv61, nullptr, "Error-- no Ipv6GlobalRouting object");
+    Ptr<Ipv6RoutingProtocol> routingv62 = ipv62->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv62 = routingv62->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv62, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // node n0
+    // Test that the right number of routes found
+    uint32_t nRoutesv60 = globalRoutingv60->GetNRoutes();
+    NS_LOG_DEBUG("TwoLinkTest nRoutes0 " << nRoutesv60);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv60, 1, "Error-- wrong number of links");
+
+    Ipv6RoutingTableEntry* routev6 = globalRoutingv60->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix::GetZero(),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:2"),
+                          "Error-- wrong gateway");
+
+    // node n1
+    // Test that the right number of routes found
+    uint32_t nRoutesv61 = globalRoutingv61->GetNRoutes();
+    NS_LOG_DEBUG("TwoLinkTest nRoutes1 " << nRoutesv61);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv61, 4, "Error-- not one route");
+
+    routev6 = globalRoutingv61->GetRoute(0);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev6->GetDest() << " gw "
+                                           << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                          Ipv6Address("2001::200:ff:fe00:1"),
+                          "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(128),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:1"),
+                          "Error-- wrong gateway");
+
+    routev6 = globalRoutingv61->GetRoute(1);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev6->GetDest() << " gw "
+                                           << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                          Ipv6Address("2001:1::200:ff:fe00:4"),
+                          "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(128),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:4"),
+                          "Error-- wrong gateway");
+
+    routev6 = globalRoutingv61->GetRoute(2);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev6->GetDest() << " gw "
+                                           << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("2001::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(64),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:1"),
+                          "Error-- wrong gateway");
+
+    routev6 = globalRoutingv61->GetRoute(3);
+    NS_LOG_DEBUG("TwoLinkTest entry dest " << routev6->GetDest() << " gw "
+                                           << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("2001:1::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(64),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:4"),
+                          "Error-- wrong gateway");
+
+    // node n2
+    // Test that the right number of routes found
+    uint32_t nRoutesv62 = globalRoutingv62->GetNRoutes();
+    NS_LOG_DEBUG("TwoLinkTest nRoutes2 " << nRoutesv62);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv62, 1, "Error-- wrong number of links");
+
+    routev6 = globalRoutingv62->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix::GetZero(),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:3"),
+                          "Error-- wrong gateway");
+
+    Simulator::Run();
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief GlobalRouting Two LAN test
+ */
+class TwoLanTest : public TestCase
+{
+  public:
+    void DoSetup() override;
+    void DoRun() override;
+    TwoLanTest();
+
+  private:
+    NodeContainer m_nodes; //!< Nodes used in the test.
+};
+
+TwoLanTest::TwoLanTest()
+    : TestCase("Global routing across two hops (broadcast links)")
+{
+}
+
+void
+TwoLanTest::DoSetup()
+{
+    m_nodes.Create(3);
+
+    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper;
+    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
+    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
+
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper2;
+    NetDeviceContainer net2 = simpleHelper.Install(m_nodes.Get(1), channel2);
+    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
+
+    InternetStackHelper internet;
+    // By default, InternetStackHelper adds a static and global routing
+    // implementation.  We just want the global for this test.
+    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    internet.SetRoutingHelper(ipv4RoutingHelper);
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+    internet.Install(m_nodes);
+
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer i = ipv4.Assign(net);
+    ipv4.SetBase("10.1.2.0", "255.255.255.0");
+    Ipv4InterfaceContainer i2 = ipv4.Assign(net2);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase(Ipv6Address("2001:0::"), Ipv6Prefix(64));
+    ipv6.Assign(net);
+    ipv6.SetBase(Ipv6Address("2001:1::"), Ipv6Prefix(64));
+    ipv6.Assign(net2);
+}
+
+void
+TwoLanTest::DoRun()
+{
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    //----------Ipv4 Tests------------
+    Ptr<Ipv4L3Protocol> ipv40 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv40, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4L3Protocol> ipv41 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv41, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4L3Protocol> ipv42 = m_nodes.Get(2)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv42, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv40 = ipv40->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv40 = routingv40->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv40, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv4RoutingProtocol> routingv41 = ipv41->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv41 = routingv41->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv41, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv4RoutingProtocol> routingv42 = ipv42->GetRoutingProtocol();
+    Ptr<Ipv4GlobalRouting> globalRoutingv42 = routingv42->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv42, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv40 = globalRoutingv40->GetNRoutes();
+    NS_LOG_DEBUG("TwoLanTest nRoutes0 " << nRoutesv40);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv40, 2, "Error-- not two entries");
+    Ipv4RoutingTableEntry* routev4 = globalRoutingv40->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.1.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("0.0.0.0"), "Error-- wrong gateway");
+    routev4 = globalRoutingv40->GetRoute(1);
+    NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.2.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("10.1.1.2"), "Error-- wrong gateway");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv41 = globalRoutingv41->GetNRoutes();
+    NS_LOG_DEBUG("TwoLanTest nRoutes1 " << nRoutesv41);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv41, 2, "Error-- not two entries");
+    routev4 = globalRoutingv41->GetRoute(0);
+    NS_LOG_DEBUG("TwoLanTest entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.1.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("0.0.0.0"), "Error-- wrong gateway");
+    routev4 = globalRoutingv41->GetRoute(1);
+    NS_LOG_DEBUG("TwoLanTest entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(), Ipv4Address("10.1.2.0"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(), Ipv4Address("0.0.0.0"), "Error-- wrong gateway");
+
+    //-------------Ipv6 Tests------------
+    Ptr<Ipv6L3Protocol> ipv60 = m_nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv60, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6L3Protocol> ipv61 = m_nodes.Get(1)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv61, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6L3Protocol> ipv62 = m_nodes.Get(2)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv62, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6RoutingProtocol> routingv60 = ipv60->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv60 = routingv60->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv60, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv6RoutingProtocol> routingv61 = ipv61->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv61 = routingv61->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv61, nullptr, "Error-- no Ipv4GlobalRouting object");
+    Ptr<Ipv6RoutingProtocol> routingv62 = ipv62->GetRoutingProtocol();
+    Ptr<Ipv6GlobalRouting> globalRoutingv62 = routingv62->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv62, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv60 = globalRoutingv60->GetNRoutes();
+    NS_LOG_DEBUG("TwoLanTest nRoutes0 " << nRoutesv60);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv60, 2, "Error-- not two entries");
+
+    Ipv6RoutingTableEntry* routev6 = globalRoutingv60->GetRoute(0);
+    NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("2001::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(64),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(), Ipv6Address("::"), "Error-- wrong gateway");
+
+    routev6 = globalRoutingv60->GetRoute(1);
+    NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("2001:1::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(64),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                          Ipv6Address("fe80::200:ff:fe00:2"),
+                          "Error-- wrong gateway");
+
+    // Test that the right number of routes found
+    uint32_t nRoutesv61 = globalRoutingv61->GetNRoutes();
+    NS_LOG_DEBUG("TwoLanTest nRoutes1 " << nRoutesv61);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv61, 2, "Error-- not two entries");
+
+    routev6 = globalRoutingv61->GetRoute(0);
+    NS_LOG_DEBUG("TwoLanTest entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("2001::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(64),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(), Ipv6Address("::"), "Error-- wrong gateway");
+
+    routev6 = globalRoutingv61->GetRoute(1);
+    NS_LOG_DEBUG("TwoLanTest entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(), Ipv6Address("2001:1::"), "Error-- wrong destination");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                          Ipv6Prefix(64),
+                          "Error-- wrong destination mask");
+    NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(), Ipv6Address("::"), "Error-- wrong gateway");
+
+    Simulator::Run();
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief GlobalRouting Bridge test
+ */
+class BridgeTest : public TestCase
+{
+  public:
+    void DoSetup() override;
+    void DoRun() override;
+    BridgeTest();
+
+  private:
+    NodeContainer m_nodes; //!< Nodes used in the test.
+};
+
+BridgeTest::BridgeTest()
+    : TestCase("Global routing across bridging topology (bug 2102)")
+{
+}
+
+void
+BridgeTest::DoSetup()
+{
+    m_nodes.Create(5);
+
+    // connect node0 to node1
+    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper;
+    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
+    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
+
+    NetDeviceContainer bridgeFacingDevices;
+    NetDeviceContainer switchDevices;
+
+    // connect node1 to node2 (switch)
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper2;
+    NetDeviceContainer net2 = simpleHelper2.Install(m_nodes.Get(1), channel2);
+    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
+    bridgeFacingDevices.Add(net2.Get(0));
+    switchDevices.Add(net2.Get(1));
+
+    // connect node2 (switch) to node3
+    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper3;
+    NetDeviceContainer net3 = simpleHelper3.Install(m_nodes.Get(2), channel3);
+    net3.Add(simpleHelper3.Install(m_nodes.Get(3), channel3));
+    bridgeFacingDevices.Add(net3.Get(1));
+    switchDevices.Add(net3.Get(0));
+
+    // connect node3 to node4
+    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper4;
+    NetDeviceContainer net4 = simpleHelper4.Install(m_nodes.Get(3), channel4);
+    net4.Add(simpleHelper4.Install(m_nodes.Get(4), channel4));
+
+    Ptr<Node> switchNode = m_nodes.Get(2);
+    BridgeHelper bridge;
+    bridge.Install(switchNode, switchDevices);
+
+    InternetStackHelper internet;
+    // By default, InternetStackHelper adds a static and global routing
+    // implementation.  We just want the global for this test.
+    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    internet.SetRoutingHelper(ipv4RoutingHelper);
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+
+    internet.Install(m_nodes.Get(0));
+    internet.Install(m_nodes.Get(1));
+    // m_nodes.Get (2) is bridge node
+    internet.Install(m_nodes.Get(3));
+    internet.Install(m_nodes.Get(4));
+
+    Ipv4AddressHelper address;
+    address.SetBase("10.1.1.0", "255.255.255.0");
+    address.Assign(net);
+
+    address.SetBase("10.1.2.0", "255.255.255.0");
+    address.Assign(bridgeFacingDevices);
+
+    address.SetBase("10.1.3.0", "255.255.255.0");
+    address.Assign(net4);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase(Ipv6Address("2001:0::"), Ipv6Prefix(64));
+    ipv6.Assign(net);
+    ipv6.SetBase(Ipv6Address("2001:1::"), Ipv6Prefix(64));
+    ipv6.Assign(bridgeFacingDevices);
+    ipv6.SetBase(Ipv6Address("2001:2::"), Ipv6Prefix(64));
+    ipv6.Assign(net4);
+}
+
+void
+BridgeTest::DoRun()
+{
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    //------------Ipv4 Tests--------------------
+    Ptr<Ipv4L3Protocol> ipv40 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv40, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv40 = ipv40->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv40, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv4GlobalRouting> globalRoutingv40 = routingv40->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv40, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    Ptr<Ipv4L3Protocol> ipv41 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv41, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv41 = ipv41->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv41, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv4GlobalRouting> globalRoutingv41 = routingv41->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv41, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Skip to n4
+    Ptr<Ipv4L3Protocol> ipv44 = m_nodes.Get(4)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv44, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv44 = ipv44->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv44, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv4GlobalRouting> globalRoutingv44 = routingv44->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv44, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    Ipv4RoutingTableEntry* routev4 = nullptr;
+    // n0
+    // Test that the right number of routes found
+    uint32_t nRoutesv40 = globalRoutingv40->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes0 " << nRoutesv40);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv40, 3, "Error-- not three entries");
+    for (uint32_t i = 0; i < globalRoutingv40->GetNRoutes(); i++)
+    {
+        routev4 = globalRoutingv40->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+    // Spot check the last route
+    if (routev4)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(),
+                              Ipv4Address("10.1.3.0"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(),
+                              Ipv4Address("10.1.1.2"),
+                              "Error-- wrong gateway");
+    }
+
+    // n1
+    // Test that the right number of routes found
+    routev4 = nullptr;
+    uint32_t nRoutesv41 = globalRoutingv41->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes1 " << nRoutesv41);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv41, 3, "Error-- not three entries");
+    for (uint32_t i = 0; i < globalRoutingv41->GetNRoutes(); i++)
+    {
+        routev4 = globalRoutingv41->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+    // Spot check the last route
+    if (routev4)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(),
+                              Ipv4Address("10.1.3.0"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(),
+                              Ipv4Address("10.1.2.2"),
+                              "Error-- wrong gateway");
+    }
+
+    // skip n2 and n3, just verify n4
+    NS_LOG_DEBUG("BridgeTest skip print out of n2 and n3, go next to node n4");
+
+    // n4
+    routev4 = nullptr;
+    // Test that the right number of routes found
+    uint32_t nRoutesv44 = globalRoutingv44->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes4 " << nRoutesv44);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv44, 3, "Error-- not three entries");
+    for (uint32_t i = 0; i < globalRoutingv44->GetNRoutes(); i++)
+    {
+        routev4 = globalRoutingv44->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+    // Spot check the last route
+    if (routev4)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(),
+                              Ipv4Address("10.1.1.0"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(),
+                              Ipv4Address("10.1.3.1"),
+                              "Error-- wrong gateway");
+    }
+
+    //------------------Ipv6 Tests----------------
+    Ptr<Ipv6L3Protocol> ipv60 = m_nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv60, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6RoutingProtocol> routingv60 = ipv60->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv60, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv6GlobalRouting> globalRoutingv60 = routingv60->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv60, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    Ptr<Ipv6L3Protocol> ipv61 = m_nodes.Get(1)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv61, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6RoutingProtocol> routingv61 = ipv61->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv61, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv6GlobalRouting> globalRoutingv61 = routingv61->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv61, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Skip to n4
+    Ptr<Ipv6L3Protocol> ipv64 = m_nodes.Get(4)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv64, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6RoutingProtocol> routingv64 = ipv64->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv64, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv6GlobalRouting> globalRoutingv64 = routingv64->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv64, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    Ipv6RoutingTableEntry* routev6 = nullptr;
+    // n0
+    // Test that the right number of routes found
+    uint32_t nRoutesv60 = globalRoutingv60->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes0 " << nRoutesv60);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv60, 3, "Error-- not three entries");
+    for (uint32_t i = 0; i < globalRoutingv60->GetNRoutes(); i++)
+    {
+        routev6 = globalRoutingv60->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    }
+    // Spot check the last route
+    if (routev6)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                              Ipv6Address("2001:2::"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                              Ipv6Prefix(64),
+                              "Error-- wrong destination mask");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                              Ipv6Address("fe80::200:ff:fe00:2"),
+                              "Error-- wrong gateway");
+    }
+
+    // n1
+    // Test that the right number of routes found
+    routev6 = nullptr;
+    uint32_t nRoutesv61 = globalRoutingv61->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes1 " << nRoutesv61);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv61, 3, "Error-- not three entries");
+    for (uint32_t i = 0; i < globalRoutingv61->GetNRoutes(); i++)
+    {
+        routev6 = globalRoutingv61->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    }
+    // Spot check the last route
+    if (routev6)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                              Ipv6Address("2001:2::"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                              Ipv6Prefix(64),
+                              "Error-- wrong destination mask");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                              Ipv6Address("fe80::200:ff:fe00:6"),
+                              "Error-- wrong gateway");
+    }
+
+    // skip n2 and n3, just verify n4
+    NS_LOG_DEBUG("BridgeTest skip print out of n2 and n3, go next to node n4");
+
+    // n4
+    routev6 = nullptr;
+    // Test that the right number of routes found
+    uint32_t nRoutesv64 = globalRoutingv64->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes4 " << nRoutesv64);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv64, 3, "Error-- not three entries");
+    for (uint32_t i = 0; i < globalRoutingv64->GetNRoutes(); i++)
+    {
+        routev6 = globalRoutingv64->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    }
+    // Spot check the last route
+    if (routev6)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                              Ipv6Address("2001::"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                              Ipv6Prefix(64),
+                              "Error-- wrong destination mask");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                              Ipv6Address("fe80::200:ff:fe00:7"),
+                              "Error-- wrong gateway");
+    }
+    Simulator::Run();
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief GlobalRouting Two bridges test
+ */
+class TwoBridgeTest : public TestCase
+{
+  public:
+    void DoSetup() override;
+    void DoRun() override;
+    TwoBridgeTest();
+
+  private:
+    NodeContainer m_nodes; //!< Nodes used in the test.
+};
+
+TwoBridgeTest::TwoBridgeTest()
+    : TestCase("Global routing across two bridges")
+{
+}
+
+void
+TwoBridgeTest::DoSetup()
+{
+    m_nodes.Create(5);
+
+    // connect node0 to node1
+    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper;
+    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
+    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
+
+    NetDeviceContainer bridgeFacingDevices;
+    NetDeviceContainer switchn2Devices;
+    NetDeviceContainer switchn3Devices;
+
+    // connect node1 to node2 (switch)
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper2;
+    NetDeviceContainer net2 = simpleHelper2.Install(m_nodes.Get(1), channel2);
+    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
+    bridgeFacingDevices.Add(net2.Get(0));
+    switchn2Devices.Add(net2.Get(1));
+
+    // connect node2 (switch) to node3
+    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper3;
+    NetDeviceContainer net3 = simpleHelper3.Install(m_nodes.Get(2), channel3);
+    net3.Add(simpleHelper3.Install(m_nodes.Get(3), channel3));
+    switchn2Devices.Add(net3.Get(0));
+    switchn3Devices.Add(net3.Get(1));
+
+    // connect node3 to node4
+    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
+    SimpleNetDeviceHelper simpleHelper4;
+    NetDeviceContainer net4 = simpleHelper4.Install(m_nodes.Get(3), channel4);
+    net4.Add(simpleHelper4.Install(m_nodes.Get(4), channel4));
+    switchn3Devices.Add(net4.Get(0));
+    bridgeFacingDevices.Add(net4.Get(1));
+
+    Ptr<Node> switchn2Node = m_nodes.Get(2);
+    BridgeHelper bridgen2Helper;
+    bridgen2Helper.Install(switchn2Node, switchn2Devices);
+
+    Ptr<Node> switchn3Node = m_nodes.Get(3);
+    BridgeHelper bridgen3Helper;
+    bridgen3Helper.Install(switchn3Node, switchn3Devices);
+
+    InternetStackHelper internet;
+    // By default, InternetStackHelper adds a static and global routing
+    // implementation.  We just want the global for this test.
+    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    internet.SetRoutingHelper(ipv4RoutingHelper);
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+
+    internet.Install(m_nodes.Get(0));
+    internet.Install(m_nodes.Get(1));
+    // m_nodes.Get (2) is bridge node
+    // m_nodes.Get (3) is bridge node
+    internet.Install(m_nodes.Get(4));
+
+    Ipv4AddressHelper addressv4;
+    addressv4.SetBase("10.1.1.0", "255.255.255.0");
+    addressv4.Assign(net);
+
+    addressv4.SetBase("10.1.2.0", "255.255.255.0");
+    addressv4.Assign(bridgeFacingDevices);
+
+    Ipv6AddressHelper addressv6;
+    addressv6.SetBase(Ipv6Address("2001:0::"), Ipv6Prefix(64));
+    addressv6.Assign(net);
+
+    addressv6.SetBase(Ipv6Address("2001:1::"), Ipv6Prefix(64));
+    addressv6.Assign(bridgeFacingDevices);
+}
+
+void
+TwoBridgeTest::DoRun()
+{
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+    //------------Ipv4 Tests-------------------
+    Ptr<Ipv4L3Protocol> ipv40 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv40, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv40 = ipv40->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv40, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv4GlobalRouting> globalRoutingv40 = routingv40->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv40, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Skip to n4
+    Ptr<Ipv4L3Protocol> ipv44 = m_nodes.Get(4)->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv44, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv4RoutingProtocol> routingv44 = ipv44->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv44, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv4GlobalRouting> globalRoutingv44 = routingv44->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv44, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    Ipv4RoutingTableEntry* routev4 = nullptr;
+    // n0
+    // Test that the right number of routes found
+    uint32_t nRoutesv40 = globalRoutingv40->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes0 " << nRoutesv40);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv40, 2, "Error-- not two entries");
+    for (uint32_t i = 0; i < globalRoutingv40->GetNRoutes(); i++)
+    {
+        routev4 = globalRoutingv40->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+    // Spot check the last route
+    if (routev4)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(),
+                              Ipv4Address("10.1.2.0"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(),
+                              Ipv4Address("10.1.1.2"),
+                              "Error-- wrong gateway");
+    }
+    // skip n2 and n3, just verify n4
+    NS_LOG_DEBUG("BridgeTest skip print out of n1-n3, go next to node n4");
+
+    // n4
+    // Test that the right number of routes found
+    routev4 = nullptr;
+    uint32_t nRoutesv44 = globalRoutingv44->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes4 " << nRoutesv44);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv44, 2, "Error-- not two entries");
+    for (uint32_t i = 0; i < globalRoutingv44->GetNRoutes(); i++)
+    {
+        routev4 = globalRoutingv44->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev4->GetDest() << " gw " << routev4->GetGateway());
+    }
+    // Spot check the last route
+    if (routev4)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetDest(),
+                              Ipv4Address("10.1.1.0"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev4->GetGateway(),
+                              Ipv4Address("10.1.2.1"),
+                              "Error-- wrong gateway");
+    }
+
+    //----------Ipv6 Tests--------------
+    Ptr<Ipv6L3Protocol> ipv60 = m_nodes.Get(0)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv60, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6RoutingProtocol> routingv60 = ipv60->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv60, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv6GlobalRouting> globalRoutingv60 = routingv60->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv60, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    // Skip to n4
+    Ptr<Ipv6L3Protocol> ipv64 = m_nodes.Get(4)->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ipv64, nullptr, "Error-- no Ipv4 object");
+    Ptr<Ipv6RoutingProtocol> routingv64 = ipv64->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routingv64, nullptr, "Error-- no Ipv4 routing protocol object");
+    Ptr<Ipv6GlobalRouting> globalRoutingv64 = routingv64->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRoutingv64, nullptr, "Error-- no Ipv4GlobalRouting object");
+
+    Ipv6RoutingTableEntry* routev6 = nullptr;
+    // n0
+    // Test that the right number of routes found
+    uint32_t nRoutesv60 = globalRoutingv60->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes0 " << nRoutesv60);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv60, 2, "Error-- not two entries");
+    for (uint32_t i = 0; i < globalRoutingv60->GetNRoutes(); i++)
+    {
+        routev6 = globalRoutingv60->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    }
+    // Spot check the last route
+    if (routev6)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                              Ipv6Address("2001:1::"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                              Ipv6Prefix(64),
+                              "Error-- wrong destination mask");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                              Ipv6Address("fe80::200:ff:fe00:2"),
+                              "Error-- wrong gateway");
+    }
+    // skip n2 and n3, just verify n4
+    NS_LOG_DEBUG("BridgeTest skip print out of n1-n3, go next to node n4");
+
+    // n4
+    // Test that the right number of routes found
+    routev6 = nullptr;
+    uint32_t nRoutesv64 = globalRoutingv64->GetNRoutes();
+    NS_LOG_DEBUG("BridgeTest nRoutes4 " << nRoutesv44);
+    NS_TEST_ASSERT_MSG_EQ(nRoutesv64, 2, "Error-- not two entries");
+    for (uint32_t i = 0; i < globalRoutingv64->GetNRoutes(); i++)
+    {
+        routev6 = globalRoutingv64->GetRoute(i);
+        NS_LOG_DEBUG("entry dest " << routev6->GetDest() << " gw " << routev6->GetGateway());
+    }
+    // Spot check the last route
+    if (routev6)
+    {
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDest(),
+                              Ipv6Address("2001::"),
+                              "Error-- wrong destination");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetDestNetworkPrefix(),
+                              Ipv6Prefix(64),
+                              "Error-- wrong destination mask");
+        NS_TEST_ASSERT_MSG_EQ(routev6->GetGateway(),
+                              Ipv6Address("fe80::200:ff:fe00:3"),
+                              "Error-- wrong gateway");
+    }
+
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief IPv4 Dynamic GlobalRouting test
+ */
+class Ipv4DynamicGlobalRoutingTestCase : public TestCase
+{
+  public:
+    Ipv4DynamicGlobalRoutingTestCase();
+    ~Ipv4DynamicGlobalRoutingTestCase() override;
+
+  private:
+    /**
+     * @brief Send some data
+     * @param index Index of the socket to use.
+     */
+    void SendData(uint8_t index);
+
+    /**
+     * @brief Shutdown a socket
+     * @param index Index of the socket to close.
+     */
+    void ShutDownSock(uint8_t index);
+
+    /**
+     * Handle an incoming packet
+     * @param socket The input socket.
+     */
+    void HandleRead(Ptr<Socket> socket);
+    void DoRun() override;
+
+    uint16_t m_count;                                      //!< Number of packets received.
+    std::vector<std::pair<Ptr<Socket>, bool>> m_sendSocks; //!< Sending sockets.
+    DataRate m_dataRate;                                   //!< Data rate.
+    uint16_t m_packetSize;                                 //!< Packet size.
+    std::vector<uint8_t>
+        m_firstInterface; //!< Packets received on the 1st interface at a given time.
+    std::vector<uint8_t>
+        m_secondInterface; //!< Packets received on the 2nd interface at a given time.
+    NodeContainer c;       //!< nodes in the topology
+};
+
+// Add some help text to this case to describe what it is intended to test
+Ipv4DynamicGlobalRoutingTestCase::Ipv4DynamicGlobalRoutingTestCase()
+    : TestCase("Ipv4 Dynamic global routing example"),
+      m_count(0)
+{
+    m_firstInterface.resize(16, 0);
+    m_secondInterface.resize(16, 0);
+    m_dataRate = DataRate("2kbps");
+    m_packetSize = 50;
+}
+
+Ipv4DynamicGlobalRoutingTestCase::~Ipv4DynamicGlobalRoutingTestCase()
+{
+    for (auto iter = m_sendSocks.begin(); iter != m_sendSocks.end(); iter++)
+    {
+        if (iter->second)
+        {
+            iter->second = false;
+            iter->first->Close();
+            iter->first = nullptr;
+        }
+    }
+}
+
+void
+Ipv4DynamicGlobalRoutingTestCase::HandleRead(Ptr<Socket> socket)
+{
+    Ptr<Packet> packet;
+    Address from;
+    while ((packet = socket->RecvFrom(from)))
+    {
+        if (packet->GetSize() == 0)
+        { // EOF
+            break;
+        }
+        Ipv4PacketInfoTag tag;
+        bool found;
+        found = packet->PeekPacketTag(tag);
+        uint8_t now = static_cast<uint8_t>(Simulator::Now().GetSeconds());
+        if (found)
+        {
+            if (tag.GetRecvIf() == 1)
+            {
+                m_firstInterface[now]++;
+            }
+            if (tag.GetRecvIf() == 2)
+            {
+                m_secondInterface[now]++;
+            }
+            m_count++;
+        }
+    }
+}
+
+void
+Ipv4DynamicGlobalRoutingTestCase::SendData(uint8_t index)
+{
+    if (!m_sendSocks[index].second)
+    {
+        return;
+    }
+    Ptr<Packet> packet = Create<Packet>(m_packetSize);
+    m_sendSocks[index].first->Send(packet);
+
+    Time tNext(MicroSeconds(m_packetSize * 8 * 1e6 / m_dataRate.GetBitRate()));
+    Simulator::Schedule(tNext, &Ipv4DynamicGlobalRoutingTestCase::SendData, this, index);
+}
+
+void
+Ipv4DynamicGlobalRoutingTestCase::ShutDownSock(uint8_t index)
+{
+    m_sendSocks[index].second = false;
+    m_sendSocks[index].first->Close();
+    m_sendSocks[index].first = nullptr;
+}
+
+// Test derived from examples/routing/dynamic-global-routing.cc
+//
+// Network topology
+//
+//  n0
+//     \ p-p
+//      \          (shared csma/cd)
+//       n2 -------------------------n3
+//      /            |        |
+//     / p-p        n4        n5 ---------- n6
+//   n1                             p-p
+//   |                                      |
+//   ----------------------------------------
+//                p-p
+//
+// Test that for node n6, the interface facing n5 receives packets at
+// times (1-2), (4-6), (8-10), (11-12), (14-16) and the interface
+// facing n1 receives packets at times (2-4), (6-8), (12-13)
+//
+void
+Ipv4DynamicGlobalRoutingTestCase::DoRun()
+{
+    // The below value configures the default behavior of global routing.
+    // By default, it is disabled.  To respond to interface events, set to true
+    Config::SetDefault("ns3::Ipv4GlobalRouting::RespondToInterfaceEvents", BooleanValue(true));
+
+    c.Create(7);
+    NodeContainer n0n2 = NodeContainer(c.Get(0), c.Get(2));
+    NodeContainer n1n2 = NodeContainer(c.Get(1), c.Get(2));
+    NodeContainer n5n6 = NodeContainer(c.Get(5), c.Get(6));
+    NodeContainer n1n6 = NodeContainer(c.Get(1), c.Get(6));
+    NodeContainer n2345 = NodeContainer(c.Get(2), c.Get(3), c.Get(4), c.Get(5));
+
+    InternetStackHelper internet;
+    Ipv4GlobalRoutingHelper routingHelper;
+    internet.SetRoutingHelper(routingHelper);
+    internet.Install(c);
+
+    // We create the channels first without any IP addressing information
+    SimpleNetDeviceHelper devHelper;
+
+    devHelper.SetNetDevicePointToPointMode(true);
+    NetDeviceContainer d0d2 = devHelper.Install(n0n2);
+    NetDeviceContainer d1d6 = devHelper.Install(n1n6);
+    NetDeviceContainer d1d2 = devHelper.Install(n1n2);
+    NetDeviceContainer d5d6 = devHelper.Install(n5n6);
+
+    devHelper.SetNetDevicePointToPointMode(false);
+    NetDeviceContainer d2345 = devHelper.Install(n2345);
+
+    // Later, we add IP addresses.
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+    ipv4.Assign(d0d2);
+
+    ipv4.SetBase("10.1.2.0", "255.255.255.0");
+    ipv4.Assign(d1d2);
+
+    ipv4.SetBase("10.1.3.0", "255.255.255.0");
+    Ipv4InterfaceContainer i5i6 = ipv4.Assign(d5d6);
+
+    ipv4.SetBase("10.250.1.0", "255.255.255.0");
+    ipv4.Assign(d2345);
+
+    ipv4.SetBase("172.16.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer i1i6 = ipv4.Assign(d1d6);
+
+    // Create router nodes, initialize routing database and set up the routing
+    // tables in the nodes.
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+    // Create the applications to send UDP datagrams of size
+    // 50 bytes at a rate of 2 Kb/s
+    TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
+    uint16_t port = 9; // Discard port (RFC 863)
+
+    std::pair<Ptr<Socket>, bool> sendSockA;
+    sendSockA.first = Socket::CreateSocket(c.Get(1), tid);
+    sendSockA.first->Bind();
+    sendSockA.first->Connect(InetSocketAddress(i5i6.GetAddress(1), port));
+    sendSockA.second = true;
+    m_sendSocks.push_back(sendSockA);
+    Simulator::Schedule(Seconds(1), &Ipv4DynamicGlobalRoutingTestCase::SendData, this, 0);
+    Simulator::Schedule(Seconds(10), &Ipv4DynamicGlobalRoutingTestCase::ShutDownSock, this, 0);
+
+    std::pair<Ptr<Socket>, bool> sendSockB;
+    sendSockB.first = Socket::CreateSocket(c.Get(1), tid);
+    sendSockB.first->Bind();
+    sendSockB.first->Connect(InetSocketAddress(i1i6.GetAddress(1), port));
+    sendSockB.second = true;
+    m_sendSocks.push_back(sendSockB);
+    Simulator::Schedule(Seconds(11), &Ipv4DynamicGlobalRoutingTestCase::SendData, this, 1);
+    Simulator::Schedule(Seconds(16), &Ipv4DynamicGlobalRoutingTestCase::ShutDownSock, this, 1);
+
+    // Create an optional packet sink to receive these packets
+    Ptr<Socket> sink2 = Socket::CreateSocket(c.Get(6), tid);
+    sink2->Bind(Address(InetSocketAddress(Ipv4Address::GetAny(), port)));
+    sink2->Listen();
+    sink2->ShutdownSend();
+
+    sink2->SetRecvPktInfo(true);
+    sink2->SetRecvCallback(MakeCallback(&Ipv4DynamicGlobalRoutingTestCase::HandleRead, this));
+
+    Ptr<Node> n1 = c.Get(1);
+    Ptr<Ipv4> ipv41 = n1->GetObject<Ipv4>();
+    // The first ifIndex is 0 for loopback, then the first p2p is numbered 1,
+    // then the next p2p is numbered 2
+    uint32_t ipv4ifIndex1 = 2;
+
+    Simulator::Schedule(Seconds(2), &Ipv4::SetDown, ipv41, ipv4ifIndex1);
+    Simulator::Schedule(Seconds(4), &Ipv4::SetUp, ipv41, ipv4ifIndex1);
+
+    Ptr<Node> n6 = c.Get(6);
+    Ptr<Ipv4> ipv46 = n6->GetObject<Ipv4>();
+    // The first ifIndex is 0 for loopback, then the first p2p is numbered 1,
+    // then the next p2p is numbered 2
+    uint32_t ipv4ifIndex6 = 2;
+    Simulator::Schedule(Seconds(6), &Ipv4::SetDown, ipv46, ipv4ifIndex6);
+    Simulator::Schedule(Seconds(8), &Ipv4::SetUp, ipv46, ipv4ifIndex6);
+
+    Simulator::Schedule(Seconds(12), &Ipv4::SetDown, ipv41, ipv4ifIndex1);
+    Simulator::Schedule(Seconds(14), &Ipv4::SetUp, ipv41, ipv4ifIndex1);
+
+    Simulator::Run();
+
+    NS_TEST_ASSERT_MSG_EQ(m_count, 70, "Dynamic global routing did not deliver all packets");
+    // Test that for node n6, the interface facing n1 receives packets at
+    // times (1-2), (4-6), (8-10), (11-12), (14-16) and the interface
+    // facing n5 receives packets at times (2-4), (6-8), (12-13)
+
+    // Note: there are two sending sockets, both on n1.
+    // The first socket sends packets at time (1-10), the second at time (11, 16).
+    // The first socket sends packets to n6, targeting the address facing n5.
+    // The second socket sends packets to n6, targeting the address facing n1.
+    // This actually doesn't matter, as n6 will accept packets sent to the "wrong" address.
+    //
+    // The shortest path netween n1 and n6 is the direct one, but the topology changes during the
+    // simulation:
+    // - (2-4): removal from n1 of the interface toward n6
+    // - (6-8): removal from n6 of the interface toward n1
+    // - (12-14): removal from n1 of the interface toward n6
+    // When the link is broken, packets are rerouted though the longest (and only) path, reaching
+    // n6 though n5.
+
+    std::vector<uint8_t> firstInterfaceTest{0, 5, 0, 0, 5, 5, 0, 0, 5, 5, 0, 5, 0, 0, 5, 5};
+    std::vector<uint8_t> secondInterfaceTest{0, 0, 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 5, 5, 0, 0};
+
+    for (uint32_t index = 0; index < firstInterfaceTest.size(); index++)
+    {
+        NS_TEST_ASSERT_MSG_EQ(firstInterfaceTest[index],
+                              m_firstInterface[index],
+                              "Dynamic global routing did deliver the wrong number of packets "
+                              "to the first interface at time "
+                                  << index);
+    }
+
+    for (uint32_t index = 0; index < secondInterfaceTest.size(); index++)
+    {
+        NS_TEST_ASSERT_MSG_EQ(secondInterfaceTest[index],
+                              m_secondInterface[index],
+                              "Dynamic global routing did deliver the wrong number of packets "
+                              "to the second interface at time "
+                                  << index);
+    }
+
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief IPv6 Dynamic GlobalRouting test
+ */
+class Ipv6DynamicGlobalRoutingTestCase : public TestCase
+{
+  public:
+    Ipv6DynamicGlobalRoutingTestCase();
+    ~Ipv6DynamicGlobalRoutingTestCase() override;
+
+  private:
+    /**
+     * @brief Send some data
+     * @param index Index of the socket to use.
+     */
+    void SendData(uint8_t index);
+
+    /**
+     * @brief Shutdown a socket
+     * @param index Index of the socket to close.
+     */
+    void ShutDownSock(uint8_t index);
+
+    /**
+     * Handle an incoming packet
+     * @param socket The input socket.
+     */
+    void HandleRead(Ptr<Socket> socket);
+    /**
+     * @brief This function does two things, set the specified ipv6 interface as up and
+     * Adds the specified Ipv6Address to the interface.
+     * @param ipv6 The Ipv6 object
+     * @param ifIndex The index of the interface
+     * @param address The Ipv6Address to be added to the interface
+     */
+    void SetInterfaceUp(Ptr<Ipv6> ipv6, uint32_t ifIndex, Ipv6InterfaceAddress address);
+
+    void DoRun() override;
+
+    uint16_t m_count;                                      //!< Number of packets received.
+    std::vector<std::pair<Ptr<Socket>, bool>> m_sendSocks; //!< Sending sockets.
+    DataRate m_dataRate;                                   //!< Data rate.
+    uint16_t m_packetSize;                                 //!< Packet size.
+    std::vector<uint8_t>
+        m_firstInterface; //!< Packets received on the 1st interface at a given time.
+    std::vector<uint8_t>
+        m_secondInterface; //!< Packets received on the 2nd interface at a given time.
+    NodeContainer c;       //!< nodes in the topology
+};
+
+// Add some help text to this case to describe what it is intended to test
+Ipv6DynamicGlobalRoutingTestCase::Ipv6DynamicGlobalRoutingTestCase()
+    : TestCase("Ipv6 Dynamic global routing example"),
+      m_count(0)
+{
+    m_firstInterface.resize(16, 0);
+    m_secondInterface.resize(16, 0);
+    m_dataRate = DataRate("2kbps");
+    m_packetSize = 50;
+}
+
+Ipv6DynamicGlobalRoutingTestCase::~Ipv6DynamicGlobalRoutingTestCase()
+{
+    for (auto iter = m_sendSocks.begin(); iter != m_sendSocks.end(); iter++)
+    {
+        if (iter->second)
+        {
+            iter->second = false;
+            iter->first->Close();
+            iter->first = nullptr;
+        }
+    }
+}
+
+void
+Ipv6DynamicGlobalRoutingTestCase::HandleRead(Ptr<Socket> socket)
+{
+    Ptr<Packet> packet;
+    Address from;
+
+    while ((packet = socket->RecvFrom(from)))
+    {
+        if (packet->GetSize() == 0)
+        { // EOF
+            break;
+        }
+        Ipv6PacketInfoTag tag;
+        bool found;
+        found = packet->PeekPacketTag(tag);
+        uint8_t now = static_cast<uint8_t>(Simulator::Now().GetSeconds());
+        if (found)
+        {
+            if (tag.GetRecvIf() == 1)
+            {
+                m_firstInterface[now]++;
+            }
+            if (tag.GetRecvIf() == 2)
+            {
+                m_secondInterface[now]++;
+            }
+            m_count++;
+        }
+    }
+}
+
+void
+Ipv6DynamicGlobalRoutingTestCase::SendData(uint8_t index)
+{
+    if (!m_sendSocks[index].second)
+    {
+        return;
+    }
+    Ptr<Packet> packet = Create<Packet>(m_packetSize);
+    m_sendSocks[index].first->Send(packet);
+
+    Time tNext(MicroSeconds(m_packetSize * 8 * 1e6 / m_dataRate.GetBitRate()));
+    Simulator::Schedule(tNext, &Ipv6DynamicGlobalRoutingTestCase::SendData, this, index);
+}
+
+void
+Ipv6DynamicGlobalRoutingTestCase::ShutDownSock(uint8_t index)
+{
+    m_sendSocks[index].second = false;
+    m_sendSocks[index].first->Close();
+    m_sendSocks[index].first = nullptr;
+}
+
+void
+Ipv6DynamicGlobalRoutingTestCase::SetInterfaceUp(Ptr<Ipv6> ipv6,
+                                                 uint32_t ifIndex,
+                                                 Ipv6InterfaceAddress address)
+{
+    ipv6->SetUp(ifIndex);
+    ipv6->AddAddress(ifIndex, address);
+}
+
+// Test derived from examples/routing/dynamic-global-routing.cc
+//
+// Network topology
+//
+//  n0
+//     \ p-p
+//      \          (shared csma/cd)
+//       n2 -------------------------n3
+//      /            |        |
+//     / p-p        n4        n5 ---------- n6
+//   n1                             p-p
+//   |                                      |
+//   ----------------------------------------
+//                p-p
+//
+// Test that for node n6, the interface facing n5 receives packets at
+// times (1-2), (4-6), (8-10), (11-12), (14-16) and the interface
+// facing n1 receives packets at times (2-4), (6-8), (12-13)
+//
+void
+Ipv6DynamicGlobalRoutingTestCase::DoRun()
+{
+    // The below value configures the default behavior of global routing.
+    // By default, it is disabled.  To respond to interface events, set to true
+    Config::SetDefault("ns3::Ipv6GlobalRouting::RespondToInterfaceEvents", BooleanValue(true));
+
+    c.Create(7);
+    NodeContainer n0n2 = NodeContainer(c.Get(0), c.Get(2));
+    NodeContainer n1n2 = NodeContainer(c.Get(1), c.Get(2));
+    NodeContainer n5n6 = NodeContainer(c.Get(5), c.Get(6));
+    NodeContainer n1n6 = NodeContainer(c.Get(1), c.Get(6));
+    NodeContainer n2345 = NodeContainer(c.Get(2), c.Get(3), c.Get(4), c.Get(5));
+
+    Ipv6GlobalRoutingHelper ipv6RoutingHelper;
+    InternetStackHelper internet;
+    internet.SetRoutingHelper(ipv6RoutingHelper);
+
+    internet.Install(c);
+
+    // We create the channels first without any IP addressing information
+    SimpleNetDeviceHelper devHelper;
+
+    devHelper.SetNetDevicePointToPointMode(true);
+    NetDeviceContainer d0d2 = devHelper.Install(n0n2);
+    NetDeviceContainer d1d6 = devHelper.Install(n1n6);
+    NetDeviceContainer d1d2 = devHelper.Install(n1n2);
+    NetDeviceContainer d5d6 = devHelper.Install(n5n6);
+
+    devHelper.SetNetDevicePointToPointMode(false);
+    NetDeviceContainer d2345 = devHelper.Install(n2345);
+
+    // Later, we add IP addresses.
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase("2001:1::", Ipv6Prefix(64));
+    ipv6.Assign(d0d2);
+    ipv6.SetBase("2001:2::", Ipv6Prefix(64));
+    ipv6.Assign(d1d2);
+    ipv6.SetBase("2001:3::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i5i6 = ipv6.Assign(d5d6);
+    ipv6.SetBase("2001:4::", Ipv6Prefix(64));
+    ipv6.Assign(d2345);
+    ipv6.SetBase("2001:5::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i1i6 = ipv6.Assign(d1d6);
+
+    // Create router nodes, initialize routing database and set up the routing
+    // tables in the nodes.
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    NeighborCacheHelper neighborCache;
+    neighborCache.SetDynamicNeighborCache(true);
+    neighborCache.PopulateNeighborCache();
+
+    // Create the applications to send UDP datagrams of size
+    // 50 bytes at a rate of 2 Kb/s
+    TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
+    uint16_t port = 9; // Discard port (RFC 863)
+
+    std::pair<Ptr<Socket>, bool> sendSockA;
+    sendSockA.first = Socket::CreateSocket(c.Get(1), tid);
+    sendSockA.first->Bind();
+    sendSockA.first->Connect(Inet6SocketAddress(i5i6.GetAddress(1, 1), port));
+    sendSockA.second = true;
+    m_sendSocks.push_back(sendSockA);
+    Simulator::Schedule(Seconds(1), &Ipv6DynamicGlobalRoutingTestCase::SendData, this, 0);
+    Simulator::Schedule(Seconds(10), &Ipv6DynamicGlobalRoutingTestCase::ShutDownSock, this, 0);
+
+    std::pair<Ptr<Socket>, bool> sendSockB;
+    sendSockB.first = Socket::CreateSocket(c.Get(1), tid);
+    sendSockB.first->Bind();
+    sendSockB.first->Connect(Inet6SocketAddress(i1i6.GetAddress(1, 1), port));
+    sendSockB.second = true;
+    m_sendSocks.push_back(sendSockB);
+    Simulator::Schedule(Seconds(11), &Ipv6DynamicGlobalRoutingTestCase::SendData, this, 1);
+    Simulator::Schedule(Seconds(16), &Ipv6DynamicGlobalRoutingTestCase::ShutDownSock, this, 1);
+
+    // Create an optional packet sink to receive these packets
+    Ptr<Socket> sink2 = Socket::CreateSocket(c.Get(6), tid);
+    sink2->Bind(Address(Inet6SocketAddress(Ipv6Address::GetAny(), port)));
+    sink2->Listen();
+    sink2->ShutdownSend();
+
+    sink2->SetRecvPktInfo(true);
+    sink2->SetRecvCallback(MakeCallback(&Ipv6DynamicGlobalRoutingTestCase::HandleRead, this));
+
+    Ptr<Node> n1 = c.Get(1);
+    Ptr<Ipv6> ipv41 = n1->GetObject<Ipv6>();
+    // The first ifIndex is 0 for loopback, then the first p2p is numbered 1,
+    // then the next p2p is numbered 2
+    uint32_t ipv4ifIndex1 = 2;
+
+    Simulator::Schedule(Seconds(2), &Ipv6::SetDown, ipv41, ipv4ifIndex1);
+    Simulator::Schedule(Seconds(4),
+                        &Ipv6DynamicGlobalRoutingTestCase::SetInterfaceUp,
+                        this,
+                        ipv41,
+                        ipv4ifIndex1,
+                        Ipv6InterfaceAddress(Ipv6Address("2001:5::200:ff:fe00:3"), Ipv6Prefix(64)));
+
+    Ptr<Node> n6 = c.Get(6);
+    Ptr<Ipv6> ipv46 = n6->GetObject<Ipv6>();
+    // The first ifIndex is 0 for loopback, then the first p2p is numbered 1,
+    // then the next p2p is numbered 2
+    uint32_t ipv4ifIndex6 = 2;
+    Simulator::Schedule(Seconds(6), &Ipv6::SetDown, ipv46, ipv4ifIndex6);
+    Simulator::Schedule(Seconds(8),
+                        &Ipv6DynamicGlobalRoutingTestCase::SetInterfaceUp,
+                        this,
+                        ipv46,
+                        ipv4ifIndex6,
+                        Ipv6InterfaceAddress(Ipv6Address("2001:5::200:ff:fe00:4"), Ipv6Prefix(64)));
+
+    Simulator::Schedule(Seconds(12), &Ipv6::SetDown, ipv41, ipv4ifIndex1);
+    Simulator::Schedule(Seconds(14),
+                        &Ipv6DynamicGlobalRoutingTestCase::SetInterfaceUp,
+                        this,
+                        ipv41,
+                        ipv4ifIndex1,
+                        Ipv6InterfaceAddress(Ipv6Address("2001:5::200:ff:fe00:3"), Ipv6Prefix(64)));
+
+    Simulator::Run();
+
+    NS_TEST_ASSERT_MSG_EQ(m_count, 70, "Dynamic global routing did not deliver all packets");
+    // Test that for node n6, the interface facing n1 receives packets at
+    // times (1-2), (4-6), (8-10), (11-12), (14-16) and the interface
+    // facing n5 receives packets at times (2-4), (6-8), (12-13)
+
+    // Note: there are two sending sockets, both on n1.
+    // The first socket sends packets at time (1-10), the second at time (11, 16).
+    // The first socket sends packets to n6, targeting the address facing n5.
+    // The second socket sends packets to n6, targeting the address facing n1.
+    // This actually doesn't matter, as n6 will accept packets sent to the "wrong" address.
+    //
+    // The shortest path netween n1 and n6 is the direct one, but the topology changes during the
+    // simulation:
+    // - (2-4): removal from n1 of the interface toward n6
+    // - (6-8): removal from n6 of the interface toward n1
+    // - (12-14): removal from n1 of the interface toward n6
+    // When the link is broken, packets are rerouted though the longest (and only) path, reaching
+    // n6 though n5.
+
+    std::vector<uint8_t> firstInterfaceTest{0, 5, 0, 0, 5, 5, 0, 0, 5, 5, 0, 5, 0, 0, 5, 5};
+    std::vector<uint8_t> secondInterfaceTest{0, 0, 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 5, 5, 0, 0};
+
+    for (uint32_t index = 0; index < firstInterfaceTest.size(); index++)
+    {
+        NS_TEST_ASSERT_MSG_EQ(firstInterfaceTest[index],
+                              m_firstInterface[index],
+                              "Dynamic global routing did deliver the wrong number of packets "
+                              "to the first interface at time "
+                                  << index);
+    }
+
+    for (uint32_t index = 0; index < secondInterfaceTest.size(); index++)
+    {
+        NS_TEST_ASSERT_MSG_EQ(secondInterfaceTest[index],
+                              m_secondInterface[index],
+                              "Dynamic global routing did deliver the wrong number of packets "
+                              "to the second interface at time "
+                                  << index);
+    }
+
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief IPv4 Dynamic GlobalRouting /32 test
+ */
+class Ipv4GlobalRoutingSlash32TestCase : public TestCase
+{
+  public:
+    Ipv4GlobalRoutingSlash32TestCase();
+    ~Ipv4GlobalRoutingSlash32TestCase() override;
+
+    Ptr<Packet> m_receivedPacket; //!< number of received packets
+
+    /**
+     * @brief Receive a packet.
+     * @param socket The receiving socket.
+     */
+    void ReceivePkt(Ptr<Socket> socket);
+    /**
+     * @brief Send a packet.
+     * @param socket The sending socket.
+     * @param to The address of the receiver.
+     */
+    void DoSendData(Ptr<Socket> socket, std::string to);
+    /**
+     * @brief Send a packet.
+     * @param socket The sending socket.
+     * @param to The address of the receiver.
+     */
+    void SendData(Ptr<Socket> socket, std::string to);
+
+  private:
+    void DoRun() override;
+};
+
+// Add some help text to this case to describe what it is intended to test
+Ipv4GlobalRoutingSlash32TestCase::Ipv4GlobalRoutingSlash32TestCase()
+    : TestCase("Slash 32 global routing example")
+{
+}
+
+Ipv4GlobalRoutingSlash32TestCase::~Ipv4GlobalRoutingSlash32TestCase()
+{
+}
+
+void
+Ipv4GlobalRoutingSlash32TestCase::ReceivePkt(Ptr<Socket> socket)
+{
+    uint32_t availableData [[maybe_unused]] = socket->GetRxAvailable();
+    m_receivedPacket = socket->Recv(std::numeric_limits<uint32_t>::max(), 0);
+    NS_TEST_ASSERT_MSG_EQ(availableData,
+                          m_receivedPacket->GetSize(),
+                          "Received packet size is not equal to Rx buffer size");
+}
+
+void
+Ipv4GlobalRoutingSlash32TestCase::DoSendData(Ptr<Socket> socket, std::string to)
+{
+    Address realTo = InetSocketAddress(Ipv4Address(to.c_str()), 1234);
+    NS_TEST_EXPECT_MSG_EQ(socket->SendTo(Create<Packet>(123), 0, realTo), 123, "100");
+}
+
+void
+Ipv4GlobalRoutingSlash32TestCase::SendData(Ptr<Socket> socket, std::string to)
+{
+    m_receivedPacket = Create<Packet>();
+    Simulator::ScheduleWithContext(socket->GetNode()->GetId(),
+                                   Seconds(60),
+                                   &Ipv4GlobalRoutingSlash32TestCase::DoSendData,
+                                   this,
+                                   socket,
+                                   to);
+    Simulator::Stop(Seconds(66));
+    Simulator::Run();
+}
+
+// Test program for this 3-router scenario, using global routing
+//
+// (a.a.a.a/32)A<--x.x.x.0/30-->B<--y.y.y.0/30-->C(c.c.c.c/32)
+//
+void
+Ipv4GlobalRoutingSlash32TestCase::DoRun()
+{
+    Ptr<Node> nA = CreateObject<Node>();
+    Ptr<Node> nB = CreateObject<Node>();
+    Ptr<Node> nC = CreateObject<Node>();
+
+    NodeContainer c = NodeContainer(nA, nB, nC);
+
+    InternetStackHelper internet;
+    internet.Install(c);
+
+    // simple links
+    NodeContainer nAnB = NodeContainer(nA, nB);
+    NodeContainer nBnC = NodeContainer(nB, nC);
+
+    SimpleNetDeviceHelper devHelper;
+
+    Ptr<SimpleNetDevice> deviceA = CreateObject<SimpleNetDevice>();
+    deviceA->SetAddress(Mac48Address::Allocate());
+    nA->AddDevice(deviceA);
+
+    NetDeviceContainer dAdB = devHelper.Install(nAnB);
+    NetDeviceContainer dBdC = devHelper.Install(nBnC);
+
+    Ptr<SimpleNetDevice> deviceC = CreateObject<SimpleNetDevice>();
+    deviceC->SetAddress(Mac48Address::Allocate());
+    nC->AddDevice(deviceC);
+
+    // Later, we add IP addresses.
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.252");
+    Ipv4InterfaceContainer iAiB = ipv4.Assign(dAdB);
+
+    ipv4.SetBase("10.1.1.4", "255.255.255.252");
+    Ipv4InterfaceContainer iBiC = ipv4.Assign(dBdC);
+
+    Ptr<Ipv4> ipv4A = nA->GetObject<Ipv4>();
+    Ptr<Ipv4> ipv4B = nB->GetObject<Ipv4>();
+    Ptr<Ipv4> ipv4C = nC->GetObject<Ipv4>();
+
+    int32_t ifIndexA = ipv4A->AddInterface(deviceA);
+    int32_t ifIndexC = ipv4C->AddInterface(deviceC);
+
+    Ipv4InterfaceAddress ifInAddrA =
+        Ipv4InterfaceAddress(Ipv4Address("172.16.1.1"), Ipv4Mask("/32"));
+    ipv4A->AddAddress(ifIndexA, ifInAddrA);
+    ipv4A->SetMetric(ifIndexA, 1);
+    ipv4A->SetUp(ifIndexA);
+
+    Ipv4InterfaceAddress ifInAddrC =
+        Ipv4InterfaceAddress(Ipv4Address("192.168.1.1"), Ipv4Mask("/32"));
+    ipv4C->AddAddress(ifIndexC, ifInAddrC);
+    ipv4C->SetMetric(ifIndexC, 1);
+    ipv4C->SetUp(ifIndexC);
+
+    // Create router nodes, initialize routing database and set up the routing
+    // tables in the nodes.
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+    // Create the UDP sockets
+    Ptr<SocketFactory> rxSocketFactory = nC->GetObject<UdpSocketFactory>();
+    Ptr<Socket> rxSocket = rxSocketFactory->CreateSocket();
+    NS_TEST_EXPECT_MSG_EQ(rxSocket->Bind(InetSocketAddress(Ipv4Address("192.168.1.1"), 1234)),
+                          0,
+                          "trivial");
+    rxSocket->SetRecvCallback(MakeCallback(&Ipv4GlobalRoutingSlash32TestCase::ReceivePkt, this));
+
+    Ptr<SocketFactory> txSocketFactory = nA->GetObject<UdpSocketFactory>();
+    Ptr<Socket> txSocket = txSocketFactory->CreateSocket();
+    txSocket->SetAllowBroadcast(true);
+
+    // ------ Now the tests ------------
+
+    // Unicast test
+    SendData(txSocket, "192.168.1.1");
+    NS_TEST_EXPECT_MSG_EQ(m_receivedPacket->GetSize(),
+                          123,
+                          "Static routing with /32 did not deliver all packets.");
+
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief This TestCase tests if ECMP Route Calculation works. It does not check the
+ * correctness of the routes.
+ */
+class EcmpRouteCalculationTestCase : public TestCase
+{
+  public:
+    EcmpRouteCalculationTestCase();
+    void DoSetup() override;
+    void DoRun() override;
+
+  private:
+    NodeContainer nodes; //!< Nodes used in the test.
+};
+
+EcmpRouteCalculationTestCase::EcmpRouteCalculationTestCase()
+    : TestCase("ECMP Route Calculation TestCase")
+{
+}
+
+void
+EcmpRouteCalculationTestCase::DoSetup()
+{
+    /*
+        This TestCase checks the resolution of issue #1243 in the issue tracker.
+        The problem was that Global Routing failed to calculate next hops when going from a
+        network vertex to a router vertex when ECMP Routes were involved.
+
+        //         Network Topology
+        //
+        //
+        //          ------n1------
+        //         /              \
+        //        /                \
+        //       n0                n3----n4----n5
+        //        \                /
+        //         \              /
+        //          ------n2------
+        //
+        //    Link n0-n1: 10.1.1.1/30,10.1.1.2/30
+        //    Link n0-n2: 10.1.2.1/30,10.1.2.2/30
+        //    Link n1-n3: 10.1.3.1/30,10.1.3.2/30
+        //    Link n2-n3: 10.1.4.1/30,10.1.4.2/30
+        //    Link n3-n4: 10.1.5.1/24,10.1.5.2/24
+        //    Link n4-n5: 10.1.6.1/24,10.1.6.2/24
+        //
+        //     Note: Link n4-n5 is a LAN LINK. All others are simple P2P links.
+    */
+
+    nodes.Create(6);
+
+    Ipv4GlobalRoutingHelper globalhelperv4;
+    Ipv6GlobalRoutingHelper globalhelperv6;
+
+    InternetStackHelper stack;
+    stack.SetRoutingHelper(globalhelperv4);
+    stack.SetRoutingHelper(globalhelperv6);
+    stack.Install(nodes);
+    SimpleNetDeviceHelper devHelper;
+    devHelper.SetNetDevicePointToPointMode(true);
+
+    Ptr<SimpleChannel> channel1 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d01 = devHelper.Install(nodes.Get(0), channel1);
+    d01.Add(devHelper.Install(nodes.Get(1), channel1));
+
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d23 = devHelper.Install(nodes.Get(2), channel2);
+    d23.Add(devHelper.Install(nodes.Get(3), channel2));
+
+    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d02 = devHelper.Install(nodes.Get(0), channel3);
+    d02.Add(devHelper.Install(nodes.Get(2), channel3));
+
+    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d34 = devHelper.Install(nodes.Get(3), channel4);
+    d34.Add(devHelper.Install(nodes.Get(4), channel4));
+
+    Ptr<SimpleChannel> channel5 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d13 = devHelper.Install(nodes.Get(1), channel5);
+    d13.Add(devHelper.Install(nodes.Get(3), channel5));
+
+    devHelper.SetNetDevicePointToPointMode(false);
+
+    Ptr<SimpleChannel> channel6 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d45 = devHelper.Install(nodes.Get(4), channel6);
+    d45.Add(devHelper.Install(nodes.Get(5), channel6));
+
+    // Assign IP addresses to the devices
+    Ipv4AddressHelper address;
+    address.SetBase("10.1.1.0", "255.255.255.252");
+    Ipv4InterfaceContainer i01 = address.Assign(d01);
+
+    address.SetBase("10.1.2.0", "255.255.255.252");
+    Ipv4InterfaceContainer i02 = address.Assign(d02);
+
+    address.SetBase("10.1.3.0", "255.255.255.252");
+    Ipv4InterfaceContainer i13 = address.Assign(d13);
+
+    address.SetBase("10.1.4.0", "255.255.255.224");
+    Ipv4InterfaceContainer i23 = address.Assign(d23);
+
+    address.SetBase("10.1.5.0", "255.255.255.0");
+    Ipv4InterfaceContainer i34 = address.Assign(d34);
+
+    address.SetBase("10.1.6.0", "255.255.255.0");
+    Ipv4InterfaceContainer i45 = address.Assign(d45);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase("2001:1::", Ipv6Prefix(64));
+    ipv6.Assign(d01);
+    ipv6.SetBase("2001:2::", Ipv6Prefix(64));
+    ipv6.Assign(d02);
+    ipv6.SetBase("2001:3::", Ipv6Prefix(64));
+    ipv6.Assign(d13);
+    ipv6.SetBase("2001:4::", Ipv6Prefix(64));
+    ipv6.Assign(d23);
+    ipv6.SetBase("2001:5::", Ipv6Prefix(64));
+    ipv6.Assign(d34);
+    ipv6.SetBase("2001:6::", Ipv6Prefix(64));
+    ipv6.Assign(d45);
+}
+
+void
+EcmpRouteCalculationTestCase::DoRun()
+{
+    // The purpose of this test is to make sure the code doesn't crash when calculating ECMP routes
+    // with a topology described in issue #1243. It does not look into the correctness of the
+    // routes. We have other tests to make sure the routes are correct.
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    Simulator::Run();
+    Simulator::Stop(Seconds(2));
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief TestCase to check calls to GlobalRoutingProtocol API. This TestCase Checks the output
+ * returned by the GlobalRoutingProtocol API to Ipv4L3 Layer.
+ */
+class GlobalRoutingv4ProtocolTestCase : public TestCase
+{
+  public:
+    GlobalRoutingv4ProtocolTestCase();
+    ~GlobalRoutingv4ProtocolTestCase() override;
+
+  private:
+    void DoSetup() override;
+    void DoRun() override;
+    /**
+     * @brief Checks the Path taken by packets by calling RouteInput() and RouteOutput() APIs of
+     * GlobalRoutingProtocol. This mimics how Ipv4L3Protocol calls these APIs.
+     * @param pathNodes Vector of nodes in the path. This includes the source node,Intermediate
+     * nextHop nodes and the destination node.
+     * @param path Vector of Ipv4Addresses in the path. This includes the source egress IpAddress,
+     * destination ingress IpAddress and intermediate nextHop Node's ingress Ipaddress that we need
+     * to check.It does not include the egress addresses for intermediate nodes.
+     */
+    void CheckPath(std::vector<Ptr<Node>> pathNodes, std::vector<Ipv4Address> path);
+
+    /**
+     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
+     * @param route Route to be used for forwarding
+     * @param packet Packet to be forwarded
+     * @param header IPv4 header of the packet
+     */
+    void MyUnicastCallback(Ptr<Ipv4Route> route,
+                           Ptr<const Packet> packet,
+                           const Ipv4Header& header);
+    /**
+     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
+     * @param packet Packet to be locally delivered
+     * @param header IPv4 header of the packet
+     * @param iif Ingress Interface index
+     */
+    void MyLocalDeliverCallback(Ptr<const Packet> packet, const Ipv4Header& header, uint32_t iif);
+    /**
+     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
+     * @param packet Packet to be locally delivered
+     * @param header IPv4 header of the packet
+     * @param errno_ Socket error number
+     */
+    void MyErrorCallback(Ptr<const Packet> packet,
+                         const Ipv4Header& header,
+                         Socket::SocketErrno errno_);
+
+    NodeContainer nodes;                                 //!< Nodes used in the test.
+    Ptr<Ipv4Route> m_lastRoute;                          //!< Route to be tested.
+    Ipv4RoutingProtocol::UnicastForwardCallback m_ucb;   ///< Unicast forward callback
+    Ipv4RoutingProtocol::MulticastForwardCallback m_mcb; ///< Multicast forward callback
+    Ipv4RoutingProtocol::LocalDeliverCallback m_lcb;     ///< Local delivery callback
+    Ipv4RoutingProtocol::ErrorCallback m_ecb;            ///< Error callback
+};
+
+// Signature: void (Ptr<Ipv4Route> route, Ptr<const Packet> packet, const Ipv4Header &header)
+void
+GlobalRoutingv4ProtocolTestCase::MyUnicastCallback(Ptr<Ipv4Route> route,
+                                                   Ptr<const Packet> packet,
+                                                   const Ipv4Header& header)
+{
+    NS_LOG_DEBUG("Unicast Forward Callback called.");
+    m_lastRoute = route;
+}
+
+// Signature: void (Ptr<const Packet>, const Ipv4Header&, uint32_t)
+void
+GlobalRoutingv4ProtocolTestCase::MyLocalDeliverCallback(Ptr<const Packet> packet,
+                                                        const Ipv4Header& header,
+                                                        uint32_t iif)
+{
+    NS_LOG_DEBUG("Local Deliver Callback called.");
+}
+
+// Signature: void (Ptr<const Packet>, const Ipv4Header&, Socket::SocketErrno)
+void
+GlobalRoutingv4ProtocolTestCase::MyErrorCallback(Ptr<const Packet> packet,
+                                                 const Ipv4Header& header,
+                                                 Socket::SocketErrno errno_)
+{
+    NS_LOG_DEBUG("Error Callback called.");
+    // Fail the test if this callback is called
+    NS_TEST_ASSERT_MSG_EQ(1, 1, "Error Callback called in RouteInput");
+}
+
+GlobalRoutingv4ProtocolTestCase::GlobalRoutingv4ProtocolTestCase()
+    : TestCase("Test API calls to GlobalRoutingProtocol")
+{
+    m_ucb = MakeCallback(&GlobalRoutingv4ProtocolTestCase::MyUnicastCallback, this);
+    m_lcb = MakeCallback(&GlobalRoutingv4ProtocolTestCase::MyLocalDeliverCallback, this);
+    m_ecb = MakeCallback(&GlobalRoutingv4ProtocolTestCase::MyErrorCallback, this);
+    // Multicast callback is not used in unicast test, can bind to null or dummy
+    Ipv4RoutingProtocol::MulticastForwardCallback mcb;
+}
+
+GlobalRoutingv4ProtocolTestCase::~GlobalRoutingv4ProtocolTestCase()
+{
+}
+
+void
+GlobalRoutingv4ProtocolTestCase::CheckPath(std::vector<Ptr<Node>> pathNodes,
+                                           std::vector<Ipv4Address> path)
+{
+    uint32_t pathSize = path.size();
+
+    Ptr<Ipv4L3Protocol> ip = pathNodes[0]->GetObject<Ipv4L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ip, nullptr, "Error-- no Ipv4 object at source node");
+    Ptr<Ipv4RoutingProtocol> routing = ip->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routing,
+                          nullptr,
+                          "Error-- no Ipv4 routing protocol object at source node");
+    Ptr<Ipv4GlobalRouting> globalRouting = routing->GetObject<Ipv4GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRouting,
+                          nullptr,
+                          "Error-- no Ipv4GlobalRouting object at source node");
+
+    Socket::SocketErrno errno_;
+    Ptr<NetDevice> oif(nullptr);
+    Ptr<Packet> packet = Create<Packet>();
+    Ipv4Header ipHeader;
+    ipHeader.SetSource(path[0]);
+    ipHeader.SetDestination(path[pathSize - 1]);
+
+    // for the source node we need to call RouteOutput()
+    m_lastRoute = globalRouting->RouteOutput(packet, ipHeader, oif, errno_);
+    // for the rest of the nodes we need to call RouteInput()
+    for (uint32_t i = 1; i < pathSize; i++)
+    {
+        // for each iteration except the last one check that the UnicastForward Callback is called.
+        // For the last node that is the destination node LocalDelivery Callback will be called.
+        if (i != pathSize - 1)
+        {
+            NS_TEST_ASSERT_MSG_EQ(m_lastRoute->GetGateway(), path[i], "Error-- wrong gateway");
+        }
+
+        ip = pathNodes[i]->GetObject<Ipv4L3Protocol>();
+        NS_TEST_ASSERT_MSG_NE(ip, nullptr, "Error-- no Ipv4 object at node");
+        routing = ip->GetRoutingProtocol();
+        NS_TEST_ASSERT_MSG_NE(routing, nullptr, "Error-- no Ipv4 routing protocol object at node");
+        globalRouting = routing->GetObject<Ipv4GlobalRouting>();
+        NS_TEST_ASSERT_MSG_NE(globalRouting,
+                              nullptr,
+                              "Error-- no Ipv4GlobalRouting object at node");
+
+        Ptr<Ipv4Interface> interf = ip->GetInterface(ip->GetInterfaceForAddress(path[i]));
+        Ptr<NetDevice> idevice = interf->GetDevice();
+
+        // call RouteInput() for the next node in the path
+        globalRouting->RouteInput(packet, ipHeader, idevice, m_ucb, m_mcb, m_lcb, m_ecb);
+    }
+}
+
+void
+GlobalRoutingv4ProtocolTestCase::DoSetup()
+{
+    /**
+     * This test case is designed to test the overall functionality of the GlobalRoutingProtocol.
+     * It tests the calls to the GlobalRoutingProtocol's RouteOutput() and RouteInput() APIs.
+     * It mimics how Ipv4L3Protocol calls these APIs. The topology covers most of the cases
+     * that are expected to be handled by the GlobalRoutingProtocol.
+     */
+
+    //
+    //
+    // Network topology
+    //
+    //  n0
+    //     \ p-p
+    //      \          (shared csma/cd)
+    //       n2 -------------------------n3
+    //      /            |        |
+    //     / p-p        n4        n5 ---------- n6-------n7----Bridge-n8--------n9
+    //   n1                             p-p     |              10.1.4.0/24
+    //   |                                      |
+    //   ----------------------------------------
+    //                p-p
+    //
+    //
+    //
+
+    nodes.Create(10);
+    NodeContainer n2345 = NodeContainer(nodes.Get(2), nodes.Get(3), nodes.Get(4), nodes.Get(5));
+
+    // We create the channels first without any IP addressing information
+    SimpleNetDeviceHelper devHelper;
+
+    devHelper.SetNetDevicePointToPointMode(true);
+
+    Ptr<SimpleChannel> channel1 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d0d2 = devHelper.Install(nodes.Get(0), channel1);
+    d0d2.Add(devHelper.Install(nodes.Get(2), channel1));
+
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d1d6 = devHelper.Install(nodes.Get(1), channel2);
+    d1d6.Add(devHelper.Install(nodes.Get(6), channel2));
+
+    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d1d2 = devHelper.Install(nodes.Get(1), channel3);
+    d1d2.Add(devHelper.Install(nodes.Get(2), channel3));
+
+    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d5d6 = devHelper.Install(nodes.Get(5), channel4);
+    d5d6.Add(devHelper.Install(nodes.Get(6), channel4));
+
+    Ptr<SimpleChannel> channel5 = CreateObject<SimpleChannel>();
+    devHelper.SetNetDevicePointToPointMode(false);
+    NetDeviceContainer d6d7 = devHelper.Install(nodes.Get(6), channel5);
+    d6d7.Add(devHelper.Install(nodes.Get(7), channel5));
+
+    devHelper.SetNetDevicePointToPointMode(false);
+    NetDeviceContainer d2345 = devHelper.Install(n2345);
+
+    // handle the bridge
+    // connect node 7 to node 8(Switch)
+    Ptr<SimpleChannel> channel6 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d78 = devHelper.Install(nodes.Get(7), channel6);
+    d78.Add(devHelper.Install(nodes.Get(8), channel6));
+
+    // connect node 8(switch) to node 9
+    Ptr<SimpleChannel> channel7 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d89 = devHelper.Install(nodes.Get(8), channel7);
+    d89.Add(devHelper.Install(nodes.Get(9), channel7));
+
+    NetDeviceContainer bridgeFacingDevices;
+    NetDeviceContainer switchDevices;
+    bridgeFacingDevices.Add(d78.Get(0));
+    switchDevices.Add(d78.Get(1));
+    bridgeFacingDevices.Add(d89.Get(1));
+    switchDevices.Add(d89.Get(0));
+
+    Ptr<Node> switchNode = nodes.Get(8);
+    BridgeHelper bridge;
+    bridge.Install(switchNode, switchDevices);
+
+    InternetStackHelper internet;
+    Ipv4GlobalRoutingHelper glbrouting;
+    internet.SetRoutingHelper(glbrouting);
+    internet.Install(nodes.Get(0));
+    internet.Install(nodes.Get(1));
+    internet.Install(nodes.Get(2));
+    internet.Install(nodes.Get(3));
+    internet.Install(nodes.Get(4));
+    internet.Install(nodes.Get(5));
+    internet.Install(nodes.Get(6));
+    internet.Install(nodes.Get(7));
+    // node 8 is a bridge node
+    internet.Install(nodes.Get(9));
+
+    // Later, we add IP addresses.
+    Ipv4AddressHelper ipv4;
+    ipv4.SetBase("10.1.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer i0d2 = ipv4.Assign(d0d2);
+
+    ipv4.SetBase("10.1.2.0", "255.255.255.0");
+    Ipv4InterfaceContainer i1d2 = ipv4.Assign(d1d2);
+
+    ipv4.SetBase("10.1.3.0", "255.255.255.0");
+    Ipv4InterfaceContainer i5i6 = ipv4.Assign(d5d6);
+
+    ipv4.SetBase("10.250.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer i2345 = ipv4.Assign(d2345);
+
+    ipv4.SetBase("172.16.1.0", "255.255.255.0");
+    Ipv4InterfaceContainer i1i6 = ipv4.Assign(d1d6);
+
+    ipv4.SetBase("10.1.4.0", "255.255.255.0");
+    Ipv4InterfaceContainer i67 = ipv4.Assign(d6d7);
+
+    ipv4.SetBase("10.1.5.0", "255.255.255.0");
+    Ipv4InterfaceContainer i79 = ipv4.Assign(bridgeFacingDevices);
+}
+
+void
+GlobalRoutingv4ProtocolTestCase::DoRun()
+{
+    // Create router nodes, initialize routing database and set up the routing
+    // tables in the nodes.
+    GlobalRouteManager<Ipv4Manager>::ResetRouterId();
+
+    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
+
+    // tests-------------------------
+    // test 1:check path from n0 to n6
+    // test 2:check path from n1 to n5
+    // test 3:check path from n1 to n9
+    // test 4:check path from n8 to n0
+
+    // Test 1: check path from n0 to n6
+    std::vector<Ipv4Address>
+        pathToCheck1; // includes the source egress destination ingress and intermediate next hops
+                      // addresses that we need to check.It does not include the egress addresses
+                      // for intermediate nodes.
+    pathToCheck1.emplace_back("10.1.1.1");
+    pathToCheck1.emplace_back("10.1.1.2");
+    pathToCheck1.emplace_back("10.1.2.1");
+    pathToCheck1.emplace_back("172.16.1.2");
+
+    std::vector<Ptr<Node>>
+        pathNodes1; // nodes in the path. Includes the source node and the destination node.
+    pathNodes1.push_back(nodes.Get(0));
+    pathNodes1.push_back(nodes.Get(2));
+    pathNodes1.push_back(nodes.Get(1));
+    pathNodes1.push_back(nodes.Get(6));
+
+    CheckPath(pathNodes1, pathToCheck1);
+
+    // Test 2:check path from n1 to n5
+    std::vector<Ipv4Address> pathToCheck2;
+    pathToCheck2.emplace_back("10.1.2.1");
+    pathToCheck2.emplace_back("10.1.2.2");
+    pathToCheck2.emplace_back("10.250.1.4");
+
+    std::vector<Ptr<Node>> pathNodes2;
+    pathNodes2.push_back(nodes.Get(1));
+    pathNodes2.push_back(nodes.Get(2));
+    pathNodes2.push_back(nodes.Get(5));
+
+    CheckPath(pathNodes2, pathToCheck2);
+
+    // Test 3:check path from n1 to n9
+    std::vector<Ipv4Address> pathToCheck3;
+    pathToCheck3.emplace_back("172.16.1.1");
+    pathToCheck3.emplace_back("172.16.1.2");
+    pathToCheck3.emplace_back("10.1.4.2");
+    pathToCheck3.emplace_back("10.1.5.2");
+
+    std::vector<Ptr<Node>> pathNodes3;
+    pathNodes3.push_back(nodes.Get(1));
+    pathNodes3.push_back(nodes.Get(6));
+    pathNodes3.push_back(nodes.Get(7));
+    pathNodes3.push_back(nodes.Get(9));
+
+    CheckPath(pathNodes3, pathToCheck3);
+
+    // Test 4:check path from n9 to n0
+    std::vector<Ipv4Address> pathToCheck4;
+    pathToCheck4.emplace_back("10.1.5.2");
+    pathToCheck4.emplace_back("10.1.5.1");
+    pathToCheck4.emplace_back("10.1.4.1");
+    pathToCheck4.emplace_back("10.1.3.1");
+    pathToCheck4.emplace_back("10.250.1.1");
+    pathToCheck4.emplace_back("10.1.1.1");
+
+    std::vector<Ptr<Node>> pathNodes4;
+    pathNodes4.push_back(nodes.Get(9));
+    pathNodes4.push_back(nodes.Get(7));
+    pathNodes4.push_back(nodes.Get(6));
+    pathNodes4.push_back(nodes.Get(5));
+    pathNodes4.push_back(nodes.Get(2));
+    pathNodes4.push_back(nodes.Get(0));
+
+    CheckPath(pathNodes4, pathToCheck4);
+
+    Simulator::Run();
+    Simulator::Stop(Seconds(10));
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief TestCase to check calls to GlobalRoutingProtocol API. This TestCase Checks the output
+ * returned by the GlobalRoutingProtocol API to Ipv4L3 Layer.
+ */
+class GlobalRoutingv6ProtocolTestCase : public TestCase
+{
+  public:
+    GlobalRoutingv6ProtocolTestCase();
+    ~GlobalRoutingv6ProtocolTestCase() override;
+
+  private:
+    void DoSetup() override;
+    void DoRun() override;
+    /**
+     * @brief Checks the Path taken by packets by calling RouteInput() and RouteOutput() APIs of
+     * GlobalRoutingProtocol. This mimics how Ipv4L3Protocol calls these APIs.
+     * @param pathNodes Vector of nodes in the path. This includes the source node,Intermediate
+     * nextHop nodes and the destination node.
+     * @param path Vector of Ipv4Addresses in the path. This includes the source egress IpAddress,
+     * destination ingress IpAddress and intermediate nextHop Node's ingress Ipaddress that we need
+     * to check.It does not include the egress addresses for intermediate nodes.
+     */
+    void CheckPath(std::vector<Ptr<Node>> pathNodes, std::vector<Ipv6Address> path);
+
+    /**
+     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
+     *
+     * @param idev The ingress netdevice
+     * @param rtentry The route entry
+     * @param p The packet
+     * @param header The IPv6 header of the packet
+     */
+    void MyUnicastCallback(Ptr<const NetDevice> idev,
+                           Ptr<Ipv6Route> rtentry,
+                           Ptr<const Packet> p,
+                           const Ipv6Header& header);
+    /**
+     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
+     * @param packet Packet to be locally delivered
+     * @param header IPv6 header of the packet
+     * @param iif Ingress Interface index
+     */
+    void MyLocalDeliverCallback(Ptr<const Packet> packet, const Ipv6Header& header, uint32_t iif);
+    /**
+     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
+     * @param packet Packet to be locally delivered
+     * @param header IPv6 header of the packet
+     * @param errno_ Socket error number
+     */
+    void MyErrorCallback(Ptr<const Packet> packet,
+                         const Ipv6Header& header,
+                         Socket::SocketErrno errno_);
+
+    NodeContainer nodes;                                 //!< Nodes used in the test.
+    Ptr<Ipv6Route> m_lastRoute;                          //!< Route to be tested.
+    Ipv6RoutingProtocol::UnicastForwardCallback m_ucb;   ///< Unicast forward callback
+    Ipv6RoutingProtocol::MulticastForwardCallback m_mcb; ///< Multicast forward callback
+    Ipv6RoutingProtocol::LocalDeliverCallback m_lcb;     ///< Local delivery callback
+    Ipv6RoutingProtocol::ErrorCallback m_ecb;            ///< Error callback
+};
+
+// Signature: void (Ptr<Ipv6Route> route, Ptr<const Packet> packet, const Ipv4Header &header)
+void
+GlobalRoutingv6ProtocolTestCase::MyUnicastCallback(Ptr<const NetDevice> idev,
+                                                   Ptr<Ipv6Route> rtentry,
+                                                   Ptr<const Packet> p,
+                                                   const Ipv6Header& header)
+{
+    NS_LOG_DEBUG("Unicast Forward Callback called.");
+    m_lastRoute = rtentry;
+}
+
+// Signature: void (Ptr<const Packet>, const Ipv4Header&, uint32_t)
+void
+GlobalRoutingv6ProtocolTestCase::MyLocalDeliverCallback(Ptr<const Packet> packet,
+                                                        const Ipv6Header& header,
+                                                        uint32_t iif)
+{
+    NS_LOG_DEBUG("Local Deliver Callback called.");
+}
+
+// Signature: void (Ptr<const Packet>, const Ipv4Header&, Socket::SocketErrno)
+void
+GlobalRoutingv6ProtocolTestCase::MyErrorCallback(Ptr<const Packet> packet,
+                                                 const Ipv6Header& header,
+                                                 Socket::SocketErrno errno_)
+{
+    NS_LOG_DEBUG("Error Callback called.");
+    // Fail the test if this callback is called
+    NS_TEST_ASSERT_MSG_EQ(1, 1, "Error Callback called in RouteInput");
+}
+
+GlobalRoutingv6ProtocolTestCase::GlobalRoutingv6ProtocolTestCase()
+    : TestCase("Test API calls to GlobalRoutingProtocol")
+{
+    m_ucb = MakeCallback(&GlobalRoutingv6ProtocolTestCase::MyUnicastCallback, this);
+    m_lcb = MakeCallback(&GlobalRoutingv6ProtocolTestCase::MyLocalDeliverCallback, this);
+    m_ecb = MakeCallback(&GlobalRoutingv6ProtocolTestCase::MyErrorCallback, this);
+    // Multicast callback is not used in unicast test, can bind to null or dummy
+    Ipv6RoutingProtocol::MulticastForwardCallback mcb;
+}
+
+GlobalRoutingv6ProtocolTestCase::~GlobalRoutingv6ProtocolTestCase()
+{
+}
+
+void
+GlobalRoutingv6ProtocolTestCase::CheckPath(std::vector<Ptr<Node>> pathNodes,
+                                           std::vector<Ipv6Address> path)
+{
+    uint32_t pathSize = path.size();
+
+    Ptr<Ipv6L3Protocol> ip = pathNodes[0]->GetObject<Ipv6L3Protocol>();
+    NS_TEST_ASSERT_MSG_NE(ip, nullptr, "Error-- no Ipv4 object at source node");
+    Ptr<Ipv6RoutingProtocol> routing = ip->GetRoutingProtocol();
+    NS_TEST_ASSERT_MSG_NE(routing,
+                          nullptr,
+                          "Error-- no Ipv4 routing protocol object at source node");
+    Ptr<Ipv6GlobalRouting> globalRouting = routing->GetObject<Ipv6GlobalRouting>();
+    NS_TEST_ASSERT_MSG_NE(globalRouting,
+                          nullptr,
+                          "Error-- no Ipv4GlobalRouting object at source node");
+
+    Socket::SocketErrno errno_;
+    Ptr<NetDevice> oif(nullptr);
+    Ptr<Packet> packet = Create<Packet>();
+    Ipv6Header ipHeader;
+    ipHeader.SetSource(path[0]);
+
+    ipHeader.SetDestination(path[pathSize - 1]);
+
+    // for the source node we need to call RouteOutput()
+    m_lastRoute = globalRouting->RouteOutput(packet, ipHeader, oif, errno_);
+    // for the rest of the nodes we need to call RouteInput()
+    for (uint32_t i = 1; i < pathSize; i++)
+    {
+        // for each iteration except the last one check that the UnicastForward Callback is called.
+        // For the last node that is the destination node LocalDelivery Callback will be called.
+        if (i != pathSize - 1)
+        {
+            NS_TEST_ASSERT_MSG_EQ(m_lastRoute->GetGateway(), path[i], "Error-- wrong gateway");
+        }
+
+        ip = pathNodes[i]->GetObject<Ipv6L3Protocol>();
+        NS_TEST_ASSERT_MSG_NE(ip, nullptr, "Error-- no Ipv6 object at node");
+        routing = ip->GetRoutingProtocol();
+        NS_TEST_ASSERT_MSG_NE(routing, nullptr, "Error-- no Ipv6 routing protocol object at node");
+        globalRouting = routing->GetObject<Ipv6GlobalRouting>();
+        NS_TEST_ASSERT_MSG_NE(globalRouting,
+                              nullptr,
+                              "Error-- no Ipv4GlobalRouting object at node");
+
+        Ptr<Ipv6Interface> interf = ip->GetInterface(ip->GetInterfaceForAddress(path[i]));
+        Ptr<NetDevice> idevice = interf->GetDevice();
+
+        // call RouteInput() for the next node in the path
+        globalRouting->RouteInput(packet, ipHeader, idevice, m_ucb, m_mcb, m_lcb, m_ecb);
+    }
+}
+
+void
+GlobalRoutingv6ProtocolTestCase::DoSetup()
+{
+    /**
+     * This test case is designed to test the overall functionality of the GlobalRoutingProtocol.
+     * It tests the calls to the GlobalRoutingProtocol's RouteOutput() and RouteInput() APIs.
+     * It mimics how Ipv4L3Protocol calls these APIs. The topology covers most of the cases
+     * that are expected to be handled by the GlobalRoutingProtocol.
+     */
+
+    //
+    //
+    // Network topology
+    //
+    //  n0
+    //     \ p-p
+    //      \          (shared csma/cd)
+    //       n2 -------------------------n3
+    //      /            |        |
+    //     / p-p        n4        n5 ---------- n6-------n7----Bridge-n8--------n9
+    //   n1                             p-p     |              10.1.4.0/24
+    //   |                                      |
+    //   ----------------------------------------
+    //                p-p
+    //
+    //
+    //
+
+    nodes.Create(10);
+    NodeContainer n2345 = NodeContainer(nodes.Get(2), nodes.Get(3), nodes.Get(4), nodes.Get(5));
+
+    // We create the channels first without any IP addressing information
+    SimpleNetDeviceHelper devHelper;
+
+    devHelper.SetNetDevicePointToPointMode(true);
+
+    Ptr<SimpleChannel> channel1 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d0d2 = devHelper.Install(nodes.Get(0), channel1);
+    d0d2.Add(devHelper.Install(nodes.Get(2), channel1));
+
+    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d1d6 = devHelper.Install(nodes.Get(1), channel2);
+    d1d6.Add(devHelper.Install(nodes.Get(6), channel2));
+
+    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d1d2 = devHelper.Install(nodes.Get(1), channel3);
+    d1d2.Add(devHelper.Install(nodes.Get(2), channel3));
+
+    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d5d6 = devHelper.Install(nodes.Get(5), channel4);
+    d5d6.Add(devHelper.Install(nodes.Get(6), channel4));
+
+    Ptr<SimpleChannel> channel5 = CreateObject<SimpleChannel>();
+    devHelper.SetNetDevicePointToPointMode(false);
+    NetDeviceContainer d6d7 = devHelper.Install(nodes.Get(6), channel5);
+    d6d7.Add(devHelper.Install(nodes.Get(7), channel5));
+
+    devHelper.SetNetDevicePointToPointMode(false);
+    NetDeviceContainer d2345 = devHelper.Install(n2345);
+
+    // handle the bridge
+    // connect node 7 to node 8(Switch)
+    Ptr<SimpleChannel> channel6 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d78 = devHelper.Install(nodes.Get(7), channel6);
+    d78.Add(devHelper.Install(nodes.Get(8), channel6));
+
+    // connect node 8(switch) to node 9
+    Ptr<SimpleChannel> channel7 = CreateObject<SimpleChannel>();
+    NetDeviceContainer d89 = devHelper.Install(nodes.Get(8), channel7);
+    d89.Add(devHelper.Install(nodes.Get(9), channel7));
+
+    NetDeviceContainer bridgeFacingDevices;
+    NetDeviceContainer switchDevices;
+    bridgeFacingDevices.Add(d78.Get(0));
+    switchDevices.Add(d78.Get(1));
+    bridgeFacingDevices.Add(d89.Get(1));
+    switchDevices.Add(d89.Get(0));
+
+    Ptr<Node> switchNode = nodes.Get(8);
+    BridgeHelper bridge;
+    bridge.Install(switchNode, switchDevices);
+
+    InternetStackHelper internet;
+    Ipv6GlobalRoutingHelper glbrouting;
+    internet.SetRoutingHelper(glbrouting);
+    internet.Install(nodes.Get(0));
+    internet.Install(nodes.Get(1));
+    internet.Install(nodes.Get(2));
+    internet.Install(nodes.Get(3));
+    internet.Install(nodes.Get(4));
+    internet.Install(nodes.Get(5));
+    internet.Install(nodes.Get(6));
+    internet.Install(nodes.Get(7));
+    // node 8 is a bridge node
+    internet.Install(nodes.Get(9));
+
+    // Later, we add IP addresses.
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase("2001:1::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i0d2 = ipv6.Assign(d0d2);
+
+    ipv6.SetBase("2001:2::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i1d2 = ipv6.Assign(d1d2);
+
+    ipv6.SetBase("2001:3::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i5i6 = ipv6.Assign(d5d6);
+
+    ipv6.SetBase("2001:4::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i2345 = ipv6.Assign(d2345);
+
+    ipv6.SetBase("2001:5::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i1i6 = ipv6.Assign(d1d6);
+
+    ipv6.SetBase("2001:6::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i67 = ipv6.Assign(d6d7);
+
+    ipv6.SetBase("2001:7::", Ipv6Prefix(64));
+    Ipv6InterfaceContainer i79 = ipv6.Assign(bridgeFacingDevices);
+}
+
+void
+GlobalRoutingv6ProtocolTestCase::DoRun()
+{
+    // Create router nodes, initialize routing database and set up the routing
+    // tables in the nodes.
+    GlobalRouteManager<Ipv6Manager>::ResetRouterId();
+
+    Ipv6GlobalRoutingHelper::PopulateRoutingTables();
+
+    // tests-------------------------
+    // test 1:check path from n0 to n6
+    // test 2:check path from n1 to n5
+    // test 3:check path from n1 to n9
+    // test 4:check path from n8 to n0
+
+    // Test 1: check path from n0 to n6
+    std::vector<Ipv6Address>
+        pathToCheck1; // includes the source egress destination ingress and intermediate next hops
+                      // addresses that we need to check.It does not include the egress addresses
+                      // for intermediate nodes.
+    pathToCheck1.emplace_back("2001:1::200:ff:fe00:1");
+    pathToCheck1.emplace_back("fe80::200:ff:fe00:2");
+    pathToCheck1.emplace_back("fe80::200:ff:fe00:5");
+    pathToCheck1.emplace_back("2001:5::200:ff:fe00:4");
+
+    std::vector<Ptr<Node>>
+        pathNodes1; // nodes in the path. Includes the source node and the destination node.
+    pathNodes1.push_back(nodes.Get(0));
+    pathNodes1.push_back(nodes.Get(2));
+    pathNodes1.push_back(nodes.Get(1));
+    pathNodes1.push_back(nodes.Get(6));
+    CheckPath(pathNodes1, pathToCheck1);
+
+    // Test 2:check path from n1 to n5
+    std::vector<Ipv6Address> pathToCheck2;
+    pathToCheck2.emplace_back("2001:2::200:ff:fe00:5");
+    pathToCheck2.emplace_back("fe80::200:ff:fe00:4");
+    pathToCheck2.emplace_back("2001:3::200:ff:fe00:7");
+    std::vector<Ptr<Node>> pathNodes2;
+    pathNodes2.push_back(nodes.Get(1));
+    pathNodes2.push_back(nodes.Get(6));
+    pathNodes2.push_back(nodes.Get(5));
+
+    CheckPath(pathNodes2, pathToCheck2);
+    // Test 3:check path from n1 to n9
+    std::vector<Ipv6Address> pathToCheck3;
+    pathToCheck3.emplace_back("2001:2::200:ff:fe00:6");
+    pathToCheck3.emplace_back("fe80::200:ff:fe00:4");
+    pathToCheck3.emplace_back("fe80::200:ff:fe00:a");
+    pathToCheck3.emplace_back("2001:7::200:ff:fe00:12");
+
+    std::vector<Ptr<Node>> pathNodes3;
+    pathNodes3.push_back(nodes.Get(1));
+    pathNodes3.push_back(nodes.Get(6));
+    pathNodes3.push_back(nodes.Get(7));
+    pathNodes3.push_back(nodes.Get(9));
+
+    CheckPath(pathNodes3, pathToCheck3);
+
+    // Test 4:check path from n9 to n0
+    std::vector<Ipv6Address> pathToCheck4;
+    pathToCheck4.emplace_back("2001:7::200:ff:fe00:12");
+    pathToCheck4.emplace_back("fe80::200:ff:fe00:f");
+    pathToCheck4.emplace_back("fe80::200:ff:fe00:9");
+    pathToCheck4.emplace_back("fe80::200:ff:fe00:3");
+    pathToCheck4.emplace_back("fe80::200:ff:fe00:6");
+    pathToCheck4.emplace_back("2001:1::200:ff:fe00:1");
+
+    std::vector<Ptr<Node>> pathNodes4;
+    pathNodes4.push_back(nodes.Get(9));
+    pathNodes4.push_back(nodes.Get(7));
+    pathNodes4.push_back(nodes.Get(6));
+    pathNodes4.push_back(nodes.Get(1));
+    pathNodes4.push_back(nodes.Get(2));
+    pathNodes4.push_back(nodes.Get(0));
+    CheckPath(pathNodes4, pathToCheck4);
+
+    Simulator::Run();
+    Simulator::Stop(Seconds(10));
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief IPv4 GlobalRouting TestSuite
+ */
+class Ipv4GlobalRoutingTestSuite : public TestSuite
+{
+  public:
+    Ipv4GlobalRoutingTestSuite();
+};
+
+Ipv4GlobalRoutingTestSuite::Ipv4GlobalRoutingTestSuite()
+    : TestSuite("global-routing", Type::UNIT)
+{
+    AddTestCase(new LinkTest, TestCase::Duration::QUICK);
+    AddTestCase(new LanTest, TestCase::Duration::QUICK);
+    AddTestCase(new TwoLinkTest, TestCase::Duration::QUICK);
+    AddTestCase(new TwoLanTest, TestCase::Duration::QUICK);
+    AddTestCase(new BridgeTest, TestCase::Duration::QUICK);
+    AddTestCase(new TwoBridgeTest, TestCase::Duration::QUICK);
+    AddTestCase(new Ipv4DynamicGlobalRoutingTestCase, TestCase::Duration::QUICK);
+    AddTestCase(new Ipv6DynamicGlobalRoutingTestCase, TestCase::Duration::QUICK);
+    AddTestCase(new Ipv4GlobalRoutingSlash32TestCase, TestCase::Duration::QUICK);
+    AddTestCase(new EcmpRouteCalculationTestCase, TestCase::Duration::QUICK);
+    AddTestCase(new GlobalRoutingv4ProtocolTestCase, TestCase::Duration::QUICK);
+    AddTestCase(new GlobalRoutingv6ProtocolTestCase, TestCase::Duration::QUICK);
+}
+
+static Ipv4GlobalRoutingTestSuite
+    g_globalRoutingTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/ipv4-address-helper-test-suite.cc ns-3.47/src/internet/test/ipv4-address-helper-test-suite.cc
--- ns-3.46/src/internet/test/ipv4-address-helper-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/ipv4-address-helper-test-suite.cc	2026-02-16 16:08:49.623230000 -0800
@@ -94,7 +94,6 @@
 void
 AddressAllocatorHelperTestCase::DoRun()
 {
-    Ipv4Address network;
     Ipv4Address address;
     Ipv4AddressHelper h;
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/ipv4-global-routing-test-suite.cc ns-3.47/src/internet/test/ipv4-global-routing-test-suite.cc
--- ns-3.46/src/internet/test/ipv4-global-routing-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/ipv4-global-routing-test-suite.cc	1969-12-31 16:00:00.000000000 -0800
@@ -1,1790 +0,0 @@
-/*
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#include "ns3/boolean.h"
-#include "ns3/bridge-helper.h"
-#include "ns3/config.h"
-#include "ns3/global-route-manager.h"
-#include "ns3/inet-socket-address.h"
-#include "ns3/internet-stack-helper.h"
-#include "ns3/ipv4-address-helper.h"
-#include "ns3/ipv4-global-routing-helper.h"
-#include "ns3/ipv4-global-routing.h"
-#include "ns3/ipv4-interface.h"
-#include "ns3/ipv4-l3-protocol.h"
-#include "ns3/ipv4-packet-info-tag.h"
-#include "ns3/ipv4-routing-protocol.h"
-#include "ns3/ipv4-routing-table-entry.h"
-#include "ns3/ipv4-static-routing-helper.h"
-#include "ns3/log.h"
-#include "ns3/node-container.h"
-#include "ns3/node.h"
-#include "ns3/packet.h"
-#include "ns3/pointer.h"
-#include "ns3/simple-channel.h"
-#include "ns3/simple-net-device-helper.h"
-#include "ns3/simple-net-device.h"
-#include "ns3/simulator.h"
-#include "ns3/socket-factory.h"
-#include "ns3/string.h"
-#include "ns3/test.h"
-#include "ns3/udp-socket-factory.h"
-#include "ns3/uinteger.h"
-
-#include <vector>
-using namespace ns3;
-
-NS_LOG_COMPONENT_DEFINE("Ipv4GlobalRoutingTestSuite");
-
-// This test suite tests the operation of global routing on a few sample
-// networks to ensure that routes are built correctly
-//
-//  Link test:
-//      n0 <--------> n1  (point-to-point link)
-//  10.1.1.1        10.1.1.2
-//      Expected routes:
-//         n0:  route to 0.0.0.0 gw 10.1.1.2
-//         n1:  route to 0.0.0.0 gw 10.1.1.1
-//      Note:  These default routes to 0.0.0.0 are generated by the extension
-//             in the global route manager to install default routes via the
-//             peer node on a point-to-point link, when the node is on a
-//             stub link
-//
-//  LAN test:
-//      n0 <--------> n1  (broadcast link on subnet 10.1.1.0/24)
-//      Expected routes:
-//         n0:  route to 10.1.1.0 gw 0.0.0.0
-//         n1:  route to 10.1.1.0 gw 0.0.0.0
-//  Two link test:
-//      n0 <--------> n1 <--------> n2  (point-to-point links)
-//  10.1.1.1       10.1.1.2/     10.1.2.2
-//                 10.1.2.1
-//      Expected routes:
-//         n0:  route to 0.0.0.0 gw 10.1.1.2
-//         n1:  route to 10.1.1.1 gw 10.1.1.1
-//              route to 10.1.2.2 gw 10.1.2.2
-//              route to 10.1.1.0 gw 10.1.1.1
-//              route to 10.1.2.0 gw 10.1.2.2
-//         n2:  route to 0.0.0.0 gw 10.1.2.1
-//      Note:  These default routes to 0.0.0.0 are generated by the extension
-//             in the global route manager to install default routes via the
-//             peer node on a point-to-point link, when the node is on a
-//             stub link
-//  Two LANs test:
-//      n0 <--------> n1 <--------> n2  (broadcast links)
-//      Expected routes:
-//         n0:  route to 10.1.1.0 gw 0.0.0.0
-//              route to 0.0.0.0 gw 10.1.1.2
-//         n1:  route to 10.1.1.1 gw 10.1.1.1
-//              route to 10.1.2.2 gw 10.1.2.2
-//              route to 10.1.1.0 gw 10.1.1.1
-//              route to 10.1.2.0 gw 10.1.2.2
-//         n2:  route to 0.0.0.0 gw 10.1.2.1
-//  Bridge test:
-//      n0 <--------> n1 <---> Bridge-n2 <---> n3 <-------> n4 (broadcast links)
-//         10.1.1.0/24        10.1.2.0/24        10.1.3.0/24
-//      Expected routes:
-//         n0:  route to 10.1.1.0 gw 0.0.0.0
-//              route to 10.1.2.0 gw 10.1.1.2
-//              route to 10.1.3.0 gw 10.1.1.2
-//         n1:  route to 10.1.1.0 gw 0.0.0.0
-//              route to 10.1.2.0 gw 0.0.0.0
-//              route to 10.1.3.0 gw 10.1.2.2
-//         n3:  route to 10.1.1.0 gw 10.1.2.1
-//              route to 10.1.2.0 gw 0.0.0.0
-//              route to 10.1.3.0 gw 0.0.0.0
-//         n4:  route to 10.1.3.0 gw 0.0.0.0
-//              route to 10.1.2.0 gw 10.1.3.1
-//              route to 10.1.1.0 gw 10.1.3.1
-//  Two Bridge test:
-//      n0 <------> n1 <---> Bridge-n2 <---> Bridge-n3 <---> n4 (broadcast links)
-//         10.1.1.0/24        10.1.2.0/24
-//      Expected routes:
-//         n0:  route to 10.1.1.0 gw 0.0.0.0
-//              route to 10.1.2.0 gw 10.1.1.2
-//         n4:  route to 10.1.2.0 gw 0.0.0.0
-//              route to 10.1.1.0 gw 10.1.2.1
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting Link test
- */
-class LinkTest : public TestCase
-{
-  public:
-    void DoSetup() override;
-    void DoRun() override;
-    LinkTest();
-
-  private:
-    NodeContainer m_nodes; //!< Nodes used in the test.
-};
-
-LinkTest::LinkTest()
-    : TestCase("Global routing on point-to-point link")
-{
-}
-
-void
-LinkTest::DoSetup()
-{
-    m_nodes.Create(2);
-
-    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper;
-    simpleHelper.SetNetDevicePointToPointMode(true);
-    NetDeviceContainer net = simpleHelper.Install(m_nodes, channel);
-
-    InternetStackHelper internet;
-    // By default, InternetStackHelper adds a static and global routing
-    // implementation.  We just want the global for this test.
-    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
-    internet.SetRoutingHelper(ipv4RoutingHelper);
-    internet.Install(m_nodes);
-
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.252");
-    Ipv4InterfaceContainer i = ipv4.Assign(net);
-}
-
-void
-LinkTest::DoRun()
-{
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Ptr<Ipv4L3Protocol> ip0 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4L3Protocol> ip1 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip1, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing0 = ip0->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting0 = routing0->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting0, nullptr, "Error-- no Ipv4GlobalRouting object");
-    Ptr<Ipv4RoutingProtocol> routing1 = ip1->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting1 = routing1->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting1, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    // Test that the right number of routes found
-    uint32_t nRoutes0 = globalRouting0->GetNRoutes();
-    NS_LOG_DEBUG("LinkTest nRoutes0 " << nRoutes0);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes0, 1, "Error-- not one route");
-    Ipv4RoutingTableEntry* route = globalRouting0->GetRoute(0);
-    NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.1.2"), "Error-- wrong gateway");
-
-    // Test that the right number of routes found
-    uint32_t nRoutes1 = globalRouting1->GetNRoutes();
-    NS_TEST_ASSERT_MSG_EQ(nRoutes1, 1, "Error-- not one route");
-    NS_LOG_DEBUG("LinkTest nRoutes1 " << nRoutes1);
-    route = globalRouting1->GetRoute(0);
-    NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.1.1"), "Error-- wrong gateway");
-
-    bool result = true;
-
-    NS_TEST_ASSERT_MSG_EQ(result, true, "Message");
-    Simulator::Run();
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting LAN test
- */
-class LanTest : public TestCase
-{
-  public:
-    void DoSetup() override;
-    void DoRun() override;
-    LanTest();
-
-  private:
-    NodeContainer m_nodes; //!< Nodes used in the test.
-};
-
-LanTest::LanTest()
-    : TestCase("Global routing on broadcast link")
-{
-}
-
-void
-LanTest::DoSetup()
-{
-    m_nodes.Create(2);
-
-    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper;
-    NetDeviceContainer net = simpleHelper.Install(m_nodes, channel);
-
-    InternetStackHelper internet;
-    // By default, InternetStackHelper adds a static and global routing
-    // implementation.  We just want the global for this test.
-    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
-    internet.SetRoutingHelper(ipv4RoutingHelper);
-    internet.Install(m_nodes);
-
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.0");
-    Ipv4InterfaceContainer i = ipv4.Assign(net);
-}
-
-void
-LanTest::DoRun()
-{
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Ptr<Ipv4L3Protocol> ip0 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4L3Protocol> ip1 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip1, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing0 = ip0->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting0 = routing0->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting0, nullptr, "Error-- no Ipv4GlobalRouting object");
-    Ptr<Ipv4RoutingProtocol> routing1 = ip1->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting1 = routing1->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting1, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    // Test that the right number of routes found
-    uint32_t nRoutes0 = globalRouting0->GetNRoutes();
-    NS_LOG_DEBUG("LanTest nRoutes0 " << nRoutes0);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes0, 1, "Error-- more than one entry");
-    for (uint32_t i = 0; i < globalRouting0->GetNRoutes(); i++)
-    {
-        Ipv4RoutingTableEntry* route = globalRouting0->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-
-    // Test that the right number of routes found
-    uint32_t nRoutes1 = globalRouting1->GetNRoutes();
-    NS_LOG_DEBUG("LanTest nRoutes1 " << nRoutes1);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes1, 1, "Error-- more than one entry");
-    for (uint32_t i = 0; i < globalRouting0->GetNRoutes(); i++)
-    {
-        Ipv4RoutingTableEntry* route = globalRouting1->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting Two Link test
- */
-class TwoLinkTest : public TestCase
-{
-  public:
-    void DoSetup() override;
-    void DoRun() override;
-    TwoLinkTest();
-
-  private:
-    NodeContainer m_nodes; //!< Nodes used in the test.
-};
-
-TwoLinkTest::TwoLinkTest()
-    : TestCase("Global routing across two hops (point-to-point links)")
-{
-}
-
-void
-TwoLinkTest::DoSetup()
-{
-    m_nodes.Create(3);
-
-    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper;
-    simpleHelper.SetNetDevicePointToPointMode(true);
-    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
-    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
-
-    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper2;
-    simpleHelper2.SetNetDevicePointToPointMode(true);
-    NetDeviceContainer net2 = simpleHelper.Install(m_nodes.Get(1), channel2);
-    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
-
-    InternetStackHelper internet;
-    // By default, InternetStackHelper adds a static and global routing
-    // implementation.  We just want the global for this test.
-    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
-    internet.SetRoutingHelper(ipv4RoutingHelper);
-    internet.Install(m_nodes);
-
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.252");
-    Ipv4InterfaceContainer i = ipv4.Assign(net);
-    ipv4.SetBase("10.1.2.0", "255.255.255.252");
-    Ipv4InterfaceContainer i2 = ipv4.Assign(net2);
-}
-
-void
-TwoLinkTest::DoRun()
-{
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Ptr<Ipv4L3Protocol> ip0 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4L3Protocol> ip1 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip1, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4L3Protocol> ip2 = m_nodes.Get(2)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip2, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing0 = ip0->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting0 = routing0->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting0, nullptr, "Error-- no Ipv4GlobalRouting object");
-    Ptr<Ipv4RoutingProtocol> routing1 = ip1->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting1 = routing1->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting1, nullptr, "Error-- no Ipv4GlobalRouting object");
-    Ptr<Ipv4RoutingProtocol> routing2 = ip2->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting2 = routing2->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting2, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    // node n0
-    // Test that the right number of routes found
-    uint32_t nRoutes0 = globalRouting0->GetNRoutes();
-    NS_LOG_DEBUG("TwoLinkTest nRoutes0 " << nRoutes0);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes0, 1, "Error-- wrong number of links");
-
-    Ipv4RoutingTableEntry* route = globalRouting0->GetRoute(0);
-    NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.1.2"), "Error-- wrong gateway");
-
-    // node n1
-    // Test that the right number of routes found
-    uint32_t nRoutes1 = globalRouting1->GetNRoutes();
-    NS_LOG_DEBUG("TwoLinkTest nRoutes1 " << nRoutes1);
-    route = globalRouting1->GetRoute(0);
-    NS_LOG_DEBUG("TwoLinkTest entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.1.1"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.1.1"), "Error-- wrong gateway");
-    route = globalRouting1->GetRoute(1);
-    NS_LOG_DEBUG("TwoLinkTest entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.2.2"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.2.2"), "Error-- wrong gateway");
-    route = globalRouting1->GetRoute(2);
-    NS_LOG_DEBUG("TwoLinkTest entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.1.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.1.1"), "Error-- wrong gateway");
-    route = globalRouting1->GetRoute(3);
-    NS_LOG_DEBUG("TwoLinkTest entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.2.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.2.2"), "Error-- wrong gateway");
-
-    // node n2
-    // Test that the right number of routes found
-    uint32_t nRoutes2 = globalRouting2->GetNRoutes();
-    NS_LOG_DEBUG("TwoLinkTest nRoutes2 " << nRoutes2);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes2, 1, "Error-- wrong number of links");
-
-    route = globalRouting2->GetRoute(0);
-    NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("0.0.0.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.2.1"), "Error-- wrong gateway");
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting Two LAN test
- */
-class TwoLanTest : public TestCase
-{
-  public:
-    void DoSetup() override;
-    void DoRun() override;
-    TwoLanTest();
-
-  private:
-    NodeContainer m_nodes; //!< Nodes used in the test.
-};
-
-TwoLanTest::TwoLanTest()
-    : TestCase("Global routing across two hops (broadcast links)")
-{
-}
-
-void
-TwoLanTest::DoSetup()
-{
-    m_nodes.Create(3);
-
-    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper;
-    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
-    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
-
-    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper2;
-    NetDeviceContainer net2 = simpleHelper.Install(m_nodes.Get(1), channel2);
-    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
-
-    InternetStackHelper internet;
-    // By default, InternetStackHelper adds a static and global routing
-    // implementation.  We just want the global for this test.
-    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
-    internet.SetRoutingHelper(ipv4RoutingHelper);
-    internet.Install(m_nodes);
-
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.0");
-    Ipv4InterfaceContainer i = ipv4.Assign(net);
-    ipv4.SetBase("10.1.2.0", "255.255.255.0");
-    Ipv4InterfaceContainer i2 = ipv4.Assign(net2);
-}
-
-void
-TwoLanTest::DoRun()
-{
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Ptr<Ipv4L3Protocol> ip0 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4L3Protocol> ip1 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip1, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4L3Protocol> ip2 = m_nodes.Get(2)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip2, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing0 = ip0->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting0 = routing0->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting0, nullptr, "Error-- no Ipv4GlobalRouting object");
-    Ptr<Ipv4RoutingProtocol> routing1 = ip1->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting1 = routing1->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting1, nullptr, "Error-- no Ipv4GlobalRouting object");
-    Ptr<Ipv4RoutingProtocol> routing2 = ip2->GetRoutingProtocol();
-    Ptr<Ipv4GlobalRouting> globalRouting2 = routing2->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting2, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    // Test that the right number of routes found
-    uint32_t nRoutes0 = globalRouting0->GetNRoutes();
-    NS_LOG_DEBUG("TwoLanTest nRoutes0 " << nRoutes0);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes0, 2, "Error-- not two entries");
-    Ipv4RoutingTableEntry* route = globalRouting0->GetRoute(0);
-    NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.1.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("0.0.0.0"), "Error-- wrong gateway");
-    route = globalRouting0->GetRoute(1);
-    NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.2.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("10.1.1.2"), "Error-- wrong gateway");
-
-    // Test that the right number of routes found
-    uint32_t nRoutes1 = globalRouting1->GetNRoutes();
-    NS_LOG_DEBUG("TwoLanTest nRoutes1 " << nRoutes1);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes1, 2, "Error-- not two entries");
-    route = globalRouting1->GetRoute(0);
-    NS_LOG_DEBUG("TwoLanTest entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.1.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("0.0.0.0"), "Error-- wrong gateway");
-    route = globalRouting1->GetRoute(1);
-    NS_LOG_DEBUG("TwoLanTest entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    NS_TEST_ASSERT_MSG_EQ(route->GetDest(), Ipv4Address("10.1.2.0"), "Error-- wrong destination");
-    NS_TEST_ASSERT_MSG_EQ(route->GetGateway(), Ipv4Address("0.0.0.0"), "Error-- wrong gateway");
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting Bridge test
- */
-class BridgeTest : public TestCase
-{
-  public:
-    void DoSetup() override;
-    void DoRun() override;
-    BridgeTest();
-
-  private:
-    NodeContainer m_nodes; //!< Nodes used in the test.
-};
-
-BridgeTest::BridgeTest()
-    : TestCase("Global routing across bridging topology (bug 2102)")
-{
-}
-
-void
-BridgeTest::DoSetup()
-{
-    m_nodes.Create(5);
-
-    // connect node0 to node1
-    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper;
-    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
-    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
-
-    NetDeviceContainer bridgeFacingDevices;
-    NetDeviceContainer switchDevices;
-
-    // connect node1 to node2 (switch)
-    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper2;
-    NetDeviceContainer net2 = simpleHelper2.Install(m_nodes.Get(1), channel2);
-    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
-    bridgeFacingDevices.Add(net2.Get(0));
-    switchDevices.Add(net2.Get(1));
-
-    // connect node2 (switch) to node3
-    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper3;
-    NetDeviceContainer net3 = simpleHelper3.Install(m_nodes.Get(2), channel3);
-    net3.Add(simpleHelper3.Install(m_nodes.Get(3), channel3));
-    bridgeFacingDevices.Add(net3.Get(1));
-    switchDevices.Add(net3.Get(0));
-
-    // connect node3 to node4
-    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper4;
-    NetDeviceContainer net4 = simpleHelper4.Install(m_nodes.Get(3), channel4);
-    net4.Add(simpleHelper4.Install(m_nodes.Get(4), channel4));
-
-    Ptr<Node> switchNode = m_nodes.Get(2);
-    BridgeHelper bridge;
-    bridge.Install(switchNode, switchDevices);
-
-    InternetStackHelper internet;
-    // By default, InternetStackHelper adds a static and global routing
-    // implementation.  We just want the global for this test.
-    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
-    internet.SetRoutingHelper(ipv4RoutingHelper);
-
-    internet.Install(m_nodes.Get(0));
-    internet.Install(m_nodes.Get(1));
-    // m_nodes.Get (2) is bridge node
-    internet.Install(m_nodes.Get(3));
-    internet.Install(m_nodes.Get(4));
-
-    Ipv4AddressHelper address;
-    address.SetBase("10.1.1.0", "255.255.255.0");
-    address.Assign(net);
-
-    address.SetBase("10.1.2.0", "255.255.255.0");
-    address.Assign(bridgeFacingDevices);
-
-    address.SetBase("10.1.3.0", "255.255.255.0");
-    address.Assign(net4);
-}
-
-void
-BridgeTest::DoRun()
-{
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Ptr<Ipv4L3Protocol> ip0 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing0 = ip0->GetRoutingProtocol();
-    NS_TEST_ASSERT_MSG_NE(routing0, nullptr, "Error-- no Ipv4 routing protocol object");
-    Ptr<Ipv4GlobalRouting> globalRouting0 = routing0->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting0, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    Ptr<Ipv4L3Protocol> ip1 = m_nodes.Get(1)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip1, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing1 = ip1->GetRoutingProtocol();
-    NS_TEST_ASSERT_MSG_NE(routing1, nullptr, "Error-- no Ipv4 routing protocol object");
-    Ptr<Ipv4GlobalRouting> globalRouting1 = routing1->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting1, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    // Skip to n4
-    Ptr<Ipv4L3Protocol> ip4 = m_nodes.Get(4)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip4, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing4 = ip4->GetRoutingProtocol();
-    NS_TEST_ASSERT_MSG_NE(routing4, nullptr, "Error-- no Ipv4 routing protocol object");
-    Ptr<Ipv4GlobalRouting> globalRouting4 = routing4->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting4, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    Ipv4RoutingTableEntry* route = nullptr;
-    // n0
-    // Test that the right number of routes found
-    uint32_t nRoutes0 = globalRouting0->GetNRoutes();
-    NS_LOG_DEBUG("BridgeTest nRoutes0 " << nRoutes0);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes0, 3, "Error-- not three entries");
-    for (uint32_t i = 0; i < globalRouting0->GetNRoutes(); i++)
-    {
-        route = globalRouting0->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-    // Spot check the last route
-    if (route)
-    {
-        NS_TEST_ASSERT_MSG_EQ(route->GetDest(),
-                              Ipv4Address("10.1.3.0"),
-                              "Error-- wrong destination");
-        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(),
-                              Ipv4Address("10.1.1.2"),
-                              "Error-- wrong gateway");
-    }
-
-    // n1
-    // Test that the right number of routes found
-    route = nullptr;
-    uint32_t nRoutes1 = globalRouting1->GetNRoutes();
-    NS_LOG_DEBUG("BridgeTest nRoutes1 " << nRoutes1);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes1, 3, "Error-- not three entries");
-    for (uint32_t i = 0; i < globalRouting1->GetNRoutes(); i++)
-    {
-        route = globalRouting1->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-    // Spot check the last route
-    if (route)
-    {
-        NS_TEST_ASSERT_MSG_EQ(route->GetDest(),
-                              Ipv4Address("10.1.3.0"),
-                              "Error-- wrong destination");
-        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(),
-                              Ipv4Address("10.1.2.2"),
-                              "Error-- wrong gateway");
-    }
-
-    // skip n2 and n3, just verify n4
-    NS_LOG_DEBUG("BridgeTest skip print out of n2 and n3, go next to node n4");
-
-    // n4
-    route = nullptr;
-    // Test that the right number of routes found
-    uint32_t nRoutes4 = globalRouting4->GetNRoutes();
-    NS_LOG_DEBUG("BridgeTest nRoutes4 " << nRoutes4);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes4, 3, "Error-- not three entries");
-    for (uint32_t i = 0; i < globalRouting4->GetNRoutes(); i++)
-    {
-        route = globalRouting4->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-    // Spot check the last route
-    if (route)
-    {
-        NS_TEST_ASSERT_MSG_EQ(route->GetDest(),
-                              Ipv4Address("10.1.1.0"),
-                              "Error-- wrong destination");
-        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(),
-                              Ipv4Address("10.1.3.1"),
-                              "Error-- wrong gateway");
-    }
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting Two bridges test
- */
-class TwoBridgeTest : public TestCase
-{
-  public:
-    void DoSetup() override;
-    void DoRun() override;
-    TwoBridgeTest();
-
-  private:
-    NodeContainer m_nodes; //!< Nodes used in the test.
-};
-
-TwoBridgeTest::TwoBridgeTest()
-    : TestCase("Global routing across two bridges")
-{
-}
-
-void
-TwoBridgeTest::DoSetup()
-{
-    m_nodes.Create(5);
-
-    // connect node0 to node1
-    Ptr<SimpleChannel> channel = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper;
-    NetDeviceContainer net = simpleHelper.Install(m_nodes.Get(0), channel);
-    net.Add(simpleHelper.Install(m_nodes.Get(1), channel));
-
-    NetDeviceContainer bridgeFacingDevices;
-    NetDeviceContainer switchn2Devices;
-    NetDeviceContainer switchn3Devices;
-
-    // connect node1 to node2 (switch)
-    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper2;
-    NetDeviceContainer net2 = simpleHelper2.Install(m_nodes.Get(1), channel2);
-    net2.Add(simpleHelper2.Install(m_nodes.Get(2), channel2));
-    bridgeFacingDevices.Add(net2.Get(0));
-    switchn2Devices.Add(net2.Get(1));
-
-    // connect node2 (switch) to node3
-    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper3;
-    NetDeviceContainer net3 = simpleHelper3.Install(m_nodes.Get(2), channel3);
-    net3.Add(simpleHelper3.Install(m_nodes.Get(3), channel3));
-    switchn2Devices.Add(net3.Get(0));
-    switchn3Devices.Add(net3.Get(1));
-
-    // connect node3 to node4
-    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
-    SimpleNetDeviceHelper simpleHelper4;
-    NetDeviceContainer net4 = simpleHelper4.Install(m_nodes.Get(3), channel4);
-    net4.Add(simpleHelper4.Install(m_nodes.Get(4), channel4));
-    switchn3Devices.Add(net4.Get(0));
-    bridgeFacingDevices.Add(net4.Get(1));
-
-    Ptr<Node> switchn2Node = m_nodes.Get(2);
-    BridgeHelper bridgen2Helper;
-    bridgen2Helper.Install(switchn2Node, switchn2Devices);
-
-    Ptr<Node> switchn3Node = m_nodes.Get(3);
-    BridgeHelper bridgen3Helper;
-    bridgen3Helper.Install(switchn3Node, switchn3Devices);
-
-    InternetStackHelper internet;
-    // By default, InternetStackHelper adds a static and global routing
-    // implementation.  We just want the global for this test.
-    Ipv4GlobalRoutingHelper ipv4RoutingHelper;
-    internet.SetRoutingHelper(ipv4RoutingHelper);
-
-    internet.Install(m_nodes.Get(0));
-    internet.Install(m_nodes.Get(1));
-    // m_nodes.Get (2) is bridge node
-    // m_nodes.Get (3) is bridge node
-    internet.Install(m_nodes.Get(4));
-
-    Ipv4AddressHelper address;
-    address.SetBase("10.1.1.0", "255.255.255.0");
-    address.Assign(net);
-
-    address.SetBase("10.1.2.0", "255.255.255.0");
-    address.Assign(bridgeFacingDevices);
-}
-
-void
-TwoBridgeTest::DoRun()
-{
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Ptr<Ipv4L3Protocol> ip0 = m_nodes.Get(0)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip0, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing0 = ip0->GetRoutingProtocol();
-    NS_TEST_ASSERT_MSG_NE(routing0, nullptr, "Error-- no Ipv4 routing protocol object");
-    Ptr<Ipv4GlobalRouting> globalRouting0 = routing0->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting0, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    // Skip to n4
-    Ptr<Ipv4L3Protocol> ip4 = m_nodes.Get(4)->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip4, nullptr, "Error-- no Ipv4 object");
-    Ptr<Ipv4RoutingProtocol> routing4 = ip4->GetRoutingProtocol();
-    NS_TEST_ASSERT_MSG_NE(routing4, nullptr, "Error-- no Ipv4 routing protocol object");
-    Ptr<Ipv4GlobalRouting> globalRouting4 = routing4->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting4, nullptr, "Error-- no Ipv4GlobalRouting object");
-
-    Ipv4RoutingTableEntry* route = nullptr;
-    // n0
-    // Test that the right number of routes found
-    uint32_t nRoutes0 = globalRouting0->GetNRoutes();
-    NS_LOG_DEBUG("BridgeTest nRoutes0 " << nRoutes0);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes0, 2, "Error-- not two entries");
-    for (uint32_t i = 0; i < globalRouting0->GetNRoutes(); i++)
-    {
-        route = globalRouting0->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-    // Spot check the last route
-    if (route)
-    {
-        NS_TEST_ASSERT_MSG_EQ(route->GetDest(),
-                              Ipv4Address("10.1.2.0"),
-                              "Error-- wrong destination");
-        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(),
-                              Ipv4Address("10.1.1.2"),
-                              "Error-- wrong gateway");
-    }
-    // skip n2 and n3, just verify n4
-    NS_LOG_DEBUG("BridgeTest skip print out of n1-n3, go next to node n4");
-
-    // n4
-    // Test that the right number of routes found
-    route = nullptr;
-    uint32_t nRoutes4 = globalRouting4->GetNRoutes();
-    NS_LOG_DEBUG("BridgeTest nRoutes4 " << nRoutes4);
-    NS_TEST_ASSERT_MSG_EQ(nRoutes4, 2, "Error-- not two entries");
-    for (uint32_t i = 0; i < globalRouting4->GetNRoutes(); i++)
-    {
-        route = globalRouting4->GetRoute(i);
-        NS_LOG_DEBUG("entry dest " << route->GetDest() << " gw " << route->GetGateway());
-    }
-    // Spot check the last route
-    if (route)
-    {
-        NS_TEST_ASSERT_MSG_EQ(route->GetDest(),
-                              Ipv4Address("10.1.1.0"),
-                              "Error-- wrong destination");
-        NS_TEST_ASSERT_MSG_EQ(route->GetGateway(),
-                              Ipv4Address("10.1.2.1"),
-                              "Error-- wrong gateway");
-    }
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 Dynamic GlobalRouting test
- */
-class Ipv4DynamicGlobalRoutingTestCase : public TestCase
-{
-  public:
-    Ipv4DynamicGlobalRoutingTestCase();
-    ~Ipv4DynamicGlobalRoutingTestCase() override;
-
-  private:
-    /**
-     * @brief Send some data
-     * @param index Index of the socket to use.
-     */
-    void SendData(uint8_t index);
-
-    /**
-     * @brief Shutdown a socket
-     * @param index Index of the socket to close.
-     */
-    void ShutDownSock(uint8_t index);
-
-    /**
-     * Handle an incoming packet
-     * @param socket The input socket.
-     */
-    void HandleRead(Ptr<Socket> socket);
-    void DoRun() override;
-
-    uint16_t m_count;                                      //!< Number of packets received.
-    std::vector<std::pair<Ptr<Socket>, bool>> m_sendSocks; //!< Sending sockets.
-    DataRate m_dataRate;                                   //!< Data rate.
-    uint16_t m_packetSize;                                 //!< Packet size.
-    std::vector<uint8_t>
-        m_firstInterface; //!< Packets received on the 1st interface at a given time.
-    std::vector<uint8_t>
-        m_secondInterface; //!< Packets received on the 2nd interface at a given time.
-};
-
-// Add some help text to this case to describe what it is intended to test
-Ipv4DynamicGlobalRoutingTestCase::Ipv4DynamicGlobalRoutingTestCase()
-    : TestCase("Dynamic global routing example"),
-      m_count(0)
-{
-    m_firstInterface.resize(16, 0);
-    m_secondInterface.resize(16, 0);
-    m_dataRate = DataRate("2kbps");
-    m_packetSize = 50;
-}
-
-Ipv4DynamicGlobalRoutingTestCase::~Ipv4DynamicGlobalRoutingTestCase()
-{
-    for (auto iter = m_sendSocks.begin(); iter != m_sendSocks.end(); iter++)
-    {
-        if (iter->second)
-        {
-            iter->second = false;
-            iter->first->Close();
-            iter->first = nullptr;
-        }
-    }
-}
-
-void
-Ipv4DynamicGlobalRoutingTestCase::HandleRead(Ptr<Socket> socket)
-{
-    Ptr<Packet> packet;
-    Address from;
-    while ((packet = socket->RecvFrom(from)))
-    {
-        if (packet->GetSize() == 0)
-        { // EOF
-            break;
-        }
-        Ipv4PacketInfoTag tag;
-        bool found;
-        found = packet->PeekPacketTag(tag);
-        uint8_t now = static_cast<uint8_t>(Simulator::Now().GetSeconds());
-        if (found)
-        {
-            if (tag.GetRecvIf() == 1)
-            {
-                m_firstInterface[now]++;
-            }
-            if (tag.GetRecvIf() == 2)
-            {
-                m_secondInterface[now]++;
-            }
-            m_count++;
-        }
-    }
-}
-
-void
-Ipv4DynamicGlobalRoutingTestCase::SendData(uint8_t index)
-{
-    if (!m_sendSocks[index].second)
-    {
-        return;
-    }
-    Ptr<Packet> packet = Create<Packet>(m_packetSize);
-    m_sendSocks[index].first->Send(packet);
-
-    Time tNext(MicroSeconds(m_packetSize * 8 * 1e6 / m_dataRate.GetBitRate()));
-    Simulator::Schedule(tNext, &Ipv4DynamicGlobalRoutingTestCase::SendData, this, index);
-}
-
-void
-Ipv4DynamicGlobalRoutingTestCase::ShutDownSock(uint8_t index)
-{
-    m_sendSocks[index].second = false;
-    m_sendSocks[index].first->Close();
-    m_sendSocks[index].first = nullptr;
-}
-
-// Test derived from examples/routing/dynamic-global-routing.cc
-//
-// Network topology
-//
-//  n0
-//     \ p-p
-//      \          (shared csma/cd)
-//       n2 -------------------------n3
-//      /            |        |
-//     / p-p        n4        n5 ---------- n6
-//   n1                             p-p
-//   |                                      |
-//   ----------------------------------------
-//                p-p
-//
-// Test that for node n6, the interface facing n5 receives packets at
-// times (1-2), (4-6), (8-10), (11-12), (14-16) and the interface
-// facing n1 receives packets at times (2-4), (6-8), (12-13)
-//
-void
-Ipv4DynamicGlobalRoutingTestCase::DoRun()
-{
-    // The below value configures the default behavior of global routing.
-    // By default, it is disabled.  To respond to interface events, set to true
-    Config::SetDefault("ns3::Ipv4GlobalRouting::RespondToInterfaceEvents", BooleanValue(true));
-
-    NodeContainer c;
-    c.Create(7);
-    NodeContainer n0n2 = NodeContainer(c.Get(0), c.Get(2));
-    NodeContainer n1n2 = NodeContainer(c.Get(1), c.Get(2));
-    NodeContainer n5n6 = NodeContainer(c.Get(5), c.Get(6));
-    NodeContainer n1n6 = NodeContainer(c.Get(1), c.Get(6));
-    NodeContainer n2345 = NodeContainer(c.Get(2), c.Get(3), c.Get(4), c.Get(5));
-
-    InternetStackHelper internet;
-    internet.Install(c);
-
-    // We create the channels first without any IP addressing information
-    SimpleNetDeviceHelper devHelper;
-
-    devHelper.SetNetDevicePointToPointMode(true);
-    NetDeviceContainer d0d2 = devHelper.Install(n0n2);
-    NetDeviceContainer d1d6 = devHelper.Install(n1n6);
-    NetDeviceContainer d1d2 = devHelper.Install(n1n2);
-    NetDeviceContainer d5d6 = devHelper.Install(n5n6);
-
-    devHelper.SetNetDevicePointToPointMode(false);
-    NetDeviceContainer d2345 = devHelper.Install(n2345);
-
-    // Later, we add IP addresses.
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.0");
-    ipv4.Assign(d0d2);
-
-    ipv4.SetBase("10.1.2.0", "255.255.255.0");
-    ipv4.Assign(d1d2);
-
-    ipv4.SetBase("10.1.3.0", "255.255.255.0");
-    Ipv4InterfaceContainer i5i6 = ipv4.Assign(d5d6);
-
-    ipv4.SetBase("10.250.1.0", "255.255.255.0");
-    ipv4.Assign(d2345);
-
-    ipv4.SetBase("172.16.1.0", "255.255.255.0");
-    Ipv4InterfaceContainer i1i6 = ipv4.Assign(d1d6);
-
-    // Create router nodes, initialize routing database and set up the routing
-    // tables in the nodes.
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    // Create the applications to send UDP datagrams of size
-    // 50 bytes at a rate of 2 Kb/s
-    TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
-    uint16_t port = 9; // Discard port (RFC 863)
-
-    std::pair<Ptr<Socket>, bool> sendSockA;
-    sendSockA.first = Socket::CreateSocket(c.Get(1), tid);
-    sendSockA.first->Bind();
-    sendSockA.first->Connect(InetSocketAddress(i5i6.GetAddress(1), port));
-    sendSockA.second = true;
-    m_sendSocks.push_back(sendSockA);
-    Simulator::Schedule(Seconds(1), &Ipv4DynamicGlobalRoutingTestCase::SendData, this, 0);
-    Simulator::Schedule(Seconds(10), &Ipv4DynamicGlobalRoutingTestCase::ShutDownSock, this, 0);
-
-    std::pair<Ptr<Socket>, bool> sendSockB;
-    sendSockB.first = Socket::CreateSocket(c.Get(1), tid);
-    sendSockB.first->Bind();
-    sendSockB.first->Connect(InetSocketAddress(i1i6.GetAddress(1), port));
-    sendSockB.second = true;
-    m_sendSocks.push_back(sendSockB);
-    Simulator::Schedule(Seconds(11), &Ipv4DynamicGlobalRoutingTestCase::SendData, this, 1);
-    Simulator::Schedule(Seconds(16), &Ipv4DynamicGlobalRoutingTestCase::ShutDownSock, this, 1);
-
-    // Create an optional packet sink to receive these packets
-    Ptr<Socket> sink2 = Socket::CreateSocket(c.Get(6), tid);
-    sink2->Bind(Address(InetSocketAddress(Ipv4Address::GetAny(), port)));
-    sink2->Listen();
-    sink2->ShutdownSend();
-
-    sink2->SetRecvPktInfo(true);
-    sink2->SetRecvCallback(MakeCallback(&Ipv4DynamicGlobalRoutingTestCase::HandleRead, this));
-
-    Ptr<Node> n1 = c.Get(1);
-    Ptr<Ipv4> ipv41 = n1->GetObject<Ipv4>();
-    // The first ifIndex is 0 for loopback, then the first p2p is numbered 1,
-    // then the next p2p is numbered 2
-    uint32_t ipv4ifIndex1 = 2;
-
-    Simulator::Schedule(Seconds(2), &Ipv4::SetDown, ipv41, ipv4ifIndex1);
-    Simulator::Schedule(Seconds(4), &Ipv4::SetUp, ipv41, ipv4ifIndex1);
-
-    Ptr<Node> n6 = c.Get(6);
-    Ptr<Ipv4> ipv46 = n6->GetObject<Ipv4>();
-    // The first ifIndex is 0 for loopback, then the first p2p is numbered 1,
-    // then the next p2p is numbered 2
-    uint32_t ipv4ifIndex6 = 2;
-    Simulator::Schedule(Seconds(6), &Ipv4::SetDown, ipv46, ipv4ifIndex6);
-    Simulator::Schedule(Seconds(8), &Ipv4::SetUp, ipv46, ipv4ifIndex6);
-
-    Simulator::Schedule(Seconds(12), &Ipv4::SetDown, ipv41, ipv4ifIndex1);
-    Simulator::Schedule(Seconds(14), &Ipv4::SetUp, ipv41, ipv4ifIndex1);
-
-    Simulator::Run();
-
-    NS_TEST_ASSERT_MSG_EQ(m_count, 70, "Dynamic global routing did not deliver all packets");
-    // Test that for node n6, the interface facing n1 receives packets at
-    // times (1-2), (4-6), (8-10), (11-12), (14-16) and the interface
-    // facing n5 receives packets at times (2-4), (6-8), (12-13)
-
-    // Note: there are two sending sockets, both on n1.
-    // The first socket sends packets at time (1-10), the second at time (11, 16).
-    // The first socket sends packets to n6, targeting the address facing n5.
-    // The second socket sends packets to n6, targeting the address facing n1.
-    // This actually doesn't matter, as n6 will accept packets sent to the "wrong" address.
-    //
-    // The shortest path netween n1 and n6 is the direct one, but the topology changes during the
-    // simulation:
-    // - (2-4): removal from n1 of the interface toward n6
-    // - (6-8): removal from n6 of the interface toward n1
-    // - (12-14): removal from n1 of the interface toward n6
-    // When the link is broken, packets are rerouted though the longest (and only) path, reaching
-    // n6 though n5.
-
-    std::vector<uint8_t> firstInterfaceTest{0, 5, 0, 0, 5, 5, 0, 0, 5, 5, 0, 5, 0, 0, 5, 5};
-    std::vector<uint8_t> secondInterfaceTest{0, 0, 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 5, 5, 0, 0};
-
-    for (uint32_t index = 0; index < firstInterfaceTest.size(); index++)
-    {
-        NS_TEST_ASSERT_MSG_EQ(firstInterfaceTest[index],
-                              m_firstInterface[index],
-                              "Dynamic global routing did deliver the wrong number of packets "
-                              "to the first interface at time "
-                                  << index);
-    }
-
-    for (uint32_t index = 0; index < secondInterfaceTest.size(); index++)
-    {
-        NS_TEST_ASSERT_MSG_EQ(secondInterfaceTest[index],
-                              m_secondInterface[index],
-                              "Dynamic global routing did deliver the wrong number of packets "
-                              "to the second interface at time "
-                                  << index);
-    }
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 Dynamic GlobalRouting /32 test
- */
-class Ipv4GlobalRoutingSlash32TestCase : public TestCase
-{
-  public:
-    Ipv4GlobalRoutingSlash32TestCase();
-    ~Ipv4GlobalRoutingSlash32TestCase() override;
-
-    Ptr<Packet> m_receivedPacket; //!< number of received packets
-
-    /**
-     * @brief Receive a packet.
-     * @param socket The receiving socket.
-     */
-    void ReceivePkt(Ptr<Socket> socket);
-    /**
-     * @brief Send a packet.
-     * @param socket The sending socket.
-     * @param to The address of the receiver.
-     */
-    void DoSendData(Ptr<Socket> socket, std::string to);
-    /**
-     * @brief Send a packet.
-     * @param socket The sending socket.
-     * @param to The address of the receiver.
-     */
-    void SendData(Ptr<Socket> socket, std::string to);
-
-  private:
-    void DoRun() override;
-};
-
-// Add some help text to this case to describe what it is intended to test
-Ipv4GlobalRoutingSlash32TestCase::Ipv4GlobalRoutingSlash32TestCase()
-    : TestCase("Slash 32 global routing example")
-{
-}
-
-Ipv4GlobalRoutingSlash32TestCase::~Ipv4GlobalRoutingSlash32TestCase()
-{
-}
-
-void
-Ipv4GlobalRoutingSlash32TestCase::ReceivePkt(Ptr<Socket> socket)
-{
-    uint32_t availableData [[maybe_unused]] = socket->GetRxAvailable();
-    m_receivedPacket = socket->Recv(std::numeric_limits<uint32_t>::max(), 0);
-    NS_TEST_ASSERT_MSG_EQ(availableData,
-                          m_receivedPacket->GetSize(),
-                          "Received packet size is not equal to Rx buffer size");
-}
-
-void
-Ipv4GlobalRoutingSlash32TestCase::DoSendData(Ptr<Socket> socket, std::string to)
-{
-    Address realTo = InetSocketAddress(Ipv4Address(to.c_str()), 1234);
-    NS_TEST_EXPECT_MSG_EQ(socket->SendTo(Create<Packet>(123), 0, realTo), 123, "100");
-}
-
-void
-Ipv4GlobalRoutingSlash32TestCase::SendData(Ptr<Socket> socket, std::string to)
-{
-    m_receivedPacket = Create<Packet>();
-    Simulator::ScheduleWithContext(socket->GetNode()->GetId(),
-                                   Seconds(60),
-                                   &Ipv4GlobalRoutingSlash32TestCase::DoSendData,
-                                   this,
-                                   socket,
-                                   to);
-    Simulator::Stop(Seconds(66));
-    Simulator::Run();
-}
-
-// Test program for this 3-router scenario, using global routing
-//
-// (a.a.a.a/32)A<--x.x.x.0/30-->B<--y.y.y.0/30-->C(c.c.c.c/32)
-//
-void
-Ipv4GlobalRoutingSlash32TestCase::DoRun()
-{
-    Ptr<Node> nA = CreateObject<Node>();
-    Ptr<Node> nB = CreateObject<Node>();
-    Ptr<Node> nC = CreateObject<Node>();
-
-    NodeContainer c = NodeContainer(nA, nB, nC);
-
-    InternetStackHelper internet;
-    internet.Install(c);
-
-    // simple links
-    NodeContainer nAnB = NodeContainer(nA, nB);
-    NodeContainer nBnC = NodeContainer(nB, nC);
-
-    SimpleNetDeviceHelper devHelper;
-
-    Ptr<SimpleNetDevice> deviceA = CreateObject<SimpleNetDevice>();
-    deviceA->SetAddress(Mac48Address::Allocate());
-    nA->AddDevice(deviceA);
-
-    NetDeviceContainer dAdB = devHelper.Install(nAnB);
-    NetDeviceContainer dBdC = devHelper.Install(nBnC);
-
-    Ptr<SimpleNetDevice> deviceC = CreateObject<SimpleNetDevice>();
-    deviceC->SetAddress(Mac48Address::Allocate());
-    nC->AddDevice(deviceC);
-
-    // Later, we add IP addresses.
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.252");
-    Ipv4InterfaceContainer iAiB = ipv4.Assign(dAdB);
-
-    ipv4.SetBase("10.1.1.4", "255.255.255.252");
-    Ipv4InterfaceContainer iBiC = ipv4.Assign(dBdC);
-
-    Ptr<Ipv4> ipv4A = nA->GetObject<Ipv4>();
-    Ptr<Ipv4> ipv4B = nB->GetObject<Ipv4>();
-    Ptr<Ipv4> ipv4C = nC->GetObject<Ipv4>();
-
-    int32_t ifIndexA = ipv4A->AddInterface(deviceA);
-    int32_t ifIndexC = ipv4C->AddInterface(deviceC);
-
-    Ipv4InterfaceAddress ifInAddrA =
-        Ipv4InterfaceAddress(Ipv4Address("172.16.1.1"), Ipv4Mask("/32"));
-    ipv4A->AddAddress(ifIndexA, ifInAddrA);
-    ipv4A->SetMetric(ifIndexA, 1);
-    ipv4A->SetUp(ifIndexA);
-
-    Ipv4InterfaceAddress ifInAddrC =
-        Ipv4InterfaceAddress(Ipv4Address("192.168.1.1"), Ipv4Mask("/32"));
-    ipv4C->AddAddress(ifIndexC, ifInAddrC);
-    ipv4C->SetMetric(ifIndexC, 1);
-    ipv4C->SetUp(ifIndexC);
-
-    // Create router nodes, initialize routing database and set up the routing
-    // tables in the nodes.
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    // Create the UDP sockets
-    Ptr<SocketFactory> rxSocketFactory = nC->GetObject<UdpSocketFactory>();
-    Ptr<Socket> rxSocket = rxSocketFactory->CreateSocket();
-    NS_TEST_EXPECT_MSG_EQ(rxSocket->Bind(InetSocketAddress(Ipv4Address("192.168.1.1"), 1234)),
-                          0,
-                          "trivial");
-    rxSocket->SetRecvCallback(MakeCallback(&Ipv4GlobalRoutingSlash32TestCase::ReceivePkt, this));
-
-    Ptr<SocketFactory> txSocketFactory = nA->GetObject<UdpSocketFactory>();
-    Ptr<Socket> txSocket = txSocketFactory->CreateSocket();
-    txSocket->SetAllowBroadcast(true);
-
-    // ------ Now the tests ------------
-
-    // Unicast test
-    SendData(txSocket, "192.168.1.1");
-    NS_TEST_EXPECT_MSG_EQ(m_receivedPacket->GetSize(),
-                          123,
-                          "Static routing with /32 did not deliver all packets.");
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief This TestCase tests if ECMP Route Calculation works. It does not check the
- * correctness of the routes.
- */
-class EcmpRouteCalculationTestCase : public TestCase
-{
-  public:
-    EcmpRouteCalculationTestCase();
-    void DoSetup() override;
-    void DoRun() override;
-
-  private:
-    NodeContainer nodes; //!< Nodes used in the test.
-};
-
-EcmpRouteCalculationTestCase::EcmpRouteCalculationTestCase()
-    : TestCase("ECMP Route Calculation TestCase")
-{
-}
-
-void
-EcmpRouteCalculationTestCase::DoSetup()
-{
-    /*
-        This TestCase checks the resolution of issue #1243 in the issue tracker.
-        The problem was that Global Routing failed to calculate next hops when going from a
-        network vertex to a router vertex when ECMP Routes were involved.
-
-        //         Network Topology
-        //
-        //
-        //          ------n1------
-        //         /              \
-        //        /                \
-        //       n0                n3----n4----n5
-        //        \                /
-        //         \              /
-        //          ------n2------
-        //
-        //    Link n0-n1: 10.1.1.1/30,10.1.1.2/30
-        //    Link n0-n2: 10.1.2.1/30,10.1.2.2/30
-        //    Link n1-n3: 10.1.3.1/30,10.1.3.2/30
-        //    Link n2-n3: 10.1.4.1/30,10.1.4.2/30
-        //    Link n3-n4: 10.1.5.1/24,10.1.5.2/24
-        //    Link n4-n5: 10.1.6.1/24,10.1.6.2/24
-        //
-        //     Note: Link n4-n5 is a LAN LINK. All others are simple P2P links.
-    */
-
-    nodes.Create(6);
-
-    Ipv4GlobalRoutingHelper globalhelper;
-    InternetStackHelper stack;
-    stack.SetRoutingHelper(globalhelper);
-    stack.Install(nodes);
-    SimpleNetDeviceHelper devHelper;
-    devHelper.SetNetDevicePointToPointMode(true);
-
-    Ptr<SimpleChannel> channel1 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d01 = devHelper.Install(nodes.Get(0), channel1);
-    d01.Add(devHelper.Install(nodes.Get(1), channel1));
-
-    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d23 = devHelper.Install(nodes.Get(2), channel2);
-    d23.Add(devHelper.Install(nodes.Get(3), channel2));
-
-    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d02 = devHelper.Install(nodes.Get(0), channel3);
-    d02.Add(devHelper.Install(nodes.Get(2), channel3));
-
-    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d34 = devHelper.Install(nodes.Get(3), channel4);
-    d34.Add(devHelper.Install(nodes.Get(4), channel4));
-
-    Ptr<SimpleChannel> channel5 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d13 = devHelper.Install(nodes.Get(1), channel5);
-    d13.Add(devHelper.Install(nodes.Get(3), channel5));
-
-    devHelper.SetNetDevicePointToPointMode(false);
-
-    Ptr<SimpleChannel> channel6 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d45 = devHelper.Install(nodes.Get(4), channel6);
-    d45.Add(devHelper.Install(nodes.Get(5), channel6));
-
-    // Assign IP addresses to the devices
-    Ipv4AddressHelper address;
-    address.SetBase("10.1.1.0", "255.255.255.252");
-    Ipv4InterfaceContainer i01 = address.Assign(d01);
-
-    address.SetBase("10.1.2.0", "255.255.255.252");
-    Ipv4InterfaceContainer i02 = address.Assign(d02);
-
-    address.SetBase("10.1.3.0", "255.255.255.252");
-    Ipv4InterfaceContainer i13 = address.Assign(d13);
-
-    address.SetBase("10.1.4.0", "255.255.255.224");
-    Ipv4InterfaceContainer i23 = address.Assign(d23);
-
-    address.SetBase("10.1.5.0", "255.255.255.0");
-    Ipv4InterfaceContainer i34 = address.Assign(d34);
-
-    address.SetBase("10.1.6.0", "255.255.255.0");
-    Ipv4InterfaceContainer i45 = address.Assign(d45);
-}
-
-void
-EcmpRouteCalculationTestCase::DoRun()
-{
-    // The purpose of this test is to make sure the code doesn't crash when calculating ECMP routes
-    // with a topology described in issue #1243. It does not look into the correctness of the
-    // routes. We have other tests to make sure the routes are correct.
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    Simulator::Run();
-    Simulator::Stop(Seconds(2));
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief TestCase to check calls to GlobalRoutingProtocol API. This TestCase Checks the output
- * returned by the GlobalRoutingProtocol API to Ipv4L3 Layer.
- */
-class GlobalRoutingProtocolTestCase : public TestCase
-{
-  public:
-    GlobalRoutingProtocolTestCase();
-    ~GlobalRoutingProtocolTestCase() override;
-
-  private:
-    void DoSetup() override;
-    void DoRun() override;
-    /**
-     * @brief Checks the Path taken by packets by calling RouteInput() and RouteOutput() APIs of
-     * GlobalRoutingProtocol. This mimics how Ipv4L3Protocol calls these APIs.
-     * @param pathNodes Vector of nodes in the path. This includes the source node,Intermediate
-     * nextHop nodes and the destination node.
-     * @param path Vector of Ipv4Addresses in the path. This includes the source egress IpAddress,
-     * destination ingress IpAddress and intermediate nextHop Node's ingress Ipaddress that we need
-     * to check.It does not include the egress addresses for intermediate nodes.
-     */
-    void CheckPath(std::vector<Ptr<Node>> pathNodes, std::vector<Ipv4Address> path);
-
-    /**
-     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
-     * @param route Route to be used for forwarding
-     * @param packet Packet to be forwarded
-     * @param header IPv4 header of the packet
-     */
-    void MyUnicastCallback(Ptr<Ipv4Route> route,
-                           Ptr<const Packet> packet,
-                           const Ipv4Header& header);
-    /**
-     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
-     * @param packet Packet to be locally delivered
-     * @param header IPv4 header of the packet
-     * @param iif Ingress Interface index
-     */
-    void MyLocalDeliverCallback(Ptr<const Packet> packet, const Ipv4Header& header, uint32_t iif);
-    /**
-     * @brief Callback function for RouteInput() API of GlobalRoutingProtocol.
-     * @param packet Packet to be locally delivered
-     * @param header IPv4 header of the packet
-     * @param errno_ Socket error number
-     */
-    void MyErrorCallback(Ptr<const Packet> packet,
-                         const Ipv4Header& header,
-                         Socket::SocketErrno errno_);
-
-    NodeContainer nodes;                                 //!< Nodes used in the test.
-    Ptr<Ipv4Route> m_lastRoute;                          //!< Route to be tested.
-    Ipv4RoutingProtocol::UnicastForwardCallback m_ucb;   ///< Unicast forward callback
-    Ipv4RoutingProtocol::MulticastForwardCallback m_mcb; ///< Multicast forward callback
-    Ipv4RoutingProtocol::LocalDeliverCallback m_lcb;     ///< Local delivery callback
-    Ipv4RoutingProtocol::ErrorCallback m_ecb;            ///< Error callback
-};
-
-// Signature: void (Ptr<Ipv4Route> route, Ptr<const Packet> packet, const Ipv4Header &header)
-void
-GlobalRoutingProtocolTestCase::MyUnicastCallback(Ptr<Ipv4Route> route,
-                                                 Ptr<const Packet> packet,
-                                                 const Ipv4Header& header)
-{
-    NS_LOG_DEBUG("Unicast Forward Callback called.");
-    m_lastRoute = route;
-}
-
-// Signature: void (Ptr<const Packet>, const Ipv4Header&, uint32_t)
-void
-GlobalRoutingProtocolTestCase::MyLocalDeliverCallback(Ptr<const Packet> packet,
-                                                      const Ipv4Header& header,
-                                                      uint32_t iif)
-{
-    NS_LOG_DEBUG("Local Deliver Callback called.");
-}
-
-// Signature: void (Ptr<const Packet>, const Ipv4Header&, Socket::SocketErrno)
-void
-GlobalRoutingProtocolTestCase::MyErrorCallback(Ptr<const Packet> packet,
-                                               const Ipv4Header& header,
-                                               Socket::SocketErrno errno_)
-{
-    NS_LOG_DEBUG("Error Callback called.");
-    // Fail the test if this callback is called
-    NS_TEST_ASSERT_MSG_EQ(1, 1, "Error Callback called in RouteInput");
-}
-
-GlobalRoutingProtocolTestCase::GlobalRoutingProtocolTestCase()
-    : TestCase("Test API calls to GlobalRoutingProtocol")
-{
-    m_ucb = MakeCallback(&GlobalRoutingProtocolTestCase::MyUnicastCallback, this);
-    m_lcb = MakeCallback(&GlobalRoutingProtocolTestCase::MyLocalDeliverCallback, this);
-    m_ecb = MakeCallback(&GlobalRoutingProtocolTestCase::MyErrorCallback, this);
-    // Multicast callback is not used in unicast test, can bind to null or dummy
-    Ipv4RoutingProtocol::MulticastForwardCallback mcb;
-}
-
-GlobalRoutingProtocolTestCase::~GlobalRoutingProtocolTestCase()
-{
-}
-
-void
-GlobalRoutingProtocolTestCase::CheckPath(std::vector<Ptr<Node>> pathNodes,
-                                         std::vector<Ipv4Address> path)
-{
-    uint32_t pathSize = path.size();
-
-    Ptr<Ipv4L3Protocol> ip = pathNodes[0]->GetObject<Ipv4L3Protocol>();
-    NS_TEST_ASSERT_MSG_NE(ip, nullptr, "Error-- no Ipv4 object at source node");
-    Ptr<Ipv4RoutingProtocol> routing = ip->GetRoutingProtocol();
-    NS_TEST_ASSERT_MSG_NE(routing,
-                          nullptr,
-                          "Error-- no Ipv4 routing protocol object at source node");
-    Ptr<Ipv4GlobalRouting> globalRouting = routing->GetObject<Ipv4GlobalRouting>();
-    NS_TEST_ASSERT_MSG_NE(globalRouting,
-                          nullptr,
-                          "Error-- no Ipv4GlobalRouting object at source node");
-
-    Socket::SocketErrno errno_;
-    Ptr<NetDevice> oif(nullptr);
-    Ptr<Packet> packet = Create<Packet>();
-    Ipv4Header ipHeader;
-    ipHeader.SetSource(path[0]);
-    ipHeader.SetDestination(path[pathSize - 1]);
-
-    // for the source node we need to call RouteOutput()
-    m_lastRoute = globalRouting->RouteOutput(packet, ipHeader, oif, errno_);
-    // for the rest of the nodes we need to call RouteInput()
-    for (uint32_t i = 1; i < pathSize; i++)
-    {
-        // for each iteration except the last one check that the UnicastForward Callback is called.
-        // For the last node that is the destination node LocalDelivery Callback will be called.
-        if (i != pathSize - 1)
-        {
-            NS_TEST_ASSERT_MSG_EQ(m_lastRoute->GetGateway(), path[i], "Error-- wrong gateway");
-        }
-
-        ip = pathNodes[i]->GetObject<Ipv4L3Protocol>();
-        NS_TEST_ASSERT_MSG_NE(ip, nullptr, "Error-- no Ipv4 object at node");
-        routing = ip->GetRoutingProtocol();
-        NS_TEST_ASSERT_MSG_NE(routing, nullptr, "Error-- no Ipv4 routing protocol object at node");
-        globalRouting = routing->GetObject<Ipv4GlobalRouting>();
-        NS_TEST_ASSERT_MSG_NE(globalRouting,
-                              nullptr,
-                              "Error-- no Ipv4GlobalRouting object at node");
-
-        Ptr<Ipv4Interface> interf = ip->GetInterface(ip->GetInterfaceForAddress(path[i]));
-        Ptr<NetDevice> idevice = interf->GetDevice();
-
-        // call RouteInput() for the next node in the path
-        globalRouting->RouteInput(packet, ipHeader, idevice, m_ucb, m_mcb, m_lcb, m_ecb);
-    }
-}
-
-void
-GlobalRoutingProtocolTestCase::DoSetup()
-{
-    /**
-     * This test case is designed to test the overall functionality of the GlobalRoutingProtocol.
-     * It tests the calls to the GlobalRoutingProtocol's RouteOutput() and RouteInput() APIs.
-     * It mimics how Ipv4L3Protocol calls these APIs. The topology covers most of the cases
-     * that are expected to be handled by the GlobalRoutingProtocol.
-     */
-
-    //
-    //
-    // Network topology
-    //
-    //  n0
-    //     \ p-p
-    //      \          (shared csma/cd)
-    //       n2 -------------------------n3
-    //      /            |        |
-    //     / p-p        n4        n5 ---------- n6-------n7----Bridge-n8--------n9
-    //   n1                             p-p     |              10.1.4.0/24
-    //   |                                      |
-    //   ----------------------------------------
-    //                p-p
-    //
-    //
-    //
-
-    nodes.Create(10);
-    NodeContainer n2345 = NodeContainer(nodes.Get(2), nodes.Get(3), nodes.Get(4), nodes.Get(5));
-
-    // We create the channels first without any IP addressing information
-    SimpleNetDeviceHelper devHelper;
-
-    devHelper.SetNetDevicePointToPointMode(true);
-
-    Ptr<SimpleChannel> channel1 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d0d2 = devHelper.Install(nodes.Get(0), channel1);
-    d0d2.Add(devHelper.Install(nodes.Get(2), channel1));
-
-    Ptr<SimpleChannel> channel2 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d1d6 = devHelper.Install(nodes.Get(1), channel2);
-    d1d6.Add(devHelper.Install(nodes.Get(6), channel2));
-
-    Ptr<SimpleChannel> channel3 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d1d2 = devHelper.Install(nodes.Get(1), channel3);
-    d1d2.Add(devHelper.Install(nodes.Get(2), channel3));
-
-    Ptr<SimpleChannel> channel4 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d5d6 = devHelper.Install(nodes.Get(5), channel4);
-    d5d6.Add(devHelper.Install(nodes.Get(6), channel4));
-
-    Ptr<SimpleChannel> channel5 = CreateObject<SimpleChannel>();
-    devHelper.SetNetDevicePointToPointMode(false);
-    NetDeviceContainer d6d7 = devHelper.Install(nodes.Get(6), channel5);
-    d6d7.Add(devHelper.Install(nodes.Get(7), channel5));
-
-    devHelper.SetNetDevicePointToPointMode(false);
-    NetDeviceContainer d2345 = devHelper.Install(n2345);
-
-    // handle the bridge
-    // connect node 7 to node 8(Switch)
-    Ptr<SimpleChannel> channel6 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d78 = devHelper.Install(nodes.Get(7), channel6);
-    d78.Add(devHelper.Install(nodes.Get(8), channel6));
-
-    // connect node 8(switch) to node 9
-    Ptr<SimpleChannel> channel7 = CreateObject<SimpleChannel>();
-    NetDeviceContainer d89 = devHelper.Install(nodes.Get(8), channel7);
-    d89.Add(devHelper.Install(nodes.Get(9), channel7));
-
-    NetDeviceContainer bridgeFacingDevices;
-    NetDeviceContainer switchDevices;
-    bridgeFacingDevices.Add(d78.Get(0));
-    switchDevices.Add(d78.Get(1));
-    bridgeFacingDevices.Add(d89.Get(1));
-    switchDevices.Add(d89.Get(0));
-
-    Ptr<Node> switchNode = nodes.Get(8);
-    BridgeHelper bridge;
-    bridge.Install(switchNode, switchDevices);
-
-    InternetStackHelper internet;
-    Ipv4GlobalRoutingHelper glbrouting;
-    internet.SetRoutingHelper(glbrouting);
-    internet.Install(nodes.Get(0));
-    internet.Install(nodes.Get(1));
-    internet.Install(nodes.Get(2));
-    internet.Install(nodes.Get(3));
-    internet.Install(nodes.Get(4));
-    internet.Install(nodes.Get(5));
-    internet.Install(nodes.Get(6));
-    internet.Install(nodes.Get(7));
-    // node 8 is a bridge node
-    internet.Install(nodes.Get(9));
-
-    // Later, we add IP addresses.
-    Ipv4AddressHelper ipv4;
-    ipv4.SetBase("10.1.1.0", "255.255.255.0");
-    Ipv4InterfaceContainer i0d2 = ipv4.Assign(d0d2);
-
-    ipv4.SetBase("10.1.2.0", "255.255.255.0");
-    Ipv4InterfaceContainer i1d2 = ipv4.Assign(d1d2);
-
-    ipv4.SetBase("10.1.3.0", "255.255.255.0");
-    Ipv4InterfaceContainer i5i6 = ipv4.Assign(d5d6);
-
-    ipv4.SetBase("10.250.1.0", "255.255.255.0");
-    Ipv4InterfaceContainer i2345 = ipv4.Assign(d2345);
-
-    ipv4.SetBase("172.16.1.0", "255.255.255.0");
-    Ipv4InterfaceContainer i1i6 = ipv4.Assign(d1d6);
-
-    ipv4.SetBase("10.1.4.0", "255.255.255.0");
-    Ipv4InterfaceContainer i67 = ipv4.Assign(d6d7);
-
-    ipv4.SetBase("10.1.5.0", "255.255.255.0");
-    Ipv4InterfaceContainer i79 = ipv4.Assign(bridgeFacingDevices);
-}
-
-void
-GlobalRoutingProtocolTestCase::DoRun()
-{
-    // Create router nodes, initialize routing database and set up the routing
-    // tables in the nodes.
-    GlobalRouteManager::ResetRouterId();
-
-    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
-
-    // tests-------------------------
-    // test 1:check path from n0 to n6
-    // test 2:check path from n1 to n5
-    // test 3:check path from n1 to n9
-    // test 4:check path from n8 to n0
-
-    // Test 1: check path from n0 to n6
-    std::vector<Ipv4Address>
-        pathToCheck1; // includes the source egress destination ingress and intermediate next hops
-                      // addresses that we need to check.It does not include the egress addresses
-                      // for intermediate nodes.
-    pathToCheck1.emplace_back("10.1.1.1");
-    pathToCheck1.emplace_back("10.1.1.2");
-    pathToCheck1.emplace_back("10.1.2.1");
-    pathToCheck1.emplace_back("172.16.1.2");
-
-    std::vector<Ptr<Node>>
-        pathNodes1; // nodes in the path. Includes the source node and the destination node.
-    pathNodes1.push_back(nodes.Get(0));
-    pathNodes1.push_back(nodes.Get(2));
-    pathNodes1.push_back(nodes.Get(1));
-    pathNodes1.push_back(nodes.Get(6));
-
-    CheckPath(pathNodes1, pathToCheck1);
-
-    // Test 2:check path from n1 to n5
-    std::vector<Ipv4Address> pathToCheck2;
-    pathToCheck2.emplace_back("10.1.2.1");
-    pathToCheck2.emplace_back("10.1.2.2");
-    pathToCheck2.emplace_back("10.250.1.4");
-
-    std::vector<Ptr<Node>> pathNodes2;
-    pathNodes2.push_back(nodes.Get(1));
-    pathNodes2.push_back(nodes.Get(2));
-    pathNodes2.push_back(nodes.Get(5));
-
-    CheckPath(pathNodes2, pathToCheck2);
-
-    // Test 3:check path from n1 to n9
-    std::vector<Ipv4Address> pathToCheck3;
-    pathToCheck3.emplace_back("172.16.1.1");
-    pathToCheck3.emplace_back("172.16.1.2");
-    pathToCheck3.emplace_back("10.1.4.2");
-    pathToCheck3.emplace_back("10.1.5.2");
-
-    std::vector<Ptr<Node>> pathNodes3;
-    pathNodes3.push_back(nodes.Get(1));
-    pathNodes3.push_back(nodes.Get(6));
-    pathNodes3.push_back(nodes.Get(7));
-    pathNodes3.push_back(nodes.Get(9));
-
-    CheckPath(pathNodes3, pathToCheck3);
-
-    // Test 4:check path from n9 to n0
-    std::vector<Ipv4Address> pathToCheck4;
-    pathToCheck4.emplace_back("10.1.5.2");
-    pathToCheck4.emplace_back("10.1.5.1");
-    pathToCheck4.emplace_back("10.1.4.1");
-    pathToCheck4.emplace_back("10.1.3.1");
-    pathToCheck4.emplace_back("10.250.1.1");
-    pathToCheck4.emplace_back("10.1.1.1");
-
-    std::vector<Ptr<Node>> pathNodes4;
-    pathNodes4.push_back(nodes.Get(9));
-    pathNodes4.push_back(nodes.Get(7));
-    pathNodes4.push_back(nodes.Get(6));
-    pathNodes4.push_back(nodes.Get(5));
-    pathNodes4.push_back(nodes.Get(2));
-    pathNodes4.push_back(nodes.Get(0));
-
-    CheckPath(pathNodes4, pathToCheck4);
-
-    Simulator::Run();
-    Simulator::Stop(Seconds(10));
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup internet-test
- *
- * @brief IPv4 GlobalRouting TestSuite
- */
-class Ipv4GlobalRoutingTestSuite : public TestSuite
-{
-  public:
-    Ipv4GlobalRoutingTestSuite();
-};
-
-Ipv4GlobalRoutingTestSuite::Ipv4GlobalRoutingTestSuite()
-    : TestSuite("ipv4-global-routing", Type::UNIT)
-{
-    AddTestCase(new LinkTest, TestCase::Duration::QUICK);
-    AddTestCase(new LanTest, TestCase::Duration::QUICK);
-    AddTestCase(new TwoLinkTest, TestCase::Duration::QUICK);
-    AddTestCase(new TwoLanTest, TestCase::Duration::QUICK);
-    AddTestCase(new BridgeTest, TestCase::Duration::QUICK);
-    AddTestCase(new TwoBridgeTest, TestCase::Duration::QUICK);
-    AddTestCase(new Ipv4DynamicGlobalRoutingTestCase, TestCase::Duration::QUICK);
-    AddTestCase(new Ipv4GlobalRoutingSlash32TestCase, TestCase::Duration::QUICK);
-    AddTestCase(new EcmpRouteCalculationTestCase, TestCase::Duration::QUICK);
-    AddTestCase(new GlobalRoutingProtocolTestCase, TestCase::Duration::QUICK);
-}
-
-static Ipv4GlobalRoutingTestSuite
-    g_globalRoutingTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/neighbor-cache-test.cc ns-3.47/src/internet/test/neighbor-cache-test.cc
--- ns-3.46/src/internet/test/neighbor-cache-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/neighbor-cache-test.cc	2026-02-16 16:08:49.625230000 -0800
@@ -380,56 +380,56 @@
     // Check if the arp caches are updated correctly at time 2 after an IP address is removed.
     constexpr auto arpCache =
         "ARP Cache of node 0 at time 0\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 1 at time 0\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 2 at time 0\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 3 at time 0\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 0 at time 1\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "10.0.1.5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 1 at time 1\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "10.0.1.5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 2 at time 1\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 3 at time 1\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "10.0.1.5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 0 at time 2\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "10.0.1.5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 1 at time 2\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "10.0.1.5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 2 at time 2\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 3 at time 2\n"
-        "10.0.1.1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.0.1.2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "10.0.1.5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "10.0.1.1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.0.1.2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.0.1.5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), arpCache, "Arp cache is incorrect.");
 
     // Check if the ndisc caches are populated correctly at time 0,
@@ -437,92 +437,92 @@
     // Check if the ndisc caches are updated correctly at time 2 after an IP address is removed.
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +0s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +0s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 3 at time +0s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 0 at time +1s\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +1s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +1s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 3 at time +1s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 0 at time +2s\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +2s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +2s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 3 at time +2s\n"
-        "2001::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "2001::200:ff:fe00:5 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 1 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 1 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 1 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "2001::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:5 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 1 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 1 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 1 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
 
     m_receivedPacket->RemoveAllByteTags();
@@ -600,20 +600,20 @@
 
     // Check if arp caches are populated correctly in the first channel
     constexpr auto arpCache = "ARP Cache of node 0 at time 0\n"
-                              "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+                              "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 1 at time 0\n"
-                              "10.1.1.1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+                              "10.1.1.1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 2 at time 0\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), arpCache, "Arp cache is incorrect.");
 
     // Check if ndisc caches are populated correctly in the first channel
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
-        "2001::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +0s\n"
-        "2001::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +0s\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
     Simulator::Destroy();
@@ -691,20 +691,20 @@
     constexpr auto arpCache =
         "ARP Cache of node 0 at time 0\n"
         "ARP Cache of node 1 at time 0\n"
-        "10.1.2.2 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.1.2.2 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 2 at time 0\n"
-        "10.1.2.1 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "10.1.2.1 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), arpCache, "Arp cache is incorrect.");
 
     // Check if ndisc caches are populated correctly on NetDeviceContainer net2.
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
         "NDISC Cache of node 1 at time +0s\n"
-        "2001:1::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001:1::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +0s\n"
-        "2001:1::200:ff:fe00:3 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "2001:1::200:ff:fe00:3 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
     Simulator::Destroy();
 }
@@ -780,9 +780,9 @@
 
     // Check if arp caches are populated correctly on Ipv4InterfaceContainer i.
     constexpr auto arpCache = "ARP Cache of node 0 at time 0\n"
-                              "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+                              "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 1 at time 0\n"
-                              "10.1.1.1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+                              "10.1.1.1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 2 at time 0\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), arpCache, "Arp cache is incorrect.");
 
@@ -790,11 +790,11 @@
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
         "NDISC Cache of node 1 at time +0s\n"
-        "2001:1::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001:1::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +0s\n"
-        "2001:1::200:ff:fe00:3 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "2001:1::200:ff:fe00:3 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
     Simulator::Destroy();
 }
@@ -922,7 +922,7 @@
     // Check the time zero output
     // Check if the STATIC_AUTOGENERATED entries are flushed and the PERMANENT entry is left.
     constexpr auto ArpCache = "ARP Cache of node 0 at time 0\n"
-                              "10.1.1.4 dev 0 lladdr 04-06-00:00:00:00:00:01 PERMANENT\n"
+                              "10.1.1.4 dev 0 lladdr 03-06-00:00:00:00:00:01 PERMANENT\n"
                               "ARP Cache of node 1 at time 0\n"
                               "ARP Cache of node 2 at time 0\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), ArpCache, "Arp cache is incorrect.");
@@ -930,7 +930,7 @@
     // Check if the STATIC_AUTOGENERATED entries are flushed and the PERMANENT entry is left.
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
-        "2001::200:ff:fe00:4 dev 0 lladdr 04-06-00:00:00:00:00:01 PERMANENT\n"
+        "2001::200:ff:fe00:4 dev 0 lladdr 03-06-00:00:00:00:00:01 PERMANENT\n"
         "NDISC Cache of node 1 at time +0s\n"
         "NDISC Cache of node 2 at time +0s\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
@@ -954,11 +954,11 @@
     // This behavior was added in the ns-3.46 release (see issue #851).
     constexpr auto ArpCache5 =
         "ARP Cache of node 0 at time 5\n"
-        "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n";
+        "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n";
     constexpr auto NdiscCache5 =
         "NDISC Cache of node 0 at time +5s\n"
-        "2001::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n";
+        "2001::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream5v4.str(),
                           ArpCache5,
                           "Arp cache is incorrect after second Flush().");
@@ -1044,28 +1044,28 @@
     // not lead to duplicate entries.
     constexpr auto ArpCache =
         "ARP Cache of node 0 at time 0\n"
-        "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 1 at time 0\n"
-        "10.1.1.1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "10.1.2.2 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "10.1.1.1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "10.1.2.2 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "ARP Cache of node 2 at time 0\n"
-        "10.1.2.1 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "10.1.2.1 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), ArpCache, "Arp cache is incorrect.");
 
     // Check if the overlapped scope of PopulateNeighborCache() calls did
     // not lead to duplicate entries.
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
-        "2001::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +0s\n"
-        "2001::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "2001:1::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:4 dev 1 lladdr 04-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001:1::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:4 dev 1 lladdr 03-06-00:00:00:00:00:04 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +0s\n"
-        "2001:1::200:ff:fe00:3 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:3 dev 0 lladdr 04-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
+        "2001:1::200:ff:fe00:3 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:3 dev 0 lladdr 03-06-00:00:00:00:00:03 STATIC_AUTOGENERATED\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
     Simulator::Destroy();
 }
@@ -1235,17 +1235,17 @@
     // Check if the dynamic neighbor cache doesn't change after an Ip address is added,
     // Check if the dynamic neighbor cache update correctly after an Ip address is removed.
     constexpr auto ArpCache = "ARP Cache of node 0 at time 0\n"
-                              "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+                              "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 1 at time 0\n"
-                              "10.1.1.1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+                              "10.1.1.1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 2 at time 0\n"
                               "ARP Cache of node 0 at time 1\n"
-                              "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+                              "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 1 at time 1\n"
-                              "10.1.1.1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+                              "10.1.1.1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 2 at time 1\n"
                               "ARP Cache of node 0 at time 2\n"
-                              "10.1.1.2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+                              "10.1.1.2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
                               "ARP Cache of node 1 at time 2\n"
                               "ARP Cache of node 2 at time 2\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v4.str(), ArpCache, "Arp cache is incorrect.");
@@ -1254,24 +1254,24 @@
     // Check if the dynamic neighbor cache update correctly after an Ip address is removed.
     constexpr auto NdiscCache =
         "NDISC Cache of node 0 at time +0s\n"
-        "2001::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +0s\n"
-        "2001::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +0s\n"
         "NDISC Cache of node 0 at time +1s\n"
-        "2001::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +1s\n"
-        "2001::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +1s\n"
         "NDISC Cache of node 0 at time +2s\n"
-        "2001::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
-        "fe80::200:ff:fe00:2 dev 0 lladdr 04-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "2001::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:2 dev 0 lladdr 03-06-00:00:00:00:00:02 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 1 at time +2s\n"
-        "fe80::200:ff:fe00:1 dev 0 lladdr 04-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
+        "fe80::200:ff:fe00:1 dev 0 lladdr 03-06-00:00:00:00:00:01 STATIC_AUTOGENERATED\n"
         "NDISC Cache of node 2 at time +2s\n";
     NS_TEST_EXPECT_MSG_EQ(stringStream1v6.str(), NdiscCache, "Ndisc cache is incorrect.");
     Simulator::Destroy();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/tcp-abe-test.cc ns-3.47/src/internet/test/tcp-abe-test.cc
--- ns-3.46/src/internet/test/tcp-abe-test.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/test/tcp-abe-test.cc	2026-02-16 16:08:49.625230000 -0800
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2025 NITK Surathkal
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Authors: N Nagabhushanam    <thechosentwins2005@gmail.com>
+ *          Namburi Yaswanth   <yaswanthnamburi1010@gmail.com>
+ *          Vishruth S Kumar   <vishruthskumar@gmail.com>
+ *          Yashas             <yashas80dj@gmail.com>
+ *          Mohit P. Tahiliani <tahiliani@nitk.edu.in>
+ */
+#include "ns3/log.h"
+#include "ns3/tcp-cubic.h"
+#include "ns3/tcp-linux-reno.h"
+#include "ns3/tcp-socket-state.h"
+#include "ns3/test.h"
+
+namespace ns3
+{
+NS_LOG_COMPONENT_DEFINE("TcpAbeTestSuite");
+
+constexpr uint32_t WITHOUT_ABE = 0; //!< Test case without ABE
+constexpr uint32_t WITH_ABE = 1;    //!< Test case with ABE
+
+/**
+ * @ingroup internet-test
+ * @brief Test Socket Base class for ABE testing
+ *
+ * This class provides access to the TCP socket state and allows
+ * manipulation of ECN state for testing purposes.
+ */
+class TestTcpSocketBase : public TcpSocketBase
+{
+  public:
+    /**
+     * @brief Get the pointer for socket state
+     * @return ptr to TcpSocketState
+     */
+    Ptr<TcpSocketState> GetTcb() const
+    {
+        return m_tcb;
+    }
+
+    /**
+     * @brief Set the ECE state for testing ECN behavior
+     */
+    void SetECE()
+    {
+        m_tcb->m_ecnState = TcpSocketState::ECN_ECE_RCVD;
+    }
+};
+
+/**
+ * @ingroup internet-test
+ * @brief Checks if users are able to enable ABE or not.
+ **/
+
+class TcpAbeToggleTest : public TestCase
+{
+  protected:
+    Ptr<TestTcpSocketBase> m_socket; //!< Socket used for testing
+
+  public:
+    /**
+     * @brief Constructor
+     * @param desc Test case description
+     */
+    TcpAbeToggleTest(const std::string& desc)
+        : TestCase(desc)
+    {
+    }
+
+    /**
+     * @brief Runs the test
+     */
+    void DoRun() override
+    {
+        NS_LOG_FUNCTION(this);
+        m_socket = CreateObject<TestTcpSocketBase>();
+        m_socket->SetUseAbe(true);
+        NS_TEST_EXPECT_MSG_EQ(m_socket->GetTcb()->m_abeEnabled, true, "ABE should be enabled");
+        NS_TEST_EXPECT_MSG_EQ(m_socket->GetTcb()->m_useEcn,
+                              true,
+                              "ECN should be enabled along with ABE");
+
+        m_socket->SetUseAbe(false);
+        NS_TEST_EXPECT_MSG_EQ(m_socket->GetTcb()->m_abeEnabled, false, "ABE should be disabled");
+    }
+};
+
+/**
+ * @ingroup internet-test
+ * @brief Test case for congestion control algorithms with ABE
+ *
+ * This test verifies that when a TCP socket has ABE enabled, the congestion
+ * window value returned by its GetSsThresh() method will be calculated using
+ * the default BetaEcn value, and when ABE is disabled, the standard
+ * multiplicative decrease by half is used.
+ * ABE should only work for TcpCubic, TcpNewReno, TcpLinuxReno as specified in
+ * RFC-8511
+ */
+class TcpAbeTest : public TestCase
+{
+  private:
+    uint32_t testCase;        //!< Test case number
+    uint32_t m_segmentSize;   //!< Segment size
+    uint32_t m_initialCwnd;   //!< Initial congestion window
+    uint32_t m_expectedCwnd;  //!< Expected congestion window after applying Beta(BetaLoss)/BetaEcn
+    uint32_t m_bytesInFlight; //!< Bytes in flight
+    TypeId m_congestionControlType;  //!< TypeId of the congestion control algorithm type used for
+                                     //!< the test
+    Ptr<TestTcpSocketBase> m_socket; //!< Socket used for testing
+
+  public:
+    /**
+     * @brief Constructor
+     * @param desc Test case description
+     * @param testCase Test case identifier (0 for without ABE, 1 for with ABE)
+     * @param segmentSize Segment size
+     * @param initialCwnd Initial congestion window
+     * @param expectedCwnd Expected congestion window after applying Beta(BetaLoss)/BetaEcn
+     * @param bytesInFlight Bytes in flight
+     * @param congestionControlType TypeId of the congestion control algorithm type used for the
+     * test
+     */
+    TcpAbeTest(uint32_t testCase,
+               uint32_t segmentSize,
+               uint32_t initialCwnd,
+               uint32_t expectedCwnd,
+               uint32_t bytesInFlight,
+               TypeId& congestionControlType,
+               const std::string& desc)
+        : TestCase(desc),
+          testCase(testCase),
+          m_segmentSize(segmentSize),
+          m_initialCwnd(initialCwnd),
+          m_expectedCwnd(expectedCwnd),
+          m_bytesInFlight(bytesInFlight),
+          m_congestionControlType(congestionControlType)
+    {
+        NS_LOG_FUNCTION(this << desc);
+    }
+
+    /**
+     * @brief Runs the test
+     */
+    void DoRun() override
+    {
+        NS_LOG_FUNCTION(this);
+        m_socket = CreateObject<TestTcpSocketBase>();
+        ObjectFactory congestionAlgorithmFactory;
+        congestionAlgorithmFactory.SetTypeId(m_congestionControlType);
+        Ptr<TcpCongestionOps> algo = congestionAlgorithmFactory.Create<TcpCongestionOps>();
+        m_socket->SetCongestionControlAlgorithm(algo);
+
+        if (testCase == WITH_ABE)
+        {
+            m_socket->SetUseAbe(true);
+            m_socket->SetECE();
+            m_socket->GetTcb()->m_segmentSize = m_segmentSize;
+            m_socket->GetTcb()->m_cWnd = m_initialCwnd;
+            m_socket->GetTcb()->m_bytesInFlight = m_bytesInFlight;
+            uint32_t newCwnd = algo->GetSsThresh(m_socket->GetTcb(), m_bytesInFlight);
+            NS_TEST_EXPECT_MSG_EQ(
+                newCwnd,
+                m_expectedCwnd,
+                m_congestionControlType.GetName() +
+                    " congestion control with ABE should apply BetaEcn correctly");
+        }
+        else if (testCase == WITHOUT_ABE)
+        {
+            m_socket->SetUseAbe(false);
+            m_socket->SetECE();
+            m_socket->GetTcb()->m_segmentSize = m_segmentSize;
+            m_socket->GetTcb()->m_cWnd = m_initialCwnd;
+            m_socket->GetTcb()->m_bytesInFlight = m_bytesInFlight;
+            uint32_t newCwnd = algo->GetSsThresh(m_socket->GetTcb(), m_bytesInFlight);
+            NS_TEST_EXPECT_MSG_EQ(
+                newCwnd,
+                m_expectedCwnd,
+                m_congestionControlType.GetName() +
+                    " congestion control without ABE should apply Beta correctly");
+        }
+        else
+        {
+            NS_TEST_EXPECT_MSG_EQ(0, 1, "Invalid test case");
+        }
+        Simulator::Destroy();
+        m_socket = nullptr;
+    }
+};
+
+/**
+ * @ingroup internet-test
+ * @brief Test suite for Alternative Backoff with ECN (ABE)
+ *
+ * This test suite verifies the behavior of TCP ABE with different
+ * congestion control algorithms.
+ */
+class TcpAbeTestSuite : public TestSuite
+{
+  public:
+    /**
+     * @brief Constructor
+     */
+    TcpAbeTestSuite()
+        : TestSuite("tcp-abe-test", Type::UNIT)
+    {
+        AddTestCase(new TcpAbeToggleTest("Test enabling and disabling ABE"),
+                    TestCase::Duration::QUICK);
+
+        TypeId cong_control_type = TcpCubic::GetTypeId();
+        AddTestCase(new TcpAbeTest(WITHOUT_ABE,
+                                   1,
+                                   1000,
+                                   700,
+                                   100,
+                                   cong_control_type,
+                                   "Test TCP CUBIC without ABE"),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TcpAbeTest(WITH_ABE,
+                                   1,
+                                   1000,
+                                   850,
+                                   100,
+                                   cong_control_type,
+                                   "Test TCP CUBIC with ABE"),
+                    TestCase::Duration::QUICK);
+
+        cong_control_type = TcpLinuxReno::GetTypeId();
+        AddTestCase(new TcpAbeTest(WITHOUT_ABE,
+                                   1,
+                                   1000,
+                                   500,
+                                   100,
+                                   cong_control_type,
+                                   "Test TCP Linux Reno without ABE"),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TcpAbeTest(WITH_ABE,
+                                   1,
+                                   1000,
+                                   700,
+                                   100,
+                                   cong_control_type,
+                                   "Test TCP Linux Reno with ABE"),
+                    TestCase::Duration::QUICK);
+
+        cong_control_type = TcpNewReno::GetTypeId();
+        AddTestCase(new TcpAbeTest(WITHOUT_ABE,
+                                   1,
+                                   1000,
+                                   500,
+                                   1000,
+                                   cong_control_type,
+                                   "Test TCP New Reno without ABE"),
+                    TestCase::Duration::QUICK);
+        AddTestCase(new TcpAbeTest(WITH_ABE,
+                                   1,
+                                   1000,
+                                   700,
+                                   1000,
+                                   cong_control_type,
+                                   "Test TCP New Reno with ABE"),
+                    TestCase::Duration::QUICK);
+    }
+};
+
+static TcpAbeTestSuite g_tcpAbeTestSuite; //!< static var for test initialization
+
+} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/tcp-fack-test.cc ns-3.47/src/internet/test/tcp-fack-test.cc
--- ns-3.46/src/internet/test/tcp-fack-test.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/internet/test/tcp-fack-test.cc	2026-02-16 16:08:49.625230000 -0800
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2018, 2025 NITK Surathkal
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Authors: Shikha Bakshi <shikhabakshi912@gmail.com>
+ *          Mohit P. Tahiliani <tahiliani@nitk.edu.in>
+ *          Jayesh Akot <akotjayesh@gmail.com>
+ */
+
+/**
+ * @file
+ * @brief Unit test for the TCP Forward Acknowledgment (FACK) implementation.
+ *
+ * This unit test creates a short packet flow and forces four consecutive lost
+ * segments, and verifies that the snd.fack variable is updated to the highest sequence
+ * number present in the incoming SACK blocks, and that its external calculation of
+ * awnd matches the internal state variable.
+ */
+
+#include "tcp-error-model.h"
+#include "tcp-general-test.h"
+
+#include "ns3/assert.h"
+#include "ns3/log.h"
+#include "ns3/node.h"
+#include "ns3/simple-channel.h"
+#include "ns3/tcp-header.h"
+#include "ns3/tcp-tx-buffer.h"
+#include "ns3/test.h"
+
+using namespace ns3;
+
+NS_LOG_COMPONENT_DEFINE("TcpFackTest");
+
+/**
+ * @ingroup internet-test
+ * @brief Test case for Forward Acknowledgment(FACK).
+ *
+ * This test verifies the correctness of FACK-related state variables,
+ * in particular the computation of sndFack and the FACK's inflight(awnd),
+ * ensuring they match the expected behavior of the implementation.
+ */
+class TcpFackTest : public TcpGeneralTest
+{
+  public:
+    /**
+     * @brief Constructor.
+     * @param congControl The TypeId of the congestion control algorithm used.
+     * @param desc Description string for the test case.
+     */
+    TcpFackTest(TypeId congControl, const std::string& desc)
+        : TcpGeneralTest(desc),
+          m_congControlType(congControl)
+
+    {
+        m_congControlTypeId = congControl;
+    }
+
+  private:
+    Ptr<TcpSeqErrorModel> m_errorModel; //!< Error model.
+    uint32_t m_pktDropped{0};           //!< The Number of packets has been dropped.
+    uint32_t m_startSeqToKill{4001};    //!< Sequence number of the first packet to drop.
+    uint32_t m_seqToKill{0};            //!< Sequence number to drop.
+    uint32_t m_pkts{4};                 //!< Number of packets to drop.
+    uint32_t m_pktSize{1000};           //!< Sender Packet Size
+
+    /**
+     * @brief Count number of packets dropped
+     * @param ipH IPv4 header of the dropped packet.
+     * @param tcpH TCP header of the dropped packet.
+     * @param p The dropped packet.
+     */
+    void PktDropped(const Ipv4Header& ipH, const TcpHeader& tcpH, Ptr<const Packet> p)
+    {
+        NS_LOG_FUNCTION(this << ipH << tcpH);
+        m_pktDropped++;
+    }
+
+    void ConfigureEnvironment() override
+    {
+        TcpGeneralTest::ConfigureEnvironment();
+        SetAppPktSize(1000);
+        SetAppPktCount(10); // send 10 packets
+    }
+
+    void ConfigureProperties() override
+    {
+        TcpGeneralTest::ConfigureProperties();
+        SetInitialCwnd(SENDER, 10); // start with 10 segments
+        SetSegmentSize(SENDER, 1000);
+        SetSegmentSize(RECEIVER, 1000);
+    }
+
+    Ptr<ErrorModel> CreateReceiverErrorModel() override
+    {
+        m_errorModel = CreateObject<TcpSeqErrorModel>();
+
+        for (uint32_t i = 0; i < m_pkts; i++)
+        {
+            m_seqToKill = m_startSeqToKill + i * m_pktSize;
+            m_errorModel->AddSeqToKill(SequenceNumber32(m_seqToKill));
+            m_errorModel->SetDropCallback(MakeCallback(&TcpFackTest::PktDropped, this));
+        }
+
+        return m_errorModel;
+    }
+
+    Ptr<TcpSocketMsgBase> CreateSenderSocket(Ptr<Node> node) override
+    {
+        Ptr<TcpSocketMsgBase> sock = TcpGeneralTest::CreateSenderSocket(node);
+        sock->SetAttribute("Fack", BooleanValue(true));
+        return sock;
+    }
+
+    void RcvAck(const Ptr<const TcpSocketState> tcb, const TcpHeader& h, SocketWho who) override
+    {
+        if (tcb->m_cWnd.Get() >= tcb->m_ssThresh)
+        {
+            Ptr<TcpSocketBase> sock = DynamicCast<TcpSocketBase>(GetSenderSocket());
+            uint32_t awnd = tcb->m_highTxMark.Get().GetValue() - sock->GetSndFack() +
+                            sock->GetTxBuffer()->GetRetransmitsCount();
+
+            NS_TEST_EXPECT_MSG_EQ(awnd, tcb->m_fackAwnd.Get(), "AWND is not correctly calculated");
+        }
+    }
+
+    void ProcessedAck(const Ptr<const TcpSocketState> tcb,
+                      const TcpHeader& h,
+                      SocketWho who) override
+    {
+        if (!h.HasOption(TcpOption::SACK))
+        {
+            return;
+        }
+
+        Ptr<const TcpOption> opt = h.GetOption(TcpOption::SACK);
+        auto sackOpt = DynamicCast<const TcpOptionSack>(opt);
+
+        if (!sackOpt)
+        {
+            return;
+        }
+
+        TcpOptionSack::SackList sacks = sackOpt->GetSackList();
+        SequenceNumber32 highest = h.GetAckNumber();
+
+        for (const auto& s : sacks)
+        {
+            if (s.second > highest)
+            {
+                highest = s.second;
+            }
+        }
+        auto sock = DynamicCast<TcpSocketBase>(GetSenderSocket());
+
+        NS_TEST_EXPECT_MSG_EQ(sock->GetSndFack(),
+                              highest.GetValue(),
+                              "snd_fack did not advance to highest SACKed seq");
+    }
+
+  private:
+    TypeId m_congControlType; //!< Congestion control algorithm type used for this test.
+};
+
+/**
+ * @ingroup internet-test
+ *
+ * @brief Test suite for verifying the behavior of the TCP FACK implementation
+ *        under controlled packet loss scenarios.
+ *
+ * Verifies:
+ *  - snd.fack correctly tracks the highest SACKed sequence
+ *  - awnd calculation matches FACK’s tracking
+ */
+
+class TcpFackTestSuite : public TestSuite
+{
+  public:
+    TcpFackTestSuite()
+        : TestSuite("tcp-fack-test", Type::UNIT)
+    {
+        TypeId cca = TcpNewReno::GetTypeId();
+
+        AddTestCase(
+            new TcpFackTest(cca, " Test snd.fack and awnd values after four packets are dropped."),
+            Duration::QUICK);
+    }
+};
+
+/**
+ * @brief Static variable for test initialization
+ */
+static TcpFackTestSuite g_tcpFackTestSuite;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/tcp-header-test.cc ns-3.47/src/internet/test/tcp-header-test.cc
--- ns-3.46/src/internet/test/tcp-header-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/tcp-header-test.cc	2026-02-16 16:08:49.626230000 -0800
@@ -42,7 +42,6 @@
      */
     TcpHeaderGetSetTestCase(std::string name);
 
-  protected:
   private:
     void DoRun() override;
     void DoTeardown() override;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/tcp-ledbat-test.cc ns-3.47/src/internet/test/tcp-ledbat-test.cc
--- ns-3.46/src/internet/test/tcp-ledbat-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/tcp-ledbat-test.cc	2026-02-16 16:08:49.626230000 -0800
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: GPL-2.0-only
  *
  * Author: Ankit Deepak <adadeepak8@gmail.com>
+ * Modified by: S B L Prateek <sblprateek@gmail.com>
  *
  */
 
@@ -33,8 +34,7 @@
      * @param segmentSize segment size
      * @param ssThresh slow start threshold
      * @param segmentsAcked segments acked
-     * @param highTxMark high tx mark
-     * @param lastAckedSeq last acked seq
+     * @param bytesInFlight bytes in flight
      * @param rtt RTT
      * @param name Name of the test
      */
@@ -42,34 +42,27 @@
                        uint32_t segmentSize,
                        uint32_t ssThresh,
                        uint32_t segmentsAcked,
-                       SequenceNumber32 highTxMark,
-                       SequenceNumber32 lastAckedSeq,
+                       uint32_t bytesInFlight,
                        Time rtt,
                        const std::string& name);
 
   private:
     void DoRun() override;
-    /**
-     * @brief Execute the test
-     */
-    void ExecuteTest();
 
-    uint32_t m_cWnd;                 //!< cWnd
-    uint32_t m_segmentSize;          //!< segment size
-    uint32_t m_segmentsAcked;        //!< segments acked
-    uint32_t m_ssThresh;             //!< ss thresh
-    Time m_rtt;                      //!< rtt
-    SequenceNumber32 m_highTxMark;   //!< high tx mark
-    SequenceNumber32 m_lastAckedSeq; //!< last acked seq
-    Ptr<TcpSocketState> m_state;     //!< state
+    uint32_t m_cWnd;             //!< cWnd
+    uint32_t m_segmentSize;      //!< segment size
+    uint32_t m_segmentsAcked;    //!< segments acked
+    uint32_t m_ssThresh;         //!< ss thresh
+    Time m_rtt;                  //!< rtt
+    uint32_t m_bytesInFlight;    //!< bytes in flight
+    Ptr<TcpSocketState> m_state; //!< state
 };
 
 TcpLedbatToNewReno::TcpLedbatToNewReno(uint32_t cWnd,
                                        uint32_t segmentSize,
                                        uint32_t ssThresh,
                                        uint32_t segmentsAcked,
-                                       SequenceNumber32 highTxMark,
-                                       SequenceNumber32 lastAckedSeq,
+                                       uint32_t bytesInFlight,
                                        Time rtt,
                                        const std::string& name)
     : TestCase(name),
@@ -78,35 +71,24 @@
       m_segmentsAcked(segmentsAcked),
       m_ssThresh(ssThresh),
       m_rtt(rtt),
-      m_highTxMark(highTxMark),
-      m_lastAckedSeq(lastAckedSeq)
+      m_bytesInFlight(bytesInFlight)
 {
 }
 
 void
 TcpLedbatToNewReno::DoRun()
 {
-    Simulator::Schedule(Seconds(0), &TcpLedbatToNewReno::ExecuteTest, this);
-    Simulator::Run();
-    Simulator::Destroy();
-}
-
-void
-TcpLedbatToNewReno::ExecuteTest()
-{
     m_state = CreateObject<TcpSocketState>();
     m_state->m_cWnd = m_cWnd;
     m_state->m_ssThresh = m_ssThresh;
     m_state->m_segmentSize = m_segmentSize;
-    m_state->m_highTxMark = m_highTxMark;
-    m_state->m_lastAckedSeq = m_lastAckedSeq;
+    m_state->m_bytesInFlight = m_bytesInFlight;
 
     Ptr<TcpSocketState> state = CreateObject<TcpSocketState>();
     state->m_cWnd = m_cWnd;
     state->m_ssThresh = m_ssThresh;
     state->m_segmentSize = m_segmentSize;
-    state->m_highTxMark = m_highTxMark;
-    state->m_lastAckedSeq = m_lastAckedSeq;
+    state->m_bytesInFlight = m_bytesInFlight;
 
     Ptr<TcpLedbat> cong = CreateObject<TcpLedbat>();
     cong->IncreaseWindow(m_state, m_segmentsAcked);
@@ -117,118 +99,48 @@
     NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(),
                           state->m_cWnd.Get(),
                           "cWnd has not updated correctly");
+
+    Simulator::Destroy();
 }
 
 /**
  * @ingroup internet-test
  *
- * @brief Test to validate cWnd increment in LEDBAT
+ * @brief Test to validate cWnd increment/decrement in LEDBAT Congestion Avoidance Phase
  */
-class TcpLedbatIncrementTest : public TestCase
+class TcpLedbatCongestionAvoidanceTest : public TestCase
 {
   public:
     /**
-     * @brief Constructor
-     *
-     * @param cWnd congestion window
-     * @param segmentSize segment size
-     * @param ssThresh slow start threshold
-     * @param segmentsAcked segments acked
-     * @param highTxMark high tx mark
-     * @param lastAckedSeq last acked seq
-     * @param rtt RTT
-     * @param name Name of the test
-     */
-    TcpLedbatIncrementTest(uint32_t cWnd,
-                           uint32_t segmentSize,
-                           uint32_t ssThresh,
-                           uint32_t segmentsAcked,
-                           SequenceNumber32 highTxMark,
-                           SequenceNumber32 lastAckedSeq,
-                           Time rtt,
-                           const std::string& name);
-
-  private:
-    void DoRun() override;
-    /**
-     * @brief Execute the test
+     * @brief Type of expected CWND behavior in LEDBAT congestion avoidance
      */
-    void ExecuteTest();
-
-    uint32_t m_cWnd;                 //!< cWnd
-    uint32_t m_segmentSize;          //!< segment size
-    uint32_t m_segmentsAcked;        //!< segments acked
-    uint32_t m_ssThresh;             //!< ss thresh
-    Time m_rtt;                      //!< rtt
-    SequenceNumber32 m_highTxMark;   //!< high tx mark
-    SequenceNumber32 m_lastAckedSeq; //!< last acked seq
-    Ptr<TcpSocketState> m_state;     //!< state
-};
-
-TcpLedbatIncrementTest::TcpLedbatIncrementTest(uint32_t cWnd,
-                                               uint32_t segmentSize,
-                                               uint32_t ssThresh,
-                                               uint32_t segmentsAcked,
-                                               SequenceNumber32 highTxMark,
-                                               SequenceNumber32 lastAckedSeq,
-                                               Time rtt,
-                                               const std::string& name)
-    : TestCase(name),
-      m_cWnd(cWnd),
-      m_segmentSize(segmentSize),
-      m_segmentsAcked(segmentsAcked),
-      m_ssThresh(ssThresh),
-      m_rtt(rtt),
-      m_highTxMark(highTxMark),
-      m_lastAckedSeq(lastAckedSeq)
-{
-}
-
-void
-TcpLedbatIncrementTest::DoRun()
-{
-    Simulator::Schedule(Seconds(0), &TcpLedbatIncrementTest::ExecuteTest, this);
-    Simulator::Run();
-    Simulator::Destroy();
-}
-
-void
-TcpLedbatIncrementTest::ExecuteTest()
-{
-    m_state = CreateObject<TcpSocketState>();
-    m_state->m_cWnd = m_cWnd;
-    m_state->m_ssThresh = m_ssThresh;
-    m_state->m_segmentSize = m_segmentSize;
-    m_state->m_highTxMark = m_highTxMark;
-    m_state->m_lastAckedSeq = m_lastAckedSeq;
-
-    Ptr<TcpLedbat> cong = CreateObject<TcpLedbat>();
-    cong->SetAttribute("SSParam", StringValue("no"));
-    cong->SetAttribute("NoiseFilterLen", UintegerValue(1));
-
-    m_state->m_rcvTimestampValue = 2;
-    m_state->m_rcvTimestampEchoReply = 1;
-    cong->PktsAcked(m_state, m_segmentsAcked, m_rtt);
-
-    m_state->m_rcvTimestampValue = 7;
-    m_state->m_rcvTimestampEchoReply = 4;
-    cong->PktsAcked(m_state, m_segmentsAcked, m_rtt);
-
-    cong->IncreaseWindow(m_state, m_segmentsAcked);
-
-    m_cWnd = m_cWnd + ((0.98 * m_segmentsAcked * m_segmentSize * m_segmentSize) / m_cWnd);
-
-    NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(), m_cWnd, "cWnd has not updated correctly");
-}
+    enum TestType
+    {
+        /**
+         * To enable check for CWND increases because the measured queue delay is
+         * below the target delay (positive offset).
+         */
+        INCREMENT_TEST,
+
+        /**
+         * To enable check that CWND increase is limited by the maximum allowed CWND,
+         * computed from flight size before ack (bytesInFlight + segmentsAcked * segmentSize).
+         */
+        MAX_CWND_TEST,
+
+        /**
+         * To enable check for CWND decreases because the measured queue delay is
+         * above the target delay (negative offset).
+         */
+        DECREMENT_TEST,
+
+        /**
+         * To enable check that CWND decrease is limited by the minimum CWND constraint
+         * (MinCwnd × segmentSize).
+         */
+        MIN_CWND_TEST
+    };
 
-/**
- * @ingroup internet-test
- *
- * @brief Test to validate cWnd decrement in LEDBAT
- */
-class TcpLedbatDecrementTest : public TestCase
-{
-  public:
     /**
      * @brief Constructor
      *
@@ -236,94 +148,212 @@
      * @param segmentSize segment size
      * @param ssThresh slow start threshold
      * @param segmentsAcked segments acked
-     * @param highTxMark high tx mark
-     * @param lastAckedSeq last acked seq
+     * @param bytesInFlight bytes in flight
      * @param rtt RTT
+     * @param rcvTimestampValue Receive timestamp for delay estimation
+     * @param currentRcvTimestampEchoReply Echo reply timestamp for current delay
+     * @param testType Expected test behavior
      * @param name Name of the test
      */
-    TcpLedbatDecrementTest(uint32_t cWnd,
-                           uint32_t segmentSize,
-                           uint32_t ssThresh,
-                           uint32_t segmentsAcked,
-                           SequenceNumber32 highTxMark,
-                           SequenceNumber32 lastAckedSeq,
-                           Time rtt,
-                           const std::string& name);
+
+    TcpLedbatCongestionAvoidanceTest(uint32_t cWnd,
+                                     uint32_t segmentSize,
+                                     uint32_t ssThresh,
+                                     uint32_t segmentsAcked,
+                                     uint32_t bytesInFlight,
+                                     Time rtt,
+                                     uint32_t rcvTimestampValue,
+                                     uint32_t currentRcvTimestampEchoReply,
+                                     TestType testType,
+                                     const std::string& name);
 
   private:
     void DoRun() override;
-    /**
-     * @brief Execute the test
-     */
-    void ExecuteTest();
 
-    uint32_t m_cWnd;                 //!< cWnd
-    uint32_t m_segmentSize;          //!< segment size
-    uint32_t m_segmentsAcked;        //!< segments acked
-    uint32_t m_ssThresh;             //!< ss thresh
-    Time m_rtt;                      //!< rtt
-    SequenceNumber32 m_highTxMark;   //!< high tx mark
-    SequenceNumber32 m_lastAckedSeq; //!< last acked seq
-    Ptr<TcpSocketState> m_state;     //!< state
+    uint32_t m_cWnd;                         //!< cWnd
+    uint32_t m_segmentSize;                  //!< segment size
+    uint32_t m_segmentsAcked;                //!< segments acked
+    uint32_t m_ssThresh;                     //!< ss thresh
+    Time m_rtt;                              //!< rtt
+    uint32_t m_bytesInFlight;                //!< bytes in flight (after ACK)
+    uint32_t m_rcvTimestampValue;            //!< received timestamp value
+    uint32_t m_currentRcvTimestampEchoReply; //!< current echoed timestamp value
+    TestType m_testType;                     //!< test type
+    Ptr<TcpSocketState> m_state;             //!< state
 };
 
-TcpLedbatDecrementTest::TcpLedbatDecrementTest(uint32_t cWnd,
-                                               uint32_t segmentSize,
-                                               uint32_t ssThresh,
-                                               uint32_t segmentsAcked,
-                                               SequenceNumber32 highTxMark,
-                                               SequenceNumber32 lastAckedSeq,
-                                               Time rtt,
-                                               const std::string& name)
+TcpLedbatCongestionAvoidanceTest::TcpLedbatCongestionAvoidanceTest(
+    uint32_t cWnd,
+    uint32_t segmentSize,
+    uint32_t ssThresh,
+    uint32_t segmentsAcked,
+    uint32_t bytesInFlight,
+    Time rtt,
+    uint32_t rcvTimestampValue,
+    uint32_t currentRcvTimestampEchoReply,
+    TestType testType,
+    const std::string& name)
     : TestCase(name),
       m_cWnd(cWnd),
       m_segmentSize(segmentSize),
       m_segmentsAcked(segmentsAcked),
       m_ssThresh(ssThresh),
       m_rtt(rtt),
-      m_highTxMark(highTxMark),
-      m_lastAckedSeq(lastAckedSeq)
-{
-}
-
-void
-TcpLedbatDecrementTest::DoRun()
+      m_bytesInFlight(bytesInFlight),
+      m_rcvTimestampValue(rcvTimestampValue),
+      m_currentRcvTimestampEchoReply(currentRcvTimestampEchoReply),
+      m_testType(testType)
 {
-    Simulator::Schedule(Seconds(0), &TcpLedbatDecrementTest::ExecuteTest, this);
-    Simulator::Run();
-    Simulator::Destroy();
 }
 
 void
-TcpLedbatDecrementTest::ExecuteTest()
+TcpLedbatCongestionAvoidanceTest::DoRun()
 {
-    UintegerValue minCwnd;
     m_state = CreateObject<TcpSocketState>();
     m_state->m_cWnd = m_cWnd;
     m_state->m_ssThresh = m_ssThresh;
     m_state->m_segmentSize = m_segmentSize;
-    m_state->m_highTxMark = m_highTxMark;
-    m_state->m_lastAckedSeq = m_lastAckedSeq;
+    m_state->m_bytesInFlight = m_bytesInFlight;
 
     Ptr<TcpLedbat> cong = CreateObject<TcpLedbat>();
+    cong->SetAttribute("Gain", DoubleValue(1.0)); // Default LEDBAT GAIN value as per RFC 6817
+    cong->SetAttribute("AllowedIncrease",
+                       DoubleValue(1.0)); // Default LEDBAT Allowed Increase value as per RFC 6817
     cong->SetAttribute("SSParam", StringValue("no"));
     cong->SetAttribute("NoiseFilterLen", UintegerValue(1));
-    cong->GetAttribute("MinCwnd", minCwnd);
+    cong->SetAttribute("MinCwnd", UintegerValue(2));
 
-    m_state->m_rcvTimestampValue = 2;
-    m_state->m_rcvTimestampEchoReply = 1;
+    // Establish base delay measurement (Base Delay = 40 - 20 = 20ms)
+    m_state->m_rcvTimestampValue = 40;
+    m_state->m_rcvTimestampEchoReply = 20;
     cong->PktsAcked(m_state, m_segmentsAcked, m_rtt);
 
-    m_state->m_rcvTimestampValue = 205;
-    m_state->m_rcvTimestampEchoReply = 6;
+    // Set current measured delay based on test parameters
+    m_state->m_rcvTimestampValue = m_rcvTimestampValue;
+    m_state->m_rcvTimestampEchoReply = m_currentRcvTimestampEchoReply;
     cong->PktsAcked(m_state, m_segmentsAcked, m_rtt);
 
-    cong->IncreaseWindow(m_state, m_segmentsAcked);
+    uint32_t oldCwnd = m_state->m_cWnd.Get();
+
+    // LEDBAT adjusts cwnd based on queuing delay relative to target:
+    // - When queuing delay < target: cwnd increases (positive offset)
+    // - When queuing delay > target: cwnd decreases (negative offset)
+    // - cwnd is bounded by max (flightSizeBeforeAck + ALLOWED_INCREASE * MSS)
+    //   and min (minCwnd * MSS)
 
-    m_cWnd = m_cWnd - ((0.98 * m_segmentsAcked * m_segmentSize * m_segmentSize) / m_cWnd);
-    m_cWnd = std::max(m_cWnd, static_cast<uint32_t>(m_segmentSize * minCwnd.Get()));
+    switch (m_testType)
+    {
+    case INCREMENT_TEST:
+        // Timestamps: rcvTimestamp=80, echoReply=20
+        // currentDelay = 80 - 20 = 60ms
+        // baseDelay = 20ms (established earlier: 40 - 20)
+        // queueDelay = currentDelay - baseDelay = 60 - 20 = 40ms
+        // offset = target - queueDelay = 100 - 40 = 60ms
+        // offset *= gain: offset = 60 * 1.0 = 60
+        // oldCwnd = 8676 bytes
+        // m_sndCwndCnt = offset * segmentsAcked * segmentSize
+        //              = 60 * 2 * 1446 = 173520
+        // inc = m_sndCwndCnt / (target * oldCwnd)
+        //     = 173520 / (100 * 8676) = 0.2
+        // newCwnd = oldCwnd + (inc * segmentSize)
+        //         = 8676 + (0.2 * 1446) = 8676 + 289.2 = 8965.2 ≈ 8965
+        // flightSizeBeforeAck = bytesInFlight + segmentsAcked * segmentSize
+        //                     = 5784 + 2 * 1446 = 8676
+        // maxCwnd = flightSizeBeforeAck + allowedIncrease * segmentSize
+        //         = 8676 + 1.0 * 1446 = 10122
+        // newCwnd = min(8965, 10122) = 8965
+        // newCwnd = max(8965, 2892) = 8965
+        // Final newCwnd = 8965 bytes
+        // increment = inc * segmentSize = 0.2 * 1446 = 289
+        cong->IncreaseWindow(m_state, m_segmentsAcked);
+        NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(),
+                              oldCwnd + 289,
+                              "cWnd of 8676 should increase by 289");
+        break;
+
+    case MAX_CWND_TEST:
+        // Timestamps: rcvTimestamp=80, echoReply=20
+        // currentDelay = 80 - 20 = 60ms
+        // baseDelay = 20ms (established earlier: 40 - 20)
+        // queueDelay = currentDelay - baseDelay = 60 - 20 = 40ms
+        // offset = target - queueDelay = 100 - 40 = 60ms
+        // offset *= gain: offset = 60 * 1.0 = 60
+        // oldCwnd = 8676 bytes, bytesInFlight = 4338 bytes
+        // m_sndCwndCnt = offset * segmentsAcked * segmentSize
+        //              = 60 * 2 * 1446 = 173520
+        // inc = m_sndCwndCnt / (target * oldCwnd)
+        //     = 173520 / (100 * 8676) = 0.2
+        // newCwnd = oldCwnd + (inc * segmentSize)
+        //         = 8676 + (0.2 * 1446) = 8676 + 289.2 = 8965.2 ≈ 8965
+        // flightSizeBeforeAck = bytesInFlight + segmentsAcked * segmentSize
+        //                     = 4338 + 2 * 1446 = 7230
+        // maxCwnd = flightSizeBeforeAck + allowedIncrease * segmentSize
+        //         = 7230 + 1.0 * 1446 = 8676
+        // Final newCwnd = min(8965, 8676) = 8676 bytes (clamped by maxCwnd)
+        cong->IncreaseWindow(m_state, m_segmentsAcked);
+        NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(), 8676, "cWnd should be limited by maxCwnd");
+        break;
+
+    case DECREMENT_TEST:
+        // Timestamps: rcvTimestamp=160, echoReply=20
+        // currentDelay = 160 - 20 = 140ms
+        // baseDelay = 20ms (established earlier: 40 - 20)
+        // queueDelay = currentDelay - baseDelay = 140 - 20 = 120ms
+        // offset = target - queueDelay = 100 - 120 = -20ms
+        // offset *= gain: offset = -20 * 1.0 = -20
+        // oldCwnd = 8676 bytes
+        // m_sndCwndCnt = offset * segmentsAcked * segmentSize
+        //              = -20 * 2 * 1446 = -57840
+        // inc = m_sndCwndCnt / (target * oldCwnd)
+        //     = -57840 / (100 * 8676) = -0.06667
+        // newCwnd = oldCwnd + (inc * segmentSize)
+        //         = 8676 + (-0.06667 * 1446) = 8676 - 96.4 = 8579.6 ≈ 8579
+        // flightSizeBeforeAck = bytesInFlight + segmentsAcked * segmentSize
+        //                     = 5784 + 2 * 1446 = 8676
+        // maxCwnd = flightSizeBeforeAck + allowedIncrease * segmentSize
+        //         = 8676 + 1.0 * 1446 = 10122
+        // newCwnd = min(8579, 10122) = 8579
+        // newCwnd = max(8579, 2892) = 8579
+        // Final newCwnd = 8579 bytes
+        // decrement = inc * segmentSize = -0.06667 * 1446 = -97
+        cong->IncreaseWindow(m_state, m_segmentsAcked);
+        NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(),
+                              oldCwnd - 97,
+                              "cWnd of 8676 should decrease by 97");
+        break;
+
+    case MIN_CWND_TEST:
+        // Timestamps: rcvTimestamp=160, echoReply=20
+        // currentDelay = 160 - 20 = 140ms
+        // baseDelay = 20ms (established earlier: 40 - 20)
+        // queueDelay = currentDelay - baseDelay = 140 - 20 = 120ms
+        // offset = target - queueDelay = 100 - 120 = -20ms
+        // offset *= gain: offset = -20 * 1.0 = -20
+        // oldCwnd = 2892 bytes, minCwnd = 2 segments = 2892 bytes
+        // m_sndCwndCnt = offset * segmentsAcked * segmentSize
+        //              = -20 * 2 * 1446 = -57840
+        // inc = m_sndCwndCnt / (target * oldCwnd)
+        //     = -57840 / (100 * 2892) = -0.2
+        // newCwnd = oldCwnd + (inc * segmentSize)
+        //         = 2892 + (-0.2 * 1446) = 2892 - 289.2 = 2602.8 ≈ 2602
+        // flightSizeBeforeAck = bytesInFlight + segmentsAcked * segmentSize
+        //                     = 5784 + 2 * 1446 = 8676
+        // maxCwnd = flightSizeBeforeAck + allowedIncrease * segmentSize
+        //         = 8676 + 1.0 * 1446 = 10122
+        // newCwnd = min(2602, 10122) = 2602
+        // Final newCwnd = max(2602, 2892) = 2892 bytes (clamped by minCwnd)
+        cong->IncreaseWindow(m_state, m_segmentsAcked);
+        NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(),
+                              2 * 1446,
+                              "cWnd should be clamped to minCwnd (2892)");
+        break;
+
+    default:
+        NS_FATAL_ERROR("Unknown Test Type");
+        break;
+    }
 
-    NS_TEST_ASSERT_MSG_EQ(m_state->m_cWnd.Get(), m_cWnd, "cWnd has not updated correctly");
+    Simulator::Destroy();
 }
 
 /**
@@ -341,8 +371,7 @@
                                            1446,
                                            4 * 1446,
                                            2,
-                                           SequenceNumber32(4753),
-                                           SequenceNumber32(3216),
+                                           1446,
                                            MilliSeconds(100),
                                            "LEDBAT falls to New Reno for slowstart"),
                     TestCase::Duration::QUICK);
@@ -350,28 +379,57 @@
                                            1446,
                                            2 * 1446,
                                            2,
-                                           SequenceNumber32(4753),
-                                           SequenceNumber32(3216),
+                                           1446,
                                            MilliSeconds(100),
                                            "LEDBAT falls to New Reno if timestamps are not found"),
                     TestCase::Duration::QUICK);
-        AddTestCase(new TcpLedbatIncrementTest(2 * 1446,
-                                               1446,
-                                               4 * 1446,
-                                               2,
-                                               SequenceNumber32(4753),
-                                               SequenceNumber32(3216),
-                                               MilliSeconds(100),
-                                               "LEDBAT increment test"),
+        AddTestCase(
+            new TcpLedbatCongestionAvoidanceTest(6 * 1446,
+                                                 1446,
+                                                 4 * 1446,
+                                                 2,
+                                                 4 * 1446,
+                                                 MilliSeconds(100),
+                                                 80,
+                                                 20,
+                                                 TcpLedbatCongestionAvoidanceTest::INCREMENT_TEST,
+                                                 "LEDBAT Congestion Avoidance Increment"),
+            TestCase::Duration::QUICK);
+        AddTestCase(new TcpLedbatCongestionAvoidanceTest(
+                        6 * 1446,
+                        1446,
+                        4 * 1446,
+                        2,
+                        3 * 1446,
+                        MilliSeconds(100),
+                        80,
+                        20,
+                        TcpLedbatCongestionAvoidanceTest::MAX_CWND_TEST,
+                        "LEDBAT Congestion Avoidance Increment (Limited by max_cwnd)"),
                     TestCase::Duration::QUICK);
-        AddTestCase(new TcpLedbatDecrementTest(2 * 1446,
-                                               1446,
-                                               4 * 1446,
-                                               2,
-                                               SequenceNumber32(4753),
-                                               SequenceNumber32(3216),
-                                               MilliSeconds(100),
-                                               "LEDBAT decrement test"),
+        AddTestCase(
+            new TcpLedbatCongestionAvoidanceTest(6 * 1446,
+                                                 1446,
+                                                 4 * 1446,
+                                                 2,
+                                                 4 * 1446,
+                                                 MilliSeconds(100),
+                                                 160,
+                                                 20,
+                                                 TcpLedbatCongestionAvoidanceTest::DECREMENT_TEST,
+                                                 "LEDBAT Congestion Avoidance Decrement"),
+            TestCase::Duration::QUICK);
+        AddTestCase(new TcpLedbatCongestionAvoidanceTest(
+                        2 * 1446,
+                        1446,
+                        4 * 1446,
+                        2,
+                        4 * 1446,
+                        MilliSeconds(100),
+                        160,
+                        20,
+                        TcpLedbatCongestionAvoidanceTest::MIN_CWND_TEST,
+                        "LEDBAT Congestion Avoidance Decrement (Limited by min_cwnd)"),
                     TestCase::Duration::QUICK);
     }
 };
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet/test/tcp-test.cc ns-3.47/src/internet/test/tcp-test.cc
--- ns-3.46/src/internet/test/tcp-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet/test/tcp-test.cc	2026-02-16 16:08:49.627230000 -0800
@@ -432,9 +432,9 @@
 void
 TcpTestCase::SetupDefaultSim6()
 {
-    Ipv6Prefix prefix = Ipv6Prefix(64);
-    Ipv6Address ipaddr0 = Ipv6Address("2001:0100:f00d:cafe::1");
-    Ipv6Address ipaddr1 = Ipv6Address("2001:0100:f00d:cafe::2");
+    auto prefix = Ipv6Prefix(64);
+    auto ipaddr0 = Ipv6Address("2001:0100:f00d:cafe::1");
+    auto ipaddr1 = Ipv6Address("2001:0100:f00d:cafe::2");
     Ptr<Node> node0 = CreateInternetNode6();
     Ptr<Node> node1 = CreateInternetNode6();
     Ptr<SimpleNetDevice> dev0 = AddSimpleNetDevice6(node0, ipaddr0, prefix);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/doc/internet-apps.rst ns-3.47/src/internet-apps/doc/internet-apps.rst
--- ns-3.46/src/internet-apps/doc/internet-apps.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/doc/internet-apps.rst	2026-02-16 16:08:49.603230000 -0800
@@ -402,10 +402,17 @@
 Each client and server has a unique identifier (DHCP Unique Identifier), which is used to identify the client and server in the message exchange.
 There are four types of DUIDs that can be used in DHCPv6:
 
-1. DUID-LLT (Link-layer Address plus Time): The DUID-LLT consists of a 32-bit timestamp and a variable-length link layer address. Not supported in this implementation.
-2. Vendor-assigned unique ID based on Enterprise Number: This consists of the vendor's enterprise number and an identifier whose source is defined by the vendor. Not supported in this implementation.
-3. **DUID-LL (Link-layer address)**: DUID-LL consists of a variable-length link-layer address as the identifier. Supported in this implementation.
-4. UUID (Universally unique identifier): This should be an identifier that is persistent across system restarts and reconfigurations. Not supported in this implementation.
+1. **DUID-LLT (Link-layer Address plus Time)**: The DUID-LLT consists of a 32-bit timestamp and a variable-length link layer address.
+2. **DUID-EN (Vendor-assigned unique ID based on Enterprise Number)**: This consists of the vendor's enterprise number and an identifier whose source is defined by the vendor.
+3. **DUID-LL (Link-layer address)**: DUID-LL consists of a variable-length link-layer address as the identifier.
+4. **DUID-UUID (Universally unique identifier)**: This should be an identifier that is persistent across system restarts and reconfigurations.
+
+In the current implementation, the DUID is created as follows:
+
+* DUID-LLT and DUID-LL: the identifier is created by selecting the NetDevices with the longest MAC address length, and among them, the one with the smallest index.
+* DUID-LLT: the creation time is taken from the simulation time.
+* DUID-EN: the Enterprise number is 0xf00dcafe (arbitrary number), and the identifier is based on the Node ID.
+* DUID-UUID: the UUID is based on the Node ID.
 
 Usage
 =====
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/examples/dhcp6-example.cc ns-3.47/src/internet-apps/examples/dhcp6-example.cc
--- ns-3.46/src/internet-apps/examples/dhcp6-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/examples/dhcp6-example.cc	2026-02-16 16:08:49.603230000 -0800
@@ -69,6 +69,7 @@
 #include "ns3/applications-module.h"
 #include "ns3/core-module.h"
 #include "ns3/csma-module.h"
+#include "ns3/enum.h"
 #include "ns3/internet-apps-module.h"
 #include "ns3/internet-module.h"
 #include "ns3/mobility-module.h"
@@ -169,6 +170,13 @@
     dhcp6Helper.SetServerAttribute("RebindTime", StringValue("16s"));
     dhcp6Helper.SetServerAttribute("PreferredLifetime", StringValue("18s"));
     dhcp6Helper.SetServerAttribute("ValidLifetime", StringValue("20s"));
+    // We set an arbitrary DUID type for the server.
+    // There is no universal standard for the server's DUID type.
+    dhcp6Helper.SetServerAttribute("DuidType", StringValue("LLT"));
+    // We set an arbitrary DUID type for the client.
+    // There is no universal standard for the client's DUID type,
+    // but usually OSes setup the DUID during the OS installation.
+    dhcp6Helper.SetClientAttribute("DuidType", StringValue("UUID"));
 
     // DHCPv6 clients
     NodeContainer nodes = NodeContainer(nonRouterNodes.Get(1), nonRouterNodes.Get(2));
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/examples/ping-example.cc ns-3.47/src/internet-apps/examples/ping-example.cc
--- ns-3.46/src/internet-apps/examples/ping-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/examples/ping-example.cc	2026-02-16 16:08:49.603230000 -0800
@@ -6,60 +6,66 @@
  * Author: Chandrakant Jena <chandrakant.barcelona@gmail.com>
  */
 
-// Example to demonstrate the working of the Ping Application
-// Network topology:
-// A ------------------------------ B ------------------------------ C
-//             100 Mbps                           100 Mbps
-//               5 ms (one way)                     5 ms (one way)
-// IPv4 addresses:
-// 10.1.1.1    <->         10.1.1.2 / 10.1.2.1     <->         10.1.2.2
-//
-// IPv6 addresses:
-// 2001:1::200:ff:fe00:1
-//     <->    2001:1::200:ff:fe00:2 / 2001:1:0:1:200:ff:fe00:3
-//                                       <-> 2001:1:0:1:200:ff:fe00:4
-//
-// The topology has three nodes interconnected by two point-to-point links.
-// Each link has 5 ms one-way delay, for a round-trip propagation delay
-// of 20 ms.  The transmission rate on each link is 100 Mbps.  The routing
-// between links is enabled by ns-3's NixVector routing.
-//
-// By default, this program will send 5 pings from node A to node C using IPv6.
-// When using IPv6, the output will look like this:
-//
-// PING 2001:1:0:1:200:ff:fe00:4 - 56 bytes of data; 104 bytes including ICMP and IPv6 headers.
-// 64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=0 ttl=63 time=20.033 ms
-// 64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=1 ttl=63 time=20.033 ms
-// 64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=2 ttl=63 time=20.033 ms
-// 64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=3 ttl=63 time=20.033 ms
-// 64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=4 ttl=63 time=20.033 ms
-//
-// --- 2001:1:0:1:200:ff:fe00:4 ping statistics ---
-// 5 packets transmitted, 5 received, 0% packet loss, time 4020ms
-// rtt min/avg/max/mdev = 20/20/20/0 ms
-//
-// When using IPv4, the output will look like this:
-//
-//   PING 10.1.2.2 - 56 bytes of data; 84 bytes including ICMP and IPv4 headers.
-//   64 bytes from (10.1.2.2): icmp_seq=0 ttl=63 time=20.027 ms
-//   64 bytes from (10.1.2.2): icmp_seq=1 ttl=63 time=20.027 ms
-//   64 bytes from (10.1.2.2): icmp_seq=2 ttl=63 time=20.027 ms
-//   64 bytes from (10.1.2.2): icmp_seq=3 ttl=63 time=20.027 ms
-//   64 bytes from (10.1.2.2): icmp_seq=4 ttl=63 time=20.027 ms
-//   --- 10.1.2.2 ping statistics ---
-//   5 packets transmitted, 5 received, 0% packet loss, time 4020ms
-//   rtt min/avg/max/mdev = 20/20/20/0 ms
-//
-// The example program will also produce four pcap traces (one for each
-// NetDevice in the scenario) that can be viewed using tcpdump or Wireshark.
-//
-// Other program options include options to change the destination and
-// source addresses, number of packets (count), packet size, interval,
-// and whether to enable logging (if logging is enabled in the build).
-//
-// The Ping application in this example starts at simulation time 1 and will
-// stop either at simulation time 50 or once 'Count' pings have been responded
-// to, whichever comes first.
+/**
+ * @file
+ * Example to demonstrate the working of the Ping Application
+ * Network topology:
+ * @verbatim
+   A ------------------------------ B ------------------------------ C
+               100 Mbps                           100 Mbps
+                 5 ms (one way)                     5 ms (one way)
+   IPv4 addresses:
+   10.1.1.1    <->         10.1.1.2 / 10.1.2.1     <->         10.1.2.2
+
+   IPv6 addresses:
+   2001:1::200:ff:fe00:1
+       <->    2001:1::200:ff:fe00:2 / 2001:1:0:1:200:ff:fe00:3
+                                          <-> 2001:1:0:1:200:ff:fe00:4
+   @endverbatim
+ *
+ * The topology has three nodes interconnected by two point-to-point links.
+ * Each link has 5 ms one-way delay, for a round-trip propagation delay
+ * of 20 ms.  The transmission rate on each link is 100 Mbps.  The routing
+ * between links is enabled by ns-3's NixVector routing.
+ *
+ * By default, this program will send 5 pings from node A to node C using IPv6.
+ * When using IPv6, the output will look like this:
+ * @verbatim
+   PING 2001:1:0:1:200:ff:fe00:4 - 56 bytes of data; 104 bytes including ICMP and IPv6 headers.
+   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=0 ttl=63 time=20.033 ms
+   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=1 ttl=63 time=20.033 ms
+   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=2 ttl=63 time=20.033 ms
+   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=3 ttl=63 time=20.033 ms
+   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=4 ttl=63 time=20.033 ms
+   --- 2001:1:0:1:200:ff:fe00:4 ping statistics ---
+   5 packets transmitted, 5 received, 0% packet loss, time 4020ms
+   rtt min/avg/max/mdev = 20/20/20/0 ms
+   @endverbatim
+ *
+ * When using IPv4, the output will look like this:
+ * @verbatim
+   PING 10.1.2.2 - 56 bytes of data; 84 bytes including ICMP and IPv4 headers.
+   64 bytes from (10.1.2.2): icmp_seq=0 ttl=63 time=20.027 ms
+   64 bytes from (10.1.2.2): icmp_seq=1 ttl=63 time=20.027 ms
+   64 bytes from (10.1.2.2): icmp_seq=2 ttl=63 time=20.027 ms
+   64 bytes from (10.1.2.2): icmp_seq=3 ttl=63 time=20.027 ms
+   64 bytes from (10.1.2.2): icmp_seq=4 ttl=63 time=20.027 ms
+   --- 10.1.2.2 ping statistics ---
+   5 packets transmitted, 5 received, 0% packet loss, time 4020ms
+   rtt min/avg/max/mdev = 20/20/20/0 ms
+   @endverbatim
+ *
+ * The example program will also produce four pcap traces (one for each
+ * NetDevice in the scenario) that can be viewed using tcpdump or Wireshark.
+ *
+ * Other program options include options to change the destination and
+ * source addresses, number of packets (count), packet size, interval,
+ * and whether to enable logging (if logging is enabled in the build).
+ *
+ * The Ping application in this example starts at simulation time 1 and will
+ * stop either at simulation time 50 or once 'Count' pings have been responded
+ * to, whichever comes first.
+ */
 
 #include "ns3/core-module.h"
 #include "ns3/internet-apps-module.h"
@@ -69,6 +75,7 @@
 #include "ns3/point-to-point-module.h"
 
 #include <fstream>
+#include <optional>
 
 using namespace ns3;
 
@@ -86,6 +93,10 @@
     std::string sourceStr;
     Address source;
     bool useIpv6{true};
+    std::optional<Ipv4Address> v4Dst;
+    std::optional<Ipv6Address> v6Dst;
+    std::optional<Ipv4Address> v4Src;
+    std::optional<Ipv6Address> v6Src;
 
     GlobalValue::Bind("ChecksumEnabled", BooleanValue(true));
 
@@ -104,39 +115,48 @@
 
     if (!destinationStr.empty())
     {
-        Ipv4Address v4Dst(destinationStr.c_str());
-        Ipv6Address v6Dst(destinationStr.c_str());
-        if (v4Dst.IsInitialized())
+        if (Ipv4Address::CheckCompatible(destinationStr))
         {
+            v4Dst = Ipv4Address(destinationStr.c_str());
             useIpv6 = false;
-            destination = v4Dst;
+            destination = v4Dst.value();
         }
-        else if (v6Dst.IsInitialized())
+        else if (Ipv6Address::CheckCompatible(destinationStr))
         {
+            v6Dst = Ipv6Address(destinationStr.c_str());
             useIpv6 = true;
-            destination = v6Dst;
+            destination = v6Dst.value();
+        }
+        else
+        {
+            NS_ABORT_MSG("Can't parse destination address " << destinationStr);
         }
     }
 
     if (!sourceStr.empty())
     {
-        Ipv4Address v4Src(sourceStr.c_str());
-        Ipv6Address v6Src(sourceStr.c_str());
-        if (v4Src.IsInitialized())
+        if (Ipv4Address::CheckCompatible(sourceStr))
         {
-            source = v4Src;
+            v4Src = Ipv4Address(sourceStr.c_str());
+            useIpv6 = false;
+            source = v4Src.value();
         }
-        else if (v6Src.IsInitialized())
+        else if (Ipv6Address::CheckCompatible(sourceStr))
+        {
+            v6Src = Ipv6Address(sourceStr.c_str());
+            useIpv6 = true;
+            source = v6Src.value();
+        }
+        else
         {
-            source = v6Src;
+            NS_ABORT_MSG("Can't parse destination address " << destinationStr);
         }
     }
     if (sourceStr.empty())
     {
         if (useIpv6)
         {
-            Ipv6Address v6Dst = Ipv6Address(destinationStr.c_str());
-            if (v6Dst.IsInitialized() && v6Dst.IsMulticast())
+            if (v6Dst && v6Dst->IsMulticast())
             {
                 std::cout << "Specify a source address to use when pinging multicast addresses"
                           << std::endl;
@@ -146,8 +166,7 @@
         }
         else
         {
-            Ipv4Address v4Dst(destinationStr.c_str());
-            if (v4Dst.IsInitialized() && (v4Dst.IsBroadcast() || v4Dst.IsMulticast()))
+            if (v4Dst && (v4Dst->IsBroadcast() || v4Dst->IsMulticast()))
             {
                 std::cout << "Specify a source address to use when pinging broadcast or multicast "
                              "addresses"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-client.cc ns-3.47/src/internet-apps/model/dhcp6-client.cc
--- ns-3.46/src/internet-apps/model/dhcp6-client.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-client.cc	2026-02-16 16:08:49.604230000 -0800
@@ -12,6 +12,7 @@
 #include "dhcp6-duid.h"
 
 #include "ns3/address-utils.h"
+#include "ns3/enum.h"
 #include "ns3/icmpv6-l4-protocol.h"
 #include "ns3/ipv6-interface.h"
 #include "ns3/ipv6-l3-protocol.h"
@@ -71,6 +72,23 @@
                           TimeValue(Seconds(100)),
                           MakeTimeAccessor(&Dhcp6Client::m_solicitInterval),
                           MakeTimeChecker())
+            .AddAttribute("DuidType",
+                          "Configure the type of DUID used by the client.",
+                          EnumValue(Duid::Type::LL),
+                          MakeEnumAccessor<Duid::Type>(&Dhcp6Client::m_duidType),
+                          MakeEnumChecker(Duid::Type::LLT,
+                                          "LLT",
+                                          Duid::Type::EN,
+                                          "EN",
+                                          Duid::Type::LL,
+                                          "LL",
+                                          Duid::Type::UUID,
+                                          "UUID"))
+            .AddAttribute("DuidEnIdentifierLength",
+                          "Length of the identifier of the DUID-EN.",
+                          IntegerValue(8),
+                          MakeIntegerAccessor(&Dhcp6Client::m_DuidEnIdentifierLength),
+                          MakeIntegerChecker<uint16_t>(4, 65529))
             .AddTraceSource("NewLease",
                             "The client has obtained a lease",
                             MakeTraceSourceAccessor(&Dhcp6Client::m_newLease),
@@ -213,8 +231,6 @@
 
 Dhcp6Client::InterfaceConfig::~InterfaceConfig()
 {
-    std::cout << "InterfaceConfig::~InterfaceConfig" << std::endl;
-
     m_socket = nullptr;
     m_client = nullptr;
 
@@ -253,7 +269,6 @@
 
         // Notify the new lease.
         m_client->m_newLease(offeredAddress);
-        std::cerr << "* got a new lease " << offeredAddress << std::endl;
     }
 }
 
@@ -873,10 +888,17 @@
     int32_t ifIndex = ipv6->GetInterfaceForDevice(device);
     Ptr<InterfaceConfig> dhcpInterface = m_interfaces[ifIndex];
 
-    Ptr<Node> node = GetNode();
     if (m_clientDuid.IsInvalid())
     {
-        m_clientDuid.Initialize(node);
+        Ptr<Node> node = GetNode();
+        // We use enterprise-number 0xf00dcafe (totally arbitrary)
+        // The current largest enterprise number is less than 0xffff, which means
+        // we use an unused number.
+        // The list of numbers is at
+        // https://www.iana.org/assignments/enterprise-numbers/
+
+        m_clientDuid.Initialize(m_duidType, node, m_DuidEnIdentifierLength);
+        NS_LOG_INFO("DHCPv6 client DUID created: " << m_clientDuid);
     }
 
     Dhcp6Header header;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-client.h ns-3.47/src/internet-apps/model/dhcp6-client.h
--- ns-3.46/src/internet-apps/model/dhcp6-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-client.h	2026-02-16 16:08:49.604230000 -0800
@@ -68,6 +68,7 @@
     void StartApplication() override;
     void StopApplication() override;
 
+  private:
     /**
      * @ingroup dhcp6
      *
@@ -232,6 +233,9 @@
 
     /**
      * @brief Used to send the Solicit message and start the DHCPv6 client.
+     *
+     * This function also builds the client's DUID on first invocation.
+     *
      * @param device The client interface.
      */
     void Boot(Ptr<NetDevice> device);
@@ -245,11 +249,10 @@
     /// Map each interface to its corresponding configuration details.
     std::unordered_map<uint32_t, Ptr<InterfaceConfig>> m_interfaces;
 
-    Duid m_clientDuid;                             //!< The client DUID.
     TracedCallback<const Ipv6Address&> m_newLease; //!< Trace the new lease.
 
     /// Track the IPv6 Address - IAID association.
-    std::unordered_map<Ipv6Address, uint32_t, Ipv6AddressHash> m_iaidMap;
+    std::unordered_map<Ipv6Address, uint32_t> m_iaidMap;
 
     /// Random variable to set transaction ID
     Ptr<RandomVariableStream> m_transactionId;
@@ -264,6 +267,16 @@
 
     /// Random variable used to create the IAID.
     Ptr<RandomVariableStream> m_iaidStream;
+
+    Duid::Type m_duidType;             //!< DUID type.
+    uint16_t m_DuidEnIdentifierLength; //!< DUID-EN identifier length.
+
+    /**
+     * The client DUID, built on first usage in the Boot() function.
+     * We avoid to use `StartApplication()` to randomize the time
+     * of DUID-LLT.
+     */
+    Duid m_clientDuid;
 };
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-duid.cc ns-3.47/src/internet-apps/model/dhcp6-duid.cc
--- ns-3.46/src/internet-apps/model/dhcp6-duid.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-duid.cc	2026-02-16 16:08:49.604230000 -0800
@@ -15,66 +15,32 @@
 #include "ns3/log.h"
 #include "ns3/loopback-net-device.h"
 #include "ns3/ptr.h"
+#include "ns3/simulator.h"
+
+#include <algorithm>
+#include <iomanip>
 
 namespace ns3
 {
 
 NS_LOG_COMPONENT_DEFINE("Dhcp6Duid");
 
-Duid::Duid()
-{
-    m_duidType = Duid::Type::LL;
-    m_hardwareType = 0;
-    m_time = Time();
-    m_identifier = std::vector<uint8_t>();
-}
-
-bool
-Duid::operator==(const Duid& o) const
-{
-    return (m_duidType == o.m_duidType && m_hardwareType == o.m_hardwareType &&
-            m_identifier == o.m_identifier);
-}
-
-bool
-operator<(const Duid& a, const Duid& b)
-{
-    if (a.m_duidType < b.m_duidType)
-    {
-        return true;
-    }
-    else if (a.m_duidType > b.m_duidType)
-    {
-        return false;
-    }
-    if (a.m_hardwareType < b.m_hardwareType)
-    {
-        return true;
-    }
-    else if (a.m_hardwareType > b.m_hardwareType)
-    {
-        return false;
-    }
-    NS_ASSERT(a.GetLength() == b.GetLength());
-    return a.m_identifier < b.m_identifier;
-}
-
 bool
 Duid::IsInvalid() const
 {
-    return m_identifier.empty();
+    return m_blob.empty();
 }
 
 uint8_t
 Duid::GetLength() const
 {
-    return m_identifier.size();
+    return m_blob.size();
 }
 
 std::vector<uint8_t>
 Duid::GetIdentifier() const
 {
-    return m_identifier;
+    return m_blob;
 }
 
 Duid::Type
@@ -84,59 +50,135 @@
 }
 
 void
-Duid::SetDuidType(Duid::Type duidType)
+Duid::InitializeLL(Ptr<Node> node)
 {
-    m_duidType = duidType;
+    NS_LOG_FUNCTION(this << node);
+
+    m_duidType = Type::LL;
+    Address duidAddress = FindSuitableNetDeviceAddress(node);
+
+    uint8_t idLen = duidAddress.GetLength();
+    NS_ASSERT_MSG(idLen == 6 || idLen == 8, "Duid: Invalid identifier length.");
+
+    m_blob.resize(idLen + Offset::LL);
+
+    // Ethernet (48 bit) - HardwareType 1
+    // EUI-64 (64 bit) - HardwareType 27
+    m_blob[0] = 0;
+    m_blob[1] = idLen == 6 ? 1 : 27;
+
+    duidAddress.CopyTo(&m_blob[Offset::LL]);
 }
 
-uint16_t
-Duid::GetHardwareType() const
+void
+Duid::InitializeLLT(Ptr<Node> node)
 {
-    return m_hardwareType;
+    NS_LOG_FUNCTION(this << node);
+
+    m_duidType = Type::LLT;
+    Address duidAddress = FindSuitableNetDeviceAddress(node);
+
+    uint8_t idLen = duidAddress.GetLength();
+    NS_ASSERT_MSG(idLen == 6 || idLen == 8, "Duid: Invalid identifier length.");
+
+    m_blob.resize(idLen + Offset::LLT);
+
+    // Ethernet (48 bit) - HardwareType 1
+    // EUI-64 (64 bit) - HardwareType 27
+    m_blob[0] = 0;
+    m_blob[1] = idLen == 6 ? 1 : 27;
+
+    // This should be (according to RFC 8415)
+    // seconds since midnight (UTC), January 1, 2000, modulo 2 ^ 32
+    // We do not have an (easy) way to measure the difference.
+
+    uint32_t time = std::floor(std::fmod(Simulator::Now().GetSeconds(), std::pow(2, 32)));
+    HtoN(time, m_blob.begin() + Offset::LL);
+
+    duidAddress.CopyTo(&m_blob[Offset::LLT]);
 }
 
 void
-Duid::SetHardwareType(uint16_t hardwareType)
+Duid::InitializeEN(uint32_t enNumber, const std::vector<uint8_t>& identifier)
 {
-    NS_LOG_FUNCTION(this << hardwareType);
-    m_hardwareType = hardwareType;
+    NS_LOG_FUNCTION(this << enNumber << identifier);
+
+    m_duidType = Type::EN;
+
+    // The DUID is going to be embedded in an option, which
+    // have a DUID length expressed 16-bit
+    // this leaves 65535 - 2 - 4 bytes for the identifier.
+    NS_ASSERT_MSG(identifier.size() < 65535 - 6,
+                  "Duid: Invalid identifier length (max length is 65529 bytes).");
+
+    m_blob.resize(identifier.size() + Offset::EN);
+
+    HtoN(enNumber, m_blob.begin());
+
+    std::move(identifier.cbegin(), identifier.cend(), &m_blob[Offset::EN]);
 }
 
 void
-Duid::SetDuid(std::vector<uint8_t> identifier)
+Duid::InitializeUUID(const std::vector<uint8_t>& uuid)
 {
-    NS_LOG_FUNCTION(this << identifier);
+    NS_LOG_FUNCTION(this << uuid);
 
-    m_duidType = Type::LL;
-    uint8_t idLen = identifier.size();
+    m_duidType = Type::UUID;
 
-    NS_ASSERT_MSG(idLen == 6 || idLen == 8, "Duid: Invalid identifier length.");
+    NS_ASSERT_MSG(uuid.size() == 16, "Duid: Invalid identifier length (UUID must be 128 bits).");
+
+    m_blob.resize(uuid.size());
+
+    std::move(uuid.cbegin(), uuid.cend(), m_blob.begin());
+}
+
+void
+Duid::Initialize(Type duidType, Ptr<Node> node, uint32_t enIdentifierLength)
+{
+    NS_LOG_FUNCTION(this << std::to_underlying(duidType) << node << enIdentifierLength);
 
-    switch (idLen)
+    switch (duidType)
     {
-    case 6:
-        // Ethernet - 48 bit length
-        SetHardwareType(1);
+    case Duid::Type::LLT: {
+        InitializeLLT(node);
         break;
-    case 8:
-        // EUI-64 - 64 bit length
-        SetHardwareType(27);
+    }
+    case Duid::Type::EN: {
+        // We use enterprise-number 0xf00dcafe (totally arbitrary)
+        // The list of numbers is at
+        // https://www.iana.org/assignments/enterprise-numbers/
+        std::vector<uint8_t> identifier(enIdentifierLength);
+        HtoN(node->GetId(), identifier.begin());
+        InitializeEN(0xf00dcafe, identifier);
         break;
     }
-
-    m_identifier.resize(idLen);
-    m_identifier = identifier;
+    case Duid::Type::LL: {
+        InitializeLL(node);
+        break;
+    }
+    case Duid::Type::UUID: {
+        std::vector<uint8_t> identifier(16);
+        HtoN(node->GetId(), identifier.begin());
+        InitializeUUID(identifier);
+        break;
+    }
+    }
 }
 
-void
-Duid::Initialize(Ptr<Node> node)
+Address
+Duid::FindSuitableNetDeviceAddress(Ptr<Node> node)
 {
+    NS_LOG_FUNCTION(this << node);
+
     Ptr<Ipv6L3Protocol> ipv6 = node->GetObject<Ipv6L3Protocol>();
+    NS_ASSERT_MSG(ipv6, "DHCPv6: No IPv6 stack found.");
     uint32_t nInterfaces = ipv6->GetNInterfaces();
 
     uint32_t maxAddressLength = 0;
     Address duidAddress;
 
+    // We select NetDevices with the longest MAC address length,
+    // and among them, the one with the smallest index.
     for (uint32_t i = 0; i < nInterfaces; i++)
     {
         Ptr<NetDevice> device = ipv6->GetNetDevice(i);
@@ -164,32 +206,14 @@
 
     NS_LOG_DEBUG("DUID of node " << node->GetId() << " is " << duidAddress);
 
-    // Consider the link-layer address of the first NetDevice in the list.
-    uint8_t buffer[16];
-    duidAddress.CopyTo(buffer);
-
-    std::vector<uint8_t> identifier(duidAddress.GetLength());
-    std::copy(buffer, buffer + duidAddress.GetLength(), identifier.begin());
-    SetDuid(identifier);
-}
-
-Time
-Duid::GetTime() const
-{
-    return m_time;
-}
-
-void
-Duid::SetTime(Time time)
-{
-    NS_LOG_FUNCTION(this << time);
-    m_time = time;
+    return duidAddress;
 }
 
 uint32_t
 Duid::GetSerializedSize() const
 {
-    return 4 + m_identifier.size();
+    // The DUID type is 2 bytes.
+    return 2 + m_blob.size();
 }
 
 void
@@ -197,12 +221,8 @@
 {
     Buffer::Iterator i = start;
     i.WriteHtonU16(static_cast<std::underlying_type_t<Duid::Type>>(m_duidType));
-    i.WriteHtonU16(m_hardwareType);
 
-    for (const auto& byte : m_identifier)
-    {
-        i.WriteU8(byte);
-    }
+    i.Write(&m_blob[0], m_blob.size());
 }
 
 uint32_t
@@ -211,15 +231,21 @@
     Buffer::Iterator i = start;
 
     m_duidType = static_cast<Duid::Type>(i.ReadNtohU16());
-    m_hardwareType = i.ReadNtohU16();
-    m_identifier.resize(len);
+    len -= 2;
 
-    for (uint32_t j = 0; j < len; j++)
-    {
-        m_identifier[j] = i.ReadU8();
-    }
+    m_blob.resize(len);
+    i.Read(&m_blob[0], m_blob.size());
+
+    return GetSerializedSize();
+}
 
-    return 4 + m_identifier.size();
+void
+Duid::HtoN(uint32_t source, std::vector<uint8_t>::iterator dest)
+{
+    dest[0] = (source >> 24);
+    dest[1] = (source >> 16) & 0xFF;
+    dest[2] = (source >> 8) & 0xFF;
+    dest[3] = source & 0xFF;
 }
 
 size_t
@@ -231,4 +257,84 @@
     std::string s(buffer.begin(), buffer.begin() + duidLen);
     return std::hash<std::string>{}(s);
 }
+
+// Comparison operators are defined because gcc-13.3 raises an error in optimized builds.
+// Other than that, the comparisons are semantically identical to the default ones.
+bool
+Duid::operator==(const Duid& other) const
+{
+    return std::is_eq(*this <=> other);
+};
+
+std::strong_ordering
+operator<=>(const Duid& a, const Duid& b)
+{
+    if (a.m_duidType == b.m_duidType)
+    {
+        return a.m_blob <=> b.m_blob;
+    }
+
+    return a.m_duidType <=> b.m_duidType;
+}
+
+std::ostream&
+operator<<(std::ostream& os, const Duid& duid)
+{
+    auto fold16 = [](std::vector<uint8_t>::const_iterator a) -> uint16_t {
+        auto f = static_cast<uint16_t>(a[0]) << 8;
+        f += static_cast<uint16_t>(a[1]);
+        return f;
+    };
+
+    auto fold32 = [](std::vector<uint8_t>::const_iterator a) -> uint32_t {
+        auto f = static_cast<uint32_t>(a[0]) << 24;
+        f += static_cast<uint32_t>(a[1]) << 16;
+        f += static_cast<uint32_t>(a[2]) << 8;
+        f += static_cast<uint32_t>(a[3]);
+        return f;
+    };
+
+    auto format = [](std::vector<uint8_t>::const_iterator start,
+                     std::vector<uint8_t>::const_iterator end) -> std::string {
+        std::ostringstream str;
+        str << std::hex;
+        std::for_each(start, end, [&](int i) { str << std::setfill('0') << std::setw(2) << i; });
+        return str.str();
+    };
+
+    switch (duid.m_duidType)
+    {
+    case Duid::Type::LLT: {
+        os << "DUID-LLT";
+        os << " HWtype: " << fold16(duid.m_blob.cbegin());
+        os << " Time: " << fold32(duid.m_blob.cbegin() + 2);
+        auto identifierStart = duid.m_blob.cbegin() + Duid::Offset::LLT;
+        os << " Identifier: 0x" << format(identifierStart, duid.m_blob.cend());
+        break;
+    }
+    case Duid::Type::EN: {
+        os << "DUID-EN";
+        os << " EnNumber: 0x"
+           << format(duid.m_blob.cbegin(), duid.m_blob.cbegin() + Duid::Offset::EN);
+        auto identifierStart = duid.m_blob.cbegin() + Duid::Offset::EN;
+        os << " Identifier: 0x" << format(identifierStart, duid.m_blob.cend());
+        break;
+    }
+    case Duid::Type::LL: {
+        os << "DUID-LL";
+        os << " HWtype: " << fold16(duid.m_blob.cbegin());
+        auto identifierStart = duid.m_blob.cbegin() + Duid::Offset::LL;
+        os << " Identifier: 0x" << format(identifierStart, duid.m_blob.cend());
+        break;
+    }
+    case Duid::Type::UUID: {
+        os << "DUID-UUID";
+        auto identifierStart = duid.m_blob.cbegin();
+        os << " UUID: 0x" << format(identifierStart, duid.m_blob.cend());
+        break;
+    }
+    }
+    return os;
+}
+
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-duid.h ns-3.47/src/internet-apps/model/dhcp6-duid.h
--- ns-3.46/src/internet-apps/model/dhcp6-duid.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-duid.h	2026-02-16 16:08:49.604230000 -0800
@@ -26,11 +26,6 @@
 class Duid
 {
   public:
-    /**
-     * @brief Default constructor.
-     */
-    Duid();
-
     /// DUID type.
     enum class Type
     {
@@ -52,7 +47,7 @@
         /**
          * @brief Returns the hash of a DUID.
          * @param x the DUID
-         * @return the hash
+         * @returns the hash
          *
          * This method uses std::hash rather than class Hash
          * as speed is more important than cryptographic robustness.
@@ -61,68 +56,75 @@
     };
 
     /**
-     * @brief Initialize the DUID for a client or server.
-     * @param node The node for which the DUID is to be generated.
-     */
-    void Initialize(Ptr<Node> node);
-
-    /**
-     * @brief Check if the DUID is invalid.
-     * @return true if the DUID is invalid.
+     * Initialize a DUID.
+     *
+     * The DUID initialization logic is the following:
+     *   - DUID-LLT and DUID-LL: the identifier is created by selecting
+     *     the NetDevices with the longest MAC address length, and among
+     *     them, the one with the smallest index.
+     *   - DUID-LLT: the creation time is taken from the simulation time.
+     *   - DUID-EN: the Enterprise number is 0xf00dcafe (arbitrary number,
+     *     currently unused), and the identifier is based on the Node ID.
+     *   - DUID-UUID: the UUID is based on the Node ID.
+     *
+     * @param duidType the DUID type.
+     * @param node the node this DUID refers to
+     * @param enIdentifierLength the DUID-EN identifier length (unused in other DUID types)
      */
-    bool IsInvalid() const;
+    void Initialize(Type duidType, Ptr<Node> node, uint32_t enIdentifierLength);
 
     /**
-     * @brief Get the DUID type
-     * @return the DUID type.
+     * @brief Initialize the DUID-LL for a client or server.
+     * @param node The node for which the DUID is to be generated.
      */
-    Type GetDuidType() const;
+    void InitializeLL(Ptr<Node> node);
 
     /**
-     * @brief Set the DUID type
-     * @param duidType the DUID type.
+     * @brief Initialize the DUID-LLT for a client or server.
+     * @param node The node for which the DUID is to be generated.
      */
-    void SetDuidType(Type duidType);
+    void InitializeLLT(Ptr<Node> node);
 
     /**
-     * @brief Get the hardware type.
-     * @return the hardware type
+     * @brief Initialize the DUID-EN for a client or server.
+     * @param enNumber The Enterprise Number.
+     * @param identifier The device Identifier.
      */
-    uint16_t GetHardwareType() const;
+    void InitializeEN(uint32_t enNumber, const std::vector<uint8_t>& identifier);
 
     /**
-     * @brief Set the hardware type.
-     * @param hardwareType the hardware type.
+     * @brief Initialize the DUID-EN for a client or server.
+     * @param uuid The UUID.
      */
-    void SetHardwareType(uint16_t hardwareType);
+    void InitializeUUID(const std::vector<uint8_t>& uuid);
 
     /**
-     * @brief Set the identifier as the DUID.
-     * @param identifier the identifier of the node.
+     * @brief Check if the DUID is invalid.
+     * @returns true if the DUID is invalid.
      */
-    void SetDuid(std::vector<uint8_t> identifier);
+    bool IsInvalid() const;
 
     /**
-     * @brief Get the time at which the DUID is generated.
-     * @return the timestamp.
+     * @brief Get the DUID type
+     * @returns the DUID type.
      */
-    Time GetTime() const;
+    Type GetDuidType() const;
 
     /**
      * @brief Get the length of the DUID.
-     * @return the DUID length.
+     * @returns the DUID length.
      */
     uint8_t GetLength() const;
 
     /**
-     * @brief Set the time at which DUID is generated.
-     * @param time the timestamp.
+     * @brief Return the identifier of the node.
+     * @returns the identifier.
      */
-    void SetTime(Time time);
+    std::vector<uint8_t> GetIdentifier() const;
 
     /**
      * @brief Get the DUID serialized size.
-     * @return The DUID serialized sized in bytes.
+     * @returns The DUID serialized sized in bytes.
      */
     uint32_t GetSerializedSize() const;
 
@@ -136,65 +138,88 @@
      * @brief Deserialize the DUID.
      * @param start The buffer iterator.
      * @param len The number of bytes to be read.
-     * @return The number of bytes read.
+     * @returns The number of bytes read.
      */
     uint32_t Deserialize(Buffer::Iterator start, uint32_t len);
 
     /**
-     * @brief Comparison operator
-     * @param duid header to compare
-     * @return true if the headers are equal
+     * @brief Comparison operator.
+     *
+     * @note Defined because gcc-13.3 raises an error in optimized builds.
+     * Other than that, the comparison is semantically identical to the default one.
+     *
+     * @param other DUID to compare to this one.
+     * @return true if the duids are equal.
      */
-    bool operator==(const Duid& duid) const;
+    bool operator==(const Duid& other) const;
 
     /**
-     * @brief Less than operator.
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
      *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operand a is less than operand b
+     * @note Defined because gcc-13.3 raises an error in optimized builds.
+     * Other than that, the comparison is semantically identical to the default one.
+     *
+     * @param a left operand DUID to compare.
+     * @param b right operand DUID to compare.
+     * @returns The result of the comparison.
      */
-    friend bool operator<(const Duid& a, const Duid& b);
+    friend std::strong_ordering operator<=>(const Duid& a, const Duid& b);
 
-    // TypeId GetInstanceTypeId() const override;
-    // void Print(std::ostream& os) const override;
-    // uint32_t GetSerializedSize() const override;
-    // void Serialize(Buffer::Iterator start) const override;
-    // uint32_t Deserialize(Buffer::Iterator start) override;
+    /**
+     * @brief Stream output operator.
+     *
+     * The DUID is printed according to its type, e.g.:
+     *   - DUID-LL HWtype: 1 Identifier: 0x000000000002
+     *   - DUID-LLT HWtype: 1 Time: 0 Identifier: 0x000000000002
+     *   - DUID-EN EnNumber: 0xf00dcafe Identifier: 0x000102030405060708090a
+     *   - DUID-UUID UUID: 0x000102030405060708090a0b0c0d0e0f
+     *
+     * @param os output stream
+     * @param duid The reference to the DUID object.
+     * @returns updated stream
+     */
+    friend std::ostream& operator<<(std::ostream& os, const Duid& duid);
 
   private:
     /**
-     * @brief Return the identifier of the node.
-     * @return the identifier.
+     * DUID data offset.
+     *
+     * Each DUID has some fixed-length fields (e.g., Hardware Type), and a
+     * variable length part. This is the offset to the variable length part.
      */
-    std::vector<uint8_t> GetIdentifier() const;
+    enum Offset
+    {
+        LLT = 6,
+        EN = 4,
+        LL = 2,
+        UUID = 0
+    };
 
     /**
-     * Type of the DUID.
-     * We currently use only DUID-LL, based on the link-layer address.
+     * Find a suitable NetDevice address to create a DUID-LL or DUID-LLT.
+     *
+     * We select the NetDevices with the longest MAC address length,
+     * and among them, the one with the smallest index.
+     *
+     * @param node The node for which the DUID is to be generated.
+     * @returns the L2 address for the DUID Hardware Identifier.
      */
-    Type m_duidType;
+    Address FindSuitableNetDeviceAddress(Ptr<Node> node);
 
-    uint16_t m_hardwareType;           //!< Valid hardware type assigned by IANA.
-    Time m_time;                       //!< Time at which the DUID is generated. Used in DUID-LLT.
-    std::vector<uint8_t> m_identifier; //!< Identifier of the node in bytes.
-};
+    /**
+     * Utility function to convert a 32-bit number to
+     * an array with its bytes in network order.
+     *
+     * @param[in] source the number to convert.
+     * @param[out] dest the converted number.
+     */
+    void HtoN(uint32_t source, std::vector<uint8_t>::iterator dest);
 
-/**
- * @brief Stream output operator
- * @param os output stream
- * @param duid The reference to the DUID object.
- * @return updated stream
- */
-std::ostream& operator<<(std::ostream& os, const Duid& duid);
-
-/**
- * Stream extraction operator
- * @param is input stream
- * @param duid The reference to the DUID object.
- * @return std::istream
- */
-std::istream& operator>>(std::istream& is, Duid& duid);
+    Type m_duidType{Duid::Type::LL}; //!< DUID type.
+
+    std::vector<uint8_t> m_blob{}; //!< DUID data, content depending on the DUID type.
+};
 
 } // namespace ns3
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-header.cc ns-3.47/src/internet-apps/model/dhcp6-header.cc
--- ns-3.46/src/internet-apps/model/dhcp6-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-header.cc	2026-02-16 16:08:49.604230000 -0800
@@ -142,8 +142,7 @@
                                  Options::OptionType optionType,
                                  Duid duid)
 {
-    // DUID type (2 bytes) + hw type (2 bytes) + Link-layer Address (variable)
-    uint16_t duidLength = 2 + 2 + duid.GetLength();
+    uint16_t duidLength = duid.GetSerializedSize();
 
     // Set the option code, length, hardware type, link layer address.
     identifier.SetOptionCode(optionType);
@@ -513,7 +512,7 @@
 
                     // Read DUID.
                     Duid duid;
-                    uint32_t read = duid.Deserialize(i, duidLength - 4);
+                    uint32_t read = duid.Deserialize(i, duidLength);
                     i.Next(read);
                     m_clientIdentifier.SetDuid(duid);
                     len += m_clientIdentifier.GetOptionLength();
@@ -535,7 +534,7 @@
 
                 // Read DUID.
                 Duid duid;
-                uint32_t read = duid.Deserialize(i, duidLength - 4);
+                uint32_t read = duid.Deserialize(i, duidLength);
                 i.Next(read);
                 m_serverIdentifier.SetDuid(duid);
                 len += m_serverIdentifier.GetOptionLength();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-options.cc ns-3.47/src/internet-apps/model/dhcp6-options.cc
--- ns-3.46/src/internet-apps/model/dhcp6-options.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-options.cc	2026-02-16 16:08:49.604230000 -0800
@@ -66,28 +66,6 @@
 {
 }
 
-IdentifierOption::IdentifierOption(uint16_t hardwareType, Address linkLayerAddress, Time time)
-{
-    NS_LOG_FUNCTION(this << hardwareType << linkLayerAddress);
-    if (time.IsZero())
-    {
-        m_duid.SetDuidType(Duid::Type::LL);
-    }
-    else
-    {
-        m_duid.SetDuidType(Duid::Type::LLT);
-    }
-
-    m_duid.SetHardwareType(hardwareType);
-
-    uint8_t buffer[16];
-    linkLayerAddress.CopyTo(buffer);
-
-    std::vector<uint8_t> identifier;
-    std::copy(buffer, buffer + linkLayerAddress.GetLength(), identifier.begin());
-    m_duid.SetDuid(identifier);
-}
-
 void
 IdentifierOption::SetDuid(Duid duid)
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-options.h ns-3.47/src/internet-apps/model/dhcp6-options.h
--- ns-3.46/src/internet-apps/model/dhcp6-options.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-options.h	2026-02-16 16:08:49.604230000 -0800
@@ -139,14 +139,6 @@
     IdentifierOption();
 
     /**
-     * @brief Constructor.
-     * @param hardwareType The hardware type.
-     * @param linkLayerAddress The link-layer address.
-     * @param time The time at which the DUID is generated.
-     */
-    IdentifierOption(uint16_t hardwareType, Address linkLayerAddress, Time time = Time());
-
-    /**
      * @brief Set the DUID.
      * @param duid The DUID.
      */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-server.cc ns-3.47/src/internet-apps/model/dhcp6-server.cc
--- ns-3.46/src/internet-apps/model/dhcp6-server.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-server.cc	2026-02-16 16:08:49.604230000 -0800
@@ -12,7 +12,9 @@
 #include "dhcp6-duid.h"
 
 #include "ns3/address-utils.h"
+#include "ns3/enum.h"
 #include "ns3/icmpv6-l4-protocol.h"
+#include "ns3/integer.h"
 #include "ns3/ipv6-interface.h"
 #include "ns3/ipv6-l3-protocol.h"
 #include "ns3/ipv6-packet-info-tag.h"
@@ -61,7 +63,24 @@
                           "4000 seconds by default in Linux.",
                           TimeValue(Seconds(4000)),
                           MakeTimeAccessor(&Dhcp6Server::m_validLifetime),
-                          MakeTimeChecker());
+                          MakeTimeChecker())
+            .AddAttribute("DuidType",
+                          "Configure the type of DUID used by the server.",
+                          EnumValue(Duid::Type::LL),
+                          MakeEnumAccessor<Duid::Type>(&Dhcp6Server::m_duidType),
+                          MakeEnumChecker(Duid::Type::LLT,
+                                          "LLT",
+                                          Duid::Type::EN,
+                                          "EN",
+                                          Duid::Type::LL,
+                                          "LL",
+                                          Duid::Type::UUID,
+                                          "UUID"))
+            .AddAttribute("DuidEnIdentifierLength",
+                          "Length of the identifier of the DUID-EN.",
+                          IntegerValue(8),
+                          MakeIntegerAccessor(&Dhcp6Server::m_DuidEnIdentifierLength),
+                          MakeIntegerChecker<uint16_t>(4, 65529));
 
     return tid;
 }
@@ -160,11 +179,6 @@
     // Available address pools and IA information is sent in this option.
     for (auto& subnet : m_subnets)
     {
-        Ipv6Address pool = subnet.GetAddressPool();
-        Ipv6Prefix prefix = subnet.GetPrefix();
-        Ipv6Address minAddress = subnet.GetMinAddress();
-        Ipv6Address maxAddress = subnet.GetMaxAddress();
-
         /*
          * Find the next available address. Checks the expired address map.
          * If there are no expired addresses, it advertises a new address.
@@ -210,6 +224,7 @@
         {
             // Allocate a new address.
             uint8_t minAddrBuf[16];
+            Ipv6Address minAddress = subnet.GetMinAddress();
             minAddress.GetBytes(minAddrBuf);
 
             // Get the latest leased address.
@@ -667,10 +682,6 @@
         return;
     }
 
-    // Initialize the DUID before responding to the client.
-    Ptr<Node> node = GetNode();
-    m_serverDuid.Initialize(node);
-
     if (header.GetMessageType() == Dhcp6Header::MessageType::SOLICIT)
     {
         ProcessSolicit(iDev, header, senderAddr);
@@ -778,6 +789,19 @@
         icmpv6->SetDhcpv6Callback(MakeCallback(&Dhcp6Server::ReceiveMflag, this));
     }
 
+    // Initialize the DUID before responding to the client.
+    if (m_serverDuid.IsInvalid())
+    {
+        Ptr<Node> node = GetNode();
+        // We use enterprise-number 0xf00dcafe (totally arbitrary)
+        // The current largest enterprise number is less than 0xffff, which means
+        // we use an unused number.
+        // The list of numbers is at
+        // https://www.iana.org/assignments/enterprise-numbers/
+        m_serverDuid.Initialize(m_duidType, node, m_DuidEnIdentifierLength);
+        NS_LOG_INFO("DHCPv6 server DUID created: " << m_serverDuid);
+    }
+
     m_leaseCleanupEvent = Simulator::Schedule(m_leaseCleanup, &Dhcp6Server::CleanLeases, this);
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp6-server.h ns-3.47/src/internet-apps/model/dhcp6-server.h
--- ns-3.46/src/internet-apps/model/dhcp6-server.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp6-server.h	2026-02-16 16:08:49.604230000 -0800
@@ -99,7 +99,7 @@
      * @brief Declined Addresses
      * Ipv6Address + Client DUID
      */
-    typedef std::unordered_map<Ipv6Address, Duid, Ipv6AddressHash> DeclinedAddresses;
+    typedef std::unordered_map<Ipv6Address, Duid> DeclinedAddresses;
 
     LeasedAddresses m_leasedAddresses;     //!< Leased addresses
     ExpiredAddresses m_expiredAddresses;   //!< Expired addresses
@@ -221,7 +221,6 @@
     void CleanLeases();
 
     Ptr<Socket> m_recvSocket;            //!< Socket bound to port 547.
-    Duid m_serverDuid;                   //!< Server DUID
     std::vector<LeaseInfo> m_subnets;    //!< List of managed subnets.
     Time m_leaseCleanup = Seconds(10.0); //!< Lease cleanup time
     EventId m_leaseCleanupEvent;         //!< Event ID for lease cleanup
@@ -260,6 +259,14 @@
      * Maximum value is REB_MAX_RT (RFC 8415, Section 7.6).
      */
     Time m_rebind;
+
+    Duid::Type m_duidType;             //!< DUID type.
+    uint16_t m_DuidEnIdentifierLength; //!< DUID-EN identifier length.
+
+    /**
+     * The server DUID, built by StartApplication() on fist usage.
+     */
+    Duid m_serverDuid;
 };
 } // namespace ns3
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp-client.cc ns-3.47/src/internet-apps/model/dhcp-client.cc
--- ns-3.46/src/internet-apps/model/dhcp-client.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp-client.cc	2026-02-16 16:08:49.603230000 -0800
@@ -166,12 +166,12 @@
     // Moreover, the length is always 16, because chaddr is 16 bytes.
     Address myAddress = m_device->GetAddress();
     NS_LOG_INFO("My address is " << myAddress);
-    uint8_t addr[Address::MAX_SIZE];
-    std::memset(addr, 0, Address::MAX_SIZE);
-    uint32_t len = myAddress.CopyTo(addr);
-    NS_ASSERT_MSG(len <= 16, "DHCP client can not handle a chaddr larger than 16 bytes");
-    m_chaddr.CopyFrom(addr, 16);
-    NS_LOG_INFO("My m_chaddr is " << m_chaddr);
+    NS_ASSERT_MSG(myAddress.GetLength() <= 16,
+                  "DHCP client can not handle a chaddr larger than 16 bytes");
+
+    m_chaddr.fill(0);
+    myAddress.CopyTo(m_chaddr.data());
+    NS_LOG_INFO("My Chaddr is " << DhcpChaddrToString(m_chaddr));
 
     bool found = false;
     for (uint32_t i = 0; i < ipv4->GetNAddresses(ifIndex); i++)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp-client.h ns-3.47/src/internet-apps/model/dhcp-client.h
--- ns-3.46/src/internet-apps/model/dhcp-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp-client.h	2026-02-16 16:08:49.603230000 -0800
@@ -140,7 +140,7 @@
     Ipv4Address m_remoteAddress;  //!< Initially set to 255.255.255.255 to start DHCP
     Ipv4Address m_offeredAddress; //!< Address offered to the client
     Ipv4Address m_myAddress;      //!< Address assigned to the client
-    Address m_chaddr;         //!< chaddr of the interface (stored as an Address for convenience).
+    DhcpChaddr m_chaddr{};    //!< chaddr of the interface (stored as an Address for convenience).
     Ipv4Mask m_myMask;        //!< Mask of the address assigned
     Ipv4Address m_server;     //!< Address of the DHCP server
     Ipv4Address m_gateway;    //!< Address of the gateway
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp-header.cc ns-3.47/src/internet-apps/model/dhcp-header.cc
--- ns-3.46/src/internet-apps/model/dhcp-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp-header.cc	2026-02-16 16:08:49.604230000 -0800
@@ -17,12 +17,35 @@
 #include "ns3/log.h"
 #include "ns3/simulator.h"
 
+#include <iomanip>
+#include <sstream>
+
+/**
+ * @file
+ * @ingroup internet-apps
+ * DhcpHeader classes implementation.
+ */
+
 namespace ns3
 {
 
 NS_LOG_COMPONENT_DEFINE("DhcpHeader");
 NS_OBJECT_ENSURE_REGISTERED(DhcpHeader);
 
+std::string
+DhcpChaddrToString(const DhcpChaddr& chaddr)
+{
+    std::ostringstream outStream;
+    outStream.fill('0');
+    outStream.setf(std::ios::hex, std::ios::basefield);
+
+    for (std::size_t i = 0; i < chaddr.size(); ++i)
+    {
+        outStream << std::setw(2) << (uint32_t)chaddr[i] << (i < chaddr.size() - 1 ? ":" : "");
+    }
+    return outStream.str();
+}
+
 DhcpHeader::DhcpHeader()
 {
     m_hType = 1;
@@ -105,27 +128,15 @@
 }
 
 void
-DhcpHeader::SetChaddr(Address addr)
-{
-    std::memset(m_chaddr, 0, 16);
-    NS_ASSERT_MSG(addr.GetLength() <= 16, "Address length too big");
-    addr.CopyTo(m_chaddr);
-}
-
-void
-DhcpHeader::SetChaddr(uint8_t* addr, uint8_t len)
+DhcpHeader::SetChaddr(DhcpChaddr addr)
 {
-    std::memset(m_chaddr, 0, 16);
-    NS_ASSERT_MSG(len <= 16, "Address length too big");
-    std::memcpy(m_chaddr, addr, len);
+    m_chaddr = addr;
 }
 
-Address
+DhcpChaddr
 DhcpHeader::GetChaddr()
 {
-    Address addr;
-    addr.CopyFrom(m_chaddr, 16);
-    return addr;
+    return m_chaddr;
 }
 
 void
@@ -313,7 +324,7 @@
     WriteTo(i, m_yiAddr);
     WriteTo(i, m_siAddr);
     WriteTo(i, m_giAddr);
-    i.Write(m_chaddr, 16);
+    i.Write(m_chaddr.data(), 16);
     i.Write(m_sname, 64);
     i.Write(m_file, 128);
     i.Write(m_magic_cookie, 4);
@@ -390,7 +401,7 @@
     ReadFrom(i, m_yiAddr);
     ReadFrom(i, m_siAddr);
     ReadFrom(i, m_giAddr);
-    i.Read(m_chaddr, 16);
+    i.Read(m_chaddr.data(), 16);
     i.Read(m_sname, 64);
     i.Read(m_file, 128);
     i.Read(m_magic_cookie, 4);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp-header.h ns-3.47/src/internet-apps/model/dhcp-header.h
--- ns-3.46/src/internet-apps/model/dhcp-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp-header.h	2026-02-16 16:08:49.604230000 -0800
@@ -18,6 +18,14 @@
 #include "ns3/header.h"
 #include "ns3/ipv4-address.h"
 
+#include <array>
+
+/**
+ * @file
+ * @ingroup internet-apps
+ * DhcpHeader classes declaration
+ */
+
 namespace ns3
 {
 
@@ -29,6 +37,24 @@
 /**
  * @ingroup dhcp
  *
+ * This is the Chaddr field, which is 16 bytes long. Originally it was
+ * meant to hold the Client Hardware Address (hence the name), but later RFCs
+ * refers to it just as the Client Identifier.
+ */
+using DhcpChaddr = std::array<uint8_t, 16>;
+
+/**
+ * @ingroup dhcp
+ * Function to pretty-print a Chaddr.
+ *
+ * @param chaddr the Chaddr
+ * @return A formatted string
+ */
+std::string DhcpChaddrToString(const DhcpChaddr& chaddr);
+
+/**
+ * @ingroup dhcp
+ *
  * @class DhcpHeader
  * @brief BOOTP header with DHCP messages.  This supports the following options:
  *        Subnet Mask (1), Address Request (50), Refresh Lease Time (51),
@@ -149,20 +175,11 @@
     void SetTime();
 
     /**
-     * @brief Set the Address of the device.
+     * @brief Set the Chaddress of the device.
      *
-     * Only the relevant bits are considered (i.e., not the type and length)
-     *
-     * @param addr Address of the device
-     */
-    void SetChaddr(Address addr);
-
-    /**
-     * @brief Set the Address of the device
      * @param addr Address of the device
-     * @param len Address length
      */
-    void SetChaddr(uint8_t* addr, uint8_t len);
+    void SetChaddr(DhcpChaddr addr);
 
     /**
      * @brief Get the Address of the client.
@@ -171,7 +188,7 @@
      *
      * @return Address of the client
      */
-    Address GetChaddr();
+    DhcpChaddr GetChaddr();
 
     /**
      * @brief Set the IPv4Address of the client
@@ -291,7 +308,7 @@
     uint32_t m_len;            //!< The length of the header
     uint16_t m_secs;           //!< Seconds elapsed
     uint16_t m_flags;          //!< BOOTP flags
-    uint8_t m_chaddr[16];      //!< The address identifier
+    DhcpChaddr m_chaddr;       //!< The client identifier
     Ipv4Address m_yiAddr;      //!< Your (client) IP address
     Ipv4Address m_ciAddr;      //!< The IP address of the client
     Ipv4Address m_siAddr;      //!< Next Server IP address
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp-server.cc ns-3.47/src/internet-apps/model/dhcp-server.cc
--- ns-3.46/src/internet-apps/model/dhcp-server.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp-server.cc	2026-02-16 16:08:49.604230000 -0800
@@ -24,6 +24,7 @@
 #include "ns3/socket.h"
 
 #include <algorithm>
+#include <limits>
 
 namespace ns3
 {
@@ -133,7 +134,8 @@
             // set infinite GRANTED_LEASED_TIME for my address
 
             myOwnAddress = ipv4->GetAddress(ifIndex, addrIndex).GetLocal();
-            m_leasedAddresses[Address()] = std::make_pair(myOwnAddress, 0xffffffff);
+            DhcpChaddr null = {};
+            m_leasedAddresses[null] = std::make_pair(myOwnAddress, PERMANENT_LEASE);
             break;
         }
     }
@@ -181,19 +183,18 @@
     NS_LOG_FUNCTION(this);
 
     // Set up timeout events and release of unsolicited addresses from the list
-    LeasedAddressIter i;
-    for (i = m_leasedAddresses.begin(); i != m_leasedAddresses.end(); i++)
+    for (auto [k, v] : m_leasedAddresses)
     {
         // update the address state
-        if (i->second.second != 0xffffffff && i->second.second != 0)
+        if (v.second != PERMANENT_LEASE && v.second != 0)
         {
-            i->second.second--;
-            if (i->second.second == 0)
+            v.second--;
+            if (v.second == 0)
             {
                 NS_LOG_INFO("Address leased state expired, address removed - "
-                            << "chaddr: " << i->first << " IP address " << i->second.first);
-                i->second.second = 0;
-                m_expiredAddresses.push_front(i->first);
+                            << "chaddr: " << DhcpChaddrToString(k) << " IP address " << v.first);
+                v.second = 0;
+                m_expiredAddresses.push_front(k);
             }
         }
     }
@@ -241,7 +242,7 @@
     NS_LOG_FUNCTION(this << iDev << header << from);
 
     DhcpHeader newDhcpHeader;
-    Address sourceChaddr = header.GetChaddr();
+    DhcpChaddr sourceChaddr = header.GetChaddr();
     uint32_t tran = header.GetTran();
     Ptr<Packet> packet = nullptr;
     Ipv4Address offeredAddress;
@@ -253,11 +254,11 @@
     {
         // We know this client from some time ago
         if (m_leasedAddresses[sourceChaddr].second != 0 &&
-            m_leasedAddresses[sourceChaddr].second != 0xffffffff)
+            m_leasedAddresses[sourceChaddr].second != PERMANENT_LEASE)
         {
             NS_LOG_LOGIC("This client is sending a DISCOVER but it has still a lease active - "
                          "perhaps it didn't shut down gracefully: "
-                         << sourceChaddr);
+                         << DhcpChaddrToString(sourceChaddr));
         }
 
         m_expiredAddresses.remove(sourceChaddr);
@@ -277,7 +278,7 @@
             // there's still hope: reuse the old ones.
             if (!m_expiredAddresses.empty())
             {
-                Address oldestChaddr = m_expiredAddresses.back();
+                DhcpChaddr oldestChaddr = m_expiredAddresses.back();
                 m_expiredAddresses.pop_back();
                 offeredAddress = m_leasedAddresses[oldestChaddr].first;
                 m_leasedAddresses.erase(oldestChaddr);
@@ -334,7 +335,7 @@
     NS_LOG_FUNCTION(this << iDev << header << from);
 
     DhcpHeader newDhcpHeader;
-    Address sourceChaddr = header.GetChaddr();
+    DhcpChaddr sourceChaddr = header.GetChaddr();
     uint32_t tran = header.GetTran();
     Ptr<Packet> packet = nullptr;
     Ipv4Address address = header.GetReq();
@@ -342,8 +343,7 @@
     NS_LOG_INFO("DHCP REQUEST from: " << from.GetIpv4() << " source port: " << from.GetPort()
                                       << " - refreshed addr: " << address);
 
-    LeasedAddressIter iter;
-    iter = m_leasedAddresses.find(sourceChaddr);
+    auto iter = m_leasedAddresses.find(sourceChaddr);
     if (iter != m_leasedAddresses.end())
     {
         // update the lease time of this address - send ACK
@@ -393,25 +393,20 @@
 }
 
 void
-DhcpServer::AddStaticDhcpEntry(Address chaddr, Ipv4Address addr)
+DhcpServer::AddStaticDhcpEntry(Address macAddr, Ipv4Address addr)
 {
-    NS_LOG_FUNCTION(this << chaddr << addr);
-    Address cleanedCaddr;
+    NS_LOG_FUNCTION(this << macAddr << addr);
+    DhcpChaddr chAddr{};
 
     NS_ASSERT_MSG(addr.Get() >= m_minAddress.Get() && addr.Get() <= m_maxAddress.Get(),
                   "Required address is not in the pool " << addr << " is not in [" << m_minAddress
                                                          << ", " << m_maxAddress << "]");
+    NS_ASSERT_MSG(macAddr.GetLength() <= 16,
+                  "DHCP server can not handle a chaddr larger than 16 bytes");
+    macAddr.CopyTo(chAddr.data());
 
-    // We need to cleanup the type from the stored chaddr, or later we'll fail to compare it.
-    // Moreover, the length is always 16, because chaddr is 16 bytes.
-    uint8_t buffer[Address::MAX_SIZE];
-    std::memset(buffer, 0, Address::MAX_SIZE);
-    uint32_t len = chaddr.CopyTo(buffer);
-    NS_ASSERT_MSG(len <= 16, "DHCP server can not handle a chaddr larger than 16 bytes");
-    cleanedCaddr.CopyFrom(buffer, 16);
-
-    NS_ASSERT_MSG(m_leasedAddresses.find(cleanedCaddr) == m_leasedAddresses.end(),
-                  "Client has already an active lease: " << m_leasedAddresses[cleanedCaddr].first);
+    NS_ASSERT_MSG(m_leasedAddresses.find(chAddr) == m_leasedAddresses.end(),
+                  "Client has already an active lease: " << m_leasedAddresses[chAddr].first);
 
     auto it = find(m_availableAddresses.begin(), m_availableAddresses.end(), addr);
     NS_ASSERT_MSG(
@@ -419,7 +414,8 @@
         "Required address is not available (perhaps it has been already assigned): " << addr);
 
     m_availableAddresses.remove(addr);
-    m_leasedAddresses[cleanedCaddr] = std::make_pair(addr, 0xffffffff);
+    NS_LOG_INFO("Added a static lease for " << DhcpChaddrToString(chAddr) << " -> " << addr);
+    m_leasedAddresses[chAddr] = std::make_pair(addr, PERMANENT_LEASE);
 }
 
 } // Namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/dhcp-server.h ns-3.47/src/internet-apps/model/dhcp-server.h
--- ns-3.46/src/internet-apps/model/dhcp-server.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/dhcp-server.h	2026-02-16 16:08:49.604230000 -0800
@@ -18,6 +18,7 @@
 #include "ns3/application.h"
 #include "ns3/ipv4-address.h"
 
+#include <limits>
 #include <map>
 
 namespace ns3
@@ -48,10 +49,10 @@
     /**
      * @brief Add a static entry to the pool.
      *
-     * @param chaddr The client chaddr.
-     * @param addr The address to handle to the client.
+     * @param macAddr The client MAC address.
+     * @param addr The IP address to handle to the client.
      */
-    void AddStaticDhcpEntry(Address chaddr, Ipv4Address addr);
+    void AddStaticDhcpEntry(Address macAddr, Ipv4Address addr);
 
   protected:
     void DoDispose() override;
@@ -97,21 +98,16 @@
     Ipv4Address m_gateway;     //!< The gateway address
 
     /// Leased address container - chaddr + IP addr / lease time
-    typedef std::map<Address, std::pair<Ipv4Address, uint32_t>> LeasedAddress;
-    /// Leased address iterator - chaddr + IP addr / lease time
-    typedef std::map<Address, std::pair<Ipv4Address, uint32_t>>::iterator LeasedAddressIter;
-    /// Leased address const iterator - chaddr + IP addr / lease time
-    typedef std::map<Address, std::pair<Ipv4Address, uint32_t>>::const_iterator LeasedAddressCIter;
+    using LeasedAddress = std::map<DhcpChaddr, std::pair<Ipv4Address, uint32_t>>;
 
     /// Expired address container - chaddr
-    typedef std::list<Address> ExpiredAddress;
-    /// Expired address iterator - chaddr
-    typedef std::list<Address>::iterator ExpiredAddressIter;
-    /// Expired address const iterator - chaddr
-    typedef std::list<Address>::const_iterator ExpiredAddressCIter;
+    using ExpiredAddress = std::list<DhcpChaddr>;
 
     /// Available address container - IP addr
-    typedef std::list<Ipv4Address> AvailableAddress;
+    using AvailableAddress = std::list<Ipv4Address>;
+
+    /// The maximum representable lease time means permanent lease.
+    static constexpr uint32_t PERMANENT_LEASE = std::numeric_limits<uint32_t>::max();
 
     LeasedAddress m_leasedAddresses;   //!< Leased address and their status (cache memory)
     ExpiredAddress m_expiredAddresses; //!< Expired addresses to be reused (chaddr of the clients)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/ping.cc ns-3.47/src/internet-apps/model/ping.cc
--- ns-3.46/src/internet-apps/model/ping.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/ping.cc	2026-02-16 16:08:49.604230000 -0800
@@ -585,8 +585,7 @@
         }
         else if (Ipv6Address::IsMatchingType(m_interfaceAddress))
         {
-            Inet6SocketAddress senderInet =
-                Inet6SocketAddress(Ipv6Address::ConvertFrom(m_interfaceAddress));
+            auto senderInet = Inet6SocketAddress(Ipv6Address::ConvertFrom(m_interfaceAddress));
             int status = m_socket->Bind(senderInet);
             NS_ASSERT_MSG(status == 0, "Failed to bind IPv6 socket");
         }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/model/v4traceroute.cc ns-3.47/src/internet-apps/model/v4traceroute.cc
--- ns-3.46/src/internet-apps/model/v4traceroute.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/model/v4traceroute.cc	2026-02-16 16:08:49.605229900 -0800
@@ -117,7 +117,7 @@
     NS_LOG_LOGIC("Application started");
     m_started = Simulator::Now();
 
-    NS_ABORT_MSG_IF(!m_remote.IsInitialized(), "'Remote' attribute not properly set");
+    NS_ABORT_MSG_IF(!m_remote.IsAny(), "'Remote' attribute not properly set");
 
     if (m_verbose)
     {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/internet-apps/test/dhcp6-test.cc ns-3.47/src/internet-apps/test/dhcp6-test.cc
--- ns-3.46/src/internet-apps/test/dhcp6-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/internet-apps/test/dhcp6-test.cc	2026-02-16 16:08:49.605229900 -0800
@@ -13,7 +13,6 @@
 #include "ns3/data-rate.h"
 #include "ns3/dhcp6-header.h"
 #include "ns3/dhcp6-helper.h"
-#include "ns3/header-serialization-test.h"
 #include "ns3/internet-apps-module.h"
 #include "ns3/internet-module.h"
 #include "ns3/internet-stack-helper.h"
@@ -26,7 +25,6 @@
 #include "ns3/simple-net-device-helper.h"
 #include "ns3/simple-net-device.h"
 #include "ns3/simulator.h"
-#include "ns3/ssid.h"
 #include "ns3/test.h"
 #include "ns3/trace-helper.h"
 
@@ -181,6 +179,106 @@
  * @ingroup dhcp6-test
  * @ingroup tests
  *
+ * @brief DHCPv6 DUID tests
+ */
+class Dhcp6DuidTestCase : public TestCase
+{
+  public:
+    Dhcp6DuidTestCase();
+    ~Dhcp6DuidTestCase() override;
+
+  private:
+    void DoRun() override;
+};
+
+Dhcp6DuidTestCase::Dhcp6DuidTestCase()
+    : TestCase("Dhcp6 DUID test case ")
+{
+}
+
+Dhcp6DuidTestCase::~Dhcp6DuidTestCase()
+{
+}
+
+void
+Dhcp6DuidTestCase::DoRun()
+{
+    NodeContainer nodes;
+    nodes.Create(2);
+
+    SimpleNetDeviceHelper simpleNetDevice;
+    simpleNetDevice.SetChannelAttribute("Delay", TimeValue(MilliSeconds(2)));
+    simpleNetDevice.SetDeviceAttribute("DataRate", DataRateValue(DataRate("5Mbps")));
+    NetDeviceContainer devices = simpleNetDevice.Install(nodes);
+
+    InternetStackHelper internetv6;
+    internetv6.Install(nodes);
+
+    Ipv6AddressHelper ipv6;
+    ipv6.SetBase(Ipv6Address("2001:cafe::"), Ipv6Prefix(64));
+    Ipv6InterfaceContainer i = ipv6.AssignWithoutAddress(devices);
+
+    Dhcp6Helper dhcp6Helper;
+    ApplicationContainer dhcpClients = dhcp6Helper.InstallDhcp6Client(nodes);
+
+    // DUID-LL
+    {
+        Duid duidTest;
+        duidTest.InitializeLL(nodes.Get(1));
+        std::ostringstream buffer;
+        buffer << duidTest;
+        NS_TEST_ASSERT_MSG_EQ(duidTest.GetSerializedSize(), 10, "Unexpected DUID-LL length.");
+        std::string reference{"DUID-LL HWtype: 1 Identifier: 0x000000000002"};
+        NS_TEST_ASSERT_MSG_EQ(buffer.str(), reference, "Unexpected DUID-LL.");
+    }
+
+    // DUID-LLT
+    {
+        Duid duidTest;
+        duidTest.InitializeLLT(nodes.Get(1));
+        std::ostringstream buffer;
+        buffer << duidTest;
+        NS_TEST_ASSERT_MSG_EQ(duidTest.GetSerializedSize(), 14, "Unexpected DUID-LLT length.");
+        std::string reference{"DUID-LLT HWtype: 1 Time: 0 Identifier: 0x000000000002"};
+        NS_TEST_ASSERT_MSG_EQ(buffer.str(), reference, "Unexpected DUID-LLT.");
+    }
+
+    // DUID-EN
+    {
+        Duid duidTest;
+        std::vector<uint8_t> identifier(32);
+        std::iota(identifier.begin(), identifier.end(), 0);
+
+        duidTest.InitializeEN(0xf00dcafe, identifier);
+        std::ostringstream buffer;
+        buffer << duidTest;
+        NS_TEST_ASSERT_MSG_EQ(duidTest.GetSerializedSize(), 38, "Unexpected DUID-EN length.");
+        std::string reference{"DUID-EN EnNumber: 0xf00dcafe Identifier: "
+                              "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"};
+        NS_TEST_ASSERT_MSG_EQ(buffer.str(), reference, "Unexpected DUID-EN.");
+    }
+
+    // DUID-UUID
+    {
+        Duid duidTest;
+        std::vector<uint8_t> identifier(16);
+        std::iota(identifier.begin(), identifier.end(), 0);
+
+        duidTest.InitializeUUID(identifier);
+        std::ostringstream buffer;
+        buffer << duidTest;
+        NS_TEST_ASSERT_MSG_EQ(duidTest.GetSerializedSize(), 18, "Unexpected DUID-UUID length.");
+        std::string reference{"DUID-UUID UUID: 0x000102030405060708090a0b0c0d0e0f"};
+        NS_TEST_ASSERT_MSG_EQ(buffer.str(), reference, "Unexpected DUID-UUID.");
+    }
+
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup dhcp6-test
+ * @ingroup tests
+ *
  * @brief DHCPv6 TestSuite
  */
 class Dhcp6TestSuite : public TestSuite
@@ -193,6 +291,7 @@
     : TestSuite("dhcp6", Type::UNIT)
 {
     AddTestCase(new Dhcp6TestCase, TestCase::Duration::QUICK);
+    AddTestCase(new Dhcp6DuidTestCase, TestCase::Duration::QUICK);
 }
 
 static Dhcp6TestSuite dhcp6TestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/CMakeLists.txt ns-3.47/src/lr-wpan/CMakeLists.txt
--- ns-3.46/src/lr-wpan/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/CMakeLists.txt	2026-02-16 16:08:49.627230000 -0800
@@ -41,7 +41,7 @@
     test/lr-wpan-ed-test.cc
     test/lr-wpan-error-model-test.cc
     test/lr-wpan-packet-test.cc
-    test/lr-wpan-pd-plme-sap-test.cc
+    test/lr-wpan-phy-test.cc
     test/lr-wpan-spectrum-value-helper-test.cc
     test/lr-wpan-ifs-test.cc
     test/lr-wpan-slotted-csmaca-test.cc
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/doc/lr-wpan.rst ns-3.47/src/lr-wpan/doc/lr-wpan.rst
--- ns-3.46/src/lr-wpan/doc/lr-wpan.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/doc/lr-wpan.rst	2026-02-16 16:08:49.628230000 -0800
@@ -7,8 +7,8 @@
 
 This chapter describes the implementation of |ns3| models for the
 low-rate, wireless personal area network (LR-WPAN) as specified by
-IEEE standard 802.15.4 (2003,2006,2011). The current emphasis is on direct transmissions running on both,
-slotted and unslotted mode (CSMA/CA) of IEEE 802.15.4 operation for use in Zigbee (TM) and 6loWPAN networks.
+IEEE standard 802.15.4 (2003,2006,2011 and a few features of 2015). The current emphasis is on direct transmissions running on both,
+slotted and unslotted mode (CSMA/CA) of IEEE 802.15.4 operation for use in Zigbee (TM) and 6LoWPAN networks.
 
 Both beacon and non-beacon modes are supported as well as the bootstrap mechanism (scan and association).
 
@@ -116,9 +116,6 @@
 
 The ``LrWpanNetDevice`` presented in this module is intended to be used with other technology profiles, such as the 6LoWPAN and ZigBee stack.
 
-
-
-
 Scope and Limitations
 ---------------------
 
@@ -133,11 +130,9 @@
 - Guaranteed Time Slots (GTS) are not supported.
 - Not all attributes are supported by the MLME-SET and MLME-GET primitives.
 - Indirect transmissions are only supported during the association process.
-- RSSI is not supported as this is part of the 2015 revision and the current implementation only supports until the 2011 revision.
 - PHY and MAC are currently not supported by the attribute system. To change the behavior of the PHY and MAC the standard SET primitives (e.g. MLME-SET.request) must be used.
 - No radio energy model is supported.
 - The PHY does not include sleep state.
-- Preamble is not fully modeled but its duration is considered.
 
 The PHY layer
 -------------
@@ -163,12 +158,40 @@
 noise across the frequency bands. The loss model can fully utilize all
 existing simple (non-spectrum phy) loss models. The Phy model uses
 the existing single spectrum channel model.
-The physical layer is modeled on packet level, that is, no preamble/SFD
-detection is done. Packet reception will be started with the first bit of the
-preamble (which is not modeled), if the SNR is more than -5 dB, see IEEE
-Std 802.15.4-2006, appendix E, Figure E.2. Reception of the packet will finish
-after the packet was completely transmitted. Other packets arriving during
-reception will add up to the interference/noise.
+The physical layer is modeled at packet level, meaning there are no
+I/Q samples, chips, correlators or real bit decoding. Reception is modeled as follows:
+The physical layer is modeled at packet level, meaning that no I/Q samples, chips, correlators, or explicit bit decoding are simulated.
+Reception is modeled as follows. The signal-to-interference-plus-noise ratio (SINR) is computed from the received signal power,
+the thermal noise floor, and the aggregate interference from other ongoing transmissions in the channel.
+
+At the end of the preamble, a preliminary SINR check is performed to determine whether the packet is decodable.
+If the SINR is greater than -5 dB (IEEE Std 802.15.4-2006, Appendix E, Figure E.2), the receiver locks onto the packet and continues reception.
+Otherwise, the packet is considered undecodable, dropped, and treated as interference for other ongoing receptions.
+
+For packets that pass the preamble check, interference is tracked over the duration of the packet.
+At the end of reception, the error rate model is applied using the accumulated SINR and packet parameters to probabilistically determine whether
+the packet is successfully received or corrupted.
+If the SNR is below the sensitivity threshold,
+the reception is labeled as failed (i.e., cannot be heard by the radio).
+
+The following figure shows the PPDU format and the relevant events and states during packet reception.
+
+::
+
+
+    +-------------+------+
+    |   Preamble  |  SFD |
+    +-------------+------+----------+------------------------+
+    |         SHR        |   PHR    |      Payload (PSDU)    |
+    |       (4 bytes)    | (1 byte) |     (0 to 127 bytes)   |
+    +--------------------+----------+------------------------+
+    ^             ^                                          ^
+    |             |                                          |
+    StartRx()   EndPreamble()                            EndRx() <==== ns-3 packet events
+
+    |-------------|------------------------------------------|   <==== PHY states during reception
+         RX_ON                     BUSY_RX
+
 
 Rx sensitivity is defined as the weakest possible signal point at which a receiver can receive and decode a packet with a high success rate.
 According to the standard (IEEE Std 802.15.4-2006, section 6.1.7), this
@@ -224,6 +247,101 @@
 Both short and extended addressing are supported. Various trace sources are
 supported, and trace sources can be hooked to sinks.
 
+MAC layer modes
+~~~~~~~~~~~~~~~
+
+The MAC layer can operate in two different modes: beacon-enabled mode and non-beacon mode.
+In beacon-enabled mode, the PAN coordinator periodically transmits beacons to
+synchronize the devices in the PAN. The beacons define superframe structures that
+organize the channel time into active and inactive periods. During the active period,
+the channel time is further divided into a contention access period (CAP) and an optional
+contention free period (CFP). In beacon mode, devices use slotted CSMA/CA to access the channel
+and take decisions on when to transmit or receive data based on the time slots of the superframe structure.
+For further details see [:ref:`2<lrwpanRef2>`] and [:ref:`4<lrwpanRef4>`].
+The following code snippet shows how to configure a PAN coordinator in beacon-enabled mode.
+
+::
+
+    NodeContainer coordinators;
+    coordinators.Create(1);
+
+    LrWpanHelper lrWpanHelper;
+    NetDeviceContainer lrwpanDevices = lrWpanHelper.Install(coordinators);
+    Ptr<LrWpanNetDevice> dev = lrwpanDevices.Get(0)->GetObject<LrWpanNetDevice>();
+
+    MlmeStartRequestParams params;
+    params.m_panCoor = true;
+    params.m_PanId = 5;
+    params.m_bcnOrd = 14; // Setting the beacon interval in time slots (2^14 time slots)
+    params.m_sfrmOrd = 6; // Setting the size of the active and inactive periods between beacons
+                          // (2^6 = 64 time slots)
+
+    dev->GetMac()->MlmeStartRequest(params); // Start the PAN coordinator with the given parameters
+
+In the example above, a PAN coordinator is created and configured to operate in beacon-enabled mode
+by setting the beacon order (``bcnOrd``) attribute to a value less than 15. The superframe order (``sfrmOrd``) attribute is set to 6,
+which means that the active period of the superframe will occupy 2^6 = 64 time slots out of the total 2^14 = 16384 slots. A single
+time slot is equivalent to 960 symbols (``aBaseSuperframeDuration``) or 15.36 ms (where each symbol is 16 us long at 62.5 ksymbols/s). The remaining slots
+(16384 - 64 = 16320 slots) will be inactive period where devices can enter a low power state to save energy.
+
+Devices maintain synchronization with the PAN coordinator by receiving the periodic beacons. If a device fails to receive
+a certain number of consecutive beacons, it considers that it has lost synchronization with the PAN coordinator
+and notifies the higher layers using the ``MLME-SYNC-LOSS.indication`` primitive. Decisions about how to recover from this situation
+are left to the higher layers (e.g., re-association).
+
+Finally, devices can also operate in non-beacon mode. Non-beacon mode is the default mode
+and implies that there is no limitation on when devices can receive or transmit data other than the constraints imposed by the
+unslotted CSMA/CA algorithm. This also implies that there are no inactive times.
+
+When both,the beacon order and superframe order attribute is set to 15 (default), the coordinator as well as any
+devices associated to the coordinator operate in non-beacon mode.
+
+The following shows a table with some time equivalents for different beacon order or superframe order values running
+on the 2.4 GHz band (symbol rate of 62.5 ksymbols/s):
+
++--------------+-------------------------+
+| BcnOrd       |     Time Interval       |
+| SfrmOrd      +                         +
+| exponents    |                         |
++==============+=========================+
+|      0       |        15.36 ms         |
++--------------+-------------------------+
+|      1       |        30.72 ms         |
++--------------+-------------------------+
+|      2       |        61.44 ms         |
++--------------+-------------------------+
+|      3       |        122.88 ms        |
++--------------+-------------------------+
+|      4       |        245.76 ms        |
++--------------+-------------------------+
+|      5       |        491.52 ms        |
++--------------+-------------------------+
+|      6       |        983.04 ms        |
++--------------+-------------------------+
+|      7       |        1.966 s          |
++--------------+-------------------------+
+|      8       |        3.932 s          |
++--------------+-------------------------+
+|      9       |        7.864 s          |
++--------------+-------------------------+
+|     10       |        15.728 s         |
++--------------+-------------------------+
+|     11       |        31.456 s         |
++--------------+-------------------------+
+|     12       |        62.912 s         |
++--------------+-------------------------+
+|     13       |        125.824 s        |
++--------------+-------------------------+
+|     14       |        251.648 s        |
++--------------+-------------------------+
+|     15       |      Non-beacon mode    |
++--------------+-------------------------+
+
+Later revisions of the standard (IEEE 802.15.4-2015) introduced a new modes of operation known as
+"behaviors". For example, the TSCH (Time Slotted Channel Hopping) behavior and the DSME
+(Deterministic and Synchronous Multi-Channel Extension) behavior. However, these new modes of operation are not
+currently supported by |ns3|.
+
 Scan and Association
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -244,7 +362,7 @@
 
 In LR-WPAN, association is used to join PANs. All devices in LR-WPAN must belong to a PAN to communicate. |ns3| uses a classic association procedure described in the standard. The standard also covers a more effective association procedure known as fast association (See IEEE 802.15.4-2015, fastA) but this association is currently not supported by |ns3|. Alternatively, |ns3| can do a "quick and dirty" association using either ```LrWpanHelper::AssociateToPan``` or ```LrWpanHelper::AssociateToBeaconPan```. These functions are used when a preset association can be done. For example, when the relationships between existing nodes and coordinators are known and can be set before the beginning of the simulation. In other situations, like in many networks in real deployments or in large networks, it is desirable that devices "associate themselves" with the best possible available coordinator candidates. This is a process known as bootstrap, and simulating this process makes it possible to demonstrate the kind of situations a node would face in which large networks to associate in real environment.
 
-Bootstrap (a.k.a. network initialization) is possible with a combination of scan and association MAC primitives. Details on the general process for this network initialization is described in the standard. Bootstrap is a complex process that not only requires the scanning networks, but also the exchange of command frames and the use of a pending transaction list (indirect transmissions) in the coordinator to store command frames. The following summarizes the whole process:
+Bootstrap (a.k.a. network initialization) [:ref:`2<lrwpanRef1>`] [:ref:`5<lrwpanRef5>`] is possible with a combination of scan and association MAC primitives. Details on the general process for this network initialization is described in the standard. Bootstrap is a complex process that not only requires the scanning networks, but also the exchange of command frames and the use of a pending transaction list (indirect transmissions) in the coordinator to store command frames. The following summarizes the whole process:
 
 .. _fig-lr-wpan-assocSequence:
 
@@ -525,13 +643,23 @@
 References
 ----------
 
+.. _lrwpanRef1:
+
 [`1 <https://ieeexplore.ieee.org/document/1700009>`_] Wireless Medium Access Control (MAC) and Physical Layer (PHY) Specifications for Low-Rate Wireless Personal Area Networks (WPANs), IEEE Computer Society, IEEE Std 802.15.4-2006, 8 September 2006.
 
+.. _lrwpanRef2:
+
 [`2 <https://ieeexplore.ieee.org/document/6012487>`_] IEEE Standard for Local and metropolitan area networks--Part 15.4: Low-Rate Wireless Personal Area Networks (LR-WPANs)," in IEEE Std 802.15.4-2011 (Revision of IEEE Std 802.15.4-2006) , vol., no., pp.1-314, 5 Sept. 2011, doi: 10.1109/IEEESTD.2011.6012487.
 
+.. _lrwpanRef3:
+
 [`3 <https://www.mdpi.com/2079-9292/11/24/4090>`_] J. Zheng and Myung J. Lee, "A comprehensive performance study of IEEE 802.15.4," Sensor Network Operations, IEEE Press, Wiley Interscience, Chapter 4, pp. 218-237, 2006.
 
+.. _lrwpanRef4:
+
 [`4 <https://dl.acm.org/doi/10.1145/3442555.3442574>`_] Alberto Gallegos Ramonet and Taku Noguchi. 2020. LR-WPAN: Beacon Enabled Direct Transmissions on Ns-3. In 2020 the 6th International Conference on Communication and Information Processing (ICCIP 2020). Association for Computing Machinery, New York, NY, USA, 115–122. https://doi.org/10.1145/3442555.3442574.
 
+.. _lrwpanRef5:
+
 [`5 <https://www.mdpi.com/2079-9292/11/24/4090>`_] Gallegos Ramonet, A.; Noguchi, T. Performance Analysis of IEEE 802.15.4 Bootstrap Process. Electronics 2022, 11, 4090. https://doi.org/10.3390/electronics11244090.
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/examples/lr-wpan-active-scan.cc ns-3.47/src/lr-wpan/examples/lr-wpan-active-scan.cc
--- ns-3.46/src/lr-wpan/examples/lr-wpan-active-scan.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/examples/lr-wpan-active-scan.cc	2026-02-16 16:08:49.628230000 -0800
@@ -79,6 +79,10 @@
 int
 main(int argc, char* argv[])
 {
+    // CommandLine options required by PyViz visualizer
+    CommandLine cmd(__FILE__);
+    cmd.Parse(argc, argv);
+
     LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_FUNC));
 
     // Create 2 PAN coordinator nodes, and 1 end device
@@ -115,17 +119,17 @@
     Ptr<ConstantPositionMobilityModel> coord1Mobility =
         CreateObject<ConstantPositionMobilityModel>();
     coord1Mobility->SetPosition(Vector(0, 0, 0));
-    coord1NetDevice->GetPhy()->SetMobility(coord1Mobility);
+    coord1->AggregateObject(coord1Mobility);
 
     Ptr<ConstantPositionMobilityModel> endNodeMobility =
         CreateObject<ConstantPositionMobilityModel>();
     endNodeMobility->SetPosition(Vector(100, 0, 0));
-    endNodeNetDevice->GetPhy()->SetMobility(endNodeMobility);
+    endNode->AggregateObject(endNodeMobility);
 
     Ptr<ConstantPositionMobilityModel> coord2Mobility =
         CreateObject<ConstantPositionMobilityModel>();
     coord2Mobility->SetPosition(Vector(206, 0, 0));
-    coord2NetDevice->GetPhy()->SetMobility(coord2Mobility);
+    coord2->AggregateObject(coord2Mobility);
 
     // MAC layer Callbacks hooks
     endNodeNetDevice->GetMac()->SetMlmeScanConfirmCallback(
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/examples/lr-wpan-bootstrap.cc ns-3.47/src/lr-wpan/examples/lr-wpan-bootstrap.cc
--- ns-3.46/src/lr-wpan/examples/lr-wpan-bootstrap.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/examples/lr-wpan-bootstrap.cc	2026-02-16 16:08:49.628230000 -0800
@@ -328,6 +328,10 @@
 int
 main(int argc, char* argv[])
 {
+    // CommandLine options required by PyViz visualizer
+    CommandLine cmd(__FILE__);
+    cmd.Parse(argc, argv);
+
     LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_FUNC | LOG_PREFIX_NODE));
 
     nodes.Create(100);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/examples/lr-wpan-data.cc ns-3.47/src/lr-wpan/examples/lr-wpan-data.cc
--- ns-3.46/src/lr-wpan/examples/lr-wpan-data.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/examples/lr-wpan-data.cc	2026-02-16 16:08:49.628230000 -0800
@@ -36,7 +36,9 @@
 static void
 DataIndication(McpsDataIndicationParams params, Ptr<Packet> p)
 {
-    NS_LOG_UNCOND("Received packet of size " << p->GetSize());
+    std::cout << "Received packet of size: " << p->GetSize()
+              << " | LQI: " << static_cast<uint16_t>(params.m_mpduLinkQuality)
+              << " | RSSI: " << static_cast<int16_t>(params.m_rssi) << " dBm\n";
 }
 
 /**
@@ -46,7 +48,7 @@
 static void
 DataConfirm(McpsDataConfirmParams params)
 {
-    NS_LOG_UNCOND("LrWpanMcpsDataConfirmStatus = " << static_cast<uint16_t>(params.m_status));
+    std::cout << "LrWpanMcpsDataConfirmStatus = " << static_cast<uint16_t>(params.m_status) << "\n";
 }
 
 /**
@@ -62,9 +64,9 @@
                         PhyEnumeration oldState,
                         PhyEnumeration newState)
 {
-    NS_LOG_UNCOND(context << " state change at " << now.As(Time::S) << " from "
-                          << LrWpanHelper::LrWpanPhyEnumerationPrinter(oldState) << " to "
-                          << LrWpanHelper::LrWpanPhyEnumerationPrinter(newState));
+    std::cout << context << " state change at " << now.As(Time::S) << " from "
+              << LrWpanHelper::LrWpanPhyEnumerationPrinter(oldState) << " to "
+              << LrWpanHelper::LrWpanPhyEnumerationPrinter(newState) << "\n";
 }
 
 int
@@ -73,6 +75,7 @@
     bool verbose = false;
     bool extended = false;
 
+    // CommandLine options are also required by PyViz visualizer
     CommandLine cmd(__FILE__);
 
     cmd.AddValue("verbose", "turn on all log components", verbose);
@@ -156,12 +159,12 @@
     Ptr<ConstantPositionMobilityModel> sender0Mobility =
         CreateObject<ConstantPositionMobilityModel>();
     sender0Mobility->SetPosition(Vector(0, 0, 0));
-    dev0->GetPhy()->SetMobility(sender0Mobility);
+    n0->AggregateObject(sender0Mobility);
+
     Ptr<ConstantPositionMobilityModel> sender1Mobility =
         CreateObject<ConstantPositionMobilityModel>();
-    // Configure position 10 m distance
     sender1Mobility->SetPosition(Vector(0, 10, 0));
-    dev1->GetPhy()->SetMobility(sender1Mobility);
+    n1->AggregateObject(sender1Mobility);
 
     McpsDataConfirmCallback cb0;
     cb0 = MakeCallback(&DataConfirm);
@@ -199,6 +202,7 @@
     }
     params.m_msduHandle = 0;
     params.m_txOptions = TX_OPTION_ACK;
+
     //  dev0->GetMac ()->McpsDataRequest (params, p0);
     Simulator::ScheduleWithContext(1,
                                    Seconds(0),
@@ -224,6 +228,7 @@
                                    params,
                                    p2);
 
+    Simulator::Stop(Seconds(10));
     Simulator::Run();
 
     Simulator::Destroy();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/examples/lr-wpan-error-distance-plot.cc ns-3.47/src/lr-wpan/examples/lr-wpan-error-distance-plot.cc
--- ns-3.46/src/lr-wpan/examples/lr-wpan-error-distance-plot.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/examples/lr-wpan-error-distance-plot.cc	2026-02-16 16:08:49.628230000 -0800
@@ -96,10 +96,12 @@
     cmd.Parse(argc, argv);
 
     os << "Packet (MSDU) size = " << packetSize << " bytes; tx power = " << txPower
-       << " dBm; channel = " << channelNumber << "; Rx sensitivity = " << rxSensitivity << " dBm";
+       << " dBm; channel = " << channelNumber;
 
     Gnuplot psrplot = Gnuplot("802.15.4-psr-distance.eps");
-    Gnuplot2dDataset psrdataset("802.15.4-psr-vs-distance");
+    std::ostringstream legendLabel;
+    legendLabel << rxSensitivity << " dBm";
+    Gnuplot2dDataset psrdataset(legendLabel.str());
 
     Ptr<Node> n0 = CreateObject<Node>();
     Ptr<Node> n1 = CreateObject<Node>();
@@ -130,6 +132,8 @@
     cb0 = MakeCallback(&LrWpanErrorDistanceCallback);
     dev1->GetMac()->SetMcpsDataIndicationCallback(cb0);
 
+    std::cout << "Generating distance plot, this might take a few seconds...\n";
+
     McpsDataRequestParams params;
     params.m_srcAddrMode = SHORT_ADDR;
     params.m_dstAddrMode = SHORT_ADDR;
@@ -160,15 +164,19 @@
 
     psrplot.SetTitle(os.str());
     psrplot.SetTerminal("postscript eps color enh \"Times-BoldItalic\"");
-    psrplot.SetLegend("distance (m)", "Packet Success Rate (PSR)");
+    psrplot.SetLegend("Distance (m)", "Packet Success Rate (PSR)");
     psrplot.SetExtra("set xrange [0:200]\n\
                       set yrange [0:1]\n\
                       set grid\n\
+                      set xtics 20\n\
+                      set key inside right top vertical Right noreverse enhanced autotitles nobox\n\
+                      set key title '{/:Bold Rx Sensitivity}'\n\
                       set style line 1 linewidth 5\n\
                       set style increment user");
     psrplot.GenerateOutput(berfile);
     berfile.close();
 
     Simulator::Destroy();
+    std::cout << "Distance plot generated\n";
     return 0;
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/examples/lr-wpan-orphan-scan.cc ns-3.47/src/lr-wpan/examples/lr-wpan-orphan-scan.cc
--- ns-3.46/src/lr-wpan/examples/lr-wpan-orphan-scan.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/examples/lr-wpan-orphan-scan.cc	2026-02-16 16:08:49.628230000 -0800
@@ -109,6 +109,10 @@
 int
 main(int argc, char* argv[])
 {
+    // CommandLine options required by PyViz visualizer
+    CommandLine cmd(__FILE__);
+    cmd.Parse(argc, argv);
+
     LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_FUNC));
 
     // Create 2 PAN coordinator nodes, and 1 end device
@@ -146,12 +150,12 @@
     Ptr<ConstantPositionMobilityModel> coord1Mobility =
         CreateObject<ConstantPositionMobilityModel>();
     coord1Mobility->SetPosition(Vector(0, 0, 0));
-    coord1NetDevice->GetPhy()->SetMobility(coord1Mobility);
+    coord1->AggregateObject(coord1Mobility);
 
     Ptr<ConstantPositionMobilityModel> endNodeMobility =
         CreateObject<ConstantPositionMobilityModel>();
     endNodeMobility->SetPosition(Vector(100, 0, 0));
-    endNodeNetDevice->GetPhy()->SetMobility(endNodeMobility);
+    endNode->AggregateObject(endNodeMobility);
 
     // MAC layer Callbacks hooks
     endNodeNetDevice->GetMac()->SetMlmeScanConfirmCallback(
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/examples/lr-wpan-phy-test.cc ns-3.47/src/lr-wpan/examples/lr-wpan-phy-test.cc
--- ns-3.46/src/lr-wpan/examples/lr-wpan-phy-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/examples/lr-wpan-phy-test.cc	2026-02-16 16:08:49.629230000 -0800
@@ -33,12 +33,13 @@
  * @param psduLength PSDU length
  * @param p packet
  * @param lqi link quality indication
+ * @param rssi received signal strength indicator
  */
 void
-ReceivePdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi)
+ReceivePdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi, int8_t rssi)
 {
-    NS_LOG_UNCOND("At: " << Simulator::Now() << " Received frame size: " << psduLength
-                         << " LQI: " << (uint16_t)lqi);
+    NS_LOG_UNCOND("At: " << Simulator::Now() << " Received frame size: " << psduLength << " LQI: "
+                         << static_cast<int16_t>(lqi) << " RSSI: " << static_cast<int16_t>(rssi));
 }
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-interference-helper.h ns-3.47/src/lr-wpan/model/lr-wpan-interference-helper.h
--- ns-3.46/src/lr-wpan/model/lr-wpan-interference-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-interference-helper.h	2026-02-16 16:08:49.629230000 -0800
@@ -40,6 +40,10 @@
 
     ~LrWpanInterferenceHelper();
 
+    // Delete copy constructor and assignment operator to avoid misuse
+    LrWpanInterferenceHelper(const LrWpanInterferenceHelper&) = delete;
+    LrWpanInterferenceHelper& operator=(const LrWpanInterferenceHelper&) = delete;
+
     /**
      * Add the given signal to the set of accumulated signals. Never add the same
      * signal more than once. The SpectrumModels of the signal and the one used
@@ -80,16 +84,6 @@
     Ptr<const SpectrumModel> GetSpectrumModel() const;
 
   private:
-    // Disable implicit copy constructors
-    /**
-     * @brief Copy constructor - defined and not implemented.
-     */
-    LrWpanInterferenceHelper(const LrWpanInterferenceHelper&);
-    /**
-     * @brief Copy constructor - defined and not implemented.
-     * @returns
-     */
-    LrWpanInterferenceHelper& operator=(const LrWpanInterferenceHelper&);
     /**
      * The helpers SpectrumModel.
      */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-mac-base.h ns-3.47/src/lr-wpan/model/lr-wpan-mac-base.h
--- ns-3.46/src/lr-wpan/model/lr-wpan-mac-base.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-mac-base.h	2026-02-16 16:08:49.629230000 -0800
@@ -330,7 +330,9 @@
 /**
  * @ingroup lr-wpan
  *
- * MCPS-DATA.indication params. See 7.1.1.3
+ * MCPS-DATA.indication params.
+ * See IEEE 802.15.4-2011, Section 7.1.1.1
+ * See IEEE 802.15.4-2015, Section 8.3.3 (e.g., RSSI)
  */
 struct McpsDataIndicationParams
 {
@@ -343,6 +345,9 @@
     Mac16Address m_dstAddr;            //!< Destination address
     Mac64Address m_dstExtAddr;         //!< Destination extended address
     uint8_t m_mpduLinkQuality{0};      //!< LQI value measured during reception of the MPDU
+    int8_t m_rssi{-128};               //!< RSSI value measured after the preamble
+                                       //!< during the reception of the packet SFD.
+                                       //!< (IEEE 802.15.4-2015 onwards)
     uint8_t m_dsn{0};                  //!< The DSN of the received data frame
 };
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-mac.cc ns-3.47/src/lr-wpan/model/lr-wpan-mac.cc
--- ns-3.46/src/lr-wpan/model/lr-wpan-mac.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-mac.cc	2026-02-16 16:08:49.630230000 -0800
@@ -242,8 +242,6 @@
     m_maxIndTxQueueSize = m_indTxQueue.max_size();
 
     m_uniformVar = CreateObject<UniformRandomVariable>();
-    m_macDsn = SequenceNumber8(m_uniformVar->GetInteger(0, 255));
-    m_macBsn = SequenceNumber8(m_uniformVar->GetInteger(0, 255));
     m_macBeaconPayload = {};
     m_macBeaconPayloadLength = 0;
     m_shortAddress = Mac16Address("FF:FF"); // FF:FF = The address is not assigned.
@@ -259,6 +257,8 @@
 {
     NS_LOG_FUNCTION(this);
 
+    m_macDsn = SequenceNumber8(m_uniformVar->GetInteger(0, 255));
+    m_macBsn = SequenceNumber8(m_uniformVar->GetInteger(0, 255));
     if (m_macRxOnWhenIdle)
     {
         m_phy->PlmeSetTRXStateRequest(IEEE_802_15_4_PHY_RX_ON);
@@ -564,7 +564,7 @@
         }
         p->AddTrailer(macTrailer);
 
-        Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+        auto txQElement = std::make_shared<TxQueueElement>();
         txQElement->txQMsduHandle = params.m_msduHandle;
         txQElement->txQPkt = p;
         EnqueueTxQElement(txQElement);
@@ -847,7 +847,7 @@
 
     commandPacket->AddTrailer(macTrailer);
 
-    Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+    auto txQElement = std::make_shared<TxQueueElement>();
     txQElement->txQPkt = commandPacket;
     EnqueueTxQElement(txQElement);
     CheckQueue();
@@ -1030,6 +1030,7 @@
 
 void
 LrWpanMac::ReceiveInPromiscuousMode(uint8_t lqi,
+                                    int8_t rssi,
                                     const LrWpanMacHeader& receivedMacHdr,
                                     Ptr<Packet> p)
 {
@@ -1046,6 +1047,7 @@
         params.m_mpduLinkQuality = lqi;
         params.m_srcPanId = receivedMacHdr.GetSrcPanId();
         params.m_srcAddrMode = receivedMacHdr.GetSrcAddrMode();
+        params.m_rssi = rssi;
 
         switch (params.m_srcAddrMode)
         {
@@ -1150,7 +1152,7 @@
         // Beacon as a result of a beacon request
         // The beacon shall be transmitted using CSMA/CA
         // IEEE 802.15.4-2011 (Section 5.1.2.1.2)
-        Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+        auto txQElement = std::make_shared<TxQueueElement>();
         txQElement->txQPkt = beaconPacket;
         EnqueueTxQElement(txQElement);
         CheckQueue();
@@ -1193,7 +1195,7 @@
 
     commandPacket->AddTrailer(macTrailer);
 
-    Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+    auto txQElement = std::make_shared<TxQueueElement>();
     txQElement->txQPkt = commandPacket;
     EnqueueTxQElement(txQElement);
     CheckQueue();
@@ -1234,7 +1236,7 @@
 
     commandPacket->AddTrailer(macTrailer);
 
-    Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+    auto txQElement = std::make_shared<TxQueueElement>();
     txQElement->txQPkt = commandPacket;
     EnqueueTxQElement(txQElement);
     CheckQueue();
@@ -1283,7 +1285,7 @@
 
     commandPacket->AddTrailer(macTrailer);
 
-    Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+    auto txQElement = std::make_shared<TxQueueElement>();
     txQElement->txQPkt = commandPacket;
     EnqueueTxQElement(txQElement);
     CheckQueue();
@@ -1342,7 +1344,7 @@
     commandPacket->AddTrailer(macTrailer);
 
     // Set the Command packet to be transmitted
-    Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+    auto txQElement = std::make_shared<TxQueueElement>();
     txQElement->txQPkt = commandPacket;
     EnqueueTxQElement(txQElement);
     CheckQueue();
@@ -1358,12 +1360,12 @@
 
     NS_ASSERT(receivedMacPayload.GetCommandFrameType() == CommandPayloadHeader::DATA_REQ);
 
-    Ptr<IndTxQueueElement> indTxQElement = Create<IndTxQueueElement>();
+    auto indTxQElement = std::make_shared<IndTxQueueElement>();
     bool elementFound;
     elementFound = DequeueInd(receivedMacHdr.GetExtSrcAddr(), indTxQElement);
     if (elementFound)
     {
-        Ptr<TxQueueElement> txQElement = Create<TxQueueElement>();
+        auto txQElement = std::make_shared<TxQueueElement>();
         txQElement->txQPkt = indTxQElement->txQPkt;
         m_txQueue.emplace_back(txQElement);
     }
@@ -2077,7 +2079,10 @@
 }
 
 void
-LrWpanMac::ReceiveData(uint8_t lqi, const LrWpanMacHeader& receivedMacHdr, Ptr<Packet> p)
+LrWpanMac::ReceiveData(uint8_t lqi,
+                       int8_t rssi,
+                       const LrWpanMacHeader& receivedMacHdr,
+                       Ptr<Packet> p)
 {
     NS_LOG_FUNCTION(this << lqi << p);
 
@@ -2090,6 +2095,7 @@
         params.m_mpduLinkQuality = lqi;
         params.m_srcPanId = receivedMacHdr.GetSrcPanId();
         params.m_srcAddrMode = receivedMacHdr.GetSrcAddrMode();
+        params.m_rssi = rssi;
 
         switch (params.m_srcAddrMode)
         {
@@ -2258,7 +2264,7 @@
         // Receive ACK to data packet
         if (!m_mcpsDataConfirmCallback.IsNull())
         {
-            Ptr<TxQueueElement> txQElement = m_txQueue.front();
+            std::shared_ptr<TxQueueElement> txQElement = m_txQueue.front();
             McpsDataConfirmParams confirmParams;
             confirmParams.m_msduHandle = txQElement->txQMsduHandle;
             confirmParams.m_status = MacStatus::SUCCESS;
@@ -2316,7 +2322,7 @@
             // check MAC is not in a IFS
             if (!m_ifsEvent.IsPending())
             {
-                Ptr<TxQueueElement> txQElement = m_txQueue.front();
+                std::shared_ptr<TxQueueElement> txQElement = m_txQueue.front();
                 m_txPkt = txQElement->txQPkt;
 
                 m_setMacState =
@@ -2392,7 +2398,7 @@
 }
 
 void
-LrWpanMac::PdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi)
+LrWpanMac::PdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi, int8_t rssi)
 {
     NS_ASSERT(m_macState == MAC_IDLE || m_macState == MAC_ACK_PENDING || m_macState == MAC_CSMA);
     NS_LOG_FUNCTION(this << psduLength << p << (uint16_t)lqi);
@@ -2443,7 +2449,7 @@
     if (m_macPromiscuousMode)
     {
         PrintReceivedPacket(receivedMacHdr);
-        ReceiveInPromiscuousMode(lqi, receivedMacHdr, p);
+        ReceiveInPromiscuousMode(lqi, rssi, receivedMacHdr, p);
         return;
     }
 
@@ -2646,7 +2652,7 @@
     }
     else if (receivedMacHdr.IsData())
     {
-        ReceiveData(lqi, receivedMacHdr, p);
+        ReceiveData(lqi, rssi, receivedMacHdr, p);
     }
     else if (receivedMacHdr.IsAcknowledgment() && m_txPkt && m_macState == MAC_ACK_PENDING)
     {
@@ -2684,7 +2690,7 @@
 }
 
 void
-LrWpanMac::EnqueueTxQElement(Ptr<TxQueueElement> txQElement)
+LrWpanMac::EnqueueTxQElement(std::shared_ptr<TxQueueElement> txQElement)
 {
     if (m_txQueue.size() < m_maxTxQueueSize)
     {
@@ -2708,7 +2714,7 @@
 void
 LrWpanMac::RemoveFirstTxQElement()
 {
-    Ptr<TxQueueElement> txQElement = m_txQueue.front();
+    std::shared_ptr<TxQueueElement> txQElement = m_txQueue.front();
     Ptr<const Packet> p = txQElement->txQPkt;
     m_numCsmacaRetry += m_csmaCa->GetNB() + 1;
 
@@ -2862,7 +2868,7 @@
         {
             // Maximum number of retransmissions has been reached.
             // remove the copy of the DATA packet that was just sent
-            Ptr<TxQueueElement> txQElement = m_txQueue.front();
+            std::shared_ptr<TxQueueElement> txQElement = m_txQueue.front();
             m_macTxDropTrace(txQElement->txQPkt);
             if (!m_mcpsDataConfirmCallback.IsNull())
             {
@@ -2888,7 +2894,7 @@
 void
 LrWpanMac::EnqueueInd(Ptr<Packet> p)
 {
-    Ptr<IndTxQueueElement> indTxQElement = Create<IndTxQueueElement>();
+    auto indTxQElement = std::make_shared<IndTxQueueElement>();
     LrWpanMacHeader peekedMacHdr;
     p->PeekHeader(peekedMacHdr);
 
@@ -2952,7 +2958,7 @@
 }
 
 bool
-LrWpanMac::DequeueInd(Mac64Address dst, Ptr<IndTxQueueElement> entry)
+LrWpanMac::DequeueInd(Mac64Address dst, std::shared_ptr<IndTxQueueElement> entry)
 {
     PurgeInd();
 
@@ -3027,7 +3033,7 @@
        << "    Frame type    |"
        << "    Expire time\n";
 
-    for (auto transaction : m_indTxQueue)
+    for (const auto& transaction : m_indTxQueue)
     {
         transaction->txQPkt->PeekHeader(peekedMacHdr);
         os << transaction->dstExtAddress << "           "
@@ -3062,7 +3068,7 @@
        << "    Dst PAN id    |"
        << "    Frame type    |\n";
 
-    for (auto transaction : m_txQueue)
+    for (const auto& transaction : m_txQueue)
     {
         transaction->txQPkt->PeekHeader(peekedMacHdr);
 
@@ -3251,7 +3257,7 @@
                 {
                     McpsDataConfirmParams confirmParams;
                     NS_ASSERT_MSG(!m_txQueue.empty(), "TxQsize = 0");
-                    Ptr<TxQueueElement> txQElement = m_txQueue.front();
+                    std::shared_ptr<TxQueueElement> txQElement = m_txQueue.front();
                     confirmParams.m_msduHandle = txQElement->txQMsduHandle;
                     confirmParams.m_status = MacStatus::SUCCESS;
                     m_mcpsDataConfirmCallback(confirmParams);
@@ -3356,7 +3362,7 @@
         if (!macHdr.IsAcknowledgment())
         {
             NS_ASSERT_MSG(!m_txQueue.empty(), "TxQsize = 0");
-            Ptr<TxQueueElement> txQElement = m_txQueue.front();
+            std::shared_ptr<TxQueueElement> txQElement = m_txQueue.front();
             m_macTxDropTrace(txQElement->txQPkt);
             if (!m_mcpsDataConfirmCallback.IsNull())
             {
@@ -3425,7 +3431,7 @@
 void
 LrWpanMac::PlmeSetTRXStateConfirm(PhyEnumeration status)
 {
-    NS_LOG_FUNCTION(this << status);
+    NS_LOG_FUNCTION(this << status << m_macState);
 
     if (m_macState == MAC_SENDING &&
         (status == IEEE_802_15_4_PHY_TX_ON || status == IEEE_802_15_4_PHY_SUCCESS))
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-mac.h ns-3.47/src/lr-wpan/model/lr-wpan-mac.h
--- ns-3.46/src/lr-wpan/model/lr-wpan-mac.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-mac.h	2026-02-16 16:08:49.630230000 -0800
@@ -289,8 +289,10 @@
      * @param psduLength number of bytes in the PSDU
      * @param p the packet to be transmitted
      * @param lqi Link quality (LQI) value measured during reception of the PPDU
+     * @param rssi The received signal strength indicator measured during the reception of the
+     * packet. Supported by IEEE 802.15.4-2015 onwards.
      */
-    void PdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi);
+    void PdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi, int8_t rssi);
 
     /**
      * IEEE 802.15.4-2006 section 6.2.1.2
@@ -741,7 +743,7 @@
     /**
      * Helper structure for managing transmission queue elements.
      */
-    struct TxQueueElement : public SimpleRefCount<TxQueueElement>
+    struct TxQueueElement
     {
         uint8_t txQMsduHandle; //!< MSDU Handle
         Ptr<Packet> txQPkt;    //!< Queued packet
@@ -750,7 +752,7 @@
     /**
      * Helper structure for managing pending transaction list elements (Indirect transmissions).
      */
-    struct IndTxQueueElement : public SimpleRefCount<IndTxQueueElement>
+    struct IndTxQueueElement
     {
         uint8_t seqNum;               //!< The sequence number of  the queued packet
         Mac16Address dstShortAddress; //!< The destination short Mac Address
@@ -763,10 +765,12 @@
      * Process a frame when promiscuous mode is active.
      *
      * @param lqi The LQI value of the received packet
+     * @param rssi The receive signal strength indicator in dBm.
      * @param receivedMacHdr The reference to the received MAC header
      * @param p The packet containing the MAC payload
      */
     void ReceiveInPromiscuousMode(uint8_t lqi,
+                                  int8_t rssi,
                                   const LrWpanMacHeader& receivedMacHdr,
                                   Ptr<Packet> p);
 
@@ -889,10 +893,14 @@
      * Used to process the reception of data.
      *
      * @param lqi The value of the link quality indicator (LQI) of the received packet.
+     * @param rssi The received signal strength indicator in dBm.
      * @param receivedMacHdr The reference to the received MAC header.
      * @param p The packet containing the data payload.
      */
-    void ReceiveData(uint8_t lqi, const LrWpanMacHeader& receivedMacHdr, Ptr<Packet> p);
+    void ReceiveData(uint8_t lqi,
+                     int8_t rssi,
+                     const LrWpanMacHeader& receivedMacHdr,
+                     Ptr<Packet> p);
 
     /**
      * Used to process an acknowledgment packet.
@@ -922,7 +930,7 @@
      *
      * @param txQElement The element added to the Tx Queue.
      */
-    void EnqueueTxQElement(Ptr<TxQueueElement> txQElement);
+    void EnqueueTxQElement(std::shared_ptr<TxQueueElement> txQElement);
 
     /**
      * Remove the tip of the transmission queue, including clean up related to the
@@ -974,7 +982,7 @@
      * @param entry The dequeued element from the pending transaction list.
      * @return The status of the dequeue
      */
-    bool DequeueInd(Mac64Address dst, Ptr<IndTxQueueElement> entry);
+    bool DequeueInd(Mac64Address dst, std::shared_ptr<IndTxQueueElement> entry);
 
     /**
      * Purge expired transactions from the pending transactions list.
@@ -1229,13 +1237,13 @@
     /**
      * The transmit queue used by the MAC.
      */
-    std::deque<Ptr<TxQueueElement>> m_txQueue;
+    std::deque<std::shared_ptr<TxQueueElement>> m_txQueue;
 
     /**
      * The indirect transmit queue used by the MAC pending messages (The pending transaction
      * list).
      */
-    std::deque<Ptr<IndTxQueueElement>> m_indTxQueue;
+    std::deque<std::shared_ptr<IndTxQueueElement>> m_indTxQueue;
 
     /**
      * The maximum size of the transmit queue.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-net-device.cc ns-3.47/src/lr-wpan/model/lr-wpan-net-device.cc
--- ns-3.46/src/lr-wpan/model/lr-wpan-net-device.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-net-device.cc	2026-02-16 16:08:49.630230000 -0800
@@ -80,6 +80,10 @@
     m_phy = CreateObject<LrWpanPhy>();
     m_csmaca = CreateObject<LrWpanCsmaCa>();
     m_mac = CreateObject<LrWpanMac>();
+
+    m_mac->SetPhy(m_phy);
+    m_mac->SetCsmaCa(m_csmaca);
+    m_csmaca->SetMac(m_mac);
 }
 
 LrWpanNetDevice::~LrWpanNetDevice()
@@ -122,10 +126,7 @@
     }
 
     NS_LOG_FUNCTION(this);
-    m_mac->SetPhy(m_phy);
-    m_mac->SetCsmaCa(m_csmaca);
     m_mac->SetMcpsDataIndicationCallback(MakeCallback(&LrWpanNetDevice::McpsDataIndication, this));
-    m_csmaca->SetMac(m_mac);
 
     Ptr<LrWpanErrorModel> model = CreateObject<LrWpanErrorModel>();
     m_phy->SetErrorModel(model);
@@ -540,6 +541,7 @@
     int64_t streamIndex = stream;
     streamIndex += m_csmaca->AssignStreams(stream);
     streamIndex += m_phy->AssignStreams(stream);
+    streamIndex += m_mac->AssignStreams(stream);
     NS_LOG_DEBUG("Number of assigned RV streams:  " << (streamIndex - stream));
     return (streamIndex - stream);
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-phy.cc ns-3.47/src/lr-wpan/model/lr-wpan-phy.cc
--- ns-3.46/src/lr-wpan/model/lr-wpan-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-phy.cc	2026-02-16 16:08:49.631230000 -0800
@@ -32,6 +32,8 @@
 #include "ns3/spectrum-channel.h"
 #include "ns3/spectrum-value.h"
 
+#include <array>
+
 namespace ns3
 {
 namespace lrwpan
@@ -43,20 +45,22 @@
 /**
  * The data and symbol rates for the different PHY options.
  * See Table 1 in section 6.1.1 IEEE 802.15.4-2006, IEEE 802.15.4c-2009, IEEE 802.15.4d-2009.
+ * You can check also Table 66 of 802.15.4-2011.
  * Bit rate is in kbit/s.  Symbol rate is in ksymbol/s.
  * The index follows LrWpanPhyOption (kb/s and ksymbol/s)
  */
-static const PhyDataAndSymbolRates dataSymbolRates[IEEE_802_15_4_INVALID_PHY_OPTION]{
-    {20.0, 20.0},
-    {40.0, 40.0},
-    {20.0, 20.0},
-    {250.0, 12.5},
-    {250.0, 50.0},
-    {250.0, 62.5},
-    {100.0, 25.0},
-    {250.0, 62.5},
-    {250.0, 62.5},
-};
+constexpr std::array<PhyDataAndSymbolRates, 10> dataSymbolRates{{
+    {20.0, 20.0},  // IEEE_802_15_4_868MHZ_BPSK
+    {40.0, 40.0},  // IEEE_802_15_4_915MHZ_BPSK
+    {20.0, 20.0},  // IEEE_802_15_4_950MHZ_BPSK
+    {250.0, 12.5}, // IEEE_802_15_4_868MHZ_ASK
+    {250.0, 50.0}, // IEEE_802_15_4_915MHZ_ASK
+    {250.0, 62.5}, // IEEE_802_15_4_780MHZ_OQPSK
+    {100.0, 25.0}, // IEEE_802_15_4_868MHZ_OQPSK
+    {250.0, 62.5}, // IEEE_802_15_4_915MHZ_OQPSK
+    {250.0, 62.5}, // IEEE_802_15_4_2_4GHZ_OQPSK
+    {0, 0},        // IEEE_802_15_4_INVALID_PHY_OPTION
+}};
 
 /**
  * The preamble, SFD, and PHR lengths in symbols for the different PHY options.
@@ -65,17 +69,18 @@
  * The PHR is 1 octet and it follows phySymbolsPerOctet in Table 23.
  * The index follows LrWpanPhyOption.
  */
-const PhyPpduHeaderSymbolNumber ppduHeaderSymbolNumbers[IEEE_802_15_4_INVALID_PHY_OPTION]{
-    {32.0, 8.0, 8.0},
-    {32.0, 8.0, 8.0},
-    {32.0, 8.0, 8.0},
-    {2.0, 1.0, 0.4},
-    {6.0, 1.0, 1.6},
-    {8.0, 2.0, 2.0},
-    {8.0, 2.0, 2.0},
-    {8.0, 2.0, 2.0},
-    {8.0, 2.0, 2.0},
-};
+constexpr std::array<PhyPpduHeaderSymbolNumber, 10> ppduHeaderSymbolNumbers{{
+    {32.0, 8.0, 8.0}, // IEEE_802_15_4_868MHZ_BPSK
+    {32.0, 8.0, 8.0}, // IEEE_802_15_4_915MHZ_BPSK
+    {32.0, 8.0, 8.0}, // IEEE_802_15_4_950MHZ_BPSK
+    {2.0, 1.0, 0.4},  // IEEE_802_15_4_868MHZ_ASK
+    {6.0, 1.0, 1.6},  // IEEE_802_15_4_915MHZ_ASK
+    {8.0, 2.0, 2.0},  // IEEE_802_15_4_780MHZ_OQPSK
+    {8.0, 2.0, 2.0},  // IEEE_802_15_4_868MHZ_OQPSK
+    {8.0, 2.0, 2.0},  // IEEE_802_15_4_915MHZ_OQPSK
+    {8.0, 2.0, 2.0},  // IEEE_802_15_4_2_4GHZ_OQPSK
+    {0, 0, 0},        // IEEE_802_15_4_INVALID_PHY_OPTION
+}};
 
 std::ostream&
 operator<<(std::ostream& os, const PhyEnumeration& state)
@@ -269,7 +274,7 @@
     m_pdDataRequest.Cancel();
 
     m_random = nullptr;
-    m_pdDataIndicationCallback = MakeNullCallback<void, uint32_t, Ptr<Packet>, uint8_t>();
+    m_pdDataIndicationCallback = MakeNullCallback<void, uint32_t, Ptr<Packet>, uint8_t, int8_t>();
     m_pdDataConfirmCallback = MakeNullCallback<void, PhyEnumeration>();
     m_plmeCcaConfirmCallback = MakeNullCallback<void, PhyEnumeration>();
     m_plmeEdConfirmCallback = MakeNullCallback<void, PhyEnumeration, uint8_t>();
@@ -354,70 +359,50 @@
 {
     NS_LOG_FUNCTION(this << spectrumRxParams);
 
-    if (!m_edRequest.IsExpired())
-    {
-        // Update the average receive power during ED.
-        Time now = Simulator::Now();
-        m_edPower.averagePower +=
-            LrWpanSpectrumValueHelper::TotalAvgPower(m_signal->GetSignalPsd(),
-                                                     m_phyPIBAttributes.phyCurrentChannel) *
-            (now - m_edPower.lastUpdate).GetTimeStep() / m_edPower.measurementLength.GetTimeStep();
-        m_edPower.lastUpdate = now;
-    }
-
+    // 1- Add the signal to the spectrum to keep track of the transmission regardless
+    // of the origin or the state of the receiving radio.
+    m_signal->AddSignal(spectrumRxParams->psd);
+
+    // 2- Update peak power if CCA or ED if it is in progress.
+    UpdateEnergyTracking();
+
+    // 3- Check the radio is on the right state to receceive (RX_ON) and that the signal
+    // is a lr-wpan signal, otherwise proceed to the end of the packet to be removed from
+    // the accumulated signals in the spectrum tracker.
     Ptr<LrWpanSpectrumSignalParameters> lrWpanRxParams =
         DynamicCast<LrWpanSpectrumSignalParameters>(spectrumRxParams);
 
-    if (!lrWpanRxParams)
+    if (!lrWpanRxParams || m_trxState != IEEE_802_15_4_PHY_RX_ON)
     {
-        CheckInterference();
-        m_signal->AddSignal(spectrumRxParams->psd);
-
-        // Update peak power if CCA is in progress.
-        if (!m_ccaRequest.IsExpired())
-        {
-            double power =
-                LrWpanSpectrumValueHelper::TotalAvgPower(m_signal->GetSignalPsd(),
-                                                         m_phyPIBAttributes.phyCurrentChannel);
-            if (m_ccaPeakPower < power)
-            {
-                m_ccaPeakPower = power;
-            }
-        }
-
+        NS_LOG_DEBUG("Non-Lr-wpan signal or radio is not on RX_ON, do not process");
         Simulator::Schedule(spectrumRxParams->duration, &LrWpanPhy::EndRx, this, spectrumRxParams);
         return;
     }
 
+    // The reception of the lr-wpan preamble start here
+    NS_LOG_INFO("Lr-wpan signal detected, processing preamble for a duration of "
+                << lrWpanRxParams->preambleDuration.GetSeconds() << " seconds");
+
+    Simulator::Schedule(lrWpanRxParams->preambleDuration,
+                        &LrWpanPhy::EndPreamble,
+                        this,
+                        lrWpanRxParams);
+}
+
+void
+LrWpanPhy::EndPreamble(Ptr<LrWpanSpectrumSignalParameters> lrWpanRxParams)
+{
+    NS_LOG_FUNCTION(this << lrWpanRxParams);
+
+    // If in progress, update CCA peak power or ED measurement.
+    UpdateEnergyTracking();
+
     Ptr<Packet> p = (lrWpanRxParams->packetBurst->GetPackets()).front();
     NS_ASSERT(p);
 
     // Prevent PHY from receiving another packet while switching the transceiver state.
     if (m_trxState == IEEE_802_15_4_PHY_RX_ON && !m_setTRXState.IsPending())
     {
-        // The specification doesn't seem to refer to BUSY_RX, but vendor
-        // data sheets suggest that this is a substate of the RX_ON state
-        // that is entered after preamble detection when the digital receiver
-        // is enabled.  Here, for now, we use BUSY_RX to mark the period between
-        // StartRx() and EndRx() states.
-
-        // We are going to BUSY_RX state when receiving the first bit of an SHR,
-        // as opposed to real receivers, which should go to this state only after
-        // successfully receiving the SHR.
-
-        // If synchronizing to the packet is possible, change to BUSY_RX state,
-        // otherwise drop the packet and stay in RX state. The actual synchronization
-        // is not modeled.
-
-        // Add any incoming packet to the current interference before checking the
-        // SINR.
-        NS_LOG_DEBUG(this << " receiving packet with power: "
-                          << 10 * log10(LrWpanSpectrumValueHelper::TotalAvgPower(
-                                      lrWpanRxParams->psd,
-                                      m_phyPIBAttributes.phyCurrentChannel)) +
-                                 30
-                          << "dBm");
-        m_signal->AddSignal(lrWpanRxParams->psd);
         Ptr<SpectrumValue> interferenceAndNoise = m_signal->GetSignalPsd();
         *interferenceAndNoise -= *lrWpanRxParams->psd;
         *interferenceAndNoise += *m_noise;
@@ -427,62 +412,64 @@
             LrWpanSpectrumValueHelper::TotalAvgPower(interferenceAndNoise,
                                                      m_phyPIBAttributes.phyCurrentChannel);
 
+        // Figure out if packet is decodable based on SINR.
         // Std. 802.15.4-2006, appendix E, Figure E.2
         // At SNR < -5 the BER is less than 10e-1.
         // It's useless to even *try* to decode the packet.
         if (10 * log10(sinr) > -5)
         {
             ChangeTrxState(IEEE_802_15_4_PHY_BUSY_RX);
+
+            NS_LOG_INFO("Preamble processing completed,"
+                        << " initiating the reception of the rests of the packet");
+
+            // Start tracking the packet interference
             m_currentRxPacket = std::make_pair(lrWpanRxParams, false);
             m_phyRxBeginTrace(p);
 
             m_rxLastUpdate = Simulator::Now();
+
+            // Calculate RSSI
+            // In theory, the RSSI calculation should be done in the PHR, not at the end
+            // of the preamble. However since there is not much difference in ns-3, it
+            // is done here for simplicity (We avoid doing an extra endSFD event)
+            m_rssi = static_cast<int8_t>(10 * log10(LrWpanSpectrumValueHelper::TotalAvgPower(
+                                                  m_signal->GetSignalPsd(),
+                                                  m_phyPIBAttributes.phyCurrentChannel)) +
+                                         30);
         }
         else
         {
+            // The packet is *undecodable* drop it.
+            m_currentRxPacket = std::make_pair(lrWpanRxParams, true);
             m_phyRxDropTrace(p);
+            NS_LOG_INFO(this << " packet undecodable due to low SINR: " << 10 * log10(sinr)
+                             << " dB");
         }
     }
     else if (m_trxState == IEEE_802_15_4_PHY_BUSY_RX)
     {
-        // Drop the new packet.
-        NS_LOG_DEBUG(this << " packet collision");
+        // We are already receiving a packet, drop the new packet.
+        NS_LOG_INFO(this << " Packet collision detected, dropping packet");
         m_phyRxDropTrace(p);
 
-        // Check if we correctly received the old packet up to now.
+        // Another lr-wpan packet is being transmitted in the vecinity while we are receiving
+        // another packet. This affects the reception of the first packet, therefore,
+        // check interference and its impact on this packet.
         CheckInterference();
-
-        // Add the incoming packet to the current interference after we have
-        // checked for successful reception of the current packet for the time
-        // before the additional interference.
-        m_signal->AddSignal(lrWpanRxParams->psd);
     }
     else
     {
-        // Simply drop the packet.
-        NS_LOG_DEBUG(this << " transceiver not in RX state");
+        // The current policy to process the preamble is first come first served.
+        // Therefore, if we are not in RX_ON or BUSY_RX state, we drop the packet.
+        // Other policies can be implemented in future extensions.
+        NS_LOG_DEBUG(this << " transceiver not in RX state, currently in " << m_trxState
+                          << " state, dropping packet");
         m_phyRxDropTrace(p);
-
-        // Add the signal power to the interference, anyway.
-        m_signal->AddSignal(lrWpanRxParams->psd);
-    }
-
-    // Update peak power if CCA is in progress.
-    if (!m_ccaRequest.IsExpired())
-    {
-        double power =
-            LrWpanSpectrumValueHelper::TotalAvgPower(m_signal->GetSignalPsd(),
-                                                     m_phyPIBAttributes.phyCurrentChannel);
-        if (m_ccaPeakPower < power)
-        {
-            m_ccaPeakPower = power;
-        }
     }
 
-    // Always call EndRx to update the interference.
-    // We keep track of this event, and if necessary cancel this event when a TX of a packet.
-
-    Simulator::Schedule(spectrumRxParams->duration, &LrWpanPhy::EndRx, this, spectrumRxParams);
+    Time endRx = (lrWpanRxParams->duration - lrWpanRxParams->preambleDuration);
+    Simulator::Schedule(endRx, &LrWpanPhy::EndRx, this, lrWpanRxParams);
 }
 
 void
@@ -536,11 +523,20 @@
 }
 
 void
-LrWpanPhy::EndRx(Ptr<SpectrumSignalParameters> par)
+LrWpanPhy::UpdateEnergyTracking()
 {
-    NS_LOG_FUNCTION(this);
-
-    Ptr<LrWpanSpectrumSignalParameters> params = DynamicCast<LrWpanSpectrumSignalParameters>(par);
+    // Update CCA peak power
+    if (!m_ccaRequest.IsExpired())
+    {
+        double power =
+            LrWpanSpectrumValueHelper::TotalAvgPower(m_signal->GetSignalPsd(),
+                                                     m_phyPIBAttributes.phyCurrentChannel);
+        if (m_ccaPeakPower < power)
+        {
+            m_ccaPeakPower = power;
+            NS_LOG_DEBUG("Updating CCA peak power " << WToDbm(power) << " dBm");
+        }
+    }
 
     if (!m_edRequest.IsExpired())
     {
@@ -552,6 +548,18 @@
             (now - m_edPower.lastUpdate).GetTimeStep() / m_edPower.measurementLength.GetTimeStep();
         m_edPower.lastUpdate = now;
     }
+}
+
+void
+LrWpanPhy::EndRx(Ptr<SpectrumSignalParameters> par)
+{
+    NS_LOG_FUNCTION(this << "End of packet signal after a duration: " << par->duration.GetSeconds()
+                         << " seconds");
+
+    Ptr<LrWpanSpectrumSignalParameters> params = DynamicCast<LrWpanSpectrumSignalParameters>(par);
+
+    // Update peak power if CCA or ED if it is in progress.
+    UpdateEnergyTracking();
 
     Ptr<LrWpanSpectrumSignalParameters> currentRxParams = m_currentRxPacket.first;
     if (currentRxParams == params)
@@ -559,7 +567,8 @@
         CheckInterference();
     }
 
-    // Update the interference.
+    // The packet reception has ended, remove it from the accumulated signals
+    // in the receptor tracker
     m_signal->RemoveSignal(par->psd);
 
     if (!params)
@@ -591,12 +600,17 @@
         {
             m_currentRxPacket = std::make_pair(nullptr, true);
             ChangeTrxState(IEEE_802_15_4_PHY_RX_ON);
-            NS_LOG_DEBUG("Packet successfully received");
+
+            NS_LOG_INFO(" Packet successfully received,"
+                        << "passing received packet to upper layer");
 
             // The packet was successfully received, push it up the stack.
             if (!m_pdDataIndicationCallback.IsNull())
             {
-                m_pdDataIndicationCallback(currentPacket->GetSize(), currentPacket, tag.Get());
+                m_pdDataIndicationCallback(currentPacket->GetSize(),
+                                           currentPacket,
+                                           tag.Get(),
+                                           m_rssi);
             }
         }
         else
@@ -653,6 +667,10 @@
 
             Ptr<LrWpanSpectrumSignalParameters> txParams = Create<LrWpanSpectrumSignalParameters>();
             txParams->duration = CalculateTxTime(p);
+
+            double preambleSymbols = ppduHeaderSymbolNumbers[m_phyOption].shrPreamble;
+            txParams->preambleDuration = Seconds(preambleSymbols / GetDataOrSymbolRate(false));
+
             txParams->txPhy = GetObject<SpectrumPhy>();
             txParams->psd = m_txPsd;
             txParams->txAntenna = m_antenna;
@@ -844,8 +862,6 @@
     // a packet being actively received)
     if (state == IEEE_802_15_4_PHY_TRX_OFF)
     {
-        CancelEd(state);
-
         if ((m_trxState == IEEE_802_15_4_PHY_BUSY_RX) && (m_currentRxPacket.first) &&
             (!m_currentRxPacket.second))
         {
@@ -855,6 +871,7 @@
         }
         else if (m_trxState == IEEE_802_15_4_PHY_RX_ON || m_trxState == IEEE_802_15_4_PHY_TX_ON)
         {
+            CancelEd(state);
             ChangeTrxState(IEEE_802_15_4_PHY_TRX_OFF);
             if (!m_plmeSetTRXStateConfirmCallback.IsNull())
             {
@@ -1348,6 +1365,15 @@
 
     m_trxStateLogger(Simulator::Now(), m_trxState, newState);
     m_trxState = newState;
+
+    if (newState == PhyEnumeration::IEEE_802_15_4_PHY_RX_ON &&
+        m_trxStatePending == PhyEnumeration::IEEE_802_15_4_PHY_TRX_OFF)
+    {
+        // The MAC is set on RxOnWhenIdle = false, therefore when need to turn off
+        // the radio
+        NS_LOG_DEBUG("Turning off radio, appliying pending state");
+        ChangeTrxState(IEEE_802_15_4_PHY_TRX_OFF);
+    }
 }
 
 bool
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-phy.h ns-3.47/src/lr-wpan/model/lr-wpan-phy.h
--- ns-3.46/src/lr-wpan/model/lr-wpan-phy.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-phy.h	2026-02-16 16:08:49.631230000 -0800
@@ -191,8 +191,10 @@
  * @param psduLength number of bytes in the PSDU
  * @param p the packet to be transmitted
  * @param lqi Link quality (LQI) value measured during reception of the PPDU
+ * @param rssi The received signal strength indicator (RSSI) measured during the last received
+ * packet.
  */
-typedef Callback<void, uint32_t, Ptr<Packet>, uint8_t> PdDataIndicationCallback;
+typedef Callback<void, uint32_t, Ptr<Packet>, uint8_t, int8_t> PdDataIndicationCallback;
 
 /**
  * @ingroup lr-wpan
@@ -587,6 +589,14 @@
     PhyOption GetMyPhyOption();
 
     /**
+     * During the reception of the preamble, if it was found that the signal was strong enough,
+     * this function is called to schedule the end of the preamble.
+     *
+     * @param lrWpanRxParams The spectrum signal parameters associated to this lr-wpan packet.
+     */
+    void EndPreamble(Ptr<LrWpanSpectrumSignalParameters> lrWpanRxParams);
+
+    /**
      * Finish the transmission of a frame. This is called at the end of a frame
      * transmission, applying possibly pending PHY state changes and firing the
      * appropriate trace sources and confirm callbacks to the MAC.
@@ -600,6 +610,15 @@
     void CheckInterference();
 
     /**
+     * Update Clear Channel Assestment (CCA) or Energy Detection (ED) energy values
+     * if an energy tracking event is currently in process. This function is called
+     * during the different events that form the reception of a packet
+     * (e.g., Preamble, payload)
+     *
+     */
+    void UpdateEnergyTracking();
+
+    /**
      * Finish the reception of a frame. This is called at the end of a frame
      * reception, applying possibly pending PHY state changes and firing the
      * appropriate trace sources and indication callbacks to the MAC. A frame
@@ -882,12 +901,19 @@
     EdPower m_edPower;
 
     /**
+     * The  Received Signal Strength Indicator value in dBm of the
+     * last received packet. This is calculated during the SHR portion of the packet.
+     * This value has a typical range of -100 dBm to 0 dBm
+     */
+    int8_t m_rssi;
+
+    /**
      * Helper value for the peak power value during CCA.
      */
     double m_ccaPeakPower;
 
     /**
-     * The receiver sensitivity.
+     * The receiver sensitivity in Watts.
      */
     double m_rxSensitivity;
 
@@ -949,7 +975,10 @@
      */
     Ptr<UniformRandomVariable> m_random;
 
-    Ptr<ErrorModel> m_postReceptionErrorModel; //!< Error model for receive packet events
+    /**
+     * Error model for receive packet events
+     */
+    Ptr<ErrorModel> m_postReceptionErrorModel;
 };
 
 } // namespace lrwpan
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.cc ns-3.47/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.cc
--- ns-3.46/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.cc	2026-02-16 16:08:49.631230000 -0800
@@ -24,7 +24,8 @@
 
 LrWpanSpectrumSignalParameters::LrWpanSpectrumSignalParameters(
     const LrWpanSpectrumSignalParameters& p)
-    : SpectrumSignalParameters(p)
+    : SpectrumSignalParameters(p),
+      preambleDuration(p.preambleDuration)
 {
     NS_LOG_FUNCTION(this << &p);
     packetBurst = p.packetBurst->Copy();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.h ns-3.47/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.h
--- ns-3.46/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/model/lr-wpan-spectrum-signal-parameters.h	2026-02-16 16:08:49.631230000 -0800
@@ -43,6 +43,11 @@
      * The packet burst being transmitted with this signal
      */
     Ptr<PacketBurst> packetBurst;
+
+    /**
+     * The duration of the preamble according to the PHY option selected.
+     */
+    Time preambleDuration;
 };
 
 } // namespace lrwpan
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/test/lr-wpan-error-model-test.cc ns-3.47/src/lr-wpan/test/lr-wpan-error-model-test.cc
--- ns-3.46/src/lr-wpan/test/lr-wpan-error-model-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/test/lr-wpan-error-model-test.cc	2026-02-16 16:08:49.631230000 -0800
@@ -76,7 +76,7 @@
 };
 
 LrWpanErrorDistanceTestCase::LrWpanErrorDistanceTestCase()
-    : TestCase("Test the 802.15.4 error model vs distance"),
+    : TestCase("IEEE 802.15.4 distance error model test"),
       m_received(0)
 {
 }
@@ -135,7 +135,7 @@
 
     Ptr<Packet> p;
     mob0->SetPosition(Vector(0, 0, 0));
-    mob1->SetPosition(Vector(100, 0, 0));
+    mob1->SetPosition(Vector(95, 0, 0));
     for (int i = 0; i < 1000; i++)
     {
         p = Create<Packet>(20);
@@ -144,9 +144,14 @@
 
     Simulator::Run();
 
-    // Test that we received 977 packets out of 1000, at distance of 100 m
-    // with default power of 0
-    NS_TEST_ASSERT_MSG_EQ(GetReceived(), 977, "Model fails");
+    // Test that we received fewer packets than 1000, at distance less than 100 m
+    // with default Tx power of 0 dBm using the default maximum sensitivity (-106.58 dBm).
+    NS_TEST_ASSERT_MSG_GT_OR_EQ(GetReceived(),
+                                990,
+                                "Not enough packets received, distance model failure");
+    NS_TEST_ASSERT_MSG_LT_OR_EQ(GetReceived(),
+                                1000,
+                                "Too many packets received, distance model failure");
 
     Simulator::Destroy();
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/test/lr-wpan-mac-test.cc ns-3.47/src/lr-wpan/test/lr-wpan-mac-test.cc
--- ns-3.46/src/lr-wpan/test/lr-wpan-mac-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/test/lr-wpan-mac-test.cc	2026-02-16 16:08:49.631230000 -0800
@@ -147,9 +147,10 @@
     // do not attempt to do multiple backoffs delays in its CSMA,
     // macMinBE and MacMaxCSMABackoffs has been set to 0.
 
-    LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_FUNC));
+    LogComponentEnableAll(LogLevel(LOG_PREFIX_NODE | LOG_PREFIX_TIME | LOG_PREFIX_FUNC));
 
     LogComponentEnable("LrWpanMac", LOG_LEVEL_DEBUG);
+    LogComponentEnable("LrWpanPhy", LOG_LEVEL_DEBUG);
     LogComponentEnable("LrWpanCsmaCa", LOG_LEVEL_DEBUG);
     LogComponentEnable("lr-wpan-mac-test", LOG_LEVEL_DEBUG);
 
@@ -251,7 +252,7 @@
     params.m_msduHandle = 0;
 
     // Send the packet that will be rejected due to channel access failure
-    Simulator::ScheduleWithContext(1,
+    Simulator::ScheduleWithContext(dev0->GetNode()->GetId(),
                                    Seconds(0.00033),
                                    &LrWpanMac::McpsDataRequest,
                                    dev0->GetMac(),
@@ -262,7 +263,7 @@
     Ptr<Packet> p2 = Create<Packet>(60); // 60 bytes of dummy data
     params.m_dstAddr = Mac16Address("00:02");
 
-    Simulator::ScheduleWithContext(2,
+    Simulator::ScheduleWithContext(dev2->GetNode()->GetId(),
                                    Seconds(0),
                                    &LrWpanMac::McpsDataRequest,
                                    dev2->GetMac(),
@@ -345,7 +346,7 @@
      *      [00:01]                       [00:02]                             [00:03]
      *  PAN Coordinator 1 (PAN: 5)       End Device                  PAN Coordinator 2 (PAN: 7)
      *
-     *       |--------100 m----------------|----------106 m -----------------------|
+     *       |--------90 m----------------|----------188 m -----------------------|
      *  Channel 12               (Active Scan channels 11-14)                 Channel 14
      *
      * Test Setup:
@@ -407,12 +408,12 @@
 
     Ptr<ConstantPositionMobilityModel> endNodeMobility =
         CreateObject<ConstantPositionMobilityModel>();
-    endNodeMobility->SetPosition(Vector(100, 0, 0));
+    endNodeMobility->SetPosition(Vector(90, 0, 0));
     endNodeNetDevice->GetPhy()->SetMobility(endNodeMobility);
 
     Ptr<ConstantPositionMobilityModel> coord2Mobility =
         CreateObject<ConstantPositionMobilityModel>();
-    coord2Mobility->SetPosition(Vector(206, 0, 0));
+    coord2Mobility->SetPosition(Vector(188, 0, 0));
     coord2NetDevice->GetPhy()->SetMobility(coord2Mobility);
 
     // MAC layer Callbacks hooks
@@ -436,7 +437,7 @@
     params.m_bcnOrd = 15;
     params.m_sfrmOrd = 15;
     params.m_logCh = 12;
-    Simulator::ScheduleWithContext(1,
+    Simulator::ScheduleWithContext(coord1NetDevice->GetNode()->GetId(),
                                    Seconds(2),
                                    &LrWpanMac::MlmeStartRequest,
                                    coord1NetDevice->GetMac(),
@@ -459,7 +460,7 @@
     params2.m_bcnOrd = 15;
     params2.m_sfrmOrd = 15;
     params2.m_logCh = 14;
-    Simulator::ScheduleWithContext(2,
+    Simulator::ScheduleWithContext(coord2NetDevice->GetNode()->GetId(),
                                    Seconds(2),
                                    &LrWpanMac::MlmeStartRequest,
                                    coord2NetDevice->GetMac(),
@@ -476,7 +477,7 @@
     scanParams.m_scanChannels = 0x7800;
     scanParams.m_scanDuration = 14;
     scanParams.m_scanType = MLMESCAN_ACTIVE;
-    Simulator::ScheduleWithContext(1,
+    Simulator::ScheduleWithContext(endNodeNetDevice->GetNode()->GetId(),
                                    Seconds(3),
                                    &LrWpanMac::MlmeScanRequest,
                                    endNodeNetDevice->GetMac(),
@@ -636,7 +637,7 @@
 
     Ptr<ConstantPositionMobilityModel> endNodeMobility =
         CreateObject<ConstantPositionMobilityModel>();
-    endNodeMobility->SetPosition(Vector(100, 0, 0));
+    endNodeMobility->SetPosition(Vector(95, 0, 0));
     endNodeNetDevice->GetPhy()->SetMobility(endNodeMobility);
 
     // MAC layer Callbacks hooks
@@ -659,7 +660,7 @@
     params.m_bcnOrd = 15;
     params.m_sfrmOrd = 15;
     params.m_logCh = 12;
-    Simulator::ScheduleWithContext(1,
+    Simulator::ScheduleWithContext(coord1NetDevice->GetNode()->GetId(),
                                    Seconds(2),
                                    &LrWpanMac::MlmeStartRequest,
                                    coord1NetDevice->GetMac(),
@@ -678,7 +679,7 @@
     scanParams.m_chPage = 0;
     scanParams.m_scanChannels = 0x7800;
     scanParams.m_scanType = MLMESCAN_ORPHAN;
-    Simulator::ScheduleWithContext(1,
+    Simulator::ScheduleWithContext(endNodeNetDevice->GetNode()->GetId(),
                                    Seconds(3),
                                    &LrWpanMac::MlmeScanRequest,
                                    endNodeNetDevice->GetMac(),
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/test/lr-wpan-pd-plme-sap-test.cc ns-3.47/src/lr-wpan/test/lr-wpan-pd-plme-sap-test.cc
--- ns-3.46/src/lr-wpan/test/lr-wpan-pd-plme-sap-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lr-wpan/test/lr-wpan-pd-plme-sap-test.cc	1969-12-31 16:00:00.000000000 -0800
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2011 The Boeing Company
- *
- * SPDX-License-Identifier: GPL-2.0-only
- *
- * Author: Gary Pei <guangyu.pei@boeing.com>
- */
-#include "ns3/log.h"
-#include "ns3/lr-wpan-mac.h"
-#include "ns3/lr-wpan-phy.h"
-#include "ns3/packet.h"
-#include "ns3/simulator.h"
-#include "ns3/single-model-spectrum-channel.h"
-#include "ns3/test.h"
-
-using namespace ns3;
-using namespace ns3::lrwpan;
-
-/**
- * @ingroup lr-wpan-test
- * @ingroup tests
- *
- * @brief LrWpan PLME and PD Interfaces Test
- */
-class LrWpanPlmeAndPdInterfaceTestCase : public TestCase
-{
-  public:
-    LrWpanPlmeAndPdInterfaceTestCase();
-    ~LrWpanPlmeAndPdInterfaceTestCase() override;
-
-  private:
-    void DoRun() override;
-
-    /**
-     * @brief Receives a PdData indication
-     * @param psduLength The PSDU length.
-     * @param p The packet.
-     * @param lqi The LQI.
-     */
-    void ReceivePdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi);
-};
-
-LrWpanPlmeAndPdInterfaceTestCase::LrWpanPlmeAndPdInterfaceTestCase()
-    : TestCase("Test the PLME and PD SAP per IEEE 802.15.4")
-{
-}
-
-LrWpanPlmeAndPdInterfaceTestCase::~LrWpanPlmeAndPdInterfaceTestCase()
-{
-}
-
-void
-LrWpanPlmeAndPdInterfaceTestCase::ReceivePdDataIndication(uint32_t psduLength,
-                                                          Ptr<Packet> p,
-                                                          uint8_t lqi)
-{
-    NS_LOG_UNCOND("At: " << Simulator::Now() << " Received frame size: " << psduLength
-                         << " LQI: " << lqi);
-}
-
-void
-LrWpanPlmeAndPdInterfaceTestCase::DoRun()
-{
-    LogComponentEnableAll(LOG_PREFIX_FUNC);
-    LogComponentEnable("LrWpanPhy", LOG_LEVEL_ALL);
-
-    Ptr<LrWpanPhy> sender = CreateObject<LrWpanPhy>();
-    Ptr<LrWpanPhy> receiver = CreateObject<LrWpanPhy>();
-
-    Ptr<SingleModelSpectrumChannel> channel = CreateObject<SingleModelSpectrumChannel>();
-    sender->SetChannel(channel);
-    receiver->SetChannel(channel);
-
-    receiver->SetPdDataIndicationCallback(
-        MakeCallback(&LrWpanPlmeAndPdInterfaceTestCase::ReceivePdDataIndication, this));
-
-    uint32_t n = 10;
-    Ptr<Packet> p = Create<Packet>(n);
-    sender->PdDataRequest(p->GetSize(), p);
-
-    Simulator::Destroy();
-}
-
-/**
- * @ingroup lr-wpan-test
- * @ingroup tests
- *
- * @brief LrWpan PLME and PD Interfaces TestSuite
- */
-class LrWpanPlmeAndPdInterfaceTestSuite : public TestSuite
-{
-  public:
-    LrWpanPlmeAndPdInterfaceTestSuite();
-};
-
-LrWpanPlmeAndPdInterfaceTestSuite::LrWpanPlmeAndPdInterfaceTestSuite()
-    : TestSuite("lr-wpan-plme-pd-sap", Type::UNIT)
-{
-    AddTestCase(new LrWpanPlmeAndPdInterfaceTestCase, TestCase::Duration::QUICK);
-}
-
-// Do not forget to allocate an instance of this TestSuite
-static LrWpanPlmeAndPdInterfaceTestSuite
-    g_lrWpanPlmeAndPdInterfaceTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lr-wpan/test/lr-wpan-phy-test.cc ns-3.47/src/lr-wpan/test/lr-wpan-phy-test.cc
--- ns-3.46/src/lr-wpan/test/lr-wpan-phy-test.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/lr-wpan/test/lr-wpan-phy-test.cc	2026-02-16 16:08:49.631230000 -0800
@@ -0,0 +1,252 @@
+/*
+ * Copyright (c) 2011 The Boeing Company
+ * Copyright (c) 2026 Tokushima University, Japan: Collision test and refactoring
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Authors: Gary Pei <guangyu.pei@boeing.com>
+ *          Alberto Gallegos Ramonet <alramonet@is.tokushima-u.ac.jp>
+ */
+#include "ns3/log.h"
+#include "ns3/lr-wpan-mac.h"
+#include "ns3/lr-wpan-phy.h"
+#include "ns3/packet.h"
+#include "ns3/simulator.h"
+#include "ns3/single-model-spectrum-channel.h"
+#include "ns3/test.h"
+
+using namespace ns3;
+using namespace ns3::lrwpan;
+
+/**
+ * @ingroup lr-wpan-test
+ * @ingroup tests
+ *
+ * @brief LrWpan PLME and PD Interfaces Test
+ */
+class LrWpanPlmeAndPdInterfaceTestCase : public TestCase
+{
+  public:
+    LrWpanPlmeAndPdInterfaceTestCase();
+    ~LrWpanPlmeAndPdInterfaceTestCase() override;
+
+  private:
+    uint16_t m_receivedPackets{0}; //!< Counts the number of received packets
+    void DoRun() override;
+
+    /**
+     * @brief Receives a PdData indication
+     * @param psduLength The PSDU length.
+     * @param p The packet.
+     * @param lqi The LQI.
+     * @param rssi The RSSI
+     */
+    void ReceivePdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi, int8_t rssi);
+};
+
+/**
+ * @ingroup lr-wpan-test
+ * @ingroup tests
+ *
+ * @brief Test the collision of two packets and the reaction ( packet drop )
+ *        of this collision in the packet preamble
+ */
+class LrWpanPhyCollisionTestCase : public TestCase
+{
+  public:
+    LrWpanPhyCollisionTestCase();
+    ~LrWpanPhyCollisionTestCase() override;
+
+  private:
+    uint16_t m_receivedPackets{0}; //!< Counts the number of received packets
+    int8_t m_receivedRssi{0};      //!< Saves the value of the received packet RSSI
+    uint16_t m_packetDroppedByCollision{
+        0}; //!< Counts the number of packets dropped due to collision
+    void DoRun() override;
+
+    /**
+     * @brief Receives a PdData indication
+     * @param psduLength The PSDU length.
+     * @param p The packet.
+     * @param lqi The LQI.
+     * @param rssi The RSSI
+     */
+    void ReceivePdDataIndication(uint32_t psduLength, Ptr<Packet> p, uint8_t lqi, int8_t rssi);
+
+    /**
+     * @brief Callback for packet drop due to collision
+     * @param p The packet.
+     */
+    void PacketCollisionDrop(Ptr<const Packet> p);
+};
+
+LrWpanPlmeAndPdInterfaceTestCase::LrWpanPlmeAndPdInterfaceTestCase()
+    : TestCase("PLME and PD SAP IEEE 802.15.4 interfaces test")
+{
+}
+
+LrWpanPlmeAndPdInterfaceTestCase::~LrWpanPlmeAndPdInterfaceTestCase()
+{
+}
+
+void
+LrWpanPlmeAndPdInterfaceTestCase::ReceivePdDataIndication(uint32_t psduLength,
+                                                          Ptr<Packet> p,
+                                                          uint8_t lqi,
+                                                          int8_t rssi)
+{
+    NS_LOG_UNCOND("At: " << Simulator::Now() << " Received frame size: " << psduLength
+                         << " LQI: " << static_cast<uint16_t>(lqi)
+                         << " RSSI: " << static_cast<int16_t>(rssi) << " dBm.");
+
+    m_receivedPackets++;
+}
+
+void
+LrWpanPlmeAndPdInterfaceTestCase::DoRun()
+{
+    // This test create two Lr-WPAN PHYs and tests a simple transmission and reception
+    // of a packet. It tests that the PHY interfaces (PLME primitives) are working.
+
+    LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_FUNC));
+    LogComponentEnable("LrWpanPhy", LOG_LEVEL_ALL);
+
+    Ptr<LrWpanPhy> sender = CreateObject<LrWpanPhy>();
+    Ptr<LrWpanPhy> receiver = CreateObject<LrWpanPhy>();
+
+    // Add the SpectrumPhy to a channel and set the initial state of the PHYs
+    // This is typically taken care by the NetDevice but in this case we are using the PHY directly.
+    Ptr<SingleModelSpectrumChannel> channel = CreateObject<SingleModelSpectrumChannel>();
+    sender->SetChannel(channel);
+    receiver->SetChannel(channel);
+    channel->AddRx(receiver);
+    sender->PlmeSetTRXStateRequest(IEEE_802_15_4_PHY_TX_ON);
+    receiver->PlmeSetTRXStateRequest(IEEE_802_15_4_PHY_RX_ON);
+
+    receiver->SetPdDataIndicationCallback(
+        MakeCallback(&LrWpanPlmeAndPdInterfaceTestCase::ReceivePdDataIndication, this));
+
+    Ptr<Packet> p = Create<Packet>(10); // Packet with 10 bytes
+    Simulator::Schedule(Seconds(1), &LrWpanPhy::PdDataRequest, sender, p->GetSize(), p);
+
+    Simulator::Stop(Seconds(5));
+    Simulator::Run();
+
+    NS_TEST_ASSERT_MSG_GT(m_receivedPackets,
+                          0,
+                          "Error,the number of expected received packets should be more than 0");
+    Simulator::Destroy();
+}
+
+LrWpanPhyCollisionTestCase::LrWpanPhyCollisionTestCase()
+    : TestCase("Collision of two IEEE 802.15.4 frames test")
+{
+}
+
+LrWpanPhyCollisionTestCase::~LrWpanPhyCollisionTestCase()
+{
+}
+
+void
+LrWpanPhyCollisionTestCase::ReceivePdDataIndication(uint32_t psduLength,
+                                                    Ptr<Packet> p,
+                                                    uint8_t lqi,
+                                                    int8_t rssi)
+{
+    NS_LOG_UNCOND("At: " << Simulator::Now() << " Received frame size: " << psduLength
+                         << " LQI: " << static_cast<uint16_t>(lqi)
+                         << " RSSI: " << static_cast<int16_t>(rssi) << " dBm.");
+
+    m_receivedPackets++;
+    m_receivedRssi = rssi;
+}
+
+void
+LrWpanPhyCollisionTestCase::PacketCollisionDrop(Ptr<const Packet> p)
+{
+    m_packetDroppedByCollision++;
+}
+
+void
+LrWpanPhyCollisionTestCase::DoRun()
+{
+    // Test the reaction of the PHY to receiving two packets simultaneously.
+    // Since the receiver device will be in BUSY_RX state after the first packet
+    // the 2nd packet will not be received. The first packet should be received correctly and
+    // the second packet should be dropped due to collision and registered
+    // in the drop trace (PhyRxDrop).
+    // The second packet also affects the received RSSI
+    // of the first packet since more energy will be present at the moment of receiving the packet.
+    // This change on energy is also tested.
+
+    LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_FUNC));
+    LogComponentEnable("LrWpanPhy", LOG_LEVEL_ALL);
+
+    Ptr<LrWpanPhy> sender = CreateObject<LrWpanPhy>();
+    Ptr<LrWpanPhy> sender2 = CreateObject<LrWpanPhy>();
+    Ptr<LrWpanPhy> receiver = CreateObject<LrWpanPhy>();
+
+    // Add the SpectrumPhy to a channel and set the initial state of the PHYs
+    // This is typically taken care by the NetDevice but in this case we are using the PHY directly.
+    Ptr<SingleModelSpectrumChannel> channel = CreateObject<SingleModelSpectrumChannel>();
+    sender->SetChannel(channel);
+    sender2->SetChannel(channel);
+    receiver->SetChannel(channel);
+    channel->AddRx(receiver);
+
+    sender->PlmeSetTRXStateRequest(IEEE_802_15_4_PHY_TX_ON);
+    sender2->PlmeSetTRXStateRequest(IEEE_802_15_4_PHY_TX_ON);
+    receiver->PlmeSetTRXStateRequest(IEEE_802_15_4_PHY_RX_ON);
+
+    // Set the callback to receive packets at the receiver PHY
+    receiver->SetPdDataIndicationCallback(
+        MakeCallback(&LrWpanPhyCollisionTestCase::ReceivePdDataIndication, this));
+
+    // Connect to trace source to log when a packet is dropped due to collision
+    receiver->TraceConnectWithoutContext(
+        "PhyRxDrop",
+        MakeCallback(&LrWpanPhyCollisionTestCase::PacketCollisionDrop, this));
+
+    Ptr<Packet> p = Create<Packet>(10); // 10 bytes
+    Ptr<Packet> p2 = Create<Packet>(9); // 9 bytes
+    Simulator::Schedule(Seconds(2), &LrWpanPhy::PdDataRequest, sender, p->GetSize(), p);
+    Simulator::Schedule(Seconds(2), &LrWpanPhy::PdDataRequest, sender2, p2->GetSize(), p2);
+
+    Simulator::Stop(Seconds(5));
+    Simulator::Run();
+
+    NS_TEST_ASSERT_MSG_EQ(m_receivedPackets,
+                          1,
+                          "Error,the receiver should only be able to receive the first packet");
+
+    NS_TEST_ASSERT_MSG_EQ(m_packetDroppedByCollision,
+                          1,
+                          "Error, one packet should be dropped due to collision");
+
+    NS_TEST_ASSERT_MSG_GT(m_receivedRssi, 0, "Error,the received RSSI should be more than 0");
+
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup lr-wpan-test
+ * @ingroup tests
+ *
+ * @brief LrWpan PLME and PD Interfaces TestSuite
+ */
+class LrWpanPlmeAndPdInterfaceTestSuite : public TestSuite
+{
+  public:
+    LrWpanPlmeAndPdInterfaceTestSuite();
+};
+
+LrWpanPlmeAndPdInterfaceTestSuite::LrWpanPlmeAndPdInterfaceTestSuite()
+    : TestSuite("lr-wpan-phy-test", Type::UNIT)
+{
+    // AddTestCase(new LrWpanPlmeAndPdInterfaceTestCase, TestCase::Duration::QUICK);
+    AddTestCase(new LrWpanPhyCollisionTestCase, TestCase::Duration::QUICK);
+}
+
+// Do not forget to allocate an instance of this TestSuite
+static LrWpanPlmeAndPdInterfaceTestSuite
+    g_lrWpanPlmeAndPdInterfaceTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/doc/source/lte-design.rst ns-3.47/src/lte/doc/source/lte-design.rst
--- ns-3.46/src/lte/doc/source/lte-design.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/doc/source/lte-design.rst	2026-02-16 16:08:49.657230100 -0800
@@ -551,8 +551,8 @@
 Let :math:`f_c` denote the  LTE Absolute Radio Frequency Channel Number, which
 identifies the carrier frequency on a 100 kHz raster; furthermore, let :math:`B` be
 the Transmission Bandwidth Configuration in number of Resource Blocks. For every
-pair :math:`(f_c,B)` used in the simulation we define a corresponding SpectrumModel using
-the functionality provided by the :ref:`sec-spectrum-module` .
+pair :math:`(f_c,B)` used in the simulation we define a corresponding ``SpectrumModel`` using
+the functionality provided by the :ref:`sec-spectrum-module`.
 model using the Spectrum framework described
 in [Baldo2009]_.  :math:`f_c` and :math:`B` can be configured for every eNB instantiated
 in the simulation; hence, each eNB can use a different spectrum model. Every UE
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/doc/source/lte-user.rst ns-3.47/src/lte/doc/source/lte-user.rst
--- ns-3.46/src/lte/doc/source/lte-user.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/doc/source/lte-user.rst	2026-02-16 16:08:49.657230100 -0800
@@ -50,10 +50,10 @@
 
 #. Initial boilerplate::
 
-    #include <ns3/core-module.h>
-    #include <ns3/network-module.h>
-    #include <ns3/mobility-module.h>
-    #include <ns3/lte-module.h>
+    #include "ns3/core-module.h"
+    #include "ns3/network-module.h"
+    #include "ns3/mobility-module.h"
+    #include "ns3/lte-module.h"
 
     using namespace ns3;
 
@@ -526,9 +526,9 @@
 
 #. Header files to be included::
 
-    #include <ns3/mobility-building-info.h>
-    #include <ns3/buildings-propagation-loss-model.h>
-    #include <ns3/building.h>
+    #include "ns3/mobility-building-info.h"
+    #include "ns3/buildings-propagation-loss-model.h"
+    #include "ns3/building.h"
 
 #. Pathloss model selection::
 
@@ -1292,8 +1292,7 @@
 
    std::vector<uint8_t> measIdList;
 
-   NetDeviceContainer::Iterator it;
-   for (it = devs.Begin(); it != devs.End(); it++)
+   for (auto it = devs.Begin(); it != devs.End(); it++)
    {
      Ptr<NetDevice> dev = *it;
      Ptr<LteEnbNetDevice> enbDev = dev->GetObject<LteEnbNetDevice>();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/examples/lena-radio-link-failure.cc ns-3.47/src/lte/examples/lena-radio-link-failure.cc
--- ns-3.46/src/lte/examples/lena-radio-link-failure.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/examples/lena-radio-link-failure.cc	2026-02-16 16:08:49.658230000 -0800
@@ -470,9 +470,16 @@
     int64_t randomStream = 1;
 
     enbDevs = lteHelper->InstallEnbDevice(enbNodes);
-    randomStream += lteHelper->AssignStreams(enbDevs, randomStream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    lteHelper->AssignStreams(enbDevs, randomStream, false);
+    randomStream += 1000;
     ueDevs = lteHelper->InstallUeDevice(ueNodes);
-    randomStream += lteHelper->AssignStreams(ueDevs, randomStream);
+    lteHelper->AssignStreams(ueDevs, randomStream, true);
+    randomStream += 1000;
 
     NS_LOG_INFO("Install the IP stack on the UEs");
     internet.Install(ueNodes);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/helper/lte-helper.cc ns-3.47/src/lte/helper/lte-helper.cc
--- ns-3.46/src/lte/helper/lte-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/helper/lte-helper.cc	2026-02-16 16:08:49.659230000 -0800
@@ -130,7 +130,8 @@
             .AddAttribute("PathlossModel",
                           "The type of pathloss model to be used. "
                           "The allowed values for this attributes are the type names "
-                          "of any class inheriting from ns3::PropagationLossModel.",
+                          "of any class inheriting from ns3::PropagationLossModel "
+                          "or ns3::SpectrumPropagationLossModel.",
                           TypeIdValue(FriisPropagationLossModel::GetTypeId()),
                           MakeTypeIdAccessor(&LteHelper::SetPathlossModelType),
                           MakeTypeIdChecker())
@@ -215,6 +216,12 @@
     return m_downlinkChannel;
 }
 
+Ptr<EpcHelper>
+LteHelper::GetEpcHelper() const
+{
+    return m_epcHelper;
+}
+
 void
 LteHelper::ChannelModelInitialization()
 {
@@ -1174,7 +1181,7 @@
  * UE change its RRC state to CONNECTED_NORMALLY, activation
  * function is called and bearer is activated.
  */
-class DrbActivator : public SimpleRefCount<DrbActivator>
+class DrbActivator
 {
   public:
     /**
@@ -1195,7 +1202,7 @@
      * @param cellId
      * @param rnti
      */
-    static void ActivateCallback(Ptr<DrbActivator> a,
+    static void ActivateCallback(std::shared_ptr<DrbActivator> a,
                                  std::string context,
                                  uint64_t imsi,
                                  uint16_t cellId,
@@ -1243,7 +1250,7 @@
 }
 
 void
-DrbActivator::ActivateCallback(Ptr<DrbActivator> a,
+DrbActivator::ActivateCallback(std::shared_ptr<DrbActivator> a,
                                std::string context,
                                uint64_t imsi,
                                uint16_t cellId,
@@ -1294,7 +1301,7 @@
     std::ostringstream path;
     path << "/NodeList/" << enbLteDevice->GetNode()->GetId() << "/DeviceList/"
          << enbLteDevice->GetIfIndex() << "/LteEnbRrc/ConnectionEstablished";
-    Ptr<DrbActivator> arg = Create<DrbActivator>(ueDevice, bearer);
+    auto arg = std::make_shared<DrbActivator>(ueDevice, bearer);
     Config::Connect(path.str(), MakeBoundCallback(&DrbActivator::ActivateCallback, arg));
 }
 
@@ -1553,8 +1560,9 @@
 }
 
 int64_t
-LteHelper::AssignStreams(NetDeviceContainer c, int64_t stream)
+LteHelper::AssignStreams(NetDeviceContainer c, int64_t stream, bool assignEpcStreams)
 {
+    NS_LOG_FUNCTION(this << stream << assignEpcStreams);
     int64_t currentStream = stream;
     if (m_fadingModel && !m_fadingStreamsAssigned)
     {
@@ -1594,7 +1602,7 @@
             currentStream += ueMac->AssignStreams(currentStream);
         }
     }
-    if (m_epcHelper)
+    if (m_epcHelper && assignEpcStreams)
     {
         currentStream += m_epcHelper->AssignStreams(currentStream);
     }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/helper/lte-helper.h ns-3.47/src/lte/helper/lte-helper.h
--- ns-3.46/src/lte/helper/lte-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/helper/lte-helper.h	2026-02-16 16:08:49.659230000 -0800
@@ -648,15 +648,16 @@
      * If TraceFadingLossModel has been set as the fading model type, this method
      * will also assign a stream number to it, if none has been assigned before.
      *
-     * If an EPC has been configured, streams will be assigned on the EPC
-     * nodes as well via EpcHelper::AssignStreams ().
+     * If an EPC has been configured, streams will be assigned on the internet stack
+     * of EPC nodes, by calling EpcHelper::AssignStreams(), if assignEpcStreams is true.
      *
      * @param c NetDeviceContainer of the set of net devices for which the
      *          LteNetDevice should be modified to use a fixed stream
      * @param stream first stream index to use
+     * @param assignEpcStreams Whether to call AssignStreams on the associated EpcHelper
      * @return the number of stream indices (possibly zero) that have been assigned
      */
-    int64_t AssignStreams(NetDeviceContainer c, int64_t stream);
+    int64_t AssignStreams(NetDeviceContainer c, int64_t stream, bool assignEpcStreams = true);
 
     /**
      * @return a pointer to the SpectrumChannel instance used for the uplink
@@ -668,6 +669,11 @@
      */
     Ptr<SpectrumChannel> GetDownlinkSpectrumChannel() const;
 
+    /**
+     * @return a pointer to the EpcHelper instance used, if any
+     */
+    Ptr<EpcHelper> GetEpcHelper() const;
+
   protected:
     // inherited from Object
     void DoInitialize() override;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/helper/radio-bearer-stats-connector.cc ns-3.47/src/lte/helper/radio-bearer-stats-connector.cc
--- ns-3.46/src/lte/helper/radio-bearer-stats-connector.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/helper/radio-bearer-stats-connector.cc	2026-02-16 16:08:49.660230200 -0800
@@ -35,7 +35,7 @@
  * and provides calculators with cellId and IMSI,
  * because most trace sources do not provide it.
  */
-struct BoundCallbackArgument : public SimpleRefCount<BoundCallbackArgument>
+struct BoundCallbackArgument
 {
   public:
     Ptr<RadioBearerStatsCalculator> stats; //!< statistics calculator
@@ -52,7 +52,7 @@
  * @param packetSize
  */
 void
-DlTxPduCallback(Ptr<BoundCallbackArgument> arg,
+DlTxPduCallback(std::shared_ptr<BoundCallbackArgument> arg,
                 std::string path,
                 uint16_t rnti,
                 uint8_t lcid,
@@ -72,7 +72,7 @@
  * @param delay
  */
 void
-DlRxPduCallback(Ptr<BoundCallbackArgument> arg,
+DlRxPduCallback(std::shared_ptr<BoundCallbackArgument> arg,
                 std::string path,
                 uint16_t rnti,
                 uint8_t lcid,
@@ -92,7 +92,7 @@
  * @param packetSize
  */
 void
-UlTxPduCallback(Ptr<BoundCallbackArgument> arg,
+UlTxPduCallback(std::shared_ptr<BoundCallbackArgument> arg,
                 std::string path,
                 uint16_t rnti,
                 uint8_t lcid,
@@ -112,7 +112,7 @@
  * @param delay
  */
 void
-UlRxPduCallback(Ptr<BoundCallbackArgument> arg,
+UlRxPduCallback(std::shared_ptr<BoundCallbackArgument> arg,
                 std::string path,
                 uint16_t rnti,
                 uint8_t lcid,
@@ -255,7 +255,7 @@
     NS_LOG_LOGIC("ueManagerPath = " << ueManagerPath);
     if (m_rlcStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_rlcStats;
@@ -286,7 +286,7 @@
     NS_LOG_LOGIC("ueManagerPath = " << ueManagerPath);
     if (m_rlcStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_rlcStats;
@@ -299,7 +299,7 @@
     }
     if (m_pdcpStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_pdcpStats;
@@ -329,7 +329,7 @@
     NS_LOG_LOGIC("basePath = " << basePath);
     if (m_rlcStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_rlcStats;
@@ -338,7 +338,7 @@
     }
     if (m_pdcpStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_pdcpStats;
@@ -368,7 +368,7 @@
     NS_LOG_LOGIC("basePath = " << basePath);
     if (m_rlcStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_rlcStats;
@@ -377,7 +377,7 @@
     }
     if (m_pdcpStats)
     {
-        Ptr<BoundCallbackArgument> arg = Create<BoundCallbackArgument>();
+        auto arg = std::make_shared<BoundCallbackArgument>();
         arg->imsi = imsi;
         arg->cellId = cellId;
         arg->stats = m_pdcpStats;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/a2-a4-rsrq-handover-algorithm.cc ns-3.47/src/lte/model/a2-a4-rsrq-handover-algorithm.cc
--- ns-3.46/src/lte/model/a2-a4-rsrq-handover-algorithm.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/a2-a4-rsrq-handover-algorithm.cc	2026-02-16 16:08:49.660230200 -0800
@@ -234,7 +234,7 @@
     }
 
     NS_ASSERT(it1 != m_neighbourCellMeasures.end());
-    Ptr<UeMeasure> neighbourCellMeasures;
+    std::shared_ptr<UeMeasure> neighbourCellMeasures;
     auto it2 = it1->second.find(cellId);
 
     if (it2 != it1->second.end())
@@ -246,7 +246,7 @@
     else
     {
         // insert a new cell entry
-        neighbourCellMeasures = Create<UeMeasure>();
+        neighbourCellMeasures = std::make_shared<UeMeasure>();
         neighbourCellMeasures->m_cellId = cellId;
         neighbourCellMeasures->m_rsrq = rsrq;
         it1->second[cellId] = neighbourCellMeasures;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/a2-a4-rsrq-handover-algorithm.h ns-3.47/src/lte/model/a2-a4-rsrq-handover-algorithm.h
--- ns-3.46/src/lte/model/a2-a4-rsrq-handover-algorithm.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/a2-a4-rsrq-handover-algorithm.h	2026-02-16 16:08:49.660230200 -0800
@@ -135,7 +135,7 @@
      * Measurements reported by a UE for a cell ID. The values are quantized
      * according 3GPP TS 36.133 section 9.1.4 and 9.1.7.
      */
-    class UeMeasure : public SimpleRefCount<UeMeasure>
+    class UeMeasure
     {
       public:
         uint16_t m_cellId; ///< Cell ID.
@@ -146,7 +146,7 @@
      * Measurements reported by a UE for several cells. The structure is a map
      * indexed by the cell ID.
      */
-    typedef std::map<uint16_t, Ptr<UeMeasure>> MeasurementRow_t;
+    typedef std::map<uint16_t, std::shared_ptr<UeMeasure>> MeasurementRow_t;
 
     /**
      * Measurements reported by several UEs. The structure is a map indexed by
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/epc-mme-application.cc ns-3.47/src/lte/model/epc-mme-application.cc
--- ns-3.46/src/lte/model/epc-mme-application.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/epc-mme-application.cc	2026-02-16 16:08:49.662230000 -0800
@@ -70,7 +70,7 @@
 EpcMmeApplication::AddEnb(uint16_t gci, Ipv4Address enbS1uAddr, EpcS1apSapEnb* enbS1apSap)
 {
     NS_LOG_FUNCTION(this << gci << enbS1uAddr << enbS1apSap);
-    Ptr<EnbInfo> enbInfo = Create<EnbInfo>();
+    auto enbInfo = std::make_shared<EnbInfo>();
     enbInfo->gci = gci;
     enbInfo->s1uAddr = enbS1uAddr;
     enbInfo->s1apSapEnb = enbS1apSap;
@@ -81,7 +81,7 @@
 EpcMmeApplication::AddUe(uint64_t imsi)
 {
     NS_LOG_FUNCTION(this << imsi);
-    Ptr<UeInfo> ueInfo = Create<UeInfo>();
+    auto ueInfo = std::make_shared<UeInfo>();
     ueInfo->imsi = imsi;
     ueInfo->mmeUeS1Id = imsi;
     ueInfo->bearerCounter = 0;
@@ -233,7 +233,7 @@
 }
 
 void
-EpcMmeApplication::RemoveBearer(Ptr<UeInfo> ueInfo, uint8_t epsBearerId)
+EpcMmeApplication::RemoveBearer(std::shared_ptr<UeInfo> ueInfo, uint8_t epsBearerId)
 {
     NS_LOG_FUNCTION(this << epsBearerId);
     auto bit = ueInfo->bearersToBeActivated.begin();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/epc-mme-application.h ns-3.47/src/lte/model/epc-mme-application.h
--- ns-3.46/src/lte/model/epc-mme-application.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/epc-mme-application.h	2026-02-16 16:08:49.662230000 -0800
@@ -187,7 +187,7 @@
     /**
      * Hold info on a UE
      */
-    struct UeInfo : public SimpleRefCount<UeInfo>
+    struct UeInfo
     {
         uint64_t imsi;                              ///< UE identifier
         uint64_t mmeUeS1Id;                         ///< mmeUeS1Id
@@ -200,19 +200,19 @@
     /**
      * UeInfo stored by IMSI
      */
-    std::map<uint64_t, Ptr<UeInfo>> m_ueInfoMap;
+    std::map<uint64_t, std::shared_ptr<UeInfo>> m_ueInfoMap;
 
     /**
      * @brief This Function erases all contexts of bearer from MME side
      * @param ueInfo UE information pointer
      * @param epsBearerId Bearer Id which need to be removed corresponding to UE
      */
-    void RemoveBearer(Ptr<UeInfo> ueInfo, uint8_t epsBearerId);
+    void RemoveBearer(std::shared_ptr<UeInfo> ueInfo, uint8_t epsBearerId);
 
     /**
      * Hold info on an ENB
      */
-    struct EnbInfo : public SimpleRefCount<EnbInfo>
+    struct EnbInfo
     {
         uint16_t gci;              ///< GCI
         Ipv4Address s1uAddr;       ///< IP address of the S1-U interface
@@ -222,7 +222,7 @@
     /**
      * EnbInfo stored by EGCI
      */
-    std::map<uint16_t, Ptr<EnbInfo>> m_enbInfoMap;
+    std::map<uint16_t, std::shared_ptr<EnbInfo>> m_enbInfoMap;
 
     EpcS1apSapMme* m_s1apSapMme; ///< EpcS1apSapMme
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/epc-pgw-application.cc ns-3.47/src/lte/model/epc-pgw-application.cc
--- ns-3.46/src/lte/model/epc-pgw-application.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/epc-pgw-application.cc	2026-02-16 16:08:49.662230000 -0800
@@ -482,8 +482,7 @@
 EpcPgwApplication::AddUe(uint64_t imsi)
 {
     NS_LOG_FUNCTION(this << imsi);
-    Ptr<UeInfo> ueInfo = Create<UeInfo>();
-    m_ueInfoByImsiMap[imsi] = ueInfo;
+    m_ueInfoByImsiMap[imsi] = std::make_shared<UeInfo>();
 }
 
 void
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/epc-pgw-application.h ns-3.47/src/lte/model/epc-pgw-application.h
--- ns-3.46/src/lte/model/epc-pgw-application.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/epc-pgw-application.h	2026-02-16 16:08:49.662230000 -0800
@@ -182,7 +182,7 @@
     /**
      * store info for each UE connected to this PGW
      */
-    class UeInfo : public SimpleRefCount<UeInfo>
+    class UeInfo
     {
       public:
         UeInfo();
@@ -290,17 +290,17 @@
     /**
      * UeInfo stored by UE IPv4 address
      */
-    std::map<Ipv4Address, Ptr<UeInfo>> m_ueInfoByAddrMap;
+    std::map<Ipv4Address, std::shared_ptr<UeInfo>> m_ueInfoByAddrMap;
 
     /**
      * UeInfo stored by UE IPv6 address
      */
-    std::map<Ipv6Address, Ptr<UeInfo>> m_ueInfoByAddrMap6;
+    std::map<Ipv6Address, std::shared_ptr<UeInfo>> m_ueInfoByAddrMap6;
 
     /**
      * UeInfo stored by IMSI
      */
-    std::map<uint64_t, Ptr<UeInfo>> m_ueInfoByImsiMap;
+    std::map<uint64_t, std::shared_ptr<UeInfo>> m_ueInfoByImsiMap;
 
     /**
      * UDP port to be used for GTP-U
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/lte-ffr-distributed-algorithm.cc ns-3.47/src/lte/model/lte-ffr-distributed-algorithm.cc
--- ns-3.46/src/lte/model/lte-ffr-distributed-algorithm.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/lte-ffr-distributed-algorithm.cc	2026-02-16 16:08:49.768230400 -0800
@@ -485,9 +485,6 @@
     m_ulEdgeRbgMap.clear();
     m_ulEdgeRbgMap.resize(m_ulBandwidth, false);
 
-    Ptr<UeMeasure> servingCellMeasures;
-    Ptr<UeMeasure> neighbourCellMeasures;
-
     uint32_t edgeUeNum = 0;
     for (auto areaIt = m_ues.begin(); areaIt != m_ues.end(); areaIt++)
     {
@@ -507,29 +504,20 @@
                 continue;
             }
 
-            servingCellMeasures = nullptr;
-            neighbourCellMeasures = nullptr;
-
             auto it2 = it1->second.find(m_cellId);
-            if (it2 != it1->second.end())
-            {
-                servingCellMeasures = it2->second;
-            }
-            else
+            if (it2 == it1->second.end())
             {
                 continue;
             }
+            std::shared_ptr<UeMeasure> servingCellMeasures = it2->second;
 
             for (it2 = it1->second.begin(); it2 != it1->second.end(); it2++)
             {
-                if (it2->first != m_cellId)
-                {
-                    neighbourCellMeasures = it2->second;
-                }
-                else
+                if (it2->first == m_cellId)
                 {
                     continue;
                 }
+                std::shared_ptr<UeMeasure> neighbourCellMeasures = it2->second;
 
                 if (servingCellMeasures && neighbourCellMeasures)
                 {
@@ -695,7 +683,7 @@
     else
     {
         // insert a new cell entry
-        Ptr<UeMeasure> cellMeasures = Create<UeMeasure>();
+        auto cellMeasures = std::make_shared<UeMeasure>();
         cellMeasures->m_cellId = cellId;
         cellMeasures->m_rsrp = rsrp;
         cellMeasures->m_rsrq = rsrq;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/lte-ffr-distributed-algorithm.h ns-3.47/src/lte/model/lte-ffr-distributed-algorithm.h
--- ns-3.46/src/lte/model/lte-ffr-distributed-algorithm.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/lte-ffr-distributed-algorithm.h	2026-02-16 16:08:49.768230400 -0800
@@ -157,7 +157,7 @@
      *
      * The values are quantized according 3GPP TS 36.133 section 9.1.4 and 9.1.7.
      */
-    class UeMeasure : public SimpleRefCount<UeMeasure>
+    class UeMeasure
     {
       public:
         uint16_t m_cellId; ///< Cell ID
@@ -166,7 +166,7 @@
     };
 
     /// Cell Id is used as the key for the following map
-    typedef std::map<uint16_t, Ptr<UeMeasure>> MeasurementRow_t;
+    typedef std::map<uint16_t, std::shared_ptr<UeMeasure>> MeasurementRow_t;
     /// RNTI is used as the key for the following map
     typedef std::map<uint16_t, MeasurementRow_t> MeasurementTable_t;
     MeasurementTable_t m_ueMeasures; ///< UE measures
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/lte-mi-error-model.cc ns-3.47/src/lte/model/lte-mi-error-model.cc
--- ns-3.46/src/lte/model/lte-mi-error-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/lte-mi-error-model.cc	2026-02-16 16:08:49.769230600 -0800
@@ -7,7 +7,7 @@
  * Original Work Authors:
  *      Marco Mezzavilla <mezzavil@dei.unipd.it>
  *      Giovanni Tomasi <tomasigv@gmail.com>
- * Original Work Acknowldegments:
+ * Original Work Acknowledgments:
  *      This work was supported by the MEDIEVAL (MultiMEDia transport
  *      for mobIlE Video AppLications) project, which is a
  *      medium-scale focused research project (STREP) of the 7th
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/lte-rrc-header.cc ns-3.47/src/lte/model/lte-rrc-header.cc
--- ns-3.46/src/lte/model/lte-rrc-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/lte-rrc-header.cc	2026-02-16 16:08:49.771230500 -0800
@@ -29,7 +29,6 @@
 #define MAX_CELL_REPORT 8
 
 #define MAX_SCELL_REPORT 5
-#define MAX_SCELL_CONF 5
 
 namespace ns3
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/model/lte-spectrum-phy.cc ns-3.47/src/lte/model/lte-spectrum-phy.cc
--- ns-3.46/src/lte/model/lte-spectrum-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/model/lte-spectrum-phy.cc	2026-02-16 16:08:49.772230600 -0800
@@ -153,6 +153,9 @@
     case LteSpectrumPhy::RX_DL_CTRL:
         os << "RX_DL_CTRL";
         break;
+    case LteSpectrumPhy::RX_UL_SRS:
+        os << "RX_UL_SRS";
+        break;
     case LteSpectrumPhy::TX_DATA:
         os << "TX_DATA";
         break;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/lte-test-carrier-aggregation-configuration.cc ns-3.47/src/lte/test/lte-test-carrier-aggregation-configuration.cc
--- ns-3.46/src/lte/test/lte-test-carrier-aggregation-configuration.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/lte-test-carrier-aggregation-configuration.cc	2026-02-16 16:08:49.776230600 -0800
@@ -343,10 +343,17 @@
         lteHelper->SetEnbDeviceAttribute("UlEarfcn", UintegerValue(configuration.m_ulEarfcn));
         lteHelper->SetUeDeviceAttribute("DlEarfcn", UintegerValue(configuration.m_dlEarfcn));
         enbDevs.Add(lteHelper->InstallEnbDevice(enbNodes.Get(i)));
-        lteHelper->AssignStreams(enbDevs, stream);
         ueDevs.Add(lteHelper->InstallUeDevice(ueNodes.Get(i)));
-        lteHelper->AssignStreams(ueDevs, stream);
     }
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    lteHelper->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
+    lteHelper->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
 
     // Calculate the DlBandwidth, UlBandwidth, DlEarfcn and UlEarfcn to which the values from UE RRC
     // would be compared
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/lte-test-cell-selection.cc ns-3.47/src/lte/test/lte-test-cell-selection.cc
--- ns-3.46/src/lte/test/lte-test-cell-selection.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/lte-test-cell-selection.cc	2026-02-16 16:08:49.776230600 -0800
@@ -259,8 +259,15 @@
         }
     }
 
-    stream += lteHelper->AssignStreams(enbDevs, stream);
-    stream += lteHelper->AssignStreams(ueDevs, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    lteHelper->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
+    lteHelper->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
 
     // Tests
     NS_ASSERT(m_ueSetupList.size() == ueDevs.GetN());
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/lte-test-deactivate-bearer.cc ns-3.47/src/lte/test/lte-test-deactivate-bearer.cc
--- ns-3.46/src/lte/test/lte-test-deactivate-bearer.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/lte-test-deactivate-bearer.cc	2026-02-16 16:08:49.776230600 -0800
@@ -210,10 +210,17 @@
 
     lteHelper->SetSchedulerType("ns3::PssFfMacScheduler");
     enbDevs = lteHelper->InstallEnbDevice(enbNodes);
-    stream += lteHelper->AssignStreams(enbDevs, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    lteHelper->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
 
     ueDevs = lteHelper->InstallUeDevice(ueNodes);
-    stream += lteHelper->AssignStreams(ueDevs, stream);
+    lteHelper->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
 
     Ptr<LteEnbNetDevice> lteEnbDev = enbDevs.Get(0)->GetObject<LteEnbNetDevice>();
     Ptr<LteEnbPhy> enbPhy = lteEnbDev->GetPhy();
@@ -234,6 +241,11 @@
 
     // Install the IP stack on the UEs
     internet.Install(ueNodes);
+    internet.AssignStreams(ueNodes, stream);
+    stream += 1000;
+    internet.AssignStreams(remoteHostContainer, stream);
+    stream += 1000;
+
     Ipv4InterfaceContainer ueIpIface;
     ueIpIface = epcHelper->AssignUeIpv4Address(NetDeviceContainer(ueDevs));
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/lte-test-phy-error-model.cc ns-3.47/src/lte/test/lte-test-phy-error-model.cc
--- ns-3.46/src/lte/test/lte-test-phy-error-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/lte-test-phy-error-model.cc	2026-02-16 16:08:49.778230700 -0800
@@ -60,19 +60,25 @@
         //    tol = n*p - binoinv(0.001, n, p)
         // endfor
 
+        // This octave code calculates a 99.9% confidence level test (0.001 failure rate)
+        // for a binomial distribution.  'p' in this case comes from the PCFICH-PDCCH error
+        // curve in lte-mi-error-model.cc:75-91.
+        // SINR 2.0 dB corresponds to BLER of roughly 0.007, SINR 4.0 dB to BLER of 0.045
+        // SINR 6.0 dB to 0.206, and SINR 7.0 dB to 0.343.
+
         // 1 interfering eNB SINR -2.0 BLER 0.007 TB size 217
-        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(2, 1078, 0.007, 9, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(2, 1078, 0.007, 9, rngRun),
                     (rngRun == 1) ? TestCase::Duration::QUICK : TestCase::Duration::TAKES_FOREVER);
         // 2 interfering eNBs SINR -4.0 BLER 0.037 TB size 217
-        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(3, 1040, 0.045, 21, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(3, 1040, 0.045, 21, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
         // 3 interfering eNBs SINR -6.0 BLER 0.21 TB size 133
-        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(4, 1250, 0.206, 40, Seconds(0.12), rngRun),
+        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(4, 1250, 0.206, 40, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
         // 4 interfering eNBs SINR -7.0 BLER 0.34 TB size 133
-        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(5, 1260, 0.343, 47, Seconds(0.12), rngRun),
+        AddTestCase(new LenaDlCtrlPhyErrorModelTestCase(5, 1260, 0.343, 47, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
 
@@ -85,27 +91,27 @@
         // endfor
 
         // MCS 2 TB size of 256 bits BLER 0.33 SINR -5.51
-        AddTestCase(new LenaDataPhyErrorModelTestCase(4, 1800, 0.33, 39, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDataPhyErrorModelTestCase(4, 1800, 0.33, 39, rngRun),
                     (rngRun == 1) ? TestCase::Duration::QUICK : TestCase::Duration::TAKES_FOREVER);
         // MCS 2 TB size of 528 bits BLER 0.11 SINR -5.51
-        AddTestCase(new LenaDataPhyErrorModelTestCase(2, 1800, 0.11, 26, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDataPhyErrorModelTestCase(2, 1800, 0.11, 26, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
         // MCS 2 TB size of 1088 bits BLER 0.02 SINR -5.51
-        AddTestCase(new LenaDataPhyErrorModelTestCase(1, 1800, 0.02, 33, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDataPhyErrorModelTestCase(1, 1800, 0.02, 33, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
         // MCS 12 TB size of 4800 bits  BLER 0.3  SINR 4.43
-        AddTestCase(new LenaDataPhyErrorModelTestCase(1, 600, 0.3, 38, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDataPhyErrorModelTestCase(1, 600, 0.3, 38, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
         // MCS 12 TB size of 1632 bits  BLER 0.55  SINR 4.43
-        AddTestCase(new LenaDataPhyErrorModelTestCase(3, 600, 0.55, 40, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDataPhyErrorModelTestCase(3, 600, 0.55, 40, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
         // MCS 16 TB size of 7272 bits (3648 x 3584) BLER 0.14 SINR 8.48
         // BLER 0.14 = 1 - ((1-0.075)*(1-0.075))
-        AddTestCase(new LenaDataPhyErrorModelTestCase(1, 470, 0.14, 29, Seconds(0.04), rngRun),
+        AddTestCase(new LenaDataPhyErrorModelTestCase(1, 470, 0.14, 29, rngRun),
                     (rngRun == 1) ? TestCase::Duration::EXTENSIVE
                                   : TestCase::Duration::TAKES_FOREVER);
     }
@@ -129,14 +135,12 @@
                                                              uint16_t dist,
                                                              double blerRef,
                                                              uint16_t toleranceRxPackets,
-                                                             Time statsStartTime,
                                                              uint32_t rngRun)
     : TestCase(BuildNameString(nUser, dist, rngRun)),
       m_nUser(nUser),
       m_dist(dist),
       m_blerRef(blerRef),
       m_toleranceRxPackets(toleranceRxPackets),
-      m_statsStartTime(statsStartTime),
       m_rngRun(rngRun)
 {
 }
@@ -150,6 +154,15 @@
 {
     SetDataDir(NS_TEST_SOURCEDIR);
     double ber = 0.03;
+    // Allow extra time in the beginning of simulation to allow RRC connection
+    // establishment + SRS.  The random variable (m_random in LteSpectrumPhy) can
+    // cause the MIB reception to be corrupted on the first reception attempt,
+    // leading the connection to be delayed for an extra MIB period of 80 ms.
+    // Earlier versions of this test used 40 milliseconds for low interference
+    // configurations, and 120 milliseconds for high interference configurations
+    // (4 or 5 eNBs), but 120 milliseconds was found to be too low for at least
+    // one case, so 150 ms is more conservative.
+    Time statsStartTime = MilliSeconds(150);
     Config::SetDefault("ns3::LteAmc::Ber", DoubleValue(ber));
     Config::SetDefault("ns3::LteAmc::AmcModel", EnumValue(LteAmc::PiroEW2010));
     Config::SetDefault("ns3::LteSpectrumPhy::CtrlErrorModelEnabled", BooleanValue(false));
@@ -200,14 +213,28 @@
     lena->SetSchedulerAttribute("UlCqiFilter", EnumValue(FfMacScheduler::PUSCH_UL_CQI));
 
     enbDevs = lena->InstallEnbDevice(enbNodes);
-    stream += lena->AssignStreams(enbDevs, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    lena->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
     ueDevs = lena->InstallUeDevice(ueNodes);
-    stream += lena->AssignStreams(ueDevs, stream);
+    lena->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
+    lena->GetDownlinkSpectrumChannel()->AssignStreams(stream);
+    stream += 1000;
+    lena->GetUplinkSpectrumChannel()->AssignStreams(stream);
+    stream += 1000;
 
     // Attach a UE to a eNB
     lena->Attach(ueDevs, enbDevs.Get(0));
 
-    // Activate an EPS bearer
+    // Activate an EPS bearer.  When ActivateDataRadioBearer is called without
+    // an EPC, it defaults to the saturation mode RLC (RLC_SM_ALWAYS), which
+    // causes one packet to be generated per TTI, which provides the traffic
+    // for this test.
     EpsBearer::Qci q = EpsBearer::GBR_CONV_VOICE;
     EpsBearer bearer(q);
     lena->ActivateDataRadioBearer(ueDevs, bearer);
@@ -232,11 +259,11 @@
     }
 
     Time statsDuration = Seconds(1);
-    Simulator::Stop(m_statsStartTime + statsDuration - Seconds(0.0001));
+    Simulator::Stop(statsStartTime + statsDuration - Seconds(0.0001));
 
     lena->EnableRlcTraces();
     Ptr<RadioBearerStatsCalculator> rlcStats = lena->GetRlcStats();
-    rlcStats->SetAttribute("StartTime", TimeValue(m_statsStartTime));
+    rlcStats->SetAttribute("StartTime", TimeValue(statsStartTime));
     rlcStats->SetAttribute("EpochDuration", TimeValue(statsDuration));
 
     Simulator::Run();
@@ -291,14 +318,12 @@
                                                                  uint16_t dist,
                                                                  double blerRef,
                                                                  uint16_t toleranceRxPackets,
-                                                                 Time statsStartTime,
                                                                  uint32_t rngRun)
     : TestCase(BuildNameString(nEnb, dist, rngRun)),
       m_nEnb(nEnb),
       m_dist(dist),
       m_blerRef(blerRef),
       m_toleranceRxPackets(toleranceRxPackets),
-      m_statsStartTime(statsStartTime),
       m_rngRun(rngRun)
 {
 }
@@ -313,6 +338,15 @@
     SetDataDir(NS_TEST_SOURCEDIR);
 
     double ber = 0.03;
+    // Allow extra time in the beginning of simulation to allow RRC connection
+    // establishment + SRS.  The random variable (m_random in LteSpectrumPhy) can
+    // cause the MIB reception to be corrupted on the first reception attempt,
+    // leading the connection to be delayed for an extra MIB period of 80 ms.
+    // Earlier versions of this test used 40 milliseconds for low interference
+    // configurations, and 120 milliseconds for high interference configurations
+    // (4 or 5 eNBs), but 120 milliseconds was found to be too low for at least
+    // one case, so 150 ms is more conservative.
+    Time statsStartTime = MilliSeconds(150);
     Config::SetDefault("ns3::LteAmc::Ber", DoubleValue(ber));
     Config::SetDefault("ns3::LteAmc::AmcModel", EnumValue(LteAmc::PiroEW2010));
     Config::SetDefault("ns3::LteSpectrumPhy::CtrlErrorModelEnabled", BooleanValue(true));
@@ -363,14 +397,28 @@
     lena->SetSchedulerAttribute("UlCqiFilter", EnumValue(FfMacScheduler::PUSCH_UL_CQI));
 
     enbDevs = lena->InstallEnbDevice(enbNodes);
-    stream += lena->AssignStreams(enbDevs, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    lena->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
     ueDevs = lena->InstallUeDevice(ueNodes);
-    stream += lena->AssignStreams(ueDevs, stream);
+    lena->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
+    lena->GetDownlinkSpectrumChannel()->AssignStreams(stream);
+    stream += 1000;
+    lena->GetUplinkSpectrumChannel()->AssignStreams(stream);
+    stream += 1000;
 
     // Attach a UE to one eNB (the others are interfering ones)
     lena->Attach(ueDevs, enbDevs.Get(0));
 
-    // Activate an EPS bearer
+    // Activate an EPS bearer.  When ActivateDataRadioBearer is called without
+    // an EPC, it defaults to the saturation mode RLC (RLC_SM_ALWAYS), which
+    // causes one packet to be generated per TTI, which provides the traffic
+    // for this test.
     EpsBearer::Qci q = EpsBearer::GBR_CONV_VOICE;
     EpsBearer bearer(q);
     lena->ActivateDataRadioBearer(ueDevs, bearer);
@@ -396,11 +444,11 @@
     uePhy->SetAttribute("NoiseFigure", DoubleValue(9.0));
 
     Time statsDuration = Seconds(1);
-    Simulator::Stop(m_statsStartTime + statsDuration - Seconds(0.0001));
+    Simulator::Stop(statsStartTime + statsDuration - Seconds(0.0001));
 
     lena->EnableRlcTraces();
     Ptr<RadioBearerStatsCalculator> rlcStats = lena->GetRlcStats();
-    rlcStats->SetAttribute("StartTime", TimeValue(m_statsStartTime));
+    rlcStats->SetAttribute("StartTime", TimeValue(statsStartTime));
     rlcStats->SetAttribute("EpochDuration", TimeValue(statsDuration));
 
     Simulator::Run();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/lte-test-phy-error-model.h ns-3.47/src/lte/test/lte-test-phy-error-model.h
--- ns-3.46/src/lte/test/lte-test-phy-error-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/lte-test-phy-error-model.h	2026-02-16 16:08:49.778230700 -0800
@@ -34,14 +34,12 @@
      * @param dist the distance between nodes
      * @param blerRef expected BLER
      * @param toleranceRxPackets receive packet loss tolerance
-     * @param statsStartTime the start time for collecting statistics
      * @param rngRun rng run
      */
     LenaDataPhyErrorModelTestCase(uint16_t nUser,
                                   uint16_t dist,
                                   double blerRef,
                                   uint16_t toleranceRxPackets,
-                                  Time statsStartTime,
                                   uint32_t rngRun);
     ~LenaDataPhyErrorModelTestCase() override;
 
@@ -59,9 +57,7 @@
     double m_dist;                 ///< the distance between nodes
     double m_blerRef;              ///< the expected BLER
     uint16_t m_toleranceRxPackets; ///< receive packet tolerance loss
-    Time m_statsStartTime; ///< Extra time in the beginning of simulation to allow RRC connection
-                           ///< establishment + SRS
-    uint32_t m_rngRun;     ///< the rng run
+    uint32_t m_rngRun;             ///< the rng run
 };
 
 /**
@@ -79,14 +75,12 @@
      * @param dist the distance between nodes
      * @param blerRef expected BLER
      * @param toleranceRxPackets receive packet loss tolerance
-     * @param statsStartTime the start time for collecting statistics
      * @param rngRun rng number
      */
     LenaDlCtrlPhyErrorModelTestCase(uint16_t nEnb,
                                     uint16_t dist,
                                     double blerRef,
                                     uint16_t toleranceRxPackets,
-                                    Time statsStartTime,
                                     uint32_t rngRun);
     ~LenaDlCtrlPhyErrorModelTestCase() override;
 
@@ -104,9 +98,7 @@
     double m_dist;                 ///< the distance between nodes
     double m_blerRef;              ///< the expected BLER
     uint16_t m_toleranceRxPackets; ///< receive packet tolerance loss
-    Time m_statsStartTime; ///< Extra time in the beginning of simulation to allow RRC connection
-                           ///< establishment + SRS
-    uint32_t m_rngRun;     ///< the rng run number
+    uint32_t m_rngRun;             ///< the rng run number
 };
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/lte-test-radio-link-failure.cc ns-3.47/src/lte/test/lte-test-radio-link-failure.cc
--- ns-3.46/src/lte/test/lte-test-radio-link-failure.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/lte-test-radio-link-failure.cc	2026-02-16 16:08:49.778230700 -0800
@@ -296,10 +296,17 @@
     NetDeviceContainer ueDevs;
 
     int64_t randomStream = 1;
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
     enbDevs = lteHelper->InstallEnbDevice(enbNodes);
-    randomStream += lteHelper->AssignStreams(enbDevs, randomStream);
+    lteHelper->AssignStreams(enbDevs, randomStream, false);
+    randomStream += 1000;
     ueDevs = lteHelper->InstallUeDevice(ueNodes);
-    randomStream += lteHelper->AssignStreams(ueDevs, randomStream);
+    lteHelper->AssignStreams(ueDevs, randomStream, true);
+    randomStream += 1000;
 
     // Install the IP stack on the UEs
     internet.Install(ueNodes);
@@ -463,7 +470,6 @@
 
     UeManager::State ueManagerState = ueManager->GetState();
     NS_TEST_ASSERT_MSG_EQ(ueManagerState, UeManager::CONNECTED_NORMALLY, "Wrong UeManager state!");
-    NS_ASSERT_MSG(ueManagerState == UeManager::CONNECTED_NORMALLY, "Wrong UeManager state!");
 
     uint16_t ueCellId = ueRrc->GetCellId();
     uint16_t enbCellId = enbLteDevice->GetCellId();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/test-lte-rrc.cc ns-3.47/src/lte/test/test-lte-rrc.cc
--- ns-3.46/src/lte/test/test-lte-rrc.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/test-lte-rrc.cc	2026-02-16 16:08:49.781230700 -0800
@@ -300,11 +300,18 @@
     int64_t stream = 1;
     NetDeviceContainer enbDevs;
     enbDevs = m_lteHelper->InstallEnbDevice(enbNodes);
-    stream += m_lteHelper->AssignStreams(enbDevs, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    m_lteHelper->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
 
     NetDeviceContainer ueDevs;
     ueDevs = m_lteHelper->InstallUeDevice(ueNodes);
-    stream += m_lteHelper->AssignStreams(ueDevs, stream);
+    m_lteHelper->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
 
     // custom code used for testing purposes
     // instead of lteHelper->Attach () and lteHelper->ActivateXxx
@@ -658,11 +665,18 @@
     int64_t stream = 1;
     NetDeviceContainer enbDevs;
     enbDevs = m_lteHelper->InstallEnbDevice(enbNodes);
-    stream += m_lteHelper->AssignStreams(enbDevs, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    m_lteHelper->AssignStreams(enbDevs, stream, false);
+    stream += 1000;
 
     NetDeviceContainer ueDevs;
     ueDevs = m_lteHelper->InstallUeDevice(ueNodes);
-    stream += m_lteHelper->AssignStreams(ueDevs, stream);
+    m_lteHelper->AssignStreams(ueDevs, stream, true);
+    stream += 1000;
 
     // custom code used for testing purposes
     // instead of lteHelper->Attach () and lteHelper->ActivateXxx
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/test-lte-x2-handover.cc ns-3.47/src/lte/test/test-lte-x2-handover.cc
--- ns-3.46/src/lte/test/test-lte-x2-handover.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/test-lte-x2-handover.cc	2026-02-16 16:08:49.781230700 -0800
@@ -264,7 +264,13 @@
 
     NetDeviceContainer enbDevices;
     enbDevices = m_lteHelper->InstallEnbDevice(enbNodes);
-    stream += m_lteHelper->AssignStreams(enbDevices, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    m_lteHelper->AssignStreams(enbDevices, stream, false);
+    stream += 1000;
     for (auto it = enbDevices.Begin(); it != enbDevices.End(); ++it)
     {
         Ptr<LteEnbRrc> enbRrc = (*it)->GetObject<LteEnbNetDevice>()->GetRrc();
@@ -273,7 +279,8 @@
 
     NetDeviceContainer ueDevices;
     ueDevices = m_lteHelper->InstallUeDevice(ueNodes);
-    stream += m_lteHelper->AssignStreams(ueDevices, stream);
+    m_lteHelper->AssignStreams(ueDevices, stream, true);
+    stream += 1000;
 
     Ipv4Address remoteHostAddr;
     Ipv4StaticRoutingHelper ipv4RoutingHelper;
@@ -522,7 +529,6 @@
 
     UeManager::State ueManagerState = ueManager->GetState();
     NS_TEST_ASSERT_MSG_EQ(ueManagerState, UeManager::CONNECTED_NORMALLY, "Wrong UeManager state!");
-    NS_ASSERT_MSG(ueManagerState == UeManager::CONNECTED_NORMALLY, "Wrong UeManager state!");
 
     uint16_t ueCellId = ueRrc->GetCellId();
     uint16_t enbCellId = enbLteDevice->GetCellId();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/lte/test/test-lte-x2-handover-measures.cc ns-3.47/src/lte/test/test-lte-x2-handover-measures.cc
--- ns-3.46/src/lte/test/test-lte-x2-handover-measures.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/lte/test/test-lte-x2-handover-measures.cc	2026-02-16 16:08:49.781230700 -0800
@@ -334,7 +334,13 @@
 
     NetDeviceContainer enbDevices;
     enbDevices = m_lteHelper->InstallEnbDevice(enbNodes);
-    stream += m_lteHelper->AssignStreams(enbDevices, stream);
+    // Call AssignStreams in the EpcHelper only once by setting 'assignEpcStreams' only once
+    // Then, increment the stream value by 1000 (a magic number that should ensure that there
+    // are no overlapping stream assignments) each time that AssignStreams is called,
+    // to lessen the possibility that random variable stream assignment changes propagate
+    // to other objects.
+    m_lteHelper->AssignStreams(enbDevices, stream, false);
+    stream += 1000;
     for (auto it = enbDevices.Begin(); it != enbDevices.End(); ++it)
     {
         Ptr<LteEnbRrc> enbRrc = (*it)->GetObject<LteEnbNetDevice>()->GetRrc();
@@ -343,7 +349,8 @@
 
     NetDeviceContainer ueDevices;
     ueDevices = m_lteHelper->InstallUeDevice(ueNodes);
-    stream += m_lteHelper->AssignStreams(ueDevices, stream);
+    m_lteHelper->AssignStreams(ueDevices, stream, true);
+    stream += 1000;
 
     Ipv4Address remoteHostAddr;
     Ipv4StaticRoutingHelper ipv4RoutingHelper;
@@ -597,7 +604,6 @@
 
     UeManager::State ueManagerState = ueManager->GetState();
     NS_TEST_ASSERT_MSG_EQ(ueManagerState, UeManager::CONNECTED_NORMALLY, "Wrong UeManager state!");
-    NS_ASSERT_MSG(ueManagerState == UeManager::CONNECTED_NORMALLY, "Wrong UeManager state!");
 
     uint16_t ueCellId = ueRrc->GetCellId();
     uint16_t enbCellId = enbLteDevice->GetCellId();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-proactive-regression.cc ns-3.47/src/mesh/test/dot11s/hwmp-proactive-regression.cc
--- ns-3.46/src/mesh/test/dot11s/hwmp-proactive-regression.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-proactive-regression.cc	2026-02-16 16:08:49.785230600 -0800
@@ -116,6 +116,8 @@
 HwmpProactiveRegressionTest::CreateDevices()
 {
     int64_t streamsUsed = 0;
+    int64_t streamNumber = 0;
+    int64_t streamIncrement = 1000;
     // 1. setup WiFi
     YansWifiPhyHelper wifiPhy;
     // This test suite output was originally based on YansErrorRateModel
@@ -137,16 +139,19 @@
     mesh.SetNumberOfInterfaces(1);
     NetDeviceContainer meshDevices = mesh.Install(wifiPhy, *m_nodes);
     // Five devices, 10 streams per device
-    streamsUsed += mesh.AssignStreams(meshDevices, streamsUsed);
+    streamsUsed = mesh.AssignStreams(meshDevices, streamNumber);
+    streamNumber += streamIncrement;
     NS_TEST_ASSERT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream mismatch");
     // No streams used here, by default, so streamsUsed should not change
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream mismatch");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    streamNumber += streamIncrement;
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed, 0, "No stream assignments expected for WifiChannel");
 
     // 3. setup TCP/IP
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
+    internetStack.AssignStreams(*m_nodes, streamNumber);
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     m_interfaces = address.Assign(meshDevices);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-proactive-regression.h ns-3.47/src/mesh/test/dot11s/hwmp-proactive-regression.h
--- ns-3.46/src/mesh/test/dot11s/hwmp-proactive-regression.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-proactive-regression.h	2026-02-16 16:08:49.785230600 -0800
@@ -6,6 +6,9 @@
  * Authors: Kirill Andreev  <andreev@iitp.ru>
  */
 
+#ifndef HWMP_PROACTIVE_REGRESSION_H
+#define HWMP_PROACTIVE_REGRESSION_H
+
 #include "ns3/ipv4-interface-container.h"
 #include "ns3/node-container.h"
 #include "ns3/nstime.h"
@@ -104,3 +107,5 @@
      */
     void HandleReadClient(Ptr<Socket> socket);
 };
+
+#endif // HWMP_PROACTIVE_REGRESSION_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-reactive-regression.cc ns-3.47/src/mesh/test/dot11s/hwmp-reactive-regression.cc
--- ns-3.46/src/mesh/test/dot11s/hwmp-reactive-regression.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-reactive-regression.cc	2026-02-16 16:08:49.786230600 -0800
@@ -105,6 +105,8 @@
 HwmpReactiveRegressionTest::CreateDevices()
 {
     int64_t streamsUsed = 0;
+    int64_t streamNumber = 0;
+    int64_t streamIncrement = 1000;
     // 1. setup WiFi
     YansWifiPhyHelper wifiPhy;
     // This test suite output was originally based on YansErrorRateModel
@@ -118,19 +120,22 @@
     // 2. setup mesh
     MeshHelper mesh = MeshHelper::Default();
     mesh.SetStackInstaller("ns3::Dot11sStack");
-    mesh.SetMacType("RandomStart", TimeValue(Seconds(0.1)));
+    mesh.SetMacType("RandomStart", TimeValue(MilliSeconds(100)));
     mesh.SetNumberOfInterfaces(1);
     NetDeviceContainer meshDevices = mesh.Install(wifiPhy, *m_nodes);
     // Six devices, 10 streams per device
-    streamsUsed += mesh.AssignStreams(meshDevices, streamsUsed);
-    NS_TEST_EXPECT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream assignment mismatch");
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
+    streamsUsed = mesh.AssignStreams(meshDevices, streamNumber);
+    streamNumber += streamIncrement;
     NS_TEST_EXPECT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream assignment mismatch");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    streamNumber += streamIncrement;
+    NS_TEST_EXPECT_MSG_EQ(streamsUsed, 0, "No stream assignments expected for WifiChannel");
 
     // 3. setup TCP/IP
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
+    internetStack.AssignStreams(*m_nodes, streamNumber);
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     m_interfaces = address.Assign(meshDevices);
@@ -167,7 +172,7 @@
         socket->Send(Create<Packet>(20));
         m_sentPktsCounter++;
         Simulator::ScheduleWithContext(socket->GetNode()->GetId(),
-                                       Seconds(0.5),
+                                       MilliSeconds(500),
                                        &HwmpReactiveRegressionTest::SendData,
                                        this,
                                        socket);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-reactive-regression.h ns-3.47/src/mesh/test/dot11s/hwmp-reactive-regression.h
--- ns-3.46/src/mesh/test/dot11s/hwmp-reactive-regression.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-reactive-regression.h	2026-02-16 16:08:49.786230600 -0800
@@ -5,6 +5,10 @@
  *
  * Authors: Kirill Andreev <andreev@iitp.ru>
  */
+
+#ifndef HWMP_REACTIVE_REGRESSION_H
+#define HWMP_REACTIVE_REGRESSION_H
+
 #include "ns3/ipv4-interface-container.h"
 #include "ns3/node-container.h"
 #include "ns3/nstime.h"
@@ -115,3 +119,5 @@
      */
     void HandleReadClient(Ptr<Socket> socket);
 };
+
+#endif // HWMP_REACTIVE_REGRESSION_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-simplest-regression.cc ns-3.47/src/mesh/test/dot11s/hwmp-simplest-regression.cc
--- ns-3.46/src/mesh/test/dot11s/hwmp-simplest-regression.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-simplest-regression.cc	2026-02-16 16:08:49.786230600 -0800
@@ -125,6 +125,8 @@
 HwmpSimplestRegressionTest::CreateDevices()
 {
     int64_t streamsUsed = 0;
+    int64_t streamNumber = 0;
+    int64_t streamIncrement = 1000;
     // 1. setup WiFi
     YansWifiPhyHelper wifiPhy;
     YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default();
@@ -134,19 +136,22 @@
     // 2. setup mesh
     MeshHelper mesh = MeshHelper::Default();
     mesh.SetStackInstaller("ns3::Dot11sStack");
-    mesh.SetMacType("RandomStart", TimeValue(Seconds(0.1)));
+    mesh.SetMacType("RandomStart", TimeValue(MilliSeconds(100)));
     mesh.SetNumberOfInterfaces(1);
     NetDeviceContainer meshDevices = mesh.Install(wifiPhy, *m_nodes);
     // Two devices, ten streams per mesh device
-    streamsUsed += mesh.AssignStreams(meshDevices, streamsUsed);
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream assignment mismatch");
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
+    streamsUsed = mesh.AssignStreams(meshDevices, streamNumber);
+    streamNumber += streamIncrement;
     NS_TEST_ASSERT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream assignment mismatch");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    streamNumber += streamIncrement;
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed, 0, "No stream assignments expected for WifiChannel");
 
     // 3. setup TCP/IP
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
+    internetStack.AssignStreams(*m_nodes, streamNumber);
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     m_interfaces = address.Assign(meshDevices);
@@ -171,7 +176,7 @@
         socket->Send(Create<Packet>(100));
         m_sentPktsCounter++;
         Simulator::ScheduleWithContext(socket->GetNode()->GetId(),
-                                       Seconds(0.05),
+                                       MilliSeconds(50),
                                        &HwmpSimplestRegressionTest::SendData,
                                        this,
                                        socket);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-simplest-regression.h ns-3.47/src/mesh/test/dot11s/hwmp-simplest-regression.h
--- ns-3.46/src/mesh/test/dot11s/hwmp-simplest-regression.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-simplest-regression.h	2026-02-16 16:08:49.786230600 -0800
@@ -6,6 +6,9 @@
  * Authors: Kirill Andreev  <andreev@iitp.ru>
  */
 
+#ifndef HWMP_SIMPLEST_REGRESSION_H
+#define HWMP_SIMPLEST_REGRESSION_H
+
 #include "ns3/ipv4-interface-container.h"
 #include "ns3/node-container.h"
 #include "ns3/nstime.h"
@@ -115,3 +118,5 @@
      */
     void HandleReadClient(Ptr<Socket> socket);
 };
+
+#endif // HWMP_SIMPLEST_REGRESSION_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-target-flags-regression.cc ns-3.47/src/mesh/test/dot11s/hwmp-target-flags-regression.cc
--- ns-3.46/src/mesh/test/dot11s/hwmp-target-flags-regression.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-target-flags-regression.cc	2026-02-16 16:08:49.786230600 -0800
@@ -143,6 +143,8 @@
 HwmpDoRfRegressionTest::CreateDevices()
 {
     int64_t streamsUsed = 0;
+    int64_t streamNumber = 0;
+    int64_t streamIncrement = 1000;
     // 1. setup WiFi
     YansWifiPhyHelper wifiPhy;
     // This test suite output was originally based on YansErrorRateModel
@@ -160,15 +162,18 @@
     mesh.SetNumberOfInterfaces(1);
     NetDeviceContainer meshDevices = mesh.Install(wifiPhy, *m_nodes);
     // Four devices, ten streams per mac
-    streamsUsed += mesh.AssignStreams(meshDevices, streamsUsed);
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream assignment mismatch");
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
+    streamsUsed = mesh.AssignStreams(meshDevices, streamNumber);
+    streamNumber += streamIncrement;
     NS_TEST_ASSERT_MSG_EQ(streamsUsed, (meshDevices.GetN() * 10), "Stream assignment mismatch");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    streamNumber += streamIncrement;
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed, 0, "No stream assignments expected for WifiChannel");
 
     // 3. setup TCP/IP
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
+    internetStack.AssignStreams(*m_nodes, streamNumber);
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     m_interfaces = address.Assign(meshDevices);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/dot11s/hwmp-target-flags-regression.h ns-3.47/src/mesh/test/dot11s/hwmp-target-flags-regression.h
--- ns-3.46/src/mesh/test/dot11s/hwmp-target-flags-regression.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/dot11s/hwmp-target-flags-regression.h	2026-02-16 16:08:49.786230600 -0800
@@ -6,6 +6,9 @@
  * Authors: Kirill Andreev  <andreev@iitp.ru>
  */
 
+#ifndef HWMP_TARGET_FLAGS_REGRESSION_H
+#define HWMP_TARGET_FLAGS_REGRESSION_H
+
 #include "ns3/ipv4-interface-container.h"
 #include "ns3/node-container.h"
 #include "ns3/nstime.h"
@@ -139,3 +142,5 @@
      */
     void HandleReadClient(Ptr<Socket> socket);
 };
+
+#endif // HWMP_TARGET_FLAGS_REGRESSION_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/flame/flame-regression.cc ns-3.47/src/mesh/test/flame/flame-regression.cc
--- ns-3.46/src/mesh/test/flame/flame-regression.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/flame/flame-regression.cc	2026-02-16 16:08:49.786230600 -0800
@@ -87,6 +87,8 @@
 FlameRegressionTest::CreateDevices()
 {
     int64_t streamsUsed = 0;
+    int64_t streamNumber = 1;
+    int64_t streamIncrement = 1000;
     // 1. setup WiFi
     YansWifiPhyHelper wifiPhy;
     // This test suite output was originally based on YansErrorRateModel
@@ -100,23 +102,24 @@
     // 2. setup mesh
     MeshHelper mesh = MeshHelper::Default();
     mesh.SetStackInstaller("ns3::FlameStack");
-    mesh.SetMacType("RandomStart", TimeValue(Seconds(0.1)));
+    mesh.SetMacType("RandomStart", TimeValue(MilliSeconds(100)));
     mesh.SetNumberOfInterfaces(1);
     NetDeviceContainer meshDevices = mesh.Install(wifiPhy, *m_nodes);
     // Three devices, eight streams per device
-    streamsUsed += mesh.AssignStreams(meshDevices, streamsUsed);
+    streamsUsed = mesh.AssignStreams(meshDevices, streamNumber);
+    streamNumber += streamIncrement;
     NS_TEST_ASSERT_MSG_EQ(streamsUsed,
                           (meshDevices.GetN() * 8),
                           "Stream assignment unexpected value");
     // No further streams used in the default wifi channel configuration
-    streamsUsed += wifiChannel.AssignStreams(chan, streamsUsed);
-    NS_TEST_ASSERT_MSG_EQ(streamsUsed,
-                          (meshDevices.GetN() * 8),
-                          "Stream assignment unexpected value");
+    streamsUsed = wifiChannel.AssignStreams(chan, streamNumber);
+    streamNumber += streamIncrement;
+    NS_TEST_ASSERT_MSG_EQ(streamsUsed, 0, "No WifiChannel stream usage expected");
     // 3. setup TCP/IP
     InternetStackHelper internetStack;
+    internetStack.SetIpv6StackInstall(false);
     internetStack.Install(*m_nodes);
-    streamsUsed += internetStack.AssignStreams(*m_nodes, streamsUsed);
+    internetStack.AssignStreams(*m_nodes, streamNumber);
     Ipv4AddressHelper address;
     address.SetBase("10.1.1.0", "255.255.255.0");
     m_interfaces = address.Assign(meshDevices);
@@ -163,7 +166,7 @@
         socket->Send(Create<Packet>(20));
         m_sentPktsCounter++;
         Simulator::ScheduleWithContext(socket->GetNode()->GetId(),
-                                       Seconds(1.1),
+                                       MilliSeconds(1100),
                                        &FlameRegressionTest::SendData,
                                        this,
                                        socket);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mesh/test/flame/flame-regression.h ns-3.47/src/mesh/test/flame/flame-regression.h
--- ns-3.46/src/mesh/test/flame/flame-regression.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mesh/test/flame/flame-regression.h	2026-02-16 16:08:49.786230600 -0800
@@ -6,6 +6,9 @@
  * Authors: Kirill Andreev <andreev@iitp.ru>
  */
 
+#ifndef FLAME_REGRESSION_H
+#define FLAME_REGRESSION_H
+
 #include "ns3/ipv4-interface-container.h"
 #include "ns3/node-container.h"
 #include "ns3/nstime.h"
@@ -106,3 +109,5 @@
      */
     void HandleReadClient(Ptr<Socket> socket);
 };
+
+#endif // FLAME_REGRESSION_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mobility/doc/mobility.rst ns-3.47/src/mobility/doc/mobility.rst
--- ns-3.46/src/mobility/doc/mobility.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mobility/doc/mobility.rst	2026-02-16 16:08:49.787230700 -0800
@@ -1,29 +1,24 @@
 .. include:: replace.txt
-
 .. _Mobility:
 
 Mobility
---------
+========
 
 .. heading hierarchy:
-   ------------- Chapter
-   ************* Section (#.#)
-   ============= Subsection (#.#.#)
-   ############# Paragraph (no number)
-
-The mobility support in |ns3| includes:
+   ============= Chapter
+   ------------- Section (#.#)
+   ~~~~~~~~~~~~~ Subsection (#.#.#)
+   ^^^^^^^^^^^^^ Paragraph (no number)
 
-- a set of mobility models which are used to track and maintain the *current* cartesian position and speed of an object.
-- a "course change notifier" trace source which can be used to register listeners to the course changes of a mobility model
-- a number of helper classes which are used to place nodes and setup mobility models (including parsers for some mobility definition formats).
+The mobility framework in |ns3| provides support for modeling movement and position in network simulations. This system enables simulation of mobile networks, wireless protocols, and location-dependent phenomena.
 
-Model Description
-*****************
+The |ns3| mobility system consists of three main components:
 
-The source code for mobility lives in the directory ``src/mobility``.
+- **Mobility Models**: Track and report on the current position, velocity, and movement patterns of network nodes
+- **Position Allocators**: Determine initial node placement
+- **Helper Classes**: Simplify configuration and provide parsers for external mobility trace formats
 
-Design
-======
+Most mobility-related source code is located in the ``src/mobility`` directory. For building-related mobility models, see ``src/buildings``.
 
 The design includes mobility models, position allocators, and helper
 functions.
@@ -41,99 +36,305 @@
 mobility models.
 
 Most users interact with the mobility system using mobility helper
-classes.  The MobilityHelper combines a mobility model and position
+classes.  The ``MobilityHelper`` combines a mobility model and position
 allocator, and can be used with a node container to install a similar
 mobility capability on a set of nodes.
 
 We first describe the coordinate system and issues
 surrounding multiple coordinate systems.
 
-Coordinate system
-#################
+Scope and Limitations
+---------------------
+
+**Current Support**:
+
+- Cartesian, geocentric cartesian, and geographic coordinates are presently supported
+- Twelve (12) open-field mobility models and one (1) buildings aware mobility model
+- Integration with external mobility trace generators
+
+**Current Limitations**:
+
+- Z-axis movement support varies by model
+- Limited built-in support for obstacle-aware movement
+- No native support for indoor mobility patterns
+
+
 
-There are many possible coordinate systems and possible translations between
-them. Currently, |ns3| provides support for the Cartesian, Geocentric Cartesian,
-and Geographic coordinate systems, which are defined as:
-
-* **Cartesian coordinates**: They represent a point in 3D space using the set of coordinates (x,y,z).
-  This is the default coordinate system in |ns3|, and it is the most suitable for simulation scenarios whose size is in the order of a few km.
-
-  * **Topocentric coordinates**: Cartesian coordinates where a specific reference
-    point on the Earth's surface, such as lat-lon, for the origin is given.
-    This coordinate system is useful for converting to and from Geocentric/Geographic coordinates.
-
-  * **Geocentric Cartesian coordinates**: Cartesian coordinate system where the
-    origin is fixed in the Earth's center of mass. The implementation follows the description in Sec. 6.3 of [38811]_.
-
-* **Geographic coordinates**: They represent a point in 3D space by assuming it
-  is located on the surface or in the orbit of the Earth. Positions are uniquely
-  described by three values: latitude, longitude and altitude. The latter reference is the Earth's surface.
+Coordinate Systems
+------------------
+
+|ns3| supports multiple coordinate systems to accommodate different simulation scenarios and real-world applications.
+
+**Cartesian Coordinates (Default)**:
+
+- **Format**: (x, y, z) coordinates in 3D space
+- **Use case**: Most suitable for simulation scenarios spanning a few kilometers
+- **Implementation**: ``ns3::Vector`` class represents both positions and velocities
+
+**Topocentric Coordinates**:
+
+- **Format**: Cartesian coordinates with a specific Earth surface reference point
+- **Use case**: Converting between local and global coordinate systems
+- **Implementation**: Extends Cartesian with geographic origin reference (``GeographicPositions::TopocentricToGeographicCoordinates`` in ``src/mobility/model/geographic-position.cc``)
+
+**Geocentric Cartesian Coordinates**:
+
+- **Format**: Earth-centered Cartesian system with origin at Earth's center of mass
+- **Use case**: Satellite communications and global-scale simulations
+- **Implementation**: ``GeocentricConstantPositionMobilityModel``
+- **Reference**: Follows 3GPP TR 38.811, Section 6.3 [:ref:`2<mobilityRef2>`]
+
+**Geographic Coordinates**:
+
+- **Format**: Latitude, longitude, and altitude relative to Earth's surface
+- **Use case**: GPS-based applications and real-world location mapping
+- **Implementation**: ``GeographicPosition`` class with conversion utilities
 
 At the moment, the geocentric Cartesian coordinates are adopted by the
 GeocentricConstantPositionMobilityModel only.
 This class implements the Get/SetPosition methods, which leverage the
-GeographicPosition class to offer conversions to and from Cartesian coordinates.
+``GeographicPosition`` class to offer conversions to and from Cartesian coordinates.
 Additionally, users can set the position of a node by its geographical coordinates
 via the methods Get/SetGeographicPosition.
 
 
-Coordinates
-###########
 
-The base class for a coordinate is called ``ns3::Vector``.  While
-positions are normally described as coordinates and not vectors in
-the literature, it is possible to reuse the same data structure to
-represent position (x,y,z) and velocity (magnitude and direction
-from the current position).  |ns3| uses class Vector for both.
+Available Mobility Models
+-------------------------
 
-There are also some additional related structures used to support
-mobility models.
+All mobility models inherit from the base class ``ns3::MobilityModel``.
+Child classes implement specific movement behaviors.
+Below is an overview of the key mobility models available in |ns3|.
+
+Core Classes
+~~~~~~~~~~~~
+
+**MobilityModel Base Class**
+
+The ``ns3::MobilityModel`` class is the foundation of the mobility system:
+
+.. sourcecode:: cpp
+
+   // Key methods
+   Vector GetPosition() const;           // Current position
+   Vector GetVelocity() const;          // Current velocity
+   double GetDistanceFrom(Ptr<const MobilityModel> other) const;
+
+   // Attributes
+   Vector Position;                     // Current position
+   Vector Velocity;                     // Current velocity
+
+   // Trace sources
+   TracedCallback<Ptr<const MobilityModel>> CourseChange;
+
+**Key Features**:
+
+- **Position Tracking**
+- **Velocity Tracking**
+- **Distance Calculation**: Calculates distance to another mobility model object
+- **Course Change Notifications**: Trace source for movement events
+- **Node Aggregation**: Typically aggregated to ``ns3::Node`` objects
+
+Child Classes
+~~~~~~~~~~~~~
+
+**Static Models:**
+
+- ``ConstantPositionMobilityModel``: Nodes remain at fixed positions throughout the simulation. The position is set once during initialization and only changes if explicitly set by the user during runtime. This model is ideal for stationary infrastructure nodes, base stations, or scenarios where mobility is not required. It has near zero computational overhead during simulation runtime.
+
+- ``GeocentricConstantPositionMobilityModel``: Similar to ConstantPositionMobilityModel but with geographic coordinate support. Positions can be specified using latitude, longitude, and altitude coordinates, which are internally converted to the simulation's Cartesian coordinate system. Useful for real-world GPS-based scenarios and satellite communication simulations.
+
+**Linear Motion Models:**
+
+- ``ConstantVelocityMobilityModel``: Nodes move in a straight line with constant speed. The velocity vector (speed and direction) is set once and remains unchanged unless explicitly modified by external events. This model is commonly used with |ns2| mobility traces where setdest commands update the velocity.
+
+- ``ConstantAccelerationMobilityModel``: Implements uniformly accelerated motion following kinematic equations. The node starts with an initial velocity and acceleration vector, with position updated according to:
+
+.. math::
+
+  \vec{p}(t) = \vec{p_0} + \vec{v_0} \cdot t + \frac{1}{2} \vec{a} \cdot t^2
+
+This model is useful for scenarios involving vehicle acceleration/deceleration or projectile motion.
+
+**Random Motion Models:**
+
+- ``RandomWalk2dMobilityModel``: Implements a two-dimensional random walk where nodes change direction and speed at regular time intervals. At each step, a new direction is randomly selected from a uniform distribution [0, 2pi], and speed is drawn from a configurable random variable. Movement is constrained within specified rectangular bounds, with reflection or wrapping behavior at boundaries. The model includes configurable parameters for step time, speed distribution, and boundary behavior.
+
+- ``RandomWalk2dOutdoorMobilityModel``: Enhanced version of RandomWalk2dMobilityModel that incorporates building awareness. Nodes avoid moving through building obstacles by checking for intersections with building polygons before committing to movement. When a building collision is detected, the node selects an alternative direction. This model requires integration with building models and is particularly useful for urban mobility scenarios.
+
+- ``RandomDirection2dMobilityModel``: Nodes move in straight lines for random durations, then pause and select new random directions. Unlike RandomWalk2d, movement occurs in longer straight-line segments rather than frequent small steps. The model alternates between movement and pause periods, with both durations drawn from configurable random variables. Direction changes occur uniformly over [0, 2pi], and speed is constant during each movement phase.
+
+- ``RandomWaypointMobilityModel``: Classic random waypoint model where nodes move from their current position to randomly selected destination waypoints. Upon reaching a waypoint, the node pauses for a random duration, then selects a new random destination. Movement between waypoints follows straight-line paths at constant speeds drawn from a configurable distribution. This model exhibits well-known characteristics including non-uniform spatial distribution and speed decay over time.
+
+- ``SteadyStateRandomWaypointMobilityModel``: Addresses the initial transient behavior of the standard RandomWaypointMobilityModel by starting nodes with positions and velocities drawn from the model's steady-state distribution. This eliminates the artificial clustering and speed artifacts present during the initial simulation phase of the standard random waypoint model, providing more realistic results from simulation start.
+
+**Advanced Models:**
+
+- ``GaussMarkovMobilityModel``: Implements a Gaussian-Markov stochastic process where velocity components are correlated over time. The model balances between random movement and momentum conservation using a tunable randomness parameter alpha in the range [0,1]. When alpha=0, movement is completely random; when alpha=1, movement maintains constant velocity. The velocity update equation is:
+
+.. math::
+
+  v_n = alpha \cdot v_{n-1} + (1-alpha) \cdot \bar{v} + \sqrt{1-alpha^2} \cdot v_{random}
+
+This model produces more realistic mobility patterns with temporal correlation, suitable for human pedestrian movement.
+
+- ``WaypointMobilityModel``: Follows user-defined sequences of waypoints with precise timing control. Each waypoint specifies a position and arrival time, allowing deterministic or scripted mobility patterns. The model supports complex trajectories, synchronized movement scenarios, and replay of real-world mobility traces. Waypoints can be added dynamically during simulation, enabling adaptive mobility patterns based on simulation events.
+
+- ``HierarchicalMobilityModel``: Supports group mobility scenarios using parent-child relationships. Child nodes move relative to a parent mobility model, with the final position being the vector sum of parent and child positions. The parent model defines the group's overall movement pattern, while child models define individual member movements relative to the group. This architecture enables scenarios like vehicular convoys, pedestrian groups, or mobile sensor clusters where individual nodes maintain local mobility within a moving group context.
+
+**Position Allocators**:
+
+- ``ListPositionAllocator``: Uses a predefined list of positions
+- ``GridPositionAllocator``: Arranges nodes in regular grid patterns
+- ``RandomRectanglePositionAllocator``: Uniform distribution within rectangular areas
+- ``RandomBoxPositionAllocator``: Uniform distribution within 3D box regions
+- ``RandomDiscPositionAllocator``: Uniform distribution within circular areas
+- ``UniformDiscPositionAllocator``: Even distribution on disc circumference
+
+Other class types are also present in the :ref:`building module<building-positioning-methods>`. They describe how positions are assigned to buildings.
+
+A position allocator is not always required, as some mobility models generate initial positions during initialization. Among the built-in ns-3 models, **SteadyStateRandomWaypointMobilityModel** is the only one with this capability.
+
+Usage
+-----
+
+Most |ns3| program authors typically interact with the mobility system only at configuration time. However, various |ns3| objects interact with mobility objects repeatedly during runtime, such as a propagation model trying to determine the path loss between two mobile nodes.
+
+Basic Configuration
+~~~~~~~~~~~~~~~~~~~
+
+Random Walk Setup
+^^^^^^^^^^^^^^^^^
+
+.. sourcecode:: cpp
+
+  // Create nodes
+  NodeContainer nodes;
+  nodes.Create(10);
+
+  // Configure mobility
+  MobilityHelper mobility;
+
+  // Grid initial placement
+  mobility.SetPositionAllocator("ns3::GridPositionAllocator",
+                                "MinX", DoubleValue(0.0),
+                                "MinY", DoubleValue(0.0),
+                                "DeltaX", DoubleValue(10.0),
+                                "DeltaY", DoubleValue(10.0),
+                                "GridWidth", UintegerValue(5),
+                                "LayoutType", StringValue("RowFirst"));
+
+  // Random walk mobility
+  mobility.SetMobilityModel("ns3::RandomWalk2dMobilityModel",
+                           "Bounds", RectangleValue(Rectangle(-100, 100, -100, 100)),
+                           "Speed", StringValue("ns3::ConstantRandomVariable[Constant=5.0]"),
+                           "Direction", StringValue("ns3::UniformRandomVariable[Min=0|Max=6.28]"));
+
+  mobility.Install(nodes);
+
+Group Mobility Configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. sourcecode:: cpp
+
+  // Reference point group mobility
+  GroupMobilityHelper groupMobility;
+
+  // Parent mobility (reference point)
+  groupMobility.SetReferencePointMobilityModel("ns3::WaypointMobilityModel");
+
+  // Child mobility (group members)
+  groupMobility.SetMemberMobilityModel("ns3::RandomWalk2dMobilityModel",
+                                       "Bounds", RectangleValue(Rectangle(-10, 10, -10, 10)));
+
+  // Set reference point waypoints
+  Ptr<WaypointMobilityModel> reference = groupMobility.GetReferencePointMobilityModel();
+  reference->AddWaypoint(Waypoint(Seconds(0), Vector(0, 0, 0)));
+  reference->AddWaypoint(Waypoint(Seconds(100), Vector(100, 0, 0)));
+  reference->AddWaypoint(Waypoint(Seconds(200), Vector(100, 100, 0)));
+
+  groupMobility.Install(groupNodes);
+
+Geographic Positioning
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. sourcecode:: cpp
+
+  // Use geocentric coordinates
+  MobilityHelper mobility;
+  mobility.SetMobilityModel("ns3::GeocentricConstantPositionMobilityModel");
+
+  // Set geographic position
+  Ptr<GeocentricConstantPositionMobilityModel> model =
+      node->GetObject<GeocentricConstantPositionMobilityModel>();
+  model->SetGeographicPosition(Vector(latitude, longitude, altitude));
+
+Advanced Usage
+~~~~~~~~~~~~~~
 
-- Rectangle
-- Box
-- Waypoint
-
-MobilityModel
-#############
-
-Describe base class
-
-- GetPosition ()
-- Position and Velocity attributes
-- GetDistanceFrom ()
-- CourseChangeNotification
-
-MobilityModel Subclasses
-########################
-
-- ConstantPosition
-- ConstantVelocity
-- ConstantAcceleration
-- GaussMarkov
-- Hierarchical
-- RandomDirection2D
-- RandomWalk2D
-- RandomWaypoint
-- SteadyStateRandomWaypoint
-- Waypoint
-- GeocentricConstantPosition
-
-PositionAllocator
-#################
-
-Position allocators usually used only at beginning, to lay out the nodes
-initial position.  However, some mobility models (e.g. RandomWaypoint) will
-use a position allocator to pick new waypoints.
-
-- ListPositionAllocator
-- GridPositionAllocator
-- RandomRectanglePositionAllocator
-- RandomBoxPositionAllocator
-- RandomDiscPositionAllocator
-- UniformDiscPositionAllocator
+Random Number Stream Assignment
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Helper
-######
+To avoid the possibility that configuration or parameter changes in a program affect its mobility behavior, it is necessary to assign fixed stream numbers to any random variables associated with mobility models. It is strongly advised to read the section on how to perform independent replications (see "Random Variables", the chapter 3.1 of ns-3 Manual).
+
+.. sourcecode:: cpp
+
+  // Assign specific random streams
+  int64_t streamIndex = 100;
+  MobilityHelper mobility;
+  // ... configure mobility ...
+  mobility.Install(nodes);
+
+  // Assign streams after installation
+  int64_t streamsUsed = mobility.AssignStreams(nodes, streamIndex);
+
+Class ``MobilityModel`` and class ``PositionAllocator`` both have public API to assign streams to underlying random variables:
+
+.. sourcecode:: cpp
+
+  /**
+   * Assign a fixed random variable stream number to the random variables
+   * used by this model. Return the number of streams (possibly zero) that
+   * have been assigned.
+   *
+   * \param stream first stream index to use
+   * \return the number of stream indices assigned by this model
+   */
+  int64_t AssignStreams(int64_t stream);
+
+External Tool Integration
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+**BonnMotion**
+
+BonnMotion is a Java software which creates and analyses mobility scenarios [:ref:`1<mobilityRef3>`] . It is developed within the Communication Systems group at the Institute of Computer Science 4 of the University of Bonn, Germany, where it serves as a tool for the investigation of mobile ad hoc network characteristics. BonnMotion is being jointly developed by the Communication Systems group at the University of Bonn, Germany, the Toilers group at the Colorado School of Mines, Golden, CO, USA, and the Distributed Systems group at the University of Osnabrück, Germany.
+
+- **Installation**: `Installation instructions <https://www.nsnam.org/wiki/HOWTO_use_ns-3_with_BonnMotion_mobility_generator_and_analysis_tool>`_ for using BonnMotion with |ns3|
+- **Documentation**: `Documentation <https://sys.cs.uos.de/bonnmotion/doc/README.pdf>`_ available
+- **Output Format**: |ns2| compatible traces
+
+**SUMO (Simulation of Urban Mobility)**
+
+Literature on the subject of urban wireless network often use SUMO to model the mobility in a given scenario, either based on a fictionnal setup or on real urban environment extracted through tools like OpenStreetMap.
+
+- **Purpose**: Realistic vehicular mobility simulation
+- **Website**: `SUMO <https://sourceforge.net/apps/mediawiki/sumo/index.php?title=Main_Page>`_
+- **Integration**: Exports |ns2| format traces
+- **Use Case**: Urban traffic scenarios
+
+**TraNS**
+
+- **Purpose**: Traffic and network simulation environment
+- **Website**: `TraNS <http://trans.epfl.ch/>`_
+- **Features**: Combines SUMO with network simulation
+- **Output**: Compatible mobility traces
+
+**|ns2| setdest Utility**
+
+The |ns2| `setdest <http://www.winlab.rutgers.edu/~zhibinwu/html/ns2_wireless_scene.htm>`_ utility can generate basic mobility patterns.
+
+Helpers
+~~~~~~~
 
 A special mobility helper is provided that is mainly aimed at supporting
 the installation of mobility to a Node container (when using containers
@@ -149,146 +350,160 @@
 the parent position).  In the GroupMobilityHelper, the parent mobility
 model is not associated with any node, and is used as the parent mobility
 model for all (distinct) child mobility models.  The reference point group
-mobility model [Camp2002]_ is the basis for this |ns3| model.
+mobility model [:ref:`1<mobilityRef1>`] is the basis for this |ns3| model.
 
-ns-2 MobilityHelper
-###################
 
-The |ns2| mobility format is a widely used mobility trace format.  The
-documentation is available at: http://www.isi.edu/nsnam/ns/doc/node172.html
+MobilityHelper
+^^^^^^^^^^^^^^
 
-Valid trace files use the following |ns2| statements:
+The ``MobilityHelper`` class simplifies mobility configuration:
 
-.. sourcecode:: bash
+.. sourcecode:: cpp
 
-   $node set X_ x1
-   $node set Y_ y1
-   $node set Z_ z1
-   $ns at $time $node setdest x2 y2 speed
-   $ns at $time $node set X_ x1
-   $ns at $time $node set Y_ Y1
-   $ns at $time $node set Z_ Z1
+   // Basic configuration
+   MobilityHelper mobility;
+   mobility.SetPositionAllocator("ns3::GridPositionAllocator",
+                                 "MinX", DoubleValue(0.0),
+                                 "MinY", DoubleValue(0.0),
+                                 "DeltaX", DoubleValue(5.0),
+                                 "DeltaY", DoubleValue(10.0));
+
+   mobility.SetMobilityModel("ns3::RandomWalk2dMobilityModel",
+                            "Bounds", RectangleValue(Rectangle(-50, 50, -50, 50)));
 
-In the above, the initial positions are set using the ``set`` statements.
-Also, this ``set`` can be specified for a future time, such as in the
-last three statements above.
+   // Installation
+   mobility.Install(nodeContainer);
 
-The command ``setdest`` instructs the simulation to start moving the
-specified node towards the coordinate (x2, y2) at the specified time.
-Note that the node may never get to the destination, but will
-proceed towards the destination at the specified speed until it
-either reaches the destination (where it will pause), is set to
-a new position (via ``set``), or sent on another course change
-(via ``setdest``).
+GroupMobilityHelper
+^^^^^^^^^^^^^^^^^^^
 
-Note that in |ns3|, movement along the Z dimension is not supported.
+Supports group mobility scenarios using the reference point group mobility model [:ref:`1<mobilityRef1>`]:
 
-Some examples of external tools that can export in this format include:
+.. sourcecode:: cpp
 
-- `BonnMotion <http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/>`_
+   GroupMobilityHelper groupMobility;
+   groupMobility.SetReferencePointMobilityModel("ns3::RandomWaypointMobilityModel");
+   groupMobility.SetMemberMobilityModel("ns3::RandomWalk2dMobilityModel");
+   groupMobility.Install(groupNodes);
 
-  - `Installation instructions <https://www.nsnam.org/wiki/HOWTO_use_ns-3_with_BonnMotion_mobility_generator_and_analysis_tool>`_ and
-  - `Documentation <https://sys.cs.uos.de/bonnmotion/doc/README.pdf>`_ for using BonnMotion with |ns3|
+Ns2MobilityHelper
+^^^^^^^^^^^^^^^^^
 
-- `SUMO <https://sourceforge.net/apps/mediawiki/sumo/index.php?title=Main_Page>`_
-- `TraNS <http://trans.epfl.ch/>`_
-- |ns2| `setdest <http://www.winlab.rutgers.edu/~zhibinwu/html/ns2_wireless_scene.htm>`_ utility
+Parses |ns2| format mobility traces for compatibility with existing tools:
 
-A special Ns2MobilityHelper object can be used to parse these files
-and convert the statements into |ns3| mobility events.  The underlying
-ConstantVelocityMobilityModel is used to model these movements.
+.. sourcecode:: cpp
 
-See below for additional usage instructions on this helper.
+   Ns2MobilityHelper ns2mobility("mobility-trace.ns_movements");
+   ns2mobility.Install();
 
-Scope and Limitations
-=====================
+The |ns2| mobility format is a widely used mobility trace format. Valid trace files use the following |ns2| statements:
 
-- Cartesian, geocentric cartesian, and geographic coordinates are presently supported
+.. sourcecode:: bash
 
-References
-==========
+   $node set X_ x1
+   $node set Y_ y1
+   $node set Z_ z1
+   $ns at $time $node setdest x2 y2 speed
 
-.. [Camp2002] T. Camp, J. Boleng, V. Davies.  "A survey of mobility models for ad hoc network research",
-   in Wireless Communications and Mobile Computing, 2002: vol. 2, pp. 2483-2502.
-.. [38811] 3GPP. 2018. TR 38.811, Study on New Radio (NR) to support non-terrestrial networks, V15.4.0. (2020-09).
+**Supported ns-2 Commands**
 
-Usage
-*****
+- ``$node set X_ x1``: Set initial X position
+- ``$node set Y_ y1``: Set initial Y position
+- ``$node set Z_ z1``: Set initial Z position
+- ``$ns at $time $node setdest x2 y2 speed``: Move to destination at specified time
+
+Note that in |ns3|, movement along the Z dimension is not supported by all mobility models.
 
-Most |ns3| program authors typically interact with the mobility system
-only at configuration time.  However, various |ns3| objects interact
-with mobility objects repeatedly during runtime, such as a propagation
-model trying to determine the path loss between two mobile nodes.
 
-Helper
-======
+Tracing and Visualization
+~~~~~~~~~~~~~~~~~~~~~~~~~
 
-A typical usage pattern can be found in the ``third.cc`` program in the
-tutorial.
+Tracing provides access to mobility models whenever the mobility model declares a course change (a change in position or velocity).  Mobility models can also be polled at any time to find the current position of the model.
 
-First, the user instantiates a ``MobilityHelper`` object and sets some
-``Attributes`` controlling the "position allocator" functionality.
+**Course Change Tracing**
+
+An example of this tracing method is the Waypoint Mobility Model. When a node selects a new waypoint, the ``NotifyCourseChange()`` function of the ``MobilityModel`` class (parent to the ``WaypointMobilityModel`` class) is called, triggering the course change trace. This allow the user to log when an object has reached certain points.
 
 .. sourcecode:: cpp
 
-  MobilityHelper mobility;
+  // Enable course change logging
+  void CourseChangeCallback(Ptr<const MobilityModel> model) {
+      Vector pos = model->GetPosition();
+      Vector vel = model->GetVelocity();
+      std::cout << "Node moved to: " << pos << " with velocity: " << vel << std::endl;
+  }
+
+  // Connect trace source
+  Config::Connect("/NodeList/*/$ns3::MobilityModel/CourseChange",
+                  MakeCallback(&CourseChangeCallback));
 
-  mobility.SetPositionAllocator("ns3::GridPositionAllocator",
-    "MinX", DoubleValue(0.0),
-    "MinY", DoubleValue(0.0),
-    "DeltaX", DoubleValue(5.0),
-    "DeltaY", DoubleValue(10.0),
-    "GridWidth", UintegerValue(3),
-    "LayoutType", StringValue("RowFirst"));
-
-This code tells the mobility helper to use a two-dimensional grid to initially
-place the nodes.  The first argument is an |ns3| TypeId specifying the
-type of mobility model; the remaining attribute/value pairs configure
-this position allocator.
+**Mobility Model Polling**
 
-Next, the user typically sets the MobilityModel subclass; e.g.:
+If client code needs access to a mobility model's position or other state information outside of course change events, it may directly query the mobility model at any time.  Mobility models are often aggregated (using ns-3 Object aggregation) to ns-3 nodes, so the mobility model pointer can usually be easily obtained from a node pointer using ``GetObject()``.
 
 .. sourcecode:: cpp
 
-  mobility.SetMobilityModel("ns3::RandomWalk2dMobilityModel",
-    "Bounds", RectangleValue(Rectangle(-50, 50, -50, 50)));
+  // Periodic position printing
+  void PrintPositions(NodeContainer nodes) {
+      for (auto i = nodes.Begin(); i != nodes.End(); ++i) {
+          Ptr<MobilityModel> mobility = (*i)->GetObject<MobilityModel>();
+          Vector pos = mobility->GetPosition();
+          std::cout << "Node " << (*i)->GetId() << ": " << pos << std::endl;
+      }
+      Simulator::Schedule(Seconds(1.0), &LogPositions, nodes);
+  }
 
-Once the helper is configured, it is typically passed a container, such as:
+Common Use Cases
+~~~~~~~~~~~~~~~~
 
-.. sourcecode:: cpp
+**Wireless Network Evaluation**
 
-  mobility.Install(wifiStaNodes);
+- **Scenario**: Mobile ad-hoc networks (MANETs)
+- **Models**: RandomWaypoint, RandomWalk2d
+- **Considerations**: Realistic speed distributions, pause times
 
-A MobilityHelper object may be reconfigured and reused for different
-NodeContainers during the configuration of an |ns3| scenario.
+**Vehicular Networks**
 
-Ns2MobilityHelper
-=================
+- **Scenario**: Vehicle-to-vehicle (V2V) communication
+- **Tools**: SUMO integration
+- **Models**: Trace-based mobility from traffic simulators
+
+**Sensor Networks**
+
+- **Scenario**: Environmental monitoring
+- **Models**: ConstantPosition with occasional RandomWalk
+- **Considerations**: Energy-efficient movement patterns
+
+**Satellite Communications**
+
+- **Scenario**: Low Earth Orbit (LEO) constellations
+- **Models**: GeocentricConstantPosition or custom orbital models
+- **Coordinates**: Geocentric Cartesian system
+
+Examples and Tests
+------------------
 
-Two example programs are provided demonstrating the use of the
-|ns2| mobility helper:
+The following example programs demonstrate mobility usage:
 
-- ns2-mobility-trace.cc
-- bonnmotion-ns2-example.cc
+- ``main-random-topology.cc`` - Random initial placement
+- ``main-random-walk.cc`` - Random walk mobility
+- ``main-grid-topology.cc`` - Grid-based positioning
+- ``ns2-mobility-trace.cc`` - |ns2| trace file parsing
+- ``reference-point-group-mobility-example.cc`` - Group mobility demonstration
 
-ns2-mobility-trace
-##################
+ns2-mobility-trace Example
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The ``ns2-mobility-trace.cc`` program is an example of loading an
-|ns2| trace file that specifies the movements of two nodes over 100
-seconds of simulation time.  It is paired with the file
-``default.ns_movements``.
+The ``ns2-mobility-trace.cc`` program is an example of loading an |ns2| trace file that specifies the movements of two nodes over 100 seconds of simulation time. It is paired with the file ``default.ns_movements``.
 
 The program behaves as follows:
 
-- a Ns2MobilityHelper object is created, with the specified trace file.
+- A Ns2MobilityHelper object is created, with the specified trace file.
 - A log file is created, using the log file name argument.
-- A node container is created with the number of nodes specified in the command line.  For this particular trace file, specify the value 2 for this argument.
-- the Install() method of Ns2MobilityHelper to set mobility to nodes. At this moment, the file is read line by line, and the movement is scheduled in the simulator.
+- A node container is created with the number of nodes specified in the command line. For this particular trace file, specify the value 2 for this argument.
+- The Install() method of Ns2MobilityHelper to set mobility to nodes. At this moment, the file is read line by line, and the movement is scheduled in the simulator.
 - A callback is configured, so each time a node changes its course a log message is printed.
 
-The example prints out messages generated by each read line from the ns2 movement trace file.   For each line, it shows if the line is correct, or of it has errors and in this case it will be ignored.
-
 Example usage:
 
 .. sourcecode:: bash
@@ -309,7 +524,7 @@
   +204480076.0ns POS: x=205.667, y=150, z=0; VEL:0, y=0, z=0
 
 bonnmotion-ns2-example
-######################
+~~~~~~~~~~~~~~~~~~~~~~
 
 The ``bonnmotion-ns2-example.cc`` program, which models the movement of
 a single mobile node for 1000 seconds of simulation time, has a few
@@ -375,100 +590,37 @@
 different than the respective position when using the trace file
 in |ns3|.
 
-Use of Random Variables
-=======================
-
-A typical use case is to evaluate protocols on a mobile topology that
-involves some randomness in the motion or initial position allocation.
-To obtain random motion and positioning that is not affected by
-the configuration of the rest of the scenario, it is recommended to
-use the "AssignStreams" facility of the random number system.
-
-Class ``MobilityModel`` and class ``PositionAllocator`` both have public
-API to assign streams to underlying random variables:
-
-.. sourcecode:: cpp
-
-  /**
-   * Assign a fixed random variable stream number to the random variables
-   * used by this model. Return the number of streams (possibly zero) that
-   * have been assigned.
-   *
-   * \param stream first stream index to use
-   * \return the number of stream indices assigned by this model
-   */
-  int64_t AssignStreams(int64_t stream);
-
-The class ``MobilityHelper`` also provides this API.  The typical usage
-pattern when using the helper is:
+reference-point-group-mobility-example
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. sourcecode:: cpp
-
-  int64_t streamIndex = /*some positive integer */
-  MobilityHelper mobility;
-  ... (configure mobility)
-  mobility.Install(wifiStaNodes);
-  int64_t streamsUsed = mobility.AssignStreams(wifiStaNodes, streamIndex);
-
-If AssignStreams is called before Install, it will not have any effect.
-
-Advanced Usage
-==============
+The reference point group mobility model [:ref:`1<mobilityRef1>`] is demonstrated in the example program ``reference-point-group-mobility-example.cc``. This example runs a short simulation that illustrates a parent WaypointMobilityModel traversing a rectangular course within a bounding box, and three member nodes independently execute a two-dimensional random walk around the parent position, within a small bounding box.
 
-A number of external tools can be used to generate traces read by
-the Ns2MobilityHelper.
+The example illustrates configuration using the GroupMobilityHelper and manual configuration without a helper; the configuration option is selectable by command-line argument.
 
-ns-2 scengen
-############
+The example outputs two mobility trace files, a course change trace and a time-series trace of node position. The latter trace file can be parsed by a Bash script (``reference-point-group-mobility-animate.sh``) to create PNG images at one-second intervals, which can then be combined using an image processing program such as ImageMagick to form a basic animated gif of the mobility.
 
-TBD
+Tests
+~~~~~
 
-BonnMotion
-##########
+Specific validation results and test cases are documented in the |ns3| test suite under ``src/mobility/test/``.
 
-http://net.cs.uni-bonn.de/wg/cs/applications/bonnmotion/
+Validation
+----------
 
-SUMO
-####
+No formal validation has been done.
 
-http://sourceforge.net/apps/mediawiki/sumo/index.php?title=Main_Page
+References
+----------
 
-TraNS
-#####
+.. _mobilityRef1:
 
-http://trans.epfl.ch/
+[`1 <https://onlinelibrary.wiley.com/doi/full/10.1002/wcm.72>`_] T. Camp, J. Boleng, V. Davies. "A survey of mobility models for ad hoc network research",
+   in Wireless Communications and Mobile Computing, 2002: vol. 2, pp. 2483-2502.
 
-Examples
-========
+.. _mobilityRef2:
 
-- main-random-topology.cc
-- main-random-walk.cc
-- main-grid-topology.cc
-- ns2-mobility-trace.cc
-- ns2-bonnmotion.cc
-
-reference-point-group-mobility-example.cc
-#########################################
-
-The reference point group mobility model ([Camp2002]_) is demonstrated
-in the example program `reference-point-group-mobility-example.cc`.
-This example runs a short simulation that illustrates a parent
-WaypointMobilityModel traversing a rectangular course within a bounding
-box, and three member nodes independently execute a two-dimensional
-random walk around the parent position, within a small bounding box.
-The example illustrates configuration using the GroupMobilityHelper
-and manual configuration without a helper; the configuration option
-is selectable by command-line argument.
-
-The example outputs two mobility trace files, a course change trace and
-a time-series trace of node position.  The latter trace file can be
-parsed by a Bash script (`reference-point-group-mobility-animate.sh`)
-to create PNG images at one-second intervals, which can then be combined
-using an image processing program such as ImageMagick to form a
-basic animated gif of the mobility.  The example and animation program
-files have further instructions on how to run them.
+[`2 <https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=3234>`_] 3GPP. 2018. TR 38.811, Study on New Radio (NR) to support non-terrestrial networks, V15.4.0. (2020-09).
 
-Validation
-**********
+.. _mobilityRef3:
 
-TBD
+[`3 <https://sys.cs.uos.de/bonnmotion/doc/README.pdf>`_] BonnMotion documentation
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mobility/helper/ns2-mobility-helper.cc ns-3.47/src/mobility/helper/ns2-mobility-helper.cc
--- ns-3.46/src/mobility/helper/ns2-mobility-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mobility/helper/ns2-mobility-helper.cc	2026-02-16 16:08:49.787230700 -0800
@@ -49,7 +49,6 @@
 #define NS2_Z_COORD "Z_"
 #define NS2_SETDEST "setdest"
 #define NS2_SET "set"
-#define NS2_NODEID "$node_("
 #define NS2_NS_SCH "$ns_"
 
 /**
@@ -782,8 +781,8 @@
     if (speed > 0)
     {
         // first calculate the time; time = distance / speed
-        double time = std::sqrt(std::pow(xFinalPosition - retval.m_finalPosition.x, 2) +
-                                std::pow(yFinalPosition - retval.m_finalPosition.y, 2)) /
+        double time = std::hypot(xFinalPosition - retval.m_finalPosition.x,
+                                 yFinalPosition - retval.m_finalPosition.y) /
                       speed;
         NS_LOG_DEBUG("at=" << at << " time=" << time);
         if (time == 0)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mobility/model/position-allocator.cc ns-3.47/src/mobility/model/position-allocator.cc
--- ns-3.46/src/mobility/model/position-allocator.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mobility/model/position-allocator.cc	2026-02-16 16:08:49.788230700 -0800
@@ -605,7 +605,7 @@
     {
         x = m_rv->GetValue(-m_rho, m_rho);
         y = m_rv->GetValue(-m_rho, m_rho);
-    } while (std::sqrt(x * x + y * y) > m_rho);
+    } while (std::hypot(x, y) > m_rho);
 
     x += m_x;
     y += m_y;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mobility/model/steady-state-random-waypoint-mobility-model.cc ns-3.47/src/mobility/model/steady-state-random-waypoint-mobility-model.cc
--- ns-3.46/src/mobility/model/steady-state-random-waypoint-mobility-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mobility/model/steady-state-random-waypoint-mobility-model.cc	2026-02-16 16:08:49.789230600 -0800
@@ -8,6 +8,7 @@
 #include "steady-state-random-waypoint-mobility-model.h"
 
 #include "ns3/double.h"
+#include "ns3/event-id.h"
 #include "ns3/simulator.h"
 #include "ns3/test.h"
 
@@ -131,6 +132,11 @@
     m_helper.Pause();
 
     // calculate the steady-state probability that a node is initially paused
+    // See the discussion in MR 2675 and
+    // Mathai, Moschopoulos and Pederzoli, Rendiconti Del Circolo Matematico Di Palermo,
+    // Serie II, Tomo XLVIII (1999), p. 163.
+    // https://www.researchgate.net/publication/250677777_Random_points_associated_with_rectangles
+
     double expectedPauseTime = (m_minPause + m_maxPause) / 2;
     double a = m_maxX - m_minX;
     double b = m_maxY - m_minY;
@@ -141,7 +147,7 @@
     double expectedTravelTime = 1.0 / 6.0 * (log1 + log2);
     expectedTravelTime +=
         1.0 / 15.0 * ((a * a * a) / (b * b) + (b * b * b) / (a * a)) -
-        1.0 / 15.0 * std::sqrt(a * a + b * b) * ((a * a) / (b * b) + (b * b) / (a * a) - 3);
+        1.0 / 15.0 * std::hypot(a, b) * ((a * a) / (b * b) + (b * b) / (a * a) - 3);
     if (v0 == v1)
     {
         expectedTravelTime /= v0;
@@ -197,7 +203,7 @@
             x2 = m_x2_r->GetValue(0, a);
             y2 = m_y2_r->GetValue(0, b);
             u1 = m_u_r->GetValue(0, 1);
-            r = std::sqrt(((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) / (a * a + b * b));
+            r = std::hypot((x2 - x1), (y2 - y1)) / std::hypot(a, b);
             NS_ASSERT(r <= 1);
         }
         double u2 = m_u_r->GetValue(0, 1);
@@ -223,12 +229,9 @@
     NS_ASSERT(m_minY <= destination.y && destination.y <= m_maxY);
     double u = m_u_r->GetValue(0, 1);
     double speed = std::pow(m_maxSpeed, u) / std::pow(m_minSpeed, u - 1);
-    double dx = (destination.x - m_current.x);
-    double dy = (destination.y - m_current.y);
-    double dz = (destination.z - m_current.z);
-    double k = speed / std::sqrt(dx * dx + dy * dy + dz * dz);
-
-    m_helper.SetVelocity(Vector(k * dx, k * dy, k * dz));
+    Vector deltaPos = destination - m_current;
+    double rate = speed / deltaPos.GetLength();
+    m_helper.SetVelocity(rate * deltaPos);
     m_helper.Unpause();
     Time travelDelay = Seconds(CalculateDistance(destination, m_current) / speed);
     m_event =
@@ -245,12 +248,9 @@
     NS_ASSERT(m_minY <= m_current.y && m_current.y <= m_maxY);
     Vector destination = m_position->GetNext();
     double speed = m_speed->GetValue();
-    double dx = (destination.x - m_current.x);
-    double dy = (destination.y - m_current.y);
-    double dz = (destination.z - m_current.z);
-    double k = speed / std::sqrt(dx * dx + dy * dy + dz * dz);
-
-    m_helper.SetVelocity(Vector(k * dx, k * dy, k * dz));
+    Vector deltaPos = destination - m_current;
+    double rate = speed / deltaPos.GetLength();
+    m_helper.SetVelocity(rate * deltaPos);
     m_helper.Unpause();
     Time travelDelay = Seconds(CalculateDistance(destination, m_current) / speed);
     m_event =
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mobility/test/steady-state-random-waypoint-mobility-model-test.cc ns-3.47/src/mobility/test/steady-state-random-waypoint-mobility-model-test.cc
--- ns-3.46/src/mobility/test/steady-state-random-waypoint-mobility-model-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mobility/test/steady-state-random-waypoint-mobility-model-test.cc	2026-02-16 16:08:49.790230800 -0800
@@ -26,7 +26,7 @@
 {
   public:
     SteadyStateRandomWaypointTest()
-        : TestCase("Check steady-state rwp mobility model velocity and position distributions")
+        : TestCase("Check steady-state rwp mobility model speed and position distributions")
     {
     }
 
@@ -91,18 +91,17 @@
 void
 SteadyStateRandomWaypointTest::DistribCompare()
 {
-    double velocity;
+    double speed;
     double sum_x = 0;
     double sum_y = 0;
     double sum_v = 0;
     for (auto i = mobilityStack.begin(); i != mobilityStack.end(); ++i)
     {
         auto model = (*i);
-        velocity =
-            std::sqrt(std::pow(model->GetVelocity().x, 2) + std::pow(model->GetVelocity().y, 2));
+        speed = model->GetVelocity().GetLength();
         sum_x += model->GetPosition().x;
         sum_y += model->GetPosition().y;
-        sum_v += velocity;
+        sum_v += speed;
     }
     double mean_x = sum_x / count;
     double mean_y = sum_y / count;
@@ -110,7 +109,7 @@
 
     NS_TEST_EXPECT_MSG_EQ_TOL(mean_x, 500, 25.0, "Got unexpected x-position mean value");
     NS_TEST_EXPECT_MSG_EQ_TOL(mean_y, 300, 15.0, "Got unexpected y-position mean value");
-    NS_TEST_EXPECT_MSG_EQ_TOL(mean_v, 2.6, 0.13, "Got unexpected velocity mean value");
+    NS_TEST_EXPECT_MSG_EQ_TOL(mean_v, 2.6, 0.13, "Got unexpected speed mean value");
 
     sum_x = 0;
     sum_y = 0;
@@ -119,13 +118,12 @@
     for (auto i = mobilityStack.begin(); i != mobilityStack.end(); ++i)
     {
         auto model = (*i);
-        velocity =
-            std::sqrt(std::pow(model->GetVelocity().x, 2) + std::pow(model->GetVelocity().y, 2));
+        speed = model->GetVelocity().GetLength();
         tmp = model->GetPosition().x - mean_x;
         sum_x += tmp * tmp;
         tmp = model->GetPosition().y - mean_y;
         sum_y += tmp * tmp;
-        tmp = velocity - mean_v;
+        tmp = speed - mean_v;
         sum_v += tmp * tmp;
     }
     double dev_x = std::sqrt(sum_x / (count - 1));
@@ -134,7 +132,7 @@
 
     NS_TEST_EXPECT_MSG_EQ_TOL(dev_x, 230, 10.0, "Got unexpected x-position standard deviation");
     NS_TEST_EXPECT_MSG_EQ_TOL(dev_y, 140, 7.0, "Got unexpected y-position standard deviation");
-    NS_TEST_EXPECT_MSG_EQ_TOL(dev_v, 4.4, 0.22, "Got unexpected velocity standard deviation");
+    NS_TEST_EXPECT_MSG_EQ_TOL(dev_v, 4.4, 0.22, "Got unexpected speed standard deviation");
 }
 
 /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mpi/model/granted-time-window-mpi-interface.h ns-3.47/src/mpi/model/granted-time-window-mpi-interface.h
--- ns-3.46/src/mpi/model/granted-time-window-mpi-interface.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mpi/model/granted-time-window-mpi-interface.h	2026-02-16 16:08:49.790230800 -0800
@@ -105,7 +105,7 @@
      * collaborators are private to restrict use.
      * It is not intended for state to be shared.
      */
-    friend ns3::DistributedSimulatorImpl;
+    friend class ns3::DistributedSimulatorImpl;
 
     /**
      * Check for received messages complete
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mpi/model/mpi-interface.h ns-3.47/src/mpi/model/mpi-interface.h
--- ns-3.46/src/mpi/model/mpi-interface.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mpi/model/mpi-interface.h	2026-02-16 16:08:49.791230700 -0800
@@ -93,13 +93,13 @@
      * MPI_COMM_WORLD communicator.
      *
      * For more complex situations, such as embedding ns-3 with other
-     * MPI simulators or libraries, the Enable(MPI_Comm communcicator)
+     * MPI simulators or libraries, the Enable(MPI_Comm communicator)
      * may be used if MPI is initialized externally or if ns-3 needs to
      * be run unique communicator.  For example if there are two
      * parallel simulators and the goal is to run each simulator on a
      * different set of ranks.
      *
-     * @note The `SimulatorImplementationType attribute in
+     * @note The `SimulatorImplementationType` attribute in
      * ns3::GlobalValues must be set before calling Enable()
      *
      * @param pargc number of command line arguments
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/mpi/model/null-message-mpi-interface.h ns-3.47/src/mpi/model/null-message-mpi-interface.h
--- ns-3.46/src/mpi/model/null-message-mpi-interface.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/mpi/model/null-message-mpi-interface.h	2026-02-16 16:08:49.791230700 -0800
@@ -67,8 +67,8 @@
      * collaborators are private to restrict use.
      * It is not intended for state to be shared.
      */
-    friend ns3::RemoteChannelBundle;
-    friend ns3::NullMessageSimulatorImpl;
+    friend class ns3::RemoteChannelBundle;
+    friend class ns3::NullMessageSimulatorImpl;
 
     /**
      * @brief Send a Null Message to across the specified bundle.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/netanim/examples/star-animation.cc ns-3.47/src/netanim/examples/star-animation.cc
--- ns-3.46/src/netanim/examples/star-animation.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/netanim/examples/star-animation.cc	2026-02-16 16:08:49.797230700 -0800
@@ -43,8 +43,8 @@
     uint32_t nSpokes = 8;
     std::string animFile = "star-animation.xml";
     uint8_t useIpv6 = 0;
-    Ipv6Address ipv6AddressBase = Ipv6Address("2001::");
-    Ipv6Prefix ipv6AddressPrefix = Ipv6Prefix(64);
+    auto ipv6AddressBase = Ipv6Address("2001::");
+    auto ipv6AddressPrefix = Ipv6Prefix(64);
 
     CommandLine cmd(__FILE__);
     cmd.AddValue("nSpokes", "Number of spoke nodes to place in the star", nSpokes);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/CMakeLists.txt ns-3.47/src/network/CMakeLists.txt
--- ns-3.46/src/network/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/CMakeLists.txt	2026-02-16 16:08:49.798230600 -0800
@@ -158,6 +158,7 @@
   HEADER_FILES ${header_files}
   LIBRARIES_TO_LINK ${libstats}
   TEST_SOURCES
+    test/address-test.cc
     test/bit-serializer-test.cc
     test/buffer-test.cc
     test/drop-tail-queue-test-suite.cc
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/doc/error-model.rst ns-3.47/src/network/doc/error-model.rst
--- ns-3.46/src/network/doc/error-model.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/doc/error-model.rst	2026-02-16 16:08:49.798230600 -0800
@@ -11,11 +11,11 @@
 This section documents a few error model objects, typically associated with
 NetDevice models, that are maintained as part of the ``network`` module:
 
-* RateErrorModel
-* ListErrorModel
-* ReceiveListErrorModel
-* BurstErrorModel
-* BinaryErrorModel
+* ``RateErrorModel``
+* ``ListErrorModel``
+* ``ReceiveListErrorModel``
+* ``BurstErrorModel``
+* ``BinaryErrorModel``
 
 Error models are used to indicate that a packet should be considered to
 be errored, according to the underlying (possibly stochastic or
@@ -55,7 +55,7 @@
 Error models are |ns3| objects and can be created using the typical
 pattern of ``CreateObject<>()``.  They have configuration attributes.
 
-An ErrorModel can be applied anywhere, but are commonly deployed on
+An ``ErrorModel`` can be applied anywhere, but are commonly deployed on
 NetDevice models so that artificial losses (mimicking channel losses)
 can be induced.
 
@@ -101,7 +101,7 @@
 * ``CsmaNetDevice``
 * ``VirtualNetDevice``
 
-However, the ErrorModel could be used anywhere where packets are used
+However, the ``ErrorModel`` could be used anywhere where packets are used
 
 Helpers
 =======
@@ -131,8 +131,8 @@
 Acknowledgements
 ****************
 
-The basic ErrorModel, RateErrorModel, and ListErrorModel classes were ported
-from |ns2| to |ns3| in 2007.  The ReceiveListErrorModel was added at that
+The basic ``ErrorModel``, ``RateErrorModel``, and ``ListErrorModel`` classes were ported
+from |ns2| to |ns3| in 2007.  The ``ReceiveListErrorModel`` was added at that
 time.
 
 The burst error model is due to Truc Anh N. Nguyen at the University of
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/doc/packets.rst ns-3.47/src/network/doc/packets.rst
--- ns-3.46/src/network/doc/packets.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/doc/packets.rst	2026-02-16 16:08:49.798230600 -0800
@@ -37,12 +37,12 @@
 pretty-print the packet headers without context. The packet metadata describes
 the type of the headers and trailers which were serialized in the byte buffer.
 The maintenance of metadata is optional and disabled by default. To enable it,
-you must call Packet::EnablePrinting() and this will allow you to get non-empty
-output from Packet::Print and Packet::Print.
+you must call ``Packet::EnablePrinting()`` and this will allow you to get non-empty
+output from ``Packet::Print()`` and ``Packet::Print()``.
 
 Also, developers often want to store data in packet objects that is not found
-in the real packets (such as timestamps or flow-ids). The Packet class
-deals with this requirement by storing a set of tags (class Tag).
+in the real packets (such as timestamps or flow-ids). The ``Packet`` class
+deals with this requirement by storing a set of tags (class ``Tag``).
 We have found two classes of use cases for these tags, which leads to
 two different types of tags. So-called 'byte' tags are used to tag a subset of
 the bytes in the packet byte buffer while 'packet' tags are used to tag the
@@ -52,7 +52,7 @@
 these two kinds of tags is that byte tags cannot be removed and are expected to
 be written once, and read many times, while packet tags are expected to be
 written once, read many times, and removed exactly once. An example of a 'byte'
-tag is a FlowIdTag which contains a flow id and is set by the application
+tag is a ``FlowIdTag`` which contains a flow id and is set by the application
 generating traffic. An example of a 'packet' tag is a cross-layer QoS class id
 set by an application and processed by a lower-level MAC layer.
 
@@ -73,10 +73,10 @@
 Packet design overview
 **********************
 
-Unlike |ns2|, in which Packet objects contain a buffer of C++
+Unlike |ns2|, in which ``Packet`` objects contain a buffer of C++
 structures corresponding to protocol headers, each network packet in
-|ns3| contains a byte Buffer, a list of byte Tags, a list of
-packet Tags, and a PacketMetadata object:
+|ns3| contains a byte ``Buffer``, a list of byte ``Tags``, a list of
+packet ``Tags``, and a ``PacketMetadata`` object:
 
 * The byte buffer stores the serialized content of the chunks added to a packet.
   The serialized representation of these chunks is expected to match that of
@@ -107,8 +107,8 @@
 
 Packets are reference counted objects. They are handled with smart pointer (Ptr)
 objects like many of the objects in the |ns3| system.  One small difference you
-will see is that class Packet does not inherit from class Object or class
-RefCountBase, and implements the Ref() and Unref() methods directly. This was
+will see is that class ``Packet`` does not inherit from class Object or class
+``RefCountBase``, and implements the ``Ref()`` and ``Unref()`` methods directly. This was
 designed to avoid the overhead of a vtable in class Packet.
 
 The Packet class is designed to be copied cheaply; the overall design
@@ -134,8 +134,8 @@
 * ``ns3::Header::PrintTo()``
 
 Basically, the first three functions are used to serialize and deserialize
-protocol control information to/from a Buffer. For example, one may define
-``class TCPHeader : public Header``. The TCPHeader object will typically
+protocol control information to/from a ``Buffer``. For example, one may define
+``class TCPHeader : public Header``. The ``TCPHeader`` object will typically
 consist of some private data (like a sequence number) and public interface
 access functions (such as checking the bounds of an input). But the underlying
 representation of the TCPHeader in a Packet Buffer is 20 serialized bytes (plus
@@ -213,10 +213,10 @@
 
 After the initial packet creation (which may possibly create some fake initial
 bytes of payload), all subsequent buffer data is added by adding objects of
-class Header or class Trailer. Note that, even if you are in the application
+class ``Header`` or class ``Trailer``. Note that, even if you are in the application
 layer, handling packets, and want to write application data, you write it as an
-ns3::Header or ns3::Trailer. If you add a Header, it is prepended to the
-packet, and if you add a Trailer, it is added to the end of the packet. If you
+``ns3::Header`` or ``ns3::Trailer``. If you add a ``Header``, it is prepended to the
+packet, and if you add a ``Trailer``, it is added to the end of the packet. If you
 have no data in the packet, then it makes no difference whether you add a Header
 or Trailer. Since the APIs and classes for header and trailer are pretty much
 identical, we'll just look at class Header here.
@@ -229,7 +229,7 @@
 * ``GetSerializedSize ()``
 * ``Print ()``
 
-To see a simple example of how these are done, look at the UdpHeader class
+To see a simple example of how these are done, look at the ``UdpHeader`` class
 headers src/internet/model/udp-header.cc. There are many other examples within
 the source code.
 
@@ -302,11 +302,11 @@
 Adding and removing Tags
 ++++++++++++++++++++++++
 
-There is a single base class of Tag that all packet tags must derive from. They
+There is a single base class of ``Tag`` that all packet tags must derive from. They
 are used in two different tag lists in the packet; the lists have different
 semantics and different expected use cases.
 
-As the names imply, ByteTags follow bytes and PacketTags follow packets. What
+As the names imply, ``ByteTags`` follow bytes and ``PacketTags`` follow packets. What
 this means is that when operations are done on packets, such as fragmentation,
 concatenation, and appending or removing headers, the byte tags keep track of
 which packet bytes they cover. For instance, if a user creates a TCP segment,
@@ -338,7 +338,7 @@
 
 If a user wants to take an existing packet object and reuse it as a new packet,
 he or she should remove all byte tags and packet tags before doing so. An
-example is the UdpEchoServer class, which takes the received packet and "turns
+example is the ``UdpEchoServer`` class, which takes the received packet and "turns
 it around" to send back to the echo client.
 
 The Packet API for byte tags is given below.::
@@ -514,7 +514,7 @@
 Private member variables
 ++++++++++++++++++++++++
 
-A Packet object's interface provides access to some private data::
+A ``Packet`` object's interface provides access to some private data::
 
   Buffer m_buffer;
   ByteTagList m_byteTagList;
@@ -523,7 +523,7 @@
   mutable uint32_t m_refCount;
   static uint32_t m_globalUid;
 
-Each Packet has a Buffer and two Tags lists, a PacketMetadata object, and a ref
+Each ``Packet`` has a ``Buffer`` and two ``Tags`` lists, a ``PacketMetadata`` object, and a ref
 count. A static member variable keeps track of the UIDs allocated. The actual
 uid of the packet is stored in the PacketMetadata.
 
@@ -536,14 +536,14 @@
 Buffer implementation
 +++++++++++++++++++++
 
-Class Buffer represents a buffer of bytes. Its size is automatically adjusted to
+Class ``Buffer`` represents a buffer of bytes. Its size is automatically adjusted to
 hold any data prepended or appended by the user. Its implementation is optimized
 to ensure that the number of buffer resizes is minimized, by creating new
 Buffers of the maximum size ever used.  The correct maximum size is learned at
 runtime during use by recording the maximum size of each packet.
 
-Authors of new Header or Trailer classes need to know the public API of the
-Buffer class.  (add summary here)
+Authors of new ``Header`` or ``Trailer`` classes need to know the public API of the
+``Buffer`` class.  (add summary here)
 
 The byte buffer is implemented as follows: ::
 
@@ -560,18 +560,18 @@
     uint32_t m_start;
     uint32_t m_size;
 
-* ``BufferData::m_count``: reference count for BufferData structure
-* ``BufferData::m_size``: size of data buffer stored in BufferData structure
+* ``BufferData::m_count``: reference count for ``BufferData`` structure
+* ``BufferData::m_size``: size of data buffer stored in ``BufferData`` structure
 * ``BufferData::m_initialStart``: offset from start of data buffer where data
   was first inserted
-* ``BufferData::m_dirtyStart``: offset from start of buffer where every Buffer
-  which holds a reference to this BufferData instance have written data so far
+* ``BufferData::m_dirtyStart``: offset from start of buffer where every ``Buffer``
+  which holds a reference to this ``BufferData`` instance have written data so far
 * ``BufferData::m_dirtySize``: size of area where data has been written so far
 * ``BufferData::m_data``: pointer to data buffer
 * ``Buffer::m_zeroAreaSize``: size of zero area which extends before
   ``m_initialStart``
 * ``Buffer::m_start``: offset from start of buffer to area used by this buffer
-* ``Buffer::m_size``: size of area used by this Buffer in its BufferData
+* ``Buffer::m_size``: size of area used by this ``Buffer`` in its ``BufferData``
   structure
 
 .. _buffer:
@@ -581,12 +581,12 @@
    Implementation overview of a packet's byte Buffer.
 
 
-This data structure is summarized in Figure :ref:`buffer`. Each Buffer holds a
-pointer to an instance of a BufferData. Most Buffers should be able to share the
-same underlying BufferData and thus simply increase the BufferData's reference
-count. If they have to change the content of a BufferData inside the Dirty Area,
+This data structure is summarized in Figure :ref:`buffer`. Each ``Buffer`` holds a
+pointer to an instance of a ``BufferData``. Most ``Buffers`` should be able to share the
+same underlying ``BufferData`` and thus simply increase the ``BufferData``'s reference
+count. If they have to change the content of a ``BufferData`` inside the Dirty Area,
 and if the reference count is not one, they first create a copy of the
-BufferData and then complete their state-changing operation.
+``BufferData`` and then complete their state-changing operation.
 
 Tags implementation
 +++++++++++++++++++
@@ -594,7 +594,7 @@
 (XXX revise me)
 
 Tags are implemented by a single pointer which points to the start of a
-linked list ofTagData data structures. Each TagData structure points
+linked list of ``TagData`` data structures. Each ``TagData`` structure points
 to the next TagData in the list (its next pointer contains zero to
 indicate the end of the linked list). Each TagData contains an integer
 unique id which identifies the type of the tag stored in the TagData.::
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/doc/queue.rst ns-3.47/src/network/doc/queue.rst
--- ns-3.46/src/network/doc/queue.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/doc/queue.rst	2026-02-16 16:08:49.798230600 -0800
@@ -18,7 +18,7 @@
 
 The source code for the new module lives in the directory ``src/network/utils``.
 
-ns3::Queue has been redesigned as a template class object to allow us to
+The ``Queue`` class has been redesigned as a template class object to allow us to
 instantiate queues storing different types of items. The unique template
 type parameter specifies the type of items stored in the queue.
 The only requirement on the item type is that it must provide a GetSize ()
@@ -29,25 +29,25 @@
 * QueueItem and subclasses (e.g., QueueDiscItem)
 * WifiMacQueueItem
 
-The internal queues of the queue discs are of type Queue<QueueDiscItem>
-(an alias of which being InternalQueue). A number of network devices
-(SimpleNetDevice, PointToPointNetDevice, CsmaNetDevice) use a Queue<Packet>
-to store packets to be transmitted. WifiNetDevices use instead queues of
-type WifiMacQueue, which is a subclass of Queue storing objects of
+The internal queues of the queue discs are of type ``Queue<QueueDiscItem>``
+(an alias of which being ``InternalQueue``). A number of network devices
+(``SimpleNetDevice``, ``PointToPointNetDevice``, ``CsmaNetDevice``) use a ``Queue<Packet>``
+to store packets to be transmitted. ``WifiNetDevices`` use instead queues of
+type ``WifiMacQueue``, which is a subclass of ``Queue`` storing objects of
 type WifiMacQueueItem. Other devices, such as WiMax and LTE, use specialized
 queues.
 
 Design
 ======
 
-The Queue class derives from the QueueBase class, which is a non-template
+The ``Queue`` class derives from the ``QueueBase`` class, which is a non-template
 class providing all the methods that are independent of the type of the items
-stored in the queue. The Queue class provides instead all the operations that
+stored in the queue. The ``Queue`` class provides instead all the operations that
 depend on the item type, such as enqueue, dequeue, peek and remove. The Queue
 class also provides the ability to trace certain queue operations such as
 enqueuing, dequeuing, and dropping.
 
-Queue is an abstract base class and is subclassed for specific scheduling and
+``Queue`` is an abstract base class and is subclassed for specific scheduling and
 drop policies. Subclasses need to define the following public methods:
 
 * ``bool Enqueue (Ptr<Item> item)``:  Enqueue a packet
@@ -57,7 +57,7 @@
 
 The Enqueue method does not allow to store a packet if the queue capacity is exceeded.
 Subclasses may also define specialized public methods. For instance, the
-WifiMacQueue class provides a method to dequeue a packet based on its tid
+``WifiMacQueue`` class provides a method to dequeue a packet based on its tid
 and MAC address.
 
 There are five trace sources that may be hooked:
@@ -79,7 +79,7 @@
 This is a basic first-in-first-out (FIFO) queue that performs a tail drop
 when the queue is full.
 
-The DropTailQueue class defines one attribute:
+The ``DropTailQueue`` class defines one attribute:
 
 * ``MaxSize``: the maximum queue size
 
@@ -114,7 +114,7 @@
 
 Please note that the SetQueue method of the PointToPointHelper class allows
 to specify "ns3::DropTailQueue" instead of "ns3::DropTailQueue<Packet>". The
-same holds for CsmaHelper, SimpleNetDeviceHelper and TrafficControlHelper.
+same holds for ``CsmaHelper``, ``SimpleNetDeviceHelper`` and ``TrafficControlHelper``.
 
 Output
 ======
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/doc/sockets-api.rst ns-3.47/src/network/doc/sockets-api.rst
--- ns-3.46/src/network/doc/sockets-api.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/doc/sockets-api.rst	2026-02-16 16:08:49.798230600 -0800
@@ -37,7 +37,7 @@
 with real sockets function calls, and all other things being equal,
 we have tried to align with a Posix sockets API.  However, note that:
 
-* ns-3 applications handle a smart pointer to a Socket object, not
+* ns-3 applications handle a smart pointer to a :cpp:class:`Socket` object, not
   a file descriptor;
 
 * there is no notion of synchronous API or a *blocking* API;
@@ -150,7 +150,7 @@
 Packet vs. buffer variants
 **************************
 
-There are two basic variants of ``Send()`` and ``Recv()`` supported::
+There are two basic variants of :cpp:func:`Send()` and :cpp:func:`Recv()` supported::
 
   virtual int Send(Ptr<Packet> p) = 0;
   int Send(const uint8_t* buf, uint32_t size);
@@ -183,7 +183,8 @@
 actual data payload in the packet (e.g. to calculate transmission delay)
 but do not want to actually produce or consume the data.  This is
 straightforward to support in ns-3; have applications call
-``Create<Packet> (size);`` instead of ``Create<Packet> (buffer, size);``.
+``Ptr<Packet> Create<Packet>(uint32_t)`` instead of
+``Ptr<Packet> Create<Packet>(const uint32_t*, uint32_t)``.
 Similarly, passing in a zero to the pointer argument in the raw buffer
 variants has the same effect.  Note that, if some subsequent code tries
 to read the Packet data buffer, the fake buffer will be converted to
@@ -202,13 +203,13 @@
                       const Address &toAddress) = 0;
 
 The first method is used if the socket has already been connected
-(``Socket::Connect()``) to a peer address.  In the case of stream-based
+(:cpp:func:`Socket::Connect()`) to a peer address.  In the case of stream-based
 sockets like TCP, the connect call is required to bind the socket to
-a peer address, and thereafter, ``Send()`` is typically used.  In the case of
+a peer address, and thereafter, :cpp:func:`Send()` is typically used.  In the case of
 datagram-based sockets like UDP, the socket is not required to
 be connected to a peer address before sending, and the socket may be used to
 send data to different destination addresses; in this case, the
-``SendTo()`` method is used to specify the destination address for
+:cpp:func:`SendTo()` method is used to specify the destination address for
 the datagram.
 
 Socket options
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/helper/application-container.h ns-3.47/src/network/helper/application-container.h
--- ns-3.46/src/network/helper/application-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/helper/application-container.h	2026-02-16 16:08:49.799230600 -0800
@@ -68,11 +68,10 @@
      * for-loop to run through the Applications
      *
      * @code
-     *   ApplicationContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some Application method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some Application method
+     *   }
      * @endcode
      *
      * @returns an iterator which refers to the first Application in the container.
@@ -89,11 +88,10 @@
      * for-loop to run through the Applications
      *
      * @code
-     *   ApplicationContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some Application method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some Application method
+     *   }
      * @endcode
      *
      * @returns an iterator which indicates an ending condition for a loop.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/helper/net-device-container.h ns-3.47/src/network/helper/net-device-container.h
--- ns-3.46/src/network/helper/net-device-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/helper/net-device-container.h	2026-02-16 16:08:49.799230600 -0800
@@ -85,11 +85,10 @@
      * for-loop to run through the NetDevices
      *
      * @code
-     *   NetDeviceContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some NetDevice method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some NetDevice method
+     *   }
      * @endcode
      *
      * @returns an iterator which refers to the first NetDevice in the container.
@@ -106,11 +105,10 @@
      * for-loop to run through the NetDevices
      *
      * @code
-     *   NetDeviceContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some NetDevice method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some NetDevice method
+     *   }
      * @endcode
      *
      * @returns an iterator which indicates an ending condition for a loop.
@@ -159,6 +157,7 @@
      *
      * @param i the index of the requested device pointer.
      * @returns the requested device pointer.
+     * @hidecaller
      */
     Ptr<NetDevice> Get(uint32_t i) const;
 
@@ -167,6 +166,7 @@
      * this container.
      *
      * @param other The NetDeviceContainer to append.
+     * @hidecaller
      */
     void Add(NetDeviceContainer other);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/helper/node-container.h ns-3.47/src/network/helper/node-container.h
--- ns-3.46/src/network/helper/node-container.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/helper/node-container.h	2026-02-16 16:08:49.799230600 -0800
@@ -112,14 +112,14 @@
      * for-loop to run through the Nodes
      *
      * @code
-     *   NodeContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some Node method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some Node method
+     *   }
      * @endcode
      *
      * @returns an iterator which refers to the first Node in the container.
+     * @hidecaller
      */
     Iterator Begin() const;
 
@@ -133,14 +133,14 @@
      * for-loop to run through the Nodes
      *
      * @code
-     *   NodeContainer::Iterator i;
-     *   for (i = container.Begin (); i != container.End (); ++i)
-     *     {
-     *       (*i)->method ();  // some Node method
-     *     }
+     *   for (auto i = container.Begin(); i != container.End(); ++i)
+     *   {
+     *       (*i)->method(); // some Node method
+     *   }
      * @endcode
      *
      * @returns an iterator which indicates an ending condition for a loop.
+     * @hidecaller
      */
     Iterator End() const;
 
@@ -186,6 +186,7 @@
      *
      * @param i the index of the requested node pointer.
      * @returns the requested node pointer.
+     * @hidecaller
      */
     Ptr<Node> Get(uint32_t i) const;
 
@@ -198,6 +199,7 @@
      * automates that task.
      *
      * @param n The number of Nodes to create
+     * @hidecaller
      */
     void Create(uint32_t n);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/model/address.cc ns-3.47/src/network/model/address.cc
--- ns-3.46/src/network/model/address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/model/address.cc	2026-02-16 16:08:49.799230600 -0800
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: GPL-2.0-only
  *
  * Author: Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
+ * Modified by: Dhiraj Lokesh <dhirajlokesh@gmail.com>
  */
 
 #include "address.h"
@@ -20,47 +21,41 @@
 
 NS_LOG_COMPONENT_DEFINE("Address");
 
-Address::Address()
-    : m_type(0),
-      m_len(0)
-{
-    // Buffer left uninitialized
-    NS_LOG_FUNCTION(this);
-}
+Address::KindTypeRegistry Address::m_typeRegistry;
 
 Address::Address(uint8_t type, const uint8_t* buffer, uint8_t len)
     : m_type(type),
       m_len(len)
 {
     NS_LOG_FUNCTION(this << static_cast<uint32_t>(type) << &buffer << static_cast<uint32_t>(len));
-    NS_ASSERT(m_len <= MAX_SIZE);
-    std::memcpy(m_data, buffer, m_len);
+    NS_ASSERT_MSG(m_len <= MAX_SIZE, "Address length too large");
+    NS_ASSERT_MSG(type != UNASSIGNED_TYPE, "Type 0 is reserved for uninitialized addresses.");
+    std::copy(buffer, buffer + len, m_data.begin());
 }
 
-Address::Address(const Address& address)
-    : m_type(address.m_type),
-      m_len(address.m_len)
+void
+Address::SetType(const std::string& kind, uint8_t length)
 {
-    NS_ASSERT(m_len <= MAX_SIZE);
-    std::memcpy(m_data, address.m_data, m_len);
-}
+    NS_LOG_FUNCTION(this << kind << static_cast<uint32_t>(length));
+    auto search = m_typeRegistry.find({kind, length});
+    NS_ASSERT_MSG(search != m_typeRegistry.end(),
+                  "No address with the given kind and length is registered.");
 
-Address&
-Address::operator=(const Address& address)
-{
-    NS_ASSERT(m_len <= MAX_SIZE);
-    m_type = address.m_type;
-    m_len = address.m_len;
-    NS_ASSERT(m_len <= MAX_SIZE);
-    std::memcpy(m_data, address.m_data, m_len);
-    return *this;
+    if (m_type != search->second)
+    {
+        NS_ASSERT_MSG(m_type == UNASSIGNED_TYPE,
+                      "You can only change the type of a type-0 address."
+                          << static_cast<uint32_t>(m_type));
+
+        m_type = search->second;
+    }
 }
 
 bool
 Address::IsInvalid() const
 {
     NS_LOG_FUNCTION(this);
-    return m_len == 0 && m_type == 0;
+    return m_len == 0 && m_type == UNASSIGNED_TYPE;
 }
 
 uint8_t
@@ -76,7 +71,7 @@
 {
     NS_LOG_FUNCTION(this << &buffer);
     NS_ASSERT(m_len <= MAX_SIZE);
-    std::memcpy(buffer, m_data, m_len);
+    std::copy(m_data.begin(), m_data.begin() + m_len, buffer);
     return m_len;
 }
 
@@ -87,7 +82,7 @@
     NS_ASSERT(len - m_len > 1);
     buffer[0] = m_type;
     buffer[1] = m_len;
-    std::memcpy(buffer + 2, m_data, m_len);
+    std::copy(m_data.begin(), m_data.begin() + m_len, buffer + 2);
     return m_len + 2;
 }
 
@@ -95,8 +90,10 @@
 Address::CopyFrom(const uint8_t* buffer, uint8_t len)
 {
     NS_LOG_FUNCTION(this << &buffer << static_cast<uint32_t>(len));
-    NS_ASSERT(len <= MAX_SIZE);
-    std::memcpy(m_data, buffer, len);
+    NS_ASSERT_MSG(m_len <= MAX_SIZE, "Address length too large");
+    NS_ASSERT_MSG(m_type != UNASSIGNED_TYPE,
+                  "Type-0 addresses are reserved. Please use SetType before using CopyFrom.");
+    std::copy(buffer, buffer + len, m_data.begin());
     m_len = len;
     return m_len;
 }
@@ -110,7 +107,7 @@
     m_len = buffer[1];
 
     NS_ASSERT(len - m_len > 1);
-    std::memcpy(m_data, buffer + 2, m_len);
+    std::copy(buffer + 2, buffer + 2 + m_len, m_data.begin());
     return m_len + 2;
 }
 
@@ -119,9 +116,7 @@
 {
     NS_LOG_FUNCTION(this << static_cast<uint32_t>(type) << static_cast<uint32_t>(len));
     NS_ASSERT(len <= MAX_SIZE);
-    /// @internal
-    /// Mac address type/length detection is discussed in \bugid{1568}
-    return (m_len == len && m_type == type) || (m_len >= len && m_type == 0);
+    return (m_len == len && m_type == type);
 }
 
 bool
@@ -132,12 +127,15 @@
 }
 
 uint8_t
-Address::Register()
+Address::Register(const std::string& kind, uint8_t length)
 {
-    NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = 1;
-    type++;
-    return type;
+    NS_LOG_FUNCTION(kind << length);
+    static uint8_t lastRegisteredType = UNASSIGNED_TYPE;
+    NS_ASSERT_MSG(m_typeRegistry.find({kind, length}) == m_typeRegistry.end(),
+                  "An address of the same kind and length is already registered.");
+    lastRegisteredType++;
+    m_typeRegistry[{kind, length}] = lastRegisteredType;
+    return lastRegisteredType;
 }
 
 uint32_t
@@ -153,7 +151,7 @@
     NS_LOG_FUNCTION(this << &buffer);
     buffer.WriteU8(m_type);
     buffer.WriteU8(m_len);
-    buffer.Write(m_data, m_len);
+    buffer.Write(m_data.data(), m_len);
 }
 
 void
@@ -163,83 +161,22 @@
     m_type = buffer.ReadU8();
     m_len = buffer.ReadU8();
     NS_ASSERT(m_len <= MAX_SIZE);
-    buffer.Read(m_data, m_len);
+    buffer.Read(m_data.data(), m_len);
 }
 
 ATTRIBUTE_HELPER_CPP(Address);
 
-bool
-operator==(const Address& a, const Address& b)
-{
-    /* Two addresses can be equal even if their types are
-     * different if one of the two types is zero. a type of
-     * zero identifies an Address which might contain meaningful
-     * payload but for which the type field could not be set because
-     * we did not know it. This can typically happen in the ARP
-     * layer where we receive an address from an ArpHeader but
-     * we do not know its type: we really want to be able to
-     * compare addresses without knowing their real type.
-     */
-    if (a.m_type != b.m_type && a.m_type != 0 && b.m_type != 0)
-    {
-        return false;
-    }
-    if (a.m_len != b.m_len)
-    {
-        return false;
-    }
-    return std::memcmp(a.m_data, b.m_data, a.m_len) == 0;
-}
-
-bool
-operator!=(const Address& a, const Address& b)
-{
-    return !(a == b);
-}
-
-bool
-operator<(const Address& a, const Address& b)
-{
-    if (a.m_type < b.m_type)
-    {
-        return true;
-    }
-    else if (a.m_type > b.m_type)
-    {
-        return false;
-    }
-    if (a.m_len < b.m_len)
-    {
-        return true;
-    }
-    else if (a.m_len > b.m_len)
-    {
-        return false;
-    }
-    NS_ASSERT(a.GetLength() == b.GetLength());
-    for (uint8_t i = 0; i < a.GetLength(); i++)
-    {
-        if (a.m_data[i] < b.m_data[i])
-        {
-            return true;
-        }
-        else if (a.m_data[i] > b.m_data[i])
-        {
-            return false;
-        }
-    }
-    return false;
-}
-
 std::ostream&
 operator<<(std::ostream& os, const Address& address)
 {
-    if (address.m_len == 0)
+    if (address.m_type == 0)
     {
+        os << "00-00:00";
         return os;
     }
+    std::ios_base::fmtflags ff = os.flags();
+    auto fill = os.fill('0');
     os.setf(std::ios::hex, std::ios::basefield);
-    os.fill('0');
     os << std::setw(2) << (uint32_t)address.m_type << "-" << std::setw(2) << (uint32_t)address.m_len
        << "-";
     for (uint8_t i = 0; i < (address.m_len - 1); ++i)
@@ -248,8 +185,8 @@
     }
     // Final byte not suffixed by ":"
     os << std::setw(2) << (uint32_t)address.m_data[address.m_len - 1];
-    os.setf(std::ios::dec, std::ios::basefield);
-    os.fill(' ');
+    os.flags(ff); // Restore stream flags
+    os.fill(fill);
     return os;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/model/address.h ns-3.47/src/network/model/address.h
--- ns-3.46/src/network/model/address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/model/address.h	2026-02-16 16:08:49.799230600 -0800
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: GPL-2.0-only
  *
  * Author: Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
+ * Modified by: Dhiraj Lokesh <dhirajlokesh@gmail.com>
  */
 
 #ifndef ADDRESS_H
@@ -14,8 +15,11 @@
 #include "ns3/attribute-helper.h"
 #include "ns3/attribute.h"
 
+#include <array>
+#include <compare>
 #include <ostream>
 #include <stdint.h>
+#include <unordered_map>
 
 namespace ns3
 {
@@ -26,6 +30,7 @@
  *
  * Network Address abstractions, including MAC, IPv4 and IPv6.
  */
+
 /**
  * @ingroup address
  * @brief a polymophic address class
@@ -62,6 +67,7 @@
  * {
  *   return Address (GetType (), m_buffer, 2);
  * }
+ *
  * MyAddress MyAddress::ConvertFrom (const Address &address)
  * {
  *   MyAddress ad;
@@ -69,9 +75,10 @@
  *   address.CopyTo (ad.m_buffer, 2);
  *   return ad;
  * }
+ *
  * uint8_t MyAddress::GetType ()
  * {
- *   static uint8_t type = Address::Register ();
+ *   static uint8_t type = Address::Register ("AddressType", 2);
  *   return type;
  * }
  * @endcode
@@ -84,10 +91,64 @@
  * operator Address() const;
  * @endcode
  *
+ * Furthermore, the specific address must call the static function
+ * ``Address::Register(kind, length)``. This is typically done in a static function like
+ * ``GetType``.
+ *
+ * The kind/length pair is used to set the address type when this is not known a-priori.
+ * The typical use-case is when you decode a header where the address kind is known, but its
+ * length is specified in the header itself. The concrete example is the ARP or NDP headers,
+ * where you have a MAC address with a variable length.
+ * In these cases the code will be (assuming the address is a MAC address):
+ * @code
+ * Address addr;
+ * addr.SetType("MacAddress", addressLen);
+ * ReadFrom(i, addr, addressLen);
+ * @endcode
+ *
+ * It is forbidden to have two specific addresses sharing the same kind and length.
+ *
  * @see attribute_Address
  */
 class Address
 {
+  private:
+    /**
+     * Key for the address registry: kind (string) / type (uint8_t)
+     */
+    using KindType = std::pair<std::string, uint8_t>;
+
+    /**
+     * Structure necessary to hash KindType, used as the key for the m_typeRegistry map
+     */
+    struct KeyHash
+    {
+        /**
+         * Functional operator for (string, uint8_t) hash computation.
+         * @param k the key to hash
+         * @return The key hash
+         */
+        std::size_t operator()(const KindType& k) const
+        {
+            return std::hash<std::string>()(k.first) ^ (std::hash<uint8_t>()(k.second));
+        }
+    };
+
+    /**
+     * Type of the address registry.
+     */
+    using KindTypeRegistry = std::unordered_map<KindType, uint8_t, Address::KeyHash>;
+
+    /**
+     * Container of allocated address types.
+     *
+     * The data is ordered by KindType (a {kind, length} pair), and sores the address type.
+     */
+    static KindTypeRegistry m_typeRegistry;
+
+    /// Unassigned Address type is reserved. Defined for clarity.
+    static constexpr uint8_t UNASSIGNED_TYPE{0};
+
   public:
     /**
      * The maximum size of a byte buffer which
@@ -98,7 +159,7 @@
     /**
      * Create an invalid address
      */
-    Address();
+    Address() = default;
     /**
      * @brief Create an address from a type and a buffer.
      *
@@ -114,19 +175,17 @@
      */
     Address(uint8_t type, const uint8_t* buffer, uint8_t len);
     /**
-     * @brief Create an address from another address.
-     * @param address the address to copy
-     */
-    Address(const Address& address);
-    /**
-     * @brief Basic assignment operator.
-     * @param address the address to copy
-     * @returns the address
+     * Set the address type.
+     *
+     * Works only if the type is not yet set.
+     * @see Register()
+     *
+     * @param kind address kind
+     * @param length address length
      */
-    Address& operator=(const Address& address);
-
+    void SetType(const std::string& kind, uint8_t length);
     /**
-     * @returns true if this address is invalid, false otherwise.
+     * @return true if this address is invalid, false otherwise.
      *
      * An address is invalid if and only if it was created
      * through the default constructor and it was never
@@ -135,19 +194,19 @@
     bool IsInvalid() const;
     /**
      * @brief Get the length of the underlying address.
-     * @returns the length of the underlying address.
+     * @return the length of the underlying address.
      */
     uint8_t GetLength() const;
     /**
      * @brief Copy the address bytes into a buffer.
      * @param buffer buffer to copy the address bytes to.
-     * @returns the number of bytes copied.
+     * @return the number of bytes copied.
      */
     uint32_t CopyTo(uint8_t buffer[MAX_SIZE]) const;
     /**
      * @param buffer buffer to copy the whole address data structure to
      * @param len the size of the buffer
-     * @returns the number of bytes copied.
+     * @return the number of bytes copied.
      *
      * Copies the type to buffer[0], the length of the address internal buffer
      * to buffer[1] and copies the internal buffer starting at buffer[2].  len
@@ -160,7 +219,7 @@
      *        a serialized representation of the address in network
      *        byte order.
      * @param len length of buffer
-     * @returns the number of bytes copied.
+     * @return the number of bytes copied.
      *
      * Copy the address bytes from buffer into to the internal buffer of this
      * address instance.
@@ -170,7 +229,7 @@
      * @param buffer pointer to a buffer of bytes which contain
      *        a copy of all the members of this Address class.
      * @param len the length of the buffer
-     * @returns the number of bytes copied.
+     * @return the number of bytes copied.
      *
      * The inverse of CopyAllTo().
      *
@@ -181,13 +240,13 @@
      * @param type a type id as returned by Address::Register
      * @param len the length associated to this type id.
      *
-     * @returns true if the type of the address stored internally
+     * @return true if the type of the address stored internally
      * is compatible with the requested type, false otherwise.
      */
     bool CheckCompatible(uint8_t type, uint8_t len) const;
     /**
      * @param type a type id as returned by Address::Register
-     * @returns true if the type of the address stored internally
+     * @return true if the type of the address stored internally
      * is compatible with the requested type, false otherwise.
      *
      * This method checks that the types are _exactly_ equal.
@@ -198,14 +257,28 @@
     bool IsMatchingType(uint8_t type) const;
     /**
      * Allocate a new type id for a new type of address.
-     * @returns a new type id.
+     *
+     * Each address-like class that needs to be converted to/from an
+     * ``Address`` needs to register itself once. This is typically done
+     * in the ``GetType`` function.
+     *
+     * The address kind and length are typically used during the buffer
+     * deserialization, where the exact address type is unknown, and only its
+     * kind and length are known, e.g., if you parse an ARP reply.
+     *
+     * It is not allowed to have two different addresses with the same
+     * kind and length.
+     *
+     * @param kind address kind, such as "MacAddress"
+     * @param length address length
+     * @return a new type id.
      */
-    static uint8_t Register();
+    static uint8_t Register(const std::string& kind, uint8_t length);
     /**
      * Get the number of bytes needed to serialize the underlying Address
      * Typically, this is GetLength () + 2
      *
-     * @returns the number of bytes required for an Address in serialized form
+     * @return the number of bytes required for an Address in serialized form
      */
     uint32_t GetSerializedSize() const;
     /**
@@ -221,40 +294,21 @@
      */
     void Deserialize(TagBuffer buffer);
 
-  private:
     /**
-     * @brief Equal to operator.
+     * @brief Three-way comparison operator.
      *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are equal
+     * @param other the other address to compare with
+     * @return comparison result
      */
-    friend bool operator==(const Address& a, const Address& b);
-
-    /**
-     * @brief Not equal to operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are not equal
-     */
-    friend bool operator!=(const Address& a, const Address& b);
-
-    /**
-     * @brief Less than operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operand a is less than operand b
-     */
-    friend bool operator<(const Address& a, const Address& b);
+    constexpr std::strong_ordering operator<=>(const Address& other) const = default;
 
+  private:
     /**
      * @brief Stream insertion operator.
      *
      * @param os the stream
      * @param address the address
-     * @returns a reference to the stream
+     * @return a reference to the stream
      */
     friend std::ostream& operator<<(std::ostream& os, const Address& address);
 
@@ -263,20 +317,17 @@
      *
      * @param is the stream
      * @param address the address
-     * @returns a reference to the stream
+     * @return a reference to the stream
      */
     friend std::istream& operator>>(std::istream& is, Address& address);
 
-    uint8_t m_type;           //!< Type of the address
-    uint8_t m_len;            //!< Length of the address
-    uint8_t m_data[MAX_SIZE]; //!< The address value
+    uint8_t m_type{UNASSIGNED_TYPE};        //!< Type of the address
+    uint8_t m_len{0};                       //!< Length of the address
+    std::array<uint8_t, MAX_SIZE> m_data{}; //!< The address value
 };
 
 ATTRIBUTE_HELPER_HEADER(Address);
 
-bool operator==(const Address& a, const Address& b);
-bool operator!=(const Address& a, const Address& b);
-bool operator<(const Address& a, const Address& b);
 std::ostream& operator<<(std::ostream& os, const Address& address);
 std::istream& operator>>(std::istream& is, Address& address);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/model/buffer.h ns-3.47/src/network/model/buffer.h
--- ns-3.46/src/network/model/buffer.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/model/buffer.h	2026-02-16 16:08:49.799230600 -0800
@@ -112,6 +112,8 @@
          * This method works only if the two iterators point
          * to the same underlying buffer. Debug builds ensure
          * this with an assert.
+         *
+         * @hidecaller
          */
         uint32_t GetDistanceFrom(const Iterator& o) const;
 
@@ -131,6 +133,8 @@
          *
          * Write the data in buffer and advance the iterator position
          * by one byte.
+         *
+         * @hidecaller
          */
         inline void WriteU8(uint8_t data);
         /**
@@ -139,6 +143,8 @@
          *
          * Write the data in buffer len times and advance the iterator position
          * by len byte.
+         *
+         * @hidecaller
          */
         inline void WriteU8(uint8_t data, uint32_t len);
         /**
@@ -201,6 +207,8 @@
          * Write the data in buffer and advance the iterator position
          * by two bytes. The data is written in network order and the
          * input data is expected to be in host order.
+         *
+         * @hidecaller
          */
         inline void WriteHtonU16(uint16_t data);
         /**
@@ -252,6 +260,8 @@
          *
          * Read data and advance the Iterator by the number of bytes
          * read.
+         *
+         * @hidecaller
          */
         inline uint8_t ReadU8();
         /**
@@ -284,6 +294,8 @@
          * Read data and advance the Iterator by the number of bytes
          * read.
          * The data is read in network format and returned in host format.
+         *
+         * @hidecaller
          */
         inline uint16_t ReadNtohU16();
         /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/model/header.h ns-3.47/src/network/model/header.h
--- ns-3.46/src/network/model/header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/model/header.h	2026-02-16 16:08:49.800230700 -0800
@@ -28,6 +28,9 @@
  *
  * Sample code which shows how to create a new type of Header, and how to use it,
  * is shown in the sample file samples/main-packet-header.cc
+ *
+ * Inheritance graph was not generated because of its size.
+ * @hideinheritancegraph
  */
 class Header : public Chunk
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/model/socket.h ns-3.47/src/network/model/socket.h
--- ns-3.46/src/network/model/socket.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/model/socket.h	2026-02-16 16:08:49.801230700 -0800
@@ -142,6 +142,7 @@
      *
      * @param node The node on which to create the socket
      * @param tid The TypeId of a SocketFactory class to use
+     * @hidecaller
      */
     static Ptr<Socket> CreateSocket(Ptr<Node> node, TypeId tid);
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/test/address-test.cc ns-3.47/src/network/test/address-test.cc
--- ns-3.46/src/network/test/address-test.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/network/test/address-test.cc	2026-02-16 16:08:49.801230700 -0800
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2025 Universita' di Firenze, Italy
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Tommaso Pecorella <tommaso.pecorella@unifi.it>
+ */
+
+#include "ns3/address.h"
+#include "ns3/test.h"
+
+#include <map>
+#include <string>
+
+using namespace ns3;
+
+/**
+ * @ingroup network-test
+ * @ingroup tests
+ *
+ * @brief Address classes Test
+ */
+class AddressTest : public TestCase
+{
+  public:
+    void DoRun() override;
+    AddressTest();
+};
+
+AddressTest::AddressTest()
+    : TestCase("Address class implementation")
+{
+}
+
+void
+AddressTest::DoRun()
+{
+    Address addrA;
+    Address addrB;
+    Address addrC;
+
+    // tests comparison of non-initialized addresses
+    NS_TEST_EXPECT_MSG_EQ(addrA == addrB, true, "Non-initialized addresses must be equal");
+
+    uint8_t bufferA[10]{1, 8, 0, 0, 0, 0, 0, 0, 0, 1};
+    uint8_t bufferB[10]{1, 8, 0, 0, 0, 0, 0, 0, 0, 2};
+    uint8_t bufferC[10]{1, 8, 0, 0, 0, 0, 0, 0, 0, 3};
+
+    addrA.CopyAllFrom(bufferA, 10);
+    addrB.CopyAllFrom(bufferB, 10);
+    addrC.CopyAllFrom(bufferC, 10);
+
+    // tests comparison of initialized addresses
+    NS_TEST_EXPECT_MSG_EQ(addrA < addrB, true, "Initialized addresses must be different, A < B");
+    NS_TEST_EXPECT_MSG_EQ(addrA < addrC, true, "Initialized addresses must be different, A < C");
+    NS_TEST_EXPECT_MSG_EQ(addrB < addrC, true, "Initialized addresses must be different, B < C");
+
+    std::map<Address, int8_t> theMap;
+    theMap.insert({addrC, 2});
+    theMap.insert({addrB, 1});
+    theMap.insert({addrA, 0});
+
+    int8_t test = -1;
+    for (auto [k, v] : theMap)
+    {
+        NS_TEST_EXPECT_MSG_EQ(v == test + 1, true, "Address map sorting must be correct");
+        test = v;
+    }
+}
+
+/**
+ * @ingroup network-test
+ * @ingroup tests
+ *
+ * @brief Address TestSuite
+ */
+class AddressTestSuite : public TestSuite
+{
+  public:
+    AddressTestSuite();
+
+  private:
+};
+
+AddressTestSuite::AddressTestSuite()
+    : TestSuite("address", Type::UNIT)
+{
+    AddTestCase(new AddressTest(), TestCase::Duration::QUICK);
+}
+
+static AddressTestSuite g_addressTestSuite; //!< Static variable for test initialization
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/test/ipv6-address-test-suite.cc ns-3.47/src/network/test/ipv6-address-test-suite.cc
--- ns-3.46/src/network/test/ipv6-address-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/test/ipv6-address-test-suite.cc	2026-02-16 16:08:49.802230600 -0800
@@ -35,7 +35,7 @@
 void
 Ipv6AddressTestCase::DoRun()
 {
-    Ipv6Address ip = Ipv6Address("2001:db8::1");
+    auto ip = Ipv6Address("2001:db8::1");
     uint8_t ipBytes[16];
     ip.Serialize(ipBytes);
     NS_TEST_ASSERT_MSG_EQ(ipBytes[0], 0x20, "Failed string conversion");
@@ -81,6 +81,11 @@
     NS_TEST_ASSERT_MSG_EQ(ipBytes[7], 0x01, "Failed string conversion");
     NS_TEST_ASSERT_MSG_EQ(ipBytes[15], 1, "Failed string conversion");
 
+    // This test checks that we're not comparing the optional member variable.
+    Ipv6Address addr = ip;
+    ip.GetHash();
+    NS_TEST_ASSERT_MSG_EQ(ip, addr, "Failed address comparison");
+
     // Please add more tests below
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/data-rate.cc ns-3.47/src/network/utils/data-rate.cc
--- ns-3.46/src/network/utils/data-rate.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/data-rate.cc	2026-02-16 16:08:49.803230800 -0800
@@ -180,42 +180,6 @@
     return *this;
 }
 
-bool
-DataRate::operator<(const DataRate& rhs) const
-{
-    return m_bps < rhs.m_bps;
-}
-
-bool
-DataRate::operator<=(const DataRate& rhs) const
-{
-    return m_bps <= rhs.m_bps;
-}
-
-bool
-DataRate::operator>(const DataRate& rhs) const
-{
-    return m_bps > rhs.m_bps;
-}
-
-bool
-DataRate::operator>=(const DataRate& rhs) const
-{
-    return m_bps >= rhs.m_bps;
-}
-
-bool
-DataRate::operator==(const DataRate& rhs) const
-{
-    return m_bps == rhs.m_bps;
-}
-
-bool
-DataRate::operator!=(const DataRate& rhs) const
-{
-    return m_bps != rhs.m_bps;
-}
-
 Time
 DataRate::CalculateBytesTxTime(uint32_t bytes) const
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/data-rate.h ns-3.47/src/network/utils/data-rate.h
--- ns-3.46/src/network/utils/data-rate.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/data-rate.h	2026-02-16 16:08:49.803230800 -0800
@@ -176,46 +176,13 @@
     DataRate& operator*=(uint64_t rhs);
 
     /**
-     * @return true if this rate is less than rhs
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
      *
      * @param rhs the datarate to compare to this datarate
+     * @returns The result of the comparison.
      */
-    bool operator<(const DataRate& rhs) const;
-
-    /**
-     * @return true if this rate is less than or equal to rhs
-     *
-     * @param rhs the datarate to compare to this datarate
-     */
-    bool operator<=(const DataRate& rhs) const;
-
-    /**
-     * @return true if this rate is greater than rhs
-     *
-     * @param rhs the datarate to compare to this datarate
-     */
-    bool operator>(const DataRate& rhs) const;
-
-    /**
-     * @return true if this rate is greater than or equal to rhs
-     *
-     * @param rhs the datarate to compare to this datarate
-     */
-    bool operator>=(const DataRate& rhs) const;
-
-    /**
-     * @return true if this rate is equal to rhs
-     *
-     * @param rhs the datarate to compare to this datarate
-     */
-    bool operator==(const DataRate& rhs) const;
-
-    /**
-     * @return true if this rate is not equal to rhs
-     *
-     * @param rhs the datarate to compare to this datarate
-     */
-    bool operator!=(const DataRate& rhs) const;
+    auto operator<=>(const DataRate& rhs) const = default;
 
     /**
      * @brief Calculate transmission time
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/inet6-socket-address.cc ns-3.47/src/network/utils/inet6-socket-address.cc
--- ns-3.46/src/network/utils/inet6-socket-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/inet6-socket-address.cc	2026-02-16 16:08:49.803230800 -0800
@@ -119,7 +119,7 @@
 Inet6SocketAddress::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("IpAddress", 18);
     return type;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/inet-socket-address.cc ns-3.47/src/network/utils/inet-socket-address.cc
--- ns-3.46/src/network/utils/inet-socket-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/inet-socket-address.cc	2026-02-16 16:08:49.803230800 -0800
@@ -120,7 +120,7 @@
 InetSocketAddress::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("IpAddress", 6);
     return type;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/inet-socket-address.h ns-3.47/src/network/utils/inet-socket-address.h
--- ns-3.46/src/network/utils/inet-socket-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/inet-socket-address.h	2026-02-16 16:08:49.803230800 -0800
@@ -94,6 +94,7 @@
      *
      * @param address the Address instance to convert from.
      * @returns an InetSocketAddress
+     * @hidecaller
      */
     static InetSocketAddress ConvertFrom(const Address& address);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/ipv4-address.cc ns-3.47/src/network/utils/ipv4-address.cc
--- ns-3.46/src/network/utils/ipv4-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/ipv4-address.cc	2026-02-16 16:08:49.803230800 -0800
@@ -25,12 +25,6 @@
 
 NS_LOG_COMPONENT_DEFINE("Ipv4Address");
 
-Ipv4Mask::Ipv4Mask()
-    : m_mask(0x66666666)
-{
-    NS_LOG_FUNCTION(this);
-}
-
 Ipv4Mask::Ipv4Mask(uint32_t mask)
     : m_mask(mask)
 {
@@ -137,24 +131,10 @@
     return tmp;
 }
 
-/**
- *  Value of a not-yet-initialized IPv4 address, corresponding to 102.102.102.102.
- *  This is totally arbitrary.
- */
-static constexpr uint32_t UNINITIALIZED = 0x66666666U;
-
-Ipv4Address::Ipv4Address()
-    : m_address(UNINITIALIZED),
-      m_initialized(false)
-{
-    NS_LOG_FUNCTION(this);
-}
-
 Ipv4Address::Ipv4Address(uint32_t address)
 {
     NS_LOG_FUNCTION(this << address);
     m_address = address;
-    m_initialized = true;
 }
 
 Ipv4Address::Ipv4Address(const char* address)
@@ -163,15 +143,27 @@
 
     if (inet_pton(AF_INET, address, &m_address) <= 0)
     {
-        NS_LOG_LOGIC("Error, can not build an IPv4 address from an invalid string: " << address);
-        m_address = 0;
-        m_initialized = false;
+        NS_ABORT_MSG("Error, can not build an IPv4 address from an invalid string: " << address);
         return;
     }
-    m_initialized = true;
     m_address = ntohl(m_address);
 }
 
+bool
+Ipv4Address::CheckCompatible(const std::string& addressStr)
+{
+    NS_LOG_FUNCTION(addressStr);
+
+    uint32_t buffer;
+
+    if (inet_pton(AF_INET, addressStr.c_str(), &buffer) <= 0)
+    {
+        NS_LOG_WARN("Error, can not build an IPv4 address from an invalid string: " << addressStr);
+        return false;
+    }
+    return true;
+}
+
 uint32_t
 Ipv4Address::Get() const
 {
@@ -184,7 +176,6 @@
 {
     NS_LOG_FUNCTION(this << address);
     m_address = address;
-    m_initialized = true;
 }
 
 void
@@ -193,12 +184,9 @@
     NS_LOG_FUNCTION(this << address);
     if (inet_pton(AF_INET, address, &m_address) <= 0)
     {
-        NS_LOG_LOGIC("Error, can not build an IPv4 address from an invalid string: " << address);
-        m_address = 0;
-        m_initialized = false;
+        NS_ABORT_MSG("Error, can not build an IPv4 address from an invalid string: " << address);
         return;
     }
-    m_initialized = true;
     m_address = ntohl(m_address);
 }
 
@@ -238,7 +226,7 @@
 Ipv4Address::IsInitialized() const
 {
     NS_LOG_FUNCTION(this);
-    return m_initialized;
+    return true;
 }
 
 bool
@@ -312,7 +300,6 @@
     ipv4.m_address |= buf[2];
     ipv4.m_address <<= 8;
     ipv4.m_address |= buf[3];
-    ipv4.m_initialized = true;
 
     return ipv4;
 }
@@ -361,7 +348,7 @@
 Ipv4Address::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("IpAddress", 4);
     return type;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/ipv4-address.h ns-3.47/src/network/utils/ipv4-address.h
--- ns-3.46/src/network/utils/ipv4-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/ipv4-address.h	2026-02-16 16:08:49.803230800 -0800
@@ -11,7 +11,9 @@
 
 #include "ns3/address.h"
 #include "ns3/attribute-helper.h"
+#include "ns3/deprecated.h"
 
+#include <compare>
 #include <ostream>
 #include <stdint.h>
 
@@ -30,7 +32,7 @@
 class Ipv4Address
 {
   public:
-    Ipv4Address();
+    Ipv4Address() = default;
     /**
      * input address is in host order.
      * @param address The host order 32-bit address
@@ -43,9 +45,28 @@
      * \c hhh.xxx.xxx.lll
      * where \c h is the high byte and \c l the
      * low byte
+     *
      * @param address C-string containing the address as described above
      */
     Ipv4Address(const char* address);
+
+    /**
+     * @brief Checks if the string contains an Ipv4Address
+     *
+     * Input address is in format:
+     * \c hhh.xxx.xxx.lll
+     * where \c h is the high byte and \c l the
+     * low byte
+     *
+     * Note: the function uses ``inet_pton`` internally.
+     *
+     * @see Address::CheckCompatible hich has a similar name but which
+     * instead checks the underlying type and length embedded in the Address.
+     *
+     * @param addressStr string containing the address as described above
+     * @return true if the string can be parsed as an IPv4 address
+     */
+    static bool CheckCompatible(const std::string& addressStr);
     /**
      * Get the host-order 32-bit IP address
      * @return the host-order 32-bit IP address
@@ -91,6 +112,7 @@
     /**
      * @return true if address is initialized (i.e., set to something), false otherwise
      */
+    NS_DEPRECATED_3_47("Use IsAny or std::optional")
     bool IsInitialized() const;
     /**
      * @return true if address is 0.0.0.0; false otherwise
@@ -190,6 +212,7 @@
     static Ipv4Address GetZero();
     /**
      * @return the 0.0.0.0 address
+     * @hidecaller
      */
     static Ipv4Address GetAny();
     /**
@@ -201,42 +224,22 @@
      */
     static Ipv4Address GetLoopback();
 
-  private:
-    /**
-     * @brief Get the underlying address type (automatically assigned).
-     *
-     * @returns the address type
-     */
-    static uint8_t GetType();
-    uint32_t m_address; //!< IPv4 address
-    bool m_initialized; //!< IPv4 address has been explicitly initialized to a valid value.
-
-    /**
-     * @brief Equal to operator.
-     *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the operands are equal.
-     */
-    friend bool operator==(const Ipv4Address& a, const Ipv4Address& b);
-
     /**
-     * @brief Not equal to operator.
+     * @brief Three-way comparison operator.
      *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the operands are not equal.
+     * @param other the other address to compare with
+     * @returns comparison result
      */
-    friend bool operator!=(const Ipv4Address& a, const Ipv4Address& b);
+    constexpr std::strong_ordering operator<=>(const Ipv4Address& other) const = default;
 
+  private:
     /**
-     * @brief Less than to operator.
+     * @brief Get the underlying address type (automatically assigned).
      *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the first operand is less than the second.
+     * @returns the address type
      */
-    friend bool operator<(const Ipv4Address& a, const Ipv4Address& b);
+    static uint8_t GetType();
+    uint32_t m_address{0}; //!< IPv4 address
 };
 
 /**
@@ -254,9 +257,9 @@
 {
   public:
     /**
-     * Will initialize to a garbage value (0x66666666)
+     * Will initialize to a zero-length mask, which will match any address.
      */
-    Ipv4Mask();
+    Ipv4Mask() = default;
     /**
      * @param mask bitwise integer representation of the mask
      *
@@ -314,25 +317,15 @@
     static Ipv4Mask GetOnes();
 
     /**
-     * @brief Equal to operator.
-     *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the operands are equal.
-     */
-    friend bool operator==(const Ipv4Mask& a, const Ipv4Mask& b);
-
-    /**
-     * @brief Not equal to operator.
+     * @brief Three-way comparison operator.
      *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the operands are not equal.
+     * @param a the other mask to compare with
+     * @returns comparison result
      */
-    friend bool operator!=(const Ipv4Mask& a, const Ipv4Mask& b);
+    constexpr std::strong_ordering operator<=>(const Ipv4Mask& a) const = default;
 
   private:
-    uint32_t m_mask; //!< IP mask
+    uint32_t m_mask{0}; //!< IP mask
 };
 
 ATTRIBUTE_HELPER_HEADER(Ipv4Address);
@@ -371,30 +364,12 @@
  */
 std::istream& operator>>(std::istream& is, Ipv4Mask& mask);
 
-inline bool
-operator==(const Ipv4Address& a, const Ipv4Address& b)
-{
-    return a.m_address == b.m_address;
-}
-
-inline bool
-operator!=(const Ipv4Address& a, const Ipv4Address& b)
-{
-    return a.m_address != b.m_address;
-}
-
-inline bool
-operator<(const Ipv4Address& a, const Ipv4Address& b)
-{
-    return a.m_address < b.m_address;
-}
-
 /**
  * @ingroup address
  *
  * @brief Class providing an hash for IPv4 addresses
  */
-class Ipv4AddressHash
+class NS_DEPRECATED_3_47("Unnecessary thanks to std::hash specialization, remove") Ipv4AddressHash
 {
   public:
     /**
@@ -408,18 +383,27 @@
     size_t operator()(const Ipv4Address& x) const;
 };
 
-inline bool
-operator==(const Ipv4Mask& a, const Ipv4Mask& b)
-{
-    return a.m_mask == b.m_mask;
-}
+} // namespace ns3
 
-inline bool
-operator!=(const Ipv4Mask& a, const Ipv4Mask& b)
+namespace std
 {
-    return a.m_mask != b.m_mask;
-}
 
-} // namespace ns3
+/**
+ * @brief Hash function class for IPv4 addresses.
+ */
+template <>
+struct hash<ns3::Ipv4Address>
+{
+    /**
+     * @brief Returns the hash of an IPv4 address.
+     * @param addr IPv4 address to hash
+     * @returns the hash of the address
+     */
+    size_t operator()(const ns3::Ipv4Address& addr) const
+    {
+        return std::hash<uint32_t>()(addr.Get());
+    }
+};
+} // namespace std
 
 #endif /* IPV4_ADDRESS_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/ipv6-address.cc ns-3.47/src/network/utils/ipv6-address.cc
--- ns-3.46/src/network/utils/ipv6-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/ipv6-address.cc	2026-02-16 16:08:49.804230700 -0800
@@ -15,6 +15,7 @@
 #include "ns3/assert.h"
 #include "ns3/log.h"
 
+#include <algorithm>
 #include <iomanip>
 #include <memory>
 
@@ -30,197 +31,67 @@
 
 NS_LOG_COMPONENT_DEFINE("Ipv6Address");
 
-#ifdef __cplusplus
-extern "C"
-{ /* } */
-#endif
-    /**
-     * @brief Mix hash keys in-place for lookuphash
-     *
-     * @param a first word of the hash key
-     * @param b second word of the hash key
-     * @param c third word of the hash key
-     */
-    void mixHashKey(uint32_t& a, uint32_t& b, uint32_t& c)
-    {
-        (a) -= (b);
-        (a) -= (c);
-        (a) ^= ((c) >> 13);
-        (b) -= (c);
-        (b) -= (a);
-        (b) ^= ((a) << 8);
-        (c) -= (a);
-        (c) -= (b);
-        (c) ^= ((b) >> 13);
-        (a) -= (b);
-        (a) -= (c);
-        (a) ^= ((c) >> 12);
-        (b) -= (c);
-        (b) -= (a);
-        (b) ^= ((a) << 16);
-        (c) -= (a);
-        (c) -= (b);
-        (c) ^= ((b) >> 5);
-        (a) -= (b);
-        (a) -= (c);
-        (a) ^= ((c) >> 3);
-        (b) -= (c);
-        (b) -= (a);
-        (b) ^= ((a) << 10);
-        (c) -= (a);
-        (c) -= (b);
-        (c) ^= ((b) >> 15);
-    }
-
-    /**
-     * @brief Get a hash key.
-     * @param k the key
-     * @param length the length of the key
-     * @param level the previous hash, or an arbitrary value
-     * @return hash
-     * @note Adapted from Jens Jakobsen implementation (chillispot).
-     */
-    static uint32_t lookuphash(unsigned char* k, uint32_t length, uint32_t level)
-    {
-        NS_LOG_FUNCTION(k << length << level);
-
-        typedef uint32_t ub4; /* unsigned 4-byte quantities */
-        uint32_t a = 0;
-        uint32_t b = 0;
-        uint32_t c = 0;
-        uint32_t len = 0;
-
-        /* Set up the internal state */
-        len = length;
-        a = b = 0x9e3779b9; /* the golden ratio; an arbitrary value */
-        c = level;          /* the previous hash value */
-
-        /* handle most of the key */
-        while (len >= 12)
-        {
-            a += (k[0] + ((ub4)k[1] << 8) + ((ub4)k[2] << 16) + ((ub4)k[3] << 24));
-            b += (k[4] + ((ub4)k[5] << 8) + ((ub4)k[6] << 16) + ((ub4)k[7] << 24));
-            c += (k[8] + ((ub4)k[9] << 8) + ((ub4)k[10] << 16) + ((ub4)k[11] << 24));
-            mixHashKey(a, b, c);
-            k += 12;
-            len -= 12;
-        }
-
-        /* handle the last 11 bytes */
-        c += length;
-        switch (len) /* all the case statements fall through */
-        {
-        case 11:
-            c += ((ub4)k[10] << 24);
-        case 10:
-            c += ((ub4)k[9] << 16);
-        case 9:
-            c += ((ub4)k[8] << 8); /* the first byte of c is reserved for the length */
-        case 8:
-            b += ((ub4)k[7] << 24);
-        case 7:
-            b += ((ub4)k[6] << 16);
-        case 6:
-            b += ((ub4)k[5] << 8);
-        case 5:
-            b += k[4];
-        case 4:
-            a += ((ub4)k[3] << 24);
-        case 3:
-            a += ((ub4)k[2] << 16);
-        case 2:
-            a += ((ub4)k[1] << 8);
-        case 1:
-            a += k[0];
-            /* case 0: nothing left to add */
-        }
-        mixHashKey(a, b, c);
-
-        /* report the result */
-        return c;
-    }
-
-#ifdef __cplusplus
-}
-#endif
-
-Ipv6Address::Ipv6Address()
+Ipv6Address::Ipv6Address(const char* address)
 {
-    NS_LOG_FUNCTION(this);
-    memset(m_address, 0x00, 16);
-    m_initialized = false;
-}
+    NS_LOG_FUNCTION(this << address);
 
-Ipv6Address::Ipv6Address(const Ipv6Address& addr)
-{
-    // Do not add function logging here, to avoid stack overflow
-    memcpy(m_address, addr.m_address, 16);
-    m_initialized = true;
+    if (inet_pton(AF_INET6, address, m_address.data()) <= 0)
+    {
+        m_address.fill(0x00);
+        NS_ABORT_MSG("Error, can not build an IPv6 address from an invalid string: " << address);
+        return;
+    }
+    m_hash.reset();
 }
 
-Ipv6Address::Ipv6Address(const Ipv6Address* addr)
+bool
+Ipv6Address::CheckCompatible(const std::string& addressStr)
 {
-    // Do not add function logging here, to avoid stack overflow
-    memcpy(m_address, addr->m_address, 16);
-    m_initialized = true;
-}
+    NS_LOG_FUNCTION(addressStr);
 
-Ipv6Address::Ipv6Address(const char* address)
-{
-    NS_LOG_FUNCTION(this << address);
+    uint8_t buffer[16];
 
-    if (inet_pton(AF_INET6, address, m_address) <= 0)
+    if (inet_pton(AF_INET6, addressStr.c_str(), &buffer) <= 0)
     {
-        memset(m_address, 0x00, 16);
-        NS_LOG_LOGIC("Error, can not build an IPv6 address from an invalid string: " << address);
-        m_initialized = false;
-        return;
+        NS_LOG_WARN("Error, can not build an IPv6 address from an invalid string: " << addressStr);
+        return false;
     }
-    m_initialized = true;
+    return true;
 }
 
 Ipv6Address::Ipv6Address(uint8_t address[16])
 {
     NS_LOG_FUNCTION(this << &address);
-    /* 128 bit => 16 bytes */
-    memcpy(m_address, address, 16);
-    m_initialized = true;
-}
-
-Ipv6Address::~Ipv6Address()
-{
-    /* do nothing */
-    NS_LOG_FUNCTION(this);
+    std::copy(address, address + 16, m_address.begin());
+    m_hash.reset();
 }
 
 void
 Ipv6Address::Set(const char* address)
 {
     NS_LOG_FUNCTION(this << address);
-    if (inet_pton(AF_INET6, address, m_address) <= 0)
+    if (inet_pton(AF_INET6, address, m_address.data()) <= 0)
     {
-        memset(m_address, 0x00, 16);
-        NS_LOG_LOGIC("Error, can not build an IPv6 address from an invalid string: " << address);
-        m_initialized = false;
+        m_address.fill(0x00);
+        NS_ABORT_MSG("Error, can not set an IPv6 address from an invalid string: " << address);
         return;
     }
-    m_initialized = true;
+    m_hash.reset();
 }
 
 void
 Ipv6Address::Set(uint8_t address[16])
 {
-    /* 128 bit => 16 bytes */
     NS_LOG_FUNCTION(this << &address);
-    memcpy(m_address, address, 16);
-    m_initialized = true;
+    std::copy(address, address + 16, m_address.begin());
+    m_hash.reset();
 }
 
 void
 Ipv6Address::Serialize(uint8_t buf[16]) const
 {
     NS_LOG_FUNCTION(this << &buf);
-    memcpy(buf, m_address, 16);
+    std::copy(m_address.begin(), m_address.end(), buf);
 }
 
 Ipv6Address
@@ -228,7 +99,6 @@
 {
     NS_LOG_FUNCTION(&buf);
     Ipv6Address ipv6((uint8_t*)buf);
-    ipv6.m_initialized = true;
     return ipv6;
 }
 
@@ -236,24 +106,11 @@
 Ipv6Address::MakeIpv4MappedAddress(Ipv4Address addr)
 {
     NS_LOG_FUNCTION(addr);
-    uint8_t buf[16] = {
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-        0xff,
-        0xff,
-        0x00,
-        0x00,
-        0x00,
-        0x00,
-    };
+    uint8_t buf[16] = {0};
+
+    buf[10] = 0xff;
+    buf[11] = 0xff;
+
     addr.Serialize(&buf[12]);
     return Ipv6Address(buf);
 }
@@ -529,7 +386,7 @@
 
     char str[INET6_ADDRSTRLEN];
 
-    if (inet_ntop(AF_INET6, m_address, str, INET6_ADDRSTRLEN))
+    if (inet_ntop(AF_INET6, m_address.data(), str, INET6_ADDRSTRLEN))
     {
         os << str;
     }
@@ -561,9 +418,9 @@
 Ipv6Address::IsIpv4MappedAddress() const
 {
     NS_LOG_FUNCTION(this);
-    static uint8_t v4MappedPrefix[12] =
+    static std::array<uint8_t, 12> v4MappedPrefix =
         {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff};
-    return memcmp(m_address, v4MappedPrefix, sizeof(v4MappedPrefix)) == 0;
+    return std::equal(v4MappedPrefix.begin(), v4MappedPrefix.end(), m_address.begin());
 }
 
 Ipv6Address
@@ -575,10 +432,9 @@
     uint8_t pref[16];
     unsigned int i = 0;
 
-    memcpy(addr, m_address, 16);
+    std::copy(m_address.begin(), m_address.end(), addr);
     ((Ipv6Prefix)prefix).GetBytes(pref);
 
-    /* a little bit ugly... */
     for (i = 0; i < 16; i++)
     {
         addr[i] = addr[i] & pref[i];
@@ -681,7 +537,7 @@
 Ipv6Address::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("IpAddress", 16);
     return type;
 }
 
@@ -745,7 +601,7 @@
 Ipv6Address::GetBytes(uint8_t buf[16]) const
 {
     NS_LOG_FUNCTION(this << &buf);
-    memcpy(buf, m_address, 16);
+    std::copy(m_address.begin(), m_address.end(), buf);
 }
 
 bool
@@ -760,7 +616,68 @@
 Ipv6Address::IsInitialized() const
 {
     NS_LOG_FUNCTION(this);
-    return m_initialized;
+    return true;
+}
+
+void
+Ipv6Address::MixHashKey(uint32_t& a, uint32_t& b, uint32_t& c)
+{
+    auto mix = [](uint32_t x, uint32_t y, uint32_t z, int shift) -> uint32_t {
+        x -= y + z;
+        x ^= (shift > 0) ? (z << shift) : (z >> (-shift));
+        return x;
+    };
+
+    a = mix(a, b, c, -13);
+    b = mix(b, c, a, 8);
+    c = mix(c, a, b, -13);
+    a = mix(a, b, c, -12);
+    b = mix(b, c, a, 16);
+    c = mix(c, a, b, -5);
+    a = mix(a, b, c, -3);
+    b = mix(b, c, a, 10);
+    c = mix(c, a, b, -15);
+}
+
+uint32_t
+Ipv6Address::GenerateHash() const
+{
+    NS_LOG_FUNCTION(this);
+
+    uint32_t a = 0x9e3779b9; /* the golden ratio; an arbitrary value */
+    uint32_t b = 0x9e3779b9; /* the golden ratio; an arbitrary value */
+    uint32_t c = 0;
+
+    auto fold = [](const uint8_t* a) -> uint32_t {
+        auto f = static_cast<uint32_t>(a[0]);
+        f += static_cast<uint32_t>(a[1]) << 8;
+        f += static_cast<uint32_t>(a[2]) << 16;
+        f += static_cast<uint32_t>(a[3]) << 24;
+        return f;
+    };
+
+    a += fold(&m_address[0]);
+    b += fold(&m_address[4]);
+    c += fold(&m_address[8]);
+    MixHashKey(a, b, c);
+
+    c += 16;
+    a += fold(&m_address[12]);
+    MixHashKey(a, b, c);
+
+    /* set the hash and report the result */
+    m_hash = c;
+    return c;
+}
+
+uint32_t
+Ipv6Address::GetHash() const
+{
+    if (m_hash)
+    {
+        return m_hash.value();
+    }
+    return GenerateHash();
 }
 
 std::ostream&
@@ -779,17 +696,10 @@
     return is;
 }
 
-Ipv6Prefix::Ipv6Prefix()
-{
-    NS_LOG_FUNCTION(this);
-    memset(m_prefix, 0x00, 16);
-    m_prefixLength = 64;
-}
-
 Ipv6Prefix::Ipv6Prefix(const char* prefix)
 {
     NS_LOG_FUNCTION(this << prefix);
-    if (inet_pton(AF_INET6, prefix, m_prefix) <= 0)
+    if (inet_pton(AF_INET6, prefix, m_prefix.data()) <= 0)
     {
         NS_ABORT_MSG("Error, can not build an IPv6 prefix from an invalid string: " << prefix);
     }
@@ -799,14 +709,14 @@
 Ipv6Prefix::Ipv6Prefix(uint8_t prefix[16])
 {
     NS_LOG_FUNCTION(this << &prefix);
-    memcpy(m_prefix, prefix, 16);
+    std::copy(prefix, prefix + 16, m_prefix.begin());
     m_prefixLength = GetMinimumPrefixLength();
 }
 
 Ipv6Prefix::Ipv6Prefix(const char* prefix, uint8_t prefixLength)
 {
     NS_LOG_FUNCTION(this << prefix);
-    if (inet_pton(AF_INET6, prefix, m_prefix) <= 0)
+    if (inet_pton(AF_INET6, prefix, m_prefix.data()) <= 0)
     {
         NS_ABORT_MSG("Error, can not build an IPv6 prefix from an invalid string: " << prefix);
     }
@@ -821,7 +731,7 @@
 Ipv6Prefix::Ipv6Prefix(uint8_t prefix[16], uint8_t prefixLength)
 {
     NS_LOG_FUNCTION(this << &prefix);
-    memcpy(m_prefix, prefix, 16);
+    std::copy(prefix, prefix + 16, m_prefix.begin());
 
     uint8_t autoLength = GetMinimumPrefixLength();
     NS_ASSERT_MSG(autoLength <= prefixLength,
@@ -838,7 +748,7 @@
     unsigned int mod = 0;
     unsigned int i = 0;
 
-    memset(m_prefix, 0x00, 16);
+    m_prefix.fill(0x00);
     m_prefixLength = prefix;
 
     NS_ASSERT(prefix <= 128);
@@ -850,7 +760,7 @@
     // __warn_memset_zero_len compiler errors in some gcc>4.5.x
     if (nb > 0)
     {
-        memset(m_prefix, 0xff, nb);
+        memset(m_prefix.data(), 0xff, nb);
     }
     if (mod)
     {
@@ -867,24 +777,6 @@
     }
 }
 
-Ipv6Prefix::Ipv6Prefix(const Ipv6Prefix& prefix)
-{
-    memcpy(m_prefix, prefix.m_prefix, 16);
-    m_prefixLength = prefix.m_prefixLength;
-}
-
-Ipv6Prefix::Ipv6Prefix(const Ipv6Prefix* prefix)
-{
-    memcpy(m_prefix, prefix->m_prefix, 16);
-    m_prefixLength = prefix->m_prefixLength;
-}
-
-Ipv6Prefix::~Ipv6Prefix()
-{
-    /* do nothing */
-    NS_LOG_FUNCTION(this);
-}
-
 bool
 Ipv6Prefix::IsMatch(Ipv6Address a, Ipv6Address b) const
 {
@@ -943,16 +835,16 @@
 Ipv6Prefix::GetBytes(uint8_t buf[16]) const
 {
     NS_LOG_FUNCTION(this << &buf);
-    memcpy(buf, m_prefix, 16);
+    memcpy(buf, m_prefix.data(), 16);
 }
 
 Ipv6Address
 Ipv6Prefix::ConvertToIpv6Address() const
 {
     uint8_t prefixBytes[16];
-    memcpy(prefixBytes, m_prefix, 16);
+    memcpy(prefixBytes, m_prefix.data(), 16);
 
-    Ipv6Address convertedPrefix = Ipv6Address(prefixBytes);
+    auto convertedPrefix = Ipv6Address(prefixBytes);
     return convertedPrefix;
 }
 
@@ -1018,11 +910,7 @@
 size_t
 Ipv6AddressHash::operator()(const Ipv6Address& x) const
 {
-    uint8_t buf[16];
-
-    x.GetBytes(buf);
-
-    return lookuphash(buf, sizeof(buf), 0);
+    return x.GetHash();
 }
 
 ATTRIBUTE_HELPER_CPP(Ipv6Address);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/ipv6-address.h ns-3.47/src/network/utils/ipv6-address.h
--- ns-3.46/src/network/utils/ipv6-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/ipv6-address.h	2026-02-16 16:08:49.804230700 -0800
@@ -14,8 +14,12 @@
 
 #include "ns3/address.h"
 #include "ns3/attribute-helper.h"
+#include "ns3/deprecated.h"
 
+#include <array>
+#include <compare>
 #include <cstring>
+#include <optional>
 #include <ostream>
 #include <stdint.h>
 
@@ -40,15 +44,31 @@
     /**
      * @brief Default constructor.
      */
-    Ipv6Address();
+    Ipv6Address() = default;
 
     /**
      * @brief Constructs an Ipv6Address by parsing the input C-string.
-     * @param address the C-string containing the IPv6 address (e.g. 2001:660:4701::1).
+     * @param address the C-string containing the IPv6 address (e.g. 2001:db8:f00d:cafe::1).
      */
     Ipv6Address(const char* address);
 
     /**
+     * @brief Checks if the string contains an Ipv6Address
+     *
+     * Input address is in format:
+     * \c 2001:db8:f00d:cafe::1
+     *
+     * Note: the function uses ``inet_pton`` internally.
+     *
+     * @see Address::CheckCompatible hich has a similar name but which
+     * instead checks the underlying type and length embedded in the Address.
+     *
+     * @param addressStr string containing the address as described above
+     * @return true if the string can be parsed as an IPv6 address
+     */
+    static bool CheckCompatible(const std::string& addressStr);
+
+    /**
      * @brief Constructs an Ipv6Address by using the input 16 bytes.
      * @param address the 128-bit address
      * @warning the parameter must point on a 16 bytes integer array!
@@ -56,25 +76,8 @@
     Ipv6Address(uint8_t address[16]);
 
     /**
-     * @brief Copy constructor.
-     * @param addr Ipv6Address object
-     */
-    Ipv6Address(const Ipv6Address& addr);
-
-    /**
-     * @brief Copy constructor.
-     * @param addr Ipv6Address pointer
-     */
-    Ipv6Address(const Ipv6Address* addr);
-
-    /**
-     * @brief Destructor.
-     */
-    ~Ipv6Address();
-
-    /**
      * @brief Sets an Ipv6Address by parsing the input C-string.
-     * @param address the C-string containing the IPv6 address (e.g. 2001:660:4701::1).
+     * @param address the C-string containing the IPv6 address (e.g. 2001:db8:f00d:cafe::1).
      */
     void Set(const char* address);
 
@@ -342,6 +345,7 @@
     /**
      * @return true if address is initialized (i.e., set to something), false otherwise
      */
+    NS_DEPRECATED_3_47("Use IsAny or std::optional")
     bool IsInitialized() const;
 
     /**
@@ -392,45 +396,66 @@
      */
     void GetBytes(uint8_t buf[16]) const;
 
-  private:
     /**
-     * @brief Return the Type of address.
-     * @return type of address
+     * @brief Get the address hash.
+     * @return the address hash
      */
-    static uint8_t GetType();
+    uint32_t GetHash() const;
 
     /**
-     * @brief The address representation on 128 bits (16 bytes).
+     * @brief Three-way comparison operator.
+     *
+     * @param other the other address to compare with
+     * @returns comparison result
      */
-    uint8_t m_address[16];
-    bool m_initialized; //!< IPv6 address has been explicitly initialized to a valid value.
+    inline constexpr std::strong_ordering operator<=>(const Ipv6Address& other) const
+    {
+        return m_address <=> other.m_address;
+    }
 
     /**
      * @brief Equal to operator.
      *
-     * @param a the first operand.
-     * @param b the first operand.
+     * @param other the other address to compare with
      * @returns true if the operands are equal.
      */
-    friend bool operator==(const Ipv6Address& a, const Ipv6Address& b);
+    inline constexpr bool operator==(const Ipv6Address& other) const
+    {
+        return m_address == other.m_address;
+    }
 
+  private:
     /**
-     * @brief Not equal to operator.
+     * @brief Mix hash keys in-place for lookuphash
      *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the operands are not equal.
+     * @param a first word of the hash key
+     * @param b second word of the hash key
+     * @param c third word of the hash key
      */
-    friend bool operator!=(const Ipv6Address& a, const Ipv6Address& b);
+    static void MixHashKey(uint32_t& a, uint32_t& b, uint32_t& c);
 
     /**
-     * @brief Less than to operator.
-     *
-     * @param a the first operand.
-     * @param b the first operand.
-     * @returns true if the first operand is less than the second.
+     * @brief Generate and return a hash key.
+     * @return hash
+     * @note Adapted from Jens Jakobsen implementation (chillispot).
      */
-    friend bool operator<(const Ipv6Address& a, const Ipv6Address& b);
+    uint32_t GenerateHash() const;
+
+    /**
+     * @brief Return the Type of address.
+     * @return type of address
+     */
+    static uint8_t GetType();
+
+    /**
+     * @brief The address representation on 128 bits (16 bytes).
+     */
+    std::array<uint8_t, 16> m_address{};
+
+    /**
+     * @brief address hash.
+     */
+    mutable std::optional<uint32_t> m_hash;
 };
 
 /**
@@ -445,14 +470,16 @@
   public:
     /**
      * @brief Default constructor.
+     *
+     * The default prefix is empty, corresponding to /0
      */
-    Ipv6Prefix();
+    Ipv6Prefix() = default;
 
     /**
      * @brief Constructs an Ipv6Prefix by using the input 16 bytes.
      *
      * The prefix length is calculated as the minimum prefix length, i.e.,
-     * 2001:db8:cafe:: will have a 47 bit prefix length.
+     * 2001:db8:cafe:: will have a 47 bit prefix length (0xE is 0b1110).
      *
      * @param prefix the 128-bit prefix
      */
@@ -490,27 +517,17 @@
     Ipv6Prefix(uint8_t prefix);
 
     /**
-     * @brief Copy constructor.
-     * @param prefix Ipv6Prefix object
-     */
-    Ipv6Prefix(const Ipv6Prefix& prefix);
-
-    /**
-     * @brief Copy constructor.
-     * @param prefix Ipv6Prefix pointer
-     */
-    Ipv6Prefix(const Ipv6Prefix* prefix);
-
-    /**
-     * @brief Destructor.
-     */
-    ~Ipv6Prefix();
-
-    /**
-     * @brief If the Address match the type.
-     * @param a a first address
-     * @param b a second address
-     * @return true if the type match, false otherwise
+     * @brief Check whether two addresses have the same bits in the prefix
+     * portion of their addresses.
+     *
+     * If the prefix length is 0, this method will return true regardless
+     * of the two address argument values.  If the prefix length is 128,
+     * this method will require that the two address arguments are the same.
+     *
+     * @param a first address to compare
+     * @param b second address to compare
+     * @return true if both addresses are equal in their masked bits,
+     * corresponding to the prefix length.
      */
     bool IsMatch(Ipv6Address a, Ipv6Address b) const;
 
@@ -571,34 +588,24 @@
      */
     static Ipv6Prefix GetZero();
 
-  private:
     /**
-     * @brief The prefix representation.
-     */
-    uint8_t m_prefix[16];
-
-    /**
-     * @brief The prefix length.
+     * @brief Three-way comparison operator.
+     *
+     * @param other the other prefix to compare with
+     * @returns comparison result
      */
-    uint8_t m_prefixLength;
+    constexpr std::strong_ordering operator<=>(const Ipv6Prefix& other) const = default;
 
+  private:
     /**
-     * @brief Equal to operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are equal
+     * @brief The prefix representation.
      */
-    friend bool operator==(const Ipv6Prefix& a, const Ipv6Prefix& b);
+    std::array<uint8_t, 16> m_prefix{};
 
     /**
-     * @brief Not equal to operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are not equal
+     * @brief The prefix length.
      */
-    friend bool operator!=(const Ipv6Prefix& a, const Ipv6Prefix& b);
+    uint8_t m_prefixLength{0};
 };
 
 ATTRIBUTE_HELPER_HEADER(Ipv6Address);
@@ -640,41 +647,11 @@
  */
 std::istream& operator>>(std::istream& is, Ipv6Prefix& prefix);
 
-inline bool
-operator==(const Ipv6Address& a, const Ipv6Address& b)
-{
-    return !std::memcmp(a.m_address, b.m_address, 16);
-}
-
-inline bool
-operator!=(const Ipv6Address& a, const Ipv6Address& b)
-{
-    return std::memcmp(a.m_address, b.m_address, 16);
-}
-
-inline bool
-operator<(const Ipv6Address& a, const Ipv6Address& b)
-{
-    return std::memcmp(a.m_address, b.m_address, 16) < 0;
-}
-
-inline bool
-operator==(const Ipv6Prefix& a, const Ipv6Prefix& b)
-{
-    return !std::memcmp(a.m_prefix, b.m_prefix, 16);
-}
-
-inline bool
-operator!=(const Ipv6Prefix& a, const Ipv6Prefix& b)
-{
-    return std::memcmp(a.m_prefix, b.m_prefix, 16);
-}
-
 /**
  * @class Ipv6AddressHash
  * @brief Hash function class for IPv6 addresses.
  */
-class Ipv6AddressHash
+class NS_DEPRECATED_3_47("Unnecessary thanks to std::hash specialization, remove") Ipv6AddressHash
 {
   public:
     /**
@@ -687,4 +664,25 @@
 
 } /* namespace ns3 */
 
+namespace std
+{
+
+/**
+ * @brief Hash function class for IPv6 addresses.
+ */
+template <>
+struct hash<ns3::Ipv6Address>
+{
+    /**
+     * @brief Returns the hash of an IPv6 address.
+     * @param addr IPv6 address to hash
+     * @returns the hash of the address
+     */
+    size_t operator()(const ns3::Ipv6Address& addr) const
+    {
+        return addr.GetHash();
+    }
+};
+} // namespace std
+
 #endif /* IPV6_ADDRESS_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/lollipop-counter.h ns-3.47/src/network/utils/lollipop-counter.h
--- ns-3.46/src/network/utils/lollipop-counter.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/lollipop-counter.h	2026-02-16 16:08:49.804230700 -0800
@@ -11,6 +11,7 @@
 
 #include "ns3/abort.h"
 
+#include <compare>
 #include <limits>
 
 namespace ns3
@@ -180,7 +181,7 @@
      * @param [in] rhs Right hand argument
      * @return The result of the operator.
      */
-    friend bool operator==(const LollipopCounter& lhs, const LollipopCounter& rhs)
+    friend constexpr bool operator==(const LollipopCounter& lhs, const LollipopCounter& rhs)
     {
         NS_ABORT_MSG_IF(lhs.m_sequenceWindow != rhs.m_sequenceWindow,
                         "Can not compare two Lollipop Counters with different sequence windows");
@@ -189,19 +190,25 @@
     }
 
     /**
-     * Arithmetic operator greater-than
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
+     *
+     * Note that the comparison is a ``std::partial_ordering``, as two
+     * lollipop counters could be not comparable, @see IsComparable() for details.
+     *
      * @param [in] lhs Left hand argument
      * @param [in] rhs Right hand argument
-     * @return The result of the operator.
+     * @returns The result of the comparison.
      */
-    friend bool operator>(const LollipopCounter& lhs, const LollipopCounter& rhs)
+    friend constexpr std::partial_ordering operator<=>(const LollipopCounter& lhs,
+                                                       const LollipopCounter& rhs)
     {
         NS_ABORT_MSG_IF(lhs.m_sequenceWindow != rhs.m_sequenceWindow,
                         "Can not compare two Lollipop Counters with different sequence windows");
 
         if (lhs.m_value == rhs.m_value)
         {
-            return false;
+            return std::partial_ordering::equivalent;
         }
 
         if ((lhs.m_value <= m_circularRegion && rhs.m_value <= m_circularRegion) ||
@@ -213,14 +220,17 @@
             if (absDiff > lhs.m_sequenceWindow)
             {
                 // They are desynchronized - comparison is impossible.
-                // return false because we can not return anything else.
-                return false;
+                return std::partial_ordering::unordered;
             }
 
             // They are synchronized - comparison according to RFC1982.
             T serialRegion = ((m_circularRegion >> 1) + 1);
-            return (((lhs.m_value < rhs.m_value) && ((rhs.m_value - lhs.m_value) > serialRegion)) ||
-                    ((lhs.m_value > rhs.m_value) && ((lhs.m_value - rhs.m_value) < serialRegion)));
+            if (((lhs.m_value < rhs.m_value) && ((rhs.m_value - lhs.m_value) > serialRegion)) ||
+                ((lhs.m_value > rhs.m_value) && ((lhs.m_value - rhs.m_value) < serialRegion)))
+            {
+                return std::partial_ordering::greater;
+            }
+            return std::partial_ordering::less;
         }
 
         // One counter is in the "high" region and the other is in the in the "lower" region
@@ -240,40 +250,18 @@
             difference = rhs.m_value - lhs.m_value;
         }
 
-        T distance = (m_maxValue - difference) +
-                     1; // this is guaranteed to be positive and between [1...m_lollipopMaxValue].
-        if (distance > lhs.m_sequenceWindow)
-        {
-            return lhsIsHigher;
-        }
-        else
-        {
-            return !lhsIsHigher;
-        }
-
-        // this should never be reached.
-        return false;
-    }
-
-    /**
-     * Arithmetic operator less-than
-     * @param [in] lhs Left hand argument
-     * @param [in] rhs Right hand argument
-     * @return The result of the operator.
-     */
-    friend bool operator<(const LollipopCounter& lhs, const LollipopCounter& rhs)
-    {
-        if (!lhs.IsComparable(rhs))
+        // this is guaranteed to be positive and between [1...m_lollipopMaxValue].
+        T distance = (m_maxValue - difference) + 1;
+        if (distance <= lhs.m_sequenceWindow)
         {
-            return false;
+            lhsIsHigher = !lhsIsHigher;
         }
 
-        if (lhs > rhs || lhs == rhs)
+        if (lhsIsHigher)
         {
-            return false;
+            return std::partial_ordering::greater;
         }
-
-        return true;
+        return std::partial_ordering::less;
     }
 
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac16-address.cc ns-3.47/src/network/utils/mac16-address.cc
--- ns-3.46/src/network/utils/mac16-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac16-address.cc	2026-02-16 16:08:49.804230700 -0800
@@ -51,14 +51,14 @@
 Mac16Address::CopyFrom(const uint8_t buffer[2])
 {
     NS_LOG_FUNCTION(this << &buffer);
-    memcpy(m_address, buffer, 2);
+    std::copy(buffer, buffer + 2, m_address.begin());
 }
 
 void
 Mac16Address::CopyTo(uint8_t buffer[2]) const
 {
     NS_LOG_FUNCTION(this << &buffer);
-    memcpy(buffer, m_address, 2);
+    std::copy(m_address.begin(), m_address.end(), buffer);
 }
 
 bool
@@ -80,7 +80,7 @@
     NS_LOG_FUNCTION(address);
     NS_ASSERT(address.CheckCompatible(GetType(), 2));
     Mac16Address retval;
-    address.CopyTo(retval.m_address);
+    address.CopyTo(retval.m_address.data());
     return retval;
 }
 
@@ -88,7 +88,7 @@
 Mac16Address::ConvertTo() const
 {
     NS_LOG_FUNCTION(this);
-    return Address(GetType(), m_address, 2);
+    return Address(GetType(), m_address.data(), 2);
 }
 
 uint16_t
@@ -129,7 +129,7 @@
 {
     NS_LOG_FUNCTION_NOARGS();
 
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("MacAddress", 2);
     return type;
 }
 
@@ -180,19 +180,16 @@
 std::ostream&
 operator<<(std::ostream& os, const Mac16Address& address)
 {
-    uint8_t ad[2];
-    address.CopyTo(ad);
-
+    std::ios_base::fmtflags ff = os.flags();
     os.setf(std::ios::hex, std::ios::basefield);
-    os.fill('0');
-    for (uint8_t i = 0; i < 1; i++)
-    {
-        os << std::setw(2) << (uint32_t)ad[i] << ":";
-    }
-    // Final byte not suffixed by ":"
-    os << std::setw(2) << (uint32_t)ad[1];
-    os.setf(std::ios::dec, std::ios::basefield);
-    os.fill(' ');
+    auto fill = os.fill('0');
+
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[0]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[1]);
+
+    os.flags(ff); // Restore stream flags
+    os.fill(fill);
+
     return os;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac16-address.h ns-3.47/src/network/utils/mac16-address.h
--- ns-3.46/src/network/utils/mac16-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac16-address.h	2026-02-16 16:08:49.804230700 -0800
@@ -14,6 +14,8 @@
 #include "ns3/attribute-helper.h"
 #include "ns3/attribute.h"
 
+#include <array>
+#include <compare>
 #include <ostream>
 #include <stdint.h>
 
@@ -165,39 +167,21 @@
      */
     bool IsMulticast() const;
 
-  private:
-    /**
-     * @brief Return the Type of address.
-     * @return type of address
-     */
-    static uint8_t GetType();
-
-    /**
-     * @brief Equal to operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are equal
-     */
-    friend bool operator==(const Mac16Address& a, const Mac16Address& b);
-
     /**
-     * @brief Not equal to operator.
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
      *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are not equal
+     * @param other address to compare to this one
+     * @returns The result of the comparison.
      */
-    friend bool operator!=(const Mac16Address& a, const Mac16Address& b);
+    constexpr std::strong_ordering operator<=>(const Mac16Address& other) const = default;
 
+  private:
     /**
-     * @brief Less than operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operand a is less than operand b
+     * @brief Return the Type of address.
+     * @return type of address
      */
-    friend bool operator<(const Mac16Address& a, const Mac16Address& b);
+    static uint8_t GetType();
 
     /**
      * @brief Stream insertion operator.
@@ -217,30 +201,12 @@
      */
     friend std::istream& operator>>(std::istream& is, Mac16Address& address);
 
-    static uint64_t m_allocationIndex; //!< Address allocation index
-    uint8_t m_address[2]{0};           //!< Address value
+    static uint64_t m_allocationIndex;  //!< Address allocation index
+    std::array<uint8_t, 2> m_address{}; //!< Address value
 };
 
 ATTRIBUTE_HELPER_HEADER(Mac16Address);
 
-inline bool
-operator==(const Mac16Address& a, const Mac16Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 2) == 0;
-}
-
-inline bool
-operator!=(const Mac16Address& a, const Mac16Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 2) != 0;
-}
-
-inline bool
-operator<(const Mac16Address& a, const Mac16Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 2) < 0;
-}
-
 std::ostream& operator<<(std::ostream& os, const Mac16Address& address);
 std::istream& operator>>(std::istream& is, Mac16Address& address);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac48-address.cc ns-3.47/src/network/utils/mac48-address.cc
--- ns-3.46/src/network/utils/mac48-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac48-address.cc	2026-02-16 16:08:49.804230700 -0800
@@ -51,14 +51,14 @@
 Mac48Address::CopyFrom(const uint8_t buffer[6])
 {
     NS_LOG_FUNCTION(this << &buffer);
-    std::memcpy(m_address, buffer, 6);
+    std::copy(buffer, buffer + 6, m_address.begin());
 }
 
 void
 Mac48Address::CopyTo(uint8_t buffer[6]) const
 {
     NS_LOG_FUNCTION(this << &buffer);
-    std::memcpy(buffer, m_address, 6);
+    std::copy(m_address.begin(), m_address.end(), buffer);
 }
 
 bool
@@ -78,7 +78,7 @@
 Mac48Address::ConvertTo() const
 {
     NS_LOG_FUNCTION(this);
-    return Address(GetType(), m_address, 6);
+    return Address(GetType(), m_address.data(), 6);
 }
 
 Mac48Address
@@ -87,7 +87,7 @@
     NS_LOG_FUNCTION(&address);
     NS_ASSERT(address.CheckCompatible(GetType(), 6));
     Mac48Address retval;
-    address.CopyTo(retval.m_address);
+    address.CopyTo(retval.m_address.data());
     return retval;
 }
 
@@ -123,7 +123,7 @@
 Mac48Address::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("MacAddress", 6);
     return type;
 }
 
@@ -229,19 +229,20 @@
 std::ostream&
 operator<<(std::ostream& os, const Mac48Address& address)
 {
-    uint8_t ad[6];
-    address.CopyTo(ad);
-
+    std::ios_base::fmtflags ff = os.flags();
     os.setf(std::ios::hex, std::ios::basefield);
-    os.fill('0');
-    for (uint8_t i = 0; i < 5; i++)
-    {
-        os << std::setw(2) << (uint32_t)ad[i] << ":";
-    }
-    // Final byte not suffixed by ":"
-    os << std::setw(2) << (uint32_t)ad[5];
-    os.setf(std::ios::dec, std::ios::basefield);
-    os.fill(' ');
+    auto fill = os.fill('0');
+
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[0]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[1]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[2]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[3]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[4]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[5]);
+
+    os.flags(ff); // Restore stream flags
+    os.fill(fill);
+
     return os;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac48-address.h ns-3.47/src/network/utils/mac48-address.h
--- ns-3.46/src/network/utils/mac48-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac48-address.h	2026-02-16 16:08:49.804230700 -0800
@@ -14,6 +14,8 @@
 #include "ns3/attribute-helper.h"
 #include "ns3/attribute.h"
 
+#include <array>
+#include <compare>
 #include <ostream>
 #include <stdint.h>
 
@@ -68,6 +70,8 @@
      * This function performs a type check and asserts if the
      * type of the input address is not compatible with an
      * Mac48Address.
+     *
+     * @hidecaller
      */
     static Mac48Address ConvertFrom(const Address& address);
     /**
@@ -115,6 +119,7 @@
 
     /**
      * @returns the broadcast address
+     * @hidecaller
      */
     static Mac48Address GetBroadcast();
 
@@ -149,39 +154,21 @@
      */
     typedef void (*TracedCallback)(Mac48Address value);
 
-  private:
-    /**
-     * @brief Return the Type of address.
-     * @return type of address
-     */
-    static uint8_t GetType();
-
-    /**
-     * @brief Equal to operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are equal
-     */
-    friend bool operator==(const Mac48Address& a, const Mac48Address& b);
-
     /**
-     * @brief Not equal to operator.
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
      *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are not equal
+     * @param other address to compare to this one
+     * @returns The result of the comparison.
      */
-    friend bool operator!=(const Mac48Address& a, const Mac48Address& b);
+    constexpr std::strong_ordering operator<=>(const Mac48Address& other) const = default;
 
+  private:
     /**
-     * @brief Less than operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operand a is less than operand b
+     * @brief Return the Type of address.
+     * @return type of address
      */
-    friend bool operator<(const Mac48Address& a, const Mac48Address& b);
+    static uint8_t GetType();
 
     /**
      * @brief Stream insertion operator.
@@ -201,30 +188,12 @@
      */
     friend std::istream& operator>>(std::istream& is, Mac48Address& address);
 
-    static uint64_t m_allocationIndex; //!< Address allocation index
-    uint8_t m_address[6]{0};           //!< Address value
+    static uint64_t m_allocationIndex;  //!< Address allocation index
+    std::array<uint8_t, 6> m_address{}; //!< Address value
 };
 
 ATTRIBUTE_HELPER_HEADER(Mac48Address);
 
-inline bool
-operator==(const Mac48Address& a, const Mac48Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 6) == 0;
-}
-
-inline bool
-operator!=(const Mac48Address& a, const Mac48Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 6) != 0;
-}
-
-inline bool
-operator<(const Mac48Address& a, const Mac48Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 6) < 0;
-}
-
 std::ostream& operator<<(std::ostream& os, const Mac48Address& address);
 std::istream& operator>>(std::istream& is, Mac48Address& address);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac64-address.cc ns-3.47/src/network/utils/mac64-address.cc
--- ns-3.46/src/network/utils/mac64-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac64-address.cc	2026-02-16 16:08:49.804230700 -0800
@@ -66,14 +66,14 @@
 Mac64Address::CopyFrom(const uint8_t buffer[8])
 {
     NS_LOG_FUNCTION(this << &buffer);
-    std::memcpy(m_address, buffer, 8);
+    std::copy(buffer, buffer + 8, m_address.begin());
 }
 
 void
 Mac64Address::CopyTo(uint8_t buffer[8]) const
 {
     NS_LOG_FUNCTION(this << &buffer);
-    std::memcpy(buffer, m_address, 8);
+    std::copy(m_address.begin(), m_address.end(), buffer);
 }
 
 bool
@@ -95,7 +95,7 @@
     NS_LOG_FUNCTION(address);
     NS_ASSERT(address.CheckCompatible(GetType(), 8));
     Mac64Address retval;
-    address.CopyTo(retval.m_address);
+    address.CopyTo(retval.m_address.data());
     return retval;
 }
 
@@ -103,7 +103,7 @@
 Mac64Address::ConvertTo() const
 {
     NS_LOG_FUNCTION(this);
-    return Address(GetType(), m_address, 8);
+    return Address(GetType(), m_address.data(), 8);
 }
 
 uint64_t
@@ -157,26 +157,29 @@
 Mac64Address::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("MacAddress", 8);
     return type;
 }
 
 std::ostream&
 operator<<(std::ostream& os, const Mac64Address& address)
 {
-    uint8_t ad[8];
-    address.CopyTo(ad);
-
+    std::ios_base::fmtflags ff = os.flags();
     os.setf(std::ios::hex, std::ios::basefield);
-    os.fill('0');
-    for (uint8_t i = 0; i < 7; i++)
-    {
-        os << std::setw(2) << (uint32_t)ad[i] << ":";
-    }
-    // Final byte not suffixed by ":"
-    os << std::setw(2) << (uint32_t)ad[7];
-    os.setf(std::ios::dec, std::ios::basefield);
-    os.fill(' ');
+    auto fill = os.fill('0');
+
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[0]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[1]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[2]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[3]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[4]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[5]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[6]) << ":";
+    os << std::setw(2) << static_cast<uint32_t>(address.m_address[7]);
+
+    os.flags(ff); // Restore stream flags
+    os.fill(fill);
+
     return os;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac64-address.h ns-3.47/src/network/utils/mac64-address.h
--- ns-3.46/src/network/utils/mac64-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac64-address.h	2026-02-16 16:08:49.804230700 -0800
@@ -14,6 +14,8 @@
 #include "ns3/attribute-helper.h"
 #include "ns3/attribute.h"
 
+#include <array>
+#include <compare>
 #include <ostream>
 #include <stdint.h>
 
@@ -116,39 +118,21 @@
      */
     static void ResetAllocationIndex();
 
-  private:
-    /**
-     * @brief Return the Type of address.
-     * @return type of address
-     */
-    static uint8_t GetType();
-
-    /**
-     * @brief Equal to operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are equal
-     */
-    friend bool operator==(const Mac64Address& a, const Mac64Address& b);
-
     /**
-     * @brief Not equal to operator.
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
      *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operands are not equal
+     * @param other address to compare to this one
+     * @returns The result of the comparison.
      */
-    friend bool operator!=(const Mac64Address& a, const Mac64Address& b);
+    constexpr std::strong_ordering operator<=>(const Mac64Address& other) const = default;
 
+  private:
     /**
-     * @brief Less than operator.
-     *
-     * @param a the first operand
-     * @param b the first operand
-     * @returns true if the operand a is less than operand b
+     * @brief Return the Type of address.
+     * @return type of address
      */
-    friend bool operator<(const Mac64Address& a, const Mac64Address& b);
+    static uint8_t GetType();
 
     /**
      * @brief Stream insertion operator.
@@ -168,8 +152,8 @@
      */
     friend std::istream& operator>>(std::istream& is, Mac64Address& address);
 
-    static uint64_t m_allocationIndex; //!< Address allocation index
-    uint8_t m_address[8]{0};           //!< Address value
+    static uint64_t m_allocationIndex;  //!< Address allocation index
+    std::array<uint8_t, 8> m_address{}; //!< Address value
 };
 
 /**
@@ -179,24 +163,6 @@
 
 ATTRIBUTE_HELPER_HEADER(Mac64Address);
 
-inline bool
-operator==(const Mac64Address& a, const Mac64Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 8) == 0;
-}
-
-inline bool
-operator!=(const Mac64Address& a, const Mac64Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 8) != 0;
-}
-
-inline bool
-operator<(const Mac64Address& a, const Mac64Address& b)
-{
-    return memcmp(a.m_address, b.m_address, 8) < 0;
-}
-
 std::ostream& operator<<(std::ostream& os, const Mac64Address& address);
 std::istream& operator>>(std::istream& is, Mac64Address& address);
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac8-address.cc ns-3.47/src/network/utils/mac8-address.cc
--- ns-3.46/src/network/utils/mac8-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac8-address.cc	2026-02-16 16:08:49.804230700 -0800
@@ -31,7 +31,7 @@
 uint8_t
 Mac8Address::GetType()
 {
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("MacAddress", 1);
     return type;
 }
 
@@ -107,28 +107,10 @@
     m_allocationIndex = 0;
 }
 
-bool
-operator<(const Mac8Address& a, const Mac8Address& b)
-{
-    return a.m_address < b.m_address;
-}
-
-bool
-operator==(const Mac8Address& a, const Mac8Address& b)
-{
-    return a.m_address == b.m_address;
-}
-
-bool
-operator!=(const Mac8Address& a, const Mac8Address& b)
-{
-    return !(a == b);
-}
-
 std::ostream&
 operator<<(std::ostream& os, const Mac8Address& address)
 {
-    os << (int)address.m_address;
+    os << static_cast<uint32_t>(address.m_address);
     return os;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/mac8-address.h ns-3.47/src/network/utils/mac8-address.h
--- ns-3.46/src/network/utils/mac8-address.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/mac8-address.h	2026-02-16 16:08:49.804230700 -0800
@@ -11,6 +11,7 @@
 
 #include "ns3/address.h"
 
+#include <compare>
 #include <iostream>
 
 namespace ns3
@@ -118,6 +119,15 @@
      */
     static void ResetAllocationIndex();
 
+    /**
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
+     *
+     * @param other address to compare to this one
+     * @returns The result of the comparison.
+     */
+    constexpr std::strong_ordering operator<=>(const Mac8Address& other) const = default;
+
   private:
     static uint8_t m_allocationIndex; //!< Address allocation index
     uint8_t m_address{255};           //!< The address.
@@ -129,41 +139,11 @@
      */
     static uint8_t GetType();
 
-    friend bool operator<(const Mac8Address& a, const Mac8Address& b);
-    friend bool operator==(const Mac8Address& a, const Mac8Address& b);
-    friend bool operator!=(const Mac8Address& a, const Mac8Address& b);
     friend std::ostream& operator<<(std::ostream& os, const Mac8Address& address);
     friend std::istream& operator>>(std::istream& is, Mac8Address& address);
 };
 
 /**
- * Address comparison, less than.
- *
- * @param a First address to compare.
- * @param b Second address to compare.
- * @return True if a < b.
- */
-bool operator<(const Mac8Address& a, const Mac8Address& b);
-
-/**
- * Address comparison, equality.
- *
- * @param a First address to compare.
- * @param b Second address to compare.
- * @return True if a == b.
- */
-bool operator==(const Mac8Address& a, const Mac8Address& b);
-
-/**
- * Address comparison, unequal.
- *
- * @param a First address to compare.
- * @param b Second address to compare.
- * @return True if a != b.
- */
-bool operator!=(const Mac8Address& a, const Mac8Address& b);
-
-/**
  * Write \pname{address} to stream \pname{os} as 8 bit integer.
  *
  * @param os The output stream.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/packetbb.cc ns-3.47/src/network/utils/packetbb.cc
--- ns-3.46/src/network/utils/packetbb.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/packetbb.cc	2026-02-16 16:08:49.805230600 -0800
@@ -281,12 +281,6 @@
     return true;
 }
 
-bool
-PbbTlvBlock::operator!=(const PbbTlvBlock& other) const
-{
-    return !(*this == other);
-}
-
 /* End PbbTlvBlock class */
 
 PbbAddressTlvBlock::PbbAddressTlvBlock()
@@ -520,12 +514,6 @@
     return true;
 }
 
-bool
-PbbAddressTlvBlock::operator!=(const PbbAddressTlvBlock& other) const
-{
-    return !(*this == other);
-}
-
 /* End PbbAddressTlvBlock Class */
 
 PbbPacket::PbbPacket()
@@ -1001,12 +989,6 @@
     return true;
 }
 
-bool
-PbbPacket::operator!=(const PbbPacket& other) const
-{
-    return !(*this == other);
-}
-
 /* End PbbPacket class */
 
 PbbMessage::PbbMessage()
@@ -1694,12 +1676,6 @@
     return true;
 }
 
-bool
-PbbMessage::operator!=(const PbbMessage& other) const
-{
-    return !(*this == other);
-}
-
 /* End PbbMessage Class */
 
 PbbMessageIpv4::PbbMessageIpv4()
@@ -2415,12 +2391,6 @@
     return m_addressTlvList == other.m_addressTlvList;
 }
 
-bool
-PbbAddressBlock::operator!=(const PbbAddressBlock& other) const
-{
-    return !(*this == other);
-}
-
 uint8_t
 PbbAddressBlock::GetPrefixFlags() const
 {
@@ -2972,12 +2942,6 @@
     return true;
 }
 
-bool
-PbbTlv::operator!=(const PbbTlv& other) const
-{
-    return !(*this == other);
-}
-
 /* End PbbTlv Class */
 
 void
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/packetbb.h ns-3.47/src/network/utils/packetbb.h
--- ns-3.46/src/network/utils/packetbb.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/packetbb.h	2026-02-16 16:08:49.805230600 -0800
@@ -189,12 +189,6 @@
      * @returns true if the blocks are equal
      */
     bool operator==(const PbbTlvBlock& other) const;
-    /**
-     * @brief Inequality operator for PbbTlvBlock
-     * @param other PbbTlvBlock to compare this one to
-     * @returns true if the blocks are not equal
-     */
-    bool operator!=(const PbbTlvBlock& other) const;
 
   private:
     std::list<Ptr<PbbTlv>> m_tlvList; //!< PbbTlv container
@@ -356,13 +350,6 @@
      */
     bool operator==(const PbbAddressTlvBlock& other) const;
 
-    /**
-     * @brief Inequality operator for PbbAddressTlvBlock
-     * @param other PbbAddressTlvBlock to compare to this one
-     * @returns true if PbbAddressTlvBlock are not equal
-     */
-    bool operator!=(const PbbAddressTlvBlock& other) const;
-
   private:
     std::list<Ptr<PbbAddressTlv>> m_tlvList; //!< PbbAddressTlv container
 };
@@ -666,14 +653,6 @@
      */
     bool operator==(const PbbPacket& other) const;
 
-    /**
-     * @brief Inequality operator for PbbPacket
-     * @param other PbbPacket to compare to this one
-     * @returns true if PbbPacket are not equal
-     */
-    bool operator!=(const PbbPacket& other) const;
-
-  protected:
   private:
     PbbTlvBlock m_tlvList;                    //!< PbbTlv container
     std::list<Ptr<PbbMessage>> m_messageList; //!< PbbTlvBlock container
@@ -1056,12 +1035,6 @@
      * @returns true if PbbMessages are equal
      */
     bool operator==(const PbbMessage& other) const;
-    /**
-     * @brief Inequality operator for PbbMessage
-     * @param other PbbMessage to compare to this one
-     * @returns true if PbbMessages are not equal
-     */
-    bool operator!=(const PbbMessage& other) const;
 
   protected:
     /**
@@ -1546,13 +1519,6 @@
      */
     bool operator==(const PbbAddressBlock& other) const;
 
-    /**
-     * @brief Inequality operator for PbbAddressBlock
-     * @param other PbbAddressBlock to compare to this one
-     * @returns true if PbbAddressBlock are not equal
-     */
-    bool operator!=(const PbbAddressBlock& other) const;
-
   protected:
     /**
      * @brief Returns address length
@@ -1778,13 +1744,6 @@
      */
     bool operator==(const PbbTlv& other) const;
 
-    /**
-     * @brief Inequality operator for PbbTlv
-     * @param other PbbTlv to compare to this one
-     * @returns true if PbbTlv are not equal
-     */
-    bool operator!=(const PbbTlv& other) const;
-
   protected:
     /**
      * @brief Set an index as starting point
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/packet-socket-address.cc ns-3.47/src/network/utils/packet-socket-address.cc
--- ns-3.46/src/network/utils/packet-socket-address.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/packet-socket-address.cc	2026-02-16 16:08:49.804230700 -0800
@@ -88,7 +88,6 @@
 PacketSocketAddress::ConvertTo() const
 {
     NS_LOG_FUNCTION(this);
-    Address address;
     uint8_t buffer[Address::MAX_SIZE];
     buffer[0] = m_protocol & 0xff;
     buffer[1] = (m_protocol >> 8) & 0xff;
@@ -145,7 +144,7 @@
 PacketSocketAddress::GetType()
 {
     NS_LOG_FUNCTION_NOARGS();
-    static uint8_t type = Address::Register();
+    static uint8_t type = Address::Register("PacketSocketAddress", Address::MAX_SIZE);
     return type;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/packet-socket-client.h ns-3.47/src/network/utils/packet-socket-client.h
--- ns-3.46/src/network/utils/packet-socket-client.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/packet-socket-client.h	2026-02-16 16:08:49.804230700 -0800
@@ -34,8 +34,8 @@
  * what concerns the underlying NetDevice and the Address scheme.
  * It is meant to be used in ns-3 tests.
  *
- * The application will send `MaxPackets' packets, one every `Interval'
- * time. Packet size (`PacketSize') can be configured.
+ * The application will send `MaxPackets` packets, one every `Interval`
+ * time. Packet size (`PacketSize`) can be configured.
  * Provides a "Tx" Traced Callback (transmitted packets, source address).
  *
  * Note: packets larger than the NetDevice MTU will not be sent.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/queue-size.cc ns-3.47/src/network/utils/queue-size.cc
--- ns-3.46/src/network/utils/queue-size.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/queue-size.cc	2026-02-16 16:08:49.805230600 -0800
@@ -113,54 +113,6 @@
     NS_LOG_FUNCTION(this << static_cast<uint16_t>(unit) << value);
 }
 
-bool
-QueueSize::operator<(const QueueSize& rhs) const
-{
-    NS_ABORT_MSG_IF(m_unit != rhs.GetUnit(), "Cannot compare heterogeneous sizes");
-
-    return m_value < rhs.m_value;
-}
-
-bool
-QueueSize::operator<=(const QueueSize& rhs) const
-{
-    NS_ABORT_MSG_IF(m_unit != rhs.GetUnit(), "Cannot compare heterogeneous sizes");
-
-    return m_value <= rhs.m_value;
-}
-
-bool
-QueueSize::operator>(const QueueSize& rhs) const
-{
-    NS_ABORT_MSG_IF(m_unit != rhs.GetUnit(), "Cannot compare heterogeneous sizes");
-
-    return m_value > rhs.m_value;
-}
-
-bool
-QueueSize::operator>=(const QueueSize& rhs) const
-{
-    NS_ABORT_MSG_IF(m_unit != rhs.GetUnit(), "Cannot compare heterogeneous sizes");
-
-    return m_value >= rhs.m_value;
-}
-
-bool
-QueueSize::operator==(const QueueSize& rhs) const
-{
-    NS_ABORT_MSG_IF(m_unit != rhs.GetUnit(), "Cannot compare heterogeneous sizes");
-
-    return m_value == rhs.m_value;
-}
-
-bool
-QueueSize::operator!=(const QueueSize& rhs) const
-{
-    NS_ABORT_MSG_IF(m_unit != rhs.GetUnit(), "Cannot compare heterogeneous sizes");
-
-    return m_value != rhs.m_value;
-}
-
 QueueSizeUnit
 QueueSize::GetUnit() const
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/queue-size.h ns-3.47/src/network/utils/queue-size.h
--- ns-3.46/src/network/utils/queue-size.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/queue-size.h	2026-02-16 16:08:49.805230600 -0800
@@ -113,46 +113,26 @@
     QueueSize(std::string size);
 
     /**
-     * @return true if this size is less than rhs
-     *
+     * @brief Equality operator.
      * @param rhs the queue size to compare to this queue size
+     * @returns true if this size is equal to rhs
      */
-    bool operator<(const QueueSize& rhs) const;
+    inline bool operator==(const QueueSize& rhs) const
+    {
+        NS_ABORT_MSG_IF(m_unit != rhs.m_unit, "Cannot compare heterogeneous sizes");
+        return m_value == rhs.m_value;
+    }
 
     /**
-     * @return true if this size is less than or equal to rhs
-     *
+     * @brief Three-way comparison operator.
      * @param rhs the queue size to compare to this queue size
+     * @returns The result of the comparison.
      */
-    bool operator<=(const QueueSize& rhs) const;
-
-    /**
-     * @return true if this size is greater than rhs
-     *
-     * @param rhs the queue size to compare to this queue size
-     */
-    bool operator>(const QueueSize& rhs) const;
-
-    /**
-     * @return true if this size is greater than or equal to rhs
-     *
-     * @param rhs the queue size to compare to this queue size
-     */
-    bool operator>=(const QueueSize& rhs) const;
-
-    /**
-     * @return true if this size is equal to rhs
-     *
-     * @param rhs the queue size to compare to this queue size
-     */
-    bool operator==(const QueueSize& rhs) const;
-
-    /**
-     * @return true if this size is not equal to rhs
-     *
-     * @param rhs the queue size to compare to this queue size
-     */
-    bool operator!=(const QueueSize& rhs) const;
+    inline std::strong_ordering operator<=>(const QueueSize& rhs) const
+    {
+        NS_ABORT_MSG_IF(m_unit != rhs.m_unit, "Cannot compare heterogeneous sizes");
+        return m_value <=> rhs.m_value;
+    }
 
     /**
      * Get the underlying unit
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/radiotap-header.cc ns-3.47/src/network/utils/radiotap-header.cc
--- ns-3.46/src/network/utils/radiotap-header.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/radiotap-header.cc	2026-02-16 16:08:49.806230800 -0800
@@ -11,22 +11,109 @@
 
 #include "ns3/log.h"
 
-#include <bit>
+#include <algorithm>
+#include <bitset>
 #include <cmath>
 #include <iomanip>
 
 namespace ns3
 {
 
+namespace
+{
+/// Number of bits per "present" field in the radiotap header
+constexpr std::size_t RADIOTAP_BITS_PER_PRESENT_FIELD{32};
+/// Size in bytes of the TSFT field in the radiotap header
+constexpr uint16_t RADIOTAP_TSFT_SIZE_B = 8;
+/// Alignment in bytes of the TSFT field in the radiotap header
+constexpr uint16_t RADIOTAP_TSFT_ALIGNMENT_B = 8;
+/// Size in bytes of the Flags field in the radiotap header
+constexpr uint16_t RADIOTAP_FLAGS_SIZE_B = 1;
+/// Size in bytes of the Rate field in the radiotap header
+constexpr uint16_t RADIOTAP_RATE_SIZE_B = 1;
+/// Size in bytes of the Channel field in the radiotap header
+constexpr uint16_t RADIOTAP_CHANNEL_SIZE_B = 4;
+/// Alignment in bytes of the Channel field in the radiotap header
+constexpr uint16_t RADIOTAP_CHANNEL_ALIGNMENT_B = 2;
+/// Size in bytes of the Antenna Signal field in the radiotap header
+constexpr uint16_t RADIOTAP_ANTENNA_SIGNAL_SIZE_B = 1;
+/// Size in bytes of the Antenna Noise field in the radiotap header
+constexpr uint16_t RADIOTAP_ANTENNA_NOISE_SIZE_B = 1;
+/// Size in bytes of the MCS field in the radiotap header
+constexpr uint16_t RADIOTAP_MCS_SIZE_B = 3;
+/// Alignment in bytes of the MCS field in the radiotap header
+constexpr uint16_t RADIOTAP_MCS_ALIGNMENT_B = 1;
+/// Size in bytes of the A-MPDU status field in the radiotap header
+constexpr uint16_t RADIOTAP_AMPDU_STATUS_SIZE_B = 8;
+/// Alignment in bytes of the A-MPDU status field in the radiotap header
+constexpr uint16_t RADIOTAP_AMPDU_STATUS_ALIGNMENT_B = 4;
+/// Size in bytes of the VHT field in the radiotap header
+constexpr uint16_t RADIOTAP_VHT_SIZE_B = 12;
+/// Alignment in bytes of the VHT field in the radiotap header
+constexpr uint16_t RADIOTAP_VHT_ALIGNMENT_B = 2;
+/// Size in bytes of the HE field in the radiotap header
+constexpr uint16_t RADIOTAP_HE_SIZE_B = 12;
+/// Alignment in bytes of the HE field in the radiotap header
+constexpr uint16_t RADIOTAP_HE_ALIGNMENT_B = 2;
+/// Size in bytes of the HE MU field in the radiotap header
+constexpr uint16_t RADIOTAP_HE_MU_SIZE_B = 12;
+/// Alignment in bytes of the HE MU field in the radiotap header
+constexpr uint16_t RADIOTAP_HE_MU_ALIGNMENT_B = 2;
+/// Size in bytes of the HE MU Other User field in the radiotap header
+constexpr uint16_t RADIOTAP_HE_MU_OTHER_USER_SIZE_B = 6;
+/// Alignment in bytes of the HE MU Other User field in the radiotap header
+constexpr uint16_t RADIOTAP_HE_MU_OTHER_USER_ALIGNMENT_B = 2;
+/// Size in bytes of the TLV fields (without data) in the radiotap header
+constexpr uint16_t RADIOTAP_TLV_HEADER_SIZE_B = 4;
+/// Alignment in bytes of the TLV fields in the radiotap header
+constexpr uint16_t RADIOTAP_TLV_ALIGNMENT_B = 4;
+/// Size in bytes of the U-SIG field in the radiotap header
+constexpr uint16_t RADIOTAP_USIG_SIZE_B = 12;
+/// Alignment in bytes of the U-SIG field in the radiotap header
+constexpr uint16_t RADIOTAP_USIG_ALIGNMENT_B = 4;
+/// Size in bytes of the known subfield of the EHT field in the radiotap header
+constexpr uint16_t RADIOTAP_EHT_KNOWN_SIZE_B = 4;
+/// Size in bytes of a data subfield of the EHT field in the radiotap header
+constexpr uint16_t RADIOTAP_EHT_DATA_SIZE_B = 4;
+/// Size in bytes of a user info subfield of the EHT field in the radiotap header
+constexpr uint16_t RADIOTAP_EHT_USER_INFO_SIZE_B = 4;
+/// Alignment in bytes of the EHT field in the radiotap header
+constexpr uint16_t RADIOTAP_EHT_ALIGNMENT_B = 4;
+
+/**
+ * @brief Calculate the padding needed to align a field
+ * @param offset Current offset in bytes
+ * @param alignment Alignment requirement in bytes
+ * @return Number of padding bytes needed
+ */
+uint8_t
+GetPadding(uint32_t offset, uint32_t alignment)
+{
+    uint8_t padding = (alignment - (offset % alignment)) % alignment;
+    return padding;
+}
+
+/**
+ * @brief Check if a specific field is present in the radiotap header
+ * @param presentBits Vector of bitsets representing the present fields
+ * @param field Field to check
+ * @return true if the field is present, false otherwise
+ */
+bool
+IsPresent(const std::vector<std::bitset<RADIOTAP_BITS_PER_PRESENT_FIELD>>& presentBits,
+          uint32_t field)
+{
+    const std::size_t bitmaskIdx = field / RADIOTAP_BITS_PER_PRESENT_FIELD;
+    const std::size_t bitIdx = field % RADIOTAP_BITS_PER_PRESENT_FIELD;
+    return (presentBits.size() > bitmaskIdx) && presentBits.at(bitmaskIdx).test(bitIdx);
+}
+
+} // namespace
+
 NS_LOG_COMPONENT_DEFINE("RadiotapHeader");
 
 NS_OBJECT_ENSURE_REGISTERED(RadiotapHeader);
 
-RadiotapHeader::RadiotapHeader()
-{
-    NS_LOG_FUNCTION(this);
-}
-
 TypeId
 RadiotapHeader::GetTypeId()
 {
@@ -47,7 +134,6 @@
 uint32_t
 RadiotapHeader::GetSerializedSize() const
 {
-    NS_LOG_FUNCTION(this);
     return m_length;
 }
 
@@ -60,9 +146,11 @@
     start.WriteU8(0);         // pad field
     start.WriteU16(m_length); // entire length of radiotap data + header
     NS_ASSERT(!m_present.empty());
+    std::vector<std::bitset<RADIOTAP_BITS_PER_PRESENT_FIELD>> presentBits;
     for (const auto present : m_present)
     {
         start.WriteU32(present); // bits describing which fields follow header
+        presentBits.emplace_back(present);
     }
 
     //
@@ -70,7 +158,7 @@
     // arrived at the MAC)
     // Reference: https://www.radiotap.org/fields/TSFT.html
     //
-    if (m_present.at(0) & RADIOTAP_TSFT) // bit 0
+    if (IsPresent(presentBits, RADIOTAP_TSFT))
     {
         SerializeTsft(start);
     }
@@ -79,7 +167,7 @@
     // Properties of transmitted and received frames.
     // Reference: https://www.radiotap.org/fields/Flags.html
     //
-    if (m_present.at(0) & RADIOTAP_FLAGS) // bit 1
+    if (IsPresent(presentBits, RADIOTAP_FLAGS))
     {
         start.WriteU8(m_flags);
     }
@@ -88,7 +176,7 @@
     // TX/RX data rate in units of 500 kbps
     // Reference: https://www.radiotap.org/fields/Rate.html
     //
-    if (m_present.at(0) & RADIOTAP_RATE) // bit 2
+    if (IsPresent(presentBits, RADIOTAP_RATE))
     {
         start.WriteU8(m_rate);
     }
@@ -97,27 +185,17 @@
     // Tx/Rx frequency in MHz, followed by flags.
     // Reference: https://www.radiotap.org/fields/Channel.html
     //
-    if (m_present.at(0) & RADIOTAP_CHANNEL) // bit 3
+    if (IsPresent(presentBits, RADIOTAP_CHANNEL))
     {
         SerializeChannel(start);
     }
 
     //
-    // The hop set and pattern for frequency-hopping radios.  We don't need it but
-    // still need to account for it.
-    // Reference: https://www.radiotap.org/fields/FHSS.html
-    //
-    if (m_present.at(0) & RADIOTAP_FHSS) // bit 4
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
     // RF signal power at the antenna, decibel difference from an arbitrary, fixed
     // reference.
     // Reference: https://www.radiotap.org/fields/Antenna%20signal.html
     //
-    if (m_present.at(0) & RADIOTAP_DBM_ANTSIGNAL) // bit 5
+    if (IsPresent(presentBits, RADIOTAP_DBM_ANTSIGNAL))
     {
         start.WriteU8(m_antennaSignal);
     }
@@ -127,92 +205,16 @@
     // reference.
     // Reference: https://www.radiotap.org/fields/Antenna%20noise.html
     //
-    if (m_present.at(0) & RADIOTAP_DBM_ANTNOISE) // bit 6
+    if (IsPresent(presentBits, RADIOTAP_DBM_ANTNOISE))
     {
         start.WriteU8(m_antennaNoise);
     }
 
     //
-    // Quality of Barker code lock.
-    // Reference: https://www.radiotap.org/fields/Lock%20quality.html
-    //
-    if (m_present.at(0) & RADIOTAP_LOCK_QUALITY) // bit 7
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Transmit power expressed as unitless distance from max power
-    // set at factory calibration (0 is max power).
-    // Reference: https://www.radiotap.org/fields/TX%20attenuation.html
-    //
-    if (m_present.at(0) & RADIOTAP_TX_ATTENUATION) // bit 8
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Transmit power expressed as decibel distance from max power
-    // set at factory calibration (0 is max power).
-    // Reference: https://www.radiotap.org/fields/dB%20TX%20attenuation.html
-    //
-    if (m_present.at(0) & RADIOTAP_DB_TX_ATTENUATION) // bit 9
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Transmit power expressed as dBm (decibels from a 1 milliwatt reference).
-    // This is the absolute power level measured at the antenna port.
-    // Reference: https://www.radiotap.org/fields/dBm%20TX%20power.html
-    //
-    if (m_present.at(0) & RADIOTAP_DBM_TX_POWER) // bit 10
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Unitless indication of the Rx/Tx antenna for this packet.
-    // The first antenna is antenna 0.
-    // Reference: https://www.radiotap.org/fields/Antenna.html
-    //
-    if (m_present.at(0) & RADIOTAP_ANTENNA) // bit 11
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // RF signal power at the antenna (decibel difference from an arbitrary fixed reference).
-    // Reference: https://www.radiotap.org/fields/dB%20antenna%20signal.html
-    //
-    if (m_present.at(0) & RADIOTAP_DB_ANTSIGNAL) // bit 12
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // RF noise power at the antenna (decibel difference from an arbitrary fixed reference).
-    // Reference: https://www.radiotap.org/fields/dB%20antenna%20noise.html
-    //
-    if (m_present.at(0) & RADIOTAP_DB_ANTNOISE) // bit 13
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Properties of received frames.
-    // Reference: https://www.radiotap.org/fields/RX%20flags.html
-    //
-    if (m_present.at(0) & RADIOTAP_RX_FLAGS) // bit 14
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
     // MCS field.
     // Reference: https://www.radiotap.org/fields/MCS.html
     //
-    if (m_present.at(0) & RADIOTAP_MCS) // bit 19
+    if (IsPresent(presentBits, RADIOTAP_MCS))
     {
         SerializeMcs(start);
     }
@@ -221,7 +223,7 @@
     // A-MPDU Status, information about the received or transmitted A-MPDU.
     // Reference: https://www.radiotap.org/fields/A-MPDU%20status.html
     //
-    if (m_present.at(0) & RADIOTAP_AMPDU_STATUS) // bit 20
+    if (IsPresent(presentBits, RADIOTAP_AMPDU_STATUS))
     {
         SerializeAmpduStatus(start);
     }
@@ -230,7 +232,7 @@
     // Information about the received or transmitted VHT frame.
     // Reference: https://www.radiotap.org/fields/VHT.html
     //
-    if (m_present.at(0) & RADIOTAP_VHT) // bit 21
+    if (IsPresent(presentBits, RADIOTAP_VHT))
     {
         SerializeVht(start);
     }
@@ -239,7 +241,7 @@
     // HE field.
     // Reference: https://www.radiotap.org/fields/HE.html
     //
-    if (m_present.at(0) & RADIOTAP_HE) // bit 23
+    if (IsPresent(presentBits, RADIOTAP_HE))
     {
         SerializeHe(start);
     }
@@ -248,7 +250,7 @@
     // HE MU field.
     // Reference: https://www.radiotap.org/fields/HE-MU.html
     //
-    if (m_present.at(0) & RADIOTAP_HE_MU) // bit 24
+    if (IsPresent(presentBits, RADIOTAP_HE_MU))
     {
         SerializeHeMu(start);
     }
@@ -257,7 +259,7 @@
     // HE MU other user field.
     // Reference: https://www.radiotap.org/fields/HE-MU-other-user.html
     //
-    if (m_present.at(0) & RADIOTAP_HE_MU_OTHER_USER) // bit 25
+    if (IsPresent(presentBits, RADIOTAP_HE_MU_OTHER_USER))
     {
         SerializeHeMuOtherUser(start);
     }
@@ -266,7 +268,7 @@
     // U-SIG field.
     // Reference: https://www.radiotap.org/fields/U-SIG.html
     //
-    if ((m_present.size() > 1) && m_present.at(1) & RADIOTAP_USIG) // bit 33
+    if (IsPresent(presentBits, RADIOTAP_USIG))
     {
         SerializeUsig(start);
     }
@@ -275,7 +277,7 @@
     // EHT field.
     // Reference: https://www.radiotap.org/fields/EHT.html
     //
-    if ((m_present.size() > 1) && m_present.at(1) & RADIOTAP_EHT_SIG) // bit 34
+    if (IsPresent(presentBits, RADIOTAP_EHT))
     {
         SerializeEht(start);
     }
@@ -285,8 +287,9 @@
 RadiotapHeader::Deserialize(Buffer::Iterator start)
 {
     NS_LOG_FUNCTION(this << &start);
+    m_present.clear();
 
-    uint8_t tmp = start.ReadU8(); // major version of radiotap header
+    auto tmp = start.ReadU8(); // major version of radiotap header
     NS_ASSERT_MSG(tmp == 0x00, "RadiotapHeader::Deserialize(): Unexpected major version");
     start.ReadU8(); // pad field
 
@@ -295,27 +298,32 @@
     uint32_t bytesRead = MIN_HEADER_SIZE;
 
     std::size_t index{0};
-    while (m_present.at(index++) & RADIOTAP_EXT)
+    std::vector<std::bitset<RADIOTAP_BITS_PER_PRESENT_FIELD>> presentBits;
+    presentBits.emplace_back(m_present.at(0));
+    while (presentBits.at(index++).test(RADIOTAP_MORE_PRESENT))
     {
         // If bit 31 of the it_present field is set, another it_present bitmask is present.
         m_present.emplace_back(start.ReadU32());
-        bytesRead += 4;
+        bytesRead += (RADIOTAP_BITS_PER_PRESENT_FIELD / 8);
+        presentBits.emplace_back(m_present.at(index));
     }
 
     //
     // Time Synchronization Function Timer (when the first bit of the MPDU arrived at the MAC)
     // Reference: https://www.radiotap.org/fields/TSFT.html
     //
-    if (m_present.at(0) & RADIOTAP_TSFT) // bit 0
+    if (IsPresent(presentBits, RADIOTAP_TSFT))
     {
-        bytesRead += DeserializeTsft(start, bytesRead);
+        const auto size = DeserializeTsft(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // Properties of transmitted and received frames.
     // Reference: https://www.radiotap.org/fields/Flags.html
     //
-    if (m_present.at(0) & RADIOTAP_FLAGS) // bit 1
+    if (IsPresent(presentBits, RADIOTAP_FLAGS))
     {
         m_flags = start.ReadU8();
         ++bytesRead;
@@ -325,7 +333,7 @@
     // TX/RX data rate in units of 500 kbps
     // Reference: https://www.radiotap.org/fields/Rate.html
     //
-    if (m_present.at(0) & RADIOTAP_RATE) // bit 2
+    if (IsPresent(presentBits, RADIOTAP_RATE))
     {
         m_rate = start.ReadU8();
         ++bytesRead;
@@ -335,19 +343,11 @@
     // Tx/Rx frequency in MHz, followed by flags.
     // Reference: https://www.radiotap.org/fields/Channel.html
     //
-    if (m_present.at(0) & RADIOTAP_CHANNEL) // bit 3
+    if (IsPresent(presentBits, RADIOTAP_CHANNEL))
     {
-        bytesRead += DeserializeChannel(start, bytesRead);
-    }
-
-    //
-    // The hop set and pattern for frequency-hopping radios.  We don't need it but
-    // still need to account for it.
-    // Reference: https://www.radiotap.org/fields/FHSS.html
-    //
-    if (m_present.at(0) & RADIOTAP_FHSS) // bit 4
-    {
-        NS_ASSERT(false); // not yet implemented
+        const auto size = DeserializeChannel(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
@@ -355,7 +355,7 @@
     // reference.
     // Reference: https://www.radiotap.org/fields/Antenna%20signal.html
     //
-    if (m_present.at(0) & RADIOTAP_DBM_ANTSIGNAL) // bit 5
+    if (IsPresent(presentBits, RADIOTAP_DBM_ANTSIGNAL))
     {
         m_antennaSignal = start.ReadU8();
         ++bytesRead;
@@ -366,169 +366,108 @@
     // reference.
     // Reference: https://www.radiotap.org/fields/Antenna%20noise.html
     //
-    if (m_present.at(0) & RADIOTAP_DBM_ANTNOISE) // bit 6
+    if (IsPresent(presentBits, RADIOTAP_DBM_ANTNOISE))
     {
         m_antennaNoise = start.ReadU8();
         ++bytesRead;
     }
 
     //
-    // Quality of Barker code lock.
-    // Reference: https://www.radiotap.org/fields/Lock%20quality.html
-    //
-    if (m_present.at(0) & RADIOTAP_LOCK_QUALITY) // bit 7
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Transmit power expressed as unitless distance from max power
-    // set at factory calibration (0 is max power).
-    // Reference: https://www.radiotap.org/fields/TX%20attenuation.html
-    //
-    if (m_present.at(0) & RADIOTAP_TX_ATTENUATION) // bit 8
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Transmit power expressed as decibel distance from max power
-    // set at factory calibration (0 is max power).
-    // Reference: https://www.radiotap.org/fields/dB%20TX%20attenuation.html
-    //
-    if (m_present.at(0) & RADIOTAP_DB_TX_ATTENUATION) // bit 9
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Transmit power expressed as dBm (decibels from a 1 milliwatt reference).
-    // This is the absolute power level measured at the antenna port.
-    // Reference: https://www.radiotap.org/fields/dBm%20TX%20power.html
-    //
-    if (m_present.at(0) & RADIOTAP_DBM_TX_POWER) // bit 10
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Unitless indication of the Rx/Tx antenna for this packet.
-    // The first antenna is antenna 0.
-    // Reference: https://www.radiotap.org/fields/Antenna.html
-    //
-    if (m_present.at(0) & RADIOTAP_ANTENNA) // bit 11
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // RF signal power at the antenna (decibel difference from an arbitrary fixed reference).
-    // Reference: https://www.radiotap.org/fields/dB%20antenna%20signal.html
-    //
-    if (m_present.at(0) & RADIOTAP_DB_ANTSIGNAL) // bit 12
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // RF noise power at the antenna (decibel difference from an arbitrary fixed reference).
-    // Reference: https://www.radiotap.org/fields/dB%20antenna%20noise.html
-    //
-    if (m_present.at(0) & RADIOTAP_DB_ANTNOISE) // bit 13
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
-    // Properties of received frames.
-    // Reference: https://www.radiotap.org/fields/RX%20flags.html
-    //
-    if (m_present.at(0) & RADIOTAP_RX_FLAGS) // bit 14
-    {
-        NS_ASSERT(false); // not yet implemented
-    }
-
-    //
     // MCS field.
     // Reference: https://www.radiotap.org/fields/MCS.html
     //
-    if (m_present.at(0) & RADIOTAP_MCS) // bit 19
+    if (IsPresent(presentBits, RADIOTAP_MCS))
     {
-        bytesRead += DeserializeMcs(start, bytesRead);
+        const auto size = DeserializeMcs(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // A-MPDU Status, information about the received or transmitted A-MPDU.
     // Reference: https://www.radiotap.org/fields/A-MPDU%20status.html
     //
-    if (m_present.at(0) & RADIOTAP_AMPDU_STATUS)
+    if (IsPresent(presentBits, RADIOTAP_AMPDU_STATUS))
     {
-        bytesRead += DeserializeAmpduStatus(start, bytesRead);
+        const auto size = DeserializeAmpduStatus(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // Information about the received or transmitted VHT frame.
     // Reference: https://www.radiotap.org/fields/VHT.html
     //
-    if (m_present.at(0) & RADIOTAP_VHT) // bit 21
+    if (IsPresent(presentBits, RADIOTAP_VHT))
     {
-        bytesRead += DeserializeVht(start, bytesRead);
+        const auto size = DeserializeVht(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // HE field.
     // Reference: https://www.radiotap.org/fields/HE.html
     //
-    if (m_present.at(0) & RADIOTAP_HE) // bit 23
+    if (IsPresent(presentBits, RADIOTAP_HE))
     {
-        bytesRead += DeserializeHe(start, bytesRead);
+        const auto size = DeserializeHe(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // HE MU field.
     // Reference: https://www.radiotap.org/fields/HE-MU.html
     //
-    if (m_present.at(0) & RADIOTAP_HE_MU) // bit 24
+    if (IsPresent(presentBits, RADIOTAP_HE_MU))
     {
-        bytesRead += DeserializeHeMu(start, bytesRead);
+        const auto size = DeserializeHeMu(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // HE MU other user field.
     // Reference: https://www.radiotap.org/fields/HE-MU-other-user.html
-    //
-    if (m_present.at(0) & RADIOTAP_HE_MU_OTHER_USER) // bit 25
+    if (IsPresent(presentBits, RADIOTAP_HE_MU_OTHER_USER))
     {
-        bytesRead += DeserializeHeMuOtherUser(start, bytesRead);
+        const auto size = DeserializeHeMuOtherUser(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // U-SIG field.
     // Reference: https://www.radiotap.org/fields/U-SIG.html
     //
-    if ((m_present.size() > 1) && m_present.at(1) & RADIOTAP_USIG) // bit 33
+    if (IsPresent(presentBits, RADIOTAP_USIG))
     {
-        bytesRead += DeserializeUsig(start, bytesRead);
+        const auto size = DeserializeUsig(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     //
     // EHT field.
     // Reference: https://www.radiotap.org/fields/EHT.html
     //
-    if ((m_present.size() > 1) && m_present.at(1) & RADIOTAP_EHT_SIG) // bit 34
+    if (IsPresent(presentBits, RADIOTAP_EHT))
     {
-        bytesRead += DeserializeEht(start, bytesRead);
+        const auto size = DeserializeEht(start, bytesRead);
+        start.Next(size);
+        bytesRead += size;
     }
 
     NS_ASSERT_MSG(m_length == bytesRead,
-                  "RadiotapHeader::Deserialize(): expected and actual lengths inconsistent");
+                  "RadiotapHeader::Deserialize(): expected and actual lengths inconsistent ("
+                      << m_length << " != " << bytesRead << ")");
     return bytesRead;
 }
 
 void
 RadiotapHeader::Print(std::ostream& os) const
 {
-    NS_LOG_FUNCTION(this << &os);
     os << " tsft=" << m_tsft << " flags=" << std::hex << m_flags << std::dec << " rate=" << +m_rate;
     if (m_present.at(0) & RADIOTAP_CHANNEL)
     {
@@ -563,13 +502,43 @@
     {
         PrintUsig(os);
     }
-    if ((m_present.size() > 1) && m_present.at(1) & RADIOTAP_EHT_SIG)
+    if ((m_present.size() > 1) && m_present.at(1) & RADIOTAP_EHT)
     {
         PrintEht(os);
     }
 }
 
 void
+RadiotapHeader::UpdatePresentField(uint32_t field)
+{
+    NS_LOG_FUNCTION(this << field);
+    const std::size_t bitmaskIdx = field / RADIOTAP_BITS_PER_PRESENT_FIELD;
+    NS_ASSERT_MSG(bitmaskIdx < m_present.size(),
+                  "Number of it_present words (" << m_present.size()
+                                                 << ") less than expected index " << bitmaskIdx);
+    const std::size_t fieldIdx = field % RADIOTAP_BITS_PER_PRESENT_FIELD;
+    const uint32_t flag = (1 << fieldIdx);
+    if (field != RADIOTAP_TLV)
+    {
+        NS_ASSERT_MSG(!(m_present.at(bitmaskIdx) & flag),
+                      "Radiotap field " << field << " already set in present field at index "
+                                        << bitmaskIdx);
+        NS_ASSERT_MSG((m_present.at(bitmaskIdx) < flag) &&
+                          ((m_present.size() == (bitmaskIdx + 1)) ||
+                           std::all_of(m_present.begin() + bitmaskIdx + 1,
+                                       m_present.end(),
+                                       [](uint32_t present) { return present == 0; })),
+                      "Radiotap fields not set in correct order");
+    }
+    if (const uint32_t morePresentWordFlag = (1 << RADIOTAP_MORE_PRESENT); bitmaskIdx > 0)
+    {
+        // Ensure that the bit indicating more present words is set in the previous word
+        m_present.at(bitmaskIdx - 1) |= morePresentWordFlag;
+    }
+    m_present.at(bitmaskIdx) |= flag;
+}
+
+void
 RadiotapHeader::SetWifiHeader(std::size_t numPresentWords)
 {
     NS_LOG_FUNCTION(this << numPresentWords);
@@ -580,9 +549,8 @@
     NS_ASSERT_MSG(m_present.size() == 1, "RadiotapHeader::SetWifiHeader() should be called once");
     for (std::size_t i = 0; i < (numPresentWords - 1); ++i)
     {
-        m_present.at(i) |= RADIOTAP_EXT;
         m_present.emplace_back(0);
-        m_length += sizeof(RadiotapExtFlags);
+        m_length += (RADIOTAP_BITS_PER_PRESENT_FIELD / 8);
     }
 }
 
@@ -591,19 +559,19 @@
 {
     NS_LOG_FUNCTION(this << value);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_TSFT), "TSFT radiotap field already present");
-    m_tsftPad = ((8 - m_length % 8) % 8);
-    m_present.at(0) |= RADIOTAP_TSFT;
-    m_length += 8 + m_tsftPad;
+    UpdatePresentField(RADIOTAP_TSFT);
+    m_tsftPad = GetPadding(m_length, RADIOTAP_TSFT_ALIGNMENT_B);
+    m_length += RADIOTAP_TSFT_SIZE_B + m_tsftPad;
     m_tsft = value;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeTsft(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_tsftPad);
     start.WriteU64(m_tsft);
 }
@@ -611,38 +579,37 @@
 uint32_t
 RadiotapHeader::DeserializeTsft(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_tsftPad = ((8 - bytesRead % 8) % 8);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_tsftPad = GetPadding(bytesRead, RADIOTAP_TSFT_ALIGNMENT_B);
     start.Next(m_tsftPad);
     m_tsft = start.ReadU64();
-    return sizeof(m_tsft) + m_tsftPad;
+    return RADIOTAP_TSFT_SIZE_B + m_tsftPad;
 }
 
 void
 RadiotapHeader::SetFrameFlags(uint8_t flags)
 {
-    NS_LOG_FUNCTION(this << +flags);
+    NS_LOG_FUNCTION(this << flags);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_FLAGS), "Flags radiotap field already present");
-    m_present.at(0) |= RADIOTAP_FLAGS;
-    m_length += 1;
+    UpdatePresentField(RADIOTAP_FLAGS);
+    m_length += RADIOTAP_FLAGS_SIZE_B;
     m_flags = flags;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SetRate(uint8_t rate)
 {
-    NS_LOG_FUNCTION(this << +rate);
+    NS_LOG_FUNCTION(this << rate);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_RATE), "Rate radiotap field already present");
-    m_present.at(0) |= RADIOTAP_RATE;
-    m_length += 1;
+    UpdatePresentField(RADIOTAP_RATE);
+    m_length += RADIOTAP_RATE_SIZE_B;
     m_rate = rate;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
@@ -650,19 +617,19 @@
 {
     NS_LOG_FUNCTION(this << channelFields.frequency << channelFields.flags);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_CHANNEL), "Channel radiotap field already present");
-    m_channelPad = ((2 - m_length % 2) % 2);
-    m_present.at(0) |= RADIOTAP_CHANNEL;
-    m_length += (sizeof(ChannelFields) + m_channelPad);
+    UpdatePresentField(RADIOTAP_CHANNEL);
+    m_channelPad = GetPadding(m_length, RADIOTAP_CHANNEL_ALIGNMENT_B);
+    m_length += RADIOTAP_CHANNEL_SIZE_B + m_channelPad;
     m_channelFields = channelFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeChannel(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_channelPad);
     start.WriteU16(m_channelFields.frequency);
     start.WriteU16(m_channelFields.flags);
@@ -671,11 +638,12 @@
 uint32_t
 RadiotapHeader::DeserializeChannel(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_channelPad = ((2 - bytesRead % 2) % 2);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_channelPad = GetPadding(bytesRead, RADIOTAP_CHANNEL_ALIGNMENT_B);
     start.Next(m_channelPad);
     m_channelFields.frequency = start.ReadU16();
     m_channelFields.flags = start.ReadU16();
-    return sizeof(ChannelFields) + m_channelPad;
+    return RADIOTAP_CHANNEL_SIZE_B + m_channelPad;
 }
 
 void
@@ -690,10 +658,8 @@
 {
     NS_LOG_FUNCTION(this << signal);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_DBM_ANTSIGNAL),
-                  "Antenna signal radiotap field already present");
-    m_present.at(0) |= RADIOTAP_DBM_ANTSIGNAL;
-    m_length += 1;
+    UpdatePresentField(RADIOTAP_DBM_ANTSIGNAL);
+    m_length += RADIOTAP_ANTENNA_SIGNAL_SIZE_B;
 
     if (signal > 127)
     {
@@ -708,8 +674,8 @@
         m_antennaSignal = static_cast<int8_t>(floor(signal + 0.5));
     }
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
@@ -717,10 +683,8 @@
 {
     NS_LOG_FUNCTION(this << noise);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_DBM_ANTNOISE),
-                  "Antenna noise radiotap field already present");
-    m_present.at(0) |= RADIOTAP_DBM_ANTNOISE;
-    m_length += 1;
+    UpdatePresentField(RADIOTAP_DBM_ANTNOISE);
+    m_length += RADIOTAP_ANTENNA_NOISE_SIZE_B;
 
     if (noise > 127.0)
     {
@@ -735,27 +699,29 @@
         m_antennaNoise = static_cast<int8_t>(floor(noise + 0.5));
     }
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SetMcsFields(const McsFields& mcsFields)
 {
-    NS_LOG_FUNCTION(this << +mcsFields.known << +mcsFields.flags << +mcsFields.mcs);
+    NS_LOG_FUNCTION(this << mcsFields.known << mcsFields.flags << mcsFields.mcs);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_MCS), "MCS radiotap field already present");
-    m_present.at(0) |= RADIOTAP_MCS;
-    m_length += sizeof(McsFields);
+    UpdatePresentField(RADIOTAP_MCS);
+    m_mcsPad = GetPadding(m_length, RADIOTAP_MCS_ALIGNMENT_B);
+    m_length += RADIOTAP_MCS_SIZE_B + m_mcsPad;
     m_mcsFields = mcsFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeMcs(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
+    start.WriteU8(0, m_mcsPad);
     start.WriteU8(m_mcsFields.known);
     start.WriteU8(m_mcsFields.flags);
     start.WriteU8(m_mcsFields.mcs);
@@ -764,10 +730,13 @@
 uint32_t
 RadiotapHeader::DeserializeMcs(Buffer::Iterator start, uint32_t bytesRead)
 {
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_mcsPad = GetPadding(bytesRead, RADIOTAP_MCS_ALIGNMENT_B);
+    start.Next(m_mcsPad);
     m_mcsFields.known = start.ReadU8();
     m_mcsFields.flags = start.ReadU8();
     m_mcsFields.mcs = start.ReadU8();
-    return sizeof(McsFields);
+    return RADIOTAP_MCS_SIZE_B + m_mcsPad;
 }
 
 void
@@ -782,20 +751,19 @@
 {
     NS_LOG_FUNCTION(this << ampduStatusFields.referenceNumber << ampduStatusFields.flags);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_AMPDU_STATUS),
-                  "A-MPDU status radiotap field already present");
-    m_ampduStatusPad = ((4 - m_length % 4) % 4);
-    m_present.at(0) |= RADIOTAP_AMPDU_STATUS;
-    m_length += (sizeof(ampduStatusFields) + m_ampduStatusPad);
+    UpdatePresentField(RADIOTAP_AMPDU_STATUS);
+    m_ampduStatusPad = GetPadding(m_length, RADIOTAP_AMPDU_STATUS_ALIGNMENT_B);
+    m_length += RADIOTAP_AMPDU_STATUS_SIZE_B + m_ampduStatusPad;
     m_ampduStatusFields = ampduStatusFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeAmpduStatus(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_ampduStatusPad);
     start.WriteU32(m_ampduStatusFields.referenceNumber);
     start.WriteU16(m_ampduStatusFields.flags);
@@ -806,13 +774,14 @@
 uint32_t
 RadiotapHeader::DeserializeAmpduStatus(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_ampduStatusPad = ((4 - bytesRead % 4) % 4);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_ampduStatusPad = GetPadding(bytesRead, RADIOTAP_AMPDU_STATUS_ALIGNMENT_B);
     start.Next(m_ampduStatusPad);
     m_ampduStatusFields.referenceNumber = start.ReadU32();
     m_ampduStatusFields.flags = start.ReadU16();
     m_ampduStatusFields.crc = start.ReadU8();
     m_ampduStatusFields.reserved = start.ReadU8();
-    return sizeof(AmpduStatusFields) + m_ampduStatusPad;
+    return RADIOTAP_AMPDU_STATUS_SIZE_B + m_ampduStatusPad;
 }
 
 void
@@ -824,24 +793,24 @@
 void
 RadiotapHeader::SetVhtFields(const VhtFields& vhtFields)
 {
-    NS_LOG_FUNCTION(this << vhtFields.known << vhtFields.flags << +vhtFields.mcsNss.at(0)
-                         << +vhtFields.mcsNss.at(1) << +vhtFields.mcsNss.at(2)
-                         << +vhtFields.mcsNss.at(3) << +vhtFields.coding << +vhtFields.groupId
-                         << +vhtFields.partialAid);
-
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_VHT), "VHT radiotap field already present");
-    m_vhtPad = ((2 - m_length % 2) % 2);
-    m_present.at(0) |= RADIOTAP_VHT;
-    m_length += (sizeof(VhtFields) + m_vhtPad);
+    NS_LOG_FUNCTION(this << vhtFields.known << vhtFields.flags << vhtFields.mcsNss.at(0)
+                         << vhtFields.mcsNss.at(1) << vhtFields.mcsNss.at(2)
+                         << vhtFields.mcsNss.at(3) << vhtFields.coding << vhtFields.groupId
+                         << vhtFields.partialAid);
+
+    UpdatePresentField(RADIOTAP_VHT);
+    m_vhtPad = GetPadding(m_length, RADIOTAP_VHT_ALIGNMENT_B);
+    m_length += RADIOTAP_VHT_SIZE_B + m_vhtPad;
     m_vhtFields = vhtFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeVht(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_vhtPad);
     start.WriteU16(m_vhtFields.known);
     start.WriteU8(m_vhtFields.flags);
@@ -858,7 +827,8 @@
 uint32_t
 RadiotapHeader::DeserializeVht(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_vhtPad = ((2 - bytesRead % 2) % 2);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_vhtPad = GetPadding(bytesRead, RADIOTAP_VHT_ALIGNMENT_B);
     start.Next(m_vhtPad);
     m_vhtFields.known = start.ReadU16();
     m_vhtFields.flags = start.ReadU8();
@@ -870,19 +840,20 @@
     m_vhtFields.coding = start.ReadU8();
     m_vhtFields.groupId = start.ReadU8();
     m_vhtFields.partialAid = start.ReadU16();
-    return sizeof(VhtFields) + m_vhtPad;
+    return RADIOTAP_VHT_SIZE_B + m_vhtPad;
 }
 
 void
 RadiotapHeader::PrintVht(std::ostream& os) const
 {
     os << " vht.known=0x" << m_vhtFields.known << " vht.flags=0x" << m_vhtFields.flags
-       << " vht.bandwidth=" << std::dec << m_vhtFields.bandwidth
-       << " vht.mcsNss[0]=" << +m_vhtFields.mcsNss.at(0)
-       << " vht.mcsNss[1]=" << +m_vhtFields.mcsNss.at(1)
-       << " vht.mcsNss[2]=" << +m_vhtFields.mcsNss.at(2)
-       << " vht.mcsNss[3]=" << +m_vhtFields.mcsNss.at(3) << " vht.coding=" << m_vhtFields.coding
-       << " vht.groupId=" << m_vhtFields.groupId << " vht.partialAid=" << m_vhtFields.partialAid;
+       << " vht.bandwidth=" << std::dec << m_vhtFields.bandwidth;
+    for (std::size_t i = 0; i < m_vhtFields.mcsNss.size(); ++i)
+    {
+        os << " vht.mcsNss[" << i << "]=" << +m_vhtFields.mcsNss.at(i);
+    }
+    os << " vht.coding=" << m_vhtFields.coding << " vht.groupId=" << m_vhtFields.groupId
+       << " vht.partialAid=" << m_vhtFields.partialAid;
 }
 
 void
@@ -891,19 +862,19 @@
     NS_LOG_FUNCTION(this << heFields.data1 << heFields.data2 << heFields.data3 << heFields.data4
                          << heFields.data5 << heFields.data6);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_HE), "HE radiotap field already present");
-    m_hePad = ((2 - m_length % 2) % 2);
-    m_present.at(0) |= RADIOTAP_HE;
-    m_length += (sizeof(heFields) + m_hePad);
+    UpdatePresentField(RADIOTAP_HE);
+    m_hePad = GetPadding(m_length, RADIOTAP_HE_ALIGNMENT_B);
+    m_length += RADIOTAP_HE_SIZE_B + m_hePad;
     m_heFields = heFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeHe(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_hePad);
     start.WriteU16(m_heFields.data1);
     start.WriteU16(m_heFields.data2);
@@ -916,7 +887,8 @@
 uint32_t
 RadiotapHeader::DeserializeHe(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_hePad = ((2 - bytesRead % 2) % 2);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_hePad = GetPadding(bytesRead, RADIOTAP_HE_ALIGNMENT_B);
     start.Next(m_hePad);
     m_heFields.data1 = start.ReadU16();
     m_heFields.data2 = start.ReadU16();
@@ -924,7 +896,7 @@
     m_heFields.data4 = start.ReadU16();
     m_heFields.data5 = start.ReadU16();
     m_heFields.data6 = start.ReadU16();
-    return sizeof(HeFields) + m_hePad;
+    return RADIOTAP_HE_SIZE_B + m_hePad;
 }
 
 void
@@ -941,19 +913,19 @@
 {
     NS_LOG_FUNCTION(this << heMuFields.flags1 << heMuFields.flags2);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_HE_MU), "HE-MU radiotap field already present");
-    m_heMuPad = ((2 - m_length % 2) % 2);
-    m_present.at(0) |= RADIOTAP_HE_MU;
-    m_length += (sizeof(heMuFields) + m_heMuPad);
+    UpdatePresentField(RADIOTAP_HE_MU);
+    m_heMuPad = GetPadding(m_length, RADIOTAP_HE_MU_ALIGNMENT_B);
+    m_length += RADIOTAP_HE_MU_SIZE_B + m_heMuPad;
     m_heMuFields = heMuFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                             << std::dec);
 }
 
 void
 RadiotapHeader::SerializeHeMu(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_heMuPad);
     start.WriteU16(m_heMuFields.flags1);
     start.WriteU16(m_heMuFields.flags2);
@@ -970,7 +942,8 @@
 uint32_t
 RadiotapHeader::DeserializeHeMu(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_heMuPad = ((2 - bytesRead % 2) % 2);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_heMuPad = GetPadding(bytesRead, RADIOTAP_HE_MU_ALIGNMENT_B);
     start.Next(m_heMuPad);
     m_heMuFields.flags1 = start.ReadU16();
     m_heMuFields.flags2 = start.ReadU16();
@@ -982,7 +955,7 @@
     {
         ruChannel = start.ReadU8();
     }
-    return sizeof(HeMuFields) + m_heMuPad;
+    return RADIOTAP_HE_MU_SIZE_B + m_heMuPad;
 }
 
 void
@@ -996,23 +969,22 @@
 RadiotapHeader::SetHeMuOtherUserFields(const HeMuOtherUserFields& heMuOtherUserFields)
 {
     NS_LOG_FUNCTION(this << heMuOtherUserFields.perUser1 << heMuOtherUserFields.perUser2
-                         << +heMuOtherUserFields.perUserPosition
-                         << +heMuOtherUserFields.perUserKnown);
+                         << heMuOtherUserFields.perUserPosition
+                         << heMuOtherUserFields.perUserKnown);
 
-    NS_ASSERT_MSG(!(m_present.at(0) & RADIOTAP_HE_MU_OTHER_USER),
-                  "HE-MU-other-user radiotap field already present");
-    m_heMuOtherUserPad = ((2 - m_length % 2) % 2);
-    m_present.at(0) |= RADIOTAP_HE_MU_OTHER_USER;
-    m_length += (sizeof(HeMuOtherUserFields) + m_heMuOtherUserPad);
+    UpdatePresentField(RADIOTAP_HE_MU_OTHER_USER);
+    m_heMuOtherUserPad = GetPadding(m_length, RADIOTAP_HE_MU_OTHER_USER_ALIGNMENT_B);
+    m_length += RADIOTAP_HE_MU_OTHER_USER_SIZE_B + m_heMuOtherUserPad;
     m_heMuOtherUserFields = heMuOtherUserFields;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
-                      << std::dec);
+    NS_LOG_LOGIC(" m_length=" << m_length << " m_present=0x" << std::hex << m_present.at(0)
+                              << std::dec);
 }
 
 void
 RadiotapHeader::SerializeHeMuOtherUser(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_heMuOtherUserPad);
     start.WriteU16(m_heMuOtherUserFields.perUser1);
     start.WriteU16(m_heMuOtherUserFields.perUser2);
@@ -1023,13 +995,14 @@
 uint32_t
 RadiotapHeader::DeserializeHeMuOtherUser(Buffer::Iterator start, uint32_t bytesRead)
 {
-    m_heMuOtherUserPad = ((2 - bytesRead % 2) % 2);
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+    m_heMuOtherUserPad = GetPadding(bytesRead, RADIOTAP_HE_MU_OTHER_USER_ALIGNMENT_B);
     start.Next(m_heMuOtherUserPad);
     m_heMuOtherUserFields.perUser1 = start.ReadU16();
     m_heMuOtherUserFields.perUser2 = start.ReadU16();
     m_heMuOtherUserFields.perUserPosition = start.ReadU8();
     m_heMuOtherUserFields.perUserKnown = start.ReadU8();
-    return sizeof(HeMuOtherUserFields) + m_heMuOtherUserPad;
+    return RADIOTAP_HE_MU_OTHER_USER_SIZE_B + m_heMuOtherUserPad;
 }
 
 void
@@ -1046,29 +1019,26 @@
 RadiotapHeader::SetUsigFields(const UsigFields& usigFields)
 {
     NS_LOG_FUNCTION(this << usigFields.common << usigFields.mask << usigFields.value);
-    NS_ASSERT_MSG(m_present.size() >= 2,
-                  "Number of it_present words (" << m_present.size() << ") is incorrect");
-    m_present.at(0) |= RADIOTAP_TLV;
-
-    NS_ASSERT_MSG(!(m_present.at(1) & RADIOTAP_USIG), "U-SIG radiotap field already present");
-    m_present.at(1) |= RADIOTAP_USIG;
-
-    m_usigTlvPad = ((8 - m_length % 8) % 8);
-    m_usigTlv.type = 32 + std::countr_zero<uint16_t>(RADIOTAP_USIG);
-    m_usigTlv.length = sizeof(UsigFields);
-    m_length += sizeof(TlvFields) + m_usigTlvPad;
 
-    m_usigPad = ((4 - m_length % 4) % 4);
+    UpdatePresentField(RADIOTAP_TLV);
+    m_usigTlvPad = GetPadding(m_length, RADIOTAP_TLV_ALIGNMENT_B);
+    m_usigTlv.type = RADIOTAP_USIG;
+    m_usigTlv.length = RADIOTAP_USIG_SIZE_B;
+    m_length += RADIOTAP_TLV_HEADER_SIZE_B + m_usigTlvPad;
+
+    UpdatePresentField(RADIOTAP_USIG);
+    m_usigPad = GetPadding(m_length, RADIOTAP_USIG_ALIGNMENT_B);
     m_usigFields = usigFields;
     m_length += m_usigTlv.length + m_usigPad;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present[0]=0x" << std::hex
-                      << m_present.at(0) << " m_present[1]=0x" << m_present.at(1) << std::dec);
+    NS_LOG_LOGIC(" m_length=" << m_length << " m_present[0]=0x" << std::hex << m_present.at(0)
+                              << " m_present[1]=0x" << m_present.at(1) << std::dec);
 }
 
 void
 RadiotapHeader::SerializeUsig(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_usigTlvPad);
     start.WriteU16(m_usigTlv.type);
     start.WriteU16(m_usigTlv.length);
@@ -1081,20 +1051,21 @@
 uint32_t
 RadiotapHeader::DeserializeUsig(Buffer::Iterator start, uint32_t bytesRead)
 {
+    NS_LOG_FUNCTION(this << &start << bytesRead);
     const auto startBytesRead = bytesRead;
-    m_usigTlvPad = ((8 - bytesRead % 8) % 8);
+    m_usigTlvPad = GetPadding(bytesRead, RADIOTAP_TLV_ALIGNMENT_B);
     start.Next(m_usigTlvPad);
     bytesRead += m_usigTlvPad;
     m_usigTlv.type = start.ReadU16();
     m_usigTlv.length = start.ReadU16();
-    bytesRead += sizeof(TlvFields);
-    m_usigPad = ((4 - bytesRead % 4) % 4);
+    bytesRead += RADIOTAP_TLV_HEADER_SIZE_B;
+    m_usigPad = GetPadding(bytesRead, RADIOTAP_USIG_ALIGNMENT_B);
     start.Next(m_usigPad);
     bytesRead += m_usigPad;
     m_usigFields.common = start.ReadU32();
     m_usigFields.value = start.ReadU32();
     m_usigFields.mask = start.ReadU32();
-    bytesRead += sizeof(UsigFields);
+    bytesRead += RADIOTAP_USIG_SIZE_B;
     return bytesRead - startBytesRead;
 }
 
@@ -1109,29 +1080,28 @@
 RadiotapHeader::SetEhtFields(const EhtFields& ehtFields)
 {
     NS_LOG_FUNCTION(this << ehtFields.known);
-    NS_ASSERT_MSG(m_present.size() >= 2,
-                  "Number of it_present words (" << m_present.size() << ") is incorrect");
-    m_present.at(0) |= RADIOTAP_TLV;
-
-    NS_ASSERT_MSG(!(m_present.at(1) & RADIOTAP_EHT_SIG), "EHT radiotap field already present");
-    m_present.at(1) |= RADIOTAP_EHT_SIG;
-
-    m_ehtTlvPad = ((8 - m_length % 8) % 8);
-    m_ehtTlv.type = 32 + std::countr_zero<uint16_t>(RADIOTAP_EHT_SIG);
-    m_ehtTlv.length = (40 + ehtFields.userInfo.size() * 4);
-    m_length += sizeof(TlvFields) + m_ehtTlvPad;
 
-    m_ehtPad = ((4 - m_length % 4) % 4);
+    UpdatePresentField(RADIOTAP_TLV);
+    m_ehtTlvPad = GetPadding(m_length, RADIOTAP_TLV_ALIGNMENT_B);
+    m_ehtTlv.type = RADIOTAP_EHT;
+    m_ehtTlv.length = RADIOTAP_EHT_KNOWN_SIZE_B +
+                      (RADIOTAP_EHT_DATA_SIZE_B * ehtFields.data.size()) +
+                      (ehtFields.userInfo.size() * RADIOTAP_EHT_USER_INFO_SIZE_B);
+    m_length += RADIOTAP_TLV_HEADER_SIZE_B + m_ehtTlvPad;
+
+    UpdatePresentField(RADIOTAP_EHT);
+    m_ehtPad = GetPadding(m_length, RADIOTAP_EHT_ALIGNMENT_B);
     m_ehtFields = ehtFields;
     m_length += m_ehtTlv.length + m_ehtPad;
 
-    NS_LOG_LOGIC(this << " m_length=" << m_length << " m_present[0]=0x" << std::hex
-                      << m_present.at(0) << " m_present[1]=0x" << m_present.at(1) << std::dec);
+    NS_LOG_LOGIC("m_length=" << m_length << " m_present[0]=0x" << std::hex << m_present.at(0)
+                             << " m_present[1]=0x" << m_present.at(1) << std::dec);
 }
 
 void
 RadiotapHeader::SerializeEht(Buffer::Iterator& start) const
 {
+    NS_LOG_FUNCTION(this << &start);
     start.WriteU8(0, m_ehtTlvPad);
     start.WriteU16(m_ehtTlv.type);
     start.WriteU16(m_ehtTlv.length);
@@ -1150,32 +1120,34 @@
 uint32_t
 RadiotapHeader::DeserializeEht(Buffer::Iterator start, uint32_t bytesRead)
 {
+    NS_LOG_FUNCTION(this << &start << bytesRead);
+
     const auto startBytesRead = bytesRead;
 
-    m_ehtTlvPad = ((8 - bytesRead % 8) % 8);
+    m_ehtTlvPad = GetPadding(bytesRead, RADIOTAP_TLV_ALIGNMENT_B);
     start.Next(m_ehtTlvPad);
     bytesRead += m_ehtTlvPad;
     m_ehtTlv.type = start.ReadU16();
     m_ehtTlv.length = start.ReadU16();
-    bytesRead += sizeof(TlvFields);
+    bytesRead += RADIOTAP_TLV_HEADER_SIZE_B;
 
-    m_ehtPad = ((4 - bytesRead % 4) % 4);
+    m_ehtPad = GetPadding(bytesRead, RADIOTAP_EHT_ALIGNMENT_B);
     start.Next(m_ehtPad);
     bytesRead += m_ehtPad;
     m_ehtFields.known = start.ReadU32();
-    bytesRead += 4;
+    bytesRead += RADIOTAP_EHT_KNOWN_SIZE_B;
     for (auto& dataField : m_ehtFields.data)
     {
         dataField = start.ReadU32();
-        bytesRead += 4;
+        bytesRead += RADIOTAP_EHT_DATA_SIZE_B;
     }
     const auto userInfosBytes = m_ehtTlv.length - bytesRead - m_ehtTlvPad;
-    NS_ASSERT(userInfosBytes % 4 == 0);
-    const std::size_t numUsers = userInfosBytes / 4;
+    NS_ASSERT(userInfosBytes % RADIOTAP_EHT_USER_INFO_SIZE_B == 0);
+    const std::size_t numUsers = userInfosBytes / RADIOTAP_EHT_USER_INFO_SIZE_B;
     for (std::size_t i = 0; i < numUsers; ++i)
     {
         m_ehtFields.userInfo.push_back(start.ReadU32());
-        bytesRead += 4;
+        bytesRead += RADIOTAP_EHT_USER_INFO_SIZE_B;
     }
 
     return bytesRead - startBytesRead;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/radiotap-header.h ns-3.47/src/network/utils/radiotap-header.h
--- ns-3.46/src/network/utils/radiotap-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/radiotap-header.h	2026-02-16 16:08:49.806230800 -0800
@@ -30,7 +30,6 @@
 class RadiotapHeader : public Header
 {
   public:
-    RadiotapHeader();
     /**
      * @brief Get the type ID.
      * @returns the object TypeId
@@ -784,6 +783,13 @@
     static constexpr int MIN_HEADER_SIZE{8}; //!< the minimum size of the radiotap header
 
     /**
+     * Update a present field.
+     *
+     * @param field The field to set in a present bitmask.
+     */
+    void UpdatePresentField(uint32_t field);
+
+    /**
      * Serialize the TSFT radiotap header.
      *
      * @param start An iterator which points to where the header should be written.
@@ -1017,45 +1023,26 @@
     void PrintEht(std::ostream& os) const;
 
     /**
-     * @brief Radiotap flags.
+     * @brief Radiotap fields.
      */
-    enum RadiotapFlags : uint32_t
+    enum RadiotapFields : uint32_t
     {
-        RADIOTAP_TSFT = 0x00000001,
-        RADIOTAP_FLAGS = 0x00000002,
-        RADIOTAP_RATE = 0x00000004,
-        RADIOTAP_CHANNEL = 0x00000008,
-        RADIOTAP_FHSS = 0x00000010,
-        RADIOTAP_DBM_ANTSIGNAL = 0x00000020,
-        RADIOTAP_DBM_ANTNOISE = 0x00000040,
-        RADIOTAP_LOCK_QUALITY = 0x00000080,
-        RADIOTAP_TX_ATTENUATION = 0x00000100,
-        RADIOTAP_DB_TX_ATTENUATION = 0x00000200,
-        RADIOTAP_DBM_TX_POWER = 0x00000400,
-        RADIOTAP_ANTENNA = 0x00000800,
-        RADIOTAP_DB_ANTSIGNAL = 0x00001000,
-        RADIOTAP_DB_ANTNOISE = 0x00002000,
-        RADIOTAP_RX_FLAGS = 0x00004000,
-        RADIOTAP_MCS = 0x00080000,
-        RADIOTAP_AMPDU_STATUS = 0x00100000,
-        RADIOTAP_VHT = 0x00200000,
-        RADIOTAP_HE = 0x00800000,
-        RADIOTAP_HE_MU = 0x01000000,
-        RADIOTAP_HE_MU_OTHER_USER = 0x02000000,
-        RADIOTAP_ZERO_LEN_PSDU = 0x04000000,
-        RADIOTAP_LSIG = 0x08000000,
-        RADIOTAP_TLV = 0x10000000,
-        RADIOTAP_EXT = 0x80000000
-    };
-
-    /**
-     * @brief Radiotap extended flags.
-     */
-    enum RadiotapExtFlags : uint32_t
-    {
-        RADIOTAP_S1G = 0x00000001,
-        RADIOTAP_USIG = 0x00000002,
-        RADIOTAP_EHT_SIG = 0x00000004
+        RADIOTAP_TSFT = 0,
+        RADIOTAP_FLAGS = 1,
+        RADIOTAP_RATE = 2,
+        RADIOTAP_CHANNEL = 3,
+        RADIOTAP_DBM_ANTSIGNAL = 5,
+        RADIOTAP_DBM_ANTNOISE = 6,
+        RADIOTAP_MCS = 19,
+        RADIOTAP_AMPDU_STATUS = 20,
+        RADIOTAP_VHT = 21,
+        RADIOTAP_HE = 23,
+        RADIOTAP_HE_MU = 24,
+        RADIOTAP_HE_MU_OTHER_USER = 25,
+        RADIOTAP_TLV = 28,
+        RADIOTAP_MORE_PRESENT = 31, // indicates that another 32 bits of present flags follow
+        RADIOTAP_USIG = 33,
+        RADIOTAP_EHT = 34,
     };
 
     uint16_t m_length{MIN_HEADER_SIZE}; //!< entire length of radiotap data + header
@@ -1077,6 +1064,7 @@
     int8_t m_antennaNoise{
         0}; //!< RF noise power at the antenna, dB difference from an arbitrary, fixed reference.
 
+    uint8_t m_mcsPad{0};     //!< MCS padding.
     McsFields m_mcsFields{}; //!< MCS fields.
 
     uint8_t m_ampduStatusPad{0}; //!< A-MPDU Status Flags, padding before A-MPDU Status Field.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/network/utils/sequence-number.h ns-3.47/src/network/utils/sequence-number.h
--- ns-3.46/src/network/utils/sequence-number.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/network/utils/sequence-number.h	2026-02-16 16:08:49.806230800 -0800
@@ -11,6 +11,7 @@
 
 #include "ns3/type-name.h"
 
+#include <compare>
 #include <iostream>
 #include <limits>
 #include <stdint.h>
@@ -256,73 +257,45 @@
     }
 
     /**
-     * Here is the critical part, how the comparison is made taking into
-     * account wrap-around.  From RFC 3626:
-     *
-     * The sequence number S1 is said to be "greater than" the sequence
-     * number S2 if:
-     *      S1 > S2 AND S1 - S2 <= MAXVALUE/2 OR
-     *      S2 > S1 AND S2 - S1 > MAXVALUE/2
-     *
-     * @param other sequence number to compare to this one
-     * @returns true if this sequence number is greater than other
-     */
-    bool operator>(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
-    {
-        static const NUMERIC_TYPE halfMaxValue = std::numeric_limits<NUMERIC_TYPE>::max() / 2;
-
-        return (((m_value > other.m_value) && (m_value - other.m_value) <= halfMaxValue) ||
-                ((other.m_value > m_value) && (other.m_value - m_value) > halfMaxValue));
-    }
-
-    /**
      * @brief Equality operator for comparing sequence number
      * @param other sequence number to compare to this sequence number
      * @returns true if the sequence numbers are equal
      */
-    bool operator==(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
+    constexpr bool operator==(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
     {
         return m_value == other.m_value;
     }
 
     /**
-     * @brief Inequality operator for comparing sequence numbers
-     * @param other sequence number to compare to this sequence number
-     * @returns true if the sequence numbers are not equal
-     */
-    bool operator!=(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
-    {
-        return m_value != other.m_value;
-    }
-
-    /**
-     * @brief Less than or equal operator for comparing sequence numbers
-     * @param other sequence number to compare to this sequence number
-     * @returns true if this sequence number is less than or equal to other
-     */
-    bool operator<=(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
-    {
-        return (!this->operator>(other));
-    }
-
-    /**
-     * @brief Greater than or equal operator for comparing sequence numbers
-     * @param other sequence number to compare to this sequence number
-     * @returns true if this sequence number is greater than or equal to other
+     * Spaceship comparison operator. All the other comparison operators
+     * are automatically generated from this one.
+     *
+     * Here is the critical part, how the comparison is made taking into
+     * account wrap-around. From RFC 3626:
+     *
+     * The sequence number S1 is said to be "greater than" the sequence
+     * number S2 if:
+     *      S1 > S2 AND S1 - S2 <= MAXVALUE/2 OR
+     *      S2 > S1 AND S2 - S1 > MAXVALUE/2
+     *
+     * @param other sequence number to compare to this one
+     * @returns The result of the comparison.
      */
-    bool operator>=(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
+    constexpr std::strong_ordering operator<=>(
+        const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
     {
-        return (this->operator>(other) || this->operator==(other));
-    }
+        constexpr auto halfMaxValue = std::numeric_limits<NUMERIC_TYPE>::max() / 2;
 
-    /**
-     * @brief Less than operator for comparing sequence numbers
-     * @param other sequence number to compare to this sequence number
-     * @returns true if this sequence number is less than other
-     */
-    bool operator<(const SequenceNumber<NUMERIC_TYPE, SIGNED_TYPE>& other) const
-    {
-        return !this->operator>(other) && m_value != other.m_value;
+        if (m_value == other.m_value)
+        {
+            return std::strong_ordering::equivalent;
+        }
+        if (((m_value > other.m_value) && (m_value - other.m_value) <= halfMaxValue) ||
+            ((other.m_value > m_value) && (other.m_value - m_value) > halfMaxValue))
+        {
+            return std::strong_ordering::greater;
+        }
+        return std::strong_ordering::less;
     }
 
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/nix-vector-routing/model/nix-vector-routing.h ns-3.47/src/nix-vector-routing/model/nix-vector-routing.h
--- ns-3.46/src/nix-vector-routing/model/nix-vector-routing.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/nix-vector-routing/model/nix-vector-routing.h	2026-02-16 16:08:49.807230700 -0800
@@ -71,9 +71,6 @@
     /// Alias for Ipv4Route and Ipv6Route classes
     using IpRoute = typename std::conditional_t<IsIpv4, Ipv4Route, Ipv6Route>;
 
-    /// Alias for Ipv4AddressHash and Ipv6AddressHash classes
-    using IpAddressHash = typename std::conditional_t<IsIpv4, Ipv4AddressHash, Ipv6AddressHash>;
-
     /// Alias for Ipv4Header and Ipv6Header classes
     using IpHeader = typename std::conditional_t<IsIpv4, Ipv4Header, Ipv6Header>;
 
@@ -499,7 +496,7 @@
      * GetNodeByIp() method.  NIX vector routing assumes IP addresses
      * are unique so mapping can be done without duplication.
      **/
-    typedef std::unordered_map<IpAddress, ns3::Ptr<ns3::Node>, IpAddressHash> IpAddressToNodeMap;
+    typedef std::unordered_map<IpAddress, ns3::Ptr<ns3::Node>> IpAddressToNodeMap;
     static IpAddressToNodeMap g_ipAddressToNodeMap; //!< Address to node map.
 
     /// Mapping of Ptr<NetDevice> to Ptr<IpInterface>.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/olsr/model/olsr-routing-protocol.cc ns-3.47/src/olsr/model/olsr-routing-protocol.cc
--- ns-3.46/src/olsr/model/olsr-routing-protocol.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/olsr/model/olsr-routing-protocol.cc	2026-02-16 16:08:49.808230600 -0800
@@ -88,12 +88,6 @@
 /// Maximum number of messages per packet.
 #define OLSR_MAX_MSGS 64
 
-/// Maximum number of hellos per message (4 possible link types * 3 possible nb types).
-#define OLSR_MAX_HELLOS 12
-
-/// Maximum number of addresses advertised on a message.
-#define OLSR_MAX_ADDRS 64
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/openflow/CMakeLists.txt ns-3.47/src/openflow/CMakeLists.txt
--- ns-3.46/src/openflow/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/openflow/CMakeLists.txt	2026-02-16 16:08:49.809230800 -0800
@@ -48,7 +48,10 @@
   return()
 endif()
 
-include_directories(${openflow_INCLUDE_DIRS})
+include_directories(
+  SYSTEM
+  ${openflow_INCLUDE_DIRS}
+)
 add_definitions(
   -DNS3_OPENFLOW
   -DENABLE_OPENFLOW
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/openflow/helper/openflow-switch-helper.cc ns-3.47/src/openflow/helper/openflow-switch-helper.cc
--- ns-3.46/src/openflow/helper/openflow-switch-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/openflow/helper/openflow-switch-helper.cc	2026-02-16 16:08:49.809230800 -0800
@@ -48,7 +48,7 @@
     NS_LOG_INFO("**** Set up Controller");
     dev->SetController(controller);
 
-    for (NetDeviceContainer::Iterator i = c.Begin(); i != c.End(); ++i)
+    for (auto i = c.Begin(); i != c.End(); ++i)
     {
         NS_LOG_INFO("**** Add SwitchPort " << *i);
         dev->AddSwitchPort(*i);
@@ -67,7 +67,7 @@
     devs.Add(dev);
     node->AddDevice(dev);
 
-    for (NetDeviceContainer::Iterator i = c.Begin(); i != c.End(); ++i)
+    for (auto i = c.Begin(); i != c.End(); ++i)
     {
         NS_LOG_INFO("**** Add SwitchPort " << *i);
         dev->AddSwitchPort(*i);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/doc/propagation.rst ns-3.47/src/propagation/doc/propagation.rst
--- ns-3.46/src/propagation/doc/propagation.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/doc/propagation.rst	2026-02-16 16:08:49.811230700 -0800
@@ -4,13 +4,46 @@
 .. _Propagation:
 
 Propagation
------------
+===========
+
+.. heading hierarchy:
+   ============= Chapter
+   ------------- Section (#.#)
+   ~~~~~~~~~~~~~ Subsection (#.#.#)
+   ^^^^^^^^^^^^^ Paragraph (no number)
 
 The |ns3| ``propagation`` module defines two generic interfaces, namely :cpp:class:`PropagationLossModel`
 and :cpp:class:`PropagationDelayModel`, to model respectively the propagation loss and the propagation delay.
 
+Scope and Limitations
+---------------------
+
+**Current Support**
+
+- A wide range of propagation loss models (Friis, Two-Ray Ground, Log-Distance, Nakagami, etc.) covering free-space, empirical, and stochastic channel conditions
+
+- Propagation delay modeling including constant speed and random delay models
+
+- Frequency-dependent propagation loss models for different simulation scenarios
+
+- Support for spectrum-aware propagation loss calculations
+
+- Compatibility with both PHY and channel modules in |ns3|
+
+
+**Current Limitations**
+
+- Some empirical models may have limited validity outside their intended environments
+
+- Computational cost can increase significantly when using complex models with large node counts
+
+- Limited built-in support for site-specific or ray-tracing–based propagation models
+
+- Integration with terrain and obstacle-aware models requires external tools or extensions
+
+
 PropagationLossModel
-********************
+--------------------
 
 Propagation loss models calculate the Rx signal power considering the Tx signal power and the
 mutual Rx and Tx antennas positions.
@@ -21,41 +54,51 @@
 
 The following propagation loss models are implemented:
 
-   * Cost231PropagationLossModel
-   * FixedRssLossModel
-   * FriisPropagationLossModel
-   * ItuR1411LosPropagationLossModel
-   * ItuR1411NlosOverRooftopPropagationLossModel
-   * JakesPropagationLossModel
-   * Kun2600MhzPropagationLossModel
-   * LogDistancePropagationLossModel
-   * MatrixPropagationLossModel
-   * NakagamiPropagationLossModel
-   * OkumuraHataPropagationLossModel
-   * RandomPropagationLossModel
-   * RangePropagationLossModel
-   * ThreeLogDistancePropagationLossModel
-   * TwoRayGroundPropagationLossModel
-   * ThreeGppPropagationLossModel
+**Simple Range Models**
 
-      * ThreeGppRMaPropagationLossModel
-      * ThreeGppUMaPropagationLossModel
-      * ThreeGppUmiStreetCanyonPropagationLossModel
-      * ThreeGppIndoorOfficePropagationLossModel
-      * ThreeGppNTNDenseUrbanPropagationLossModel
-      * ThreeGppNTNUrbanPropagationLossModel
-      * ThreeGppNTNSuburbanPropagationLossModel
-      * ThreeGppNTNRuralPropagationLossModel
+- FriisPropagationLossModel
+- TwoRayGroundPropagationLossModel
+- LogDistancePropagationLossModel
+- ThreeLogDistancePropagationLossModel
+- RangePropagationLossModel
+
+**Fading Models**
+
+- NakagamiPropagationLossModel
+- JakesPropagationLossModel
+
+**Abstract Models**
+
+- FixedRssLossModel
+- RandomPropagationLossModel
+- MatrixPropagationLossModel
+
+**Macro and Standard Models**
+
+- Cost231PropagationLossModel
+- OkumuraHataPropagationLossModel
+- Kun2600MhzPropagationLossModel
+- ItuR1411LosPropagationLossModel
+- ItuR1411NlosOverRooftopPropagationLossModel
+- ThreeGppPropagationLossModel
+   - ThreeGppRMaPropagationLossModel
+   - ThreeGppUMaPropagationLossModel
+   - ThreeGppUmiStreetCanyonPropagationLossModel
+   - ThreeGppIndoorOfficePropagationLossModel
+   - ThreeGppNTNDenseUrbanPropagationLossModel
+   - ThreeGppNTNUrbanPropagationLossModel
+   - ThreeGppNTNSuburbanPropagationLossModel
+   - ThreeGppNTNRuralPropagationLossModel
 
 Other models could be available thanks to other modules, e.g., the ``building`` module.
 
-Each of the available propagation loss models of ns-3 is explained in
+Each ofT the available propagation loss models of ns-3 is explained in
 one of the following subsections.
 
 FriisPropagationLossModel
-=========================
+~~~~~~~~~~~~~~~~~~~~~~~~~
 
-This model implements the Friis propagation loss model. This model was first described in [friis]_.
+This model implements the Friis propagation loss model. This model was first described in the work of H.T. [1]_.
 The original equation was described as:
 
 .. math::
@@ -147,7 +190,7 @@
 
 
 TwoRayGroundPropagationLossModel
-================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This model implements a Two-Ray Ground propagation loss model ported from NS2
 
@@ -180,7 +223,7 @@
 
 
 LogDistancePropagationLossModel
-===============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This model implements a log distance propagation model.
 
@@ -207,7 +250,7 @@
 the reference distance, the tx power is returned.
 
 ThreeLogDistancePropagationLossModel
-====================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This model implements a log distance path loss propagation model with three distance
 fields. This model is the same as ns3::LogDistancePropagationLossModel
@@ -261,21 +304,18 @@
 reference distance defaults to 1m and reference loss defaults to
 :cpp:class:`FriisPropagationLossModel` with 5.15 GHz and is thus :math:`L_0` = 46.67 dB.
 
-JakesPropagationLossModel
-=========================
-
-ToDo
-````
+RangePropagationLossModel
+~~~~~~~~~~~~~~~~~~~~~~~~~
 
-RandomPropagationLossModel
-==========================
+This propagation loss depends only on the distance (range) between transmitter and receiver.
 
-The propagation loss is totally random, and it changes each time the model is called.
-As a consequence, all the packets (even those between two fixed nodes) experience a random
-propagation loss.
+The single MaxRange attribute (units of meters) determines path loss.
+Receivers at or within MaxRange meters receive the transmission at the
+transmit power level. Receivers beyond MaxRange receive at power
+-1000 dBm (effectively zero).
 
 NakagamiPropagationLossModel
-============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This propagation loss model implements the Nakagami-m fast fading
 model, which accounts for the variations in signal strength due to multipath
@@ -306,8 +346,77 @@
 For :math:`m = 1` the Nakagami-m distribution equals the Rayleigh distribution. Thus
 this model also implements Rayleigh distribution based fast fading.
 
+JakesPropagationLossModel
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This model implements a single path stationary Jakes propagation loss model for simulating Rayleigh fading channels.
+This model was first described in Microwave Mobile Communications [2]_, but the implementation available in ns-3 is based on a later document by Zheng et al. [3]_ (Section II.A).
+The Jakes method is used to model effective Rayleigh fading channels through a deterministic approach, providing a
+computational simplification at the cost of slightly decreased accuracy compared to statistical methods.
+This model considers one transmitter-receiver pair and calculates the complex channel coefficients that represent the fading envelope.
+
+The model generates complex channel coefficients using the sum-of-sinusoids approach:
+
+.. math::
+
+  X(t) = X_c(t) + j X_s(t)
+
+where the in-phase and quadrature components are given by:
+
+.. math::
+
+  X_c(t) = \frac{2}{\sqrt{M}}\sum_{n=1}^{M}\cos(\psi_n)\cos(\omega_d t\cos(\alpha_n)+\phi_n)
+
+.. math::
+
+  X_s(t) = \frac{2}{\sqrt{M}}\sum_{n=1}^{M}\sin(\psi_n)\cos(\omega_d t\cos(\alpha_n)+\phi_n)
+
+with the angle of arrival for the n-th sinusoid:
+
+.. math::
+
+  \alpha_n = \frac{2\pi n - \pi + \theta}{4M}, \quad n=1,2, \ldots,M
+
+where:
+
+  :math:`M` : number of sinusoids (determines accuracy vs. computational complexity)
+  :math:`\omega_d` : maximum Doppler frequency (rad/s)
+  :math:`\theta` : uniformly distributed random variable over :math:[-\pi, \pi)
+  :math:`\phi_n` : uniformly distributed random phase over :math:[-\pi, \pi) for each sinusoid
+  :math:`\psi_n` : uniformly distributed random variable over :math:[-\pi, \pi) for each sinusoid
+  :math:`t` : time variable
+
+The parameters :math:\theta, :math:\phi_n, and :math:\psi_n are statistically independent and uniformly distributed over :math:[-\pi, \pi) for all :math:n. The resulting complex coefficient :math:X(t) has a magnitude that follows a Rayleigh distribution, making it suitable for modeling non-line-of-sight propagation scenarios.
+
+The channel gain in dB is calculated from the complex coefficient as:
+
+.. math::
+
+  G_{dB}(t) = 10 \log_{10}\left(\frac{\left|X(t)\right|^2}{2}\right)
+
+where :math:\left|X(t)\right|^2 = X_c(t)^2 + X_s(t)^2 is the squared magnitude of the complex coefficient.
+The implementation uses oscillators where each oscillator contributes:
+
+.. math::
+
+  \text{oscillator}_n(t) = A_n e^{j(\omega_n t + \phi)}
+
+with complex amplitude:
+
+.. math::
+
+  A_n = \frac{2}{\sqrt{M}} e^{j\psi_n} = \frac{2}{\sqrt{M}}(\cos(\psi_n) + j\sin(\psi_n))
+
+and angular frequency:
+
+.. math::
+
+  \omega_n = \omega_d \cos(\alpha_n)
+
+The total complex gain is the sum of all oscillator contributions.
+
 FixedRssLossModel
-=================
+~~~~~~~~~~~~~~~~~
 
 This model sets a constant received power level independent of the transmit power.
 
@@ -316,31 +425,150 @@
 models, it should be the first loss model in the chain.
 Else it will disregard the losses computed by loss models that precede it in the chain.
 
+RandomPropagationLossModel
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This model implements a completely random propagation loss mechanism where the attenuation varies
+randomly for each transmission, regardless of the distance or environmental conditions between nodes.
+The propagation loss changes every time the model is called, meaning that all packets (even those
+transmitted between two fixed nodes) experience independent random propagation losses.
+
+*Purpose and Use Cases*:
+
+The Random Propagation Loss Model is primarily designed for software testing purposes
+rather than for modeling a real-world channel. It serves several specific use cases:
+
+* Show how to randomize results and work with the random stream module
+
+* Fuzzing Testing: Provides random inputs to test the robustness of networking protocols and applications under unpredictable channel conditions
+
+* Development Testing: Helps developers identify edge cases and validate that their code handles a wide range of signal strength variations
+
+* Statistical Analysis: Enables Monte Carlo simulations where propagation effects need to be decoupled from spatial relationships
+
+This model could also be used to generate something akin to small scale
+fading effects when paired with a path loss model, if the user wanted to
+create some new model from scratch using a specially-designed random
+variable.
+
+
+*Mathematical Formulation*:
+
+The received power is calculated as:
+
+.. math::
+
+  P_r = P_t - X
+
+where:
+
+  :math:`P_r` : received power in dBm
+  :math:`P_t` : transmitted power in dBm
+  :math:`X` : random attenuation value in dB, drawn from the configured random variable
+
+The attenuation value :math:X is generated independently for each call to the model, ensuring complete randomness across all transmissions.
+Configuration:
+The model uses a configurable random variable to determine the attenuation values:
+
+
+Default: ns3::ConstantRandomVariable[Constant=1.0] (constant 1 dB attenuation)
+
+Configurable: Any random variable stream can be used (uniform, exponential, normal, etc.)
+
+Example Configurations:
+
+.. sourcecode:: cpp
+
+  // Uniform random attenuation between 0 and 10 dB
+  randomLoss->SetAttribute("Variable", StringValue("ns3::UniformRandomVariable[Min=0.0|Max=10.0]"));
+
+  // Normal distribution with mean 5 dB, standard deviation 2 dB
+  randomLoss->SetAttribute("Variable", StringValue("ns3::NormalRandomVariable[Mean=5.0|Variance=4.0]"));
+
+  // Exponential distribution with mean 3 dB
+  randomLoss->SetAttribute("Variable", StringValue("ns3::ExponentialRandomVariable[Mean=3.0]"));
+
+Important Characteristics:
+
+* No Spatial Correlation: The loss is completely independent of distance between transmitter and receiver
+
+* No Environmental Modeling: Physical obstacles, terrain, or atmospheric conditions are ignored
+
+* Temporal Independence: Each packet transmission experiences an independent random loss
+
+* Deterministic Reproducibility: When using the same random seed, results are reproducible for testing
+
+Stream Management:
+
+The model supports proper random stream assignment for reproducible results across simulation runs. The random variable stream can be configured to ensure deterministic behavior when needed for regression testing.
+
 MatrixPropagationLossModel
-==========================
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The propagation loss is fixed for each pair of nodes and doesn't depend on their actual positions.
 This model should be useful for synthetic tests. Note that by default the propagation loss is
 assumed to be symmetric.
 
-RangePropagationLossModel
-=========================
+Cost231PropagationLossModel
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-This propagation loss depends only on the distance (range) between transmitter and receiver.
+This model implements the COST 231 [5]_ propagation loss model, also known as the Hata Model PCS Extension.
+It is a radio propagation model that extends the Hata Model (which in turn is based on the Okumura Model)
+to cover a more elaborate range of frequencies.
 
-The single MaxRange attribute (units of meters) determines path loss.
-Receivers at or within MaxRange meters receive the transmission at the
-transmit power level. Receivers beyond MaxRange receive at power
--1000 dBm (effectively zero).
+COST (COopération européenne dans le domaine de la recherche Scientifique et Technique) is a European Union
+Forum for cooperative scientific research which developed this model based on various experiments and research
+studies. This model is specifically applicable to urban areas and can be extended to evaluate path loss in
+suburban or rural quasi-open/open areas.
+
+*Model Applicability*:
+
+* Frequency Range: 1500 MHz to 2000 MHz
+* Mobile Station Antenna Height: 1 to 10 meters
+* Base Station Antenna Height: 30 to 200 meters
+* Link Distance: up to 20 kilometers
+* Environment: Urban areas (primary), extensible to suburban/rural
+
+*Mathematical Formulation*:
+
+The path loss in dB is calculated using the COST 231 formula:
+
+.. math::
+
+  L = 46.3 + 33.9 \log_{10}(f) - 13.82 \log_{10}(h_b) - C_H + (44.9 - 6.55 \log_{10}(h_b)) \log_{10}(d) + S
+
+where the mobile antenna height correction factor :math:`C_H` is given by:
+
+.. math::
+  C_H = 0.8 + (1.11 \log_{10}(f) - 0.7) h_m - 1.56 \log_{10}(f)
+
+*Parameters*:
+
+  :math:`f` : frequency in MHz
+  :math:`h_b` : base station antenna height in meters
+  :math:`h_m` : mobile station antenna height in meters
+  :math:`d` : distance between transmitter and receiver in kilometers
+  :math:`S` : shadowing factor in dB (default: 10 dB)
+  :math:`L` : path loss in dB
+
+Model Behavior:
+
+For distances smaller than the minimum distance threshold (default 0.5m), the model returns 0 dB path loss
+The model includes a configurable shadowing factor to account for environmental variations
+Default parameters are set for 2.3 GHz frequency with base station height of 50m and mobile station height of 3m
+
+Implementation Notes:
+
+The model uses logarithmic calculations in base 10, with frequency converted to MHz, distance to kilometers, and all height measurements in meters. The final result represents the path loss that should be subtracted from the transmitted power to obtain the received power level.
 
 OkumuraHataPropagationLossModel
-===============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This model is used to model open area pathloss for long distance (i.e., > 1 Km).
 In order to include all the possible frequencies usable by LTE we need to consider
 several variants of the well known Okumura Hata model. In fact, the original Okumura
-Hata model [hata]_ is designed for frequencies ranging from 150 MHz to 1500 MHz,
-the COST231 [cost231]_ extends it for the frequency range from 1500 MHz to 2000 MHz.
+Hata model [4]_ is designed for frequencies ranging from 150 MHz to 1500 MHz,
+the COST231 [5]_ extends it for the frequency range from 1500 MHz to 2000 MHz.
 Another important aspect is the scenarios considered by the models, in fact the all
 models are originally designed for urban scenario and then only the standard one and
 the COST231 are extended to suburban, while only the standard one has been extended
@@ -416,15 +644,19 @@
 The literature lacks of extensions of the COST231 to open area (for suburban it seems that
 we can just impose C = 0); therefore we consider it a special case of the suburban one.
 
+Kun2600MhzPropagationLossModel
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Cost231PropagationLossModel
-===========================
+This is the empirical model for the pathloss at 2600 MHz for urban areas which is described in Kun et al. [8]_.
+The model is as follows. Let :math:`d` be the distance between the transmitter and the receiver
+in meters; the pathloss :math:`L` in dB is calculated as:
 
-ToDo
-````
+.. math::
+
+  L = 36 + 26\log{d}
 
 ItuR1411LosPropagationLossModel
-===============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This model is designed for Line-of-Sight (LoS) short range outdoor communication in the
 frequency range 300 MHz to 100 GHz.  This model provides an upper and lower bound
@@ -464,14 +696,14 @@
 
 
 ItuR1411NlosOverRooftopPropagationLossModel
-===========================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 This model is designed for Non-Line-of-Sight (LoS) short range outdoor communication over
 rooftops in the frequency range 300 MHz to 100 GHz. This model includes several scenario-dependent
 parameters, such as average street width, orientation, etc. It is advised to set the values of
 these parameters manually (using the ns-3 attribute system) according to the desired scenario.
 
-In detail, the model is based on [walfisch]_ and [ikegami]_, where the loss is expressed
+In detail, the model is based on work by Walfisch et al. [6]_ and Ikegami et al. [7]_, where the loss is expressed
 as the sum of free-space loss (:math:`L_{bf}`), the diffraction loss from rooftop to
 street (:math:`L_{rts}`) and the reduction due to multiple screen diffraction past
 rows of building (:math:`L_{msd}`). The formula is:
@@ -577,23 +809,11 @@
 
   \rho = \sqrt{\Delta h_b^2 + b^2}
 
-
-Kun2600MhzPropagationLossModel
-==============================
-
-This is the empirical model for the pathloss at 2600 MHz for urban areas which is described in [kun2600mhz]_.
-The model is as follows. Let :math:`d` be the distance between the transmitter and the receiver
-in meters; the pathloss :math:`L` in dB is calculated as:
-
-.. math::
-
-  L = 36 + 26\log{d}
-
 ThreeGppPropagationLossModel
-============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The base class :cpp:class:`ThreeGppPropagationLossModel` and its derived classes implement
-the path loss and shadow fading models described in 3GPP TR 38.901 [38901]_.
+the path loss and shadow fading models described in 3GPP TR 38.901 [9]_.
 3GPP TR 38.901 includes multiple scenarios modeling different propagation
 environments, i.e., indoor, outdoor urban and rural, for frequencies between
 0.5 and 100 GHz.
@@ -669,7 +889,7 @@
 the method GetShadowing, which generates an additional random loss component
 characterized by Gaussian distribution with zero mean and scenario-specific
 standard deviation. Subsequent shadowing components of each BS-UT link are
-correlated as described in 3GPP TR 38.901, Sec. 7.4.4 [38901]_.
+correlated as described in 3GPP TR 38.901, Sec. 7.4.4 [9]_.
 
 *Note 1*: The TR defines height ranges for UTs and BSs, depending on the chosen
 propagation model (for the exact values, please see below in the specific model
@@ -688,7 +908,7 @@
 ThreeGppRMaPropagationLossModel
 ```````````````````````````````
 
-This class implements the LOS/NLOS path loss and shadow fading models described in 3GPP TR 38.901 [38901]_, Table 7.4.1-1 for the RMa scenario.
+This class implements the LOS/NLOS path loss and shadow fading models described in 3GPP TR 38.901 [9]_, Table 7.4.1-1 for the RMa scenario.
 It supports frequencies between 0.5 and 30 GHz.
 It is possible to configure some scenario-related parameters through the attributes AvgBuildingHeight and AvgStreetWidth.
 
@@ -703,7 +923,7 @@
 ```````````````````````````````
 
 This implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.901 [38901]_, Table 7.4.1-1 for the UMa scenario. It supports frequencies
+TR 38.901 [9]_, Table 7.4.1-1 for the UMa scenario. It supports frequencies
 between 0.5 and 100 GHz.
 
 As specified in the TR, the 2D distance between the transmitter and the receiver
@@ -716,7 +936,7 @@
 ```````````````````````````````````````````
 
 This implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.901 [38901]_, Table 7.4.1-1 for the UMi-Street Canyon scenario. It
+TR 38.901 [9]_, Table 7.4.1-1 for the UMi-Street Canyon scenario. It
 supports frequencies between 0.5 and 100 GHz.
 
 As specified in the TR, the 2D distance between the transmitter and the receiver
@@ -731,7 +951,7 @@
 ````````````````````````````````````````
 
 This implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.901 [38901]_, Table 7.4.1-1 for the Indoor-Office scenario. It supports
+TR 38.901 [9]_, Table 7.4.1-1 for the Indoor-Office scenario. It supports
 frequencies between 0.5 and 100 GHz.
 
 As specified in the TR, the 3D distance between the transmitter and the receiver
@@ -747,14 +967,14 @@
 The test cases :cpp:class:`ThreeGppRmaPropagationLossModelTestCase`,
 :cpp:class:`ThreeGppUmaPropagationLossModelTestCase`,
 :cpp:class:`ThreeGppUmiPropagationLossModelTestCase` and
-:cpp:class:`ThreeGppIndoorOfficePropagationLossModelTestCase` compute the path loss between two nodes and compares it with the value obtained using the formulas in 3GPP TR 38.901 [38901]_, Table 7.4.1-1.
+:cpp:class:`ThreeGppIndoorOfficePropagationLossModelTestCase` compute the path loss between two nodes and compares it with the value obtained using the formulas in 3GPP TR 38.901 [9]_, Table 7.4.1-1.
 The test case :cpp:class:`ThreeGppShadowingTestCase` checks if the shadowing is correctly computed by testing the deviation of the overall propagation loss from the path loss. The test is carried out for all the scenarios, both in LOS and NLOS condition.
 
 ThreeGppNTNPropagationLossModel
-===============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Four different classes have been derived from the base class :cpp:class:`ThreeGppPropagationLossModel`
-to support Non-Terrestrial Networks, one for each scenario presented in 3GPP TR 38.811 [38811]_, i.e.,
+to support Non-Terrestrial Networks, one for each scenario presented in 3GPP TR 38.811 [12]_, i.e.,
 dense urban, urban, suburban and rural.
 
 *Implemented features:*
@@ -769,30 +989,205 @@
 ThreeGppNTNDenseUrbanPropagationLossModel
 `````````````````````````````````````````
 This class implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.811 [38811]_, Table 6.6.2-1 for the Dense Urban scenario. It
+TR 38.811 [12]_, Table 6.6.2-1 for the Dense Urban scenario. It
 supports frequencies between 0.5 and 100 GHz.
 
 ThreeGppNTNUrbanPropagationLossModel
 ````````````````````````````````````
 This class implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.811 [38811]_, Table 6.6.2-2 for the Urban scenario. It
+TR 38.811 [12]_, Table 6.6.2-2 for the Urban scenario. It
 supports frequencies between 0.5 and 100 GHz.
 
 ThreeGppNTNSuburbanPropagationLossModel
 ```````````````````````````````````````
 This class implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.811 [38811]_, Table 6.6.2-3 for the Suburban scenario. It
+TR 38.811 [12]_, Table 6.6.2-3 for the Suburban scenario. It
 supports frequencies between 0.5 and 100 GHz.
 
 ThreeGppNTNRuralPropagationLossModel
 ````````````````````````````````````
 This class implements the LOS/NLOS path loss and shadow fading models described in 3GPP
-TR 38.811 [38811]_, Table 6.6.2-3 for the Rural scenario. It
+TR 38.811 [12]_, Table 6.6.2-3 for the Rural scenario. It
 supports frequencies between 0.5 and 100 GHz.
 
 
+Usage
+-----
+
+This section describes how to effectively use propagation models in ns-3 simulations, covering basic configuration, advanced techniques, performance optimization, and common applications.
+
+Basic Configuration
+~~~~~~~~~~~~~~~~~~~
+
+**Setting Up a Propagation Model**
+
+The most fundamental step in using propagation models is configuring them properly in your ns-3 simulation. Here's the basic workflow:
+
+1. Include Required Headers
+
+.. sourcecode:: cpp
+
+   #include "ns3/propagation-loss-model.h"
+   #include "ns3/propagation-delay-model.h"
+   #include "ns3/yans-wifi-helper.h"
+
+2. Create and Configure the Channel
+
+.. sourcecode:: cpp
+
+   // Create the wireless channel
+   YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
+
+   // Set the propagation loss model
+   channel.AddPropagationLoss("ns3::FriisPropagationLossModel");
+
+   // Set the propagation delay model
+   channel.SetPropagationDelay("ns3::ConstantSpeedPropagationDelayModel");
+
+3. Apply to PHY Helper
+
+.. sourcecode:: cpp
+
+   YansWifiPhyHelper phy;
+   phy.SetChannel(channel.Create());
+
+**Setting Model Parameters**
+
+Most propagation models accept parameters that can be configured using attributes:
+
+.. sourcecode:: cpp
+
+   // Configure Friis model parameters
+   channel.AddPropagationLoss("ns3::FriisPropagationLossModel",
+                              "Frequency", DoubleValue(2.4e9),
+                              "SystemLoss", DoubleValue(1.0));
+
+.. sourcecode:: cpp
+
+   // Configure Log Distance model
+   channel.AddPropagationLoss("ns3::LogDistancePropagationLossModel",
+                              "Exponent", DoubleValue(3.0),
+                              "ReferenceDistance", DoubleValue(1.0),
+                              "ReferenceLoss", DoubleValue(46.6777));
+
+**Using Different Models**
+
+ns-3 provides several built-in propagation models:
+
+.. sourcecode:: cpp
+
+   // Free space model (ideal conditions)
+   channel.AddPropagationLoss("ns3::FriisPropagationLossModel");
+
+.. sourcecode:: cpp
+
+   // Log distance model (general purpose)
+   channel.AddPropagationLoss("ns3::LogDistancePropagationLossModel");
+
+.. sourcecode:: cpp
+
+   // Two-ray ground reflection model
+   channel.AddPropagationLoss("ns3::TwoRayGroundPropagationLossModel");
+
+.. sourcecode:: cpp
+
+   // Okumura-Hata model (urban environments)
+   channel.AddPropagationLoss("ns3::OkumuraHataPropagationLossModel");
+
+Model Stacking
+~~~~~~~~~~~~~~
+
+One must be aware of the stackability of propagation models in ns-3. This allows you to combine different propagation effects for more realistic simulations but if not used properly, it may lead to flawed results. The order matters! The Base model should be set first, then additional effects
+
+Propagation loss models can be chained together, where each model in the chain applies its loss calculation to the signal:
+
+.. sourcecode:: cpp
+
+   YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
+   // Free space model (ideal conditions)
+   channel.AddPropagationLoss("ns3::FriisPropagationLossModel");
+   // Add a random propagation loss model for fading
+   channel.AddPropagationLoss("ns3::RandomPropagationLossModel");
+
+As the above example shows, the stacking of models is very easy and does not require any additional tool. This also means that the user must be careful not to stack models by inadvertence.
+
+Classification for stacking
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If we take the list of available loss models and create two categories, base loss model and stacking models, here is the result:
+
+**Base Models (Primary Path Loss - Use ONE (1) as foundation)**:
+
+   * FriisPropagationLossModel
+   * TwoRayGroundPropagationLossModel
+   * LogDistancePropagationLossModel
+   * ThreeLogDistancePropagationLossModel
+   * RangePropagationLossModel
+   * OkumuraHataPropagationLossModel
+   * Cost231PropagationLossModel
+   * ItuR1411LosPropagationLossModel
+   * ItuR1411NlosOverRooftopPropagationLossModel
+   * Kun2600MhzPropagationLossModel
+
+**Stacking Models (Additional Effects - Stack on base models)**:
+
+   * JakesPropagationLossModel
+   * RandomPropagationLossModel
+   * NakagamiPropagationLossModel
+   * FixedRssLossModel
+   * MatrixPropagationLossModel
+
+**Complete 3GPP Models (Self-contained)**:
+
+3GPP models are comprehensive, do not stack other models with them as they already include multiple propagation effects.
+
+   * ThreeGppPropagationLossModel
+
+      * ThreeGppRMaPropagationLossModel
+      * ThreeGppUMaPropagationLossModel
+      * ThreeGppUmiStreetCanyonPropagationLossModel
+      * ThreeGppIndoorOfficePropagationLossModel
+      * ThreeGppNTNDenseUrbanPropagationLossModel
+      * ThreeGppNTNUrbanPropagationLossModel
+      * ThreeGppNTNSuburbanPropagationLossModel
+      * ThreeGppNTNRuralPropagationLossModel
+
+Typical Stacking Patterns
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Pattern 1: Base + Shadowing**
+
+.. sourcecode:: cpp
+
+   // Base path loss
+   channel.AddPropagationLoss("ns3::LogDistancePropagationLossModel");
+   // Add log-normal shadowing
+   channel.AddPropagationLoss("ns3::RandomPropagationLossModel",
+                           "Variable", StringValue("ns3::LogNormalRandomVariable[Mu=0.0|Sigma=4.0]"));
+
+**Pattern 2: Base + Fading**
+
+.. sourcecode:: cpp
+
+   // Base path loss
+   channel.AddPropagationLoss("ns3::FriisPropagationLossModel");
+   // Add Nakagami fading
+   channel.AddPropagationLoss("ns3::NakagamiPropagationLossModel");
+
+**Pattern 3: Base + Shadowing + Fading**
+
+.. sourcecode:: cpp
+
+   // Base path loss
+   channel.AddPropagationLoss("ns3::TwoRayGroundPropagationLossModel");
+   // Add shadowing
+   channel.AddPropagationLoss("ns3::RandomPropagationLossModel",
+                           "Variable", StringValue("ns3::LogNormalRandomVariable[Mu=0.0|Sigma=6.0]"));
+   // Add fading
+   channel.AddPropagationLoss("ns3::JakesPropagationLossModel");
+
 ChannelConditionModel
-*********************
+---------------------
 
 The loss models require to know if two nodes are in Line-of-Sight (LoS) or if
 they are not. The interface for that is represented by this class. The main
@@ -821,72 +1216,72 @@
 * :cpp:class:`BuildingsChannelConditionModel` (see the ``building`` module documentation for further details)
 
 ThreeGppChannelConditionModel
-=============================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 This is the base class for the 3GPP channel condition models.
 It provides the possibility to update the condition of each channel periodically,
 after a given time period which can be configured through the attribute "UpdatePeriod".
 If "UpdatePeriod" is set to 0, the channel condition is never updated.
-It has five derived classes implementing the channel condition models described in 3GPP TR 38.901 [38901]_ for different propagation scenarios.
+It has five derived classes implementing the channel condition models described in 3GPP TR 38.901 [9]_ for different propagation scenarios.
 
 ThreeGppRmaChannelConditionModel
 ````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.901 [38901]_, Table 7.4.2-1, for the RMa scenario.
+This class implements the statistical channel condition model described in 3GPP TR 38.901 [9]_, Table 7.4.2-1, for the RMa scenario.
 
 ThreeGppUmaChannelConditionModel
 ````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.901 [38901]_, Table 7.4.2-1, for the UMa scenario.
+This class implements the statistical channel condition model described in 3GPP TR 38.901 [9]_, Table 7.4.2-1, for the UMa scenario.
 
 ThreeGppUmiStreetCanyonChannelConditionModel
 ````````````````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.901 [38901]_, Table 7.4.2-1, for the UMi-Street Canyon scenario.
+This class implements the statistical channel condition model described in 3GPP TR 38.901 [9]_, Table 7.4.2-1, for the UMi-Street Canyon scenario.
 
 ThreeGppIndoorMixedOfficeChannelConditionModel
 ``````````````````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.901 [38901]_, Table 7.4.2-1, for the Indoor-Mixed office scenario.
+This class implements the statistical channel condition model described in 3GPP TR 38.901 [9]_, Table 7.4.2-1, for the Indoor-Mixed office scenario.
 
 ThreeGppIndoorOpenOfficeChannelConditionModel
 `````````````````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.901 [38901]_, Table 7.4.2-1, for the Indoor-Open office scenario.
+This class implements the statistical channel condition model described in 3GPP TR 38.901 [9]_, Table 7.4.2-1, for the Indoor-Open office scenario.
 
 ThreeGppNTNChannelConditionModel
-================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 This is the base class for the 3GPP NTN channel condition models.
 It provides the possibility to update the condition of each channel periodically,
 after a given time period which can be configured through the attribute "UpdatePeriod".
 If "UpdatePeriod" is set to 0, the channel condition is never updated.
-It has four derived classes implementing the channel condition models described in 3GPP TR 38.811 [38811]_
+It has four derived classes implementing the channel condition models described in 3GPP TR 38.811 [12]_
 for the different propagation scenarios. i.e., dense urban, urban, suburban and rural.
 
 ThreeGppDenseUrbanChannelConditionModel
 ```````````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.811 [38811]_,
+This class implements the statistical channel condition model described in 3GPP TR 38.811 [12]_,
 Table 6.6.1-1, for the Dense Urban scenario.
 
 ThreeGppUrbanChannelConditionModel
 ``````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.811 [38811]_,
+This class implements the statistical channel condition model described in 3GPP TR 38.811 [12]_,
 Table 6.6.1-1, for the Urban scenario.
 
 ThreeGppSuburbanStreetCanyonChannelConditionModel
 `````````````````````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.811 [38811]_,
+This class implements the statistical channel condition model described in 3GPP TR 38.811 [12]_,
 Table 6.6.1-1, for the Suburban scenario.
 
 ThreeGppRuralChannelConditionModel
 ``````````````````````````````````
-This class implements the statistical channel condition model described in 3GPP TR 38.811 [38811]_,
+This class implements the statistical channel condition model described in 3GPP TR 38.811 [12]_,
 Table 6.6.1-1, for the Rural office scenario.
 
 
 Testing
-=======
+~~~~~~~
 The test suite :cpp:class:`ChannelConditionModelsTestSuite` contains a single test case:
 
-* :cpp:class:`ThreeGppChannelConditionModelTestCase`, which tests all the 3GPP channel condition models. It determines the channel condition between two nodes multiple times, estimates the LOS probability, and compares it with the value given by the formulas in 3GPP TR 38.901 [38901]_, Table 7.4.2-1
+* :cpp:class:`ThreeGppChannelConditionModelTestCase`, which tests all the 3GPP channel condition models. It determines the channel condition between two nodes multiple times, estimates the LOS probability, and compares it with the value given by the formulas in 3GPP TR 38.901 [9]_, Table 7.4.2-1
 
 
 PropagationDelayModel
-*********************
+---------------------
 
 The following propagation delay models are implemented:
 
@@ -894,7 +1289,7 @@
 * RandomPropagationDelayModel
 
 ConstantSpeedPropagationDelayModel
-==================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 In this model, the signal travels with constant speed.
 The delay is calculated according with the transmitter and receiver positions.
@@ -902,17 +1297,17 @@
 Beware that, according to this model, the Earth is flat.
 
 RandomPropagationDelayModel
-===========================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The propagation delay is totally random, and it changes each time the model is called.
 All the packets (even those between two fixed nodes) experience a random delay.
 As a consequence, the packets order is not preserved.
 
 Models for vehicular environments
-*********************************
+---------------------------------
 
-The 3GPP TR 37.885 [37885]_ specifications extends the channel modeling framework
-described in TR 38.901 [38901]_ to simulate wireless channels in vehicular environments.
+The 3GPP TR 37.885 [10]_ specifications extends the channel modeling framework
+described in TR 38.901 [9]_ to simulate wireless channels in vehicular environments.
 The extended framework supports frequencies between 0.5 to 100 GHz and provides
 the possibility to simulate urban and highway propagation environments.
 To do so, new propagation loss and channel condition models, as well as new
@@ -921,7 +1316,7 @@
 .. _sec-3gpp-v2v-ch-cond:
 
 Vehicular channel condition models
-==================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 To properly capture channel dynamics in vehicular environments, three different
 channel conditions have been identified:
@@ -963,7 +1358,7 @@
 increase the simulation time.
 To solve this problem, we implemented two fully-probabilistic models
 that can be used as an alternative to the ones included in TR 37.885.
-These models are based on the work carried out by M. Boban et al. [Boban2016Modeling]_,
+These models are based on the work carried out by M. Boban et al. [11]_,
 which derived a statistical representation of the three channel conditions,
 With the fully-probabilistic models there is no need to determine the presence of blocking
 buildings in a deterministic way, and therefore the computational effort is
@@ -975,9 +1370,9 @@
 The classes implementing the fully-probabilistic models are:
 
   * :cpp:class:`ProbabilisticV2vUrbanChannelConditionModel`: implements the model
-    described in [Boban2016Modeling]_ for the urban scenario.
+    described in M. Boban et al. [11]_ for the urban scenario.
   * :cpp:class:`ProbabilisticV2vHighwayChannelConditionModel`: implements the model
-    described in [Boban2016Modeling]_ for the highway scenario.
+    described in M. Boban et al. [11]_ for the highway scenario.
 
 Both the classes own the attribute "Density", which can be used to select the
 proper value depending on the scenario that have to be simulated.
@@ -990,7 +1385,7 @@
 indoor scenarios.
 
 Vehicular propagation loss models
-=================================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The propagation models described in TR 37.885 determines the attenuation caused
 by path loss and shadowing by considering the propagation environment and the
@@ -1017,7 +1412,7 @@
 .. _sec-3gpp-v2v-ff:
 
 Vehicular fast fading model
-===========================
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The fast fading model described in Sec. 6.2.3 of TR 37.885 is based on the one
 specified in TR 38.901, whose implementation is provided in the ``spectrum`` module
@@ -1043,7 +1438,7 @@
 :math:`v_{scatt} = 0` (by default, the value is set to 0).
 
 Example
-=======
+~~~~~~~
 
 We implemented the example ``three-gpp-v2v-channel-example.cc`` which shows how to
 configure the different classes to simulate wireless propagation in vehicular
@@ -1088,28 +1483,33 @@
 
 
 References
-**********
+----------
+
+.. [1] Friis, H.T., "A Note on a Simple Transmission Formula," Proceedings of the IRE , vol.34, no.5, pp.254,256, May 1946
+
+.. [2] W. C. Jakes, Microwave Mobile Communications. Piscataway, NJ:IEEE Press, 1994.
 
-.. [friis] Friis, H.T., "A Note on a Simple Transmission Formula," Proceedings of the IRE , vol.34, no.5, pp.254,256, May 1946
+.. [3] ZHENG, Yahong Rosa et XIAO, Chengshan. Simulation models with correct statistical properties for Rayleigh fading channels. IEEE Transactions on communications, 2003,
+   vol. 51, no 6, p. 920-928.
 
-.. [hata] M.Hata, "Empirical formula for propagation loss in land mobile radio
+.. [4] M.Hata, "Empirical formula for propagation loss in land mobile radio
    services", IEEE Trans. on Vehicular Technology, vol. 29, pp. 317-325, 1980
 
-.. [cost231] “Digital Mobile Radio: COST 231 View on the Evolution Towards 3rd Generation Systems”, Commission of the European Communities, L-2920, Luxembourg, 1989
+.. [5] “Digital Mobile Radio: COST 231 View on the Evolution Towards 3rd Generation Systems”, Commission of the European Communities, L-2920, Luxembourg, 1989
 
-.. [walfisch]  J.Walfisch and H.L. Bertoni, "A Theoretical model of UHF propagation in urban environments," in IEEE Trans. Antennas Propagat., vol.36, 1988, pp.1788- 1796
+.. [6]  J.Walfisch and H.L. Bertoni, "A Theoretical model of UHF propagation in urban environments," in IEEE Trans. Antennas Propagat., vol.36, 1988, pp.1788- 1796
 
-.. [ikegami] F.Ikegami, T.Takeuchi, and S.Yoshida, "Theoretical prediction of mean field strength for Urban Mobile Radio", in IEEE Trans. Antennas Propagat., Vol.39, No.3, 1991
+.. [7] F.Ikegami, T.Takeuchi, and S.Yoshida, "Theoretical prediction of mean field strength for Urban Mobile Radio", in IEEE Trans. Antennas Propagat., Vol.39, No.3, 1991
 
-.. [kun2600mhz] Sun Kun, Wang Ping, Li Yingze, "Path loss models for suburban scenario at 2.3GHz, 2.6GHz and 3.5GHz",
+.. [8] Sun Kun, Wang Ping, Li Yingze, "Path loss models for suburban scenario at 2.3GHz, 2.6GHz and 3.5GHz",
    in Proc. of the 8th International Symposium on Antennas, Propagation and EM Theory (ISAPE),  Kunming,  China, Nov 2008.
 
-.. [38901] 3GPP. 2018. TR 38.901, Study on channel model for frequencies from 0.5 to 100 GHz, V15.0.0. (2018-06).
+.. [9] 3GPP. 2018. TR 38.901, Study on channel model for frequencies from 0.5 to 100 GHz, V15.0.0. (2018-06).
 
-.. [37885] 3GPP. 2019. TR 37.885, Study on evaluation methodology of new Vehicle-to-Everything (V2X) use cases for LTE and NR, V15.3.0. (2019-06).
+.. [10] 3GPP. 2019. TR 37.885, Study on evaluation methodology of new Vehicle-to-Everything (V2X) use cases for LTE and NR, V15.3.0. (2019-06).
 
-.. [Boban2016Modeling]  M. Boban,  X. Gong, and  W. Xu, “Modeling the evolution
+.. [11]  M. Boban,  X. Gong, and  W. Xu, “Modeling the evolution
    of line-of-sight blockage for V2V channels,” in IEEE 84th Vehicular Technology
    Conference (VTC-Fall), 2016.
 
-.. [38811] 3GPP. 2018. TR 38.811, Study on New Radio (NR) to support non-terrestrial networks, V15.4.0. (2020-09).
+.. [12] 3GPP. 2018. TR 38.811, Study on New Radio (NR) to support non-terrestrial networks, V15.4.0. (2020-09).
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/model/cost231-propagation-loss-model.cc ns-3.47/src/propagation/model/cost231-propagation-loss-model.cc
--- ns-3.46/src/propagation/model/cost231-propagation-loss-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/model/cost231-propagation-loss-model.cc	2026-02-16 16:08:49.811230700 -0800
@@ -39,7 +39,7 @@
                           MakeDoubleAccessor(&Cost231PropagationLossModel::m_lambda),
                           MakeDoubleChecker<double>())
             .AddAttribute("Frequency",
-                          "The Frequency  (default is 2.3 GHz).",
+                          "Carrier Frequency.",
                           DoubleValue(2.3e9),
                           MakeDoubleAccessor(&Cost231PropagationLossModel::m_frequency),
                           MakeDoubleChecker<double>())
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/model/cost231-propagation-loss-model.h ns-3.47/src/propagation/model/cost231-propagation-loss-model.h
--- ns-3.46/src/propagation/model/cost231-propagation-loss-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/model/cost231-propagation-loss-model.h	2026-02-16 16:08:49.811230700 -0800
@@ -35,6 +35,12 @@
  *  Base station Antenna Height: 30m to 200m
  *  Link Distance:up to 20 km
  *
+ *  @note Although the classical COST-231 model has been empirically validated
+ *  in the 1500 MHz to 2000 MHz frequency range, the ns-3 implementation
+ *  historically defaults to 2.3 GHz due to its original use in WiMAX studies.
+ *  Users are advised to consciously select the carrier frequency appropriate
+ *  for their scenario and to exercise caution when operating far outside the
+ *  classical validity range.
  */
 
 class Cost231PropagationLossModel : public PropagationLossModel
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/model/propagation-loss-model.cc ns-3.47/src/propagation/model/propagation-loss-model.cc
--- ns-3.46/src/propagation/model/propagation-loss-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/model/propagation-loss-model.cc	2026-02-16 16:08:49.812230800 -0800
@@ -209,20 +209,6 @@
 }
 
 double
-FriisPropagationLossModel::DbmToW(double dbm) const
-{
-    double mw = std::pow(10.0, dbm / 10.0);
-    return mw / 1000.0;
-}
-
-double
-FriisPropagationLossModel::DbmFromW(double w) const
-{
-    double dbm = std::log10(w * 1000.0) * 10.0;
-    return dbm;
-}
-
-double
 FriisPropagationLossModel::DoCalcRxPower(double txPowerDbm,
                                          Ptr<MobilityModel> a,
                                          Ptr<MobilityModel> b) const
@@ -370,20 +356,6 @@
 }
 
 double
-TwoRayGroundPropagationLossModel::DbmToW(double dbm) const
-{
-    double mw = std::pow(10.0, dbm / 10.0);
-    return mw / 1000.0;
-}
-
-double
-TwoRayGroundPropagationLossModel::DbmFromW(double w) const
-{
-    double dbm = std::log10(w * 1000.0) * 10.0;
-    return dbm;
-}
-
-double
 TwoRayGroundPropagationLossModel::DoCalcRxPower(double txPowerDbm,
                                                 Ptr<MobilityModel> a,
                                                 Ptr<MobilityModel> b) const
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/model/propagation-loss-model.h ns-3.47/src/propagation/model/propagation-loss-model.h
--- ns-3.46/src/propagation/model/propagation-loss-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/model/propagation-loss-model.h	2026-02-16 16:08:49.812230800 -0800
@@ -296,20 +296,6 @@
                          Ptr<MobilityModel> b) const override;
     int64_t DoAssignStreams(int64_t stream) override;
 
-    /**
-     * Transforms a Dbm value to Watt
-     * @param dbm the Dbm value
-     * @return the Watts
-     */
-    double DbmToW(double dbm) const;
-
-    /**
-     * Transforms a Watt value to Dbm
-     * @param w the Watt value
-     * @return the Dbm
-     */
-    double DbmFromW(double w) const;
-
     double m_lambda;     //!< the carrier wavelength
     double m_frequency;  //!< the carrier frequency
     double m_systemLoss; //!< the system loss (linear factor)
@@ -408,20 +394,6 @@
                          Ptr<MobilityModel> b) const override;
     int64_t DoAssignStreams(int64_t stream) override;
 
-    /**
-     * Transforms a Dbm value to Watt
-     * @param dbm the Dbm value
-     * @return the Watts
-     */
-    double DbmToW(double dbm) const;
-
-    /**
-     * Transforms a Watt value to Dbm
-     * @param w the Watt value
-     * @return the Dbm
-     */
-    double DbmFromW(double w) const;
-
     double m_lambda;       //!< the carrier wavelength
     double m_frequency;    //!< the carrier frequency
     double m_systemLoss;   //!< the system loss (linear factor)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/model/three-gpp-propagation-loss-model.cc ns-3.47/src/propagation/model/three-gpp-propagation-loss-model.cc
--- ns-3.46/src/propagation/model/three-gpp-propagation-loss-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/model/three-gpp-propagation-loss-model.cc	2026-02-16 16:08:49.812230800 -0800
@@ -298,7 +298,13 @@
                 "Enable/disable Building Penetration Losses.",
                 BooleanValue(true),
                 MakeBooleanAccessor(&ThreeGppPropagationLossModel::m_buildingPenLossesEnabled),
-                MakeBooleanChecker());
+                MakeBooleanChecker())
+            .AddAttribute("MeanVehicularLoss",
+                          "9dB for standard cars, 20dB for cars with "
+                          "metal coated glass panels.",
+                          DoubleValue(9),
+                          MakeDoubleAccessor(&ThreeGppPropagationLossModel::m_meanVehicleO2iLoss),
+                          MakeDoubleChecker<double>());
     return tid;
 }
 
@@ -317,11 +323,15 @@
 
     m_normalO2iLowLossVar = CreateObject<NormalRandomVariable>();
     m_normalO2iLowLossVar->SetAttribute("Mean", DoubleValue(0));
-    m_normalO2iLowLossVar->SetAttribute("Variance", DoubleValue(4.4));
+    m_normalO2iLowLossVar->SetStdDev(4.4);
 
     m_normalO2iHighLossVar = CreateObject<NormalRandomVariable>();
     m_normalO2iHighLossVar->SetAttribute("Mean", DoubleValue(0));
-    m_normalO2iHighLossVar->SetAttribute("Variance", DoubleValue(6.5));
+    m_normalO2iHighLossVar->SetStdDev(6.5);
+
+    m_normalO2iVehicularLossVar = CreateObject<NormalRandomVariable>();
+    // mean is set via an attribute, so must be set in NotifyConstructionCompleted
+    m_normalO2iVehicularLossVar->SetStdDev(5);
 }
 
 ThreeGppPropagationLossModel::~ThreeGppPropagationLossModel()
@@ -338,6 +348,12 @@
 }
 
 void
+ThreeGppPropagationLossModel::NotifyConstructionCompleted()
+{
+    m_normalO2iVehicularLossVar->SetAttribute("Mean", DoubleValue(m_meanVehicleO2iLoss));
+}
+
+void
 ThreeGppPropagationLossModel::SetChannelConditionModel(Ptr<ChannelConditionModel> model)
 {
     NS_LOG_FUNCTION(this);
@@ -401,16 +417,26 @@
          (cond->GetO2iCondition() == ChannelCondition::O2iConditionValue::I2I &&
           cond->GetLosCondition() == ChannelCondition::LosConditionValue::NLOS)))
     {
-        if (IsO2iLowPenetrationLoss(cond))
+        if (m_frequency < 6e9)
         {
-            rxPow -= GetO2iLowPenetrationLoss(a, b, cond->GetLosCondition());
+            // TR 38.901 has a backward compatibility O2I penetration loss for sub 6GHz channels
+            rxPow -= GetO2iSub6GhzPenetrationLoss(a, b, cond->GetLosCondition());
         }
         else
         {
-            rxPow -= GetO2iHighPenetrationLoss(a, b, cond->GetLosCondition());
+            if (IsO2iLowPenetrationLoss(cond))
+            {
+                rxPow -= GetO2iLowPenetrationLoss(a, b, cond->GetLosCondition());
+            }
+            else
+            {
+                rxPow -= GetO2iHighPenetrationLoss(a, b, cond->GetLosCondition());
+            }
         }
     }
 
+    rxPow -= GetO2iVehicularLoss(a, b, cond->GetO2iCondition());
+
     return rxPow;
 }
 
@@ -441,6 +467,69 @@
 }
 
 double
+ThreeGppPropagationLossModel::GetO2iSub6GhzPenetrationLoss(
+    Ptr<MobilityModel> a,
+    Ptr<MobilityModel> b,
+    ChannelCondition::LosConditionValue cond) const
+{
+    NS_LOG_FUNCTION(this);
+
+    double o2iLossValue = 0;
+    double lossTw = 0;
+    double lossIn = 0;
+    double lossNormalVariate = 0;
+
+    // compute the channel key
+    uint32_t key = GetKey(a, b);
+
+    bool notFound = false;     // indicates if the o2iLoss value has not been computed yet
+    bool newCondition = false; // indicates if the channel condition has changed
+
+    auto it = m_o2iLossMap.end(); // the o2iLoss map iterator
+    if (m_o2iLossMap.find(key) != m_o2iLossMap.end())
+    {
+        // found the o2iLoss value in the map
+        it = m_o2iLossMap.find(key);
+        newCondition = (it->second.m_condition != cond); // true if the condition changed
+    }
+    else
+    {
+        notFound = true;
+        // add a new entry in the map and update the iterator
+        O2iLossMapItem newItem;
+        it = m_o2iLossMap.insert(it, std::make_pair(key, newItem));
+    }
+
+    if (notFound || newCondition)
+    {
+        // distance2dIn is a single, link-specific, uniformly distributed variable
+        // between 0 and 25 m for UMa and UMi-Street Canyon.
+        double distance2dIn = GetO2iDistance2dInSub6Ghz();
+
+        // calculate penetration losses, see TR 38.901 Table 7.4.3-3
+        lossTw = 20;
+
+        // calculate indoor loss
+        lossIn = 0.5 * distance2dIn;
+
+        // calculate low loss standard deviation
+        lossNormalVariate = 0;
+
+        o2iLossValue = lossTw + lossIn + lossNormalVariate;
+
+        // update the entry in the map
+        it->second.m_o2iLoss = o2iLossValue;
+        it->second.m_condition = cond;
+    }
+    else
+    {
+        o2iLossValue = it->second.m_o2iLoss;
+    }
+
+    return o2iLossValue;
+}
+
+double
 ThreeGppPropagationLossModel::GetO2iLowPenetrationLoss(
     Ptr<MobilityModel> a,
     Ptr<MobilityModel> b,
@@ -497,16 +586,16 @@
         lowlossNormalVariate = m_normalO2iLowLossVar->GetValue();
 
         o2iLossValue = lowLossTw + lossIn + lowlossNormalVariate;
+
+        // update the entry in the map
+        it->second.m_o2iLoss = o2iLossValue;
+        it->second.m_condition = cond;
     }
     else
     {
         o2iLossValue = it->second.m_o2iLoss;
     }
 
-    // update the entry in the map
-    it->second.m_o2iLoss = o2iLossValue;
-    it->second.m_condition = cond;
-
     return o2iLossValue;
 }
 
@@ -569,16 +658,16 @@
         highlossNormalVariate = m_normalO2iHighLossVar->GetValue();
 
         o2iLossValue = highLossTw + lossIn + highlossNormalVariate;
+
+        // update the entry in the map
+        it->second.m_o2iLoss = o2iLossValue;
+        it->second.m_condition = cond;
     }
     else
     {
         o2iLossValue = it->second.m_o2iLoss;
     }
 
-    // update the entry in the map
-    it->second.m_o2iLoss = o2iLossValue;
-    it->second.m_condition = cond;
-
     return o2iLossValue;
 }
 
@@ -674,8 +763,8 @@
     m_randomO2iVar2->SetStream(stream + 2);
     m_normalO2iLowLossVar->SetStream(stream + 3);
     m_normalO2iHighLossVar->SetStream(stream + 4);
-
-    return 5;
+    m_normalO2iVehicularLossVar->SetStream(stream + 5);
+    return 6;
 }
 
 double
@@ -718,6 +807,67 @@
     }
 }
 
+double
+ThreeGppPropagationLossModel::GetO2iDistance2dInSub6Ghz() const
+{
+    return 0;
+}
+
+void
+ThreeGppPropagationLossModel::ClearO2iLossCacheMap()
+{
+    NS_LOG_FUNCTION(this);
+    m_o2iLossMap.clear();
+    m_o2iVehicularUtLossMap.clear();
+}
+
+double
+ThreeGppPropagationLossModel::GetO2iVehicularLoss(Ptr<MobilityModel> a,
+                                                  Ptr<MobilityModel> b,
+                                                  ChannelCondition::O2iConditionValue cond) const
+{
+    NS_LOG_FUNCTION(this);
+    double o2iVehicularLoss = 0.0;
+
+    // O2I penetration loss for vehicles only applies between 600 MHz and 60 GHz.
+    if (m_frequency >= 0.6e9 && m_frequency < 60e9)
+    {
+        for (const auto& mob : {a, b})
+        {
+            auto velocityKmH = mob->GetVelocity().GetLength() * 3.6;
+            auto idNode = mob->GetObject<Node>()->GetId();
+            // Slow nodes are considered as pedestrians (no loss)
+            if (velocityKmH < 30)
+            {
+                m_o2iVehicularUtLossMap[idNode] = 0.0;
+            }
+            // Fast nodes are considered vehicles.
+            // Create one loss per terminal, and reuse throughout simulation.
+            else if (velocityKmH >= 30 && velocityKmH <= 120)
+            {
+                if (m_o2iVehicularUtLossMap.find(idNode) == m_o2iVehicularUtLossMap.end())
+                {
+                    m_o2iVehicularUtLossMap[idNode] = m_normalO2iVehicularLossVar->GetValue();
+                }
+            }
+            // Very fast nodes are not modeled
+            else
+            {
+                m_o2iVehicularUtLossMap[idNode] = 0.0;
+                NS_LOG_WARN(
+                    "O2I loss for high-speed (>120 km/h) transit and satellites not implemented");
+            }
+
+            // Sum vehicle individual losses
+            if (cond == ChannelCondition::O2iConditionValue::O2O)
+            {
+                o2iVehicularLoss += m_o2iVehicularUtLossMap.at(idNode);
+            }
+        }
+    }
+    return o2iVehicularLoss;
+}
+
 // ------------------------------------------------------------------------- //
 
 NS_OBJECT_ENSURE_REGISTERED(ThreeGppRmaPropagationLossModel);
@@ -1099,6 +1249,13 @@
 }
 
 double
+ThreeGppUmaPropagationLossModel::GetO2iDistance2dInSub6Ghz() const
+{
+    // distance2dIn is a single, link-specific, uniformly distributed variable between 0 and 25 m.
+    return m_randomO2iVar1->GetValue(0, 25);
+}
+
+double
 ThreeGppUmaPropagationLossModel::GetLossNlos(Ptr<MobilityModel> a, Ptr<MobilityModel> b) const
 {
     NS_LOG_FUNCTION(this);
@@ -1150,20 +1307,25 @@
 {
     NS_LOG_FUNCTION(this);
     double shadowingStd;
-
-    if (cond == ChannelCondition::LosConditionValue::LOS)
-    {
-        shadowingStd = 4.0;
-    }
-    else if (cond == ChannelCondition::LosConditionValue::NLOS)
+    if (m_frequency < 6e9)
     {
-        shadowingStd = 6.0;
+        shadowingStd = 7.0;
     }
     else
     {
-        NS_FATAL_ERROR("Unknown channel condition");
+        if (cond == ChannelCondition::LosConditionValue::LOS)
+        {
+            shadowingStd = 4.0;
+        }
+        else if (cond == ChannelCondition::LosConditionValue::NLOS)
+        {
+            shadowingStd = 6.0;
+        }
+        else
+        {
+            NS_FATAL_ERROR("Unknown channel condition");
+        }
     }
-
     return shadowingStd;
 }
 
@@ -1196,9 +1358,9 @@
 {
     NS_LOG_FUNCTION(this);
 
-    m_normRandomVariable->SetStream(stream);
-    m_uniformVar->SetStream(stream + 1);
-    return 2;
+    int64_t streams = ThreeGppPropagationLossModel::DoAssignStreams(stream);
+    m_uniformVar->SetStream(stream + streams);
+    return streams + 1;
 }
 
 // ------------------------------------------------------------------------- //
@@ -1254,6 +1416,13 @@
 }
 
 double
+ThreeGppUmiStreetCanyonPropagationLossModel::GetO2iDistance2dInSub6Ghz() const
+{
+    // distance2dIn is a single, link-specific, uniformly distributed variable between 0 and 25 m.
+    return m_randomO2iVar1->GetValue(0, 25);
+}
+
+double
 ThreeGppUmiStreetCanyonPropagationLossModel::GetLossLos(Ptr<MobilityModel> a,
                                                         Ptr<MobilityModel> b) const
 {
@@ -1374,19 +1543,25 @@
     NS_LOG_FUNCTION(this);
     double shadowingStd;
 
-    if (cond == ChannelCondition::LosConditionValue::LOS)
-    {
-        shadowingStd = 4.0;
-    }
-    else if (cond == ChannelCondition::LosConditionValue::NLOS)
+    if (m_frequency < 6e9)
     {
-        shadowingStd = 7.82;
+        shadowingStd = 7.0;
     }
     else
     {
-        NS_FATAL_ERROR("Unknown channel condition");
+        if (cond == ChannelCondition::LosConditionValue::LOS)
+        {
+            shadowingStd = 4.0;
+        }
+        else if (cond == ChannelCondition::LosConditionValue::NLOS)
+        {
+            shadowingStd = 7.82;
+        }
+        else
+        {
+            NS_FATAL_ERROR("Unknown channel condition");
+        }
     }
-
     return shadowingStd;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/model/three-gpp-propagation-loss-model.h ns-3.47/src/propagation/model/three-gpp-propagation-loss-model.h
--- ns-3.46/src/propagation/model/three-gpp-propagation-loss-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/model/three-gpp-propagation-loss-model.h	2026-02-16 16:08:49.812230800 -0800
@@ -75,6 +75,13 @@
      */
     bool IsO2iLowPenetrationLoss(Ptr<const ChannelCondition> cond) const;
 
+    /**
+     * @brief Clear cached O2I Building Penetration loss.
+     *        Triggers a recomputation of losses.
+     *        Used for testing.
+     */
+    void ClearO2iLossCacheMap();
+
   private:
     /**
      * Computes the received power by applying the pathloss model described in
@@ -89,8 +96,6 @@
                          Ptr<MobilityModel> a,
                          Ptr<MobilityModel> b) const override;
 
-    int64_t DoAssignStreams(int64_t stream) override;
-
     /**
      * @brief Computes the pathloss between a and b
      * @param cond the channel condition
@@ -138,6 +143,22 @@
      *        independent realization and stores it in the map, otherwise it calculates
      *        a new value as defined in 3GPP TR 38.901 7.4.3.1.
      *
+     *
+     * @param a tx mobility model (used for the key calculation)
+     * @param b rx mobility model (used for the key calculation)
+     * @param cond the LOS/NLOS channel condition
+     * @return o2iLoss
+     */
+    double GetO2iSub6GhzPenetrationLoss(Ptr<MobilityModel> a,
+                                        Ptr<MobilityModel> b,
+                                        ChannelCondition::LosConditionValue cond) const;
+
+    /**
+     * @brief Retrieves the o2i building penetration loss value by looking at m_o2iLossMap.
+     *        If not found or if the channel condition changed it generates a new
+     *        independent realization and stores it in the map, otherwise it calculates
+     *        a new value as defined in 3GPP TR 38.901 7.4.3.1.
+     *
      *        Note that all child classes should implement this function to support
      *        low losses calculation. As such, this function should be purely virtual.
      *
@@ -169,6 +190,20 @@
                                              ChannelCondition::LosConditionValue cond) const;
 
     /**
+     * @brief Computes the o2i vehicular penetration loss by looking at m_o2iVehicularUtLossMap.
+     *        The loss is composed by the sum of losses for each UT, if vehicles,
+     *        as implied by their speed (30-120 km/h).
+     *        The loss for each UT is defined in 3GPP TR 38.901 7.4.3.2.
+     *
+     * @param a tx mobility model (used for the key calculation)
+     * @param b rx mobility model (used for the key calculation)
+     * @param cond the O2I channel condition (O2I, O2O)
+     * @return vehicular o2iLoss
+     */
+    double GetO2iVehicularLoss(Ptr<MobilityModel> a,
+                               Ptr<MobilityModel> b,
+                               ChannelCondition::O2iConditionValue cond) const;
+    /**
      * @brief Indicates the condition of the o2i building penetration loss
      *        (defined in 3GPP TR 38.901 7.4.3.1).
      *        The default implementation returns the condition as set
@@ -253,6 +288,18 @@
 
   protected:
     void DoDispose() override;
+    void NotifyConstructionCompleted() override;
+    /**
+     * @brief Returns a a single, link-specific, uniformly distributed variable
+     *        value depending on the specific 3GPP scenario (UMa, UMi-Street Canyon, RMa),
+     *        i.e., between 0 and 25 m for UMa and UMi-Street Canyon.
+     *        According to 3GPP TR 38.901 this 2D−in distance shall be UT-specifically
+     *        generated. 2D−in distance is used for the O2I penetration losses
+     *        calculation according to 3GPP TR 38.901 7.4.3.
+     *        See GetO2iLowPenetrationLoss/GetO2iHighPenetrationLoss functions.
+     * @return Returns 02i 2D distance (in meters) used to calculate low/high losses.
+     */
+    virtual double GetO2iDistance2dInSub6Ghz() const;
 
     /**
      * @brief Computes the 2D distance between two 3D vectors
@@ -265,8 +312,9 @@
     Ptr<ChannelConditionModel> m_channelConditionModel; //!< pointer to the channel condition model
     double m_frequency;                                 //!< operating frequency in Hz
     bool m_shadowingEnabled;                            //!< enable/disable shadowing
-    bool m_enforceRanges;                           //!< strictly enforce TR 38.901 parameter ranges
-    bool m_buildingPenLossesEnabled;                //!< enable/disable building penetration losses
+    bool m_enforceRanges;            //!< strictly enforce TR 38.901 parameter ranges
+    bool m_buildingPenLossesEnabled; //!< enable/disable building penetration losses
+    double m_meanVehicleO2iLoss; //!< normal cars (9dB), cars with metal coated glass panels (20dB)
     Ptr<NormalRandomVariable> m_normRandomVariable; //!< normal random variable
 
     /** Define a struct for the m_shadowingMap entries */
@@ -290,6 +338,11 @@
     mutable std::unordered_map<uint32_t, O2iLossMapItem>
         m_o2iLossMap; //!< map to store the o2i Loss values
 
+    mutable std::unordered_map<uint32_t, double>
+        m_o2iVehicularUtLossMap; //!< vehicular O2I loss for each individual UT with speed x, such
+                                 //!< that 30 < x <= 120 km/h.
+                                 //!< UT is addressed by NodeId
+
     Ptr<UniformRandomVariable> m_randomO2iVar1; //!< a uniform random variable for the calculation
                                                 //!< of the indoor loss, see TR38.901 Table 7.4.3-2
     Ptr<UniformRandomVariable> m_randomO2iVar2; //!< a uniform random variable for the calculation
@@ -300,6 +353,11 @@
     Ptr<NormalRandomVariable>
         m_normalO2iHighLossVar; //!< a normal random variable for the calculation of 02i high loss,
                                 //!< see TR38.901 Table 7.4.3-2
+
+    Ptr<NormalRandomVariable>
+        m_normalO2iVehicularLossVar; //!< a normal random variable for the calculation of
+                                     //!< penetration loss for vehicles see TR38.901 section 7.4.3.2
+    int64_t DoAssignStreams(int64_t stream) override;
 };
 
 /**
@@ -469,6 +527,18 @@
     double GetO2iDistance2dIn() const override;
 
     /**
+     * @brief Returns a a single, link-specific, uniformly distributed variable
+     *        value depending on the specific 3GPP scenario (UMa, UMi-Street Canyon, RMa),
+     *        i.e., between 0 and 25 m for UMa and UMi-Street Canyon.
+     *        According to 3GPP TR 38.901 this 2D−in distance shall be UT-specifically
+     *        generated. 2D−in distance is used for the O2I penetration losses
+     *        calculation according to 3GPP TR 38.901 7.4.3.
+     *        See GetO2iLowPenetrationLoss/GetO2iHighPenetrationLoss functions.
+     * @return Returns 02i 2D distance (in meters) used to calculate low/high losses.
+     */
+    double GetO2iDistance2dInSub6Ghz() const override;
+
+    /**
      * @brief Computes the pathloss between a and b considering that the line of
      *        sight is obstructed.
      * @param a tx mobility model
@@ -564,6 +634,18 @@
     double GetO2iDistance2dIn() const override;
 
     /**
+     * @brief Returns a a single, link-specific, uniformly distributed variable
+     *        value depending on the specific 3GPP scenario (UMa, UMi-Street Canyon, RMa),
+     *        i.e., between 0 and 25 m for UMa and UMi-Street Canyon.
+     *        According to 3GPP TR 38.901 this 2D−in distance shall be UT-specifically
+     *        generated. 2D−in distance is used for the O2I penetration losses
+     *        calculation according to 3GPP TR 38.901 7.4.3.
+     *        See GetO2iLowPenetrationLoss/GetO2iHighPenetrationLoss functions.
+     * @return Returns 02i 2D distance (in meters) used to calculate low/high losses.
+     */
+    double GetO2iDistance2dInSub6Ghz() const override;
+
+    /**
      * @brief Computes the pathloss between a and b considering that the line of
      *        sight is obstructed.
      * @param a tx mobility model
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/propagation/test/three-gpp-propagation-loss-model-test-suite.cc ns-3.47/src/propagation/test/three-gpp-propagation-loss-model-test-suite.cc
--- ns-3.46/src/propagation/test/three-gpp-propagation-loss-model-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/propagation/test/three-gpp-propagation-loss-model-test-suite.cc	2026-02-16 16:08:49.813230800 -0800
@@ -1008,14 +1008,16 @@
      * @param hBs the BS height in meters
      * @param hUt the UT height in meters
      * @param distance the initial distance between the BS and the UT
-     * @param shadowingEnabled true if shadowging must be enabled
+     * @param shadowingEnabled true if shadowing must be enabled
+     * @param frequency channel frequency
      */
     void RunTest(uint16_t testNum,
                  std::string propagationLossModelType,
                  double hBs,
                  double hUt,
                  double distance,
-                 bool shadowingEnabled);
+                 bool shadowingEnabled,
+                 double frequency);
 
     /**
      * Compute the propagation loss
@@ -1023,7 +1025,7 @@
      * @param b the second mobility model
      * @param testNum the index of the experiment
      */
-    void EvaluateLoss(Ptr<MobilityModel> a, Ptr<MobilityModel> b, uint8_t testNum);
+    void EvaluateLoss(Ptr<MobilityModel> a, Ptr<MobilityModel> b, uint16_t testNum);
 
     /**
      * Change the channel condition model
@@ -1044,6 +1046,7 @@
                                   //!< case in dB
         double m_shadowingStdNlos; //!< the standard deviation of the shadowing component in the
                                    //!< NLOS case in dB
+        double m_frequency;        //!< channel frequency
     };
 
     TestVectors<TestVector> m_testVectors;         //!< array containing all the test vectors
@@ -1062,7 +1065,9 @@
 }
 
 void
-ThreeGppShadowingTestCase::EvaluateLoss(Ptr<MobilityModel> a, Ptr<MobilityModel> b, uint8_t testNum)
+ThreeGppShadowingTestCase::EvaluateLoss(Ptr<MobilityModel> a,
+                                        Ptr<MobilityModel> b,
+                                        uint16_t testNum)
 {
     double loss = m_lossModel->CalcRxPower(0, a, b);
     m_results.at(testNum).push_back(loss);
@@ -1080,7 +1085,8 @@
                                    double hBs,
                                    double hUt,
                                    double distance,
-                                   bool shadowingEnabled)
+                                   bool shadowingEnabled,
+                                   double frequency)
 {
     // Add a new entry for this test in the results map
     m_results[testNum] = std::vector<double>();
@@ -1102,9 +1108,10 @@
     // Create the propagation loss model
     ObjectFactory propagationLossModelFactory = ObjectFactory(propagationLossModelType);
     m_lossModel = propagationLossModelFactory.Create<ThreeGppPropagationLossModel>();
-    m_lossModel->SetAttribute("Frequency", DoubleValue(3.5e9));
+    m_lossModel->SetAttribute("Frequency", DoubleValue(frequency));
     m_lossModel->SetAttribute("ShadowingEnabled",
                               BooleanValue(shadowingEnabled)); // enable the shadow fading
+    m_lossModel->AssignStreams(testNum);
 
     // Set the channel condition to LOS
     Ptr<ChannelConditionModel> losCondModel = CreateObject<AlwaysLosChannelConditionModel>();
@@ -1163,6 +1170,9 @@
     // RMa, UMa, UMi and Indoor-Office.
 
     TestVector testVector;
+
+    // Above 6GHz
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppRmaPropagationLossModel";
     testVector.m_hBs = 25;
     testVector.m_hUt = 1.6;
@@ -1171,6 +1181,7 @@
     testVector.m_shadowingStdNlos = 8;
     m_testVectors.Add(testVector);
 
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppRmaPropagationLossModel";
     testVector.m_hBs = 25;
     testVector.m_hUt = 1.6;
@@ -1179,6 +1190,7 @@
     testVector.m_shadowingStdNlos = 8;
     m_testVectors.Add(testVector);
 
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppUmaPropagationLossModel";
     testVector.m_hBs = 25;
     testVector.m_hUt = 1.6;
@@ -1187,6 +1199,7 @@
     testVector.m_shadowingStdNlos = 6;
     m_testVectors.Add(testVector);
 
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppUmiStreetCanyonPropagationLossModel";
     testVector.m_hBs = 10;
     testVector.m_hUt = 1.6;
@@ -1195,6 +1208,7 @@
     testVector.m_shadowingStdNlos = 7.82;
     m_testVectors.Add(testVector);
 
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppIndoorOfficePropagationLossModel";
     testVector.m_hBs = 3;
     testVector.m_hUt = 1;
@@ -1203,6 +1217,7 @@
     testVector.m_shadowingStdNlos = 8.03;
     m_testVectors.Add(testVector);
 
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppV2vUrbanPropagationLossModel";
     testVector.m_hBs = 1.6;
     testVector.m_hUt = 1.6;
@@ -1211,6 +1226,7 @@
     testVector.m_shadowingStdNlos = 4;
     m_testVectors.Add(testVector);
 
+    testVector.m_frequency = 6e9;
     testVector.m_propagationLossModelType = "ns3::ThreeGppV2vHighwayPropagationLossModel";
     testVector.m_hBs = 1.6;
     testVector.m_hUt = 1.6;
@@ -1219,7 +1235,26 @@
     testVector.m_shadowingStdNlos = 4;
     m_testVectors.Add(testVector);
 
-    uint16_t numSamples = 250;
+    // Sub 6GHz
+    testVector.m_frequency = 3.5e9;
+    testVector.m_propagationLossModelType = "ns3::ThreeGppUmaPropagationLossModel";
+    testVector.m_hBs = 25;
+    testVector.m_hUt = 1.6;
+    testVector.m_distance = 100;
+    testVector.m_shadowingStdLos = 7;
+    testVector.m_shadowingStdNlos = 7;
+    m_testVectors.Add(testVector);
+
+    testVector.m_frequency = 3.5e9;
+    testVector.m_propagationLossModelType = "ns3::ThreeGppUmiStreetCanyonPropagationLossModel";
+    testVector.m_hBs = 10;
+    testVector.m_hUt = 1.6;
+    testVector.m_distance = 100;
+    testVector.m_shadowingStdLos = 7;
+    testVector.m_shadowingStdNlos = 7;
+    m_testVectors.Add(testVector);
+
+    uint16_t numSamples = 400;
 
     for (std::size_t tvIndex = 0; tvIndex < m_testVectors.GetN(); tvIndex++)
     {
@@ -1233,12 +1268,13 @@
                     tv.m_hBs,
                     tv.m_hUt,
                     tv.m_distance,
-                    true);
+                    true,
+                    tv.m_frequency);
         }
 
         // analyze the results
-        std::vector<double> mean_vector; // the vector containing the mean propagation loss for each
-                                         // position (sample mean)
+        std::vector<double> mean_vector; // the vector containing the mean propagation loss for
+                                         // each position (sample mean)
 
         uint16_t numPositions = m_results.at(0).size();
         for (uint16_t k = 0; k < numPositions; k++)
@@ -1259,7 +1295,8 @@
                 tv.m_hBs,
                 tv.m_hUt,
                 tv.m_distance,
-                false);
+                false,
+                tv.m_frequency);
         std::vector<double> true_mean =
             m_results.at(numSamples); // the result of the last test is the true mean
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/sixlowpan/examples/example-ping-lr-wpan.cc ns-3.47/src/sixlowpan/examples/example-ping-lr-wpan.cc
--- ns-3.46/src/sixlowpan/examples/example-ping-lr-wpan.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/sixlowpan/examples/example-ping-lr-wpan.cc	2026-02-16 16:08:49.813230800 -0800
@@ -19,6 +19,8 @@
 
 using namespace ns3;
 
+NS_LOG_COMPONENT_DEFINE("ExamplePingLrWpan");
+
 int
 main(int argc, char** argv)
 {
@@ -26,6 +28,7 @@
     bool disablePcap = false;
     bool disableAsciiTrace = false;
     bool enableLSixlowLogLevelInfo = false;
+    int64_t streamNumber = 1;
 
     CommandLine cmd(__FILE__);
     cmd.AddValue("verbose", "turn on log components", verbose);
@@ -71,6 +74,12 @@
     lrWpanHelper.AddPropagationLossModel("ns3::LogDistancePropagationLossModel");
     // Add and install the LrWpanNetDevice for each node
     NetDeviceContainer lrwpanDevices = lrWpanHelper.Install(nodes);
+    auto streamsUsed = lrWpanHelper.AssignStreams(lrwpanDevices, streamNumber);
+    NS_LOG_DEBUG("Random variable streams used by lrWpanHelper: " << streamsUsed);
+    streamNumber += 1000; // Increment the stream number without chaining the output
+    streamsUsed = lrWpanHelper.GetChannel()->AssignStreams(streamNumber);
+    NS_LOG_DEBUG("Random variable streams used by spectrum channel: " << streamsUsed);
+    streamNumber += 1000;
 
     // Manual PAN association and extended and short address assignment.
     // Association using the MAC functions can also be used instead of this step.
@@ -78,9 +87,15 @@
 
     InternetStackHelper internetv6;
     internetv6.Install(nodes);
+    streamsUsed = internetv6.AssignStreams(nodes, streamNumber);
+    NS_LOG_DEBUG("Random variable streams used by internetv6: " << streamsUsed);
+    streamNumber += 1000;
 
     SixLowPanHelper sixlowpan;
     NetDeviceContainer devices = sixlowpan.Install(lrwpanDevices);
+    streamsUsed = sixlowpan.AssignStreams(devices, streamNumber);
+    NS_LOG_DEBUG("Random variable streams used by sixlowpan: " << streamsUsed);
+    streamNumber += 1000;
 
     Ipv6AddressHelper ipv6;
     ipv6.SetBase(Ipv6Address("2001:2::"), Ipv6Prefix(64));
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/sixlowpan/model/sixlowpan-net-device.cc ns-3.47/src/sixlowpan/model/sixlowpan-net-device.cc
--- ns-3.46/src/sixlowpan/model/sixlowpan-net-device.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/sixlowpan/model/sixlowpan-net-device.cc	2026-02-16 16:08:49.814230700 -0800
@@ -1067,7 +1067,7 @@
         iphcHeader.SetSac(false);
         iphcHeader.SetDac(false);
 
-        Ipv6Address checker = Ipv6Address("fe80:0000:0000:0000:0000:00ff:fe00:1");
+        auto checker = Ipv6Address("fe80:0000:0000:0000:0000:00ff:fe00:1");
         uint8_t unicastAddrCheckerBuf[16];
         checker.GetBytes(unicastAddrCheckerBuf);
         uint8_t addressBuf[16];
@@ -1292,7 +1292,7 @@
                     // Stateless compression
 
                     uint8_t multicastAddrCheckerBuf[16];
-                    Ipv6Address multicastCheckAddress = Ipv6Address("ff02::1");
+                    auto multicastCheckAddress = Ipv6Address("ff02::1");
                     multicastCheckAddress.GetBytes(multicastAddrCheckerBuf);
 
                     // The address takes the form ff02::00XX.
@@ -2450,7 +2450,7 @@
                                                    fragNHeader.GetDatagramTag());
     }
 
-    Ptr<Fragments> fragments;
+    std::shared_ptr<Fragments> fragments;
 
     auto it = m_fragments.find(key);
     if (it == m_fragments.end())
@@ -2472,7 +2472,7 @@
             m_fragments[oldestKey] = nullptr;
             m_fragments.erase(oldestKey);
         }
-        fragments = Create<Fragments>();
+        fragments = std::make_shared<Fragments>();
         fragments->SetPacketSize(packetSize);
         m_fragments.insert(std::make_pair(key, fragments));
         uint32_t ifIndex = GetIfIndex();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/sixlowpan/model/sixlowpan-net-device.h ns-3.47/src/sixlowpan/model/sixlowpan-net-device.h
--- ns-3.46/src/sixlowpan/model/sixlowpan-net-device.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/sixlowpan/model/sixlowpan-net-device.h	2026-02-16 16:08:49.814230700 -0800
@@ -460,7 +460,7 @@
     /**
      * @brief A Set of Fragments.
      */
-    class Fragments : public SimpleRefCount<Fragments>
+    class Fragments
     {
       public:
         /**
@@ -595,7 +595,7 @@
     /**
      * Container for fragment key -> fragments.
      */
-    typedef std::map<FragmentKey_t, Ptr<Fragments>> MapFragments_t;
+    typedef std::map<FragmentKey_t, std::shared_ptr<Fragments>> MapFragments_t;
 
     MapFragments_t m_fragments;       //!< Fragments hold to be rebuilt.
     Time m_fragmentExpirationTimeout; //!< Time limit for fragment rebuilding.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/sixlowpan/test/sixlowpan-example-ping-lr-wpan.reflog ns-3.47/src/sixlowpan/test/sixlowpan-example-ping-lr-wpan.reflog
--- ns-3.46/src/sixlowpan/test/sixlowpan-example-ping-lr-wpan.reflog	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/sixlowpan/test/sixlowpan-example-ping-lr-wpan.reflog	2026-02-16 16:08:49.814230700 -0800
@@ -1,16 +1,16 @@
 Device 0: pseudo-Mac-48 02:00:00:00:00:01, IPv6 Address 2001:2::ff:fe00:1
 Device 1: pseudo-Mac-48 02:00:00:00:00:02, IPv6 Address 2001:2::ff:fe00:2
 PING 2001:2::ff:fe00:2 - 16 bytes of data; 64 bytes including ICMP and IPv6 headers.
-24 bytes from (2001:2::ff:fe00:2): icmp_seq=0 ttl=64 time=21.216 ms
-24 bytes from (2001:2::ff:fe00:2): icmp_seq=1 ttl=64 time=7.712 ms
-24 bytes from (2001:2::ff:fe00:2): icmp_seq=2 ttl=64 time=7.712 ms
-24 bytes from (2001:2::ff:fe00:2): icmp_seq=3 ttl=64 time=7.392 ms
-24 bytes from (2001:2::ff:fe00:2): icmp_seq=4 ttl=64 time=10.592 ms
+24 bytes from (2001:2::ff:fe00:2): icmp_seq=0 ttl=64 time=22.576 ms
+24 bytes from (2001:2::ff:fe00:2): icmp_seq=1 ttl=64 time=9.312 ms
+24 bytes from (2001:2::ff:fe00:2): icmp_seq=2 ttl=64 time=10.272 ms
+24 bytes from (2001:2::ff:fe00:2): icmp_seq=3 ttl=64 time=10.272 ms
+24 bytes from (2001:2::ff:fe00:2): icmp_seq=4 ttl=64 time=8.992 ms
 
 --- 2001:2::ff:fe00:2 ping statistics ---
-5 packets transmitted, 5 received, 0% packet loss, time 4010ms
-rtt min/avg/max/mdev = 7/10.4/21/6.066 ms
+5 packets transmitted, 5 received, 0% packet loss, time 4008ms
+rtt min/avg/max/mdev = 8/11.8/22/5.762 ms
 NDISC Cache of node 0 at time +9s
-2001:2::ff:fe00:2 dev 2 lladdr 00-06-02:00:00:00:00:02 REACHABLE
+2001:2::ff:fe00:2 dev 2 lladdr 01-06-02:00:00:00:00:02 REACHABLE
 NDISC Cache of node 1 at time +9s
-2001:2::ff:fe00:1 dev 2 lladdr 00-06-02:00:00:00:00:01 REACHABLE
+2001:2::ff:fe00:1 dev 2 lladdr 01-06-02:00:00:00:00:01 REACHABLE
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/doc/spectrum.rst ns-3.47/src/spectrum/doc/spectrum.rst
--- ns-3.46/src/spectrum/doc/spectrum.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/doc/spectrum.rst	2026-02-16 16:08:49.819230800 -0800
@@ -635,8 +635,8 @@
 antenna. H :sub:`u,s` :math:`(t,\tau)` is generated by the superposition of N different multi-path
 components, called clusters, each of which composed of M different rays.
 The channel matrix generation procedure accounts for large and small scale
-propagation phenomena. The classes ThreeGppSpectrumPropagationLossModel and
-ThreeGppChannelModel included in the spectrum module take care of the generation
+propagation phenomena. The classes ``ThreeGppSpectrumPropagationLossModel`` and
+``ThreeGppChannelModel`` included in the spectrum module take care of the generation
 of the channel coefficients and the computation of the frequency-dependent
 propagation loss.
 
@@ -655,72 +655,78 @@
 parameters, are saved and used under the assumption that, within a pair, the
 definition of the "s" and "u" node will always be the same. For more details,
 please have a look at the documentation of the classes
-ThreeGppChannelModel and ThreeGppSpectrumPropagationLossModel.
+``ThreeGppChannelModel`` and ``ThreeGppSpectrumPropagationLossModel``.
 
 **Note:**
 
-  * Currently, no error model is provided; a link-to-system campaign may be
-    needed to incorporate it in existing modules.
-
-  * The model does not include any spatial consistency update procedure
-    (see [TR38901]_, Sec. 7.6.1). The implementation of this feature is left
-    as future work.
+  * Since ns-3.47, the model includes an update technique aligned with the
+    3GPP TR 38.901 spatial-consistency procedure (see [TR38901]_, Sec. 7.6.3),
+    based on Procedure A (Sec. 7.6.3.2). The channel-parameter evolution follows
+    the Procedure A update equations to preserve correlation across consecutive
+    channel evaluations, while the criteria for *when* to apply an update are adapted
+    for ns-3’s discrete-event operation (i.e., updates are triggered when the
+    channel is evaluated and the configured ``UpdatePeriod`` has elapsed; if the
+    maximum distance the BS and/or UT traveled between evaluations exceeds
+    the 1 m step-size constraint, the model falls back to re-generating channel
+    parameters and thus the channel matrix). Drop-based spatial consistency across
+    multiple initial locations (Sec. 7.6.3.1) and Procedure B are not implemented.
 
   * Issue regarding the blockage model: according to 3GPP TR 38.901 v15.0.0
     (2018-06) section 7.6.4.1, the blocking region for self-blocking is provided
-    in LCS.
-
-    However, here, clusterAOA and clusterZOA are in GCS and blocking check is
+    in LCS. However, here, clusterAOA and clusterZOA are in GCS and blocking check is
     performed for self-blocking similar to non-self blocking, that is in GCS.
     One would expect the angles to be transposed to LCS before checking
     self-blockage.
 
+  * No error model is provided in this module; a link-to-system campaign may be
+    needed to incorporate it in existing modules.
+
 
 ThreeGppSpectrumPropagationLossModel
 ####################################
 
-The class ThreeGppSpectrumPropagationLossModel implements the
-PhasedArraySpectrumPropagationLossModel interface and enables the modeling of frequency
+The class ``ThreeGppSpectrumPropagationLossModel`` implements the
+``PhasedArraySpectrumPropagationLossModel`` interface and enables the modeling of frequency
 dependent propagation phenomena while taking into account the specific pair of the
 phased antenna array at the transmitter and the receiver. The main method is
-DoCalcRxPowerSpectralDensity, which takes as input the SpectrumSignalParameters
+``DoCalcRxPowerSpectralDensity``, which takes as input the SpectrumSignalParameters
 structure, which contains, among others, the power spectral density (PSD) of the
 transmitted signal, and the precoding matrix of the transmitter which is needed
 for MIMO computations. Other input parameters are the mobility models of the
 transmitting and receiving node, and the phased antenna array of the
 transmitting and receiving node.
-DoCalcRxPowerSpectralDensity computes the PSD of the received signal
+``DoCalcRxPowerSpectralDensity`` computes the PSD of the received signal
 (used in case of single stream transmission), and the frequency domain 3D
 channel matrix between receive and transmit antenna ports (needed for MIMO
 computations when multiple streams are transmitted).
-Finally, it returns the SpectrumSignalParameters which contains the
+Finally, it returns the ``SpectrumSignalParameters`` which contains the
 received PSD and the frequency domain 3D spectrum channel matrix.
 
 Procedure used to compute the PSD of the received signal and the frequency domain 3D
 spectrum channel matrix:
 
 1. Retrieve the beamforming vectors
-To account for the beamforming, ThreeGppSpectrumPropagationLossModel has to
+To account for the beamforming, ``ThreeGppSpectrumPropagationLossModel`` has to
 retrieve the beamforming vectors of the transmitting and receiving antennas.
-The method DoCalcRxPowerSpectralDensity uses the antenna objects
+The method ``DoCalcRxPowerSpectralDensity`` uses the antenna objects
 that are passed as parameters for both the transmitting and receiving devices,
 and calls the method GetBeamformingVector to retrieve the beamforming vectors
 of these antenna objects.
 
 2. Retrieve the channel matrix and the channel params
-The ThreeGppSpectrumPropagationLossModel relies on the ThreeGppChannelModel class
+The ``ThreeGppSpectrumPropagationLossModel`` relies on the ``ThreeGppChannelModel`` class
 to obtain the channel matrix and channel parameters.
-In particular, it makes use of the method GetChannel,
-which returns a ChannelMatrix object containing the channel
+In particular, it makes use of the method ``GetChannel``,
+which returns a ``ChannelMatrix`` object containing the channel
 matrix, the generation time, the node pair, and the phased antenna array pair among
 which is created this channel matrix.
-Additionally, it makes use of the method GetParams which
-returns a ChannelParams object containing the channel parameters.
+Additionally, it makes use of the method ``GetParams`` which
+returns a ``ChannelParams`` object containing the channel parameters.
 Notice that the channel information is split into these two structures
 (ChannelMatrix and ChannelParams) to support multiple collocated phased antenna arrays at
-TX/RX node. ChannelParams (also its specialization ThreeGppChannelParams structure)
+TX/RX node. ``ChannelParams`` (also its specialization ``ThreeGppChannelParams`` structure)
 contains parameters which are common for all the channels among
-the same RX/TX node pair, while ChannelMatrix contains the channel matrix for the specific pair
+the same RX/TX node pair, while ``ChannelMatrix`` contains the channel matrix for the specific pair
 of the phased antenna arrays of TX/RX nodes.
 For example, if the TX and the RX node have multiple collocated antenna arrays,
 then there will be multiple channel matrices among this pair of nodes for different pairs
@@ -731,11 +737,11 @@
 On the other hand, each pair of TX and RX antenna arrays has a specific channel matrix
 and fading, which depends on the actual antenna element positions and field patterns of
 each pair of antenna array subpartitions.
-The ThreeGppChannelModel instance is automatically
-created in the ThreeGppSpectrumPropagationLossModel constructor and it can
+The ``ThreeGppChannelModel`` instance is automatically
+created in the ``ThreeGppSpectrumPropagationLossModel`` constructor and it can
 be configured by using the method SetChannelModelAttribute.
 
-Notice that in MultiModelSpectrumChannel it is checked whether the TX/RX
+Notice that in ``MultiModelSpectrumChannel`` it is checked whether the TX/RX
 SpectrumPhy instances belong to different TX/RX nodes.
 This is needed to avoid pathloss models calculations among
 the phased antenna arrays of the same node, because there are no models yet
@@ -743,8 +749,8 @@
 
 4. Compute the long term component
 The method GetLongTerm returns the long term component obtained by multiplying
-the channel matrix and the beamforming vectors. The function CalculateLongTermComponent
-calculates the long term component per RX and TX port pair. Finally, GetLongTerm
+the channel matrix and the beamforming vectors. The function ``CalculateLongTermComponent``
+calculates the long term component per RX and TX port pair. Finally, ``GetLongTerm``
 returns a 3D long term channel matrix whose dimensions are the number of the
 receive antenna ports, the number transmit antenna ports, and
 the number of clusters. When multiple ports are being configured note that
@@ -759,7 +765,7 @@
 
 5. Apply the small scale fading, calculate the channel gain, generate the
 frequency domain 3D spectrum channel matrix, and finally compute the received PSD
-The method CalcBeamformingGain computes the channel gain in each sub-band and
+The method ``CalcBeamformingGain`` computes the channel gain in each sub-band and
 applies it to the PSD of the transmitted signal to obtain the received PSD.
 To compute the sub-band gain, it accounts for the Doppler phenomenon and the
 time dispersion effect on each cluster.
@@ -772,7 +778,7 @@
 distribution depends on the parameter :math:`v_{scatt}`.
 The value of :math:`v_{scatt}` can be configured using the attribute "vScatt"
 (by default it is set to 0, so that the scattering effect is not considered).
-Function GenSpectrumChannelMatrix generates the received PSD for each pair of
+Function ``GenSpectrumChannelMatrix`` generates the received PSD for each pair of
 the transmit and receive antenna ports. It creates a frequency domain 3D spectrum
 channel matrix whose dimensions are the number of receive antenna ports,
 the number of transmit antenna ports, and the number of resource blocks.
@@ -785,11 +791,13 @@
 ThreeGppChannelModel
 ####################
 
-The class ThreeGppChannelModel implements the channel matrix generation procedure
-described in Sec. of [TR38901]_.
-The main method is GetChannel, which takes as input the mobility models of
+The class ``ThreeGppChannelModel`` implements the channel matrix generation procedure described in
+3GPP TR 38.901 [TR38901]_ and an update technique aligned with 3GPP TR 38.901 spatial-consistency
+feature (Procedure A), with ns-3-specific update triggering criteria suited to discrete-event simulation.
+
+The main method is ``GetChannel``, which takes as input the mobility models of
 the transmitter and receiver nodes, the associated antenna objects,
-and returns a ChannelMatrix object containing:
+and returns a ``ChannelMatrix`` object containing:
 
 * the channel matrix of size UxSxN, where U is the number of receiving antenna elements, S is the number of transmitting antenna elements and N is the number of clusters
 
@@ -803,15 +811,257 @@
 
 * other channel parameters
 
-The ChannelMatrix objects are saved in the map m_channelMap and updated when the coherence time
-expires, or in case the LOS/NLOS channel condition changes.
-The coherence time can be configured through
-the attribute "UpdatePeriod", and should be chosen by taking into account all the
-factors that affects the channel variability, such as mobility, frequency,
-propagation scenario, etc. By default, it is set to 0, which means that the
-channel is recomputed only when the LOS/NLOS condition changes.
-It is possible to configure the propagation scenario and the operating frequency
-of interest through the attributes "Scenario" and "Frequency", respectively.
+The ``ChannelMatrix`` objects are cached per link in the map ``m_channelMatrixMap``.
+A **new channel realization** (i.e., a new ``ChannelMatrix`` generated from newly
+generated channel parameters) is created when:
+
+* the **channel condition** for the link changes (e.g., LOS/NLOS and/or O2I
+  transitions), as reported by the configured channel-condition model; or
+* the **antenna-array configuration** changes (e.g., number of antenna
+  elements/ports), since this changes the required matrix dimensions.
+* spatial-consistency updates are enabled (``UpdatePeriod`` is nonzero) and
+  an update attempt observes a maximum endpoint displacement > 1 m (exceeding the
+  Procedure A step-size constraint), the model re-generates channel parameters
+  (and thus the channel matrix).
+
+If spatial-consistency updates are enabled and the 1 m step-size constraint is
+satisfied, the cached channel parameters are **updated** (Procedure A), and the
+``ChannelMatrix`` is recomputed from the updated parameters (without drawing a
+new independent realization).
+
+The channel condition is provided by the configured channel-condition model, which
+may be time-varying (e.g., stochastic blockage) and may be evaluated/updated
+according to its own internal logic (potentially with its own periodicity),
+independent of the small-scale fading evolution modeled in the
+``ThreeGppChannelModel``. The channel condition may change regardless of whether
+the nodes are static or mobile.
+
+Antenna-parameter changes are expected to be rare; one example is NR initial
+access/attachment, where a simplified receiver configuration may be assumed
+during attachment and then restored to the user-configured antenna array,
+triggering a matrix regeneration.
+
+The ``ChannelMatrix`` itself does **not** depend on beamforming. Beamforming
+vectors (and the precoding matrix) are applied later in
+``ThreeGppSpectrumPropagationLossModel::DoCalcRxPowerSpectralDensity`` when
+computing the long-term component and the received PSD (see the procedure in
+``ThreeGppSpectrumPropagationLossModel`` above). Beamforming weights may change
+at each reception/transmission without forcing regeneration of the cached
+``ChannelMatrix``.
+
+The propagation scenario and the operating frequency can be configured through
+the ``Scenario`` and ``Frequency`` attributes, respectively.
+Spatial-consistency evolution aligned with **3GPP TR 38.901 Procedure A update
+equations** is enabled when the ``UpdatePeriod`` attribute is set to a
+**non-zero** value (see below).
+
+**Spatial consistency procedure:** The spatial consistency update technique is aligned
+with 3GPP TR 38.901 Sec. 7.6.3.2 (**Procedure A**). Procedure A aims to preserve
+spatial correlation of cluster-specific random terms across consecutive channel
+evaluations as nodes move (e.g., cluster delays from Fig. 7.5-1, Step 5; the
+per-cluster shadowing term used in the cluster-power computation; and the random
+offsets/signs used for AOD/AOA/ZOD/ZOA generation in Step 7).
+
+In ns-3, the **channel-parameter evolution** follows the Procedure A update equations,
+while the **update-triggering criteria** are adapted to a discrete-event simulation
+environment. In TR 38.901, Procedure A is described for updates at about 1 m
+granularity, without prescribing a discrete evaluation schedule, and there is no
+specified time or distance at which a new channel realization is drawn;
+in ns-3, updates are attempted when the channel is evaluated and the configured
+``UpdatePeriod`` has elapsed, and the model enforces the 1 m step-size constraint
+(falling back to re-generation of channel parameters when it is exceeded).
+
+With Procedure A, large-scale parameters evolve as a first-order, spatially
+correlated stochastic process. Continuity is obtained by evolving a cached channel
+state over time/distance; no position-indexed channel field is defined. As a
+consequence, the channel is trajectory-dependent, and revisiting the same location
+does not necessarily reproduce the exact same channel realization. This behavior is
+consistent with the stochastic modeling approach defined in TR 38.901 [TR38901]_.
+Users requiring strict position-based repeatability (including under LOS/NLOS state
+changes driven by the channel-condition model) would have to implement a map/field-based
+approach (e.g., spatially correlated LSP maps in WINNER-derived frameworks such as
+QuaDRiGa) [WIN2D112]_ [QUADRIGA]_ or an explicitly position-seeded regeneration policy.
+
+Drop-based spatial consistency across multiple initial locations (TR 38.901 Sec. 7.6.3.1)
+and Procedure B (TR 38.901 Sec. 7.6.3.2) are not implemented in this model.
+
+The initial channel realization for a link is generated using the standard 3GPP
+cluster/ray generation procedure (Fig. 7.5-1). Spatial consistency is then introduced
+through subsequent Procedure A updates of the same link. During each update attempt,
+Procedure A updates the channel parameters in a cluster-wise manner. First, cluster
+delays are updated according to Eq. 7.6-9 in [TR38901]_. Second, cluster powers are
+updated using the updated cluster delays; during this step, the per-cluster shadowing
+term is evolved (not re-generated) based on the correlation distance of the configured
+scenario. Third, cluster departure and arrival angles are updated using the UT/BS motion
+information, as defined in Eqs. 7.6-10b and 7.6-10c and the subsequent angle-update
+equations in Eqs. 7.6-11 to 7.6-14.
+
+In ns-3, the update period is defined by ``UpdatePeriod`` and is analogous to
+:math:`t_{\Delta}` in TR 38.901 Sec. 7.6.3.2. When the channel is evaluated and
+``UpdatePeriod`` has elapsed since the last parameter generation/update,
+the model proceeds as follows:
+
+* If the maximum endpoint displacement since the last parameter generation/update is
+  **within 1 m**, the model applies **Procedure A updates** to obtain a
+  spatially consistent channel evolution.
+* If the maximum endpoint displacement is **greater than 1 m**, the model **re-generates**
+  the channel parameters (and thus the channel matrix) instead of applying
+  Procedure A for that update step.
+
+The small update distance is consistent with the intent of Procedure A to evolve
+small-scale channel parameters smoothly as the terminal moves; larger displacements
+between evaluations are handled by re-generation rather than attempting to
+extrapolate multiple intermediate updates.
+
+**Note on the 1 m update distance vs. correlation distances:** The 1 m rule is a
+step-size constraint for applying Procedure A updates (i.e., the model updates
+the channel in small spatial increments). It does not imply that the channel is
+only correlated over 1 m. The scenario correlation distances provided in TR 38.901
+Table 7.6.3.1-2 parameterize the *rate* at which cluster-level random variables
+(e.g., per-cluster shadowing) decorrelate with displacement across successive
+updates, via an exponential correlation function (TR 38.901, Eq. 7.4-5) of the form
+:math:`R(\Delta x) = \exp(-\Delta x / d_{cor})`. Consequently, when Procedure A is
+applied repeatedly over many small steps, variables can remain correlated over
+tens of meters when :math:`d_{cor}` is large, even though each individual update
+step is limited to 1 m.
+
+**Design note (displacement > 1 m and update triggering):** TR 38.901 recommends
+choosing :math:`t_{\Delta}` such that the displacement between successive updates
+remains within 1 m (often expressed as :math:`v \cdot t_{\Delta} < 1\,\text{m}`).
+In link-/physical-level channel simulators, the channel is commonly evolved at a
+fixed update distance/period along a user trajectory (e.g., NYUSIM) [NYUSIM]_.
+In a system-level simulator, however, forcing periodic channel updates even when
+the link is not used (no traffic, no transmissions) may be computationally
+expensive in large-scale simulations.
+
+Since ns-3 is a discrete-event simulator, channel evaluations may occur at
+irregular times (e.g., sparse or bursty traffic), and an update attempt may
+observe a displacement larger than 1 m. In that situation, several behaviors are
+possible: (i) keep the previous channel state without updating, (ii) re-generate
+a new realization, (iii) approximate the motion by splitting the interval into
+smaller steps and applying multiple updates, or (iv) force periodic channel
+updates independent of traffic (i.e., drive the evolution from a dedicated timer
+and update internal state even when the channel is not queried).
+
+Option (i) avoids discontinuities but may keep the channel unchanged over large
+displacements. Option (ii) is conservative and simple, but introduces a new
+realization when the update-distance constraint is exceeded. Option (iii) would
+require assumptions about the intermediate trajectory (only start/end positions
+are known) and increases computational cost. Option (iv) may also be
+computationally expensive in large-scale simulations, since it updates channels
+even when the channel is not evaluated.
+
+ns-3 chooses option (ii) as a fallback: if the displacement exceeds 1 m at an
+update instant, the channel parameters are re-generated.
+
+**How to choose ``UpdatePeriod``.** To obtain Procedure A updates (rather than
+frequent re-generation), ``UpdatePeriod`` should be selected so that the expected
+maximum endpoint displacement stays below 1 m between updates, i.e.,
+
+:math:`\text{UpdatePeriod} \lesssim 1\,\text{m} / v_{\mathrm{end,max}}`.
+
+Example: if the maximum expected endpoint speed is 5 m/s, choosing
+``UpdatePeriod`` <= 0.2 s keeps the maximum endpoint displacement below 1 m.
+
+**Important:** ``UpdatePeriod`` sets **a minimum interval** between successive
+channel updates; the channel state is updated only when the channel is evaluated,
+so the effective update instants are traffic-driven (e.g., receptions/transmissions
+or other events that require channel evaluation). For mobile scenarios,
+users should ensure that the channel is evaluated at least as often as ``UpdatePeriod``
+(e.g., by periodic transmissions/receptions, control signaling, or application traffic).
+Otherwise, spatial-consistency updates may be skipped and the model may fall back to
+re-generation after larger displacements.
+
+If the channel is not evaluated for longer than ``UpdatePeriod``, then the next
+evaluation triggers an update attempt based on the current displacement since
+the last generation/update (Procedure A if within 1 m, otherwise re-generation).
+
+The following rule-of-thumb values illustrate a recommended upper bound for
+``UpdatePeriod`` to keep the maximum endpoint displacement within 1 m. Users should also
+ensure that the channel is evaluated at least that often (e.g., via periodic
+signaling or traffic), otherwise updates may be skipped.
+
++--------------+-------------+------------------------+---------------------------------------------+
+| Speed (km/h) | Speed (m/s) | Max ``UpdatePeriod``   | Recommended channel-evaluation / signaling  |
+|              |             | (ms)                   | period (ms)                                 |
++==============+=============+========================+=============================================+
+| 3            | 0.833       | 1200                   | <= 1200                                     |
++--------------+-------------+------------------------+---------------------------------------------+
+| 30           | 8.33        | 120                    | <= 120                                      |
++--------------+-------------+------------------------+---------------------------------------------+
+| 60           | 16.67       | 60                     | <= 60                                       |
++--------------+-------------+------------------------+---------------------------------------------+
+
+For links where both endpoints move (e.g., V2V), two different “displacements” are
+relevant:
+
+* The **maximum endpoint displacement** (max motion of either endpoint since the last
+  generation/update) is used to enforce the **1 m step-size constraint** for
+  applying Procedure A.
+* The **relative (geometry) displacement** (change in the Tx--Rx relative position
+  vector) is used as the displacement input for **correlation updates of
+  cluster-level random terms** (e.g., the per-cluster shadowing term used in the
+  cluster power update).
+
+For Procedure A, several cluster-level random terms are evolved as functions of
+displacement between consecutive updates, using exponential spatial correlation
+models (e.g., [TR38901]_, Eq. 7.4-5). In TR 38.901 the procedure is described
+from the perspective of a moving terminal; when both endpoints move (e.g., V2V),
+ns-3 generalizes the triggering constraint by using the maximum displacement of
+either endpoint since the last generation/update (maximum endpoint displacement),
+while still using the change of the Tx--Rx relative position vector (relative/geometry
+displacement) as the displacement input for correlation updates of cluster-level
+random terms (e.g., the per-cluster shadowing term used in the cluster power
+update).
+
+Thus, vehicles traveling in the same direction at similar speeds may have a small
+relative (geometry) displacement (slow geometry evolution), even though the
+maximum endpoint displacement can be large and may force a smaller ``UpdatePeriod``
+to avoid exceeding the 1 m step-size constraint.
+
+Note that **relative (geometry) displacement close to zero does not necessarily
+mean that no update occurs**. Procedure A includes update components that are
+driven by **endpoint motion over** :math:`t_{\Delta}` (in ns-3, ``UpdatePeriod``
+is the analogue of :math:`t_{\Delta}`), in addition to components that use
+displacement-dependent spatial correlation. For example, the per-cluster delay
+evolution uses endpoint velocity terms over :math:`t_{\Delta}` (see [TR38901]_,
+Eq. 7.6-9). Cluster-angle updates also use the endpoint motion through the
+rotation/velocity terms (see [TR38901]_, Eqs. 7.6-10b and 7.6-10c) and the
+subsequent angle-update equations (Eqs. 7.6-11 to 7.6-14). Therefore, even in
+scenarios where the Tx--Rx geometry changes slowly (e.g., near-parallel motion),
+ns-3 may still perform a Procedure A update when ``UpdatePeriod`` expires
+(subject to the 1 m step-size constraint), and **motion-/time-dependent
+components** (e.g., angle evolution and related Doppler/phase terms) may still
+evolve. In contrast, updates that explicitly take the relative/geometry
+displacement as input (e.g., per-cluster shadowing correlation via the
+exponential model in [TR38901]_, Eq. 7.4-5) will exhibit little or no change when
+the relative/geometry displacement is near zero.
+
+Example (V2V, same direction): v1 = 120 km/h, v2 = 100 km/h, so the relevant
+speed for the 1 m step-size constraint is the maximum endpoint speed,
+:math:`v_{\mathrm{end,max}} = \max(v_1, v_2) = 120` km/h = 33.3 m/s. To keep the
+maximum endpoint displacement below 1 m between updates, choose
+``UpdatePeriod`` <= 30 ms and ensure the channel is evaluated (e.g., via periodic
+signaling/traffic) with a period <= 30 ms.
+
+**Channel consistency applicability:** The correlation distances used by the
+Procedure A updates depend on the configured scenario. For terrestrial cellular
+scenarios such as UMa, UMi, RMa, and Indoor, the spatial correlation distances
+used for per-cluster shadowing updates are specified in 3GPP TR 38.901 (see
+Table 7.6.3.1-2, "Correlation distance for spatial consistency") and are
+reflected in the corresponding 3GPP parameter tables of the ``ThreeGppChannelModel``.
+
+For other scenarios, such as V2V and NTN, the relevant 3GPP technical reports
+(e.g., TR 37.885 for V2X and TR 38.811 for NTN) describe channel models and
+mobility assumptions but do not provide standardized tables for cluster
+correlation distances comparable to those in TR 38.901. Consequently, while the
+spatial consistency procedure can be used when these scenarios are configured,
+users should be aware that appropriate correlation-distance values must be
+selected and configured in the ``ThreeGppChannelModel`` parameter tables based
+on the intended deployment and propagation environment. In the absence of
+standardized 3GPP values, the current implementation uses a short correlation
+distance (1 m) for per-cluster shadowing, resulting in weak spatial correlation
+unless configured otherwise.
 
 **Blockage model:** 3GPP TR 38.901 also provides an optional
 feature that can be used to model the blockage effect due to the
@@ -824,23 +1074,23 @@
 stochastic (Model A) and geometric (Model B). In this work, we
 used the implementation provided by [Zhang]_, which
 uses the stochastic method. In particular, the model is implemented by the
-method CalcAttenuationOfBlockage, which computes the additional attenuation.
+method ``CalcAttenuationOfBlockage``, which computes the additional attenuation.
 The blockage feature can be disable through the attribute "Blockage". Also, the
 attributes "NumNonselfBlocking", "PortraitMode" and "BlockerSpeed" can be used
 to configure the model.
 
 Testing
 #######
-The test suite ThreeGppChannelTestSuite includes five test cases:
+The test suite ``ThreeGppChannelTestSuite`` includes five test cases:
 
-* ThreeGppChannelMatrixComputationTest checks if the channel matrix has the
+* ``ThreeGppChannelMatrixComputationTest`` checks if the channel matrix has the
   correct dimensions and if it correctly normalized
 
-* ThreeGppChannelMatrixUpdateTest, which checks if the channel matrix
+* ``ThreeGppChannelMatrixUpdateTest``, which checks if the channel matrix
   is correctly updated when the coherence time exceeds
 
-* ThreeGppSpectrumPropagationLossModelTest, which tests the functionalities
-  of the class ThreeGppSpectrumPropagationLossModel. It builds a simple
+* ``ThreeGppSpectrumPropagationLossModelTest``, which tests the functionalities
+  of the class ``ThreeGppSpectrumPropagationLossModel``. It builds a simple
   network composed of two nodes, computes the power spectral density
   received by the receiving node, and
 
@@ -850,12 +1100,35 @@
        the beamforming vectors,
     3. Checks if the long term is updated when changing the channel matrix
 
-* ThreeGppCalcLongTermMultiPortTest, which tests that the channel matrices are
+* ``ThreeGppCalcLongTermMultiPortTest``, which tests that the channel matrices are
   correctly generated when multiple transmit and receive antenna ports are used.
 
-* ThreeGppMimoPolarizationTest, which tests that the channel matrices are
+* ``ThreeGppMimoPolarizationTest``, which tests that the channel matrices are
   correctly generated when dual-polarized antennas are being used.
 
+* ``ThreeGppChannelConsistencyTest`` is designed to verify that consecutive
+  channel realizations remain spatially consistent while the user is moving,
+  which is the scenario addressed by the implemented channel consistency
+  Procedure A (3GPP TR 38.901, Sec. 7.6.3).
+
+  The test considers both LOS and NLOS conditions, thereby exercising the
+  different channel update mechanisms defined for each case,
+  in particular the cluster delay update (Eq. 7.6-9) and the cluster departure
+  and arrival angle updates (Eqs. 7.6-10b and 7.6-10c). Multiple test cases
+  are defined for different carrier frequencies and user speeds. For each test case,
+  the channel coherence time is computed and the channel update period
+  (configured through the UpdatePeriod attribute) is set accordingly.
+
+  The test verifies that spatially consistent channel updates produce
+  significantly smoother channel evolution (approximately five times smaller
+  variations) compared to generating independent channel realizations under
+  the same LOS/NLOS conditions. To evaluate channel consistency, several metrics are tracked, including
+  per-cluster parameters such as cluster delay, cluster power, and
+  cluster angles (AOA and ZOA), as well as a global channel metric, the
+  Frobenius norm of the channel matrix. For the per-cluster analysis,
+  the third cluster, as defined in Table 7.8.5 for channel consistency
+  calibration, is selected.
+
 **Note:** TR 38.901 includes a calibration procedure that can be used to validate
 the model, but it requires some additional features which are not currently
 implemented, thus is left as future work.
@@ -881,6 +1154,18 @@
 .. [TR38901] 3GPP. 2018. TR 38.901. Study on channel for frequencies from 0.5 to
    100 GHz. V.15.0.0. (2018-06).
 
+.. [NYUSIM] Shihao Ju and Theodore S. Rappaport. "Simulating Motion - Incorporating Spatial
+   Consistency into the NYUSIM Channel Model." NYU WIRELESS.
+
+.. [WIN2D112] IST-4-027756 WINNER II, Deliverable D1.1.2, "WINNER II channel models:
+   Part I - Channel models", 2007. Available at:
+   https://signserv.signal.uu.se/Publications/WINNER/WIN2D112.pdf
+
+.. [QUADRIGA] S. Jaeckel, L. Raschkowski, K. Börner, and L. Thiele, "QuaDRiGa:
+   Quasi Deterministic Radio Channel Generator - User Manual and Documentation",
+   Fraunhofer HHI. Available at:
+   https://quadriga-channel-model.de/wp-content/uploads/2015/02/quadriga_documentation_v1.2.3.pdf
+
 .. [Zhang] Menglei Zhang, Michele Polese, Marco Mezzavilla, Sundeep Rangan,
    Michele Zorzi. "ns-3 Implementation of the 3GPP MIMO Channel Model for
    Frequency Spectrum above 6 GHz". In Proceedings of the Workshop on ns-3
@@ -1124,7 +1409,7 @@
 Implementation
 ##############
 The channel spectrum estimation procedure is already implemented (as described in [ns3Zugno]_ ) into the classes
-ThreeGppChannelModel and ThreeGppSpectrumPropagationLossModel, that have been extended to support NTN through the
+``ThreeGppChannelModel`` and ``ThreeGppSpectrumPropagationLossModel``, that have been extended to support NTN through the
 introduction of the appropriate parameters.
 3GPP considers two frequencies of interest: S-band (2GHz) and Ka-band (20/30GHz). Many channel parameters are dependent
 on the frequency band in use, but no specific range of frequencies of these bands has been given by 3GPP. Hence, this
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/examples/CMakeLists.txt ns-3.47/src/spectrum/examples/CMakeLists.txt
--- ns-3.46/src/spectrum/examples/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/examples/CMakeLists.txt	2026-02-16 16:08:49.821230600 -0800
@@ -74,3 +74,15 @@
     ${libmobility}
     ${libspectrum}
 )
+
+build_lib_example(
+  NAME three-gpp-channel-consistency-example
+  SOURCE_FILES three-gpp-channel-consistency-example.cc
+  LIBRARIES_TO_LINK
+    ${libantenna}
+    ${libbuildings}
+    ${libcore}
+    ${libmobility}
+    ${libpropagation}
+    ${libspectrum}
+)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/examples/three-gpp-channel-consistency-example.cc ns-3.47/src/spectrum/examples/three-gpp-channel-consistency-example.cc
--- ns-3.46/src/spectrum/examples/three-gpp-channel-consistency-example.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/spectrum/examples/three-gpp-channel-consistency-example.cc	2026-02-16 16:08:49.821230600 -0800
@@ -0,0 +1,389 @@
+/*
+ * Copyright (c) 2020, University of Padova, Dep. of Information Engineering, SIGNET lab
+ * Copyright (c) 2026, Centre Tecnologic de Telecomunicacions de Catalunya (CTTC)
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ */
+
+/*
+ * @file
+ * This example shows how to configure the channel consistent updates in a mobile environment.
+ * This example is using the channel consistency feature of the 3GPP channel model described in
+ * 3GPP TR 38.901, Sec. 7.6.3. This feature enables the generation of spatially
+ * correlated channel realizations for simulations with mobility. This feature is
+ * automatically enabled by setting up the UpdatePeriod attribute of ThreeGppChannelModel to be
+ * different than 0. The example is the following 38.901 7.8.5 scenario setup Config2 parameters
+ * for channel consistency evaluation of mobile users (Procedure A). The channel model is
+ * 3GPP UMi Street Canyon, while the channel condition model is set to be a deterministic
+ * channel condition model based on the buildings obstacles that are configured in the
+ * example, according to a typical 3GPP urban setup. See the illustration in the following figure:
+ *
+ *   _______    _______
+ *  |      |    |      |
+ *  |      |    |      |
+ *  |      |    |      |
+ *  |      |    |      |
+ *  |      |    |      |
+ *  |______|    |______|
+ *    < < < < <
+ *   _______  ^  _______
+ *  |      |  ^ |      |
+ *  |      |  ^ |      |
+ *  |      |  ^ |      |
+ *  |      |  ^ |      |
+ *  |      | UE |      |
+ *  |______|    |______|
+ *           * gNB
+ *
+ * gNB is at the fixed position (shown by * in the previous illustration), and at the height of 10
+ * meters. The user is moving away from gNB as illustrated in the figure with ^ and <, and first, it
+ * is in the LOS state, and later, after turning into a perpendicular street, it is in the NLOS
+ * state.
+ *
+ * The UE moves with a speed of 30km/h, the frequency used is 30GHz, and the channel update is
+ * 0.5ms, configured based on the channel coherence time.
+ *
+ * The antenna array is set to quasiomni, and it is fixed during the whole simulation to eliminate
+ * possible effects of the impact of the beamforming update while the user is moving. The example
+ * tries to focus only on the changes in the SNR caused by the channel updates as the user moves.
+ * The bearing angels are set to 90 at gNB and -90 at UE.
+ *
+ * This example generates the output file '3gpp-channel-consistency-output.txt'. Each row of this
+ * output file is organized as follows:
+ * Time[s] TxPosX[m] TxPosY[m] RxPosX[m] RxPosY[m] ChannelState SNR[dB] Pathloss[dB]
+ * An additional python script three-gpp-channel-consistency-example.py reads this
+ * output file and generates two figures:
+ * (i) 3gpp-channel-consistency.gif, a GIF representing the simulation scenario and the UE mobility;
+ * it includes the "zoomed view" of the SNR, and also it includes the variation of the
+ * channel updates.
+ * (ii) 3gpp-channel-consistency-snr.png, which represents the behavior of the SNR over
+ * the time, the absolute value and the varying rate over time.
+ *
+ */
+
+#include "ns3/buildings-module.h"
+#include "ns3/core-module.h"
+#include "ns3/mobility-module.h"
+#include "ns3/network-module.h"
+#include "ns3/spectrum-signal-parameters.h"
+#include "ns3/three-gpp-channel-model.h"
+#include "ns3/three-gpp-propagation-loss-model.h"
+#include "ns3/three-gpp-spectrum-propagation-loss-model.h"
+#include "ns3/uniform-planar-array.h"
+
+#include <fstream>
+
+using namespace ns3;
+
+/// the log component
+NS_LOG_COMPONENT_DEFINE("ThreeGppChannelConsistencyExample");
+
+/// the PropagationLossModel object
+static Ptr<ThreeGppPropagationLossModel> m_propagationLossModel;
+/// the SpectrumPropagationLossModel object
+static Ptr<ThreeGppSpectrumPropagationLossModel> m_spectrumLossModel;
+/// the ChannelConditionModel object
+static Ptr<ChannelConditionModel> m_condModel;
+
+/**
+ * @brief A structure that holds the parameters for the ComputeSnr
+ * function.
+ */
+struct ComputeSnrParams
+{
+    Ptr<MobilityModel> txMob;               //!< the tx mobility model
+    Ptr<MobilityModel> rxMob;               //!< the rx mobility model
+    Ptr<SpectrumSignalParameters> txParams; //!< the params of the tx signal
+    double noiseFigure;                     //!< the noise figure in dB
+    Ptr<PhasedArrayModel> txAntenna;        //!< the tx antenna array
+    Ptr<PhasedArrayModel> rxAntenna;        //!< the rx antenna array
+};
+
+/**
+ * Set QuasiOmni beamforming vector to the antenna array
+ * @param antenna the antenna array to which will be set quasi omni beamforming vector
+ */
+static void
+CreateQuasiOmniBf(Ptr<PhasedArrayModel> antenna)
+{
+    PhasedArrayModel::ComplexVector antennaWeights;
+
+    auto antennaRows = antenna->GetNumRows();
+    auto antennaColumns = antenna->GetNumColumns();
+    auto numElemsPerPort = antenna->GetNumElemsPerPort();
+
+    double power = 1 / sqrt(numElemsPerPort);
+    size_t numPolarizations = antenna->IsDualPol() ? 2 : 1;
+
+    PhasedArrayModel::ComplexVector omni(antennaRows * antennaColumns * numPolarizations);
+    uint16_t bfIndex = 0;
+    for (size_t pol = 0; pol < numPolarizations; pol++)
+    {
+        for (uint32_t ind = 0; ind < antennaRows; ind++)
+        {
+            std::complex<double> c = 0.0;
+            if (antennaRows % 2 == 0)
+            {
+                c = exp(std::complex<double>(0, M_PI * ind * ind / antennaRows));
+            }
+            else
+            {
+                c = exp(std::complex<double>(0, M_PI * ind * (ind + 1) / antennaRows));
+            }
+            for (uint32_t ind2 = 0; ind2 < antennaColumns; ind2++)
+            {
+                std::complex<double> d = 0.0;
+                if (antennaColumns % 2 == 0)
+                {
+                    d = exp(std::complex<double>(0, M_PI * ind2 * ind2 / antennaColumns));
+                }
+                else
+                {
+                    d = exp(std::complex<double>(0, M_PI * ind2 * (ind2 + 1) / antennaColumns));
+                }
+                omni[bfIndex] = (c * d * power);
+                bfIndex++;
+            }
+        }
+    }
+
+    antenna->SetBeamformingVector(omni);
+}
+
+/**
+ * Computes the SNR
+ * @param params A structure that holds a bunch of parameters needed by ComputeSnr function to
+ * calculate the average SNR
+ */
+static void
+ComputeSnr(const ComputeSnrParams& params)
+{
+    // check the channel condition
+    Ptr<ChannelCondition> cond = m_condModel->GetChannelCondition(params.txMob, params.rxMob);
+    // apply the pathloss
+    double propagationGainDb = m_propagationLossModel->CalcRxPower(0, params.txMob, params.rxMob);
+    double propagationGainLinear = std::pow(10.0, (propagationGainDb) / 10.0);
+    *(params.txParams->psd) *= propagationGainLinear;
+    // apply the fast fading and the beamforming gain
+    auto rxParams = m_spectrumLossModel->CalcRxPowerSpectralDensity(params.txParams,
+                                                                    params.txMob,
+                                                                    params.rxMob,
+                                                                    params.txAntenna,
+                                                                    params.rxAntenna);
+    Ptr<SpectrumValue> rxPsd = rxParams->psd;
+    // create the noise psd
+    // taken from lte-spectrum-value-helper
+    const double kT_dBm_Hz = -174.0; // dBm/Hz
+    double kT_W_Hz = std::pow(10.0, (kT_dBm_Hz - 30) / 10.0);
+    double noiseFigureLinear = std::pow(10.0, params.noiseFigure / 10.0);
+    double noisePowerSpectralDensity = kT_W_Hz * noiseFigureLinear;
+    Ptr<SpectrumValue> noisePsd = Create<SpectrumValue>(params.txParams->psd->GetSpectrumModel());
+    (*noisePsd) = noisePowerSpectralDensity;
+    // print the SNR and pathloss values to the output file
+    std::ofstream f;
+    f.open("3gpp-channel-consistency-output.txt", std::ios::out | std::ios::app);
+    f << Simulator::Now().GetSeconds() << " " // time [s]
+      << params.txMob->GetPosition().x << " " << params.txMob->GetPosition().y << " "
+      << params.rxMob->GetPosition().x << " " << params.rxMob->GetPosition().y << " "
+      << cond->GetLosCondition() << " "                  // channel state
+      << 10 * log10(Sum(*rxPsd) / Sum(*noisePsd)) << " " // SNR [dB]
+      << -propagationGainDb << std::endl;                // pathloss [dB]
+    f.close();
+}
+
+/**
+ * Generates a GNU-plottable file representing the buildings deployed in the
+ * scenario
+ * @param filename the name of the output file
+ */
+void
+PrintGnuplottableBuildingListToFile(std::string filename)
+{
+    std::ofstream outFile;
+    outFile.open(filename, std::ios_base::out | std::ios_base::trunc);
+    if (!outFile.is_open())
+    {
+        NS_LOG_ERROR("Can't open file " << filename);
+        return;
+    }
+    for (auto it = BuildingList::Begin(); it != BuildingList::End(); ++it)
+    {
+        Box box = (*it)->GetBoundaries();
+        outFile << box.xMin << " " << box.yMin << " " << box.xMax << " " << box.yMax << std::endl;
+    }
+}
+
+int
+main(int argc, char* argv[])
+{
+    double frequency = 30e9;          // operating frequency in Hz
+    double txPow_dbm = 35.0;          // tx power in dBm
+    double noiseFigure = 9.0;         // noise figure in dB
+    Time simTime = Seconds(40);       // simulation time
+    Time timeRes = MicroSeconds(500); // time resolution and the channel update time
+    double speed = 8.33;              // speed of the mobile node in the scenario [m/s]
+    double rbWidthHz = 720e3;         // RB width in Hz
+    uint32_t numRb = 275;             // number of resource blocks
+    double gNBHeight = 10;            // the height of the gNB
+    double ueHeight =
+        1.1; // the height of the UE (a bit higher than 1m because of the BP distance calculation)
+
+    // fix random parameters
+    RngSeedManager::SetSeed(1);
+    RngSeedManager::SetRun(1);
+    uint64_t stream = 10e3;
+
+    // create the nodes
+    NodeContainer nodes;
+    nodes.Create(2);
+
+    // create the tx and rx devices
+    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
+    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
+
+    // associate the nodes and the devices
+    nodes.Get(0)->AddDevice(txDev);
+    txDev->SetNode(nodes.Get(0));
+    nodes.Get(1)->AddDevice(rxDev);
+    rxDev->SetNode(nodes.Get(1));
+
+    // create the antenna objects and set their dimensions
+    Ptr<PhasedArrayModel> txAntenna =
+        CreateObjectWithAttributes<UniformPlanarArray>("NumColumns",
+                                                       UintegerValue(2),
+                                                       "NumRows",
+                                                       UintegerValue(2),
+                                                       "BearingAngle",
+                                                       DoubleValue(M_PI / 2));
+    Ptr<PhasedArrayModel> rxAntenna =
+        CreateObjectWithAttributes<UniformPlanarArray>("NumColumns",
+                                                       UintegerValue(2),
+                                                       "NumRows",
+                                                       UintegerValue(2),
+                                                       "BearingAngle",
+                                                       DoubleValue(-M_PI / 2));
+
+    Ptr<MobilityModel> txMob;
+    Ptr<MobilityModel> rxMob;
+
+    // create a grid of buildings
+    double buildingSizeX = 250 - 3.5 * 2 - 3; // m
+    double buildingSizeY = 433 - 3.5 * 2 - 3; // m
+    double streetWidth = 20;                  // m
+    double buildingHeight = 10;               // m
+    uint32_t numBuildingsX = 2;
+    uint32_t numBuildingsY = 2;
+    double maxAxisX = (buildingSizeX + streetWidth) * numBuildingsX;
+    double maxAxisY = (buildingSizeY + streetWidth) * numBuildingsY;
+
+    std::vector<Ptr<Building>> buildingVector;
+    for (uint32_t buildingIdX = 0; buildingIdX < numBuildingsX; ++buildingIdX)
+    {
+        for (uint32_t buildingIdY = 0; buildingIdY < numBuildingsY; ++buildingIdY)
+        {
+            Ptr<Building> building;
+            building = CreateObject<Building>();
+
+            building->SetBoundaries(Box(buildingIdX * (buildingSizeX + streetWidth),
+                                        buildingIdX * (buildingSizeX + streetWidth) + buildingSizeX,
+                                        buildingIdY * (buildingSizeY + streetWidth),
+                                        buildingIdY * (buildingSizeY + streetWidth) + buildingSizeY,
+                                        0.0,
+                                        buildingHeight));
+            building->SetNRoomsX(1);
+            building->SetNRoomsY(1);
+            building->SetNFloors(1);
+            buildingVector.push_back(building);
+        }
+    }
+
+    // set the mobility models of the TX(gNB) and RX(mobile UE)
+    txMob = CreateObject<ConstantPositionMobilityModel>();
+    txMob->SetPosition(Vector(maxAxisX / 2 - streetWidth / 2, -20, gNBHeight));
+    nodes.Get(0)->AggregateObject(txMob);
+
+    Time nextWaypoint;
+    rxMob = CreateObject<WaypointMobilityModel>();
+    rxMob->GetObject<WaypointMobilityModel>()->AddWaypoint(
+        Waypoint(nextWaypoint, Vector(maxAxisX / 2 - streetWidth / 2, maxAxisY / 4, ueHeight)));
+    nextWaypoint += Seconds((maxAxisY - maxAxisY / 2 - streetWidth) / 2 / speed);
+    rxMob->GetObject<WaypointMobilityModel>()->AddWaypoint(
+        Waypoint(nextWaypoint,
+                 Vector(maxAxisX / 2 - streetWidth / 2, maxAxisY / 2 - streetWidth / 2, ueHeight)));
+    nextWaypoint += Seconds((maxAxisX - streetWidth) / 2 / speed);
+    rxMob->GetObject<WaypointMobilityModel>()->AddWaypoint(
+        Waypoint(nextWaypoint, Vector(0.0, maxAxisY / 2 - streetWidth / 2, ueHeight)));
+    nextWaypoint = Seconds(0);
+    nodes.Get(1)->AggregateObject(rxMob);
+
+    // create the channel condition model
+    m_condModel = CreateObject<BuildingsChannelConditionModel>();
+
+    // create the propagation loss model
+    m_propagationLossModel = CreateObject<ThreeGppUmiStreetCanyonPropagationLossModel>();
+
+    m_propagationLossModel->SetAttribute("Frequency", DoubleValue(frequency));
+    m_propagationLossModel->SetAttribute("ShadowingEnabled", BooleanValue(true));
+    m_propagationLossModel->SetAttribute("ChannelConditionModel", PointerValue(m_condModel));
+    stream += m_propagationLossModel->AssignStreams(stream);
+
+    // create the channel model
+    Ptr<ThreeGppChannelModel> channelModel = CreateObject<ThreeGppChannelModel>();
+    channelModel->SetAttribute("Scenario", StringValue("UMi-StreetCanyon"));
+    channelModel->SetAttribute("Frequency", DoubleValue(frequency));
+    channelModel->SetAttribute("ChannelConditionModel", PointerValue(m_condModel));
+    channelModel->SetAttribute("UpdatePeriod", TimeValue(timeRes));
+    stream += channelModel->AssignStreams(stream);
+
+    // create the spectrum propagation loss model
+    m_spectrumLossModel = CreateObjectWithAttributes<ThreeGppSpectrumPropagationLossModel>(
+        "ChannelModel",
+        PointerValue(channelModel));
+
+    BuildingsHelper::Install(nodes);
+
+    // create the tx power spectral density
+    Bands rbs;
+    double freqSubBand = frequency;
+    for (uint32_t n = 0; n < numRb; ++n)
+    {
+        BandInfo rb;
+        rb.fl = freqSubBand;
+        freqSubBand += rbWidthHz / 2;
+        rb.fc = freqSubBand;
+        freqSubBand += rbWidthHz / 2;
+        rb.fh = freqSubBand;
+        rbs.push_back(rb);
+    }
+    Ptr<SpectrumModel> spectrumModel = Create<SpectrumModel>(rbs);
+    Ptr<SpectrumValue> txPsd = Create<SpectrumValue>(spectrumModel);
+    Ptr<SpectrumSignalParameters> txParams = Create<SpectrumSignalParameters>();
+    double txPow_w = std::pow(10., (txPow_dbm - 30) / 10);
+    double txPowDens = (txPow_w / (numRb * rbWidthHz));
+    (*txPsd) = txPowDens;
+    txParams->psd = txPsd->Copy();
+
+    CreateQuasiOmniBf(txAntenna);
+    CreateQuasiOmniBf(rxAntenna);
+
+    for (int i = 0; i < simTime / timeRes; i++)
+    {
+        ComputeSnrParams params{txMob, rxMob, txParams->Copy(), noiseFigure, txAntenna, rxAntenna};
+        Simulator::Schedule(timeRes * i, &ComputeSnr, params);
+    }
+
+    // initialize the output file
+    std::ofstream f;
+    f.open("3gpp-channel-consistency-output.txt", std::ios::out);
+    f << "Time[s] TxPosX[m] TxPosY[m] RxPosX[m] RxPosY[m] ChannelState SNR[dB] Pathloss[dB]"
+      << std::endl;
+    f.close();
+
+    // print the list of buildings to file
+    PrintGnuplottableBuildingListToFile("3gpp-channel-consistency-buildings.txt");
+
+    Simulator::Run();
+    Simulator::Destroy();
+    return 0;
+}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/examples/three-gpp-channel-consistency-example.py ns-3.47/src/spectrum/examples/three-gpp-channel-consistency-example.py
--- ns-3.46/src/spectrum/examples/three-gpp-channel-consistency-example.py	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/spectrum/examples/three-gpp-channel-consistency-example.py	2026-02-16 16:08:49.821230600 -0800
@@ -0,0 +1,259 @@
+#!/bin/python3
+
+# SPDX-License-Identifier: GPL-2.0-only
+
+import matplotlib.animation as animation
+import matplotlib.patches as patches
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+from matplotlib.lines import Line2D
+
+#
+# Plot the traces generated by three-gpp-channel-consistency-example
+#
+
+# -----------------------------------------------------------------------------
+# Load data
+# -----------------------------------------------------------------------------
+df = pd.read_csv("3gpp-channel-consistency-output.txt", sep=r"\s+", comment="#")
+
+# Column names from the ns-3 trace
+TIME_COL = "Time[s]"
+TX_X_COL = "TxPosX[m]"
+TX_Y_COL = "TxPosY[m]"
+RX_X_COL = "RxPosX[m]"
+RX_Y_COL = "RxPosY[m]"
+STATE_COL = "ChannelState"
+SNR_COL = "SNR[dB]"
+
+# Convenience series
+time = df[TIME_COL]
+tx_x = df[TX_X_COL]
+tx_y = df[TX_Y_COL]
+rx_x = df[RX_X_COL]
+rx_y = df[RX_Y_COL]
+snr = df[SNR_COL]
+
+# Channel state (for LOS/NLOS vertical lines)
+state = df[STATE_COL].astype(str)
+
+# -----------------------------------------------------------------------------
+# Varying rates
+# -----------------------------------------------------------------------------
+snr_diff = snr.diff().fillna(0.0)
+df["SNRdiff"] = snr_diff
+# -----------------------------------------------------------------------------
+# Compute LOS/NLOS transition times and types
+# -----------------------------------------------------------------------------
+prev_state = state.shift(1, fill_value=state.iloc[0])
+
+times_LN = []  # LOS -> NLOS(NLOSv)
+times_NL = []  # NLOS(NLOSv) -> LOS
+
+for t, s_prev, s_cur in zip(time, prev_state, state):
+    if s_prev == s_cur:
+        continue
+
+    s_prev_is_nlos = s_prev.startswith("NLOS")
+    s_cur_is_nlos = s_cur.startswith("NLOS")
+
+    if s_prev == "LOS" and s_cur_is_nlos:
+        times_LN.append(t)
+    elif s_prev_is_nlos and s_cur == "LOS":
+        times_NL.append(t)
+
+
+def add_state_vlines(ax, add_legend=False):
+    """Draw vertical lines for LOS/NLOS transitions, with different types."""
+    # NLOS -> LOS (green, solid)
+    for t in times_NL:
+        ax.axvline(t, color="green", linestyle="-", linewidth=0.9, alpha=0.6)
+    # LOS -> NLOS (red, dashed)
+    for t in times_LN:
+        ax.axvline(t, color="red", linestyle="--", linewidth=0.9, alpha=0.6)
+
+    if add_legend:
+        legend_lines = [
+            Line2D([0], [0], color="green", linestyle="-", linewidth=1.2, label="NLOS → LOS"),
+            Line2D([0], [0], color="red", linestyle="--", linewidth=1.2, label="LOS → NLOS"),
+        ]
+        ax.legend(handles=legend_lines, loc="upper right", fontsize=8)
+
+
+# -----------------------------------------------------------------------------
+# Global plot params
+# -----------------------------------------------------------------------------
+zoom_window_s = 0.05  # SNR zoom window (example: 0.05 = 50 ms)
+
+t_min = time.min()
+t_max = time.max()
+
+# SNR limits
+snr_min = snr.min()
+snr_max = snr.max()
+snr_margin = 3.0
+# snr_ymin = snr_min - snr_margin
+# snr_ymax = snr_max + snr_margin
+snr_ymax = 30
+snr_ymin = -60
+
+
+# ΔSNR limits: symmetric around 0 with minimum span
+max_abs_dsnr = float(np.max(np.abs(snr_diff)))
+if max_abs_dsnr < 1.0:
+    max_abs_dsnr = 1.0  # at least +/- 1 dB
+# snr_diff_ymin = -1.2 * max_abs_dsnr
+# snr_diff_ymax = +1.2 * max_abs_dsnr
+
+snr_diff_ymin = -35
+snr_diff_ymax = +35
+
+
+# -----------------------------------------------------------------------------
+# Frame downsampling for animation (lightweight)
+# Simulation: 40 s @ 1 ms => ~40 000 samples
+# We cap GIF to ~800 frames => ~1 frame each 50 ms
+# -----------------------------------------------------------------------------
+n_samples = len(df)
+max_frames = 800  # target max frames in the GIF
+frame_step = max(1, n_samples // max_frames)
+frame_indices = range(0, n_samples, frame_step)
+
+# -----------------------------------------------------------------------------
+# 2×2 Animated Figure:
+# row 0: [map, SNR global]
+# row 1: [SNR zoom, ΔSNR]
+# -----------------------------------------------------------------------------
+fig, axes = plt.subplots(2, 2, figsize=(24, 9), dpi=150)
+
+ax_map = axes[0, 0]
+ax_snr = axes[0, 1]
+ax_snr_zoom = axes[1, 0]
+ax_snr_diff = axes[1, 1]
+
+# -----------------------------------------------------------------------------
+# MAP
+# -----------------------------------------------------------------------------
+ax_map.set_xlabel("X [m]")
+ax_map.set_ylabel("Y [m]")
+ax_map.set_aspect("equal")
+
+ax_map.set_xlim(-25, 600)
+ax_map.set_ylim(-25, 1000)
+
+tx_circle = patches.Circle((0.0, 0.0), 5.0, color="blue", alpha=0.35)
+rx_circle = patches.Circle((0.0, 0.0), 5.0, color="red", alpha=0.35)
+ax_map.add_patch(tx_circle)
+ax_map.add_patch(rx_circle)
+
+buildings = pd.read_csv(
+    "3gpp-channel-consistency-buildings.txt", sep=r"\s+", comment="#", header=None
+)
+
+for _, b in buildings.iterrows():
+    x0, y0, x1, y1 = b
+    rect = patches.Rectangle((x0, y0), x1 - x0, y1 - y0, color="gray", alpha=0.5)
+    ax_map.add_patch(rect)
+
+# -----------------------------------------------------------------------------
+# SNR Global
+# -----------------------------------------------------------------------------
+ax_snr.set_xlabel("Time [s]")
+ax_snr.set_ylabel("SNR [dB]")
+ax_snr.grid(True)
+ax_snr.set_xlim(t_min, t_max)
+ax_snr.set_ylim(snr_ymin, snr_ymax)
+(snr_line,) = ax_snr.plot([], [], "k-", linewidth=1.5)
+
+add_state_vlines(ax_snr, add_legend=True)
+
+# -----------------------------------------------------------------------------
+# SNR Zoom
+# -----------------------------------------------------------------------------
+ax_snr_zoom.set_xlabel("Time [s]")
+ax_snr_zoom.set_ylabel("SNR [dB] (zoom)")
+ax_snr_zoom.grid(True)
+ax_snr_zoom.set_ylim(snr_ymin, snr_ymax)
+(snr_zoom_line,) = ax_snr_zoom.plot([], [], "k-", linewidth=1.5)
+
+add_state_vlines(ax_snr_zoom)
+
+# -----------------------------------------------------------------------------
+# ΔSNR (global)
+# -----------------------------------------------------------------------------
+ax_snr_diff.set_xlabel("Time [s]")
+ax_snr_diff.set_ylabel("ΔSNR [dB]")
+ax_snr_diff.grid(True)
+ax_snr_diff.set_xlim(t_min, t_max)
+ax_snr_diff.set_ylim(snr_diff_ymin, snr_diff_ymax)
+(snr_diff_line,) = ax_snr_diff.plot([], [], "k-", linewidth=1.5)
+
+add_state_vlines(ax_snr_diff)
+
+# -----------------------------------------------------------------------------
+# Static SNR + ΔSNR figure (2×1)
+# -----------------------------------------------------------------------------
+fig_snr, axes_snr = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
+
+axes_snr[0].plot(time, snr, "k-", linewidth=1.2)
+axes_snr[0].set_ylabel("SNR [dB]")
+axes_snr[0].grid(True)
+add_state_vlines(axes_snr[0], add_legend=True)
+
+axes_snr[1].plot(time, snr_diff, "k-", linewidth=1.2)
+axes_snr[1].set_ylabel("ΔSNR [dB]")
+axes_snr[1].set_xlabel("Time [s]")
+axes_snr[1].grid(True)
+add_state_vlines(axes_snr[1])
+
+axes_snr[0].set_xlim(t_min, t_max)
+axes_snr[0].set_ylim(snr_ymin, snr_ymax)
+axes_snr[1].set_ylim(snr_diff_ymin, snr_diff_ymax)
+fig_snr.tight_layout()
+fig_snr.savefig("channel_consistency_snr_and_dsnr.png", dpi=300, bbox_inches="tight")
+plt.close(fig_snr)
+
+
+# -----------------------------------------------------------------------------
+# -----------------------------------------------------------------------------
+# Animation update function (uses downsampled frame_indices)
+# -----------------------------------------------------------------------------
+def update(frame_idx):
+    # frame_idx is an actual index into df (from frame_indices)
+    t_now = time.iloc[frame_idx]
+
+    # Map
+    tx_circle.set_center((tx_x.iloc[frame_idx], tx_y.iloc[frame_idx]))
+    rx_circle.set_center((rx_x.iloc[frame_idx], rx_y.iloc[frame_idx]))
+    ax_map.set_title(f"Time = {t_now:.3f} s")
+
+    # Global SNR
+    snr_line.set_data(time.iloc[: frame_idx + 1], snr.iloc[: frame_idx + 1])
+
+    # SNR zoom
+    t0 = max(t_min, t_now - zoom_window_s / 2.0)
+    t1 = min(t_max, t_now + zoom_window_s / 2.0)
+    mask = (time >= t0) & (time <= t1)
+
+    ax_snr_zoom.set_xlim(t0, t1)
+    snr_zoom_line.set_data(time[mask], snr[mask])
+
+    # ΔSNR (global)
+    snr_diff_line.set_data(time.iloc[: frame_idx + 1], snr_diff.iloc[: frame_idx + 1])
+
+    return (tx_circle, rx_circle, snr_line, snr_zoom_line, snr_diff_line)
+
+
+# -----------------------------------------------------------------------------
+# Run animation (downsampled frames, lighter GIF)
+# -----------------------------------------------------------------------------
+ani = animation.FuncAnimation(
+    fig,
+    update,
+    frames=frame_indices,
+    interval=0,
+    blit=False,
+)
+
+ani.save("3gpp-channel-consistency.gif", writer="pillow", fps=25, dpi=90)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/examples/three-gpp-channel-example.cc ns-3.47/src/spectrum/examples/three-gpp-channel-example.cc
--- ns-3.46/src/spectrum/examples/three-gpp-channel-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/examples/three-gpp-channel-example.cc	2026-02-16 16:08:49.821230600 -0800
@@ -6,25 +6,30 @@
  */
 
 /**
+ * @file
  * This example shows how to configure the 3GPP channel model classes to
  * compute the SNR between two nodes.
  * The simulation involves two static nodes which are placed at a certain
- * distance from each other and communicates through a wireless channel at
+ * distance from each other and communicate through a wireless channel at
  * 2 GHz with a bandwidth of 18 MHz. The default propagation environment is
- * 3D-urban macro (UMa) and it can be configured changing the value of the
+ * 3D-urban macro (UMa), and it can be configured changing the value of the
  * string "scenario".
- * Each node hosts a SimpleNetDevice and has an antenna array with 4 elements.
+ * Each node hosts has an antenna array with 4 antenna elements.
+ *
+ * The example writes its main results to the file ``snr-trace.txt``.
+ * Each row contains: (i) the simulation time (s), (ii) the SNR obtained with
+ * steering vectors (``DoBeamforming``), (iii) the SNR obtained with matched-filter
+ * combining, and (iv) the propagation gain.
  */
 
 #include "ns3/channel-condition-model.h"
 #include "ns3/constant-position-mobility-model.h"
+#include "ns3/constant-velocity-mobility-model.h"
 #include "ns3/core-module.h"
 #include "ns3/lte-spectrum-value-helper.h"
 #include "ns3/mobility-model.h"
-#include "ns3/net-device.h"
 #include "ns3/node-container.h"
 #include "ns3/node.h"
-#include "ns3/simple-net-device.h"
 #include "ns3/spectrum-signal-parameters.h"
 #include "ns3/three-gpp-channel-model.h"
 #include "ns3/three-gpp-propagation-loss-model.h"
@@ -58,19 +63,63 @@
 };
 
 /**
- * Perform the beamforming using the DFT beamforming method
- * @param thisDevice the device performing the beamforming
- * @param thisAntenna the antenna object associated to thisDevice
- * @param otherDevice the device towards which point the beam
+ * Perform a simple (DFT/steering) beamforming toward the other node.
+ *
+ * This function builds a per-element complex weight vector based on the geometric
+ * direction between two nodes and the antenna element locations returned by
+ * `PhasedArrayModel::GetElementLocation()`. The weights are unit-norm (equal
+ * power across elements) and have the form:
+ *
+ *   w[i] = exp(j * phase_i) / sqrt(N),
+ *   phase_i = sign * 2*pi * ( u · r_i )
+ *
+ * where `u` is the unit direction corresponding to the azimuth/inclination
+ * angles of the line-of-sight vector between the nodes, `r_i` is the location of
+ * element `i` in the array coordinate system, and `N` is the number of antenna
+ * elements.
+ *
+ * Sign convention and how to use this function:
+ *
+ * The 3GPP channel coefficients are constructed using spatial phase terms of the
+ * form exp(+j*2*pi*(u·r)) (see `ThreeGppChannelModel::GetNewChannel()`), i.e., the
+ * array *response/steering vector* toward a direction is proportional to:
+ *
+ *   a(theta)[i] = exp(+j*2*pi*(u·r_i)).
+ *
+ * In standard array processing, a transmit precoder that steers energy toward
+ * that direction uses the complex conjugate of the response, therefore
+ * sign = -1  (phase = -2*pi*(u·r_i)).
+ *
+ * For receive combining, the effective scalar channel is computed using the
+ * Hermitian inner product:
+ *
+ *   h_eff = w_rx^H * H * w_tx .
+ *
+ * To obtain matched-filter (maximum-ratio) receive combining toward the same
+ * direction, the stored receive weight vector should be proportional to the
+ * response a(theta), so that `w_rx^H` applies the conjugation at combining time
+ * sign = +1  (phase = +2*pi*(u·r_i)).
+ *
+ * Therefore, when this function is used to set beamforming vectors explicitly in
+ * this example:
+ *   - Use `sign = -1` when generating TX weights (precoding).
+ *   - Use `sign = +1` when generating RX weights (so that Hermitian combining
+ *     applies the conjugation).
+ *
+ * @param txMob The mobility model of the node for which the weights are generated.
+ * @param thisAntenna The antenna array on which the beamforming vector is set.
+ * @param rxMob The mobility model of the peer node toward/from which the beam is steered.
+ * @param sign Phase progression sign: typically -1 for TX weights and +1 for RX weights.
  */
 static void
-DoBeamforming(Ptr<NetDevice> thisDevice,
+DoBeamforming(Ptr<MobilityModel> txMob,
               Ptr<PhasedArrayModel> thisAntenna,
-              Ptr<NetDevice> otherDevice)
+              Ptr<MobilityModel> rxMob,
+              double sign)
 {
-    // retrieve the position of the two devices
-    Vector aPos = thisDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
-    Vector bPos = otherDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
+    // retrieve the position of the two nodes
+    Vector aPos = txMob->GetPosition();
+    Vector bPos = rxMob->GetPosition();
 
     // compute the azimuth and the elevation angles
     Angles completeAngle(bPos, aPos);
@@ -94,7 +143,7 @@
     for (uint64_t ind = 0; ind < totNoArrayElements; ind++)
     {
         Vector loc = thisAntenna->GetElementLocation(ind);
-        double phase = -2 * M_PI *
+        double phase = sign * 2 * M_PI *
                        (sinVAngleRadian * cosHAngleRadian * loc.x +
                         sinVAngleRadian * sinHAngleRadian * loc.y + cosVAngleRadian * loc.z);
         antennaWeights[ind] = exp(std::complex<double>(0, phase)) * power;
@@ -105,6 +154,76 @@
 }
 
 /**
+ * Build the RX matched-filter (maximum-ratio) combining vector by aggregating
+ * the contributions of all clusters and normalizing the result:
+ * \f[
+ * \mathbf{g} = \sum_{c} \mathbf{H}_{c}\mathbf{w}_{\rm tx}, \qquad
+ * \mathbf{w}_{\rm rx} = \frac{\mathbf{g}}{\|\mathbf{g}\|}
+ * \f]
+ *
+ * The per-cluster channel matrix \f$\mathbf{H}_{c}\f$ is obtained from
+ * ``params->m_channel(u, s, c)`` (u: RX element index, s: TX element index, c: cluster index).
+ *
+ * @param params Channel parameters containing the per-cluster channel matrix.
+ * @param txAntenna TX phased-array model.
+ * @param rxAntenna RX phased-array model.
+ * @param wTx Unit-norm TX beamforming/precoding vector (size must match
+ * ``txAntenna->GetNumElems()``).
+ *
+ * @return The RX combining vector (size ``rxAntenna->GetNumElems()``), normalized to unit norm
+ *         (unless the channel is all-zero, in which case a zero vector is returned).
+ */
+
+static ns3::PhasedArrayModel::ComplexVector
+BuildRxMatchedFilterOverAllClusters(
+    ns3::Ptr<const ns3::MatrixBasedChannelModel::ChannelMatrix> params,
+    ns3::Ptr<const ns3::PhasedArrayModel> txAntenna,
+    ns3::Ptr<const ns3::PhasedArrayModel> rxAntenna,
+    const ns3::PhasedArrayModel::ComplexVector& wTx)
+{
+    using C = std::complex<double>;
+
+    const uint64_t nTx = txAntenna->GetNumElems();
+    const uint64_t nRx = rxAntenna->GetNumElems();
+    const size_t numClusters = params->m_channel.GetNumPages();
+
+    NS_ASSERT_MSG(wTx.GetSize() == nTx, "wTx size does not match TX antenna elements");
+    NS_ASSERT_MSG(params->m_channel.GetNumRows() == nRx, "Channel rows != #RX elements");
+    NS_ASSERT_MSG(params->m_channel.GetNumCols() == nTx, "Channel cols != #TX elements");
+
+    ns3::PhasedArrayModel::ComplexVector g(nRx);
+
+    // g = sum_c (H_c * wTx)
+    for (size_t c = 0; c < numClusters; ++c)
+    {
+        for (uint64_t u = 0; u < nRx; ++u)
+        {
+            C acc{0.0, 0.0};
+            for (uint64_t s = 0; s < nTx; ++s)
+            {
+                acc += params->m_channel(u, s, c) * wTx[s];
+            }
+            g[u] += acc;
+        }
+    }
+    // Normalize: wRx = g / ||g||
+    double norm2 = 0.0;
+    for (uint64_t u = 0; u < nRx; ++u)
+    {
+        norm2 += std::norm(g[u]);
+    }
+    if (norm2 > 0.0)
+    {
+        const double invNorm = 1.0 / std::sqrt(norm2);
+        for (uint64_t u = 0; u < nRx; ++u)
+        {
+            g[u] *= invNorm;
+        }
+    }
+    return g; // unit-norm (unless channel is all-zero)
+}
+
+/**
  * Compute the average SNR
  * @param params A structure that holds the parameters that are needed to perform calculations in
  * ComputeSnr
@@ -140,22 +259,49 @@
     NS_ASSERT_MSG(params.txAntenna, "params.txAntenna is nullptr!");
     NS_ASSERT_MSG(params.rxAntenna, "params.rxAntenna is nullptr!");
 
+    // set/update the beamforming vectors on each call
+    double sign = -1.0;
+    DoBeamforming(params.txMob, params.txAntenna, params.rxMob, sign);
+    sign = +1.0;
+    DoBeamforming(params.rxMob, params.rxAntenna, params.txMob, sign);
     // apply the fast fading and the beamforming gain
     auto rxParams = m_spectrumLossModel->CalcRxPowerSpectralDensity(txParams,
                                                                     params.txMob,
                                                                     params.rxMob,
                                                                     params.txAntenna,
                                                                     params.rxAntenna);
-    auto rxPsd = rxParams->psd;
-    NS_LOG_DEBUG("Average rx power " << 10 * log10(Sum(*rxPsd) * 180e3) << " dB");
-
-    // compute the SNR
-    NS_LOG_DEBUG("Average SNR " << 10 * log10(Sum(*rxPsd) / Sum(*noisePsd)) << " dB");
+    auto rxPsdSteering = rxParams->psd;
+    NS_LOG_DEBUG("Average rx power " << 10 * log10(Sum(*rxPsdSteering) * 180e3) << " dB");
+    NS_LOG_DEBUG("Average SNR " << 10 * log10(Sum(*rxPsdSteering) / Sum(*noisePsd)) << " dB");
+
+    Ptr<ThreeGppChannelModel> channelModel =
+        DynamicCast<ThreeGppChannelModel>(m_spectrumLossModel->GetChannelModel());
+    Ptr<const ThreeGppChannelModel::ChannelMatrix> channelMatrix =
+        channelModel->GetChannel(params.txMob, params.rxMob, params.txAntenna, params.rxAntenna);
+
+    const auto& wTx = params.txAntenna->GetBeamformingVectorRef();
+    auto wRx =
+        BuildRxMatchedFilterOverAllClusters(channelMatrix, params.txAntenna, params.rxAntenna, wTx);
+    params.rxAntenna->SetBeamformingVector(wRx); // Install RX matched-filter (MRC) combiner
+
+    auto rxParamsMatchedFilterComb =
+        m_spectrumLossModel->CalcRxPowerSpectralDensity(txParams,
+                                                        params.txMob,
+                                                        params.rxMob,
+                                                        params.txAntenna,
+                                                        params.rxAntenna);
+
+    auto rxPsdMatchedFilterComb = rxParamsMatchedFilterComb->psd;
+    NS_LOG_DEBUG("Average rx power when using matched filter combining "
+                 << 10 * log10(Sum(*rxPsdMatchedFilterComb) * 180e3) << " dB");
+    NS_LOG_DEBUG("Average SNR when using matched filter combining "
+                 << 10 * log10(Sum(*rxPsdMatchedFilterComb) / Sum(*noisePsd)) << " dB");
 
     // print the SNR and pathloss values in the snr-trace.txt file
     std::ofstream f;
     f.open("snr-trace.txt", std::ios::out | std::ios::app);
-    f << Simulator::Now().GetSeconds() << " " << 10 * log10(Sum(*rxPsd) / Sum(*noisePsd)) << " "
+    f << Simulator::Now().GetSeconds() << " " << 10 * log10(Sum(*rxPsdSteering) / Sum(*noisePsd))
+      << " " << 10 * log10(Sum(*rxPsdMatchedFilterComb) / Sum(*noisePsd)) << " "
       << propagationGainDb << std::endl;
     f.close();
 }
@@ -171,6 +317,16 @@
     uint32_t timeRes = 10;        // time resolution in milliseconds
     std::string scenario = "UMa"; // 3GPP propagation scenario
 
+    CommandLine cmd(__FILE__);
+    cmd.AddValue("frequency", "Operating frequency in Hz", frequency);
+    cmd.AddValue("txPow", "TX power in dBm", txPow);
+    cmd.AddValue("noiseFigure", "Noise figure in dB", noiseFigure);
+    cmd.AddValue("distance", "Distance between TX and RX nodes in meters", distance);
+    cmd.AddValue("simTime", "Simulation time in milliseconds", simTime);
+    cmd.AddValue("timeRes", "Time resolution in milliseconds", timeRes);
+    cmd.AddValue("scenario", "3GPP propagation scenario", scenario);
+    cmd.Parse(argc, argv);
+
     Config::SetDefault("ns3::ThreeGppChannelModel::UpdatePeriod",
                        TimeValue(MilliSeconds(1))); // update the channel at each iteration
     Config::SetDefault("ns3::ThreeGppChannelConditionModel::UpdatePeriod",
@@ -239,21 +395,12 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models, set the positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(Vector(0.0, 0.0, 10.0));
-    Ptr<MobilityModel> rxMob = CreateObject<ConstantPositionMobilityModel>();
+    Ptr<ConstantVelocityMobilityModel> rxMob = CreateObject<ConstantVelocityMobilityModel>();
     rxMob->SetPosition(Vector(distance, 0.0, 1.6));
+    rxMob->SetVelocity(Vector(0.5, 0, 0)); // set small velocity to allow for channel updates
 
     // assign the mobility models to the nodes
     nodes.Get(0)->AggregateObject(txMob);
@@ -271,10 +418,6 @@
                                                        "NumRows",
                                                        UintegerValue(2));
 
-    // set the beamforming vectors
-    DoBeamforming(txDev, txAntenna, rxDev);
-    DoBeamforming(rxDev, rxAntenna, txDev);
-
     for (int i = 0; i < floor(simTime / timeRes); i++)
     {
         ComputeSnrParams params{txMob, rxMob, txPow, noiseFigure, txAntenna, rxAntenna};
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/examples/three-gpp-ntn-channel-example.cc ns-3.47/src/spectrum/examples/three-gpp-ntn-channel-example.cc
--- ns-3.46/src/spectrum/examples/three-gpp-ntn-channel-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/examples/three-gpp-ntn-channel-example.cc	2026-02-16 16:08:49.821230600 -0800
@@ -12,7 +12,7 @@
  * Specifically, most changes (which are also highlighted throughout the code)
  * impact the main method, and comprise:
  * - the configuration of ad-hoc propagation and channel condition models;
- * - the use of GeocentricConstantPositionMobilityModel for the nodes mobility.
+ * - the use of GeocentricConstantPositionMobilityModel for the nodes' mobility.
  * The pre-configured parameters are the one provided by 3GPP in TR 38.821,
  * more specifically scenario 10 in down-link mode.
  * Two static nodes, one on the ground and one in orbit, communicate with each other.
@@ -21,17 +21,12 @@
  */
 
 #include "ns3/antenna-model.h"
-#include "ns3/channel-condition-model.h"
-#include "ns3/constant-position-mobility-model.h"
 #include "ns3/core-module.h"
 #include "ns3/geocentric-constant-position-mobility-model.h"
 #include "ns3/isotropic-antenna-model.h"
 #include "ns3/mobility-model.h"
-#include "ns3/net-device.h"
 #include "ns3/node-container.h"
 #include "ns3/node.h"
-#include "ns3/parabolic-antenna-model.h"
-#include "ns3/simple-net-device.h"
 #include "ns3/spectrum-signal-parameters.h"
 #include "ns3/three-gpp-channel-model.h"
 #include "ns3/three-gpp-propagation-loss-model.h"
@@ -195,21 +190,15 @@
 
 /**
  * Perform the beamforming using the DFT beamforming method
- * @param thisDevice the device performing the beamforming
- * @param thisAntenna the antenna object associated to thisDevice
- * @param otherDevice the device towards which point the beam
+ * @param txMob the mobility model of the node performing the beamforming
+ * @param thisAntenna the antenna object associated to txMob
+ * @param rxMob the mobility model of the node towards which will point the beam
  */
 static void
-DoBeamforming(Ptr<NetDevice> thisDevice,
-              Ptr<PhasedArrayModel> thisAntenna,
-              Ptr<NetDevice> otherDevice)
+DoBeamforming(Ptr<MobilityModel> rxMob, Ptr<PhasedArrayModel> thisAntenna, Ptr<MobilityModel> txMob)
 {
-    // retrieve the position of the two devices
-    Vector aPos = thisDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
-    Vector bPos = otherDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
-
     // compute the azimuth and the elevation angles
-    Angles completeAngle(bPos, aPos);
+    Angles completeAngle(txMob->GetPosition(), rxMob->GetPosition());
     double hAngleRadian = completeAngle.GetAzimuth();
     double vAngleRadian = completeAngle.GetInclination(); // the elevation angle
 
@@ -405,16 +394,6 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // Start changes with respect to three-gpp-channel-example, here a mobility model
     // tailored to NTN scenarios is used (GeocentricConstantPositionMobilityModel)
     // create the tx and rx mobility models, set the positions
@@ -467,8 +446,8 @@
     // End changes with respect to three-gpp-channel-example
 
     // set the beamforming vectors
-    DoBeamforming(rxDev, rxAntenna, txDev);
-    DoBeamforming(txDev, txAntenna, rxDev);
+    DoBeamforming(rxMob, rxAntenna, txMob);
+    DoBeamforming(txMob, txAntenna, rxMob);
 
     // Open the output results file
     resultsFile.open("ntn-snr-trace.txt", std::ios::out);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/model/friis-spectrum-propagation-loss.h ns-3.47/src/spectrum/model/friis-spectrum-propagation-loss.h
--- ns-3.46/src/spectrum/model/friis-spectrum-propagation-loss.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/model/friis-spectrum-propagation-loss.h	2026-02-16 16:08:49.822230800 -0800
@@ -21,9 +21,7 @@
  * @brief Friis spectrum propagation loss model
  *
  * The propagation loss is calculated according to a simplified version of Friis'
- * formula in which antenna gains are unitary:
- *
- * \f$ L = \frac{4 \pi * d * f}{C^2}\f$
+ * formula in which antenna gains are unitary: \f$L = \frac{4 \pi * d * f}{C^2}\f$
  *
  * where C = 3e8 m/s is the light speed in the vacuum. The intended
  * use is to calculate Prx = Ptx * G
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/model/three-gpp-channel-model.cc ns-3.47/src/spectrum/model/three-gpp-channel-model.cc
--- ns-3.46/src/spectrum/model/three-gpp-channel-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/model/three-gpp-channel-model.cc	2026-02-16 16:08:49.824230700 -0800
@@ -3,13 +3,14 @@
  * University of Padova
  * Copyright (c) 2015, NYU WIRELESS, Tandon School of Engineering,
  * New York University
+ * Copyright (c) 2026, CTTC, Centre Tecnologic de Telecomunicacions de Catalunya
  *
  * SPDX-License-Identifier: GPL-2.0-only
  *
  */
-
 #include "three-gpp-channel-model.h"
 
+#include "ns3/boolean.h"
 #include "ns3/double.h"
 #include "ns3/geocentric-constant-position-mobility-model.h"
 #include "ns3/integer.h"
@@ -29,17 +30,32 @@
 
 namespace ns3
 {
-
 NS_LOG_COMPONENT_DEFINE("ThreeGppChannelModel");
 
 NS_OBJECT_ENSURE_REGISTERED(ThreeGppChannelModel);
 
 /// Conversion factor: degrees to radians
-static constexpr double DEG2RAD = M_PI / 180.0;
+constexpr double DEG2RAD = M_PI / 180.0;
+/**
+ * Maximum 2D displacement (in meters) allowed for a single channel-consistency update step.
+ *
+ * This value reflects the TR 38.901 guidance to limit the update displacement to within 1 m.
+ * If the effective endpoint displacement exceeds this threshold, ns-3 regenerates the channel
+ * parameters (fallback) rather than attempting a consistency update over a larger step.
+ */
+constexpr double kMaxConsistencyStepMeters = 1.0;
+
+/**
+ * Small threshold (in meters) used to treat very small displacements as zero.
+ *
+ * This avoids triggering channel-consistency updates due to numerical noise when the effective
+ * endpoint displacement is negligible.
+ */
+static constexpr double kNoDisplacementEpsMeters = 1e-6;
 
 /// The ray offset angles within a cluster, given for rms angle spread normalized to 1.
 /// (Table 7.5-3)
-static constexpr std::array<double, 20> offSetAlpha = {
+static constexpr std::array offSetAlpha = {
     0.0447, -0.0447, 0.1413, -0.1413, 0.2492, -0.2492, 0.3715, -0.3715, 0.5129, -0.5129,
     0.6797, -0.6797, 0.8844, -0.8844, 1.1481, -1.1481, 1.5195, -1.5195, 2.1551, -2.1551,
 };
@@ -65,9 +81,7 @@
 /**
  * The square root matrix for <em>RMa NLOS</em>, which is generated using the
  * Cholesky decomposition according to table 7.5-6 Part 2 and follows the order
- * of [SF, K, DS, ASD, ASA, ZSD, ZSA].
- * The parameter K is ignored.
- *
+ * of [SF, DS, ASD, ASA, ZSD, ZSA].
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
  */
@@ -118,8 +132,7 @@
 /**
  * The square root matrix for <em>UMa NLOS</em>, which is generated using the
  * Cholesky decomposition according to table 7.5-6 Part 1 and follows the order
- * of [SF, K, DS, ASD, ASA, ZSD, ZSA].
- * The parameter K is ignored.
+ * of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -136,7 +149,7 @@
 /**
  * The square root matrix for <em>UMa O2I</em>, which is generated using the
  * Cholesky decomposition according to table 7.5-6 Part 1 and follows the order
- * of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -171,8 +184,7 @@
 /**
  * The square root matrix for <em>UMi NLOS</em>, which is generated using the
  * Cholesky decomposition according to table 7.5-6 Part 1 and follows the order
- * of [SF, K, DS, ASD, ASA, ZSD, ZSA].
- * The parameter K is ignored.
+ * of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -189,7 +201,7 @@
 /**
  * The square root matrix for <em>UMi O2I</em>, which is generated using the
  * Cholesky decomposition according to table 7.5-6 Part 1 and follows the order
- * of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -224,8 +236,7 @@
 /**
  * The square root matrix for <em>Indoor-Office NLOS</em>, which is generated
  * using the Cholesky decomposition according to table 7.5-6 Part 2 and follows
- * the order of [SF, K, DS, ASD, ASA, ZSD, ZSA].
- * The parameter K is ignored.
+ * the order of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -260,7 +271,7 @@
 /**
  * The square root matrix for <em>NTN Dense Urban NLOS</em>, which is generated
  * using the Cholesky decomposition according to 3GPP TR 38.811 v15.4.0 table 6.7.2-2 and follows
- * the order of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * the order of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -295,7 +306,7 @@
 /**
  * The square root matrix for <em>NTN Urban NLOS</em>, which is generated
  * using the Cholesky decomposition according to 3GPP TR 38.811 v15.4.0 table 6.7.2-4 and follows
- * the order of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * the order of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The square root matrix is dependent on the elevation angle, thus requiring a map.
  *
@@ -407,7 +418,7 @@
 /**
  * The square root matrix for <em>NTN Suburban NLOS</em>, which is generated
  * using the Cholesky decomposition according to 3GPP TR 38.811 v15.4.0 table 6.7.2-6 and follows
- * the order of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * the order of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -442,7 +453,7 @@
 /**
  * The square root matrix for <em>NTN Rural NLOS S Band</em>, which is generated
  * using the Cholesky decomposition according to 3GPP TR 38.811 v15.4.0 table 6.7.2-8a and follows
- * the order of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * the order of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The square root matrix is dependent on the elevation angle, thus requiring a map.
  *
@@ -536,10 +547,10 @@
 /**
  * The square root matrix for <em>NTN Rural NLOS Ka Band</em>, which is generated
  * using the Cholesky decomposition according to 3GPP TR 38.811 v15.4.0 table 6.7.2-8b and follows
- * the order of [SF, K, DS, ASD, ASA, ZSD, ZSA].
+ * the order of [SF, DS, ASD, ASA, ZSD, ZSA].
  *
  * The square root matrix is dependent on the elevation angle, which acts as the corresponding map's
- * key..
+ * key.
  *
  * The Matlab file to generate the matrices can be found in
  * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
@@ -1065,6 +1076,43 @@
      }},
 };
 
+/**
+ * According to table 7.5-6, only cluster number equal to 8, 10, 11, 12, 19 and 20 is valid.
+ * (38.901) Not sure why the other cases are in Table 7.5-2. Added case 2 and 3 for the NTN
+ * according to table 6.7.2-1aa (28.811)
+ */
+static const std::unordered_map<int, double> cNlosTablePhi = {
+    {2, 0.501},
+    {3, 0.680},
+    {4, 0.779},
+    {5, 0.860},
+    {8, 1.018},
+    {10, 1.090},
+    {11, 1.123},
+    {12, 1.146},
+    {14, 1.190},
+    {15, 1.221},
+    {16, 1.226},
+    {19, 1.273},
+    {20, 1.289},
+};
+/**
+ * Table 7.5-4 (38.901)
+ * Added case 2, 3 and 4 for the NTN according to table 6.7.2-1ab (28.811)
+ */
+static const std::unordered_map<int, double> cNlosTableTheta = {
+    {2, 0.430},
+    {3, 0.594},
+    {4, 0.697},
+    {8, 0.889},
+    {10, 0.957},
+    {11, 1.031},
+    {12, 1.104},
+    {15, 1.1088},
+    {19, 1.184},
+    {20, 1.178},
+};
+
 ThreeGppChannelModel::ThreeGppChannelModel()
 {
     NS_LOG_FUNCTION(this);
@@ -1123,11 +1171,16 @@
                           MakePointerAccessor(&ThreeGppChannelModel::SetChannelConditionModel,
                                               &ThreeGppChannelModel::GetChannelConditionModel),
                           MakePointerChecker<ChannelConditionModel>())
-            .AddAttribute("UpdatePeriod",
-                          "Specify the channel coherence time",
-                          TimeValue(MilliSeconds(0)),
-                          MakeTimeAccessor(&ThreeGppChannelModel::m_updatePeriod),
-                          MakeTimeChecker())
+            .AddAttribute(
+                "UpdatePeriod",
+                "Spatial-consistency update period. When non-zero, ns-3 attempts to evolve cached "
+                "channel parameters using update equations aligned with 3GPP TR 38.901 "
+                "Procedure A (Sec. 7.6.3.2) whenever the channel is evaluated and the period has "
+                "elapsed. If the 1 m step-size constraint is violated at an update attempt, the "
+                "model re-generates channel parameters (new realization).",
+                TimeValue(MilliSeconds(0)),
+                MakeTimeAccessor(&ThreeGppChannelModel::m_updatePeriod),
+                MakeTimeChecker())
             // attributes for the blockage model
             .AddAttribute("Blockage",
                           "Enable blockage model A (sec 7.6.4.1)",
@@ -1156,9 +1209,7 @@
                           "delayed (reflected) paths",
                           DoubleValue(0.0),
                           MakeDoubleAccessor(&ThreeGppChannelModel::m_vScatt),
-                          MakeDoubleChecker<double>(0.0))
-
-        ;
+                          MakeDoubleChecker<double>(0.0));
     return tid;
 }
 
@@ -1177,7 +1228,7 @@
 }
 
 void
-ThreeGppChannelModel::SetFrequency(double f)
+ThreeGppChannelModel::SetFrequency(const double f)
 {
     NS_LOG_FUNCTION(this);
     NS_ASSERT_MSG(f >= 500.0e6 && f <= 100.0e9,
@@ -1215,8 +1266,8 @@
 }
 
 Ptr<const ThreeGppChannelModel::ParamsTable>
-ThreeGppChannelModel::GetThreeGppTable(const Ptr<const MobilityModel> aMob,
-                                       const Ptr<const MobilityModel> bMob,
+ThreeGppChannelModel::GetThreeGppTable(Ptr<const MobilityModel> aMob,
+                                       Ptr<const MobilityModel> bMob,
                                        Ptr<const ChannelCondition> channelCondition) const
 {
     NS_LOG_FUNCTION(this);
@@ -1270,6 +1321,8 @@
             table3gpp->m_uXpr = 12;
             table3gpp->m_sigXpr = 4;
             table3gpp->m_perClusterShadowingStd = 3;
+            table3gpp->m_perClusterRayDcorDistance = 50;
+            table3gpp->m_blockerDcorDistance = 10;
 
             for (uint8_t row = 0; row < 7; row++)
             {
@@ -1305,6 +1358,8 @@
             table3gpp->m_uXpr = 7;
             table3gpp->m_sigXpr = 3;
             table3gpp->m_perClusterShadowingStd = 3;
+            table3gpp->m_perClusterRayDcorDistance = 60;
+            table3gpp->m_blockerDcorDistance = 10;
 
             for (uint8_t row = 0; row < 6; row++)
             {
@@ -1340,6 +1395,8 @@
             table3gpp->m_uXpr = 7;
             table3gpp->m_sigXpr = 3;
             table3gpp->m_perClusterShadowingStd = 3;
+            table3gpp->m_perClusterRayDcorDistance = 15;
+            table3gpp->m_blockerDcorDistance = 5;
 
             for (uint8_t row = 0; row < 6; row++)
             {
@@ -1378,6 +1435,8 @@
             table3gpp->m_uXpr = 8;
             table3gpp->m_sigXpr = 4;
             table3gpp->m_perClusterShadowingStd = 3;
+            table3gpp->m_perClusterRayDcorDistance = 40;
+            table3gpp->m_blockerDcorDistance = 10;
 
             for (uint8_t row = 0; row < 7; row++)
             {
@@ -1423,6 +1482,8 @@
                 table3gpp->m_uXpr = 7;
                 table3gpp->m_sigXpr = 3;
                 table3gpp->m_perClusterShadowingStd = 3;
+                table3gpp->m_perClusterRayDcorDistance = 50;
+                table3gpp->m_blockerDcorDistance = 10;
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -1457,6 +1518,8 @@
                 table3gpp->m_uXpr = 9;
                 table3gpp->m_sigXpr = 5;
                 table3gpp->m_perClusterShadowingStd = 4;
+                table3gpp->m_perClusterRayDcorDistance = 15;
+                table3gpp->m_blockerDcorDistance = 5;
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -1496,6 +1559,8 @@
             table3gpp->m_uXpr = 9;
             table3gpp->m_sigXpr = 3;
             table3gpp->m_perClusterShadowingStd = 3;
+            table3gpp->m_perClusterRayDcorDistance = 12;
+            table3gpp->m_blockerDcorDistance = 10;
 
             for (uint8_t row = 0; row < 7; row++)
             {
@@ -1535,6 +1600,8 @@
                 table3gpp->m_uXpr = 8;
                 table3gpp->m_sigXpr = 3;
                 table3gpp->m_perClusterShadowingStd = 3;
+                table3gpp->m_perClusterRayDcorDistance = 15;
+                table3gpp->m_blockerDcorDistance = 10;
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -1569,6 +1636,8 @@
                 table3gpp->m_uXpr = 9;
                 table3gpp->m_sigXpr = 5;
                 table3gpp->m_perClusterShadowingStd = 4;
+                table3gpp->m_perClusterRayDcorDistance = 15;
+                table3gpp->m_blockerDcorDistance = 5;
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -1608,6 +1677,8 @@
             table3gpp->m_uXpr = 11;
             table3gpp->m_sigXpr = 4;
             table3gpp->m_perClusterShadowingStd = 6;
+            table3gpp->m_perClusterRayDcorDistance = 10;
+            table3gpp->m_blockerDcorDistance = 5;
 
             for (uint8_t row = 0; row < 7; row++)
             {
@@ -1642,6 +1713,8 @@
             table3gpp->m_uXpr = 10;
             table3gpp->m_sigXpr = 4;
             table3gpp->m_perClusterShadowingStd = 3;
+            table3gpp->m_perClusterRayDcorDistance = 10;
+            table3gpp->m_blockerDcorDistance = 5;
 
             for (uint8_t row = 0; row < 6; row++)
             {
@@ -1878,12 +1951,12 @@
     }
     else if (m_scenario.substr(0, 3) == "NTN")
     {
-        std::string freqBand = (fcGHz < 13) ? "S" : "Ka";
+        std::string freqBand = fcGHz < 13 ? "S" : "Ka";
 
         double elevAngle = 0;
         bool isSatellite = false; // flag to indicate if one of the two nodes is a satellite
-                                  // if so, parameters will be set accordingly to NOTE 8 of
-                                  // Table 6.7.2 from 3GPP 38.811 V15.4.0 (2020-09)
+        // if so, parameters will be set accordingly to NOTE 8 of
+        // Table 6.7.2 from 3GPP 38.811 V15.4.0 (2020-09)
 
         Ptr<MobilityModel> aMobNonConst = ConstCast<MobilityModel>(aMob);
         Ptr<MobilityModel> bMobNonConst = ConstCast<MobilityModel>(bMob);
@@ -1892,7 +1965,7 @@
                 ConstCast<MobilityModel>(aMob)) && // Transform to NS_ASSERT
             DynamicCast<GeocentricConstantPositionMobilityModel>(
                 ConstCast<MobilityModel>(bMob))) // check if aMob and bMob are of type
-                                                 // GeocentricConstantPositionMobilityModel
+        // GeocentricConstantPositionMobilityModel
         {
             Ptr<GeocentricConstantPositionMobilityModel> aNTNMob =
                 DynamicCast<GeocentricConstantPositionMobilityModel>(aMobNonConst);
@@ -1923,65 +1996,50 @@
         }
 
         // Round the elevation angle into a two-digits integer between 10 and 90.
-        int elevAngleQuantized = (elevAngle < 10) ? 10 : round(elevAngle / 10) * 10;
+        int elevAngleQuantized = elevAngle < 10 ? 10 : round(elevAngle / 10) * 10;
 
         if (m_scenario == "NTN-DenseUrban")
         {
             if (channelCondition->IsLos())
             {
-                table3gpp->m_uLgDS =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
+                table3gpp->m_uLgDS = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
                 table3gpp->m_sigLgDS =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
 
                 // table3gpp->m_uLgASD=-1.79769e+308;  //FOR SATELLITES
-                table3gpp->m_uLgASD =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
+                table3gpp->m_uLgASD = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
                 // table3gpp->m_sigLgASD=0; //FOR SATELLITES
                 table3gpp->m_sigLgASD =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
 
-                table3gpp->m_uLgASA =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
+                table3gpp->m_uLgASA = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
                 table3gpp->m_sigLgASA =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
                 table3gpp->m_sigLgZSA =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
 
                 // table3gpp->m_uLgZSD=-1.79769e+308;  //FOR SATELLITES
-                table3gpp->m_uLgZSD =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
+                table3gpp->m_uLgZSD = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
                 // table3gpp->m_sigLgZSD= 0; //FOR SATELLITES
                 table3gpp->m_sigLgZSD =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
 
-                table3gpp->m_uK =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
-                table3gpp->m_numOfCluster = NTNDenseUrbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNDenseUrbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] *
-                    1e-9;
-                table3gpp->m_cASD =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNDenseUrbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                table3gpp->m_uK = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
+                table3gpp->m_numOfCluster =
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNDenseUrbanLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 7; row++)
                 {
@@ -1994,47 +2052,35 @@
             else if (channelCondition->IsNlos())
             {
                 NS_LOG_UNCOND("Dense Urban NLOS");
-                table3gpp->m_uLgDS =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
+                table3gpp->m_uLgDS = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
                 table3gpp->m_sigLgDS =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
-                table3gpp->m_sigLgASD = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
-                table3gpp->m_sigLgASA = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
-                table3gpp->m_sigLgZSA = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
-                table3gpp->m_sigLgZSD = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_rTau =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
-                table3gpp->m_numOfCluster = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
+                table3gpp->m_sigLgASD =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
+                table3gpp->m_sigLgASA =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
+                table3gpp->m_sigLgZSA =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
+                table3gpp->m_sigLgZSD =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_rTau = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
+                table3gpp->m_numOfCluster =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
                 table3gpp->m_cDS =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] *
-                    1e-9;
-                table3gpp->m_cASD =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNDenseUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNDenseUrbanNLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -2049,50 +2095,31 @@
         {
             if (channelCondition->IsLos())
             {
-                table3gpp->m_uLgDS =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
-                table3gpp->m_sigLgDS =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
-                table3gpp->m_sigLgASD =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
-                table3gpp->m_sigLgASA =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
-                table3gpp->m_sigLgZSA =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
-                table3gpp->m_sigLgZSD =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_uK =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
+                table3gpp->m_uLgDS = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
+                table3gpp->m_sigLgDS = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
+                table3gpp->m_sigLgASD = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
+                table3gpp->m_sigLgASA = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
+                table3gpp->m_sigLgZSA = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
+                table3gpp->m_sigLgZSD = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_uK = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
                 table3gpp->m_numOfCluster =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNUrbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] * 1e-9;
-                table3gpp->m_cASD =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNUrbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNUrbanLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 7; row++)
                 {
@@ -2104,50 +2131,31 @@
             }
             else if (channelCondition->IsNlos())
             {
-                table3gpp->m_uLgDS =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
-                table3gpp->m_sigLgDS =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
-                table3gpp->m_sigLgASD =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
-                table3gpp->m_sigLgASA =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
-                table3gpp->m_sigLgZSA =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
-                table3gpp->m_sigLgZSD =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_uK =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
+                table3gpp->m_uLgDS = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
+                table3gpp->m_sigLgDS = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
+                table3gpp->m_sigLgASD = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
+                table3gpp->m_sigLgASA = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
+                table3gpp->m_sigLgZSA = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
+                table3gpp->m_sigLgZSD = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_uK = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
                 table3gpp->m_numOfCluster =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] * 1e-9;
-                table3gpp->m_cASD =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNUrbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNUrbanNLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -2163,50 +2171,35 @@
         {
             if (channelCondition->IsLos())
             {
-                table3gpp->m_uLgDS =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
-                table3gpp->m_sigLgDS =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
+                table3gpp->m_uLgDS = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
+                table3gpp->m_sigLgDS = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
                 table3gpp->m_sigLgASD =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
                 table3gpp->m_sigLgASA =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
                 table3gpp->m_sigLgZSA =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
                 table3gpp->m_sigLgZSD =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_uK =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
-                table3gpp->m_numOfCluster = NTNSuburbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNSuburbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] * 1e-9;
-                table3gpp->m_cASD =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNSuburbanLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_uK = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
+                table3gpp->m_numOfCluster =
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNSuburbanLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 7; row++)
                 {
@@ -2218,51 +2211,35 @@
             }
             else if (channelCondition->IsNlos())
             {
-                table3gpp->m_uLgDS =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
-                table3gpp->m_sigLgDS =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
+                table3gpp->m_uLgDS = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
+                table3gpp->m_sigLgDS = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
                 table3gpp->m_sigLgASD =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
                 table3gpp->m_sigLgASA =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
                 table3gpp->m_sigLgZSA =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
                 table3gpp->m_sigLgZSD =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_uK =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
-                table3gpp->m_numOfCluster = NTNSuburbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNSuburbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] *
-                    1e-9;
-                table3gpp->m_cASD =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNSuburbanNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_uK = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
+                table3gpp->m_numOfCluster =
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNSuburbanNLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 6; row++)
                 {
@@ -2277,50 +2254,31 @@
         {
             if (channelCondition->IsLos())
             {
-                table3gpp->m_uLgDS =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
-                table3gpp->m_sigLgDS =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
-                table3gpp->m_sigLgASD =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
-                table3gpp->m_sigLgASA =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
-                table3gpp->m_sigLgZSA =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
-                table3gpp->m_sigLgZSD =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_uK =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
+                table3gpp->m_uLgDS = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
+                table3gpp->m_sigLgDS = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
+                table3gpp->m_sigLgASD = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
+                table3gpp->m_sigLgASA = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
+                table3gpp->m_sigLgZSA = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
+                table3gpp->m_sigLgZSD = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_uK = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
                 table3gpp->m_numOfCluster =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNRuralLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] * 1e-9;
-                table3gpp->m_cASD =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNRuralLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNRuralLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 for (uint8_t row = 0; row < 7; row++)
                 {
@@ -2332,50 +2290,31 @@
             }
             else if (channelCondition->IsNlos())
             {
-                table3gpp->m_uLgDS =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgDS];
-                table3gpp->m_sigLgDS =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgDS];
-                table3gpp->m_uLgASD =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASD];
-                table3gpp->m_sigLgASD =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASD];
-                table3gpp->m_uLgASA =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgASA];
-                table3gpp->m_sigLgASA =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgASA];
-                table3gpp->m_uLgZSA =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSA];
-                table3gpp->m_sigLgZSA =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSA];
-                table3gpp->m_uLgZSD =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uLgZSD];
-                table3gpp->m_sigLgZSD =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigLgZSD];
-                table3gpp->m_uK =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uK];
-                table3gpp->m_sigK =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigK];
-                table3gpp->m_rTau =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::rTau];
-                table3gpp->m_uXpr =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::uXpr];
-                table3gpp->m_sigXpr =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::sigXpr];
+                table3gpp->m_uLgDS = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uLgDS];
+                table3gpp->m_sigLgDS = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigLgDS];
+                table3gpp->m_uLgASD = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uLgASD];
+                table3gpp->m_sigLgASD = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASD];
+                table3gpp->m_uLgASA = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uLgASA];
+                table3gpp->m_sigLgASA = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigLgASA];
+                table3gpp->m_uLgZSA = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSA];
+                table3gpp->m_sigLgZSA = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSA];
+                table3gpp->m_uLgZSD = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uLgZSD];
+                table3gpp->m_sigLgZSD = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigLgZSD];
+                table3gpp->m_uK = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uK];
+                table3gpp->m_sigK = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigK];
+                table3gpp->m_rTau = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[rTau];
+                table3gpp->m_uXpr = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[uXpr];
+                table3gpp->m_sigXpr = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[sigXpr];
                 table3gpp->m_numOfCluster =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::numOfCluster];
-                table3gpp->m_raysPerCluster = NTNRuralNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::raysPerCluster];
-                table3gpp->m_cDS =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cDS] * 1e-9;
-                table3gpp->m_cASD =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASD];
-                table3gpp->m_cASA =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cASA];
-                table3gpp->m_cZSA =
-                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[Table3gppParams::cZSA];
-                table3gpp->m_perClusterShadowingStd = NTNRuralNLOS.at(freqBand).at(
-                    elevAngleQuantized)[Table3gppParams::perClusterShadowingStd];
+                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[numOfCluster];
+                table3gpp->m_raysPerCluster =
+                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[raysPerCluster];
+                table3gpp->m_cDS = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[cDS] * 1e-9;
+                table3gpp->m_cASD = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[cASD];
+                table3gpp->m_cASA = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[cASA];
+                table3gpp->m_cZSA = NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[cZSA];
+                table3gpp->m_perClusterShadowingStd =
+                    NTNRuralNLOS.at(freqBand).at(elevAngleQuantized)[perClusterShadowingStd];
 
                 if (freqBand == "S")
                 {
@@ -2420,53 +2359,151 @@
 }
 
 bool
-ThreeGppChannelModel::ChannelParamsNeedsUpdate(Ptr<const ThreeGppChannelParams> channelParams,
-                                               Ptr<const ChannelCondition> channelCondition) const
+ThreeGppChannelModel::NewChannelParamsNeeded(uint64_t channelParamsKey,
+                                             Ptr<const ChannelCondition> condition,
+                                             Ptr<const MobilityModel> aMob,
+                                             Ptr<const MobilityModel> bMob) const
 {
     NS_LOG_FUNCTION(this);
 
-    bool update = false;
-
-    // if the channel condition is different the channel has to be updated
-    if (!channelCondition->IsEqual(channelParams->m_losCondition, channelParams->m_o2iCondition))
+    if (const auto it = m_channelParamsMap.find(channelParamsKey); it == m_channelParamsMap.end())
     {
-        NS_LOG_DEBUG("Update the channel condition");
-        update = true;
+        NS_LOG_DEBUG("New channel parameters will be created for the first time for this link:"
+                     << channelParamsKey);
+        return true;
     }
-
-    // if the coherence time is over the channel has to be updated
-    if (!m_updatePeriod.IsZero() &&
-        Simulator::Now() - channelParams->m_generatedTime > m_updatePeriod)
+    else
     {
-        NS_LOG_DEBUG("Generation time " << channelParams->m_generatedTime.As(Time::NS) << " now "
-                                        << Now().As(Time::NS));
-        update = true;
+        if (!m_updatePeriod.IsZero())
+        {
+            const double endpointDisplacement =
+                ComputeEndpointDisplacement2d(aMob,
+                                              bMob,
+                                              it->second->m_lastPositionFirst,
+                                              it->second->m_lastPositionSecond);
+
+            if (endpointDisplacement > kMaxConsistencyStepMeters)
+            {
+                NS_LOG_DEBUG("New channel parameters needed because endpoint displacement > 1 m");
+                return true;
+            }
+        }
+
+        if (!condition->IsEqual(it->second->m_losCondition, it->second->m_o2iCondition))
+        {
+            NS_LOG_DEBUG("New channel parameters needed because LOS or O2I condition changed");
+            return true;
+        }
     }
+    return false;
+}
 
-    return update;
+bool
+ThreeGppChannelModel::NewChannelMatrixNeeded(const uint64_t channelMatrixKey,
+                                             Ptr<const ThreeGppChannelParams> channelParams,
+                                             Ptr<const PhasedArrayModel> aAntenna,
+                                             Ptr<const PhasedArrayModel> bAntenna) const
+{
+    NS_LOG_FUNCTION(this);
+    if (const auto itMatrix = m_channelMatrixMap.find(channelMatrixKey);
+        itMatrix == m_channelMatrixMap.end())
+    {
+        NS_LOG_DEBUG("Generate a new channel matrix. Matrix not found.");
+        // This function call is needed because of the API of PhasedArrayModel/UniformPlannarArray,
+        // which assumes that this function is called when the channel is created
+        const bool channelNeedsToBeCreated = aAntenna->IsChannelOutOfDate(bAntenna);
+        NS_ASSERT_MSG(channelNeedsToBeCreated,
+                      "This assert is to ensure that this code stays aligned with "
+                      "PhasedArrayModel/UniformPlannarArray "
+                      "API, which expects that this function is being called on the initial "
+                      "channel matrix creation.");
+        return true;
+    }
+    else
+    {
+        const Time paramsT = channelParams->m_generatedTime;
+        const Time matrixT = itMatrix->second->m_generatedTime;
+        const bool paramsNewer = paramsT > matrixT;
+        const bool antennaChanged = AntennaSetupChanged(aAntenna, bAntenna, itMatrix->second);
+
+        // channel parameters changed
+        if (paramsNewer || antennaChanged)
+        {
+            NS_LOG_DEBUG("Generate a new channel matrix: paramsNewer="
+                         << paramsNewer << " antennaChanged=" << antennaChanged << " paramsT="
+                         << paramsT.As(Time::NS) << " matrixT=" << matrixT.As(Time::NS));
+            return true;
+        }
+        return false;
+    }
 }
 
 bool
-ThreeGppChannelModel::ChannelMatrixNeedsUpdate(Ptr<const ThreeGppChannelParams> channelParams,
-                                               Ptr<const ChannelMatrix> channelMatrix)
+ThreeGppChannelModel::ChannelUpdateNeeded(Ptr<const ThreeGppChannelParams> channelParams,
+                                          Ptr<const MobilityModel> aMob,
+                                          Ptr<const MobilityModel> bMob) const
 {
-    return channelParams->m_generatedTime > channelMatrix->m_generatedTime;
+    NS_LOG_FUNCTION(this);
+    // Channel updates are disabled
+    if (m_updatePeriod.IsZero())
+    {
+        return false;
+    }
+
+    const double endpointDisplacement =
+        ComputeEndpointDisplacement2d(aMob,
+                                      bMob,
+                                      channelParams->m_lastPositionFirst,
+                                      channelParams->m_lastPositionSecond);
+
+    // No endpoint displacement -> no channel consistency update needed
+    if (endpointDisplacement < kNoDisplacementEpsMeters)
+    {
+        return false;
+    }
+
+    NS_ABORT_MSG_IF(endpointDisplacement > kMaxConsistencyStepMeters,
+                    "Endpoint displacement exceeded 1 m; channel parameters should have been "
+                    "regenerated before attempting a consistency update.");
+
+    // if the spatial consistency update period has elapsed, update the channel
+    if (Simulator::Now() - channelParams->m_generatedTime > m_updatePeriod)
+    {
+        NS_LOG_DEBUG("Updating channel generated at "
+                     << channelParams->m_generatedTime.As(Time::NS));
+        return true;
+    }
+    return false;
 }
 
 bool
 ThreeGppChannelModel::AntennaSetupChanged(Ptr<const PhasedArrayModel> aAntenna,
                                           Ptr<const PhasedArrayModel> bAntenna,
-                                          Ptr<const ChannelMatrix> channelMatrix)
+                                          Ptr<const ChannelMatrix> channelMatrix) const
 {
+    NS_LOG_FUNCTION(this);
     // This allows changing the number of antenna ports during execution,
     // which is used by nr's initial association.
-    size_t sAntNumElems = aAntenna->GetNumElems();
-    size_t uAntNumElems = bAntenna->GetNumElems();
-    size_t chanNumRows = channelMatrix->m_channel.GetNumRows();
-    size_t chanNumCols = channelMatrix->m_channel.GetNumCols();
-    return (((uAntNumElems != chanNumRows) || (sAntNumElems != chanNumCols)) &&
-            ((uAntNumElems != chanNumCols) || (sAntNumElems != chanNumRows))) ||
-           aAntenna->IsChannelOutOfDate(bAntenna);
+    const size_t sAntNumElems = aAntenna->GetNumElems();
+    const size_t uAntNumElems = bAntenna->GetNumElems();
+    const size_t chanNumRows = channelMatrix->m_channel.GetNumRows();
+    const size_t chanNumCols = channelMatrix->m_channel.GetNumCols();
+    const bool dimsMatchNormal = uAntNumElems == chanNumRows && sAntNumElems == chanNumCols;
+    const bool dimsMatchSwapped = uAntNumElems == chanNumCols && sAntNumElems == chanNumRows;
+    const bool dimsMismatch = !(dimsMatchNormal || dimsMatchSwapped);
+    const bool channelOutOfDate = aAntenna->IsChannelOutOfDate(bAntenna);
+    const bool changed = dimsMismatch || channelOutOfDate;
+
+    if (changed)
+    {
+        NS_LOG_DEBUG("AntennaSetupChanged: dimsMismatch="
+                     << dimsMismatch << " channelOutOfDate=" << channelOutOfDate
+                     << " uAntNumElems=" << uAntNumElems << " sAntNumElems=" << sAntNumElems
+                     << " chanNumRows=" << chanNumRows << " chanNumCols=" << chanNumCols
+                     << " aAntennaId=" << aAntenna->GetId() << " bAntennaId=" << bAntenna->GetId());
+    }
+
+    return changed;
 }
 
 Ptr<const MatrixBasedChannelModel::ChannelMatrix>
@@ -2478,84 +2515,64 @@
     NS_LOG_FUNCTION(this);
 
     // Compute the channel params key. The key is reciprocal, i.e., key (a, b) = key (b, a)
-    uint64_t channelParamsKey =
+    const uint64_t channelParamsKey =
         GetKey(aMob->GetObject<Node>()->GetId(), bMob->GetObject<Node>()->GetId());
-    // Compute the channel matrix key. The key is reciprocal, i.e., key (a, b) = key (b, a)
-    uint64_t channelMatrixKey = GetKey(aAntenna->GetId(), bAntenna->GetId());
-
     // retrieve the channel condition
-    Ptr<const ChannelCondition> condition =
+    const Ptr<const ChannelCondition> condition =
         m_channelConditionModel->GetChannelCondition(aMob, bMob);
-
-    // Check if the channel is present in the map and return it, otherwise
-    // generate a new channel
-    bool updateParams = false;
-    bool updateMatrix = false;
-    bool notFoundParams = false;
-    bool notFoundMatrix = false;
-    Ptr<ChannelMatrix> channelMatrix;
-    Ptr<ThreeGppChannelParams> channelParams;
-
-    if (m_channelParamsMap.find(channelParamsKey) != m_channelParamsMap.end())
-    {
-        channelParams = m_channelParamsMap[channelParamsKey];
-        // check if it has to be updated
-        updateParams = ChannelParamsNeedsUpdate(channelParams, condition);
-    }
-    else
+    // Enforce canonical ordering when selecting scenario parameters.
+    Ptr<const MobilityModel> aMobOrdered = aMob;
+    Ptr<const MobilityModel> bMobOrdered = bMob;
+    if (aMob->GetObject<Node>()->GetId() > bMob->GetObject<Node>()->GetId())
     {
-        NS_LOG_DEBUG("channel params not found");
-        notFoundParams = true;
+        std::swap(aMobOrdered, bMobOrdered);
     }
-
     // get the 3GPP parameters
-    Ptr<const ParamsTable> table3gpp = GetThreeGppTable(aMob, bMob, condition);
+    const Ptr<const ParamsTable> table3gpp = GetThreeGppTable(aMobOrdered, bMobOrdered, condition);
 
-    if (notFoundParams || updateParams)
+    if (NewChannelParamsNeeded(channelParamsKey, condition, aMob, bMob))
     {
-        // Step 4: Generate large scale parameters. All LSPS are uncorrelated.
-        // Step 5: Generate Delays.
-        // Step 6: Generate cluster powers.
-        // Step 7: Generate arrival and departure angles for both azimuth and elevation.
-        // Step 8: Coupling of rays within a cluster for both azimuth and elevation
-        // shuffle all the arrays to perform random coupling
-        // Step 9: Generate the cross polarization power ratios
-        // Step 10: Draw initial phases
-        channelParams = GenerateChannelParameters(condition, table3gpp, aMob, bMob);
-        // store or replace the channel parameters
-        m_channelParamsMap[channelParamsKey] = channelParams;
-    }
-
-    if (m_channelMatrixMap.find(channelMatrixKey) != m_channelMatrixMap.end())
-    {
-        // channel matrix present in the map
-        NS_LOG_DEBUG("channel matrix present in the map");
-        channelMatrix = m_channelMatrixMap[channelMatrixKey];
-        updateMatrix = ChannelMatrixNeedsUpdate(channelParams, channelMatrix);
-        updateMatrix |= AntennaSetupChanged(aAntenna, bAntenna, channelMatrix);
+        NS_LOG_DEBUG(
+            "Create new or regenerate the channel parameters because the condition has changed");
+        m_channelParamsMap.insert_or_assign(
+            channelParamsKey,
+            GenerateChannelParameters(condition, table3gpp, aMobOrdered, bMobOrdered));
     }
     else
     {
-        NS_LOG_DEBUG("channel matrix not found");
-        notFoundMatrix = true;
+        const auto it = m_channelParamsMap.find(channelParamsKey);
+        NS_ASSERT(it != m_channelParamsMap.end());
+        if (ChannelUpdateNeeded(it->second, aMob, bMob))
+        {
+            NS_LOG_DEBUG("Update the channel parameters using consistency procedure");
+            UpdateChannelParameters(it->second, condition, aMob, bMob);
+        }
+        else
+        {
+            NS_LOG_DEBUG("No channel params update. Using already existing channel params.");
+        }
     }
 
-    // If the channel is not present in the map or if it has to be updated
-    // generate a new realization
-    if (notFoundMatrix || updateMatrix)
-    {
-        // channel matrix not found or has to be updated, generate a new one
-        channelMatrix = GetNewChannel(channelParams, table3gpp, aMob, bMob, aAntenna, bAntenna);
-        channelMatrix->m_antennaPair =
-            std::make_pair(aAntenna->GetId(),
-                           bAntenna->GetId()); // save antenna pair, with the exact order of s and u
-                                               // antennas at the moment of the channel generation
-
-        // store or replace the channel matrix in the channel map
-        m_channelMatrixMap[channelMatrixKey] = channelMatrix;
+    // Compute the channel matrix key. The key is reciprocal, i.e., key (a, b) = key (b, a)
+    const uint64_t channelMatrixKey = GetKey(aAntenna->GetId(), bAntenna->GetId());
+    // If the channel matrix is not present in the map or if it has to be updated
+    if (NewChannelMatrixNeeded(channelMatrixKey,
+                               m_channelParamsMap.find(channelParamsKey)->second,
+                               aAntenna,
+                               bAntenna))
+    {
+        m_channelMatrixMap.insert_or_assign(
+            channelMatrixKey,
+            GetNewChannel(m_channelParamsMap.find(channelParamsKey)->second,
+                          table3gpp,
+                          aMob,
+                          bMob,
+                          aAntenna,
+                          bAntenna));
     }
 
-    return channelMatrix;
+    NS_ASSERT(m_channelMatrixMap.contains(channelMatrixKey));
+    return m_channelMatrixMap.find(channelMatrixKey)->second;
 }
 
 Ptr<const MatrixBasedChannelModel::ChannelParams>
@@ -2564,161 +2581,209 @@
     NS_LOG_FUNCTION(this);
 
     // Compute the channel key. The key is reciprocal, i.e., key (a, b) = key (b, a)
-    uint64_t channelParamsKey =
+    const uint64_t channelParamsKey =
         GetKey(aMob->GetObject<Node>()->GetId(), bMob->GetObject<Node>()->GetId());
 
-    if (m_channelParamsMap.find(channelParamsKey) != m_channelParamsMap.end())
+    if (m_channelParamsMap.contains(channelParamsKey))
     {
         return m_channelParamsMap.find(channelParamsKey)->second;
     }
-    else
-    {
-        NS_LOG_WARN("Channel params map not found. Returning a nullptr.");
-        return nullptr;
-    }
+    NS_LOG_WARN("Channel params map not found. Returning a nullptr.");
+    return nullptr;
 }
 
-Ptr<ThreeGppChannelModel::ThreeGppChannelParams>
-ThreeGppChannelModel::GenerateChannelParameters(const Ptr<const ChannelCondition> channelCondition,
-                                                const Ptr<const ParamsTable> table3gpp,
-                                                const Ptr<const MobilityModel> aMob,
-                                                const Ptr<const MobilityModel> bMob) const
+ThreeGppChannelModel::LargeScaleParameters
+ThreeGppChannelModel::GenerateLSPs(const ChannelCondition::LosConditionValue losCondition,
+                                   Ptr<const ParamsTable> table3gpp) const
 {
     NS_LOG_FUNCTION(this);
-    // create a channel matrix instance
-    Ptr<ThreeGppChannelParams> channelParams = Create<ThreeGppChannelParams>();
-    channelParams->m_generatedTime = Simulator::Now();
-    channelParams->m_nodeIds =
-        std::make_pair(aMob->GetObject<Node>()->GetId(), bMob->GetObject<Node>()->GetId());
-    channelParams->m_losCondition = channelCondition->GetLosCondition();
-    channelParams->m_o2iCondition = channelCondition->GetO2iCondition();
-
-    // Step 4: Generate large scale parameters. All LSPS are uncorrelated.
-    DoubleVector LSPsIndep;
-    DoubleVector LSPs;
-    uint8_t paramNum = 6;
-    if (channelParams->m_losCondition == ChannelCondition::LOS)
-    {
-        paramNum = 7;
-    }
+    DoubleVector lspIndepRandomVar;
+    DoubleVector lsp;
+    const uint8_t paramNum = losCondition == ChannelCondition::LOS ? 7 : 6;
 
     // Generate paramNum independent LSPs.
     for (uint8_t iter = 0; iter < paramNum; iter++)
     {
-        LSPsIndep.push_back(m_normalRv->GetValue());
+        lspIndepRandomVar.push_back(m_normalRv->GetValue());
     }
     for (uint8_t row = 0; row < paramNum; row++)
     {
         double temp = 0;
         for (uint8_t column = 0; column < paramNum; column++)
         {
-            temp += table3gpp->m_sqrtC[row][column] * LSPsIndep[column];
+            temp += table3gpp->m_sqrtC[row][column] * lspIndepRandomVar[column];
         }
-        LSPs.push_back(temp);
+        lsp.push_back(temp);
     }
 
+    LargeScaleParameters lsps;
     // NOTE the shadowing is generated in the propagation loss model
-    double DS;
-    double ASD;
-    double ASA;
-    double ZSA;
-    double ZSD;
-    double kFactor = 0;
-    if (channelParams->m_losCondition == ChannelCondition::LOS)
-    {
-        kFactor = LSPs[1] * table3gpp->m_sigK + table3gpp->m_uK;
-        DS = pow(10, LSPs[2] * table3gpp->m_sigLgDS + table3gpp->m_uLgDS);
-        ASD = pow(10, LSPs[3] * table3gpp->m_sigLgASD + table3gpp->m_uLgASD);
-        ASA = pow(10, LSPs[4] * table3gpp->m_sigLgASA + table3gpp->m_uLgASA);
-        ZSD = pow(10, LSPs[5] * table3gpp->m_sigLgZSD + table3gpp->m_uLgZSD);
-        ZSA = pow(10, LSPs[6] * table3gpp->m_sigLgZSA + table3gpp->m_uLgZSA);
+    // For LOS, LSP is following the order of [SF,K,DS,ASD,ASA,ZSD,ZSA].
+    // For NLOS, LSP is following the order of [SF,DS,ASD,ASA,ZSD,ZSA].
+    if (losCondition == ChannelCondition::LOS)
+    {
+        lsps.kFactor = lsp[1] * table3gpp->m_sigK + table3gpp->m_uK;
+        lsps.DS = pow(10, lsp[2] * table3gpp->m_sigLgDS + table3gpp->m_uLgDS);
+        lsps.ASD = pow(10, lsp[3] * table3gpp->m_sigLgASD + table3gpp->m_uLgASD);
+        lsps.ASA = pow(10, lsp[4] * table3gpp->m_sigLgASA + table3gpp->m_uLgASA);
+        lsps.ZSD = pow(10, lsp[5] * table3gpp->m_sigLgZSD + table3gpp->m_uLgZSD);
+        lsps.ZSA = pow(10, lsp[6] * table3gpp->m_sigLgZSA + table3gpp->m_uLgZSA);
     }
     else
     {
-        DS = pow(10, LSPs[1] * table3gpp->m_sigLgDS + table3gpp->m_uLgDS);
-        ASD = pow(10, LSPs[2] * table3gpp->m_sigLgASD + table3gpp->m_uLgASD);
-        ASA = pow(10, LSPs[3] * table3gpp->m_sigLgASA + table3gpp->m_uLgASA);
-        ZSD = pow(10, LSPs[4] * table3gpp->m_sigLgZSD + table3gpp->m_uLgZSD);
-        ZSA = pow(10, LSPs[5] * table3gpp->m_sigLgZSA + table3gpp->m_uLgZSA);
-    }
-    ASD = std::min(ASD, 104.0);
-    ASA = std::min(ASA, 104.0);
-    ZSD = std::min(ZSD, 52.0);
-    ZSA = std::min(ZSA, 52.0);
-
-    // save DS and K_factor parameters in the structure
-    channelParams->m_DS = DS;
-    channelParams->m_K_factor = kFactor;
+        lsps.DS = pow(10, lsp[1] * table3gpp->m_sigLgDS + table3gpp->m_uLgDS);
+        lsps.ASD = pow(10, lsp[2] * table3gpp->m_sigLgASD + table3gpp->m_uLgASD);
+        lsps.ASA = pow(10, lsp[3] * table3gpp->m_sigLgASA + table3gpp->m_uLgASA);
+        lsps.ZSD = pow(10, lsp[4] * table3gpp->m_sigLgZSD + table3gpp->m_uLgZSD);
+        lsps.ZSA = pow(10, lsp[5] * table3gpp->m_sigLgZSA + table3gpp->m_uLgZSA);
+    }
+    lsps.ASD = std::min(lsps.ASD, 104.0);
+    lsps.ASA = std::min(lsps.ASA, 104.0);
+    lsps.ZSD = std::min(lsps.ZSD, 52.0);
+    lsps.ZSA = std::min(lsps.ZSA, 52.0);
+    NS_LOG_INFO("K-factor=" << lsps.kFactor << ", DS=" << lsps.DS << ", ASD=" << lsps.ASD
+                            << ", ASA=" << lsps.ASA << ", ZSD=" << lsps.ZSD
+                            << ", ZSA=" << lsps.ZSA);
+    return lsps;
+}
+
+void
+ThreeGppChannelModel::GenerateClusterDelays(const double DS,
+                                            Ptr<const ParamsTable> table3gpp,
+                                            double* minTau,
+                                            DoubleVector* clusterDelays) const
+{
+    NS_LOG_FUNCTION(this);
 
-    NS_LOG_INFO("K-factor=" << kFactor << ", DS=" << DS << ", ASD=" << ASD << ", ASA=" << ASA
-                            << ", ZSD=" << ZSD << ", ZSA=" << ZSA);
+    NS_ASSERT_MSG(DS > 0, "Delay spread must be positive");
+    NS_ASSERT_MSG(table3gpp->m_numOfCluster > 0, "Number of clusters must be positive");
+
+    // Clear output vector and resize
+    clusterDelays->clear();
+    clusterDelays->resize(table3gpp->m_numOfCluster);
 
-    // Step 5: Generate Delays.
-    DoubleVector clusterDelay;
-    double minTau = 100.0;
     for (uint8_t cIndex = 0; cIndex < table3gpp->m_numOfCluster; cIndex++)
     {
-        double tau = -1 * table3gpp->m_rTau * DS * log(m_uniformRv->GetValue(0, 1)); //(7.5-1)
-        if (minTau > tau)
+        const double tau = -1 * table3gpp->m_rTau * DS * log(m_uniformRv->GetValue(0, 1)); //(7.5-1)
+        if (*minTau > tau)
         {
-            minTau = tau;
+            *minTau = tau;
         }
-        clusterDelay.push_back(tau);
+        (*clusterDelays)[cIndex] = tau;
     }
 
     for (uint8_t cIndex = 0; cIndex < table3gpp->m_numOfCluster; cIndex++)
     {
-        clusterDelay[cIndex] -= minTau;
+        (*clusterDelays)[cIndex] -= *minTau;
     }
-    std::sort(clusterDelay.begin(), clusterDelay.end()); //(7.5-2)
+    // perform cluster delay sorting as per (7.5-2)
+    std::ranges::sort(*clusterDelays);
+}
 
-    /* since the scaled Los delays are not to be used in cluster power generation,
-     * we will generate cluster power first and resume to compute Los cluster delay later.*/
+void
+ThreeGppChannelModel::GenerateClusterShadowingTerm(Ptr<const ParamsTable> table3gpp,
+                                                   DoubleVector* clusterShadowing) const
+{
+    NS_LOG_FUNCTION(this);
+    clusterShadowing->clear();
+    clusterShadowing->resize(table3gpp->m_numOfCluster);
 
-    // Step 6: Generate cluster powers.
-    DoubleVector clusterPower;
-    double powerSum = 0;
     for (uint8_t cIndex = 0; cIndex < table3gpp->m_numOfCluster; cIndex++)
     {
-        double power =
-            exp(-1 * clusterDelay[cIndex] * (table3gpp->m_rTau - 1) / table3gpp->m_rTau / DS) *
-            pow(10,
-                -1 * m_normalRv->GetValue() * table3gpp->m_perClusterShadowingStd / 10.0); //(7.5-5)
-        powerSum += power;
-        clusterPower.push_back(power);
+        (*clusterShadowing)[cIndex] = m_normalRv->GetValue() * table3gpp->m_perClusterShadowingStd;
     }
-    channelParams->m_clusterPower = clusterPower;
+}
 
-    double powerMax = 0;
+void
+ThreeGppChannelModel::UpdateClusterShadowingTerm(Ptr<const ParamsTable> table3gpp,
+                                                 DoubleVector* clusterShadowing,
+                                                 const double displacementLength) const
+{
+    NS_LOG_FUNCTION(this);
+    // Normalized auto correlation function 7.4-5
+    const double R = exp(-1 * displacementLength / table3gpp->m_perClusterRayDcorDistance);
 
     for (uint8_t cIndex = 0; cIndex < table3gpp->m_numOfCluster; cIndex++)
     {
-        channelParams->m_clusterPower[cIndex] =
-            channelParams->m_clusterPower[cIndex] / powerSum; //(7.5-6)
+        // compute a new correlated shadowing
+        (*clusterShadowing)[cIndex] =
+            R * (*clusterShadowing)[cIndex] +
+            sqrt(1 - R * R) * m_normalRv->GetValue() * table3gpp->m_perClusterShadowingStd;
     }
+}
 
-    DoubleVector clusterPowerForAngles; // this power is only for equation (7.5-9) and (7.5-14), not
-                                        // for (7.5-22)
-    if (channelParams->m_losCondition == ChannelCondition::LOS)
+void
+ThreeGppChannelModel::GenerateClusterPowers(const DoubleVector& clusterDelays,
+                                            const double DS,
+                                            Ptr<const ParamsTable> table3gpp,
+                                            const DoubleVector& clusterShadowing,
+                                            DoubleVector* clusterPowers) const
+{
+    NS_LOG_FUNCTION(this);
+    // Clear and resize the output vector
+    clusterPowers->clear();
+    clusterPowers->resize(clusterDelays.size());
+
+    double powerSum = 0;
+    for (size_t cIndex = 0; cIndex < clusterDelays.size(); cIndex++)
+    {
+        const double power =
+            exp(-1 * clusterDelays[cIndex] * (table3gpp->m_rTau - 1) / table3gpp->m_rTau / DS) *
+            pow(10, -1 * clusterShadowing[cIndex] / 10.0); //(7.5-5)
+        powerSum += power;
+        (*clusterPowers)[cIndex] = power;
+    }
+
+    // Normalize cluster powers with NS_ASSERT for division-by-zero protection
+
+    NS_ASSERT_MSG(powerSum > 0, "Power sum must be greater than zero. Time: " << Simulator::Now());
+
+    for (size_t cIndex = 0; cIndex < clusterPowers->size(); cIndex++)
+    {
+        (*clusterPowers)[cIndex] = (*clusterPowers)[cIndex] / powerSum; //(7.5-6)
+    }
+
+    double totalPower = 0;
+    for (size_t cIndex = 0; cIndex < clusterPowers->size(); cIndex++)
     {
-        double kLinear = pow(10, kFactor / 10.0);
+        totalPower += (*clusterPowers)[cIndex];
+    }
+
+    NS_ASSERT_MSG(std::round(totalPower) == 1, "Total power must be equal to 1");
+}
+
+MatrixBasedChannelModel::DoubleVector
+ThreeGppChannelModel::RemoveWeakClusters(DoubleVector* clusterPowers,
+                                         DoubleVector* clusterDelays,
+                                         const ChannelCondition::LosConditionValue losCondition,
+                                         Ptr<const ParamsTable> table3gpp,
+                                         const double kFactor,
+                                         double* powerMax) const
+{
+    NS_LOG_FUNCTION(this);
+    DoubleVector clusterPowersForAngles;
+    // this power is only for equation (7.5-9) and (7.5-14), not
+    // for (7.5-22)
+
+    if (losCondition == ChannelCondition::LOS)
+    {
+        const double kLinear = pow(10, kFactor / 10.0);
 
         for (uint8_t cIndex = 0; cIndex < table3gpp->m_numOfCluster; cIndex++)
         {
             if (cIndex == 0)
             {
-                clusterPowerForAngles.push_back(channelParams->m_clusterPower[cIndex] /
-                                                    (1 + kLinear) +
-                                                kLinear / (1 + kLinear)); //(7.5-8)
+                clusterPowersForAngles.push_back((*clusterPowers)[cIndex] / (1 + kLinear) +
+                                                 kLinear / (1 + kLinear)); //(7.5-8)
             }
             else
             {
-                clusterPowerForAngles.push_back(channelParams->m_clusterPower[cIndex] /
-                                                (1 + kLinear)); //(7.5-8)
+                clusterPowersForAngles.push_back((*clusterPowers)[cIndex] /
+                                                 (1 + kLinear)); //(7.5-8)
             }
-            if (powerMax < clusterPowerForAngles[cIndex])
+            if (*powerMax < clusterPowersForAngles[cIndex])
             {
-                powerMax = clusterPowerForAngles[cIndex];
+                *powerMax = clusterPowersForAngles[cIndex];
             }
         }
     }
@@ -2726,10 +2791,10 @@
     {
         for (uint8_t cIndex = 0; cIndex < table3gpp->m_numOfCluster; cIndex++)
         {
-            clusterPowerForAngles.push_back(channelParams->m_clusterPower[cIndex]); //(7.5-6)
-            if (powerMax < clusterPowerForAngles[cIndex])
+            clusterPowersForAngles.push_back((*clusterPowers)[cIndex]); //(7.5-6)
+            if (*powerMax < clusterPowersForAngles[cIndex])
             {
-                powerMax = clusterPowerForAngles[cIndex];
+                *powerMax = clusterPowersForAngles[cIndex];
             }
         }
     }
@@ -2739,147 +2804,132 @@
     double thresh = 0.0032;
     for (uint8_t cIndex = table3gpp->m_numOfCluster; cIndex > 0; cIndex--)
     {
-        if (clusterPowerForAngles[cIndex - 1] < thresh * powerMax)
+        if (clusterPowersForAngles[cIndex - 1] < thresh * *powerMax)
         {
-            clusterPowerForAngles.erase(clusterPowerForAngles.begin() + cIndex - 1);
-            channelParams->m_clusterPower.erase(channelParams->m_clusterPower.begin() + cIndex - 1);
-            clusterDelay.erase(clusterDelay.begin() + cIndex - 1);
+            clusterPowersForAngles.erase(clusterPowersForAngles.begin() + cIndex - 1);
+            clusterPowers->erase(clusterPowers->begin() + cIndex - 1);
+            clusterDelays->erase(clusterDelays->begin() + cIndex - 1);
         }
     }
+    return clusterPowersForAngles;
+}
 
-    NS_ASSERT(channelParams->m_clusterPower.size() < UINT8_MAX);
-    channelParams->m_reducedClusterNumber = channelParams->m_clusterPower.size();
-    // Resume step 5 to compute the delay for LoS condition.
-    if (channelParams->m_losCondition == ChannelCondition::LOS)
+void
+ThreeGppChannelModel::AdjustClusterDelaysForLosCondition(DoubleVector* clusterDelays,
+                                                         const uint8_t reducedClusterNumber,
+                                                         const double kFactor) const
+{
+    NS_LOG_FUNCTION(this);
+    const double cTau = 0.7705 - 0.0433 * kFactor + 2e-4 * pow(kFactor, 2) +
+                        17e-6 * pow(kFactor,
+                                    3); //(7.5-3)
+    for (uint8_t cIndex = 0; cIndex < reducedClusterNumber; cIndex++)
     {
-        double cTau =
-            0.7705 - 0.0433 * kFactor + 2e-4 * pow(kFactor, 2) + 17e-6 * pow(kFactor, 3); //(7.5-3)
-        for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
+        (*clusterDelays)[cIndex] = (*clusterDelays)[cIndex] / cTau; //(7.5-4)
+    }
+}
+
+double
+ThreeGppChannelModel::CalculateCphi(const ChannelCondition::LosConditionValue losCondition,
+                                    Ptr<const ParamsTable> table3gpp,
+                                    const double kFactor)
+{
+    try
+    {
+        double cPhi = cNlosTablePhi.at(table3gpp->m_numOfCluster);
+        if (losCondition == ChannelCondition::LOS)
         {
-            clusterDelay[cIndex] = clusterDelay[cIndex] / cTau; //(7.5-4)
+            cPhi *= 1.1035 - 0.028 * kFactor - 2e-3 * pow(kFactor, 2) +
+                    1e-4 * pow(kFactor, 3); // (7.5-10)
         }
+        return cPhi;
     }
-
-    // Step 7: Generate arrival and departure angles for both azimuth and elevation.
-
-    double cNlos;
-    // According to table 7.5-6, only cluster number equals to 8, 10, 11, 12, 19 and 20 is valid.
-    // Not sure why the other cases are in Table 7.5-2.
-    // Added case 2 and 3 for the NTN according to table 6.7.2-1aa (28.811)
-    switch (table3gpp->m_numOfCluster) // Table 7.5-2
-    {
-    case 2:
-        cNlos = 0.501;
-        break;
-    case 3:
-        cNlos = 0.680;
-        break;
-    case 4:
-        cNlos = 0.779;
-        break;
-    case 5:
-        cNlos = 0.860;
-        break;
-    case 8:
-        cNlos = 1.018;
-        break;
-    case 10:
-        cNlos = 1.090;
-        break;
-    case 11:
-        cNlos = 1.123;
-        break;
-    case 12:
-        cNlos = 1.146;
-        break;
-    case 14:
-        cNlos = 1.190;
-        break;
-    case 15:
-        cNlos = 1.221;
-        break;
-    case 16:
-        cNlos = 1.226;
-        break;
-    case 19:
-        cNlos = 1.273;
-        break;
-    case 20:
-        cNlos = 1.289;
-        break;
-    default:
-        NS_FATAL_ERROR("Invalid cluster number");
+    catch (const std::out_of_range&)
+    {
+        NS_FATAL_ERROR("Invalid cluster number in cNlosTablePhi");
     }
+}
 
-    double cPhi = cNlos;
-
-    if (channelParams->m_losCondition == ChannelCondition::LOS)
+double
+ThreeGppChannelModel::CalculateCtheta(const ChannelCondition::LosConditionValue losCondition,
+                                      Ptr<const ParamsTable> table3gpp,
+                                      const double kFactor)
+{
+    try
     {
-        cPhi *= (1.1035 - 0.028 * kFactor - 2e-3 * pow(kFactor, 2) +
-                 1e-4 * pow(kFactor, 3)); //(7.5-10))
+        double cTheta = cNlosTableTheta.at(table3gpp->m_numOfCluster);
+        if (losCondition == ChannelCondition::LOS)
+        {
+            cTheta *= 1.3086 + 0.0339 * kFactor - 0.0077 * pow(kFactor, 2) +
+                      2e-4 * pow(kFactor, 3); // (7.5-15)
+        }
+        return cTheta;
     }
-
-    // Added case 2, 3 and 4 for the NTN according to table 6.7.2-1ab (28.811)
-    switch (table3gpp->m_numOfCluster) // Table 7.5-4
+    catch (const std::out_of_range&)
     {
-    case 2:
-        cNlos = 0.430;
-        break;
-    case 3:
-        cNlos = 0.594;
-        break;
-    case 4:
-        cNlos = 0.697;
-        break;
-    case 8:
-        cNlos = 0.889;
-        break;
-    case 10:
-        cNlos = 0.957;
-        break;
-    case 11:
-        cNlos = 1.031;
-        break;
-    case 12:
-        cNlos = 1.104;
-        break;
-    case 15:
-        cNlos = 1.1088;
-        break;
-    case 19:
-        cNlos = 1.184;
-        break;
-    case 20:
-        cNlos = 1.178;
-        break;
-    default:
-        NS_FATAL_ERROR("Invalid cluster number");
+        NS_FATAL_ERROR("Invalid cluster number in cNlosTableTheta");
     }
+}
 
-    double cTheta = cNlos;
-    if (channelCondition->IsLos())
+void
+ThreeGppChannelModel::GenerateClusterXnNLos(const uint8_t clusterNumber,
+                                            std::vector<int>* clusterSign) const
+{
+    NS_LOG_FUNCTION(this);
+    clusterSign->clear();
+    clusterSign->resize(clusterNumber);
+
+    for (uint8_t cIndex = 0; cIndex < clusterNumber; cIndex++)
     {
-        cTheta *= (1.3086 + 0.0339 * kFactor - 0.0077 * pow(kFactor, 2) +
-                   2e-4 * pow(kFactor, 3)); //(7.5-15)
+        int Xn = 1;
+        if (m_uniformRv->GetValue(0, 1) < 0.5)
+        {
+            Xn = -1;
+        }
+
+        (*clusterSign)[cIndex] = Xn;
     }
+}
+
+void
+ThreeGppChannelModel::GenerateClusterAngles(Ptr<const ThreeGppChannelParams> channelParams,
+                                            const DoubleVector& clusterPowerForAngles,
+                                            const double powerMax,
+                                            const double cPhi,
+                                            const double cTheta,
+                                            const LargeScaleParameters& lsps,
+                                            Ptr<const MobilityModel> aMob,
+                                            Ptr<const MobilityModel> bMob,
+                                            Ptr<const ParamsTable> table3gpp,
+                                            Double2DVector* clusterAngles) const
+{
+    NS_LOG_FUNCTION(this);
+    clusterAngles->clear();
+    clusterAngles->resize(4);
 
     DoubleVector clusterAoa;
     DoubleVector clusterAod;
     DoubleVector clusterZoa;
     DoubleVector clusterZod;
+
+    clusterAoa.resize(channelParams->m_reducedClusterNumber);
+    clusterAod.resize(channelParams->m_reducedClusterNumber);
+    clusterZoa.resize(channelParams->m_reducedClusterNumber);
+    clusterZod.resize(channelParams->m_reducedClusterNumber);
+
     for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
     {
-        double logCalc = -1 * log(clusterPowerForAngles[cIndex] / powerMax);
+        const double logCalc = -1 * log(clusterPowerForAngles[cIndex] / powerMax);
         double angle = 2 * sqrt(logCalc) / 1.4 / cPhi; //(7.5-9)
-        clusterAoa.push_back(ASA * angle);
-        clusterAod.push_back(ASD * angle);
+        clusterAoa[cIndex] = lsps.ASA * angle;
+        clusterAod[cIndex] = lsps.ASD * angle;
         angle = logCalc / cTheta; //(7.5-14)
-        clusterZoa.push_back(ZSA * angle);
-        clusterZod.push_back(ZSD * angle);
+        clusterZoa[cIndex] = lsps.ZSA * angle;
+        clusterZod[cIndex] = lsps.ZSD * angle;
     }
 
-    Angles sAngle(bMob->GetPosition(), aMob->GetPosition());
-    Angles uAngle(aMob->GetPosition(), bMob->GetPosition());
+    const Angles sAngle(bMob->GetPosition(), aMob->GetPosition());
+    const Angles uAngle(aMob->GetPosition(), bMob->GetPosition());
 
     for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
     {
@@ -2888,21 +2938,23 @@
         {
             Xn = -1;
         }
-        clusterAoa[cIndex] = clusterAoa[cIndex] * Xn + (m_normalRv->GetValue() * ASA / 7.0) +
+
+        clusterAoa[cIndex] = clusterAoa[cIndex] * Xn + m_normalRv->GetValue() * lsps.ASA / 7.0 +
                              RadiansToDegrees(uAngle.GetAzimuth()); //(7.5-11)
-        clusterAod[cIndex] = clusterAod[cIndex] * Xn + (m_normalRv->GetValue() * ASD / 7.0) +
+        clusterAod[cIndex] = clusterAod[cIndex] * Xn + m_normalRv->GetValue() * lsps.ASD / 7.0 +
                              RadiansToDegrees(sAngle.GetAzimuth());
-        if (channelCondition->IsO2i())
+        if (channelParams->m_o2iCondition == ChannelCondition::O2I)
         {
             clusterZoa[cIndex] =
-                clusterZoa[cIndex] * Xn + (m_normalRv->GetValue() * ZSA / 7.0) + 90; //(7.5-16)
+                clusterZoa[cIndex] * Xn + m_normalRv->GetValue() * lsps.ZSA / 7.0 + 90;
+            //(7.5-16)
         }
         else
         {
-            clusterZoa[cIndex] = clusterZoa[cIndex] * Xn + (m_normalRv->GetValue() * ZSA / 7.0) +
+            clusterZoa[cIndex] = clusterZoa[cIndex] * Xn + m_normalRv->GetValue() * lsps.ZSA / 7.0 +
                                  RadiansToDegrees(uAngle.GetInclination()); //(7.5-16)
         }
-        clusterZod[cIndex] = clusterZod[cIndex] * Xn + (m_normalRv->GetValue() * ZSD / 7.0) +
+        clusterZod[cIndex] = clusterZod[cIndex] * Xn + m_normalRv->GetValue() * lsps.ZSD / 7.0 +
                              RadiansToDegrees(sAngle.GetInclination()) +
                              table3gpp->m_offsetZOD; //(7.5-19)
     }
@@ -2911,10 +2963,10 @@
     {
         // The 7.5-12 can be rewrite as Theta_n,ZOA = Theta_n,ZOA - (Theta_1,ZOA - Theta_LOS,ZOA) =
         // Theta_n,ZOA - diffZOA, Similar as AOD, ZSA and ZSD.
-        double diffAoa = clusterAoa[0] - RadiansToDegrees(uAngle.GetAzimuth());
-        double diffAod = clusterAod[0] - RadiansToDegrees(sAngle.GetAzimuth());
-        double diffZsa = clusterZoa[0] - RadiansToDegrees(uAngle.GetInclination());
-        double diffZsd = clusterZod[0] - RadiansToDegrees(sAngle.GetInclination());
+        const double diffAoa = clusterAoa[0] - RadiansToDegrees(uAngle.GetAzimuth());
+        const double diffAod = clusterAod[0] - RadiansToDegrees(sAngle.GetAzimuth());
+        const double diffZsa = clusterZoa[0] - RadiansToDegrees(uAngle.GetInclination());
+        const double diffZsd = clusterZod[0] - RadiansToDegrees(sAngle.GetInclination());
 
         for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
         {
@@ -2925,7 +2977,7 @@
         }
     }
 
-    double sizeTemp = clusterZoa.size();
+    const double sizeTemp = clusterZoa.size();
     for (uint8_t ind = 0; ind < 4; ind++)
     {
         DoubleVector angleDegree;
@@ -2985,260 +3037,950 @@
         }
     }
 
-    DoubleVector attenuationDb;
+    (*clusterAngles)[AOA_INDEX] = clusterAoa;
+    (*clusterAngles)[AOD_INDEX] = clusterAod;
+    (*clusterAngles)[ZOA_INDEX] = clusterZoa;
+    (*clusterAngles)[ZOD_INDEX] = clusterZod;
+}
+
+void
+ThreeGppChannelModel::UpdateClusterDelay(DoubleVector* clusterDelay,
+                                         DoubleVector* delayConsistency,
+                                         Ptr<const ThreeGppChannelParams> channelParams) const
+{
+    NS_LOG_FUNCTION(this);
+
+    clusterDelay->clear();
+    clusterDelay->resize(channelParams->m_reducedClusterNumber);
+
+    NS_ASSERT(channelParams->m_delayConsistency.empty() == false);
+    NS_ASSERT(Simulator::Now() != channelParams->m_generatedTime);
+
+    // update cluster delays based on equation (7.6-9)
+    for (size_t cInd = 0; cInd < channelParams->m_reducedClusterNumber; cInd++)
+    {
+        const double timeSeconds = (Simulator::Now() - channelParams->m_generatedTime).GetSeconds();
+
+        (*delayConsistency)[cInd] -=
+            (sin(channelParams->m_angle.at(ZOA_INDEX).at(cInd) * M_PI / 180) *
+                 cos(channelParams->m_angle.at(AOA_INDEX).at(cInd) * M_PI / 180) *
+                 channelParams->m_rxSpeed.x +
+             sin(channelParams->m_angle.at(ZOA_INDEX).at(cInd) * M_PI / 180) *
+                 sin(channelParams->m_angle.at(AOA_INDEX).at(cInd) * M_PI / 180) *
+                 channelParams->m_rxSpeed.y +
+             (sin(channelParams->m_angle.at(ZOD_INDEX).at(cInd) * M_PI / 180) *
+                  cos(channelParams->m_angle.at(AOD_INDEX).at(cInd) * M_PI / 180) *
+                  channelParams->m_txSpeed.x +
+              sin(channelParams->m_angle.at(ZOD_INDEX).at(cInd) * M_PI / 180) *
+                  sin(channelParams->m_angle.at(AOD_INDEX).at(cInd) * M_PI / 180) *
+                  channelParams->m_txSpeed.y)) /
+            3e8 * timeSeconds;
+    }
+
+    // normalize the delays by removing the min value (7.6-10a)
+    double minTau = std::numeric_limits<double>::max();
+    for (size_t cInd = 0; cInd < channelParams->m_reducedClusterNumber; cInd++)
+    {
+        if (minTau > (*delayConsistency)[cInd])
+        {
+            minTau = (*delayConsistency)[cInd];
+        }
+    }
+
+    for (size_t cInd = 0; cInd < channelParams->m_reducedClusterNumber; cInd++)
+    {
+        // normalize the updated cluster delay (7.6-10a), and then use these cluster delays to
+        // generate cluster powers
+        (*clusterDelay)[cInd] = (*delayConsistency)[cInd] - minTau;
+    }
+}
+
+Vector
+ThreeGppChannelModel::ApplyVelocityRotation(const double alphaRad,
+                                            const double betaRad,
+                                            const double gammaRad,
+                                            const double etaRad,
+                                            const Vector& speed,
+                                            const int Xn) const
+{
+    NS_LOG_FUNCTION(this);
+    // only x and y components are used, no need to calculate the z component
+    const double sinAlpha = sin(alphaRad);
+    const double cosAlpha = cos(alphaRad);
+    const double sinBeta = sin(betaRad);
+    const double cosBeta = cos(betaRad);
+    const double sinGamma = sin(gammaRad);
+    const double cosGamma = cos(gammaRad);
+    const double sinEta = sin(etaRad);
+    const double cosEta = cos(etaRad);
+
+    Vector vRotated;
+
+    vRotated.x = (cosAlpha * cosBeta * cosGamma * cosEta - sinAlpha * sinEta * Xn -
+                  cosAlpha * sinBeta * sinGamma * cosEta) *
+                     speed.x +
+                 (-1 * cosAlpha * cosBeta * cosGamma * sinEta - sinAlpha * cosEta * Xn +
+                  cosAlpha * sinBeta * sinGamma * sinEta) *
+                     speed.y;
+
+    vRotated.y = (sinAlpha * cosBeta * cosGamma * cosEta + cosAlpha * sinEta * Xn -
+                  sinAlpha * sinBeta * sinGamma * cosEta) *
+                     speed.x +
+                 (-1 * sinAlpha * cosBeta * cosGamma * sinEta + cosAlpha * cosEta * Xn +
+                  sinAlpha * sinBeta * sinGamma * sinEta) *
+                     speed.y;
+
+    vRotated.z = 0;
+
+    return vRotated;
+}
+
+void
+ThreeGppChannelModel::UpdateClusterAngles(Ptr<const ThreeGppChannelParams> channelParams,
+                                          Double2DVector* clusterAngles,
+                                          const DoubleVector& prevClusterDelay) const
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT(prevClusterDelay.size() == channelParams->m_reducedClusterNumber);
+    const DoubleVector prevClusterAoa = channelParams->m_angle[AOA_INDEX];
+    const DoubleVector prevClusterZoa = channelParams->m_angle[ZOA_INDEX];
+    const DoubleVector prevClusterAod = channelParams->m_angle[AOD_INDEX];
+    const DoubleVector prevClusterZod = channelParams->m_angle[ZOD_INDEX];
+    const auto rxSpeed = channelParams->m_rxSpeed;
+    const auto txSpeed = channelParams->m_txSpeed;
+    const bool los = channelParams->m_losCondition == ChannelCondition::LOS;
+
+    for (size_t cInd = 0; cInd < prevClusterDelay.size(); cInd++)
+    {
+        // compute cluster relative speed
+        Vector vPrimeRx;
+        Vector vPrimeTx;
+        if (los)
+        {
+            vPrimeRx = rxSpeed - txSpeed; // (7.6-10b)
+            vPrimeTx = txSpeed - rxSpeed; // (7.6-10c)
+        }
+        else
+        {
+            NS_ASSERT(channelParams->m_clusterXnNlosSign.empty() == false);
+            NS_ASSERT(channelParams->m_clusterXnNlosSign.size() ==
+                      channelParams->m_reducedClusterNumber);
+            const int Xn = channelParams->m_clusterXnNlosSign[cInd];
+            double alphaRad = M_PI + DegreesToRadians(prevClusterAod[cInd]);
+            const double betaRad = M_PI / 2 - DegreesToRadians(prevClusterZod[cInd]);
+            const double gammaRad = M_PI / 2 - DegreesToRadians(prevClusterZoa[cInd]);
+            double etaRad = -1 * DegreesToRadians(prevClusterAoa[cInd]);
+
+            Vector rxSpeedPrime =
+                ApplyVelocityRotation(alphaRad, betaRad, gammaRad, etaRad, rxSpeed, Xn);
+
+            alphaRad = -1 * DegreesToRadians(prevClusterAod[cInd]);
+            etaRad = M_PI + DegreesToRadians(prevClusterAoa[cInd]);
+
+            Vector txSpeedPrime =
+                ApplyVelocityRotation(alphaRad, betaRad, gammaRad, etaRad, txSpeed, Xn);
+            vPrimeRx = rxSpeedPrime - txSpeed; // (7.6-10b)
+            vPrimeTx = txSpeedPrime - rxSpeed; // (7.6-10c)
+        }
+
+        using DPV = std::vector<std::pair<double, double>>;
+        const auto& cachedAngleSincos = channelParams->m_cachedAngleSincos;
+        const DPV& zoaSinCos = cachedAngleSincos[ZOA_INDEX];
+        const DPV& zodSinCos = cachedAngleSincos[ZOD_INDEX];
+        const DPV& aoaSinCos = cachedAngleSincos[AOA_INDEX];
+        const DPV& aodSinCos = cachedAngleSincos[AOD_INDEX];
+
+        const double deltaT =
+            Simulator::Now().GetSeconds() - channelParams->m_generatedTime.GetSeconds();
+
+        // update the angles according to equations (7.6-11) - (7.6-14)
+        (*clusterAngles)[AOD_INDEX][cInd] =
+            prevClusterAod[cInd] +
+            RadiansToDegrees(
+                (-aodSinCos[cInd].first * vPrimeRx.x + aodSinCos[cInd].second * vPrimeRx.y) /
+                (3e8 * prevClusterDelay[cInd] * zodSinCos[cInd].first) * deltaT);
+
+        (*clusterAngles)[ZOD_INDEX][cInd] =
+            prevClusterZod[cInd] +
+            RadiansToDegrees((zodSinCos[cInd].second * aodSinCos[cInd].second * vPrimeRx.x +
+                              zodSinCos[cInd].second * -aodSinCos[cInd].first * vPrimeRx.y) /
+                             (3e8 * prevClusterDelay[cInd]) * deltaT);
+
+        (*clusterAngles)[AOA_INDEX][cInd] =
+            prevClusterAoa[cInd] +
+            RadiansToDegrees(
+                (-aoaSinCos[cInd].first * vPrimeTx.x + aoaSinCos[cInd].second * vPrimeTx.y) /
+                (3e8 * prevClusterDelay[cInd] * zoaSinCos[cInd].first) * deltaT);
+
+        (*clusterAngles)[ZOA_INDEX][cInd] =
+            prevClusterZoa[cInd] +
+            RadiansToDegrees((zoaSinCos[cInd].second * aoaSinCos[cInd].second * vPrimeTx.x +
+                              zoaSinCos[cInd].second * aoaSinCos[cInd].first * vPrimeTx.y) /
+                             (3e8 * prevClusterDelay[cInd]) * deltaT);
+    }
+
+    NS_LOG_DEBUG("Cluster angles updated");
+}
+
+void
+ThreeGppChannelModel::ApplyAttenuationToClusterPowers(
+    Double2DVector* nonSelfBlocking,
+    DoubleVector* clusterPowers,
+    DoubleVector* attenuation_dB,
+    Ptr<const ThreeGppChannelParams> channelParams,
+    const DoubleVector& clusterAoa,
+    const DoubleVector& clusterZoa,
+    Ptr<const ParamsTable> table3gpp) const
+{
+    NS_LOG_FUNCTION(this);
+
     if (m_blockage)
     {
-        attenuationDb = CalcAttenuationOfBlockage(channelParams, clusterAoa, clusterZoa);
+        CalcAttenuationOfBlockage(nonSelfBlocking,
+                                  attenuation_dB,
+                                  channelParams,
+                                  clusterAoa,
+                                  clusterZoa,
+                                  table3gpp);
         for (uint8_t cInd = 0; cInd < channelParams->m_reducedClusterNumber; cInd++)
         {
-            channelParams->m_clusterPower[cInd] =
-                channelParams->m_clusterPower[cInd] / pow(10, attenuationDb[cInd] / 10.0);
+            (*clusterPowers)[cInd] =
+                (*clusterPowers)[cInd] / pow(10, (*attenuation_dB)[cInd] / 10.0);
         }
     }
     else
     {
-        attenuationDb.push_back(0);
+        attenuation_dB->push_back(0);
     }
+}
 
-    // store attenuation
-    channelParams->m_attenuation_dB = attenuationDb;
+void
+ThreeGppChannelModel::ComputeRayAngles(Ptr<const ThreeGppChannelParams> channelParams,
+                                       Ptr<const ParamsTable> table3gpp,
+                                       Double2DVector* rayAoaRadian,
+                                       Double2DVector* rayAodRadian,
+                                       Double2DVector* rayZoaRadian,
+                                       Double2DVector* rayZodRadian) const
+{
+    NS_LOG_FUNCTION(this);
 
-    // Step 8: Coupling of rays within a cluster for both azimuth and elevation
-    // shuffle all the arrays to perform random coupling
-    MatrixBasedChannelModel::Double2DVector rayAoaRadian(
-        channelParams->m_reducedClusterNumber,
-        DoubleVector(table3gpp->m_raysPerCluster,
-                     0)); // rayAoaRadian[n][m], where n is cluster index, m is ray index
-    MatrixBasedChannelModel::Double2DVector rayAodRadian(
-        channelParams->m_reducedClusterNumber,
-        DoubleVector(table3gpp->m_raysPerCluster,
-                     0)); // rayAodRadian[n][m], where n is cluster index, m is ray index
-    MatrixBasedChannelModel::Double2DVector rayZoaRadian(
-        channelParams->m_reducedClusterNumber,
-        DoubleVector(table3gpp->m_raysPerCluster,
-                     0)); // rayZoaRadian[n][m], where n is cluster index, m is ray index
-    MatrixBasedChannelModel::Double2DVector rayZodRadian(
-        channelParams->m_reducedClusterNumber,
-        DoubleVector(table3gpp->m_raysPerCluster,
-                     0)); // rayZodRadian[n][m], where n is cluster index, m is ray index
+    const DoubleVector& clusterAoa = channelParams->m_angle[AOA_INDEX];
+    const DoubleVector& clusterAod = channelParams->m_angle[AOD_INDEX];
+    const DoubleVector& clusterZoa = channelParams->m_angle[ZOA_INDEX];
+    const DoubleVector& clusterZod = channelParams->m_angle[ZOD_INDEX];
+
+    // Resize/initialize containers: [numClusters][raysPerCluster]
+    *rayAoaRadian = Double2DVector(channelParams->m_reducedClusterNumber,
+                                   DoubleVector(table3gpp->m_raysPerCluster, 0));
+    *rayAodRadian = Double2DVector(channelParams->m_reducedClusterNumber,
+                                   DoubleVector(table3gpp->m_raysPerCluster, 0));
+    *rayZoaRadian = Double2DVector(channelParams->m_reducedClusterNumber,
+                                   DoubleVector(table3gpp->m_raysPerCluster, 0));
+    *rayZodRadian = Double2DVector(channelParams->m_reducedClusterNumber,
+                                   DoubleVector(table3gpp->m_raysPerCluster, 0));
+
+    const double pow10_uLgZSD = std::pow(10.0, table3gpp->m_uLgZSD);
 
-    const double pow10_uLgZSD = pow(10, table3gpp->m_uLgZSD);
+    // NOTE: offSetAlpha[m] must be available in scope (same as previous implementation)
     for (uint8_t nInd = 0; nInd < channelParams->m_reducedClusterNumber; nInd++)
     {
         for (uint8_t mInd = 0; mInd < table3gpp->m_raysPerCluster; mInd++)
         {
-            double tempAoa = clusterAoa[nInd] + table3gpp->m_cASA * offSetAlpha[mInd]; //(7.5-13)
-            double tempZoa = clusterZoa[nInd] + table3gpp->m_cZSA * offSetAlpha[mInd]; //(7.5-18)
-            std::tie(rayAoaRadian[nInd][mInd], rayZoaRadian[nInd][mInd]) =
+            const double tempAoa =
+                clusterAoa[nInd] + table3gpp->m_cASA * offSetAlpha[mInd]; // (7.5-13)
+            const double tempZoa =
+                clusterZoa[nInd] + table3gpp->m_cZSA * offSetAlpha[mInd]; // (7.5-18)
+            std::tie((*rayAoaRadian)[nInd][mInd], (*rayZoaRadian)[nInd][mInd]) =
                 WrapAngles(DegreesToRadians(tempAoa), DegreesToRadians(tempZoa));
 
-            double tempAod = clusterAod[nInd] + table3gpp->m_cASD * offSetAlpha[mInd];    //(7.5-13)
-            double tempZod = clusterZod[nInd] + 0.375 * pow10_uLgZSD * offSetAlpha[mInd]; //(7.5-20)
-            std::tie(rayAodRadian[nInd][mInd], rayZodRadian[nInd][mInd]) =
+            const double tempAod =
+                clusterAod[nInd] + table3gpp->m_cASD * offSetAlpha[mInd]; // (7.5-13)
+            const double tempZod = clusterZod[nInd] + 0.375 * pow10_uLgZSD * offSetAlpha[mInd];
+            // (7.5-20)
+            std::tie((*rayAodRadian)[nInd][mInd], (*rayZodRadian)[nInd][mInd]) =
                 WrapAngles(DegreesToRadians(tempAod), DegreesToRadians(tempZod));
         }
     }
+}
 
+void
+ThreeGppChannelModel::RandomRaysCoupling(Ptr<const ThreeGppChannelParams> channelParams,
+                                         Double2DVector* rayAoaRadian,
+                                         Double2DVector* rayAodRadian,
+                                         Double2DVector* rayZoaRadian,
+                                         Double2DVector* rayZodRadian) const
+{
+    NS_LOG_FUNCTION(this);
     for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
     {
-        Shuffle(rayAodRadian[cIndex].begin(), rayAodRadian[cIndex].end(), m_uniformRvShuffle);
-        Shuffle(rayAoaRadian[cIndex].begin(), rayAoaRadian[cIndex].end(), m_uniformRvShuffle);
-        Shuffle(rayZodRadian[cIndex].begin(), rayZodRadian[cIndex].end(), m_uniformRvShuffle);
-        Shuffle(rayZoaRadian[cIndex].begin(), rayZoaRadian[cIndex].end(), m_uniformRvShuffle);
+        Shuffle((*rayAodRadian)[cIndex].begin(), (*rayAodRadian)[cIndex].end(), m_uniformRvShuffle);
+        Shuffle((*rayAoaRadian)[cIndex].begin(), (*rayAoaRadian)[cIndex].end(), m_uniformRvShuffle);
+        Shuffle((*rayZodRadian)[cIndex].begin(), (*rayZodRadian)[cIndex].end(), m_uniformRvShuffle);
+        Shuffle((*rayZoaRadian)[cIndex].begin(), (*rayZoaRadian)[cIndex].end(), m_uniformRvShuffle);
     }
+}
 
-    // store values
-    channelParams->m_rayAodRadian = rayAodRadian;
-    channelParams->m_rayAoaRadian = rayAoaRadian;
-    channelParams->m_rayZodRadian = rayZodRadian;
-    channelParams->m_rayZoaRadian = rayZoaRadian;
-
-    // Step 9: Generate the cross polarization power ratios
-    // Step 10: Draw initial phases
-
-    // vector containing the cross polarization power ratios, as defined by 7.5-21
-    auto& crossPolarizationPowerRatios = channelParams->m_crossPolarizationPowerRatios;
-    // rayAoaRadian[n][m], where n is cluster index, m is ray index
-    auto& clusterPhase = channelParams->m_clusterPhase;
+void
+ThreeGppChannelModel::GenerateCrossPolPowerRatiosAndInitialPhases(
+    Double2DVector* crossPolarizationPowerRatios,
+    Double3DVector* clusterPhase,
+    const uint8_t reducedClusterNumber,
+    Ptr<const ParamsTable> table3gpp) const
+{
+    // a vector containing the cross-polarization power ratios, as defined by 7.5-21
+    // store the PHI values for all the possible combination of polarizations
+    clusterPhase->clear();
+    clusterPhase->resize(reducedClusterNumber);
+    crossPolarizationPowerRatios->clear();
+    crossPolarizationPowerRatios->resize(reducedClusterNumber);
 
     const double uXprLinear = pow(10, table3gpp->m_uXpr / 10.0);     // convert to linear
     const double sigXprLinear = pow(10, table3gpp->m_sigXpr / 10.0); // convert to linear
 
-    // store the PHI values for all the possible combination of polarization
-    clusterPhase.resize(channelParams->m_reducedClusterNumber);
-    crossPolarizationPowerRatios.resize(channelParams->m_reducedClusterNumber);
-    for (uint8_t nInd = 0; nInd < channelParams->m_reducedClusterNumber; nInd++)
+    for (uint8_t clusterIndex = 0; clusterIndex < reducedClusterNumber; clusterIndex++)
     {
-        clusterPhase[nInd].resize(table3gpp->m_raysPerCluster);
-        crossPolarizationPowerRatios[nInd].resize(table3gpp->m_raysPerCluster);
-        for (uint8_t mInd = 0; mInd < table3gpp->m_raysPerCluster; mInd++)
-        {
-            clusterPhase[nInd][mInd].resize(4);
-            // used to store the XPR values
-            crossPolarizationPowerRatios[nInd][mInd] =
+        (*clusterPhase)[clusterIndex].resize(table3gpp->m_raysPerCluster);
+        (*crossPolarizationPowerRatios)[clusterIndex].resize(table3gpp->m_raysPerCluster);
+        for (uint8_t rayIndex = 0; rayIndex < table3gpp->m_raysPerCluster; rayIndex++)
+        {
+            (*clusterPhase)[clusterIndex][rayIndex].resize(4);
+            // stores the XPR values
+            (*crossPolarizationPowerRatios)[clusterIndex][rayIndex] =
                 std::pow(10, (m_normalRv->GetValue() * sigXprLinear + uXprLinear) / 10.0);
-            for (uint8_t pInd = 0; pInd < 4; pInd++)
+            for (uint8_t polIndex = 0; polIndex < 4; polIndex++)
             {
-                // used to store the PHI values
-                clusterPhase[nInd][mInd][pInd] = m_uniformRv->GetValue(-1 * M_PI, M_PI);
+                // stores the PHI values
+                (*clusterPhase)[clusterIndex][rayIndex][polIndex] =
+                    m_uniformRv->GetValue(-1 * M_PI, M_PI);
             }
         }
     }
+}
 
-    uint8_t cluster1st = 0;
-    uint8_t cluster2nd = 0; // first and second strongest cluster;
-    double maxPower = 0;
-    for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
+void
+ThreeGppChannelModel::FindStrongestClusters(Ptr<const ThreeGppChannelParams> channelParams,
+                                            Ptr<const ParamsTable> table3gpp,
+                                            uint8_t* cluster1st,
+                                            uint8_t* cluster2nd,
+                                            DoubleVector* clusterDelay,
+                                            Double2DVector* angles,
+                                            DoubleVector* alpha,
+                                            DoubleVector* dTerm,
+                                            DoubleVector* clusterPower) const
+
+{
+    NS_LOG_FUNCTION(this);
+    NS_ABORT_IF(table3gpp == nullptr);
+    NS_ABORT_IF(channelParams == nullptr);
+    NS_ABORT_IF(cluster1st == nullptr || cluster2nd == nullptr);
+    NS_ABORT_IF(clusterDelay == nullptr || angles == nullptr || alpha == nullptr ||
+                dTerm == nullptr);
+    NS_ABORT_IF(angles->size() != ZOD_INDEX + 1); // expects 4 directions: AOA/AOD/ZOA/ZOD
+    NS_ABORT_IF(channelParams->m_reducedClusterNumber == 0);
+    NS_ABORT_IF(channelParams->m_clusterPower.size() != channelParams->m_reducedClusterNumber);
+    NS_ABORT_IF(alpha->size() != channelParams->m_reducedClusterNumber);
+    NS_ABORT_IF(dTerm->size() != channelParams->m_reducedClusterNumber);
+    NS_ABORT_IF(clusterDelay->size() != channelParams->m_reducedClusterNumber);
+
+    for (auto dir : {AOA_INDEX, AOD_INDEX, ZOA_INDEX, ZOD_INDEX})
+    {
+        NS_ABORT_IF((*angles)[dir].size() != channelParams->m_reducedClusterNumber);
+    }
+
+    *cluster1st = 0;
+    *cluster2nd = 0;
+
+    if (channelParams->m_reducedClusterNumber > 1)
     {
-        if (maxPower < channelParams->m_clusterPower[cIndex])
+        // Find strongest
+        double maxPower = channelParams->m_clusterPower[0];
+        for (uint8_t cIndex = 1; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
+        {
+            if (maxPower < channelParams->m_clusterPower[cIndex])
+            {
+                maxPower = channelParams->m_clusterPower[cIndex];
+                *cluster1st = cIndex;
+            }
+        }
+        // Initialize second-strongest to "some other index"
+        *cluster2nd = (*cluster1st == 0) ? 1 : 0;
+        maxPower = channelParams->m_clusterPower[*cluster2nd];
+
+        // Find second-strongest (must be != cluster1st)
+        for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
         {
-            maxPower = channelParams->m_clusterPower[cIndex];
-            cluster1st = cIndex;
+            if (cIndex != *cluster1st && maxPower < channelParams->m_clusterPower[cIndex])
+            {
+                maxPower = channelParams->m_clusterPower[cIndex];
+                *cluster2nd = cIndex;
+            }
         }
     }
-    channelParams->m_cluster1st = cluster1st;
-    maxPower = 0;
-    for (uint8_t cIndex = 0; cIndex < channelParams->m_reducedClusterNumber; cIndex++)
+    NS_ABORT_IF(channelParams->m_reducedClusterNumber > 1 && *cluster1st == *cluster2nd);
+    NS_LOG_INFO("1st strongest cluster:" << +*cluster1st
+                                         << ", 2nd strongest cluster:" << +*cluster2nd);
+
+    // store the values for the subclusters
+    if (*cluster1st == *cluster2nd)
+    {
+        clusterDelay->push_back((*clusterDelay)[*cluster1st] + 1.28 * table3gpp->m_cDS);
+        clusterDelay->push_back((*clusterDelay)[*cluster1st] + 2.56 * table3gpp->m_cDS);
+
+        for (auto dir : {AOA_INDEX, AOD_INDEX, ZOA_INDEX, ZOD_INDEX})
+        {
+            auto& v = (*angles)[dir];
+            v.push_back(v[*cluster1st]);
+            v.push_back(v[*cluster1st]);
+        }
+
+        alpha->push_back((*alpha)[*cluster1st]);
+        alpha->push_back((*alpha)[*cluster1st]);
+        dTerm->push_back((*dTerm)[*cluster1st]);
+        dTerm->push_back((*dTerm)[*cluster1st]);
+        clusterPower->push_back((*clusterPower)[*cluster1st]);
+        clusterPower->push_back((*clusterPower)[*cluster1st]);
+    }
+    else
     {
-        if (maxPower < channelParams->m_clusterPower[cIndex] && cluster1st != cIndex)
+        const uint8_t min = std::min(*cluster1st, *cluster2nd);
+        const uint8_t max = std::max(*cluster1st, *cluster2nd);
+
+        clusterDelay->push_back((*clusterDelay)[min] + 1.28 * table3gpp->m_cDS);
+        clusterDelay->push_back((*clusterDelay)[min] + 2.56 * table3gpp->m_cDS);
+        clusterDelay->push_back((*clusterDelay)[max] + 1.28 * table3gpp->m_cDS);
+        clusterDelay->push_back((*clusterDelay)[max] + 2.56 * table3gpp->m_cDS);
+
+        for (auto dir : {AOA_INDEX, AOD_INDEX, ZOA_INDEX, ZOD_INDEX})
         {
-            maxPower = channelParams->m_clusterPower[cIndex];
-            cluster2nd = cIndex;
+            auto& v = (*angles)[dir];
+            v.push_back(v[min]);
+            v.push_back(v[min]);
+            v.push_back(v[max]);
+            v.push_back(v[max]);
         }
+
+        alpha->push_back((*alpha)[min]);
+        alpha->push_back((*alpha)[min]);
+        alpha->push_back((*alpha)[max]);
+        alpha->push_back((*alpha)[max]);
+
+        dTerm->push_back((*dTerm)[min]);
+        dTerm->push_back((*dTerm)[min]);
+        dTerm->push_back((*dTerm)[max]);
+        dTerm->push_back((*dTerm)[max]);
+
+        clusterPower->push_back((*clusterPower)[min]);
+        clusterPower->push_back((*clusterPower)[min]);
+        clusterPower->push_back((*clusterPower)[max]);
+        clusterPower->push_back((*clusterPower)[max]);
     }
-    channelParams->m_cluster2nd = cluster2nd;
 
-    NS_LOG_INFO("1st strongest cluster:" << +cluster1st
-                                         << ", 2nd strongest cluster:" << +cluster2nd);
+    const std::size_t expectedExtra = (*cluster1st == *cluster2nd) ? 2u : 4u;
+    const std::size_t expectedSize =
+        static_cast<std::size_t>(channelParams->m_reducedClusterNumber) + expectedExtra;
 
-    // store the delays and the angles for the subclusters
-    if (cluster1st == cluster2nd)
+    NS_ABORT_IF(clusterDelay->size() != expectedSize);
+    NS_ABORT_IF(alpha->size() != expectedSize);
+    NS_ABORT_IF(dTerm->size() != expectedSize);
+    for (auto dir : {AOA_INDEX, AOD_INDEX, ZOA_INDEX, ZOD_INDEX})
     {
-        clusterDelay.push_back(clusterDelay[cluster1st] + 1.28 * table3gpp->m_cDS);
-        clusterDelay.push_back(clusterDelay[cluster1st] + 2.56 * table3gpp->m_cDS);
-
-        clusterAoa.push_back(clusterAoa[cluster1st]);
-        clusterAoa.push_back(clusterAoa[cluster1st]);
+        NS_ABORT_IF((*angles)[dir].size() != expectedSize);
+    }
+}
 
-        clusterZoa.push_back(clusterZoa[cluster1st]);
-        clusterZoa.push_back(clusterZoa[cluster1st]);
+void
+ThreeGppChannelModel::TrimToBaseClusters(
+    const uint8_t reducedClusterNumber,
+    DoubleVector* delay,
+    Double2DVector* angles,
+    std::vector<std::vector<std::pair<double, double>>>* cachedAngleSincos,
+    DoubleVector* alpha,
+    DoubleVector* dTerm,
+    DoubleVector* clusterPower) const
+{
+    NS_LOG_FUNCTION(this);
+    NS_ABORT_IF(delay == nullptr || angles == nullptr);
+    NS_ABORT_IF(angles->size() != ZOD_INDEX + 1); // expects 4 directions: AOA/ZOA/AOD/ZOD
 
-        clusterAod.push_back(clusterAod[cluster1st]);
-        clusterAod.push_back(clusterAod[cluster1st]);
+    const auto totalClusterNumber = delay->size();
+    if (reducedClusterNumber == totalClusterNumber)
+    {
+        NS_LOG_DEBUG("Nothing to trim, the reduced cluster number is equal to the vector of "
+                     "per cluster delays");
+        return; // already trimmed
+    }
+    NS_ABORT_IF(totalClusterNumber < reducedClusterNumber);
+    const auto extraClusters = totalClusterNumber - reducedClusterNumber;
+    NS_ABORT_MSG_IF(!(extraClusters == 2 || extraClusters == 4),
+                    "The number of extra clusters to be removed can be either 2 or 4.");
+    // Trim delay
+    delay->erase(delay->end() - extraClusters, delay->end());
 
-        clusterZod.push_back(clusterZod[cluster1st]);
-        clusterZod.push_back(clusterZod[cluster1st]);
+    // Trim angles
+    for (auto dir : {AOA_INDEX, ZOA_INDEX, AOD_INDEX, ZOD_INDEX})
+    {
+        NS_ABORT_IF((*angles)[dir].size() != totalClusterNumber);
+        (*angles)[dir].erase((*angles)[dir].end() - extraClusters, (*angles)[dir].end());
     }
-    else
+
+    // Trim cached sin/cos angles (used by Doppler)
+    if (cachedAngleSincos && !cachedAngleSincos->empty())
     {
-        double min;
-        double max;
-        if (cluster1st < cluster2nd)
-        {
-            min = cluster1st;
-            max = cluster2nd;
-        }
-        else
+        for (auto dir : {AOA_INDEX, ZOA_INDEX, AOD_INDEX, ZOD_INDEX})
         {
-            min = cluster2nd;
-            max = cluster1st;
+            NS_ABORT_IF((*cachedAngleSincos)[dir].size() != totalClusterNumber);
+            (*cachedAngleSincos)[dir].erase((*cachedAngleSincos)[dir].end() - extraClusters,
+                                            (*cachedAngleSincos)[dir].end());
         }
-        clusterDelay.push_back(clusterDelay[min] + 1.28 * table3gpp->m_cDS);
-        clusterDelay.push_back(clusterDelay[min] + 2.56 * table3gpp->m_cDS);
-        clusterDelay.push_back(clusterDelay[max] + 1.28 * table3gpp->m_cDS);
-        clusterDelay.push_back(clusterDelay[max] + 2.56 * table3gpp->m_cDS);
-
-        clusterAoa.push_back(clusterAoa[min]);
-        clusterAoa.push_back(clusterAoa[min]);
-        clusterAoa.push_back(clusterAoa[max]);
-        clusterAoa.push_back(clusterAoa[max]);
-
-        clusterZoa.push_back(clusterZoa[min]);
-        clusterZoa.push_back(clusterZoa[min]);
-        clusterZoa.push_back(clusterZoa[max]);
-        clusterZoa.push_back(clusterZoa[max]);
-
-        clusterAod.push_back(clusterAod[min]);
-        clusterAod.push_back(clusterAod[min]);
-        clusterAod.push_back(clusterAod[max]);
-        clusterAod.push_back(clusterAod[max]);
-
-        clusterZod.push_back(clusterZod[min]);
-        clusterZod.push_back(clusterZod[min]);
-        clusterZod.push_back(clusterZod[max]);
-        clusterZod.push_back(clusterZod[max]);
-    }
-
-    channelParams->m_delay = clusterDelay;
-    channelParams->m_angle.clear();
-    channelParams->m_angle.push_back(clusterAoa);
-    channelParams->m_angle.push_back(clusterZoa);
-    channelParams->m_angle.push_back(clusterAod);
-    channelParams->m_angle.push_back(clusterZod);
+    }
 
-    // Precompute angles sincos
-    channelParams->m_cachedAngleSincos.resize(channelParams->m_angle.size());
+    // Trim Doppler terms
+    if (alpha && !alpha->empty())
+    {
+        NS_ABORT_IF(alpha->size() != totalClusterNumber);
+        alpha->erase(alpha->end() - extraClusters, alpha->end());
+    }
+    if (dTerm && !dTerm->empty())
+    {
+        NS_ABORT_IF(dTerm->size() != totalClusterNumber);
+        dTerm->erase(dTerm->end() - extraClusters, dTerm->end());
+    }
+
+    NS_ABORT_IF(delay->size() != reducedClusterNumber);
+    for (auto dir : {AOA_INDEX, ZOA_INDEX, AOD_INDEX, ZOD_INDEX})
+    {
+        NS_ABORT_IF((*angles)[dir].size() != reducedClusterNumber);
+    }
+
+    // Trim cluster powers
+    if (clusterPower && !clusterPower->empty())
+    {
+        NS_ABORT_IF(clusterPower->size() != totalClusterNumber);
+        clusterPower->erase(clusterPower->end() - extraClusters, clusterPower->end());
+    }
+}
+
+void
+ThreeGppChannelModel::PrecomputeAnglesSinCos(
+    Ptr<const ThreeGppChannelParams> channelParams,
+    std::vector<std::vector<std::pair<double, double>>>* cachedAngleSinCos) const
+{
+    NS_LOG_FUNCTION(this);
+    cachedAngleSinCos->resize(channelParams->m_angle.size());
     for (size_t direction = 0; direction < channelParams->m_angle.size(); direction++)
     {
-        channelParams->m_cachedAngleSincos[direction].resize(
-            channelParams->m_angle[direction].size());
+        (*cachedAngleSinCos)[direction].resize(channelParams->m_angle[direction].size());
         for (size_t cluster = 0; cluster < channelParams->m_angle[direction].size(); cluster++)
         {
-            channelParams->m_cachedAngleSincos[direction][cluster] = {
+            (*cachedAngleSinCos)[direction][cluster] = {
                 sin(channelParams->m_angle[direction][cluster] * DEG2RAD),
                 cos(channelParams->m_angle[direction][cluster] * DEG2RAD)};
         }
     }
+}
 
-    // Compute alpha and D as described in 3GPP TR 37.885 v15.3.0, Sec. 6.2.3
-    // These terms account for an additional Doppler contribution due to the
-    // presence of moving objects in the surrounding environment, such as in
-    // vehicular scenarios.
-    // This contribution is applied only to the delayed (reflected) paths and
-    // must be properly configured by setting the value of
-    // m_vScatt, which is defined as "maximum speed of the vehicle in the
-    // layout".
-    // By default, m_vScatt is set to 0, so there is no additional Doppler
-    // contribution.
-
-    DoubleVector dopplerTermAlpha;
-    DoubleVector dopplerTermD;
-
-    // 2 or 4 is added to account for additional subrays for the 1st and 2nd clusters, if there is
-    // only one cluster then would be added 2 more subrays (see creation of Husn channel matrix)
-    uint8_t updatedClusterNumber = (channelParams->m_reducedClusterNumber == 1)
-                                       ? channelParams->m_reducedClusterNumber + 2
-                                       : channelParams->m_reducedClusterNumber + 4;
-
-    for (uint8_t cIndex = 0; cIndex < updatedClusterNumber; cIndex++)
-    {
-        double alpha = 0;
-        double D = 0;
-        if (cIndex != 0)
-        {
-            alpha = m_uniformRvDoppler->GetValue(-1, 1);
-            D = m_uniformRvDoppler->GetValue(-m_vScatt, m_vScatt);
-        }
-        dopplerTermAlpha.push_back(alpha);
-        dopplerTermD.push_back(D);
+void
+ThreeGppChannelModel::GenerateDopplerTerms(const uint8_t reducedClusterNumber,
+                                           DoubleVector* dopplerTermAlpha,
+                                           DoubleVector* dopplerTermD) const
+{
+    NS_ABORT_IF(dopplerTermAlpha == nullptr || dopplerTermD == nullptr);
+    dopplerTermAlpha->assign(reducedClusterNumber, 0.0);
+    dopplerTermD->assign(reducedClusterNumber, 0.0);
+
+    for (uint8_t cIndex = 1; cIndex < reducedClusterNumber; ++cIndex)
+    {
+        (*dopplerTermAlpha)[cIndex] = m_uniformRvDoppler->GetValue(-1, 1);
+        (*dopplerTermD)[cIndex] = m_uniformRvDoppler->GetValue(-m_vScatt, m_vScatt);
+    }
+}
+
+double
+ThreeGppChannelModel::ComputeEndpointDisplacement2d(Ptr<const MobilityModel> aMob,
+                                                    Ptr<const MobilityModel> bMob,
+                                                    const Vector& lastPositionFirst,
+                                                    const Vector& lastPositionSecond) const
+
+{
+    Ptr<const MobilityModel> firstMob = aMob;
+    Ptr<const MobilityModel> secondMob = bMob;
+    if (aMob->GetObject<Node>()->GetId() > bMob->GetObject<Node>()->GetId())
+    {
+        std::swap(firstMob, secondMob);
+    }
+
+    const Vector posFirst = firstMob->GetPosition();
+    const Vector posSecond = secondMob->GetPosition();
+
+    const double dispFirst =
+        std::hypot(posFirst.x - lastPositionFirst.x, posFirst.y - lastPositionFirst.y);
+    const double dispSecond =
+        std::hypot(posSecond.x - lastPositionSecond.x, posSecond.y - lastPositionSecond.y);
+
+    return std::max(dispFirst, dispSecond);
+}
+
+void
+ThreeGppChannelModel::UpdateLinkGeometry(Ptr<const MobilityModel> aMob,
+                                         Ptr<const MobilityModel> bMob,
+                                         double* distance2D,
+                                         double* distance3D,
+                                         double* endpointDisplacement2D,
+                                         double* relativeDisplacement2D,
+                                         Vector* lastPositionFirst,
+                                         Vector* lastPositionSecond,
+                                         Vector2D* lastRelativePosition2D) const
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT(distance2D && distance3D && endpointDisplacement2D && relativeDisplacement2D &&
+              lastPositionFirst && lastPositionSecond && lastRelativePosition2D);
+
+    // Canonical ordering by node ID (min ID / max ID)
+    Ptr<const MobilityModel> firstMob = aMob;
+    Ptr<const MobilityModel> secondMob = bMob;
+    if (aMob->GetObject<Node>()->GetId() > bMob->GetObject<Node>()->GetId())
+    {
+        std::swap(firstMob, secondMob);
+    }
+
+    // Obtain the new positions
+    const Vector posFirst = firstMob->GetPosition();
+    const Vector posSecond = secondMob->GetPosition();
+
+    const double deltaX = posFirst.x - posSecond.x;
+    const double deltaY = posFirst.y - posSecond.y;
+
+    // Link 2D distance
+    *distance2D = std::hypot(deltaX, deltaY);
+
+    // Link 3D distance
+    const double deltaZ = posFirst.z - posSecond.z;
+    *distance3D = std::hypot(*distance2D, deltaZ);
+
+    // Relative (geometry) displacement: change in the canonical Tx-Rx position vector
+    const Vector2D relNow{deltaX, deltaY};
+    *relativeDisplacement2D = (relNow - *lastRelativePosition2D).GetLength();
+
+    // Endpoint displacement: max motion of the two endpoints since the last update
+    const double dispFirst =
+        std::hypot(posFirst.x - lastPositionFirst->x, posFirst.y - lastPositionFirst->y);
+    const double dispSecond =
+        std::hypot(posSecond.x - lastPositionSecond->x, posSecond.y - lastPositionSecond->y);
+    *endpointDisplacement2D = std::max(dispFirst, dispSecond);
+
+    NS_ASSERT(std::isfinite(*distance2D));
+    NS_ASSERT(std::isfinite(*distance3D));
+    NS_ASSERT(std::isfinite(*relativeDisplacement2D));
+    NS_ASSERT(std::isfinite(*endpointDisplacement2D));
+    NS_ASSERT(*distance2D >= 0.0);
+    NS_ASSERT(*distance3D >= 0.0);
+    NS_ASSERT(*relativeDisplacement2D >= 0.0);
+    NS_ASSERT(*endpointDisplacement2D >= 0.0);
+
+    // Store the new positions for next time (canonically ordered endpoints)
+    *lastPositionFirst = posFirst;
+    *lastPositionSecond = posSecond;
+    *lastRelativePosition2D = relNow;
+}
+
+Ptr<ThreeGppChannelModel::ThreeGppChannelParams>
+ThreeGppChannelModel::GenerateChannelParameters(Ptr<const ChannelCondition> channelCondition,
+                                                Ptr<const ParamsTable> table3gpp,
+                                                Ptr<const MobilityModel> aMob,
+                                                Ptr<const MobilityModel> bMob) const
+{
+    NS_LOG_FUNCTION(this);
+    // Enforce canonical ordering (by node id) for deterministic parameter generation.
+    Ptr<const MobilityModel> aMobOrdered = aMob;
+    Ptr<const MobilityModel> bMobOrdered = bMob;
+    const uint32_t idA = aMob->GetObject<Node>()->GetId();
+    const uint32_t idB = bMob->GetObject<Node>()->GetId();
+    if (idA > idB)
+    {
+        std::swap(aMobOrdered, bMobOrdered);
+    }
+
+    // Create new channel parameters instance
+    Ptr<ThreeGppChannelParams> channelParams = Create<ThreeGppChannelParams>();
+    // Set basic parameters
+    channelParams->m_generatedTime = Simulator::Now();
+    channelParams->m_nodeIds = {aMobOrdered->GetObject<Node>()->GetId(),
+                                bMobOrdered->GetObject<Node>()->GetId()};
+    channelParams->m_losCondition = channelCondition->GetLosCondition();
+    channelParams->m_o2iCondition = channelCondition->GetO2iCondition();
+    // Angles are generated with aMob as transmitter (departure) and bMob as receiver (arrival).
+    channelParams->m_txSpeed = aMobOrdered->GetVelocity();
+    channelParams->m_rxSpeed = bMobOrdered->GetVelocity();
+    UpdateLinkGeometry(aMobOrdered,
+                       bMobOrdered,
+                       &channelParams->m_dis2D,
+                       &channelParams->m_dis3D,
+                       &channelParams->m_endpointDisplacement2D,
+                       &channelParams->m_relativeDisplacement2D,
+                       &channelParams->m_lastPositionFirst,
+                       &channelParams->m_lastPositionSecond,
+                       &channelParams->m_lastRelativePosition2D);
+
+    // Step 4: Generate large-scale parameters. All LSPS are uncorrelated.
+    const LargeScaleParameters lsps = GenerateLSPs(channelParams->m_losCondition, table3gpp);
+
+    channelParams->m_DS = lsps.DS;
+    channelParams->m_K_factor = lsps.kFactor;
+
+    // Step 5: Generate Delays and normalize them. Save minTau to be used for channel consistency.
+    double minTau = 100.0;
+    GenerateClusterDelays(lsps.DS, table3gpp, &minTau, &channelParams->m_delay);
+    /* since the scaled Los delays are not to be used in cluster power generation,
+     * we will generate cluster power first and resume to compute Los cluster delay later.*/
+
+    // Step 6: Generate cluster powers.
+    GenerateClusterShadowingTerm(table3gpp, &channelParams->m_clusterShadowing);
+    GenerateClusterPowers(channelParams->m_delay,
+                          lsps.DS,
+                          table3gpp,
+                          channelParams->m_clusterShadowing,
+                          &channelParams->m_clusterPower);
+    double powerMax = 0;
+    const DoubleVector clusterPowerForAngles = RemoveWeakClusters(&channelParams->m_clusterPower,
+                                                                  &channelParams->m_delay,
+                                                                  channelParams->m_losCondition,
+                                                                  table3gpp,
+                                                                  lsps.kFactor,
+                                                                  &powerMax);
+
+    channelParams->m_reducedClusterNumber = channelParams->m_clusterPower.size();
+    // Resume step 5 to compute the delay for LoS condition.
+    if (channelParams->m_losCondition == ChannelCondition::LOS)
+    {
+        AdjustClusterDelaysForLosCondition(&channelParams->m_delay,
+                                           channelParams->m_reducedClusterNumber,
+                                           channelParams->m_K_factor);
     }
-    channelParams->m_alpha = dopplerTermAlpha;
-    channelParams->m_D = dopplerTermD;
+
+    // Step 7: Generate arrival and departure angles for both azimuth and elevation.
+    const auto cPhi = CalculateCphi(channelParams->m_losCondition, table3gpp, lsps.kFactor);
+    const auto cTheta = CalculateCtheta(channelParams->m_losCondition, table3gpp, lsps.kFactor);
+
+    GenerateClusterAngles(channelParams,
+                          clusterPowerForAngles,
+                          powerMax,
+                          cPhi,
+                          cTheta,
+                          lsps,
+                          aMobOrdered,
+                          bMobOrdered,
+                          table3gpp,
+                          &channelParams->m_angle);
+
+    // if blockage enabled, calculate, apply and store attenuation
+    ApplyAttenuationToClusterPowers(&channelParams->m_nonSelfBlocking,
+                                    &channelParams->m_clusterPower,
+                                    &channelParams->m_attenuation_dB,
+                                    channelParams,
+                                    channelParams->m_angle[AOA_INDEX],
+                                    channelParams->m_angle[ZOA_INDEX],
+                                    table3gpp);
+    // Step 8: Coupling of rays within a cluster for both azimuth and elevation
+    // shuffle all the arrays to perform random coupling
+    // Step a): update per-ray angles around cluster means (no shuffling)
+    ComputeRayAngles(channelParams,
+                     table3gpp,
+                     &channelParams->m_rayAoaRadian,
+                     &channelParams->m_rayAodRadian,
+                     &channelParams->m_rayZoaRadian,
+                     &channelParams->m_rayZodRadian);
+
+    // Step b): random coupling by shuffling rays within each cluster
+    RandomRaysCoupling(channelParams,
+                       &channelParams->m_rayAoaRadian,
+                       &channelParams->m_rayAodRadian,
+                       &channelParams->m_rayZoaRadian,
+                       &channelParams->m_rayZodRadian);
+
+    // Step 9: Generate the cross-polarization power ratios
+    // Step 10: Draw initial phases
+    GenerateCrossPolPowerRatiosAndInitialPhases(&channelParams->m_crossPolarizationPowerRatios,
+                                                &channelParams->m_clusterPhase,
+                                                channelParams->m_reducedClusterNumber,
+                                                table3gpp);
+
+    // Generate Doppler terms
+    GenerateDopplerTerms(channelParams->m_reducedClusterNumber,
+                         &channelParams->m_alpha,
+                         &channelParams->m_D);
+
+    // save delay consistency for the channel updates with the reduced cluster number
+    channelParams->m_delayConsistency = channelParams->m_delay;
+    for (uint8_t cInd = 0; cInd < channelParams->m_reducedClusterNumber; cInd++)
+    {
+        // 7.6.3.2, Procedure A, k=0 -> t_0
+        if (channelParams->m_losCondition != ChannelCondition::LOS)
+        {
+            channelParams->m_delayConsistency[cInd] += minTau;
+        }
+        // distance between RX antenna and TX antenna at t_0 in [m] divided by the speed of light
+        // [m/s]
+        channelParams->m_delayConsistency[cInd] += channelParams->m_dis3D / 3e8;
+    }
+
+    FindStrongestClusters(channelParams,
+                          table3gpp,
+                          &channelParams->m_cluster1st,
+                          &channelParams->m_cluster2nd,
+                          &channelParams->m_delay,
+                          &channelParams->m_angle,
+                          &channelParams->m_alpha,
+                          &channelParams->m_D,
+                          &channelParams->m_clusterPower);
+
+    // Precompute angles sincos
+    PrecomputeAnglesSinCos(channelParams, &channelParams->m_cachedAngleSincos);
 
     return channelParams;
 }
 
+void
+ThreeGppChannelModel::UpdateChannelParameters(Ptr<ThreeGppChannelParams> channelParams,
+                                              Ptr<const ChannelCondition> channelCondition,
+                                              Ptr<const MobilityModel> aMob,
+                                              Ptr<const MobilityModel> bMob) const
+{
+    NS_LOG_FUNCTION(this);
+    NS_ASSERT_MSG(channelParams != nullptr, "Channel parameters cannot be null");
+    NS_ASSERT_MSG(channelCondition != nullptr, "Channel condition cannot be null");
+    NS_ASSERT_MSG(aMob != nullptr, "Mobility model A cannot be null");
+    NS_ASSERT_MSG(bMob != nullptr, "Mobility model B cannot be null");
+
+    // Ensure a/b ordering matches the direction used when the params were generated.
+    // This keeps angle/speed associations consistent across reciprocal calls.
+    Ptr<const MobilityModel> aMobOrdered = aMob;
+    Ptr<const MobilityModel> bMobOrdered = bMob;
+    const auto storedIds = channelParams->m_nodeIds;
+    const auto callIds =
+        std::make_pair(aMob->GetObject<Node>()->GetId(), bMob->GetObject<Node>()->GetId());
+    const auto reversedIds = std::make_pair(callIds.second, callIds.first);
+    NS_ASSERT_MSG(storedIds == callIds || storedIds == reversedIds,
+                  "Channel params node ids do not match this link");
+    if (storedIds != callIds)
+    {
+        std::swap(aMobOrdered, bMobOrdered);
+    }
+
+    TrimToBaseClusters(channelParams->m_reducedClusterNumber,
+                       &channelParams->m_delay,
+                       &channelParams->m_angle,
+                       &channelParams->m_cachedAngleSincos,
+                       &channelParams->m_alpha,
+                       &channelParams->m_D,
+                       &channelParams->m_clusterPower);
+
+    UpdateLinkGeometry(aMobOrdered,
+                       bMobOrdered,
+                       &channelParams->m_dis2D,
+                       &channelParams->m_dis3D,
+                       &channelParams->m_endpointDisplacement2D,
+                       &channelParams->m_relativeDisplacement2D,
+                       &channelParams->m_lastPositionFirst,
+                       &channelParams->m_lastPositionSecond,
+                       &channelParams->m_lastRelativePosition2D);
+
+    channelParams->m_txSpeed = aMobOrdered->GetVelocity();
+    channelParams->m_rxSpeed = bMobOrdered->GetVelocity();
+    channelParams->m_losCondition = channelCondition->GetLosCondition();
+    channelParams->m_o2iCondition = channelCondition->GetO2iCondition();
+
+    // Update LSPs for new distance
+    // Update cluster delay (7.6-9, 7.6-10, 7.6-10aa, 7.6-10a)
+    const DoubleVector prevClusterDelay = channelParams->m_delayConsistency;
+    UpdateClusterDelay(&channelParams->m_delay, &channelParams->m_delayConsistency, channelParams);
+
+    // Get the 3GPP parameter table
+    const Ptr<const ParamsTable> table3gpp =
+        GetThreeGppTable(aMobOrdered, bMobOrdered, channelCondition);
+
+    UpdateClusterShadowingTerm(table3gpp,
+                               &channelParams->m_clusterShadowing,
+                               channelParams->m_relativeDisplacement2D);
+    // According to 3GPP 38.901. Procedure A, cluster powers are updated as in Step 6 using the
+    // cluster delays from Equation (7.6-10a).
+
+    GenerateClusterPowers(channelParams->m_delay,
+                          channelParams->m_DS,
+                          table3gpp,
+                          channelParams->m_clusterShadowing,
+                          &channelParams->m_clusterPower);
+
+    // draw random signs from cluster angles +1,-1 and save them to reuse them for the channel
+    // update
+    if (channelParams->m_losCondition != ChannelCondition::LOS &&
+        channelParams->m_clusterXnNlosSign.empty())
+    {
+        GenerateClusterXnNLos(channelParams->m_reducedClusterNumber,
+                              &channelParams->m_clusterXnNlosSign);
+    }
+
+    // Update cluster departure and arrival angles
+    const Double2DVector previousAngles = channelParams->m_angle;
+    UpdateClusterAngles(channelParams, &channelParams->m_angle, prevClusterDelay);
+
+    NS_ABORT_IF(channelParams->m_clusterPower.empty());
+
+    // if blockage enabled, calculate, apply and store attenuation
+    ApplyAttenuationToClusterPowers(&channelParams->m_nonSelfBlocking,
+                                    &channelParams->m_clusterPower,
+                                    &channelParams->m_attenuation_dB,
+                                    channelParams,
+                                    channelParams->m_angle[AOA_INDEX],
+                                    channelParams->m_angle[ZOA_INDEX],
+                                    table3gpp);
+
+    ShiftRayAnglesToUpdatedClusterMeans(table3gpp,
+                                        channelParams,
+                                        previousAngles,
+                                        &channelParams->m_rayAoaRadian,
+                                        &channelParams->m_rayAodRadian,
+                                        &channelParams->m_rayZoaRadian,
+                                        &channelParams->m_rayZodRadian);
+
+    FindStrongestClusters(channelParams,
+                          table3gpp,
+                          &channelParams->m_cluster1st,
+                          &channelParams->m_cluster2nd,
+                          &channelParams->m_delay,
+                          &channelParams->m_angle,
+                          &channelParams->m_alpha,
+                          &channelParams->m_D,
+                          &channelParams->m_clusterPower);
+
+    // Precompute angle sin/cos for efficiency
+    PrecomputeAnglesSinCos(channelParams, &channelParams->m_cachedAngleSincos);
+
+    channelParams->m_generatedTime = Simulator::Now(); // Update timing information
+
+    NS_LOG_DEBUG("Updated channel parameters for consistency (Procedure A): "
+                 << "Clusters: " << channelParams->m_reducedClusterNumber
+                 << ", DS: " << channelParams->m_DS << ", K-factor: " << channelParams->m_K_factor);
+}
+
 Ptr<MatrixBasedChannelModel::ChannelMatrix>
 ThreeGppChannelModel::GetNewChannel(Ptr<const ThreeGppChannelParams> channelParams,
                                     Ptr<const ParamsTable> table3gpp,
-                                    const Ptr<const MobilityModel> sMob,
-                                    const Ptr<const MobilityModel> uMob,
+                                    Ptr<const MobilityModel> sMob,
+                                    Ptr<const MobilityModel> uMob,
                                     Ptr<const PhasedArrayModel> sAntenna,
                                     Ptr<const PhasedArrayModel> uAntenna) const
 {
     NS_LOG_FUNCTION(this);
 
     NS_ASSERT_MSG(m_frequency > 0.0, "Set the operating frequency first!");
+    NS_ASSERT_MSG(channelParams != nullptr, "Channel parameters cannot be null");
+    NS_ASSERT_MSG(sMob != nullptr && uMob != nullptr, "Mobility models cannot be null");
+    NS_ASSERT_MSG(sAntenna != nullptr && uAntenna != nullptr, "Antennas cannot be null");
+    const auto callIds =
+        std::make_pair(sMob->GetObject<Node>()->GetId(), uMob->GetObject<Node>()->GetId());
+    const auto reversedIds = std::make_pair(callIds.second, callIds.first);
+    NS_ASSERT_MSG(channelParams->m_nodeIds == callIds || channelParams->m_nodeIds == reversedIds,
+                  "Channel params node ids do not match this link");
 
     // create a channel matrix instance
     Ptr<ChannelMatrix> channelMatrix = Create<ChannelMatrix>();
@@ -3246,13 +3988,16 @@
     // save in which order is generated this matrix
     channelMatrix->m_nodeIds =
         std::make_pair(sMob->GetObject<Node>()->GetId(), uMob->GetObject<Node>()->GetId());
-    // check if channelParams structure is generated in direction s-to-u or u-to-s
-    bool isSameDirection = (channelParams->m_nodeIds == channelMatrix->m_nodeIds);
-
-    MatrixBasedChannelModel::Double2DVector rayAodRadian;
-    MatrixBasedChannelModel::Double2DVector rayAoaRadian;
-    MatrixBasedChannelModel::Double2DVector rayZodRadian;
-    MatrixBasedChannelModel::Double2DVector rayZoaRadian;
+    // check if channelParams structure is generated in a direction s-to-u or u-to-s
+    bool isSameDirection = channelParams->m_nodeIds == channelMatrix->m_nodeIds;
+    channelMatrix->m_antennaPair =
+        std::make_pair(sAntenna->GetId(),
+                       uAntenna->GetId()); // save antenna pair, with the exact order of s and u
+
+    Double2DVector rayAodRadian;
+    Double2DVector rayAoaRadian;
+    Double2DVector rayZodRadian;
+    Double2DVector rayZoaRadian;
 
     // if channel params is generated in the same direction in which we
     // generate the channel matrix, angles and zenith od departure and arrival are ok,
@@ -3283,7 +4028,7 @@
     // the total cluster will generally be numReducedCLuster + 4.
     // However, it might be that m_cluster1st = m_cluster2nd. In this case the
     // total number of clusters will be numReducedCLuster + 2.
-    uint16_t numOverallCluster = (channelParams->m_cluster1st != channelParams->m_cluster2nd)
+    uint16_t numOverallCluster = channelParams->m_cluster1st != channelParams->m_cluster2nd
                                      ? channelParams->m_reducedClusterNumber + 4
                                      : channelParams->m_reducedClusterNumber + 2;
     Complex3DVector hUsn(uSize, sSize, numOverallCluster); // channel coefficient hUsn (u, s, n);
@@ -3303,15 +4048,7 @@
     NS_ASSERT(table3gpp->m_raysPerCluster <= rayAoaRadian[0].size());
     NS_ASSERT(table3gpp->m_raysPerCluster <= rayAodRadian[0].size());
 
-    double x = sMob->GetPosition().x - uMob->GetPosition().x;
-    double y = sMob->GetPosition().y - uMob->GetPosition().y;
-    double distance2D = sqrt(x * x + y * y);
-    // NOTE we assume hUT = min (height(a), height(b)) and
-    // hBS = max (height (a), height (b))
-    double hUt = std::min(sMob->GetPosition().z, uMob->GetPosition().z);
-    double hBs = std::max(sMob->GetPosition().z, uMob->GetPosition().z);
-    // compute the 3D distance using eq. 7.4-1
-    double distance3D = std::sqrt(distance2D * distance2D + (hBs - hUt) * (hBs - hUt));
+    double distance3D = channelParams->m_dis3D;
 
     Angles sAngle(uMob->GetPosition(), sMob->GetPosition());
     Angles uAngle(sMob->GetPosition(), uMob->GetPosition());
@@ -3376,13 +4113,13 @@
                                    channelParams->m_rayZodRadian[nIndex][mIndex]),
                             polSa);
                     raysPreComp[std::make_pair(polSa, polUa)](nIndex, mIndex) =
-                        std::complex<double>(cos(initialPhase[0]), sin(initialPhase[0])) *
+                        std::complex(cos(initialPhase[0]), sin(initialPhase[0])) *
                             rxFieldPatternTheta * txFieldPatternTheta +
-                        std::complex<double>(cos(initialPhase[1]), sin(initialPhase[1])) *
+                        std::complex(cos(initialPhase[1]), sin(initialPhase[1])) *
                             std::sqrt(1.0 / k) * rxFieldPatternTheta * txFieldPatternPhi +
-                        std::complex<double>(cos(initialPhase[2]), sin(initialPhase[2])) *
+                        std::complex(cos(initialPhase[2]), sin(initialPhase[2])) *
                             std::sqrt(1.0 / k) * rxFieldPatternPhi * txFieldPatternTheta +
-                        std::complex<double>(cos(initialPhase[3]), sin(initialPhase[3])) *
+                        std::complex(cos(initialPhase[3]), sin(initialPhase[3])) *
                             rxFieldPatternPhi * txFieldPatternPhi;
                 }
             }
@@ -3441,8 +4178,8 @@
                         rays += raysPreComp[std::make_pair(sAntenna->GetElemPol(sIndex),
                                                            uAntenna->GetElemPol(uIndex))](nIndex,
                                                                                           mIndex) *
-                                std::complex<double>(cos(rxPhaseDiff), sin(rxPhaseDiff)) *
-                                std::complex<double>(cos(txPhaseDiff), sin(txPhaseDiff));
+                                std::complex(cos(rxPhaseDiff), sin(rxPhaseDiff)) *
+                                std::complex(cos(txPhaseDiff), sin(txPhaseDiff));
                     }
                     rays *=
                         sqrt(channelParams->m_clusterPower[nIndex] / table3gpp->m_raysPerCluster);
@@ -3472,8 +4209,8 @@
                             raysPreComp[std::make_pair(sAntenna->GetElemPol(sIndex),
                                                        uAntenna->GetElemPol(uIndex))](nIndex,
                                                                                       mIndex) *
-                            std::complex<double>(cos(rxPhaseDiff), sin(rxPhaseDiff)) *
-                            std::complex<double>(cos(txPhaseDiff), sin(txPhaseDiff));
+                            std::complex(cos(rxPhaseDiff), sin(rxPhaseDiff)) *
+                            std::complex(cos(txPhaseDiff), sin(txPhaseDiff));
 
                         switch (mIndex)
                         {
@@ -3522,8 +4259,8 @@
     if (channelParams->m_losCondition == ChannelCondition::LOS) //(7.5-29) && (7.5-30)
     {
         double lambda = 3.0e8 / m_frequency; // the wavelength of the carrier frequency
-        std::complex<double> phaseDiffDueToDistance(cos(-2 * M_PI * distance3D / lambda),
-                                                    sin(-2 * M_PI * distance3D / lambda));
+        std::complex phaseDiffDueToDistance(cos(-2 * M_PI * distance3D / lambda),
+                                            sin(-2 * M_PI * distance3D / lambda));
 
         const double sinUAngleIncl = sin(uAngle.GetInclination());
         const double cosUAngleIncl = cos(uAngle.GetInclination());
@@ -3559,9 +4296,8 @@
 
                 ray = (rxFieldPatternTheta * txFieldPatternTheta -
                        rxFieldPatternPhi * txFieldPatternPhi) *
-                      phaseDiffDueToDistance *
-                      std::complex<double>(cos(rxPhaseDiff), sin(rxPhaseDiff)) *
-                      std::complex<double>(cos(txPhaseDiff), sin(txPhaseDiff));
+                      phaseDiffDueToDistance * std::complex(cos(rxPhaseDiff), sin(rxPhaseDiff)) *
+                      std::complex(cos(txPhaseDiff), sin(txPhaseDiff));
 
                 double kLinear = pow(10, channelParams->m_K_factor / 10.0);
                 // the LOS path should be attenuated if blockage is enabled.
@@ -3579,17 +4315,29 @@
         }
     }
 
-    NS_LOG_DEBUG("Husn (sAntenna, uAntenna):" << sAntenna->GetId() << ", " << uAntenna->GetId());
-    for (size_t cIndex = 0; cIndex < hUsn.GetNumPages(); cIndex++)
+    std::ostringstream oss;
+    oss << "Husn (sAntenna, uAntenna): " << sAntenna->GetId() << ", " << uAntenna->GetId()
+        << " | vals=[";
+
+    bool first = true;
+    for (size_t cIndex = 0; cIndex < hUsn.GetNumPages(); ++cIndex)
     {
-        for (size_t rowIdx = 0; rowIdx < hUsn.GetNumRows(); rowIdx++)
+        for (size_t rowIdx = 0; rowIdx < hUsn.GetNumRows(); ++rowIdx)
         {
-            for (size_t colIdx = 0; colIdx < hUsn.GetNumCols(); colIdx++)
+            for (size_t colIdx = 0; colIdx < hUsn.GetNumCols(); ++colIdx)
             {
-                NS_LOG_DEBUG(" " << hUsn(rowIdx, colIdx, cIndex) << ",");
+                if (!first)
+                {
+                    oss << ", ";
+                }
+                first = false;
+                oss << hUsn(rowIdx, colIdx, cIndex);
             }
         }
     }
+    oss << "]";
+
+    NS_LOG_DEBUG(oss.str());
 
     NS_LOG_INFO("size of coefficient matrix (rows, columns, clusters) = ("
                 << hUsn.GetNumRows() << ", " << hUsn.GetNumCols() << ", " << hUsn.GetNumPages()
@@ -3619,20 +4367,56 @@
     return std::make_pair(azimuthRad, inclinationRad);
 }
 
-MatrixBasedChannelModel::DoubleVector
-ThreeGppChannelModel::CalcAttenuationOfBlockage(
-    const Ptr<ThreeGppChannelModel::ThreeGppChannelParams> channelParams,
-    const DoubleVector& clusterAOA,
-    const DoubleVector& clusterZOA) const
+void
+ThreeGppChannelModel::ShiftRayAnglesToUpdatedClusterMeans(
+    Ptr<const ParamsTable> table3gpp,
+    Ptr<const ThreeGppChannelParams> channelParams,
+    const Double2DVector& prevClusterAngles,
+    Double2DVector* rayAodRadian,
+    Double2DVector* rayAoaRadian,
+    Double2DVector* rayZodRadian,
+    Double2DVector* rayZoaRadian) const
 {
-    NS_LOG_FUNCTION(this);
+    NS_LOG_FUNCTION(this << channelParams);
+
+    for (size_t n = 0; n < channelParams->m_reducedClusterNumber; ++n)
+    {
+        // Compute per-cluster deltas (degrees) and convert to radians
+        const double dAoaRad = DegreesToRadians(prevClusterAngles[AOA_INDEX][n] -
+                                                channelParams->m_angle[AOA_INDEX][n]);
+        const double dAodRad = DegreesToRadians(prevClusterAngles[AOD_INDEX][n] -
+                                                channelParams->m_angle[AOD_INDEX][n]);
+        const double dZoaRad = DegreesToRadians(prevClusterAngles[ZOA_INDEX][n] -
+                                                channelParams->m_angle[ZOA_INDEX][n]);
+        const double dZodRad = DegreesToRadians(prevClusterAngles[ZOD_INDEX][n] -
+                                                channelParams->m_angle[ZOD_INDEX][n]);
+
+        // Apply deltas to each ray, preserving coupling and offsets
+        for (size_t m = 0; m < table3gpp->m_raysPerCluster; ++m)
+        {
+            std::tie((*rayAoaRadian)[n][m], (*rayZoaRadian)[n][m]) =
+                WrapAngles((*rayAoaRadian)[n][m] + dAoaRad, (*rayZoaRadian)[n][m] + dZoaRad);
+            std::tie((*rayAodRadian)[n][m], (*rayZodRadian)[n][m]) =
+                WrapAngles((*rayAodRadian)[n][m] + dAodRad, (*rayZodRadian)[n][m] + dZodRad);
+        }
+    }
+}
 
-    auto clusterNum = clusterAOA.size();
+void
+ThreeGppChannelModel::CalcAttenuationOfBlockage(Double2DVector* nonSelfBlocking,
+                                                DoubleVector* powerAttenuation,
+                                                Ptr<const ThreeGppChannelParams> channelParams,
+                                                const DoubleVector& clusterAOA,
+                                                const DoubleVector& clusterZOA,
+                                                Ptr<const ParamsTable> table3gpp) const
+{
+    NS_LOG_FUNCTION(this);
 
+    const auto clusterNum = clusterAOA.size();
     // Initial power attenuation for all clusters to be 0 dB
-    DoubleVector powerAttenuation(clusterNum, 0);
+    *powerAttenuation = DoubleVector(clusterNum, 0);
 
-    // step a: the number of non-self blocking blockers is stored in m_numNonSelfBlocking.
+    // step a: the number of non-self-blocking blockers is stored in m_numNonSelfBlocking.
 
     // step b:Generate the size and location of each blocker
     // generate self blocking (i.e., for blockage from the human body)
@@ -3651,14 +4435,14 @@
     }
 
     // generate or update non-self blocking
-    if (channelParams->m_nonSelfBlocking.empty()) // generate new blocking regions
+    if (nonSelfBlocking->empty()) // generate new blocking regions
     {
         for (uint16_t blockInd = 0; blockInd < m_numNonSelfBlocking; blockInd++)
         {
             // draw value from table 7.6.4.1-2 Blocking region parameters
             DoubleVector table;
             table.push_back(m_normalRv->GetValue()); // phi_k: store the normal RV that will be
-                                                     // mapped to uniform (0,360) later.
+            // mapped to uniform (0,360) later.
             if (m_scenario == "InH-OfficeMixed" || m_scenario == "InH-OfficeOpen")
             {
                 table.push_back(m_uniformRv->GetValue(15, 45)); // x_k
@@ -3673,39 +4457,20 @@
                 table.push_back(5);                            // y_k
                 table.push_back(10);                           // r
             }
-            channelParams->m_nonSelfBlocking.push_back(table);
+            nonSelfBlocking->push_back(table);
         }
     }
     else
     {
-        double deltaX = sqrt(pow(channelParams->m_preLocUT.x - channelParams->m_locUT.x, 2) +
-                             pow(channelParams->m_preLocUT.y - channelParams->m_locUT.y, 2));
         // if deltaX and speed are both 0, the autocorrelation is 1, skip updating
-        if (deltaX > 1e-6 || m_blockerSpeed > 1e-6)
+        if (const double deltaX = channelParams->m_endpointDisplacement2D;
+            deltaX > 1e-6 || m_blockerSpeed > 1e-6)
         {
-            double corrDis;
-            // draw value from table 7.6.4.1-4: Spatial correlation distance for different
-            // m_scenarios.
-            if (m_scenario == "InH-OfficeMixed" || m_scenario == "InH-OfficeOpen")
-            {
-                // InH, correlation distance = 5;
-                corrDis = 5;
-            }
-            else
-            {
-                if (channelParams->m_o2iCondition == ChannelCondition::O2I) // outdoor to indoor
-                {
-                    corrDis = 5;
-                }
-                else // LOS or NLOS
-                {
-                    corrDis = 10;
-                }
-            }
+            const double corrDis = table3gpp->m_blockerDcorDistance;
             double R;
-            if (m_blockerSpeed > 1e-6) // speed not equal to 0
+            if (m_blockerSpeed > 1e-6) // speed is not equal to 0
             {
-                double corrT = corrDis / m_blockerSpeed;
+                const double corrT = corrDis / m_blockerSpeed;
                 R = exp(-1 * (deltaX / corrDis +
                               (Now().GetSeconds() - channelParams->m_generatedTime.GetSeconds()) /
                                   corrT));
@@ -3728,16 +4493,16 @@
 
             // The following formula was obtained from MATLAB numerical simulation.
 
-            if (R * R * (-0.069) + R * 1.074 - 0.002 <
+            if (R * R * -0.069 + R * 1.074 - 0.002 <
                 1) // transform only when the correlation of normal RV is smaller than 1
             {
-                R = R * R * (-0.069) + R * 1.074 - 0.002;
+                R = R * R * -0.069 + R * 1.074 - 0.002;
             }
             for (uint16_t blockInd = 0; blockInd < m_numNonSelfBlocking; blockInd++)
             {
                 // Generate a new correlated normal RV with the following formula
-                channelParams->m_nonSelfBlocking[blockInd][PHI_INDEX] =
-                    R * channelParams->m_nonSelfBlocking[blockInd][PHI_INDEX] +
+                (*nonSelfBlocking)[blockInd][PHI_INDEX] =
+                    R * (*nonSelfBlocking)[blockInd][PHI_INDEX] +
                     sqrt(1 - R * R) * m_normalRv->GetValue();
             }
         }
@@ -3751,28 +4516,26 @@
         NS_ASSERT_MSG(clusterZOA[cInd] >= 0 && clusterZOA[cInd] <= 180,
                       "the ZOA should be the range of [0,180]");
 
-        // check self blocking
+        // check self-blocking
         NS_LOG_INFO("AOA=" << clusterAOA[cInd] << " Block Region[" << phiSb - xSb / 2.0 << ","
                            << phiSb + xSb / 2.0 << "]");
         NS_LOG_INFO("ZOA=" << clusterZOA[cInd] << " Block Region[" << thetaSb - ySb / 2.0 << ","
                            << thetaSb + ySb / 2.0 << "]");
-        if (std::abs(clusterAOA[cInd] - phiSb) < (xSb / 2.0) &&
-            std::abs(clusterZOA[cInd] - thetaSb) < (ySb / 2.0))
+        if (std::abs(clusterAOA[cInd] - phiSb) < xSb / 2.0 &&
+            std::abs(clusterZOA[cInd] - thetaSb) < ySb / 2.0)
         {
-            powerAttenuation[cInd] += 30; // attenuate by 30 dB.
+            (*powerAttenuation)[cInd] += 30; // attenuate by 30 dB.
             NS_LOG_INFO("Cluster[" << +cInd
                                    << "] is blocked by self blocking region and reduce 30 dB power,"
                                       "the attenuation is ["
-                                   << powerAttenuation[cInd] << " dB]");
+                                   << (*powerAttenuation)[cInd] << " dB]");
         }
 
-        // check non-self blocking
+        // check non-self-blocking
         for (uint16_t blockInd = 0; blockInd < m_numNonSelfBlocking; blockInd++)
         {
-            // The normal RV is transformed to uniform RV with the desired correlation.
-            double phiK =
-                (0.5 * erfc(-1 * channelParams->m_nonSelfBlocking[blockInd][PHI_INDEX] / sqrt(2))) *
-                360;
+            // The normal RV is transformed to a uniform RV with the desired correlation.
+            double phiK = 0.5 * erfc(-1 * (*nonSelfBlocking)[blockInd][PHI_INDEX] / sqrt(2)) * 360;
             while (phiK > 360)
             {
                 phiK -= 360;
@@ -3783,22 +4546,21 @@
                 phiK += 360;
             }
 
-            double xK = channelParams->m_nonSelfBlocking[blockInd][X_INDEX];
-            double thetaK = channelParams->m_nonSelfBlocking[blockInd][THETA_INDEX];
-            double yK = channelParams->m_nonSelfBlocking[blockInd][Y_INDEX];
+            const double xK = (*nonSelfBlocking)[blockInd][X_INDEX];
+            const double thetaK = (*nonSelfBlocking)[blockInd][THETA_INDEX];
+            const double yK = (*nonSelfBlocking)[blockInd][Y_INDEX];
 
             NS_LOG_INFO("AOA=" << clusterAOA[cInd] << " Block Region[" << phiK - xK << ","
                                << phiK + xK << "]");
             NS_LOG_INFO("ZOA=" << clusterZOA[cInd] << " Block Region[" << thetaK - yK << ","
                                << thetaK + yK << "]");
 
-            if (std::abs(clusterAOA[cInd] - phiK) < (xK) &&
-                std::abs(clusterZOA[cInd] - thetaK) < (yK))
+            if (std::abs(clusterAOA[cInd] - phiK) < xK && std::abs(clusterZOA[cInd] - thetaK) < yK)
             {
-                double A1 = clusterAOA[cInd] - (phiK + xK / 2.0);   //(7.6-24)
-                double A2 = clusterAOA[cInd] - (phiK - xK / 2.0);   //(7.6-25)
-                double Z1 = clusterZOA[cInd] - (thetaK + yK / 2.0); //(7.6-26)
-                double Z2 = clusterZOA[cInd] - (thetaK - yK / 2.0); //(7.6-27)
+                const double A1 = clusterAOA[cInd] - (phiK + xK / 2.0);   //(7.6-24)
+                const double A2 = clusterAOA[cInd] - (phiK - xK / 2.0);   //(7.6-25)
+                const double Z1 = clusterZOA[cInd] - (thetaK + yK / 2.0); //(7.6-26)
+                const double Z2 = clusterZOA[cInd] - (thetaK - yK / 2.0); //(7.6-27)
                 int signA1;
                 int signA2;
                 int signZ1;
@@ -3839,35 +4601,30 @@
                 {
                     signZ2 = 1;
                 }
-                double lambda = 3e8 / m_frequency;
-                double fA1 =
-                    atan(signA1 * M_PI / 2.0 *
-                         sqrt(M_PI / lambda * channelParams->m_nonSelfBlocking[blockInd][R_INDEX] *
-                              (1.0 / cos(DegreesToRadians(A1)) - 1))) /
-                    M_PI; //(7.6-23)
-                double fA2 =
-                    atan(signA2 * M_PI / 2.0 *
-                         sqrt(M_PI / lambda * channelParams->m_nonSelfBlocking[blockInd][R_INDEX] *
-                              (1.0 / cos(DegreesToRadians(A2)) - 1))) /
-                    M_PI;
-                double fZ1 =
-                    atan(signZ1 * M_PI / 2.0 *
-                         sqrt(M_PI / lambda * channelParams->m_nonSelfBlocking[blockInd][R_INDEX] *
-                              (1.0 / cos(DegreesToRadians(Z1)) - 1))) /
-                    M_PI;
-                double fZ2 =
-                    atan(signZ2 * M_PI / 2.0 *
-                         sqrt(M_PI / lambda * channelParams->m_nonSelfBlocking[blockInd][R_INDEX] *
-                              (1.0 / cos(DegreesToRadians(Z2)) - 1))) /
-                    M_PI;
-                double lDb = -20 * log10(1 - (fA1 + fA2) * (fZ1 + fZ2)); //(7.6-22)
-                powerAttenuation[cInd] += lDb;
+                const double lambda = 3e8 / m_frequency;
+                const double fA1 = atan(signA1 * M_PI / 2.0 *
+                                        sqrt(M_PI / lambda * (*nonSelfBlocking)[blockInd][R_INDEX] *
+                                             (1.0 / cos(DegreesToRadians(A1)) - 1))) /
+                                   M_PI; //(7.6-23)
+                const double fA2 = atan(signA2 * M_PI / 2.0 *
+                                        sqrt(M_PI / lambda * (*nonSelfBlocking)[blockInd][R_INDEX] *
+                                             (1.0 / cos(DegreesToRadians(A2)) - 1))) /
+                                   M_PI;
+                const double fZ1 = atan(signZ1 * M_PI / 2.0 *
+                                        sqrt(M_PI / lambda * (*nonSelfBlocking)[blockInd][R_INDEX] *
+                                             (1.0 / cos(DegreesToRadians(Z1)) - 1))) /
+                                   M_PI;
+                const double fZ2 = atan(signZ2 * M_PI / 2.0 *
+                                        sqrt(M_PI / lambda * (*nonSelfBlocking)[blockInd][R_INDEX] *
+                                             (1.0 / cos(DegreesToRadians(Z2)) - 1))) /
+                                   M_PI;
+                const double lDb = -20 * log10(1 - (fA1 + fA2) * (fZ1 + fZ2)); //(7.6-22)
+                (*powerAttenuation)[cInd] += lDb;
                 NS_LOG_INFO("Cluster[" << +cInd << "] is blocked by no-self blocking, the loss is ["
                                        << lDb << "] dB");
             }
         }
     }
-    return powerAttenuation;
 }
 
 int64_t
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/model/three-gpp-channel-model.h ns-3.47/src/spectrum/model/three-gpp-channel-model.h
--- ns-3.46/src/spectrum/model/three-gpp-channel-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/model/three-gpp-channel-model.h	2026-02-16 16:08:49.824230700 -0800
@@ -3,6 +3,7 @@
  * New York University
  * Copyright (c) 2019 SIGNET Lab, Department of Information Engineering,
  * University of Padova
+ * Copyright (c) 2026, CTTC, Centre Tecnologic de Telecomunicacions de Catalunya
  *
  * SPDX-License-Identifier: GPL-2.0-only
  */
@@ -12,12 +13,8 @@
 
 #include "matrix-based-channel-model.h"
 
-#include "ns3/angles.h"
-#include "ns3/boolean.h"
 #include "ns3/channel-condition-model.h"
-#include "ns3/deprecated.h"
 
-#include <complex.h>
 #include <unordered_map>
 
 namespace ns3
@@ -26,13 +23,9 @@
 class MobilityModel;
 
 /**
- * @ingroup spectrum
- * @brief Channel Matrix Generation following 3GPP TR 38.901
- *
- * The class implements the channel matrix generation procedure
- * described in 3GPP TR 38.901.
- *
- * @see GetChannel
+ * ThreeGppChannelModel extends MatrixBasedChannelModel and represents a channel
+ * model based on 3GPP specifications, including functionality for
+ * managing channel conditions, scenarios, and frequency requirements.
  */
 class ThreeGppChannelModel : public MatrixBasedChannelModel
 {
@@ -92,7 +85,7 @@
     std::string GetScenario() const;
 
     /**
-     * Looks for the channel matrix associated to the aMob and bMob pair in m_channelMatrixMap.
+     * Looks for the channel matrix associated with the aMob and bMob pair in m_channelMatrixMap.
      * If found, it checks if it has to be updated. If not found or if it has to
      * be updated, it generates a new uncorrelated channel matrix using the
      * method GetNewChannel and updates m_channelMap.
@@ -109,8 +102,8 @@
                                         Ptr<const PhasedArrayModel> bAntenna) override;
 
     /**
-     * Looks for the channel params associated to the aMob and bMob pair in
-     * m_channelParamsMap. If not found it will return a nullptr.
+     * Looks for the channel params associated with the aMob and bMob pair in
+     * m_channelParamsMap. If not found, it will return a nullptr.
      *
      * @param aMob mobility model of the a device
      * @param bMob mobility model of the b device
@@ -127,13 +120,12 @@
      */
     int64_t AssignStreams(int64_t stream);
 
-  protected:
     /**
      * Wrap an (azimuth, inclination) angle pair in a valid range.
      * Specifically, inclination must be in [0, M_PI] and azimuth in [0, 2*M_PI).
-     * If the inclination angle is outside of its range, the azimuth angle is
+     * If the inclination angle is outside its range, the azimuth angle is
      * rotated by M_PI.
-     * This methods aims specifically at solving the problem of generating angles at
+     * This method aims specifically at solving the problem of generating angles at
      * the boundaries of the angle domain, specifically, generating angle distributions
      * close to inclinationRad=0 and inclinationRad=M_PI.
      *
@@ -147,71 +139,112 @@
      * Extends the struct ChannelParams by including information that is used
      * within the ThreeGppChannelModel class
      */
-    struct ThreeGppChannelParams : public MatrixBasedChannelModel::ChannelParams
+    struct ThreeGppChannelParams : ChannelParams
     {
-        ChannelCondition::LosConditionValue
-            m_losCondition; //!< contains the information about the LOS state of the channel
-        ChannelCondition::O2iConditionValue
-            m_o2iCondition; //!< contains the information about the O2I state of the channel
-        // TODO these are not currently used, they have to be correctly set when including the
-        // spatial consistent update procedure
-        /*The following parameters are stored for spatial consistent updating. The notation is
+        /// contains the information about the LOS state of the channel
+        ChannelCondition::LosConditionValue m_losCondition;
+        /// contains the information about the O2I state of the channel
+        ChannelCondition::O2iConditionValue m_o2iCondition;
+        /*The following parameters are stored for spatially consistent updating. The notation is
         that of 3GPP technical reports, but it can apply also to other channel realizations*/
-        MatrixBasedChannelModel::Double2DVector m_nonSelfBlocking; //!< store the blockages
-        Vector m_preLocUT; //!< location of UT when generating the previous channel
-        Vector m_locUT;    //!< location of UT
-        MatrixBasedChannelModel::Double2DVector
-            m_norRvAngles; //!< stores the normal variable for random angles angle[cluster][id]
-                           //!< generated for equation (7.6-11)-(7.6-14), where id =
-                           //!< 0(aoa),1(zoa),2(aod),3(zod)
-        double m_DS;       //!< delay spread
-        double m_K_factor; //!< K factor
-        uint8_t m_reducedClusterNumber; //!< reduced cluster number;
-        MatrixBasedChannelModel::Double2DVector
-            m_rayAodRadian; //!< the vector containing AOD angles
-        MatrixBasedChannelModel::Double2DVector
-            m_rayAoaRadian; //!< the vector containing AOA angles
-        MatrixBasedChannelModel::Double2DVector
-            m_rayZodRadian; //!< the vector containing ZOD angles
-        MatrixBasedChannelModel::Double2DVector
-            m_rayZoaRadian; //!< the vector containing ZOA angles
-        MatrixBasedChannelModel::Double3DVector m_clusterPhase; //!< the initial random phases
-        MatrixBasedChannelModel::Double2DVector
-            m_crossPolarizationPowerRatios; //!< cross polarization power ratios
-        Vector m_speed;                     //!< velocity
-        double m_dis2D;                     //!< 2D distance between tx and rx
-        double m_dis3D;                     //!< 3D distance between tx and rx
-        DoubleVector m_clusterPower;        //!< cluster powers
-        DoubleVector m_attenuation_dB;      //!< vector that stores the attenuation of the blockage
-        uint8_t m_cluster1st;               //!< index of the first strongest cluster
-        uint8_t m_cluster2nd;               //!< index of the second strongest cluster
+        /// store the blockages
+        Double2DVector m_nonSelfBlocking;
+        /** stores the normal variable for random angles angle[cluster][id]
+         * generated for equation (7.6-11)-(7.6-14), where id follows
+         * MatrixBasedChannelModel::{AOA,ZOA,AOD,ZOD}_INDEX ordering.
+         */
+        Double2DVector m_norRvAngles;
+        /// delay spread
+        double m_DS;
+        /// K factor
+        double m_K_factor;
+        /// reduced cluster number
+        uint8_t m_reducedClusterNumber;
+        /** The signs of the XN per cluster -1 or +1, per NLOS used in channel consistency procedure
+         */
+        std::vector<int> m_clusterXnNlosSign;
+        Double2DVector m_rayAodRadian;                 //!< the vector containing AOD angles
+        Double2DVector m_rayAoaRadian;                 //!< the vector containing AOA angles
+        Double2DVector m_rayZodRadian;                 //!< the vector containing ZOD angles
+        Double2DVector m_rayZoaRadian;                 //!< the vector containing ZOA angles
+        Double3DVector m_clusterPhase;                 //!< the initial random phases
+        Double2DVector m_crossPolarizationPowerRatios; //!< cross-polarization power ratios
+        double m_dis2D;                                //!< 2D distance between tx and rx
+        double m_dis3D;                                //!< 3D distance between tx and rx
+        DoubleVector m_clusterShadowing;               //!< cluster shadowing
+        DoubleVector m_clusterPower;                   //!< cluster powers
+        DoubleVector m_attenuation_dB;   //!< vector that stores the attenuation of the blockage
+        uint8_t m_cluster1st;            //!< index of the first strongest cluster
+        uint8_t m_cluster2nd;            //!< index of the second-strongest cluster
+        Vector m_txSpeed;                //!< TX velocity
+        Vector m_rxSpeed;                //!< RX velocity
+        DoubleVector m_delayConsistency; //!< cluster delay for consistency update
+        /**
+         * The effective 2D displacement used to determine whether a channel-consistency update
+         * can be applied, computed between consecutive endpoint positions.
+         *
+         * Since channel parameters are stored using a reciprocal key, endpoint-specific state is
+         * handled using a canonical ordering by node ID (min ID / max ID). The displacement is
+         * computed as the maximum of the two endpoint displacements since the last
+         * generation/update.
+         */
+        double m_endpointDisplacement2D{0.0};
+        /**
+         * 2D displacement of the canonical relative position vector between the two endpoints
+         * since the last generation/update.
+         *
+         * This corresponds to the change in the link geometry in the horizontal plane and is used
+         * by Procedure A updates that evolve cluster-level random variables as a function of
+         * displacement (e.g., per-cluster shadowing correlation).
+         */
+        double m_relativeDisplacement2D{0.0};
+        /**
+         * Position of the endpoint with the smallest node ID (canonical endpoint ordering) at the
+         * time of the last channel parameter generation/update.
+         */
+        Vector m_lastPositionFirst;
+        /**
+         * Position of the endpoint with the largest node ID (canonical endpoint ordering) at the
+         * time of the last channel parameter generation/update.
+         */
+        Vector m_lastPositionSecond;
+        /**
+         * Canonical 2D relative position vector (first -> second) stored at the time of the last
+         * channel parameter generation/update.
+         *
+         * This is used to compute m_relativeDisplacement2D as the 2D displacement of the link
+         * geometry between consecutive updates.
+         */
+        Vector2D m_lastRelativePosition2D;
     };
 
     /**
      * Data structure that stores the parameters of 3GPP TR 38.901, Table 7.5-6,
      * for a certain scenario
      */
-    struct ParamsTable : public SimpleRefCount<ParamsTable>
+    struct ParamsTable : SimpleRefCount<ParamsTable>
     {
         uint8_t m_numOfCluster = 0;   //!< Number of clusters
         uint8_t m_raysPerCluster = 0; //!< Number of rays per cluster
         double m_uLgDS = 0;           //!< Mean value of 10-base logarithm of delay spread
         double m_sigLgDS = 0; //!< Standard deviation value of 10-base logarithm of delay spread
-        double m_uLgASD =
-            0; //!< Mean value of 10-base logarithm of azimuth angle spread of departure
-        double m_sigLgASD =
-            0; //!< Standard deviation of 10-base logarithm of azimuth angle spread of departure
-        double m_uLgASA = 0; //!< Mean value of 10-base logarithm of azimuth angle spread of arrival
-        double m_sigLgASA =
-            0; //!< Standard deviation of 10-base logarithm of azimuth angle spread of arrival
-        double m_uLgZSA = 0; //!< Mean value of 10-base logarithm of zenith angle spread of arrival
-        double m_sigLgZSA =
-            0; //!< Standard deviation of 10-base logarithm of zenith angle spread of arrival
-        double m_uLgZSD =
-            0; //!< Mean value of 10-base logarithm of zenith angle spread of departure
-        double m_sigLgZSD =
-            0; //!< Standard deviation of 10-base logarithm of zenith angle spread of departure
-        double m_offsetZOD = 0;              //!< Offset of zenith angle of departure
+        //! Mean value of 10-base logarithm of azimuth angle spread of departure
+        double m_uLgASD = 0;
+        //! Standard deviation of 10-base logarithm of azimuth angle spread of departure
+        double m_sigLgASD = 0;
+        //! Mean value of 10-base logarithm of azimuth angle spread of arrival
+        double m_uLgASA = 0;
+        //! Standard deviation of 10-base logarithm of azimuth angle spread of arrival
+        double m_sigLgASA = 0;
+        //! Mean value of 10-base logarithm of zenith angle spread of arrival
+        double m_uLgZSA = 0;
+        //! Standard deviation of 10-base logarithm of zenith angle spread of arrival
+        double m_sigLgZSA = 0;
+        //! Mean value of 10-base logarithm of zenith angle spread of departure
+        double m_uLgZSD = 0;
+        //! Standard deviation of 10-base logarithm of zenith angle spread of departure
+        double m_sigLgZSD = 0;
+        double m_offsetZOD = 0;              //!< Offset of a zenith angle of departure
         double m_cDS = 0;                    //!< Cluster DS
         double m_cASD = 0;                   //!< Cluster ASD (Azimuth angle Spread of Departure)
         double m_cASA = 0;                   //!< Cluster ASA (Azimuth angle Spread of Arrival)
@@ -222,8 +255,22 @@
         double m_uXpr = 0;                   //!< Mean of Cross-Polarization Ratio
         double m_sigXpr = 0;                 //!< Standard deviation of Cross-Polarization Ratio
         double m_perClusterShadowingStd = 0; //!< Per cluster shadowing standard deviation
-        double m_sqrtC[7][7]; //!< The square root matrix and follows the order of [SF, K, DS, ASD,
-                              //!< ASA, ZSD, ZSA]
+        //! Correlation distance for spatial consistency (7.6.3.1-2) in meters.
+        //! Default value set to 1, because V2V and NTN models documentations do not provide this
+        //! parameter value
+        double m_perClusterRayDcorDistance = 1;
+        //! The spatial correlation distance for the random variable determining the centre of the
+        //! blocker in meters; Default value set to 1, because V2V and NTN models documentations do
+        //! not provide
+        // this parameter value
+        double m_blockerDcorDistance = 1;
+
+        /**
+         * For LOS, LSP is following the order of [SF,K,DS,ASD,ASA,ZSD,ZSA].
+         * For NLOS, LSP is following the order of [SF,DS,ASD,ASA,ZSD,ZSA].
+         * https://github.com/nyuwireless-unipd/ns3-mmwave/blob/master/src/mmwave/model/BeamFormingMatrix/SqrtMatrix.m
+         */
+        double m_sqrtC[7][7];
     };
 
     /**
@@ -234,21 +281,62 @@
      * @return the parameters table
      */
     virtual Ptr<const ParamsTable> GetThreeGppTable(
-        const Ptr<const MobilityModel> aMob,
-        const Ptr<const MobilityModel> bMob,
+        Ptr<const MobilityModel> aMob,
+        Ptr<const MobilityModel> bMob,
         Ptr<const ChannelCondition> channelCondition) const;
 
+  protected:
+    /**
+     * Update distance- and displacement-related values between two nodes.
+     *
+     * The function computes the current 2D and 3D distances between two mobility models.
+     *
+     * It also computes two 2D displacement measures with respect to previously stored state.
+     * Since channel parameters are stored using a reciprocal key, endpoint-specific state is
+     * handled using a canonical ordering by node ID (min ID / max ID).
+     * ``endpointDisplacement2D`` is the maximum 2D displacement of the two endpoints since the last
+     * channel parameter generation/update.
+     * ``relativeDisplacement2D`` is the 2D displacement of the canonical relative position vector
+     * between the endpoints since the last generation/update.
+     *
+     * @param aMob The mobility model of the first node (unordered).
+     * @param bMob The mobility model of the second node (unordered).
+     * @param distance2D Output parameter that stores the 2D distance between the two nodes.
+     * @param distance3D Output parameter that stores the 3D distance between the two nodes.
+     * @param endpointDisplacement2D Output parameter that stores the effective 2D displacement
+     *        (in meters).
+     * @param relativeDisplacement2D Output parameter that stores the 2D displacement (in meters)
+     *        of the canonical relative position vector between the two endpoints since the last
+     *        channel parameter generation/update.
+     * @param lastPositionFirst Position of the endpoint with the smallest node ID at the time of
+     *        the last channel parameter generation/update.
+     * @param lastPositionSecond Position of the endpoint with the largest node ID at the time of
+     *        the last channel parameter generation/update.
+     * @param lastRelativePosition2D Previously stored canonical 2D relative position vector
+     *        (first endpoint minus second endpoint) at the time of the last channel parameter
+     *        generation/update.
+     */
+    void UpdateLinkGeometry(Ptr<const MobilityModel> aMob,
+                            Ptr<const MobilityModel> bMob,
+                            double* distance2D,
+                            double* distance3D,
+                            double* endpointDisplacement2D,
+                            double* relativeDisplacement2D,
+                            Vector* lastPositionFirst,
+                            Vector* lastPositionSecond,
+                            Vector2D* lastRelativePosition2D) const;
+
     /**
      * Prepare 3gpp channel parameters among the nodes a and b.
      * The function does the following steps described in 3GPP 38.901:
      *
-     * Step 4: Generate large scale parameters. All LSPS are uncorrelated.
+     * Step 4: Generate large scale parameters. All LSPs are uncorrelated.
      * Step 5: Generate Delays.
      * Step 6: Generate cluster powers.
      * Step 7: Generate arrival and departure angles for both azimuth and elevation.
      * Step 8: Coupling of rays within a cluster for both azimuth and elevation
      * shuffle all the arrays to perform random coupling
-     * Step 9: Generate the cross polarization power ratios
+     * Step 9: Generate the cross-polarization power ratios
      * Step 10: Draw initial phases
      *
      * All relevant generated parameters are added then to ThreeGppChannelParams
@@ -261,10 +349,617 @@
      * according 38.901 steps from 4 to 10.
      */
     Ptr<ThreeGppChannelParams> GenerateChannelParameters(
-        const Ptr<const ChannelCondition> channelCondition,
-        const Ptr<const ParamsTable> table3gpp,
-        const Ptr<const MobilityModel> aMob,
-        const Ptr<const MobilityModel> bMob) const;
+        Ptr<const ChannelCondition> channelCondition,
+        Ptr<const ParamsTable> table3gpp,
+        Ptr<const MobilityModel> aMob,
+        Ptr<const MobilityModel> bMob) const;
+
+    /**
+     * @brief Large-scale channel parameters (3GPP TR 38.901).
+     *
+     * Contains the large-scale statistics used by the channel model.
+     * Shadow fading (SF) is handled in the propagation loss model, not here.
+     *
+     * @note Units:
+     *   - DS: delay spread [s]
+     *   - ASD/ASA/ZSD/ZSA: angular spreads [deg]
+     *   - kFactor: Rician K-factor [dB] (LOS only; 0 for NLOS usage)
+     */
+    struct LargeScaleParameters
+    {
+        /** Delay spread [s]. */
+        double DS{0};
+        /** Azimuth spread of departure [deg]. */
+        double ASD{0};
+        /** Azimuth spread of arrival [deg]. */
+        double ASA{0};
+        /** Zenith spread of arrival [deg]. */
+        double ZSA{0};
+        /** Zenith spread of departure [deg]. */
+        double ZSD{0};
+        /** Rician K-factor [dB] (used in LOS). */
+        double kFactor{0};
+    };
+
+    /**
+     * @brief Generate large-scale parameters (LSPs) for the current channel state.
+     *
+     * Draws correlated large-scale parameters according to the 3GPP table and the
+     * specified LOS/NLOS condition, including delay spread (DS), angular spreads
+     * (ASD/ASA/ZSD/ZSA), and K-factor for LOS.
+     *
+     * @param losCondition Line-of-sight condition (LOS or NLOS).
+     * @param table3gpp Pointer to the 3GPP parameters table (means, std-devs, sqrt correlation).
+     * @return LargeScaleParameters structure containing the generated LSPs.
+     */
+    LargeScaleParameters GenerateLSPs(const ChannelCondition::LosConditionValue losCondition,
+                                      Ptr<const ParamsTable> table3gpp) const;
+
+    /**
+     * Generate the cluster delays.
+     * @param DS Delay Spread
+     * @param table3gpp 3GPP parameters from the table
+     * @param minTau the minimum delay
+     * @param clusterDelays output vector for cluster delays
+     */
+
+    void GenerateClusterDelays(const double DS,
+                               Ptr<const ParamsTable> table3gpp,
+                               double* minTau,
+                               DoubleVector* clusterDelays) const;
+
+    /**
+     * @brief Generate per-cluster shadowing terms (in dB) as specified by 3GPP TR 38.901.
+     *
+     * Draws one large-scale shadowing realization per cluster using the scenario-specific
+     * per-cluster shadowing standard deviation from the provided 3GPP table. These terms
+     * represent the intrinsic random strength of each cluster and are later used when
+     * computing and updating cluster powers.
+     *
+     * Behavior:
+     * - The output vector is resized to the number of clusters defined by the table.
+     * - Each entry is an independent zero-mean Gaussian random variable with standard
+     *   deviation equal to table3gpp->m_perClusterShadowingStd (units: dB).
+     *
+     * @param table3gpp Pointer to the 3GPP parameters table (provides number of clusters
+     *                  and per-cluster shadowing std-dev).
+     * @param clusterShadowing Output vector filled with per-cluster shadowing values (dB);
+     *                         size equals the number of clusters.
+     */
+    void GenerateClusterShadowingTerm(Ptr<const ParamsTable> table3gpp,
+                                      DoubleVector* clusterShadowing) const;
+
+    /**
+     * Update shadowing per cluster by using the normalized auto correlation function R, which is
+     * defined as an exponential function in ITU-R P.1816 and used by 38.901 in 7.4-5 equation for
+     * the correlation of shadowing.
+     * @param table3gpp The 3GPP parameters
+     * @param clusterShadowing The previous values of the per-cluster shadowing.
+     * @param displacementLength The displacement length between the nodes.
+     */
+    void UpdateClusterShadowingTerm(Ptr<const ParamsTable> table3gpp,
+                                    DoubleVector* clusterShadowing,
+                                    const double displacementLength) const;
+
+    /**
+     * Generate cluster powers.
+     * @param clusterDelays cluster delays
+     * @param DS Delay Spread
+     * @param table3gpp 3GPP parameters from the table
+     * @param clusterShadowing the shadowing realization per each cluster, representing the
+     * intrinsic random strength of each cluster
+     * @param clusterPowers output vector for cluster powers
+     */
+    void GenerateClusterPowers(const DoubleVector& clusterDelays,
+                               const double DS,
+                               Ptr<const ParamsTable> table3gpp,
+                               const DoubleVector& clusterShadowing,
+                               DoubleVector* clusterPowers) const;
+
+    /**
+     * Remove the clusters with less power.
+     * @param clusterPowers input/output vector of cluster powers (will be modified)
+     * @param clusterDelays input/output vector of cluster delays (will be modified)
+     * @param losCondition LOS condition
+     * @param table3gpp 3GPP parameters from the table
+     * @param kFactor K factor
+     * @param powerMax output for maximum power
+     * @return Vector of per-cluster powers used for angle generation, after removing clusters
+     * more than 25 dB below the strongest cluster.
+     */
+    DoubleVector RemoveWeakClusters(DoubleVector* clusterPowers,
+                                    DoubleVector* clusterDelays,
+                                    const ChannelCondition::LosConditionValue losCondition,
+                                    Ptr<const ParamsTable> table3gpp,
+                                    const double kFactor,
+                                    double* powerMax) const;
+
+    /**
+     * @brief Adjusts cluster delays for LOS channel condition based on the K-factor.
+     *
+     * This function scales the initial NLOS cluster delays to reflect LOS conditions
+     * as described in 3GPP TR 38.901. The delays are normalized using a correction
+     * factor `cTau` that depends on the Rician K-factor, as defined in Table 7.5-3.
+     *
+     * The formula used to compute `cTau` is:
+     * \f[
+     * c_{\tau} = 0.7705 - 0.0433K + 0.0002K^2 + 0.000017K^3
+     * \f]
+     *
+     * Each cluster delay \f$\tau_i\f$ is then scaled by:
+     * \f[
+     * tau_i^{\text{LOS}} = \frac{\tau_i^{\text{NLOS}}}{c_{\tau}}
+     * \f]
+     * (see Equation 7.5-4)
+     *
+     * @param clusterDelays Pointer to a vector of initial (NLOS) cluster delays, which will be
+     * updated in place.
+     * @param reducedClusterNumber The number of clusters to be considered (can be < total
+     * clusters).
+     * @param kFactor The Rician K-factor, used to adjust the delay scaling factor `cTau`.
+     */
+    void AdjustClusterDelaysForLosCondition(DoubleVector* clusterDelays,
+                                            const uint8_t reducedClusterNumber,
+                                            const double kFactor) const;
+
+    /**
+     * @brief Calculates the cPhi parameter for azimuth angular spread modeling.
+     *
+     * This function computes the `cPhi` coefficient used to model the azimuth
+     * angular spread (ASA or ASD) for a given channel condition and cluster configuration.
+     *
+     * - In NLOS conditions, the value is taken directly from the `cNlosTablePhi`.
+     * - In LOS conditions, the NLOS value is adjusted using a polynomial function
+     *   of the Rician K-factor, as defined in Table 7.5-10 of 3GPP TR 38.901.
+     *
+     * The formula applied under LOS is:
+     * \f[
+     * c_{\phi,\text{LOS}} = c_{\phi,\text{NLOS}} \cdot (1.1035 - 0.028K - 0.002K^2 + 0.0001K^3)
+     * \f]
+     *
+     * @param losCondition Indicates whether the channel is LOS or NLOS.
+     * @param table3gpp Pointer to the table containing the number of clusters.
+     * @param kFactor The Rician K-factor, used to scale cPhi in LOS conditions.
+     *
+     * @return The computed cPhi value.
+     *
+     * @warning NS_FATAL_ERROR if the number of clusters is invalid or not found in the lookup
+     * table.
+     */
+    static double CalculateCphi(const ChannelCondition::LosConditionValue losCondition,
+                                Ptr<const ParamsTable> table3gpp,
+                                const double kFactor);
+
+    /**
+     * @brief Calculates the cTheta parameter for zenith angular spread modeling.
+     *
+     * This function computes the `cTheta` coefficient used to model the zenith
+     * angular spread (ZSA or ZSD) for a given channel condition and cluster configuration.
+     *
+     * - In NLOS conditions, the value is taken directly from the `cNlosTableTheta`.
+     * - In LOS conditions, the NLOS value is adjusted using a polynomial function
+     *   of the Rician K-factor, as defined in Table 7.5-15 of 3GPP TR 38.901.
+     *
+     * The formula applied under LOS is:
+     * \f[
+     * c_{\theta,\text{LOS}} = c_{\theta,\text{NLOS}} \cdot (1.3086 + 0.0339K - 0.0077K^2 +
+     * 0.0002K^3) \f]
+     *
+     * @param losCondition Indicates whether the channel is LOS or NLOS.
+     * @param table3gpp Pointer to the table containing the number of clusters.
+     * @param kFactor The Rician K-factor, used to scale cTheta in LOS conditions.
+     *
+     * @return The computed cTheta value.
+     *
+     * @warning NS_FATAL_ERROR if the number of clusters is invalid or not found in the lookup
+     * table.
+     */
+    static double CalculateCtheta(const ChannelCondition::LosConditionValue losCondition,
+                                  Ptr<const ParamsTable> table3gpp,
+                                  const double kFactor);
+
+    /**
+     * @brief Generate a random sign (+1 or -1) for each cluster for XN for NLOS computation for
+     * channel consistency updates.
+     *
+     * Fills the provided vector with one sign per cluster, where each sign is
+     * independently drawn from a fair Bernoulli distribution over {+1, -1}.
+     * The output vector is cleared and reserved to the specified size.
+     *
+     * @param clusterNumber Number of clusters to generate signs for.
+     * @param clusterSign Output pointer to the vector that will contain the signs
+     *        (size = clusterNumber), each element being either +1 or -1.
+     */
+    void GenerateClusterXnNLos(const uint8_t clusterNumber, std::vector<int>* clusterSign) const;
+
+    /**
+     * Generate cluster angles for a 3GPP channel model based on provided parameters.
+     * This method computes angles such as Angle of Arrival (AoA), Angle of Departure (AoD),
+     * Zenith Angle of Arrival (ZoA), and Zenith Angle of Departure (ZoD) for each cluster.
+     *
+     * @param channelParams Pointer to the structure containing channel parameters.
+     * @param clusterPowerForAngles A vector containing the power of clusters used for angle
+     * generation.
+     * @param powerMax The maximum power among clusters.
+     * @param cPhi Constant used for calculating azimuthal angles.
+     * @param cTheta Constant used for calculating zenith angles.
+     * @param lsps Reference to the structure holding large-scale parameters.
+     * @param aMob Pointer to the mobility model of Node A.
+     * @param bMob Pointer to the mobility model of Node B.
+     * @param table3gpp Pointer to the table containing 3GPP-specific parameters.
+     * @param clusterAngles Per-cluster angles [deg]; duplicated for sub-clusters.
+     */
+    void GenerateClusterAngles(Ptr<const ThreeGppChannelParams> channelParams,
+                               const DoubleVector& clusterPowerForAngles,
+                               double powerMax,
+                               double cPhi,
+                               double cTheta,
+                               const LargeScaleParameters& lsps,
+                               Ptr<const MobilityModel> aMob,
+                               Ptr<const MobilityModel> bMob,
+                               Ptr<const ParamsTable> table3gpp,
+                               Double2DVector* clusterAngles) const;
+
+    /**
+     * @brief Calculates the per-cluster power attenuation (in dB) due to self-blocking and
+     *        non-self-blocking effects, based on the 3GPP TR 38.901 blockage model
+     * (Section 7.6.4.1).
+     *
+     * This method applies the spatial blockage attenuation to each cluster of the channel model
+     * by determining whether the corresponding azimuth (AOA) and zenith (ZOA) angles fall
+     * into predefined self-blocking or randomly generated non-self-blocking regions. The resulting
+     * attenuation values are returned as a vector.
+     *
+     * - **Self-blocking** simulates the user’s own body blocking the signal. The region is defined
+     *   using fixed or portrait-mode-specific parameters (Table 7.6.4.1-1).
+     *
+     * - **Non-self-blocking** simulates environmental blockers (e.g., furniture, walls) and
+     *   is modeled using correlated random variables for blocker location and size, based on
+     *   scenario-specific rules from Table 7.6.4.1-2 and spatial correlation models
+     * (Tables 7.6.4.1-3, 7.6.4.1-4).
+     *
+     * @param [in,out] nonSelfBlocking
+     *      Pointer to a 2D vector representing the existing or to-be-generated non-self-blocking
+     *      regions. Each inner vector contains the parameters for a single blocker:
+     *      [phi_k, x_k, theta_k, y_k, r_k], as defined in 3GPP Table 7.6.4.1-2.
+     *
+     * @param [in,out] powerAttenuation A vector of attenuation values in decibels (dB),
+     *      with one value per cluster to be generated.
+     *
+     * @param [in] channelParams
+     *      Pointer to the channel parameters, including UT location and generation time, used
+     *      to calculate spatial correlation and motion updates for non-self-blocking regions.
+     *
+     * @param [in] clusterAOA
+     *      A vector of azimuth angles of arrival (AOA) in degrees for each cluster.
+     *      Must be in the range [0°, 360°].
+     *
+     * @param [in] clusterZOA
+     *      A vector of zenith angles of arrival (ZOA) in degrees for each cluster.
+     *      Must be in the range [0°, 180°].
+     *
+     * @param [in] table3gpp Table with 3GPP parameters
+     *
+     * @note
+     * - Follows the 3GPP TR 38.901 blockage model precisely, including the transformation
+     *   from normally-distributed to uniformly distributed random variables with correlated
+     * updates.
+     * - Uses scenario-specific correlation distances and blocker dimensions.
+     * - Attenuation for non-self-blocking is computed using the formulas (7.6-22) to (7.6-27).
+     * - A cluster can be affected by both self-blocking and non-self-blocking.
+     *
+     * @see 3GPP TR 38.901 v15.x.x or v16.x.x, Section 7.6.4.1
+     */
+    void CalcAttenuationOfBlockage(Double2DVector* nonSelfBlocking,
+                                   DoubleVector* powerAttenuation,
+                                   Ptr<const ThreeGppChannelParams> channelParams,
+                                   const DoubleVector& clusterAOA,
+                                   const DoubleVector& clusterZOA,
+                                   Ptr<const ParamsTable> table3gpp) const;
+
+    /**
+     * Updates the cluster delays for the 3GPP channel model based on the
+     * channel parameters and consistency requirements.
+     *
+     * @param clusterDelay Pointer to the vector that stores the current cluster delays to be
+     * updated.
+     * @param delayConsistency Pointer to the vector capturing the consistency of the delays.
+     * @param channelParams Smart pointer to a constant ThreeGppChannelParams object containing
+     *                      the necessary channel parameters for update computations.
+     */
+    void UpdateClusterDelay(DoubleVector* clusterDelay,
+                            DoubleVector* delayConsistency,
+                            Ptr<const ThreeGppChannelParams> channelParams) const;
+
+    /**
+     * @brief Rotate a 3D velocity vector by specified angles, around y-axis and z-axis.
+     * Rotation matrices being used are the ones defined in 38.901 in Equation (7.1-2).
+     * The formula being applied is the one from Procedure A, Rn,rx and Rn,ry from formula 7.6-10b
+     * and 7.6-10c:
+     *
+     *                                      [[ 1,  0,  0 ],
+     * R = Rz (alphaRad)  * Ry (betaRad) *  [ 0,  Xn, 0 ],  * Ry (gammaRad) * Rz (etaRad)
+     *                                      [ 0,  0,  1 ]]
+     *
+     * NOTE: Currently, only x and y components are calculated, the z component is not used.
+     *
+     * @param alphaRad the first angle from the rotation formula R [rad]
+     * @param betaRad the second angle from the rotation formula R [rad]
+     * @param gammaRad the third angle from the rotation formula R [rad]
+     * @param etaRad the fourth angle from the rotation formula R [rad]
+     * @param speed Input velocity vector in the original (unrotated) frame [m/s].
+     * @param Xn Direction sign indicator; use +1 to keep orientation, −1 to flip.
+     * @return The rotated (and possibly sign-flipped) velocity vector in the target frame [m/s].
+     */
+    Vector ApplyVelocityRotation(double alphaRad,
+                                 double betaRad,
+                                 double gammaRad,
+                                 double etaRad,
+                                 const Vector& speed,
+                                 const int Xn) const;
+
+    /**
+     * @brief Update cluster angles based on node mobility according to 3GPP TR 38.901.
+     *
+     * This function implements the spatial consistency procedure for updating cluster
+     * angles when nodes move, as specified in 3GPP TR 38.901 Section 7.6.3. The cluster
+     * angles are updated based on the relative velocity between transmitter and receiver
+     * nodes, considering both Line-of-Sight (LOS) and Non-Line-of-Sight (NLOS) conditions.
+     *
+     * The update equations are based on 3GPP TR 38.901 equations (7.6-11) to (7.6-14):
+     * - For LOS: Simple relative velocity calculation
+     * - For NLOS: Complex velocity transformation considering cluster-specific directions
+     *
+     * @param channelParams Pointer to the channel parameters containing node velocities,
+     *                      LOS condition, and reduced cluster number. Must contain valid
+     *                      m_rxSpeed, m_txSpeed, m_losCondition, and m_reducedClusterNumber.
+     * @param clusterAngles Cluster angles {AOA, ZOA, AOD, ZOD}
+     * @param prevClusterDelay Vector containing the previous cluster delays in seconds.
+     *                         Used in the angle update calculations. Size must equal
+     *                         channelParams->m_reducedClusterNumber.
+     */
+    void UpdateClusterAngles(Ptr<const ThreeGppChannelParams> channelParams,
+                             Double2DVector* clusterAngles,
+                             const DoubleVector& prevClusterDelay) const;
+
+    /**
+     * Shift per-ray angles to follow updated cluster mean angles while preserving
+     * intra-cluster offsets and existing random coupling.
+     * This is used during Spatial Consistency Procedure A updates when cluster means
+     * change, but ray coupling, XPR, and initial phases must remain the same.
+     *
+     * @param table3gpp Table of 3GPP parameters.
+     * @param channelParams Channel parameters to update (rays stored inside will be modified).
+     * @param prevClusterAngles Previous cluster angles {AOA, ZOA, AOD, ZOD}
+     * @param rayAoaRadian Ray AOA in radian to be updated.
+     * @param rayAodRadian Ray AOD in radian to be updated.
+     * @param rayZodRadian Ray ZOD in radian to be updated.
+     * @param rayZoaRadian Ray ZOA in radian to be updated.
+     */
+    void ShiftRayAnglesToUpdatedClusterMeans(Ptr<const ParamsTable> table3gpp,
+                                             Ptr<const ThreeGppChannelParams> channelParams,
+                                             const Double2DVector& prevClusterAngles,
+                                             Double2DVector* rayAodRadian,
+                                             Double2DVector* rayAoaRadian,
+                                             Double2DVector* rayZodRadian,
+                                             Double2DVector* rayZoaRadian) const;
+
+    /**
+     * @brief Applies blockage-based attenuation to the cluster powers according to
+     *        the 3GPP TR 38.901 blockage model (Section 7.6.4.1).
+     *
+     * This method attenuates the power of each multipath cluster based on whether
+     * it is blocked by self- or non-self-blocking regions. The attenuation is calculated
+     * using the `CalcAttenuationOfBlockage()` method and applied in dB to each cluster power.
+     *
+     * If the `m_blockage` flag is enabled, each cluster is checked for intersection
+     * with:
+     *   - **Self-blocking** regions, defined based on device orientation.
+     *   - **Non-self-blocking** regions, generated or updated using spatial correlation and
+     *     scenario-specific parameters.
+     *
+     * The attenuation is subtracted from the cluster powers using:
+     * \f[
+     * P' = \frac{P}{10^{A_{\text{dB}}/10}}
+     * \f]
+     * where \f$ A_{\text{dB}} \f$ is the blockage attenuation for the cluster.
+     *
+     *@param [in,out] nonSelfBlocking
+     *      Pointer to the 2D vector of non-self-blocking regions to be generated.
+     *      Each inner vector contains the parameters for a single blocker as defined in
+     *      3GPP Table 7.6.4.1-2.
+     *
+     * @param [in,out] clusterPowers
+     *      Pointer to the vector of cluster powers (linear scale), modified in-place
+     *      with the applied attenuation (one per cluster).
+     *
+     * @param [out] attenuation_dB
+     *      Pointer to the output vector containing the per-cluster attenuation values
+     *      in dB. This is filled using `CalcAttenuationOfBlockage()`.
+     *
+     * @param [in] channelParams
+     *      Pointer to the channel parameters (e.g., UT location, time of generation),
+     *      used for computing motion-based updates to non-self-blocking regions.
+     *
+     * @param [in] clusterAoa
+     *      Azimuth angles of arrival (AOA) for each cluster, in degrees.
+     *
+     * @param [in] clusterZoa
+     *      Zenith angles of arrival (ZOA) for each cluster, in degrees.
+     *
+     * @param [in] table3gpp
+     *      Table with 3GPP scenario dependent parameters
+     *
+     * @note
+     * - If `m_blockage` is false, no attenuation is applied, and `attenuation_dB` contains a single
+     * 0.
+     * - This function assumes the vectors are pre-sized and match `reducedClusterNumber`.
+     * - Internally uses `CalcAttenuationOfBlockage()` to compute spatial intersection with
+     * blockers.
+     *
+     * @see ThreeGppChannelModel::CalcAttenuationOfBlockage
+     * @see 3GPP TR 38.901 v15.x.x or v16.x.x, Section 7.6.4.1
+     */
+    void ApplyAttenuationToClusterPowers(Double2DVector* nonSelfBlocking,
+                                         DoubleVector* clusterPowers,
+                                         DoubleVector* attenuation_dB,
+                                         Ptr<const ThreeGppChannelParams> channelParams,
+                                         const DoubleVector& clusterAoa,
+                                         const DoubleVector& clusterZoa,
+                                         Ptr<const ParamsTable> table3gpp) const;
+
+    /**
+     * @brief Compute per-ray angles (no shuffling), centered on cluster means.
+     *
+     * Initializes and fills per-ray AOA/AOD/ZOA/ZOD angles in radians for each
+     * cluster according to 3GPP intra-cluster offsets. Angles are wrapped to valid
+     * ranges via WrapAngles.
+     *
+     * @param channelParams Channel parameters (provides reduced cluster count).
+     * @param table3gpp 3GPP parameters table (provides rays per cluster and spread constants).
+     * @param[out] rayAoaRadian Per-ray azimuth-of-arrival angles [rad] as
+     * [numClusters][raysPerCluster].
+     * @param[out] rayAodRadian Per-ray azimuth-of-departure angles [rad] as
+     * [numClusters][raysPerCluster].
+     * @param[out] rayZoaRadian Per-ray zenith-of-arrival angles [rad] as
+     * [numClusters][raysPerCluster].
+     * @param[out] rayZodRadian Per-ray zenith-of-departure angles [rad] as
+     * [numClusters][raysPerCluster].
+     */
+    void ComputeRayAngles(Ptr<const ThreeGppChannelParams> channelParams,
+                          Ptr<const ParamsTable> table3gpp,
+                          Double2DVector* rayAoaRadian,
+                          Double2DVector* rayAodRadian,
+                          Double2DVector* rayZoaRadian,
+                          Double2DVector* rayZodRadian) const;
+
+    /**
+     * @brief Randomly couples rays within each cluster by shuffling per-ray angles.
+     *
+     * Applies in-place shuffling of per-ray AOA/AOD/ZOA/ZOD arrays independently
+     * for each cluster, realizing random intra-cluster ray coupling.
+     *
+     * @param channelParams Channel parameters (provides reduced cluster count).
+     * @param[in,out] rayAoaRadian Per-ray AOA angles [rad] as [numClusters][raysPerCluster].
+     * @param[in,out] rayAodRadian Per-ray AOD angles [rad] as [numClusters][raysPerCluster].
+     * @param[in,out] rayZoaRadian Per-ray ZOA angles [rad] as [numClusters][raysPerCluster].
+     * @param[in,out] rayZodRadian Per-ray ZOD angles [rad] as [numClusters][raysPerCluster].
+     */
+    void RandomRaysCoupling(Ptr<const ThreeGppChannelParams> channelParams,
+                            Double2DVector* rayAoaRadian,
+                            Double2DVector* rayAodRadian,
+                            Double2DVector* rayZoaRadian,
+                            Double2DVector* rayZodRadian) const;
+    /**
+     * @brief Generate cross-polarization power ratios and initial per-ray phases.
+     *
+     * For each cluster and ray, draws the cross-polarization power ratio (XPR) according
+     * to the 3GPP parameters and initializes four polarization-dependent phases
+     * uniformly in [-pi, pi]. The outputs are resized/filled by this function.
+     *
+     * @param[out] crossPolarizationPowerRatios Matrix [numClusters][raysPerCluster] with
+     * XPR values (linear scale).
+     * @param[out] clusterPhase 3D array [numClusters][raysPerCluster][4] with initial
+     * phases (radians).
+     * @param reducedClusterNumber Number of (possibly reduced) clusters to generate.
+     * @param table3gpp Pointer to the 3GPP parameters table (uXpr, sigXpr, rays per
+     * cluster).
+     */
+    void GenerateCrossPolPowerRatiosAndInitialPhases(Double2DVector* crossPolarizationPowerRatios,
+                                                     Double3DVector* clusterPhase,
+                                                     const uint8_t reducedClusterNumber,
+                                                     Ptr<const ParamsTable> table3gpp) const;
+    /**
+     * @brief Identify the two strongest base clusters and append their derived subclusters.
+     *
+     * Finds the indices of the strongest and second-strongest clusters (by power) within the
+     * base (reduced) cluster set, and appends the corresponding subclusters by extending the
+     * per-cluster delay/angle vectors according to the 3GPP strongest-cluster subclustering rule.
+     * The associated Doppler terms (alpha and dTerm) are appended in the same order,
+     * inheriting the parent cluster values for each appended subcluster.
+     *
+     * @param channelParams Current channel parameters (cluster powers and reduced cluster count).
+     * @param table3gpp 3GPP parameters table (e.g., cDS).
+     * @param[out] cluster1st Index of the strongest base cluster.
+     * @param[out] cluster2nd Index of the second-strongest base cluster.
+     * @param[in,out] clusterDelay Per-cluster delays [s]; subcluster delays are appended.
+     * @param[in,out] angles Per-cluster angles [deg]; subcluster angles are appended/duplicated.
+     * @param[in,out] alpha Per-cluster Doppler alpha terms; subcluster terms are appended.
+     * @param[in,out] dTerm Per-cluster Doppler D terms; subcluster terms are appended.
+     * @param[in,out] clusterPower Per-cluster power; subcluster powers are appended.
+     */
+    void FindStrongestClusters(Ptr<const ThreeGppChannelParams> channelParams,
+                               Ptr<const ParamsTable> table3gpp,
+                               uint8_t* cluster1st,
+                               uint8_t* cluster2nd,
+                               DoubleVector* clusterDelay,
+                               Double2DVector* angles,
+                               DoubleVector* alpha,
+                               DoubleVector* dTerm,
+                               DoubleVector* clusterPower) const;
+
+    /**
+     * @brief Trim per-cluster vectors back to the base (reduced) cluster set.
+     *
+     * After channel generation/update, two or four derived subclusters may be appended
+     * at the end of the per-cluster vectors (to represent the additional strongest-cluster
+     * subclusters). This helper removes any such appended tail so that the vectors
+     * contain exactly @p reducedClusterNumber base clusters.
+     *
+     * @param reducedClusterNumber Number of base clusters (after weak-cluster removal).
+     * @param delay Per-cluster delays (tail subclusters, if present, are removed).
+     * @param angles Per-cluster angles (AOA/ZOA/AOD/ZOD; tail subclusters, if present, are
+     * removed).
+     * @param cachedAngleSincos Optional cached sin/cos pairs per cluster angle (tail
+     * subclusters, if present, are removed).
+     * @param alpha Optional Doppler alpha terms per cluster (tail subclusters, if present, are
+     * removed).
+     * @param dTerm Optional Doppler D terms per cluster (tail subclusters, if present, are
+     * removed).
+     * @param clusterPower Per cluster power (tail subclusters, if present, are removed).
+     */
+    void TrimToBaseClusters(const uint8_t reducedClusterNumber,
+                            DoubleVector* delay,
+                            Double2DVector* angles,
+                            std::vector<std::vector<std::pair<double, double>>>* cachedAngleSincos,
+                            DoubleVector* alpha,
+                            DoubleVector* dTerm,
+                            DoubleVector* clusterPower) const;
+
+    /**
+     * @brief Precomputes the sine and cosine values for angles.
+     *
+     * This method precomputes the sine and cosine values of specified angles,
+     * storing them in a vector of pairs. The computation is based on provided
+     * channel parameters.
+     *
+     * @param channelParams Pointer to the constant ThreeGppChannelParams object containing
+     * channel-specific parameters.
+     * @param cachedAngleSinCos Pointer to the vector where precomputed sine and cosine values will
+     * be stored.
+     */
+    void PrecomputeAnglesSinCos(
+        Ptr<const ThreeGppChannelParams> channelParams,
+        std::vector<std::vector<std::pair<double, double>>>* cachedAngleSinCos) const;
+
+    /**
+     * @brief Generate additional Doppler terms for delayed (reflected) paths.
+     *
+     * Produces per-cluster Doppler parameters used to model moving scatterers
+     * as per TR 37.885. v15.3.0, Sec. 6.2.3. These terms account for an additional
+     * Doppler contribution due to the presence of moving objects in the surrounding
+     * environment, such as in vehicular scenarios. This contribution is applied
+     * only to the delayed (reflected) paths and must be properly configured by
+     * setting the value of m_vScatt, which is defined as "maximum speed of the
+     * vehicle in the layout".
+     *
+     * @param reducedClusterNumber Number of (possibly reduced) clusters.
+     * @param[out] dopplerTermAlpha Per-cluster alpha terms in [-1, 1].
+     * @param[out] dopplerTermD Per-cluster Doppler speed terms in [-vScatt, vScatt] [m/s].
+     */
+    void GenerateDopplerTerms(const uint8_t reducedClusterNumber,
+                              DoubleVector* dopplerTermAlpha,
+                              DoubleVector* dopplerTermD) const;
 
     /**
      * Compute the channel matrix between two nodes a and b, and their
@@ -279,43 +974,107 @@
      * @param uAntenna the antenna array of node u
      * @return the channel realization
      */
-
     virtual Ptr<ChannelMatrix> GetNewChannel(Ptr<const ThreeGppChannelParams> channelParams,
                                              Ptr<const ParamsTable> table3gpp,
-                                             const Ptr<const MobilityModel> sMob,
-                                             const Ptr<const MobilityModel> uMob,
+                                             Ptr<const MobilityModel> sMob,
+                                             Ptr<const MobilityModel> uMob,
                                              Ptr<const PhasedArrayModel> sAntenna,
                                              Ptr<const PhasedArrayModel> uAntenna) const;
+
+    /**
+     * @brief Update channel parameters for spatial consistency using 3GPP TR 38.901 procedure A.
+     *
+     * This function updates existing channel parameters to maintain spatial and temporal
+     * consistency when nodes move. It reuses most of the functions from GenerateChannelParameters
+     * but applies spatial correlation to LSPs and temporal evolution to phases.
+     *
+     * Following 3GPP TR 38.901 Procedure A, the function:
+     * - Updates Large Scale Parameters (LSPs)
+     * - Regenerates cluster delays and powers
+     * - Filters weak clusters
+     * - Updates angles and phases with continuity from previous channel state
+     * - Maintains Doppler terms and cross-polarization ratios for consistency
+     * - Preserves phase evolution according to 3GPP specifications
+     *
+     * @param channelParams existing channel parameters to update
+     * @param channelCondition current channel condition
+     * @param aMob mobility model of device A
+     * @param bMob mobility model of device B
+     */
+    void UpdateChannelParameters(Ptr<ThreeGppChannelParams> channelParams,
+                                 Ptr<const ChannelCondition> channelCondition,
+                                 Ptr<const MobilityModel> aMob,
+                                 Ptr<const MobilityModel> bMob) const;
+
     /**
-     * Applies the blockage model A described in 3GPP TR 38.901
-     * @param channelParams the channel parameters structure
-     * @param clusterAOA vector containing the azimuth angle of arrival for each cluster
-     * @param clusterZOA vector containing the zenith angle of arrival for each cluster
-     * @return vector containing the power attenuation for each cluster
-     */
-    DoubleVector CalcAttenuationOfBlockage(
-        const Ptr<ThreeGppChannelModel::ThreeGppChannelParams> channelParams,
-        const DoubleVector& clusterAOA,
-        const DoubleVector& clusterZOA) const;
+     * Compute the effective 2D displacement of a link with respect to previously stored endpoint
+     * positions.
+     *
+     * The channel parameters are stored using a reciprocal key, therefore endpoint-specific state
+     * is handled using a canonical ordering by node ID (min ID / max ID). The returned endpoint
+     * displacement is the maximum of the two endpoint displacements, so that the channel can evolve
+     * even when the change in inter-node distance between consecutive updates is close to zero
+     * (e.g., parallel motion in V2V).
+     *
+     * @param aMob The mobility model of the first node (unordered).
+     * @param bMob The mobility model of the second node (unordered).
+     * @param lastPositionFirst Previously stored position of the endpoint with the smallest node
+     * ID.
+     * @param lastPositionSecond Previously stored position of the endpoint with the largest node
+     * ID.
+     * @return The effective 2D displacement (in meters).
+     */
+    double ComputeEndpointDisplacement2d(Ptr<const MobilityModel> aMob,
+                                         Ptr<const MobilityModel> bMob,
+                                         const Vector& lastPositionFirst,
+                                         const Vector& lastPositionSecond) const;
 
     /**
      * Check if the channel params has to be updated
-     * @param channelParams channel params
-     * @param channelCondition the channel condition
+     * @param channelParamsKey the channel params key
+     * @param condition the pointer to the channel condition instance
+     * @param aMob mobility model of the device a
+     * @param bMob mobility model of the device b
      * @return true if the channel params has to be updated, false otherwise
      */
-    bool ChannelParamsNeedsUpdate(Ptr<const ThreeGppChannelParams> channelParams,
-                                  Ptr<const ChannelCondition> channelCondition) const;
+    bool NewChannelParamsNeeded(const uint64_t channelParamsKey,
+                                Ptr<const ChannelCondition> condition,
+                                Ptr<const MobilityModel> aMob,
+                                Ptr<const MobilityModel> bMob) const;
 
     /**
-     * Check if the channel matrix has to be updated (it needs update when the channel params
-     * generation time is more recent than channel matrix generation time
-     * @param channelParams channel params structure
-     * @param channelMatrix channel matrix structure
-     * @return true if the channel matrix has to be updated, false otherwise
-     */
-    bool ChannelMatrixNeedsUpdate(Ptr<const ThreeGppChannelParams> channelParams,
-                                  Ptr<const ChannelMatrix> channelMatrix);
+     * @brief Check if channel matrix needs update based on parameter changes or
+     * the update period expired.
+     *
+     * This method determines whether a new channel matrix should be generated by comparing
+     * the generation times of the current channel parameters and existing channel matrix.
+     * It also considers any potential changes in antenna configurations.
+     *
+     * @param channelMatrixKey Channel matrix key
+     * @param channelParams Current channel parameters to check against
+     * @param aAntenna Transmitting antenna model configuration
+     * @param bAntenna Receiving antenna model configuration
+     * @return whether the channel matrix needs an update
+     */
+    bool NewChannelMatrixNeeded(uint64_t channelMatrixKey,
+                                Ptr<const ThreeGppChannelParams> channelParams,
+                                Ptr<const PhasedArrayModel> aAntenna,
+                                Ptr<const PhasedArrayModel> bAntenna) const;
+
+    /**
+     * @brief Check if spatial-consistency requires updating channel parameters.
+     *
+     * Returns true when spatial consistency is enabled, and the update period
+     * (UpdatePeriod) has elapsed since the parameters were generated.
+     *
+     * @param channelParams Channel parameters whose generation time is checked.
+     * @param aMob mobility model of the device a
+     * @param bMob mobility model of the device b
+     * @return true if a parameter update is needed; false otherwise.
+     */
+    bool ChannelUpdateNeeded(Ptr<const ThreeGppChannelParams> channelParams,
+                             Ptr<const MobilityModel> aMob,
+                             Ptr<const MobilityModel> bMob) const;
 
     /**
      * Check if the channel matrix has to be updated due to
@@ -327,44 +1086,63 @@
      */
     bool AntennaSetupChanged(Ptr<const PhasedArrayModel> aAntenna,
                              Ptr<const PhasedArrayModel> bAntenna,
-                             Ptr<const ChannelMatrix> channelMatrix);
-
-    std::unordered_map<uint64_t, Ptr<ChannelMatrix>>
-        m_channelMatrixMap; //!< map containing the channel realizations per pair of
-                            //!< PhasedAntennaArray instances, the key of this map is reciprocal
-                            //!< uniquely identifies a pair of PhasedAntennaArrays
-    std::unordered_map<uint64_t, Ptr<ThreeGppChannelParams>>
-        m_channelParamsMap; //!< map containing the common channel parameters per pair of nodes, the
-                            //!< key of this map is reciprocal and uniquely identifies a pair of
-                            //!< nodes
-    Time m_updatePeriod;    //!< the channel update period
-    double m_frequency;     //!< the operating frequency
-    std::string m_scenario; //!< the 3GPP scenario
-    Ptr<ChannelConditionModel> m_channelConditionModel; //!< the channel condition model
-    Ptr<UniformRandomVariable> m_uniformRv;             //!< uniform random variable
-    Ptr<NormalRandomVariable> m_normalRv;               //!< normal random variable
-    Ptr<UniformRandomVariable>
-        m_uniformRvShuffle; //!< uniform random variable used to shuffle array in GetNewChannel
-
-    // Variable used to compute the additional Doppler contribution for the delayed
-    // (reflected) paths, as described in 3GPP TR 37.885 v15.3.0, Sec. 6.2.3.
-    double m_vScatt; //!< value used to compute the additional Doppler contribution for the delayed
-                     //!< paths
-    Ptr<UniformRandomVariable> m_uniformRvDoppler; //!< uniform random variable, used to compute the
-                                                   //!< additional Doppler contribution
+                             Ptr<const ChannelMatrix> channelMatrix) const;
 
+    /**
+     * map containing the channel realizations per a pair of
+     * PhasedAntennaArray instances; the key of this map is reciprocal
+     * uniquely identifies a pair of PhasedAntennaArrays
+     */
+    std::unordered_map<uint64_t, Ptr<ChannelMatrix>> m_channelMatrixMap;
+    /**
+     * map containing the common channel parameters per a pair of nodes, the
+     * key of this map is reciprocal and uniquely identifies a pair of
+     * nodes
+     */
+    std::unordered_map<uint64_t, Ptr<ThreeGppChannelParams>> m_channelParamsMap;
+    /// the channel update period enables spatial consistency, procedure A
+    Time m_updatePeriod;
+    /// the operating frequency
+    double m_frequency;
+    /// the 3GPP scenario
+    std::string m_scenario;
+    /// the channel condition model
+    Ptr<ChannelConditionModel> m_channelConditionModel;
+    /// uniform random variable
+    Ptr<UniformRandomVariable> m_uniformRv;
+    /// normal random variable
+    Ptr<NormalRandomVariable> m_normalRv;
+    /// uniform random variable used to shuffle an array in GetNewChannel
+    Ptr<UniformRandomVariable> m_uniformRvShuffle;
+    /**
+     * Variable used to compute the additional Doppler contribution for the delayed
+     * (reflected) paths, as described in 3GPP TR 37.885 v15.3.0, Sec. 6.2.3.
+     */
+    double m_vScatt;
+    /**
+     * Uniform random variable, used to compute the additional Doppler contribution
+     */
+    Ptr<UniformRandomVariable> m_uniformRvDoppler;
     // parameters for the blockage model
-    bool m_blockage;               //!< enables the blockage model A
-    uint16_t m_numNonSelfBlocking; //!< number of non-self-blocking regions
-    bool m_portraitMode;           //!< true if portrait mode, false if landscape
-    double m_blockerSpeed;         //!< the blocker speed
-
-    static const uint8_t PHI_INDEX = 0; //!< index of the PHI value in the m_nonSelfBlocking array
-    static const uint8_t X_INDEX = 1;   //!< index of the X value in the m_nonSelfBlocking array
-    static const uint8_t THETA_INDEX =
-        2;                            //!< index of the THETA value in the m_nonSelfBlocking array
-    static const uint8_t Y_INDEX = 3; //!< index of the Y value in the m_nonSelfBlocking array
-    static const uint8_t R_INDEX = 4; //!< index of the R value in the m_nonSelfBlocking array
+    /// enables the blockage model A
+    bool m_blockage;
+    /// number of non-self-blocking regions
+    uint16_t m_numNonSelfBlocking;
+    /// true if portrait mode, false if landscape
+    bool m_portraitMode;
+    /// the blocker speed
+    double m_blockerSpeed;
+
+    /// index of the PHI value in the m_nonSelfBlocking array
+    static constexpr uint8_t PHI_INDEX = 0;
+    /// index of the X value in the m_nonSelfBlocking array
+    static constexpr uint8_t X_INDEX = 1;
+    /// index of the THETA value in the m_nonSelfBlocking array
+    static constexpr uint8_t THETA_INDEX = 2;
+    /// index of the Y value in the m_nonSelfBlocking array
+    static constexpr uint8_t Y_INDEX = 3;
+    /// index of the R value in the m_nonSelfBlocking array
+    static constexpr uint8_t R_INDEX = 4;
 };
 } // namespace ns3
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.cc ns-3.47/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.cc
--- ns-3.46/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.cc	2026-02-16 16:08:49.825230800 -0800
@@ -15,14 +15,11 @@
 
 #include "ns3/double.h"
 #include "ns3/log.h"
-#include "ns3/net-device.h"
 #include "ns3/node.h"
 #include "ns3/pointer.h"
 #include "ns3/simulator.h"
 #include "ns3/string.h"
 
-#include <map>
-
 namespace ns3
 {
 
@@ -80,6 +77,7 @@
 double
 ThreeGppSpectrumPropagationLossModel::GetFrequency() const
 {
+    NS_ASSERT_MSG(m_channelModel != nullptr, "Channel model is not set");
     DoubleValue freq;
     m_channelModel->GetAttribute("Frequency", freq);
     return freq.Get();
@@ -89,6 +87,7 @@
 ThreeGppSpectrumPropagationLossModel::SetChannelModelAttribute(const std::string& name,
                                                                const AttributeValue& value)
 {
+    NS_ASSERT_MSG(m_channelModel != nullptr, "Channel model is not set");
     m_channelModel->SetAttribute(name, value);
 }
 
@@ -96,29 +95,30 @@
 ThreeGppSpectrumPropagationLossModel::GetChannelModelAttribute(const std::string& name,
                                                                AttributeValue& value) const
 {
+    NS_ASSERT_MSG(m_channelModel != nullptr, "Channel model is not set");
     m_channelModel->GetAttribute(name, value);
 }
 
 Ptr<const MatrixBasedChannelModel::Complex3DVector>
 ThreeGppSpectrumPropagationLossModel::CalcLongTerm(
-    Ptr<const MatrixBasedChannelModel::ChannelMatrix> params,
+    Ptr<const MatrixBasedChannelModel::ChannelMatrix> channelMatrix,
     Ptr<const PhasedArrayModel> sAnt,
     Ptr<const PhasedArrayModel> uAnt) const
 {
     NS_LOG_FUNCTION(this);
 
+    NS_ASSERT_MSG(sAnt != nullptr && uAnt != nullptr, "Improper call to the method");
     const PhasedArrayModel::ComplexVector& sW = sAnt->GetBeamformingVectorRef();
     const PhasedArrayModel::ComplexVector& uW = uAnt->GetBeamformingVectorRef();
-    size_t sAntNumElems = sW.GetSize();
-    size_t uAntNumElems = uW.GetSize();
-    NS_ASSERT(uAntNumElems == params->m_channel.GetNumRows());
-    NS_ASSERT(sAntNumElems == params->m_channel.GetNumCols());
+    const size_t sAntNumElems = sW.GetSize();
+    const size_t uAntNumElems = uW.GetSize();
+    NS_ASSERT(uAntNumElems == channelMatrix->m_channel.GetNumRows());
+    NS_ASSERT(sAntNumElems == channelMatrix->m_channel.GetNumCols());
     NS_LOG_DEBUG("CalcLongTerm with " << uW.GetSize() << " u antenna elements and " << sW.GetSize()
                                       << " s antenna elements, and with "
                                       << " s ports: " << sAnt->GetNumPorts()
                                       << " u ports: " << uAnt->GetNumPorts());
-    NS_ASSERT_MSG((sAnt != nullptr) && (uAnt != nullptr), "Improper call to the method");
-    size_t numClusters = params->m_channel.GetNumPages();
+    size_t numClusters = channelMatrix->m_channel.GetNumPages();
     // create and initialize the size of the longTerm 3D matrix
     Ptr<MatrixBasedChannelModel::Complex3DVector> longTerm =
         Create<MatrixBasedChannelModel::Complex3DVector>(uAnt->GetNumPorts(),
@@ -133,7 +133,12 @@
             for (size_t cIndex = 0; cIndex < numClusters; cIndex++)
             {
                 longTerm->Elem(uPortIdx, sPortIdx, cIndex) =
-                    CalculateLongTermComponent(params, sAnt, uAnt, sPortIdx, uPortIdx, cIndex);
+                    CalculateLongTermComponent(channelMatrix,
+                                               sAnt,
+                                               uAnt,
+                                               sPortIdx,
+                                               uPortIdx,
+                                               cIndex);
             }
         }
     }
@@ -145,17 +150,17 @@
     Ptr<const MatrixBasedChannelModel::ChannelMatrix> params,
     Ptr<const PhasedArrayModel> sAnt,
     Ptr<const PhasedArrayModel> uAnt,
-    uint16_t sPortIdx,
-    uint16_t uPortIdx,
-    uint16_t cIndex) const
+    const uint16_t sPortIdx,
+    const uint16_t uPortIdx,
+    const uint16_t cIndex) const
 {
     NS_LOG_FUNCTION(this);
     const PhasedArrayModel::ComplexVector& sW = sAnt->GetBeamformingVectorRef();
     const PhasedArrayModel::ComplexVector& uW = uAnt->GetBeamformingVectorRef();
-    auto sPortElems = sAnt->GetNumElemsPerPort();
-    auto uPortElems = uAnt->GetNumElemsPerPort();
-    auto startS = sAnt->ArrayIndexFromPortIndex(sPortIdx, 0);
-    auto startU = uAnt->ArrayIndexFromPortIndex(uPortIdx, 0);
+    const auto sPortElems = sAnt->GetNumElemsPerPort();
+    const auto uPortElems = uAnt->GetNumElemsPerPort();
+    const auto startS = sAnt->ArrayIndexFromPortIndex(sPortIdx, 0);
+    const auto startU = uAnt->ArrayIndexFromPortIndex(uPortIdx, 0);
     std::complex<double> txSum(0, 0);
     // limiting multiplication operations to the port location
     auto sIndex = startS;
@@ -171,22 +176,20 @@
         auto uIndex = startU;
         for (size_t rIndex = 0; rIndex < uPortElems; rIndex++, uIndex++)
         {
-            rxSum += uW[uIndex - startU] * params->m_channel(uIndex, sIndex, cIndex);
-            auto testV = (rIndex % uElemsPerPort);
-            auto ptInc = uElemsPerPort - 1;
-            if (testV == ptInc)
+            rxSum += std::conj(uW[uIndex - startU]) * params->m_channel(uIndex, sIndex, cIndex);
+            const auto testV = rIndex % uElemsPerPort;
+            if (const auto ptInc = uElemsPerPort - 1; testV == ptInc)
             {
-                auto incVal = uAnt->GetNumColumns() - uElemsPerPort;
+                const auto incVal = uAnt->GetNumColumns() - uElemsPerPort;
                 uIndex += incVal; // Increment by a factor to reach next column in a port
             }
         }
 
         txSum += sW[sIndex - startS] * rxSum;
-        auto testV = (tIndex % sElemsPerPort);
-        auto ptInc = sElemsPerPort - 1;
-        if (testV == ptInc)
+        const auto testV = tIndex % sElemsPerPort;
+        if (const auto ptInc = sElemsPerPort - 1; testV == ptInc)
         {
-            size_t incVal = sAnt->GetNumColumns() - sElemsPerPort;
+            const size_t incVal = sAnt->GetNumColumns() - sElemsPerPort;
             sIndex += incVal; // Increment by a factor to reach next column in a port
         }
     }
@@ -200,20 +203,20 @@
     Ptr<const MatrixBasedChannelModel::ChannelMatrix> channelMatrix,
     Ptr<const MatrixBasedChannelModel::ChannelParams> channelParams,
     const Vector& sSpeed,
-    const ns3::Vector& uSpeed,
-    uint8_t numTxPorts,
-    uint8_t numRxPorts,
-    bool isReverse) const
+    const Vector& uSpeed,
+    const uint8_t numTxPorts,
+    const uint8_t numRxPorts,
+    const bool isReverse) const
 
 {
     NS_LOG_FUNCTION(this);
     Ptr<SpectrumSignalParameters> rxParams = params->Copy();
-    size_t numCluster = channelMatrix->m_channel.GetNumPages();
+    const size_t numCluster = channelMatrix->m_channel.GetNumPages();
     // compute the doppler term
     // NOTE the update of Doppler is simplified by only taking the center angle of
     // each cluster in to consideration.
-    double slotTime = Simulator::Now().GetSeconds();
-    double factor = 2 * M_PI * slotTime * GetFrequency() / 3e8;
+    const double slotTime = Simulator::Now().GetSeconds();
+    const double factor = 2 * M_PI * slotTime * GetFrequency() / 3e8;
     PhasedArrayModel::ComplexVector doppler(numCluster);
 
     // Make sure that all the structures that are passed to this function
@@ -227,19 +230,28 @@
     NS_ASSERT(numCluster <= longTerm->GetNumPages());
 
     // check if channelParams structure is generated in direction s-to-u or u-to-s
-    bool isSameDir = (channelParams->m_nodeIds == channelMatrix->m_nodeIds);
+    const bool isSameDir = channelParams->m_nodeIds == channelMatrix->m_nodeIds;
 
     // if channel params is generated in the same direction in which we
     // generate the channel matrix, angles and zenith of departure and arrival are ok,
     // just set them to corresponding variable that will be used for the generation
     // of channel matrix, otherwise we need to flip angles and zeniths of departure and arrival
     using DPV = std::vector<std::pair<double, double>>;
-    using MBCM = MatrixBasedChannelModel;
     const auto& cachedAngleSincos = channelParams->m_cachedAngleSincos;
-    const DPV& zoa = cachedAngleSincos[isSameDir ? MBCM::ZOA_INDEX : MBCM::ZOD_INDEX];
-    const DPV& zod = cachedAngleSincos[isSameDir ? MBCM::ZOD_INDEX : MBCM::ZOA_INDEX];
-    const DPV& aoa = cachedAngleSincos[isSameDir ? MBCM::AOA_INDEX : MBCM::AOD_INDEX];
-    const DPV& aod = cachedAngleSincos[isSameDir ? MBCM::AOD_INDEX : MBCM::AOA_INDEX];
+    NS_ASSERT_MSG(cachedAngleSincos.size() > MatrixBasedChannelModel::ZOD_INDEX,
+                  "Cached angle sin/cos not initialized");
+    const DPV& zoa = cachedAngleSincos[isSameDir ? MatrixBasedChannelModel::ZOA_INDEX
+                                                 : MatrixBasedChannelModel::ZOD_INDEX];
+    const DPV& zod = cachedAngleSincos[isSameDir ? MatrixBasedChannelModel::ZOD_INDEX
+                                                 : MatrixBasedChannelModel::ZOA_INDEX];
+    const DPV& aoa = cachedAngleSincos[isSameDir ? MatrixBasedChannelModel::AOA_INDEX
+                                                 : MatrixBasedChannelModel::AOD_INDEX];
+    const DPV& aod = cachedAngleSincos[isSameDir ? MatrixBasedChannelModel::AOD_INDEX
+                                                 : MatrixBasedChannelModel::AOA_INDEX];
+    NS_ASSERT(numCluster <= zoa.size());
+    NS_ASSERT(numCluster <= zod.size());
+    NS_ASSERT(numCluster <= aoa.size());
+    NS_ASSERT(numCluster <= aod.size());
 
     for (size_t cIndex = 0; cIndex < numCluster; cIndex++)
     {
@@ -254,18 +266,18 @@
         // By default, m_vScatt is set to 0, so there is no additional Doppler
         // contribution.
 
-        double alpha = channelParams->m_alpha[cIndex];
-        double D = channelParams->m_D[cIndex];
+        const double alpha = channelParams->m_alpha[cIndex];
+        const double D = channelParams->m_D[cIndex];
 
         // cluster angle angle[direction][n], where direction = 0(aoa), 1(zoa).
-        double tempDoppler =
+        const double tempDoppler =
             factor *
-            ((zoa[cIndex].first * aoa[cIndex].second * uSpeed.x +
-              zoa[cIndex].first * aoa[cIndex].first * uSpeed.y + zoa[cIndex].second * uSpeed.z) +
+            (zoa[cIndex].first * aoa[cIndex].second * uSpeed.x +
+             zoa[cIndex].first * aoa[cIndex].first * uSpeed.y + zoa[cIndex].second * uSpeed.z +
              (zod[cIndex].first * aod[cIndex].second * sSpeed.x +
               zod[cIndex].first * aod[cIndex].first * sSpeed.y + zod[cIndex].second * sSpeed.z) +
              2 * alpha * D);
-        doppler[cIndex] = std::complex<double>(cos(tempDoppler), sin(tempDoppler));
+        doppler[cIndex] = std::complex(cos(tempDoppler), sin(tempDoppler));
     }
 
     NS_ASSERT(numCluster <= doppler.GetSize());
@@ -289,8 +301,7 @@
     if (!rxParams->precodingMatrix)
     {
         // When the precoding matrix P is not set, we create one with a single column
-        ComplexMatrixArray page =
-            ComplexMatrixArray(rxParams->spectrumChannelMatrix->GetNumCols(), 1, 1);
+        auto page = ComplexMatrixArray(rxParams->spectrumChannelMatrix->GetNumCols(), 1, 1);
         // Initialize it to the inverse square of the number of txPorts
         page.Elem(0, 0, 0) = 1.0 / sqrt(rxParams->spectrumChannelMatrix->GetNumCols());
         for (size_t rowI = 0; rowI < rxParams->spectrumChannelMatrix->GetNumCols(); rowI++)
@@ -341,22 +352,26 @@
     PhasedArrayModel::ComplexVector doppler,
     uint8_t numTxPorts,
     uint8_t numRxPorts,
-    bool isReverse) const
+    const bool isReverse)
 {
-    size_t numCluster = channelMatrix->m_channel.GetNumPages();
-    auto numRb = inPsd->GetValuesN();
+    const size_t numCluster = channelMatrix->m_channel.GetNumPages();
+    const auto numRb = inPsd->GetValuesN();
+    NS_ASSERT_MSG(numCluster <= channelParams->m_delay.size(),
+                  "Channel params delays size is smaller than number of clusters");
 
-    auto directionalLongTerm = isReverse ? longTerm->Transpose() : (*longTerm);
+    auto directionalLongTerm = isReverse ? longTerm->Transpose() : *longTerm;
 
     Ptr<MatrixBasedChannelModel::Complex3DVector> chanSpct =
-        Create<MatrixBasedChannelModel::Complex3DVector>(numRxPorts, numTxPorts, (uint16_t)numRb);
+        Create<MatrixBasedChannelModel::Complex3DVector>(numRxPorts,
+                                                         numTxPorts,
+                                                         static_cast<uint16_t>(numRb));
 
     // Precompute the delay until numRb, numCluster or RB width changes
     // Whenever the channelParams is updated, the number of numRbs, numClusters
     // and RB width (12*SCS) are reset, ensuring these values are updated too
-    double rbWidth = inPsd->ConstBandsBegin()->fh - inPsd->ConstBandsBegin()->fl;
 
-    if (channelParams->m_cachedDelaySincos.GetNumRows() != numRb ||
+    if (const double rbWidth = inPsd->ConstBandsBegin()->fh - inPsd->ConstBandsBegin()->fl;
+        channelParams->m_cachedDelaySincos.GetNumRows() != numRb ||
         channelParams->m_cachedDelaySincos.GetNumCols() != numCluster ||
         channelParams->m_cachedRbWidth != rbWidth)
     {
@@ -365,14 +380,14 @@
         auto sbit = inPsd->ConstBandsBegin(); // band iterator
         for (unsigned i = 0; i < numRb; i++)
         {
-            double fsb = (*sbit).fc; // center frequency of the sub-band
+            const double fsb = sbit->fc; // center frequency of the sub-band
             for (std::size_t cIndex = 0; cIndex < numCluster; cIndex++)
             {
-                double delay = -2 * M_PI * fsb * (channelParams->m_delay[cIndex]);
+                const double delay = -2 * M_PI * fsb * channelParams->m_delay[cIndex];
                 channelParams->m_cachedDelaySincos(i, cIndex) =
-                    std::complex<double>(cos(delay), sin(delay));
+                    std::complex(cos(delay), sin(delay));
             }
-            sbit++;
+            ++sbit;
         }
     }
 
@@ -395,14 +410,14 @@
     // Compute the frequency-domain channel matrix
     while (vit != inPsd->ValuesEnd())
     {
-        if ((*vit) != 0.00)
+        if (*vit != 0.00)
         {
             auto sqrtVit = sqrt(*vit);
             for (auto rxPortIdx = 0; rxPortIdx < numRxPorts; rxPortIdx++)
             {
                 for (auto txPortIdx = 0; txPortIdx < numTxPorts; txPortIdx++)
                 {
-                    std::complex<double> subsbandGain(0.0, 0.0);
+                    std::complex subsbandGain(0.0, 0.0);
                     for (size_t cIndex = 0; cIndex < numCluster; cIndex++)
                     {
                         subsbandGain += directionalLongTerm(rxPortIdx, txPortIdx, cIndex) *
@@ -414,8 +429,8 @@
                 }
             }
         }
-        vit++;
-        iRb++;
+        ++vit;
+        ++iRb;
     }
     return chanSpct;
 }
@@ -431,10 +446,10 @@
 
     // check if the channel matrix was generated considering a as the s-node and
     // b as the u-node or vice-versa
-    auto isReverse =
+    const auto isReverse =
         channelMatrix->IsReverse(aPhasedArrayModel->GetId(), bPhasedArrayModel->GetId());
-    auto sAntenna = isReverse ? bPhasedArrayModel : aPhasedArrayModel;
-    auto uAntenna = isReverse ? aPhasedArrayModel : bPhasedArrayModel;
+    const auto sAntenna = isReverse ? bPhasedArrayModel : aPhasedArrayModel;
+    const auto uAntenna = isReverse ? aPhasedArrayModel : bPhasedArrayModel;
 
     PhasedArrayModel::ComplexVector sW;
     PhasedArrayModel::ComplexVector uW;
@@ -453,11 +468,11 @@
     bool notFound = false; // indicates if the long term has not been computed yet
 
     // compute the long term key, the key is unique for each tx-rx pair
-    uint64_t longTermId =
+    const uint64_t longTermId =
         MatrixBasedChannelModel::GetKey(aPhasedArrayModel->GetId(), bPhasedArrayModel->GetId());
 
     // look for the long term in the map and check if it is valid
-    if (m_longTermMap.find(longTermId) != m_longTermMap.end())
+    if (m_longTermMap.contains(longTermId))
     {
         NS_LOG_DEBUG("found the long term component in the map");
         longTerm = m_longTermMap[longTermId]->m_longTerm;
@@ -465,9 +480,9 @@
         // check if the channel matrix has been updated
         // or the s beam has been changed
         // or the u beam has been changed
-        update = (m_longTermMap[longTermId]->m_channel->m_generatedTime !=
-                      channelMatrix->m_generatedTime ||
-                  m_longTermMap[longTermId]->m_sW != sW || m_longTermMap[longTermId]->m_uW != uW);
+        update = m_longTermMap[longTermId]->m_channel->m_generatedTime !=
+                     channelMatrix->m_generatedTime ||
+                 m_longTermMap[longTermId]->m_sW != sW || m_longTermMap[longTermId]->m_uW != uW;
     }
     else
     {
@@ -504,9 +519,10 @@
 {
     NS_LOG_FUNCTION(this << spectrumSignalParams << a << b << aPhasedArrayModel
                          << bPhasedArrayModel);
+    NS_ASSERT_MSG(m_channelModel != nullptr, "Channel model is not set");
 
-    uint32_t aId = a->GetObject<Node>()->GetId(); // id of the node a
-    uint32_t bId = b->GetObject<Node>()->GetId(); // id of the node b
+    const uint32_t aId = a->GetObject<Node>()->GetId(); // id of the node a
+    const uint32_t bId = b->GetObject<Node>()->GetId(); // id of the node b
     NS_ASSERT_MSG(aPhasedArrayModel, "Antenna not found for node " << aId);
     NS_LOG_DEBUG("a node " << aId << " antenna " << aPhasedArrayModel);
     NS_ASSERT_MSG(bPhasedArrayModel, "Antenna not found for node " << bId);
@@ -514,14 +530,16 @@
 
     Ptr<const MatrixBasedChannelModel::ChannelMatrix> channelMatrix =
         m_channelModel->GetChannel(a, b, aPhasedArrayModel, bPhasedArrayModel);
-    Ptr<const MatrixBasedChannelModel::ChannelParams> channelParams =
+    const Ptr<const MatrixBasedChannelModel::ChannelParams> channelParams =
         m_channelModel->GetParams(a, b);
+    NS_ASSERT_MSG(channelMatrix != nullptr, "Channel matrix is null");
+    NS_ASSERT_MSG(channelParams != nullptr, "Channel params are null");
 
     // retrieve the long term component
-    Ptr<const MatrixBasedChannelModel::Complex3DVector> longTerm =
+    const Ptr<const MatrixBasedChannelModel::Complex3DVector> longTerm =
         GetLongTerm(channelMatrix, aPhasedArrayModel, bPhasedArrayModel);
 
-    auto isReverse =
+    const auto isReverse =
         channelMatrix->IsReverse(aPhasedArrayModel->GetId(), bPhasedArrayModel->GetId());
 
     // apply the beamforming gain
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.h ns-3.47/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.h
--- ns-3.46/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/model/three-gpp-spectrum-propagation-loss-model.h	2026-02-16 16:08:49.825230800 -0800
@@ -16,7 +16,6 @@
 #include "ns3/random-variable-stream.h"
 
 #include <complex.h>
-#include <map>
 #include <unordered_map>
 
 class ThreeGppCalcLongTermMultiPortTest;
@@ -125,7 +124,7 @@
     /**
      * Data structure that stores the long term component for a tx-rx pair
      */
-    struct LongTerm : public SimpleRefCount<LongTerm>
+    struct LongTerm : SimpleRefCount<LongTerm>
     {
         Ptr<const MatrixBasedChannelModel::Complex3DVector>
             m_longTerm; //!< vector containing the long term component for each cluster
@@ -149,15 +148,15 @@
      * @param isReverse true if params and longTerm were computed with RX->TX switched
      * @return 3D spectrum channel matrix with dimensions numRxPorts * numTxPorts * numRBs
      */
-    Ptr<MatrixBasedChannelModel::Complex3DVector> GenSpectrumChannelMatrix(
+    static Ptr<MatrixBasedChannelModel::Complex3DVector> GenSpectrumChannelMatrix(
         Ptr<SpectrumValue> inPsd,
         Ptr<const MatrixBasedChannelModel::Complex3DVector> longTerm,
         Ptr<const MatrixBasedChannelModel::ChannelMatrix> channelMatrix,
         Ptr<const MatrixBasedChannelModel::ChannelParams> channelParams,
         PhasedArrayModel::ComplexVector doppler,
-        uint8_t numTxPorts,
-        uint8_t numRxPorts,
-        bool isReverse) const;
+        const uint8_t numTxPorts,
+        const uint8_t numRxPorts,
+        const bool isReverse);
 
     /**
      * Get the operating frequency
@@ -193,6 +192,13 @@
     /**
      * @brief Computes a longTerm component from a specific port of s device to the
      * specific port of u device and for a specific cluster index
+     *
+     * The beamformed (long-term) effective channel coefficient:
+     * \f$ h_{\rm eff} = \mathbf{w}_{\rm rx}^H \mathbf{H} \mathbf{w}_{\rm tx} \f$.
+     *
+     * @note The RX beamforming vector is stored on the antenna as \f$\mathbf{w}_{\rm rx}\f$.
+     *       This function applies it as a Hermitian inner product (i.e., uses the complex
+     *       conjugate of the RX weights) when forming \f$\mathbf{w}_{\rm rx}^H\f$.
      * @param params The params that include the channel matrix
      * @param sAnt pointer to first antenna
      * @param uAnt uAnt pointer to second antenna
@@ -205,9 +211,9 @@
         Ptr<const MatrixBasedChannelModel::ChannelMatrix> params,
         Ptr<const PhasedArrayModel> sAnt,
         Ptr<const PhasedArrayModel> uAnt,
-        uint16_t sPortIdx,
-        uint16_t uPortIdx,
-        uint16_t cIndex) const;
+        const uint16_t sPortIdx,
+        const uint16_t uPortIdx,
+        const uint16_t cIndex) const;
 
     /**
      * @brief Computes the beamforming gain and applies it to the TX PSD
@@ -218,7 +224,7 @@
      * @param sSpeed the speed of the first node
      * @param uSpeed the speed of the second node
      * @param numTxPorts the number of the ports of the first node
-     * @param numRxPorts the number of the porst of the second node
+     * @param numRxPorts the number of the ports of the second node
      * @param isReverse indicator that tells whether the channel matrix is reverse
      * @return
      */
@@ -229,15 +235,16 @@
         Ptr<const MatrixBasedChannelModel::ChannelParams> channelParams,
         const Vector& sSpeed,
         const Vector& uSpeed,
-        uint8_t numTxPorts,
-        uint8_t numRxPorts,
-        bool isReverse) const;
+        const uint8_t numTxPorts,
+        const uint8_t numRxPorts,
+        const bool isReverse) const;
 
     int64_t DoAssignStreams(int64_t stream) override;
 
-    mutable std::unordered_map<uint64_t, Ptr<const LongTerm>>
-        m_longTermMap;                           //!< map containing the long term components
-    Ptr<MatrixBasedChannelModel> m_channelModel; //!< the model to generate the channel matrix
+    //! map containing the long-term components
+    mutable std::unordered_map<uint64_t, Ptr<const LongTerm>> m_longTermMap;
+    //! the model to generate the channel matrix
+    Ptr<MatrixBasedChannelModel> m_channelModel;
 };
 } // namespace ns3
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/test/three-gpp-channel-test-suite.cc ns-3.47/src/spectrum/test/three-gpp-channel-test-suite.cc
--- ns-3.46/src/spectrum/test/three-gpp-channel-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/test/three-gpp-channel-test-suite.cc	2026-02-16 16:08:49.826230800 -0800
@@ -1,14 +1,16 @@
 /*
  * Copyright (c) 2019 SIGNET Lab, Department of Information Engineering,
- *
+ * Copyright (c) 2026, Centre Tecnologic de Telecomunicacions de Catalunya (CTTC)
  * SPDX-License-Identifier: GPL-2.0-only
  */
 
 #include "ns3/abort.h"
 #include "ns3/angles.h"
+#include "ns3/boolean.h"
 #include "ns3/channel-condition-model.h"
 #include "ns3/config.h"
 #include "ns3/constant-position-mobility-model.h"
+#include "ns3/constant-velocity-mobility-model.h"
 #include "ns3/double.h"
 #include "ns3/ism-spectrum-value-helper.h"
 #include "ns3/isotropic-antenna-model.h"
@@ -16,7 +18,6 @@
 #include "ns3/node-container.h"
 #include "ns3/pointer.h"
 #include "ns3/rng-seed-manager.h"
-#include "ns3/simple-net-device.h"
 #include "ns3/simulator.h"
 #include "ns3/spectrum-signal-parameters.h"
 #include "ns3/string.h"
@@ -148,41 +149,32 @@
 
     // create the channel condition model
     Ptr<ChannelConditionModel> channelConditionModel =
-        CreateObject<NeverLosChannelConditionModel>();
+        CreateObject<AlwaysLosChannelConditionModel>();
 
     // create the ThreeGppChannelModel object used to generate the channel matrix
     Ptr<ThreeGppChannelModel> channelModel = CreateObject<ThreeGppChannelModel>();
     channelModel->SetAttribute("Frequency", DoubleValue(60.0e9));
     channelModel->SetAttribute("Scenario", StringValue("RMa"));
     channelModel->SetAttribute("ChannelConditionModel", PointerValue(channelConditionModel));
-    channelModel->SetAttribute("UpdatePeriod", TimeValue(MilliSeconds(updatePeriodMs - 1)));
+    channelModel->SetAttribute("UpdatePeriod", TimeValue(MilliSeconds(updatePeriodMs)));
     channelModel->AssignStreams(1);
 
     // create the tx and rx nodes
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models and set their positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(Vector(0.0, 0.0, 10.0));
-    Ptr<MobilityModel> rxMob = CreateObject<ConstantPositionMobilityModel>();
-    rxMob->SetPosition(Vector(100.0, 0.0, 10.0));
+    Ptr<ConstantVelocityMobilityModel> rxMob = CreateObject<ConstantVelocityMobilityModel>();
+    rxMob->SetPosition(Vector(30.0, 0.0, 10.0));
+    rxMob->SetVelocity(Vector(50, 0, 0));
 
     // associate the nodes and the mobility models
     nodes.Get(0)->AggregateObject(txMob);
     nodes.Get(1)->AggregateObject(rxMob);
 
-    // create the tx and rx antennas and set the their dimensions
+    // create the tx and rx antennas and set their dimensions
     Ptr<PhasedArrayModel> txAntenna = CreateObjectWithAttributes<UniformPlanarArray>(
         "NumColumns",
         UintegerValue(m_txAntennaElements),
@@ -221,10 +213,10 @@
         "The second dimension of H should be equal to the number of rx antenna elements");
 
     // test if the channel matrix is correctly generated
-    uint16_t numIt = 1000;
+    uint16_t numIt = 2000;
     for (uint16_t i = 0; i < numIt; i++)
     {
-        Simulator::Schedule(MilliSeconds(updatePeriodMs * i),
+        Simulator::Schedule(MilliSeconds(updatePeriodMs * i + i),
                             &ThreeGppChannelMatrixComputationTest::DoComputeNorm,
                             this,
                             channelModel,
@@ -248,25 +240,34 @@
     double sampleStd = 0;
     for (auto i : m_normVector)
     {
-        sampleStd += ((i - sampleMean) * (i - sampleMean));
+        const double diff = i - sampleMean;
+        sampleStd += diff * diff;
     }
     sampleStd = std::sqrt(sampleStd / (numIt - 1));
 
-    // perform the one sample t-test with a significance level of 0.05 to test
-    // the hypothesis "E [|H|^2] = M*N, where |H| indicates the Frobenius norm of
-    // H, M is the number of transmit antenna elements, and N is the number of
-    // the receive antenna elements"
-    double t = (sampleMean - m_txAntennaElements * m_txAntennaElements * m_rxAntennaElements *
-                                 m_rxAntennaElements) /
-               (sampleStd / std::sqrt(numIt));
-
-    // Using a significance level of 0.05, we reject the null hypothesis if |t| is
-    // greater than the critical value from a t-distribution with df = numIt-1
-    NS_TEST_ASSERT_MSG_EQ_TOL(
-        std::abs(t),
-        0,
-        1.65,
-        "We reject the hypothesis E[|H|^2] = M*N with a significance level of 0.05");
+    NS_TEST_ASSERT_MSG_NE(sampleStd,
+                          0,
+                          "The STD should be different from zero. Channel matrix not updated");
+
+    if (sampleStd != 0)
+    {
+        // perform the one sample t-test with a significance level of 0.05 to test
+        // the hypothesis "E [|H|^2] = M*N, where |H| indicates the Frobenius norm of
+        // H, M is the number of transmit antenna elements, and N is the number of
+        // the receive antenna elements"
+        double t = (sampleMean - m_txAntennaElements * m_txAntennaElements * m_rxAntennaElements *
+                                     m_rxAntennaElements) /
+                   (sampleStd / std::sqrt(numIt));
+
+        // Using a significance level of 0.05, we reject the null hypothesis if |t| is
+        // greater than the critical value from a t-distribution with df = numIt-1
+
+        NS_TEST_ASSERT_MSG_EQ_TOL(
+            std::abs(t),
+            0,
+            1.65,
+            "We reject the hypothesis E[|H|^2] = M*N with a significance level of 0.05");
+    }
 
     Simulator::Destroy();
 }
@@ -308,8 +309,10 @@
     void DoRun() override;
 
     /**
-     * This method is used to schedule the channel matrix computation at different
-     * time instants and to check if it correctly updated
+     * This method first slightly moves the user and then calculates the channel matrix.
+     * This function is used for channel matrix computation at different time instants
+     * and to check if whether the channel matrix is updated following the updatePeriod
+     * configuration.
      * @param channelModel the ThreeGppChannelModel object used to generate the channel matrix
      * @param txMob the mobility model of the first node
      * @param rxMob the mobility model of the second node
@@ -317,17 +320,19 @@
      * @param rxAntenna the antenna object associated to the second node
      * @param update whether if the channel matrix should be updated or not
      */
-    void DoGetChannel(Ptr<ThreeGppChannelModel> channelModel,
-                      Ptr<MobilityModel> txMob,
-                      Ptr<MobilityModel> rxMob,
-                      Ptr<PhasedArrayModel> txAntenna,
-                      Ptr<PhasedArrayModel> rxAntenna,
-                      bool update);
+    void MoveRxAndComputerSnr(Ptr<ThreeGppChannelModel> channelModel,
+                              Ptr<MobilityModel> txMob,
+                              Ptr<MobilityModel> rxMob,
+                              Ptr<PhasedArrayModel> txAntenna,
+                              Ptr<PhasedArrayModel> rxAntenna,
+                              bool update);
 
     Ptr<const ThreeGppChannelModel::ChannelMatrix>
-        m_currentChannel;            //!< used by DoGetChannel to store the current channel matrix
-    uint32_t m_txAntennaElements{4}; //!< number of rows and columns of tx antenna array
-    uint32_t m_rxAntennaElements{4}; //!< number of rows and columns of rx antenna array
+        m_currentChannel;      //!< used by DoGetChannel to store the current channel matrix
+    bool m_initialized{false}; //!< tracks whether previous matrix snapshot is set
+    ComplexMatrixArray m_previousChannelMatrix; //!< snapshot of previous channel matrix
+    uint32_t m_txAntennaElements{4};            //!< number of rows and columns of tx antenna array
+    uint32_t m_rxAntennaElements{4};            //!< number of rows and columns of rx antenna array
     uint32_t m_txPorts{1}; //!< number of horizontal and vertical ports of tx antenna array
     uint32_t m_rxPorts{1}; //!< number of horizontal and vertical ports of rx antenna array
 };
@@ -349,36 +354,60 @@
 }
 
 void
-ThreeGppChannelMatrixUpdateTest::DoGetChannel(Ptr<ThreeGppChannelModel> channelModel,
-                                              Ptr<MobilityModel> txMob,
-                                              Ptr<MobilityModel> rxMob,
-                                              Ptr<PhasedArrayModel> txAntenna,
-                                              Ptr<PhasedArrayModel> rxAntenna,
-                                              bool update)
-{
+ThreeGppChannelMatrixUpdateTest::MoveRxAndComputerSnr(Ptr<ThreeGppChannelModel> channelModel,
+                                                      Ptr<MobilityModel> txMob,
+                                                      Ptr<MobilityModel> rxMob,
+                                                      Ptr<PhasedArrayModel> txAntenna,
+                                                      Ptr<PhasedArrayModel> rxAntenna,
+                                                      bool update)
+{
+    // 3GPP channel update is activated only if the UE displacement is at least 1e-6, hence we
+    // add this minor movement to allow triggering of the channel update per the configured channel
+    // update period
+    rxMob->SetPosition(Vector(rxMob->GetPosition().x + 0.01,
+                              rxMob->GetPosition().y + 0.01,
+                              rxMob->GetPosition().z));
+
     // retrieve the channel matrix
-    Ptr<const ThreeGppChannelModel::ChannelMatrix> channelMatrix =
+    Ptr<const ThreeGppChannelModel::ChannelMatrix> newChannelMatrix =
         channelModel->GetChannel(txMob, rxMob, txAntenna, rxAntenna);
 
-    if (!m_currentChannel)
+    // For in-place updates, the ChannelMatrix pointer may remain the same.
+    // Deep-compare the matrix values with a small tolerance.
+    if (!m_initialized)
     {
-        // this is the first time we compute the channel matrix, we initialize
-        // m_currentChannel
-        m_currentChannel = channelMatrix;
+        m_previousChannelMatrix = newChannelMatrix->m_channel; // deep copy
+        m_currentChannel = newChannelMatrix;
+        m_initialized = true;
+        return;
     }
-    else
+
+    // Use absolute tolerance on complex values (magnitude comparison)
+    std::complex<double> tol(1e-12, 0.0);
+    bool equal = m_previousChannelMatrix.IsAlmostEqual(newChannelMatrix->m_channel, tol);
+    bool changed = !equal;
+
+    NS_TEST_ASSERT_MSG_EQ(
+        changed,
+        update,
+        Simulator::Now().GetMilliSeconds()
+            << " The channel matrix is not updated respecting the updatePeriod configuration");
+
+    // Update the previous snapshot if an update was expected and occurred
+    if (changed)
     {
-        // compare the old and the new channel matrices
-        NS_TEST_ASSERT_MSG_EQ((m_currentChannel->m_channel != channelMatrix->m_channel),
-                              update,
-                              Simulator::Now().GetMilliSeconds()
-                                  << " The channel matrix is not correctly updated");
+        m_previousChannelMatrix = newChannelMatrix->m_channel;
+        m_currentChannel = newChannelMatrix;
     }
 }
 
 void
 ThreeGppChannelMatrixUpdateTest::DoRun()
 {
+    m_initialized = false;
+    m_previousChannelMatrix = ComplexMatrixArray();
+    m_currentChannel = nullptr;
+
     // Build the scenario for the test
     uint32_t updatePeriodMs = 100; // update period in ms
 
@@ -397,16 +426,6 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models and set their positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(Vector(0.0, 0.0, 10.0));
@@ -448,7 +467,7 @@
     uint32_t firstTimeMs =
         1; // time instant at which the channel matrix is generated for the first time
     Simulator::Schedule(MilliSeconds(firstTimeMs),
-                        &ThreeGppChannelMatrixUpdateTest::DoGetChannel,
+                        &ThreeGppChannelMatrixUpdateTest::MoveRxAndComputerSnr,
                         this,
                         channelModel,
                         txMob,
@@ -460,7 +479,7 @@
     // call GetChannel before the update period is exceeded, the channel matrix
     // should not be updated
     Simulator::Schedule(MilliSeconds(firstTimeMs + updatePeriodMs / 2),
-                        &ThreeGppChannelMatrixUpdateTest::DoGetChannel,
+                        &ThreeGppChannelMatrixUpdateTest::MoveRxAndComputerSnr,
                         this,
                         channelModel,
                         txMob,
@@ -472,7 +491,7 @@
     // call GetChannel when the update period is exceeded, the channel matrix
     // should be recomputed
     Simulator::Schedule(MilliSeconds(firstTimeMs + updatePeriodMs + 1),
-                        &ThreeGppChannelMatrixUpdateTest::DoGetChannel,
+                        &ThreeGppChannelMatrixUpdateTest::MoveRxAndComputerSnr,
                         this,
                         channelModel,
                         txMob,
@@ -488,6 +507,469 @@
 /**
  * @ingroup spectrum-tests
  *
+ * Test case for channel consistency Procedure A (according to 3GPP 38.901 Section 7.6.3.)
+ *
+ * This test verifies that the consistent channel updates produce smoother changes in the channel
+ * matrix than generating a completely new channel. The test checks both channel condition LOS and
+ * NLOS. This is very important because, according to the Procedure A, the per-cluster variables
+ * updates are performed differently for LOS and NLOS. For example, there are different formulas for
+ * different channel conditions for cluster delay update (eq. 7.6-9) and cluster departure/arrival
+ * angles (eq. 7.6-10b and 7.6-10c). The test allows configuring different speeds and frequencies.
+ * Based on these parameters is calculated the channel coherence time and the channel update period
+ * is set based on this value.
+ *
+ * The test verifies that the evolution of the general metric like the Frobenius channel norm, and
+ * per-cluster variables such as cluster delay, cluster power, and cluster angles (AOA and ZOA) is
+ * smoother with channel updates than with the new channel realizations. For the cluster metrics is
+ * selected the third cluster as defined in Table 7.8.5 for the calibration of the channel
+ * consistency.
+ *
+ * The test checks that the channel consistent updates according 3GPP 38.901 Procedure A result in a
+ * channel that evolves more smoothly than generating a completely new channel. Variations are at
+ * least 5 times lower than these of new channel realizations.
+ *
+ * To be able to get the results not only for the updated channel but also for the new channel
+ * realizations, we create new nodes that are tracking the update positions of the original node for
+ * which the channel updates are being generated. New nodes are forcing new channel realizations.
+ */
+class ThreeGppChannelConsistencyTest : public TestCase
+{
+  public:
+    /**
+     *Constructor
+     */
+    ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue losCondition,
+                                   double speedKmh,
+                                   double freqHz);
+
+    /**
+     * Destructor
+     */
+    ~ThreeGppChannelConsistencyTest() override;
+
+    /**
+     * ChannelConsistencyMetrics structure contains the metrics that are needed to test the channel
+     * consistency.
+     */
+    struct ChannelConsistencyMetrics
+    {
+        //! the channel norm value
+        double channelNorm;
+        //! the third cluster power in dB
+        double thirdClusterPower_dB;
+        //! the third cluster delay
+        double thirdClusterDelay;
+        //! the third cluster AOA
+        double thirdClusterAoa;
+        //! the third cluster ZOA
+        double thirdClusterZoa;
+
+        /**
+         * Operator + for easier calculations of the other metrics (e.g., mean values).
+         * @param other the other object of ChannelConsistencyMetrics
+         * @return the sum of the metrics
+         */
+        ChannelConsistencyMetrics operator+(const ChannelConsistencyMetrics& other) const
+        {
+            return ChannelConsistencyMetrics{channelNorm + other.channelNorm,
+                                             thirdClusterPower_dB + other.thirdClusterPower_dB,
+                                             thirdClusterDelay + other.thirdClusterDelay,
+                                             thirdClusterAoa + other.thirdClusterAoa,
+                                             thirdClusterZoa + other.thirdClusterZoa};
+        }
+
+        /**
+         * Operator += for easier calculations of the other metrics (e.g., mean)
+         * @param other the other object of the  channel consistency metrics
+         * @return the sum of the metrics
+         */
+        ChannelConsistencyMetrics operator+=(const ChannelConsistencyMetrics& other)
+        {
+            this->channelNorm = this->channelNorm + other.channelNorm;
+            this->thirdClusterPower_dB = this->thirdClusterPower_dB + other.thirdClusterPower_dB;
+            this->thirdClusterDelay = this->thirdClusterDelay + other.thirdClusterDelay;
+            this->thirdClusterAoa = this->thirdClusterAoa + other.thirdClusterAoa;
+            this->thirdClusterZoa = this->thirdClusterZoa + other.thirdClusterZoa;
+            return *this;
+        }
+
+        /**
+         * Operator / used for easier calculations of the statistics, and test conditions.
+         * @param iterations The value with which will be divided each metric.
+         * @return the new object with the metrics that are divided by the given value.
+         */
+        ChannelConsistencyMetrics operator/(double iterations) const
+        {
+            ChannelConsistencyMetrics result;
+            result.channelNorm = this->channelNorm / iterations;
+            result.thirdClusterPower_dB = this->thirdClusterPower_dB / iterations;
+            result.thirdClusterDelay = this->thirdClusterDelay / iterations;
+            result.thirdClusterAoa = this->thirdClusterAoa / iterations;
+            result.thirdClusterZoa = this->thirdClusterZoa / iterations;
+            return result;
+        }
+
+        /**
+         * Operator /= used for easier calculations of the statistics, and test conditions.
+         * @param iterations The value with which will be divided each metric.
+         * @return the new object with the metrics that are divided by the given value.s
+         */
+        ChannelConsistencyMetrics operator/=(double iterations)
+        {
+            *this = *this / iterations;
+            return *this;
+        }
+
+        /**
+         *Used for easier calculations of the statistics, and test conditions.
+         *@return the new object with the absolute values of all the metrics
+         */
+        ChannelConsistencyMetrics abs() const
+        {
+            return ChannelConsistencyMetrics{std::abs(channelNorm),
+                                             std::abs(thirdClusterPower_dB),
+                                             std::abs(thirdClusterDelay),
+                                             std::abs(thirdClusterAoa),
+                                             std::abs(thirdClusterZoa)};
+        }
+
+        /**
+         * Used for checking the channel consistency test condition.
+         * @param other the values of the object containing the set of consistency metric
+         * @return true if the metrics of this object have lower values than these of the other
+         * object
+         */
+        bool operator<(const ChannelConsistencyMetrics& other) const
+        {
+            return (this->channelNorm < other.channelNorm) &&
+                   (this->thirdClusterPower_dB < other.thirdClusterPower_dB) &&
+                   (this->thirdClusterDelay < other.thirdClusterDelay) &&
+                   (this->thirdClusterAoa < other.thirdClusterAoa) &&
+                   (this->thirdClusterZoa < other.thirdClusterZoa);
+        }
+    };
+
+    /**
+     * Used for the calculation of the delta of all the metrics between 2 channel consistency metric
+     * objects. Returns a new object containing these delta values for each of the metrics.
+     * @param metrics1 The first channel consistency metrics object.
+     * @param metrics2 The second channel consistency metrics object.
+     * @return The channel consistency object containing the deltas of each of the metrics between
+     * the first and the second object.
+     */
+    ChannelConsistencyMetrics ComputeDeltas(ChannelConsistencyMetrics metrics1,
+                                            ChannelConsistencyMetrics metrics2)
+    {
+        ChannelConsistencyMetrics deltas;
+        deltas.channelNorm = metrics1.channelNorm - metrics2.channelNorm;
+        deltas.thirdClusterPower_dB = metrics1.thirdClusterPower_dB - metrics2.thirdClusterPower_dB;
+        deltas.thirdClusterDelay = metrics1.thirdClusterDelay - metrics2.thirdClusterDelay;
+        deltas.thirdClusterAoa = metrics1.thirdClusterAoa - metrics2.thirdClusterAoa;
+        deltas.thirdClusterZoa = metrics1.thirdClusterZoa - metrics2.thirdClusterZoa;
+        return deltas;
+    }
+
+  private:
+    /**
+     * Build the test scenario
+     */
+    void DoRun() override;
+
+    /**
+     * Compute the channel consistency metrics to evaluate the varying rate of general channel
+     * metrics like:
+     *    - Frobenius norm of the channel matrix and stores it in m_normVector, and per-cluster
+     * metrics like:
+     *    - Cluster power (of the third cluster as per Table 7.8.5 from 38.901 for the calibration
+     * of the channel consistency)
+     *    - Cluster delay (of the third cluster as per Table 7.8.5 from 38.901 for the calibration
+     * of the channel consistency)
+     *    - Cluster AOA and ZOA (of the third cluster as per Table 7.8.5 from 38.901 for the
+     * calibration of the channel consistency)
+     *
+     * @param channelModel the ThreeGppChannelModel object used to generate the channel matrix
+     * @param txMob the mobility model of the first node
+     * @param rxMob the mobility model of the second node
+     * @param txAntenna the antenna object associated to the first node
+     * @param rxAntenna the antenna object associated to the second node
+     * @param metricsVectorToBeUsed the metrics vector to be used as the output of the function
+     * @param distancesVectorToBeUsed the metrics distances vector to be used as the output of the
+     * function
+     */
+    void DoComputeConsistencyMetrics(
+        Ptr<ThreeGppChannelModel> channelModel,
+        Ptr<MobilityModel> txMob,
+        Ptr<MobilityModel> rxMob,
+        Ptr<PhasedArrayModel> txAntenna,
+        Ptr<PhasedArrayModel> rxAntenna,
+        std::vector<ChannelConsistencyMetrics>* metricsVectorToBeUsed,
+        std::vector<ChannelConsistencyMetrics>* distancesVectorToBeUsed);
+
+    //! consistency metrics for the channel updates
+    std::vector<ChannelConsistencyMetrics> m_consistencyMetricsChannelUpdates;
+    //! consistency metrics for the new channel realizations
+    std::vector<ChannelConsistencyMetrics> m_consistencyMetricsNewChannelRealizations;
+    //! consistency metric deltas for the channel updates
+    std::vector<ChannelConsistencyMetrics> m_consistencyMetricsDeltasChannelUpdates;
+    //! consistency metric deltas for the new channel realizations
+    std::vector<ChannelConsistencyMetrics> m_consistencyMetricsDeltasNewChannelRealizations;
+
+    uint32_t m_txAntennaElements{4}; //!< number of rows and columns of tx antenna array
+    uint32_t m_rxAntennaElements{4}; //!< number of rows and columns of rx antenna array
+    //! The type of channel condition to be set; can be LOS or NLOS
+    ChannelCondition::LosConditionValue m_losCondition{ChannelCondition::LOS};
+    //! The speed of the user to be used in the test case
+    double m_speedKmh{30.0};
+    //! The carrier frequency to be used in the test case
+    double m_freqHz{4.0e9};
+};
+
+/**
+ * ThreeGppChannelConsistency constructor
+ * @param losCondition the LOS condition to be used in the test setup
+ * @param speedKmh the speed of the user to be used in the test setup
+ * @param freqHz the central carrier frequency to be used in the test setup
+ */
+ThreeGppChannelConsistencyTest::ThreeGppChannelConsistencyTest(
+    ChannelCondition::LosConditionValue losCondition,
+    double speedKmh,
+    double freqHz)
+    : TestCase(std::string("Check the channel consistency feature for losCondition ") +
+               ((losCondition == ChannelCondition::LOS) ? "LOS" : "NLOS") +
+               ", speed=" + std::to_string(speedKmh) +
+               " kmph, and freq=" + std::to_string(freqHz / 1e9) + " GHz.")
+{
+    m_losCondition = losCondition;
+    m_speedKmh = speedKmh;
+    m_freqHz = freqHz;
+}
+
+ThreeGppChannelConsistencyTest::~ThreeGppChannelConsistencyTest()
+{
+}
+
+void
+ThreeGppChannelConsistencyTest::DoComputeConsistencyMetrics(
+    Ptr<ThreeGppChannelModel> channelModel,
+    Ptr<MobilityModel> txMob,
+    Ptr<MobilityModel> rxMob,
+    Ptr<PhasedArrayModel> txAntenna,
+    Ptr<PhasedArrayModel> rxAntenna,
+    std::vector<ChannelConsistencyMetrics>* metricsVectorToBeUsed,
+    std::vector<ChannelConsistencyMetrics>* distancesVectorToBeUsed)
+{
+    uint64_t txAntennaElements = txAntenna->GetNumElems();
+    uint64_t rxAntennaElements = rxAntenna->GetNumElems();
+
+    Ptr<const ThreeGppChannelModel::ChannelMatrix> channelMatrix =
+        channelModel->GetChannel(txMob, rxMob, txAntenna, rxAntenna);
+
+    double channelNorm = 0;
+    uint16_t numTotalClusters = channelMatrix->m_channel.GetNumPages();
+    for (uint16_t cIndex = 0; cIndex < numTotalClusters; cIndex++)
+    {
+        double clusterNorm = 0;
+        for (uint64_t sIndex = 0; sIndex < txAntennaElements; sIndex++)
+        {
+            for (uint64_t uIndex = 0; uIndex < rxAntennaElements; uIndex++)
+            {
+                clusterNorm +=
+                    std::pow(std::abs(channelMatrix->m_channel(uIndex, sIndex, cIndex)), 2);
+            }
+        }
+        channelNorm += clusterNorm;
+    }
+
+    Ptr<const MatrixBasedChannelModel::ChannelParams> baseParams =
+        channelModel->GetParams(txMob, rxMob);
+    Ptr<const ThreeGppChannelModel::ThreeGppChannelParams> channelParams =
+        DynamicCast<const ThreeGppChannelModel::ThreeGppChannelParams>(baseParams);
+
+    double thirdClusterPower_dB = std::numeric_limits<double>::quiet_NaN();
+    // delay of the third cluster
+    double thirdClusterDelay = std::numeric_limits<double>::quiet_NaN();
+    // AOA of the third cluster
+    double thirdClusterAoa = std::numeric_limits<double>::quiet_NaN();
+    // ZOA of the third cluster
+    double thirdClusterZoa = std::numeric_limits<double>::quiet_NaN();
+
+    if (channelParams->m_clusterPower.size() > 2)
+    {
+        uint8_t thirdClusterIndex = 2; // the index of the third cluster
+        thirdClusterPower_dB = 10 * log10(channelParams->m_clusterPower.at(thirdClusterIndex));
+        // delay of the third cluster
+        thirdClusterDelay = channelParams->m_delay.at(thirdClusterIndex);
+        // AOA of the third cluster
+        thirdClusterAoa = channelParams->m_angle.at(0).at(thirdClusterIndex);
+        // ZOA of the third cluster
+        thirdClusterZoa = channelParams->m_angle.at(1).at(thirdClusterIndex);
+    }
+
+    metricsVectorToBeUsed->push_back(
+        {channelNorm, thirdClusterPower_dB, thirdClusterDelay, thirdClusterAoa, thirdClusterZoa});
+
+    if (metricsVectorToBeUsed->size() > 1)
+    {
+        ChannelConsistencyMetrics deltas =
+            ComputeDeltas(metricsVectorToBeUsed->at(metricsVectorToBeUsed->size() - 2),
+                          metricsVectorToBeUsed->at(metricsVectorToBeUsed->size() - 1));
+        // we calculate the absolute value of the difference between the last two norms
+        distancesVectorToBeUsed->push_back(deltas);
+    }
+}
+
+void
+ThreeGppChannelConsistencyTest::DoRun()
+{
+    RngSeedManager::SetSeed(1);
+    RngSeedManager::SetRun(1);
+    double speedMps = m_speedKmh * 1000 / 3600;
+    // calculate doppler spread
+    double fDoppler = (speedMps * m_freqHz) / 3e8;
+    // calculate channel coherence time
+    Time cTime = Seconds(1 / fDoppler);
+    // set update period
+    const Time& updatePeriod = cTime;
+
+    uint16_t iterations = 100;
+    m_consistencyMetricsChannelUpdates.reserve(iterations);
+    m_consistencyMetricsNewChannelRealizations.reserve(iterations);
+    m_consistencyMetricsDeltasChannelUpdates.reserve(iterations - 1);
+    m_consistencyMetricsDeltasNewChannelRealizations.reserve(iterations - 1);
+
+    // create the channel condition model
+    Ptr<ChannelConditionModel> channelConditionModel;
+
+    if (m_losCondition == ChannelCondition::LOS)
+    {
+        channelConditionModel = CreateObject<AlwaysLosChannelConditionModel>();
+    }
+    else
+    {
+        channelConditionModel = CreateObject<NeverLosChannelConditionModel>();
+    }
+
+    // create the ThreeGppChannelModel object used to generate the channel matrix
+    Ptr<ThreeGppChannelModel> channelModel = CreateObject<ThreeGppChannelModel>();
+    channelModel->SetAttribute("Frequency", DoubleValue(m_freqHz));
+    channelModel->SetAttribute("Scenario", StringValue("RMa"));
+    channelModel->SetAttribute("ChannelConditionModel", PointerValue(channelConditionModel));
+    channelModel->SetAttribute("UpdatePeriod", TimeValue(updatePeriod));
+    channelModel->AssignStreams(1);
+
+    // create the tx and rx nodes
+    NodeContainer nodeTx;
+    NodeContainer nodeRx;
+    NodeContainer nodesRxOther;
+    // we compare the channel update between the TX1 and RX1, and the independent channel
+    // generations between the same TX and 100 different RX that are at the time instant of the
+    // channel generation at the same position as the RX1
+    nodeTx.Create(1);
+    nodeRx.Create(1);
+    nodesRxOther.Create(100);
+
+    // create the tx and rx mobility models and set their positions
+    Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
+    txMob->SetPosition(Vector(0.0, 0.0, 10.0));
+    Ptr<ConstantVelocityMobilityModel> rxMob = CreateObject<ConstantVelocityMobilityModel>();
+    rxMob->SetPosition(Vector(30.0, 0.0, 1.0));
+    rxMob->SetVelocity(Vector(speedMps, 0, 0));
+
+    // associate the nodes and the mobility models
+    nodeTx.Get(0)->AggregateObject(txMob);
+    nodeRx.Get(0)->AggregateObject(rxMob);
+
+    std::vector<Ptr<ConstantVelocityMobilityModel>> rxMobCopies{100};
+    rxMobCopies.reserve(100);
+
+    for (uint32_t i = 0; i < 100; i++)
+    {
+        Ptr<ConstantVelocityMobilityModel> rxMobCopy =
+            CreateObject<ConstantVelocityMobilityModel>();
+        rxMobCopy->SetPosition(Vector(30.0, 0.0, 1.0));
+        rxMobCopy->SetVelocity(Vector(speedMps, 0, 0));
+        nodesRxOther.Get(i)->AggregateObject(rxMobCopy);
+        rxMobCopies[i] = rxMobCopy;
+    }
+
+    // create the tx and rx antennas and set the their dimensions
+    Ptr<PhasedArrayModel> txAntenna = CreateObjectWithAttributes<UniformPlanarArray>(
+        "NumColumns",
+        UintegerValue(m_txAntennaElements),
+        "NumRows",
+        UintegerValue(m_txAntennaElements),
+        "AntennaElement",
+        PointerValue(CreateObject<IsotropicAntennaModel>()),
+        "BearingAngle",
+        DoubleValue(0));
+
+    Ptr<PhasedArrayModel> rxAntenna = CreateObjectWithAttributes<UniformPlanarArray>(
+        "NumColumns",
+        UintegerValue(m_rxAntennaElements),
+        "NumRows",
+        UintegerValue(m_rxAntennaElements),
+        "AntennaElement",
+        PointerValue(CreateObject<IsotropicAntennaModel>()),
+        "BearingAngle",
+        DoubleValue(M_PI));
+
+    // test if the channel matrix is correctly updated
+    for (uint16_t i = 0; i < iterations; i++)
+    {
+        Simulator::Schedule(updatePeriod * i + NanoSeconds(1),
+                            &ThreeGppChannelConsistencyTest::DoComputeConsistencyMetrics,
+                            this,
+                            channelModel,
+                            txMob,
+                            rxMob,
+                            txAntenna,
+                            rxAntenna,
+                            &m_consistencyMetricsChannelUpdates,
+                            &m_consistencyMetricsDeltasChannelUpdates);
+
+        Simulator::Schedule(updatePeriod * i + NanoSeconds(1),
+                            &ThreeGppChannelConsistencyTest::DoComputeConsistencyMetrics,
+                            this,
+                            channelModel,
+                            txMob,
+                            rxMobCopies[i],
+                            txAntenna,
+                            rxAntenna,
+                            &m_consistencyMetricsNewChannelRealizations,
+                            &m_consistencyMetricsDeltasNewChannelRealizations);
+    }
+
+    Simulator::Run();
+
+    // compute the sample mean of the channel updates
+    ChannelConsistencyMetrics metricMeanChannelUpdates =
+        m_consistencyMetricsDeltasChannelUpdates.at(0).abs();
+    ChannelConsistencyMetrics metricMeanNewChannelRealizations =
+        m_consistencyMetricsDeltasNewChannelRealizations.at(0).abs();
+
+    for (auto i : m_consistencyMetricsDeltasChannelUpdates)
+    {
+        metricMeanChannelUpdates += i.abs();
+    }
+    metricMeanChannelUpdates /= m_consistencyMetricsDeltasChannelUpdates.size();
+
+    for (auto i : m_consistencyMetricsDeltasNewChannelRealizations)
+    {
+        metricMeanNewChannelRealizations += i.abs();
+    }
+    metricMeanNewChannelRealizations /= m_consistencyMetricsDeltasNewChannelRealizations.size();
+
+    NS_TEST_ASSERT_MSG_EQ(metricMeanChannelUpdates < metricMeanNewChannelRealizations / 5,
+                          true,
+                          "Check that the average distances between the consistency metrics of "
+                          "the consecutive channel updates "
+                          " are lower than of the consecutive new channel generations.");
+    Simulator::Destroy();
+}
+
+/**
+ * @ingroup spectrum-tests
+ *
  * Test case for the ThreeGppChannelModel class.
  * It checks if the channel realizations are correctly
  * updated after a change in the number of antenna elements.
@@ -590,16 +1072,6 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models and set their positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(Vector(0.0, 0.0, 10.0));
@@ -727,8 +1199,10 @@
     Ptr<MobilityModel> txMob;               //!< the mobility model of the tx device
     Ptr<MobilityModel> rxMob;               //!< the mobility model of the rx device
     Ptr<SpectrumValue> rxPsdOld;            //!< the previously received PSD
-    Ptr<PhasedArrayModel> txAntenna;        //!< the antenna array of the tx device
-    Ptr<PhasedArrayModel> rxAntenna;        //!< the antenna array of the rx device
+    Ptr<const ComplexMatrixArray>
+        rxSpectrumChannelMatrix;     //!< the spectrum channel matrix of the received signal
+    Ptr<PhasedArrayModel> txAntenna; //!< the antenna array of the tx device
+    Ptr<PhasedArrayModel> rxAntenna; //!< the antenna array of the rx device
 };
 
 /**
@@ -771,29 +1245,39 @@
     void DoRun() override;
 
     /**
-     * Points the beam of thisDevice towards otherDevice
-     * @param thisDevice the device to configure
+     * Perform beamforming from this node towards the other node
+     * @param thisMob the mobility model of this device
      * @param thisAntenna the antenna object associated to thisDevice
-     * @param otherDevice the device to communicate with
-     * @param otherAntenna the antenna object associated to otherDevice
+     * @param otherMob the mobility model of the other device
+     * @param otherAntenna the antenna array object of to otherDevice
      */
-    void DoBeamforming(Ptr<NetDevice> thisDevice,
+    void DoBeamforming(Ptr<MobilityModel> thisMob,
                        Ptr<PhasedArrayModel> thisAntenna,
-                       Ptr<NetDevice> otherDevice,
+                       Ptr<MobilityModel> otherMob,
                        Ptr<PhasedArrayModel> otherAntenna);
 
     /**
-     * Test of the long term component is correctly updated when the channel
-     * matrix is recomputed
+     *  Test of the long-term component is correctly updated when the positions and beamforming
+     * change
+     * @param params a structure that contains the set of parameters needed by
+     * CheckUpdateAfterChangingPositionsAndBeamforming in order to perform calculations
+     */
+    void CheckUpdateAfterChangingPositionsAndBeamforming(const CheckLongTermUpdateParams& params);
+
+    /**
+     * Test of the long-term component is correctly updated when the update period expires.
+     * This function slightly moves the position of the tx node to allow the update to be performed.
      * @param params a structure that contains the set of parameters needed by CheckLongTermUpdate
      * in order to perform calculations
      */
-    void CheckLongTermUpdate(const CheckLongTermUpdateParams& params);
+    void CheckLongTermUpdate(CheckLongTermUpdateParams& params);
 
     uint32_t m_txAntennaElements{4}; //!< number of rows and columns of tx antenna array
     uint32_t m_rxAntennaElements{4}; //!< number of rows and columns of rx antenna array
     uint32_t m_txPorts{1}; //!< number of horizontal and vertical ports of tx antenna array
     uint32_t m_rxPorts{1}; //!< number of horizontal and vertical ports of rx antenna array
+    //! used as the input for the long-term update check after the update period expires
+    Ptr<SpectrumSignalParameters> m_rxParamsOld;
 };
 
 ThreeGppSpectrumPropagationLossModelTest::ThreeGppSpectrumPropagationLossModelTest(
@@ -814,13 +1298,13 @@
 }
 
 void
-ThreeGppSpectrumPropagationLossModelTest::DoBeamforming(Ptr<NetDevice> thisDevice,
+ThreeGppSpectrumPropagationLossModelTest::DoBeamforming(Ptr<MobilityModel> thisMob,
                                                         Ptr<PhasedArrayModel> thisAntenna,
-                                                        Ptr<NetDevice> otherDevice,
+                                                        Ptr<MobilityModel> otherMob,
                                                         Ptr<PhasedArrayModel> otherAntenna)
 {
-    Vector aPos = thisDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
-    Vector bPos = otherDevice->GetNode()->GetObject<MobilityModel>()->GetPosition();
+    Vector aPos = thisMob->GetPosition();
+    Vector bPos = otherMob->GetPosition();
 
     // compute the azimuth and the elevation angles
     Angles completeAngle(bPos, aPos);
@@ -831,15 +1315,43 @@
 }
 
 void
-ThreeGppSpectrumPropagationLossModelTest::CheckLongTermUpdate(
+ThreeGppSpectrumPropagationLossModelTest::CheckUpdateAfterChangingPositionsAndBeamforming(
     const CheckLongTermUpdateParams& params)
 {
+    // Reciprocity check: evaluate reverse direction after changing positions/beamforming.
+    auto rxPsdNewParams = params.lossModel->DoCalcRxPowerSpectralDensity(params.txParams,
+                                                                         params.rxMob,
+                                                                         params.txMob,
+                                                                         params.rxAntenna,
+                                                                         params.txAntenna);
+
+    NS_TEST_ASSERT_MSG_EQ((*params.rxPsdOld == *rxPsdNewParams->psd),
+                          false,
+                          "Changing the positions and BF vectors the rx PSD does not change");
+
+    NS_TEST_ASSERT_MSG_EQ(
+        (*params.rxSpectrumChannelMatrix == *rxPsdNewParams->spectrumChannelMatrix),
+        false,
+        "Changing the BF should change de frequency domain channel matrix");
+
+    m_rxParamsOld = rxPsdNewParams;
+}
+
+void
+ThreeGppSpectrumPropagationLossModelTest::CheckLongTermUpdate(CheckLongTermUpdateParams& params)
+{
+    // we need to slightly move the node to allow the channel to be updated upon the update period,
+    // but less than 1 meter, which would trigger new channel generation
+    params.txMob->SetPosition(Vector(params.txMob->GetPosition().x + 0.1,
+                                     params.txMob->GetPosition().y,
+                                     params.txMob->GetPosition().z));
+
     auto rxPsdNewParams = params.lossModel->DoCalcRxPowerSpectralDensity(params.txParams,
                                                                          params.txMob,
                                                                          params.rxMob,
                                                                          params.txAntenna,
                                                                          params.rxAntenna);
-    NS_TEST_ASSERT_MSG_EQ((*params.rxPsdOld == *rxPsdNewParams->psd),
+    NS_TEST_ASSERT_MSG_EQ((*m_rxParamsOld->psd == *rxPsdNewParams->psd),
                           false,
                           "The long term is not updated when the channel matrix is recomputed");
 }
@@ -869,16 +1381,6 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models and set their positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(Vector(0.0, 0.0, 10.0));
@@ -895,7 +1397,7 @@
         "NumColumns",
         UintegerValue(m_txAntennaElements),
         "NumRows",
-        UintegerValue(m_rxAntennaElements),
+        UintegerValue(m_txAntennaElements),
         "AntennaElement",
         PointerValue(CreateObject<IsotropicAntennaModel>()),
         "NumVerticalPorts",
@@ -916,8 +1418,8 @@
         UintegerValue(m_rxPorts));
 
     // set the beamforming vectors
-    DoBeamforming(txDev, txAntenna, rxDev, rxAntenna);
-    DoBeamforming(rxDev, rxAntenna, txDev, txAntenna);
+    DoBeamforming(txMob, txAntenna, rxMob, rxAntenna);
+    DoBeamforming(rxMob, rxAntenna, txMob, txAntenna);
 
     // create the tx psd
     SpectrumValue5MhzFactory sf;
@@ -931,7 +1433,7 @@
     auto rxParamsOld =
         lossModel->DoCalcRxPowerSpectralDensity(txParams, txMob, rxMob, txAntenna, rxAntenna);
 
-    // 1) check that the rx PSD is equal for both the direct and the reverse channel
+    // 1) check reciprocity: rx PSD is equal for both the direct and the reverse channel
     auto rxParamsNew =
         lossModel->DoCalcRxPowerSpectralDensity(txParams, rxMob, txMob, rxAntenna, txAntenna);
     if ((rxParamsOld->spectrumChannelMatrix->GetNumCols() ==
@@ -964,25 +1466,30 @@
     PhasedArrayModel::ComplexVector txBfVector = txAntenna->GetBeamformingVector();
     txBfVector[0] = std::complex<double>(0.0, 0.0);
     txAntenna->SetBeamformingVector(txBfVector);
-
-    rxParamsNew =
-        lossModel->DoCalcRxPowerSpectralDensity(txParams, rxMob, txMob, rxAntenna, txAntenna);
-    NS_TEST_ASSERT_MSG_EQ((*rxParamsOld->psd == *rxParamsNew->psd),
-                          false,
-                          "Changing the BF vectors the rx PSD does not change");
-
-    NS_TEST_ASSERT_MSG_EQ(
-        (*rxParamsOld->spectrumChannelMatrix == *rxParamsNew->spectrumChannelMatrix),
-        false,
-        "Changing the BF should change de frequency domain channel matrix");
-
-    // update rxPsdOld
-    rxParamsOld = rxParamsNew;
+    CheckLongTermUpdateParams paramsPositionUpdate{lossModel,
+                                                   txParams,
+                                                   rxMob,
+                                                   txMob,
+                                                   rxParamsOld->psd,
+                                                   rxParamsOld->spectrumChannelMatrix,
+                                                   rxAntenna,
+                                                   txAntenna};
+    Simulator::Schedule(
+        MilliSeconds(50),
+        &ThreeGppSpectrumPropagationLossModelTest::CheckUpdateAfterChangingPositionsAndBeamforming,
+        this,
+        paramsPositionUpdate);
 
     // 3) check if the long term is updated when the channel matrix is recomputed
-    CheckLongTermUpdateParams
-        params{lossModel, txParams, txMob, rxMob, rxParamsOld->psd, txAntenna, rxAntenna};
-    Simulator::Schedule(MilliSeconds(101),
+    CheckLongTermUpdateParams params{lossModel,
+                                     txParams,
+                                     txMob,
+                                     rxMob,
+                                     rxParamsOld->psd,
+                                     rxParamsOld->spectrumChannelMatrix,
+                                     txAntenna,
+                                     rxAntenna};
+    Simulator::Schedule(MilliSeconds(151),
                         &ThreeGppSpectrumPropagationLossModelTest::CheckLongTermUpdate,
                         this,
                         params);
@@ -1064,16 +1571,6 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models and set their positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(Vector(0.0, 0.0, 10.0));
@@ -1388,16 +1885,6 @@
     NodeContainer nodes;
     nodes.Create(2);
 
-    // create the tx and rx devices
-    Ptr<SimpleNetDevice> txDev = CreateObject<SimpleNetDevice>();
-    Ptr<SimpleNetDevice> rxDev = CreateObject<SimpleNetDevice>();
-
-    // associate the nodes and the devices
-    nodes.Get(0)->AddDevice(txDev);
-    txDev->SetNode(nodes.Get(0));
-    nodes.Get(1)->AddDevice(rxDev);
-    rxDev->SetNode(nodes.Get(1));
-
     // create the tx and rx mobility models and set their positions
     Ptr<MobilityModel> txMob = CreateObject<ConstantPositionMobilityModel>();
     txMob->SetPosition(m_txLoc);
@@ -1467,6 +1954,31 @@
 ThreeGppChannelTestSuite::ThreeGppChannelTestSuite()
     : TestSuite("three-gpp-channel", Type::UNIT)
 {
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::LOS, 30, 4e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::NLOS, 30, 4e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::LOS, 30, 30e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::NLOS, 30, 30e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::LOS, 10, 30e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::NLOS, 10, 30e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::LOS, 10, 4e9),
+        TestCase::Duration::QUICK);
+    AddTestCase(
+        new ThreeGppChannelConsistencyTest(ChannelCondition::LosConditionValue::NLOS, 10, 4e9),
+        TestCase::Duration::QUICK);
+
     AddTestCase(new ThreeGppChannelMatrixComputationTest(2, 2, 1, 1), TestCase::Duration::QUICK);
     AddTestCase(new ThreeGppChannelMatrixComputationTest(4, 2, 1, 1), TestCase::Duration::QUICK);
     AddTestCase(new ThreeGppChannelMatrixComputationTest(2, 2, 2, 2), TestCase::Duration::QUICK);
@@ -1479,12 +1991,14 @@
     AddTestCase(new ThreeGppAntennaSetupChangedTest(), TestCase::Duration::QUICK);
     AddTestCase(new ThreeGppSpectrumPropagationLossModelTest(4, 4, 1, 1),
                 TestCase::Duration::QUICK);
+
     AddTestCase(new ThreeGppSpectrumPropagationLossModelTest(4, 4, 2, 2),
                 TestCase::Duration::QUICK);
     AddTestCase(new ThreeGppSpectrumPropagationLossModelTest(4, 2, 2, 2),
                 TestCase::Duration::QUICK);
     AddTestCase(new ThreeGppSpectrumPropagationLossModelTest(4, 2, 2, 1),
                 TestCase::Duration::QUICK);
+
     AddTestCase(new ThreeGppCalcLongTermMultiPortTest(), TestCase::Duration::QUICK);
 
     /**
@@ -1496,6 +2010,7 @@
      *   (0,0)   (0,0)   (-5.8,)   (-5.8,0)
      *   (0,0)   (0,0)   (-5.8,0)  (-5.8,0)
      */
+
     std::valarray<std::complex<double>> testChannel1 =
         {5.9, 5.9, 0, 0, 5.9, 5.9, 0, 0, 0, 0, -5.8, -5.8, 0, 0, -5.8, -5.8};
     AddTestCase(new ThreeGppMimoPolarizationTest("Face-to-face. 0 and 0 pol. slant angles.",
@@ -1516,6 +2031,7 @@
      *   (3,0)   (3,0)   (-5,0)  (-5,0)
      *   (3,0)   (3,0)   (-5,0)  (-5,0)
      */
+
     AddTestCase(
         new ThreeGppMimoPolarizationTest("Face-to-face. 30 and 0 pol. slant angles.",
                                          Vector{0, 0, 3},
@@ -1535,6 +2051,7 @@
      *  (4,0)  (4,0)  (4,0)  (4,0)
      *  (4,0)  (4,0)  (4,0)  (4,0)
      */
+
     AddTestCase(
         new ThreeGppMimoPolarizationTest("Face-to-face. 45 and 0 pol. slant angles.",
                                          Vector{0, 0, 3},
@@ -1554,6 +2071,7 @@
      *  (5.8,0)  (5.8,0)  (0,0)  (0,0)
      *  (5.8,0)  (5.8,0)  (0,0)  (0,0)
      */
+
     AddTestCase(new ThreeGppMimoPolarizationTest(
                     "Face-to-face. 90 and 0 pol. slant angles.",
                     Vector{0, 0, 3},
@@ -1577,6 +2095,7 @@
      *  Notice that we expect almost the same matrix as in the first case in
      *  which
      */
+
     AddTestCase(
         new ThreeGppMimoPolarizationTest("Face-to-face. Different positions. Different bearing "
                                          "angles. 0 and 0 pol. slant angles.",
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/spectrum/test/two-ray-splm-test-suite.cc ns-3.47/src/spectrum/test/two-ray-splm-test-suite.cc
--- ns-3.46/src/spectrum/test/two-ray-splm-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/spectrum/test/two-ray-splm-test-suite.cc	2026-02-16 16:08:49.826230800 -0800
@@ -5,6 +5,7 @@
  */
 
 #include "ns3/abort.h"
+#include "ns3/boolean.h"
 #include "ns3/config.h"
 #include "ns3/constant-position-mobility-model.h"
 #include "ns3/double.h"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/stats/test/histogram-test-suite.cc ns-3.47/src/stats/test/histogram-test-suite.cc
--- ns-3.46/src/stats/test/histogram-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/stats/test/histogram-test-suite.cc	2026-02-16 16:08:49.830230700 -0800
@@ -18,7 +18,6 @@
  */
 class HistogramTestCase : public ns3::TestCase
 {
-  private:
   public:
     HistogramTestCase();
     void DoRun() override;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/test/ns3tcp/ns3tcp-socket-writer.h ns-3.47/src/test/ns3tcp/ns3tcp-socket-writer.h
--- ns-3.46/src/test/ns3tcp/ns3tcp-socket-writer.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/test/ns3tcp/ns3tcp-socket-writer.h	2026-02-16 16:08:49.832230800 -0800
@@ -4,6 +4,9 @@
  * SPDX-License-Identifier: GPL-2.0-only
  */
 
+#ifndef NS3TCP_SOCKET_WRITER_H
+#define NS3TCP_SOCKET_WRITER_H
+
 #include "ns3/address.h"
 #include "ns3/application.h"
 #include "ns3/node.h"
@@ -59,3 +62,5 @@
     bool m_isConnected;   //!< True if the socket setup has been done.
 };
 } // namespace ns3
+
+#endif // NS3TCP_SOCKET_WRITER_H
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/traffic-control/examples/fqcodel-l4s-example.cc ns-3.47/src/traffic-control/examples/fqcodel-l4s-example.cc
--- ns-3.46/src/traffic-control/examples/fqcodel-l4s-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/traffic-control/examples/fqcodel-l4s-example.cc	2026-02-16 16:08:49.836230800 -0800
@@ -517,7 +517,7 @@
     ////////////////////////////////////////////////////////////
     // command-line argument parsing                          //
     ////////////////////////////////////////////////////////////
-    CommandLine cmd;
+    CommandLine cmd(__FILE__);
     cmd.AddValue("n0TcpType", "n0 TCP type (bic, dctcp, or reno)", n0TcpType);
     cmd.AddValue("n1TcpType", "n1 TCP type (bic, dctcp, or reno)", n1TcpType);
     cmd.AddValue("scenarioNum",
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/traffic-control/test/adaptive-red-queue-disc-test-suite.cc ns-3.47/src/traffic-control/test/adaptive-red-queue-disc-test-suite.cc
--- ns-3.46/src/traffic-control/test/adaptive-red-queue-disc-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/traffic-control/test/adaptive-red-queue-disc-test-suite.cc	2026-02-16 16:08:49.838230800 -0800
@@ -116,7 +116,6 @@
     double minTh = 70;
     double maxTh = 150;
     uint32_t qSize = 300;
-    Address dest;
 
     // test 1: Verify automatic setting of QW. [QW = 0.0 with default LinkBandwidth]
     Ptr<RedQueueDisc> queue = CreateObject<RedQueueDisc>();
@@ -378,7 +377,6 @@
 void
 AredQueueDiscTestCase::EnqueueWithDelay(Ptr<RedQueueDisc> queue, uint32_t size, uint32_t nPkt)
 {
-    Address dest;
     double delay = 0.01; // enqueue packets with delay to allow m_curMaxP to adapt
     for (uint32_t i = 0; i < nPkt; i++)
     {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/traffic-control/test/cobalt-queue-disc-test-suite.cc ns-3.47/src/traffic-control/test/cobalt-queue-disc-test-suite.cc
--- ns-3.46/src/traffic-control/test/cobalt-queue-disc-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/traffic-control/test/cobalt-queue-disc-test-suite.cc	2026-02-16 16:08:49.838230800 -0800
@@ -333,7 +333,6 @@
 void
 CobaltQueueDiscDropTest::EnqueueWithDelay(Ptr<CobaltQueueDisc> queue, uint32_t size, uint32_t nPkt)
 {
-    Address dest;
     double delay = 0.01; // enqueue packets with delay
     for (uint32_t i = 0; i < nPkt; i++)
     {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/traffic-control/test/pie-queue-disc-test-suite.cc ns-3.47/src/traffic-control/test/pie-queue-disc-test-suite.cc
--- ns-3.46/src/traffic-control/test/pie-queue-disc-test-suite.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/traffic-control/test/pie-queue-disc-test-suite.cc	2026-02-16 16:08:49.838230800 -0800
@@ -786,7 +786,6 @@
                                        uint32_t nPkt,
                                        Ptr<PieQueueDiscTestItem> testAttributes)
 {
-    Address dest;
     double delay = 0.01; // enqueue packets with delay
     for (uint32_t i = 0; i < nPkt; i++)
     {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/uan/examples/uan-cw-example.cc ns-3.47/src/uan/examples/uan-cw-example.cc
--- ns-3.46/src/uan/examples/uan-cw-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/uan/examples/uan-cw-example.cc	2026-02-16 16:08:49.839230800 -0800
@@ -29,13 +29,12 @@
  * with respect to changes in CW.
  */
 
-#include "uan-cw-example.h"
-
 #include "ns3/applications-module.h"
 #include "ns3/core-module.h"
 #include "ns3/mobility-module.h"
 #include "ns3/network-module.h"
 #include "ns3/stats-module.h"
+#include "ns3/uan-module.h"
 
 #include <fstream>
 
@@ -43,6 +42,75 @@
 
 NS_LOG_COMPONENT_DEFINE("UanCwExample");
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
+/**
+ * @ingroup uan
+ * @brief Helper class for UAN CW MAC example.
+ *
+ * An experiment measures the average throughput for a series of CW values.
+ *
+ * @see uan-cw-example.cc
+ */
+class Experiment
+{
+  public:
+    /**
+     * Run an experiment across a range of congestion window values.
+     *
+     * @param uan The Uan stack helper to configure nodes in the model.
+     * @return The data set of CW values and measured throughput
+     */
+    Gnuplot2dDataset Run(UanHelper& uan);
+    /**
+     * Receive all available packets from a socket.
+     *
+     * @param socket The receive socket.
+     */
+    void ReceivePacket(Ptr<Socket> socket);
+    /**
+     * Assign new random positions to a set of nodes.  New positions
+     * are randomly assigned within the bounding box.
+     *
+     * @param nodes The nodes to reposition.
+     */
+    void UpdatePositions(NodeContainer& nodes) const;
+    /** Save the throughput from a single run. */
+    void ResetData();
+    /**
+     * Compute average throughput for a set of runs, then increment CW.
+     *
+     * @param cw CW value for completed runs.
+     */
+    void IncrementCw(uint32_t cw);
+
+    uint32_t m_numNodes;   //!< Number of transmitting nodes.
+    uint32_t m_dataRate;   //!< DataRate in bps.
+    double m_depth;        //!< Depth of transmitting and sink nodes.
+    double m_boundary;     //!< Size of boundary in meters.
+    uint32_t m_packetSize; //!< Generated packet size in bytes.
+    uint32_t m_bytesTotal; //!< Total bytes received.
+    uint32_t m_cwMin;      //!< Min CW to simulate.
+    uint32_t m_cwMax;      //!< Max CW to simulate.
+    uint32_t m_cwStep;     //!< CW step size, default 10.
+    uint32_t m_avgs;       //!< Number of topologies to test for each cw point.
+
+    Time m_slotTime; //!< Slot time duration.
+    Time m_simTime;  //!< Simulation run time, default 1000 s.
+
+    std::string m_gnudatfile;     //!< Name for GNU Plot output, default uan-cw-example.gpl.
+    std::string m_asciitracefile; //!< Name for ascii trace file, default uan-cw-example.asc.
+    std::string m_bhCfgFile;      //!< (Unused)
+
+    Gnuplot2dDataset m_data;           //!< Container for the simulation data.
+    std::vector<double> m_throughputs; //!< Throughput for each run.
+
+    /** Default constructor. */
+    Experiment();
+};
+
 Experiment::Experiment()
     : m_numNodes(15),
       m_dataRate(80),
@@ -247,6 +315,8 @@
     }
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char** argv)
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/uan/examples/uan-cw-example.h ns-3.47/src/uan/examples/uan-cw-example.h
--- ns-3.46/src/uan/examples/uan-cw-example.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/uan/examples/uan-cw-example.h	1969-12-31 16:00:00.000000000 -0800
@@ -1,83 +0,0 @@
-/*
- * Copyright (c) 2009 University of Washington
- *
- * SPDX-License-Identifier: GPL-2.0-only
- *
- * Author: Leonard Tracy <lentracy@gmail.com>
- */
-
-#ifndef UAN_CW_EXAMPLE_H
-#define UAN_CW_EXAMPLE_H
-
-#include "ns3/network-module.h"
-#include "ns3/stats-module.h"
-#include "ns3/uan-module.h"
-
-using namespace ns3;
-
-/**
- * @ingroup uan
- * @brief Helper class for UAN CW MAC example.
- *
- * An experiment measures the average throughput for a series of CW values.
- *
- * @see uan-cw-example.cc
- */
-class Experiment
-{
-  public:
-    /**
-     * Run an experiment across a range of congestion window values.
-     *
-     * @param uan The Uan stack helper to configure nodes in the model.
-     * @return The data set of CW values and measured throughput
-     */
-    Gnuplot2dDataset Run(UanHelper& uan);
-    /**
-     * Receive all available packets from a socket.
-     *
-     * @param socket The receive socket.
-     */
-    void ReceivePacket(Ptr<Socket> socket);
-    /**
-     * Assign new random positions to a set of nodes.  New positions
-     * are randomly assigned within the bounding box.
-     *
-     * @param nodes The nodes to reposition.
-     */
-    void UpdatePositions(NodeContainer& nodes) const;
-    /** Save the throughput from a single run. */
-    void ResetData();
-    /**
-     * Compute average throughput for a set of runs, then increment CW.
-     *
-     * @param cw CW value for completed runs.
-     */
-    void IncrementCw(uint32_t cw);
-
-    uint32_t m_numNodes;   //!< Number of transmitting nodes.
-    uint32_t m_dataRate;   //!< DataRate in bps.
-    double m_depth;        //!< Depth of transmitting and sink nodes.
-    double m_boundary;     //!< Size of boundary in meters.
-    uint32_t m_packetSize; //!< Generated packet size in bytes.
-    uint32_t m_bytesTotal; //!< Total bytes received.
-    uint32_t m_cwMin;      //!< Min CW to simulate.
-    uint32_t m_cwMax;      //!< Max CW to simulate.
-    uint32_t m_cwStep;     //!< CW step size, default 10.
-    uint32_t m_avgs;       //!< Number of topologies to test for each cw point.
-
-    Time m_slotTime; //!< Slot time duration.
-    Time m_simTime;  //!< Simulation run time, default 1000 s.
-
-    std::string m_gnudatfile;     //!< Name for GNU Plot output, default uan-cw-example.gpl.
-    std::string m_asciitracefile; //!< Name for ascii trace file, default uan-cw-example.asc.
-    std::string m_bhCfgFile;      //!< (Unused)
-
-    Gnuplot2dDataset m_data;           //!< Container for the simulation data.
-    std::vector<double> m_throughputs; //!< Throughput for each run.
-
-    /** Default constructor. */
-    Experiment();
-};
-
-#endif /* UAN_CW_EXAMPLE_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/uan/examples/uan-rc-example.cc ns-3.47/src/uan/examples/uan-rc-example.cc
--- ns-3.46/src/uan/examples/uan-rc-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/uan/examples/uan-rc-example.cc	2026-02-16 16:08:49.839230800 -0800
@@ -40,8 +40,6 @@
  * For questions about this MAC protocol email "lentracy@gmail.com"
  */
 
-#include "uan-rc-example.h"
-
 #include "ns3/applications-module.h"
 #include "ns3/callback.h"
 #include "ns3/config.h"
@@ -50,6 +48,7 @@
 #include "ns3/mobility-module.h"
 #include "ns3/network-module.h"
 #include "ns3/stats-module.h"
+#include "ns3/uan-module.h"
 
 #include <fstream>
 
@@ -57,6 +56,94 @@
 
 NS_LOG_COMPONENT_DEFINE("UanRcExample");
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
+/**
+ * @ingroup uan
+ *
+ * Container for the parameters describing a single experiment.
+ *
+ * An experiment samples a range of parameter values.  The parameter
+ * controls the number of nodes (if m_doNode is true), or the
+ * UanMacRcGw MaxReservations attribute.
+ *
+ * @see uan-rc-example.cc
+ *
+ */
+class Experiment
+{
+  public:
+    uint32_t m_simMin;    //!< Minimum parameter to test.
+    uint32_t m_simMax;    //!< Maximum parameter to test.
+    uint32_t m_simStep;   //!< Amount to increment param per trial
+    uint32_t m_numRates;  //!< Number of divided rates ( (NumberRates+1)%TotalRate should be 0).
+    uint32_t m_totalRate; //!< Total channel capacity.
+    uint32_t m_maxRange;  //!< Maximum range between gateway and acoustic node.
+    uint32_t m_numNodes;  //!< Number of nodes (invalid for m_doNode true).
+    uint32_t m_pktSize;   //!< Packet size in bytes.
+    bool m_doNode;        //!< 1 for do max nodes simulation (invalidates AMin and AMax values).
+    Time m_sifs;          //!< SIFS time duration.
+    Time m_simTime;       //!< Simulation time per trial
+
+    std::string m_gnuplotfile; //!< Filename for GnuPlot.
+
+    uint32_t m_bytesTotal; //!< Total number of bytes received in a simulation run.
+
+    UanModesList m_dataModes;    //!< List of UanTxModes used for data channels.
+    UanModesList m_controlModes; //!< List of UanTxModes used for control channels.
+
+    /**
+     * Callback to receive a packet.
+     *
+     * @param socket The socket receiving packets.
+     */
+    void ReceivePacket(Ptr<Socket> socket);
+    /**
+     * Create a UanTxMode.
+     *
+     * The mode physical rate is set equal to m_totalRate.
+     * The data rate and bandwidth are set to
+     *
+     * \f[{\rm{rate, bandwidth}} = {\rm{kass}}\frac{{{\rm{m\_totalRate}}}}{{{\rm{m\_numRates}} +
+     * 1}}\f]
+     *
+     * The center frequency is set to
+     *
+     * \f[{f_{center}} = {\rm{fc}} + \frac{{ \pm {\rm{m\_totalRate}} \mp {\rm{rate}}}}{2}\f]
+     *
+     * where the upper sign is taken if upperblock is true.
+     *
+     * @param kass Fraction of total bandwidth assigned to mode.
+     * @param fc Mode center frequency offset.
+     * @param upperblock Sign choice in setting the center frequency.
+     * @param name Mode name.
+     * @return The new mode.
+     */
+    UanTxMode CreateMode(uint32_t kass, uint32_t fc, bool upperblock, std::string name) const;
+    /**
+     * Create m_numRates matching control and data modes.
+     *
+     * @param fc Mode center frequency offset.
+     */
+    void CreateDualModes(uint32_t fc);
+    /**
+     * Run a parametrized experiment.
+     *
+     * The parameter sets either the number of nodes (if m_doNode is true)
+     * or the \"a\" parameter, which controls the UanMacRcGw MaxReservations
+     * attribute.
+     *
+     * @param param The parameter value.
+     * @return The total number of bytes delivered.
+     */
+    uint32_t Run(uint32_t param);
+
+    /** Default constructor. */
+    Experiment();
+};
+
 Experiment::Experiment()
     : m_simMin(1),
       m_simMax(1),
@@ -237,6 +324,8 @@
     return m_bytesTotal;
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char* argv[])
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/uan/examples/uan-rc-example.h ns-3.47/src/uan/examples/uan-rc-example.h
--- ns-3.46/src/uan/examples/uan-rc-example.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/uan/examples/uan-rc-example.h	1969-12-31 16:00:00.000000000 -0800
@@ -1,102 +0,0 @@
-/*
- * Copyright (c) 2009 University of Washington
- *
- * SPDX-License-Identifier: GPL-2.0-only
- *
- * Author: Leonard Tracy <lentracy@gmail.com>
- */
-
-#ifndef UAN_RC_EXAMPLE_H
-#define UAN_RC_EXAMPLE_H
-
-#include "ns3/network-module.h"
-#include "ns3/stats-module.h"
-#include "ns3/uan-module.h"
-
-using namespace ns3;
-
-/**
- * @ingroup uan
- *
- * Container for the parameters describing a single experiment.
- *
- * An experiment samples a range of parameter values.  The parameter
- * controls the number of nodes (if m_doNode is true), or the
- * UanMacRcGw MaxReservations attribute.
- *
- * @see uan-rc-example.cc
- *
- */
-class Experiment
-{
-  public:
-    uint32_t m_simMin;    //!< Minimum parameter to test.
-    uint32_t m_simMax;    //!< Maximum parameter to test.
-    uint32_t m_simStep;   //!< Amount to increment param per trial
-    uint32_t m_numRates;  //!< Number of divided rates ( (NumberRates+1)%TotalRate should be 0).
-    uint32_t m_totalRate; //!< Total channel capacity.
-    uint32_t m_maxRange;  //!< Maximum range between gateway and acoustic node.
-    uint32_t m_numNodes;  //!< Number of nodes (invalid for m_doNode true).
-    uint32_t m_pktSize;   //!< Packet size in bytes.
-    bool m_doNode;        //!< 1 for do max nodes simulation (invalidates AMin and AMax values).
-    Time m_sifs;          //!< SIFS time duration.
-    Time m_simTime;       //!< Simulation time per trial
-
-    std::string m_gnuplotfile; //!< Filename for GnuPlot.
-
-    uint32_t m_bytesTotal; //!< Total number of bytes received in a simulation run.
-
-    UanModesList m_dataModes;    //!< List of UanTxModes used for data channels.
-    UanModesList m_controlModes; //!< List of UanTxModes used for control channels.
-
-    /**
-     * Callback to receive a packet.
-     *
-     * @param socket The socket receiving packets.
-     */
-    void ReceivePacket(Ptr<Socket> socket);
-    /**
-     * Create a UanTxMode.
-     *
-     * The mode physical rate is set equal to m_totalRate.
-     * The data rate and bandwidth are set to
-     *
-     * \f[{\rm{rate, bandwidth}} = {\rm{kass}}\frac{{{\rm{m\_totalRate}}}}{{{\rm{m\_numRates}} +
-     * 1}}\f]
-     *
-     * The center frequency is set to
-     *
-     * \f[{f_{center}} = {\rm{fc}} + \frac{{ \pm {\rm{m\_totalRate}} \mp {\rm{rate}}}}{2}\f]
-     *
-     * where the upper sign is taken if upperblock is true.
-     *
-     * @param kass Fraction of total bandwidth assigned to mode.
-     * @param fc Mode center frequency offset.
-     * @param upperblock Sign choice in setting the center frequency.
-     * @param name Mode name.
-     * @return The new mode.
-     */
-    UanTxMode CreateMode(uint32_t kass, uint32_t fc, bool upperblock, std::string name) const;
-    /**
-     * Create m_numRates matching control and data modes.
-     *
-     * @param fc Mode center frequency offset.
-     */
-    void CreateDualModes(uint32_t fc);
-    /**
-     * Run a parametrized experiment.
-     *
-     * The parameter sets either the number of nodes (if m_doNode is true)
-     * or the \"a\" parameter, which controls the UanMacRcGw MaxReservations
-     * attribute.
-     *
-     * @param param The parameter value.
-     * @return The total number of bytes delivered.
-     */
-    uint32_t Run(uint32_t param);
-
-    /** Default constructor. */
-    Experiment();
-};
-
-#endif /* UAN_RC_EXAMPLE_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/CMakeLists.txt ns-3.47/src/visualizer/CMakeLists.txt
--- ns-3.46/src/visualizer/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/CMakeLists.txt	2026-02-16 16:08:49.842230800 -0800
@@ -13,13 +13,17 @@
 
 build_lib(
   LIBNAME visualizer
-  SOURCE_FILES model/pyviz.cc
-               model/visual-simulator-impl.cc
-  HEADER_FILES model/pyviz.h
+  SOURCE_FILES
+    model/fast-clipping.cc
+    model/pyviz.cc
+    model/visual-simulator-impl.cc
+  HEADER_FILES model/fast-clipping.h
+               model/pyviz.h
   LIBRARIES_TO_LINK
     ${python_libraries}
     ${libinternet}
     ${libwifi}
+    ${liblr-wpan}
     ${libpoint-to-point}
 )
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/model/fast-clipping.cc ns-3.47/src/visualizer/model/fast-clipping.cc
--- ns-3.46/src/visualizer/model/fast-clipping.cc	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/visualizer/model/fast-clipping.cc	2026-02-16 16:08:49.842230800 -0800
@@ -0,0 +1,629 @@
+/*
+ * Copyright (c) 2008 INESC Porto
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Gustavo Carneiro  <gjc@inescporto.pt>
+ */
+
+#include "fast-clipping.h"
+
+#include <cstdint>
+
+namespace ns3
+{
+namespace visualizer
+{
+
+FastClipping::FastClipping(Vector2 clipMin, Vector2 clipMax)
+    : m_clipMin(clipMin),
+      m_clipMax(clipMax)
+{
+}
+
+void
+FastClipping::ClipStartTop(Line& line) const
+{
+    line.start.x += line.dx * (m_clipMin.y - line.start.y) / line.dy;
+    line.start.y = m_clipMin.y;
+}
+
+void
+FastClipping::ClipStartBottom(Line& line) const
+{
+    line.start.x += line.dx * (m_clipMax.y - line.start.y) / line.dy;
+    line.start.y = m_clipMax.y;
+}
+
+void
+FastClipping::ClipStartRight(Line& line) const
+{
+    line.start.y += line.dy * (m_clipMax.x - line.start.x) / line.dx;
+    line.start.x = m_clipMax.x;
+}
+
+void
+FastClipping::ClipStartLeft(Line& line) const
+{
+    line.start.y += line.dy * (m_clipMin.x - line.start.x) / line.dx;
+    line.start.x = m_clipMin.x;
+}
+
+void
+FastClipping::ClipEndTop(Line& line) const
+{
+    line.end.x += line.dx * (m_clipMin.y - line.end.y) / line.dy;
+    line.end.y = m_clipMin.y;
+}
+
+void
+FastClipping::ClipEndBottom(Line& line) const
+{
+    line.end.x += line.dx * (m_clipMax.y - line.end.y) / line.dy;
+    line.end.y = m_clipMax.y;
+}
+
+void
+FastClipping::ClipEndRight(Line& line) const
+{
+    line.end.y += line.dy * (m_clipMax.x - line.end.x) / line.dx;
+    line.end.x = m_clipMax.x;
+}
+
+void
+FastClipping::ClipEndLeft(Line& line) const
+{
+    line.end.y += line.dy * (m_clipMin.x - line.end.x) / line.dx;
+    line.end.x = m_clipMin.x;
+}
+
+bool
+FastClipping::ClipLine(Line& line)
+{
+    uint8_t lineCode = 0;
+
+    if (line.end.y < m_clipMin.y)
+    {
+        lineCode |= 8;
+    }
+    else if (line.end.y > m_clipMax.y)
+    {
+        lineCode |= 4;
+    }
+
+    if (line.end.x > m_clipMax.x)
+    {
+        lineCode |= 2;
+    }
+    else if (line.end.x < m_clipMin.x)
+    {
+        lineCode |= 1;
+    }
+
+    if (line.start.y < m_clipMin.y)
+    {
+        lineCode |= 128;
+    }
+    else if (line.start.y > m_clipMax.y)
+    {
+        lineCode |= 64;
+    }
+
+    if (line.start.x > m_clipMax.x)
+    {
+        lineCode |= 32;
+    }
+    else if (line.start.x < m_clipMin.x)
+    {
+        lineCode |= 16;
+    }
+
+    // 9 - 8 - A
+    // |   |   |
+    // 1 - 0 - 2
+    // |   |   |
+    // 5 - 4 - 6
+    switch (lineCode)
+    {
+    // center
+    case 0x00:
+        return true;
+
+    case 0x01:
+        ClipEndLeft(line);
+        return true;
+
+    case 0x02:
+        ClipEndRight(line);
+        return true;
+
+    case 0x04:
+        ClipEndBottom(line);
+        return true;
+
+    case 0x05:
+        ClipEndLeft(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        return true;
+
+    case 0x06:
+        ClipEndRight(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        return true;
+
+    case 0x08:
+        ClipEndTop(line);
+        return true;
+
+    case 0x09:
+        ClipEndLeft(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            ClipEndTop(line);
+        }
+        return true;
+
+    case 0x0A:
+        ClipEndRight(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            ClipEndTop(line);
+        }
+        return true;
+
+    // left
+    case 0x10:
+        ClipStartLeft(line);
+        return true;
+
+    case 0x12:
+        ClipStartLeft(line);
+        ClipEndRight(line);
+        return true;
+
+    case 0x14:
+        ClipStartLeft(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipEndBottom(line);
+        return true;
+
+    case 0x16:
+        ClipStartLeft(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipEndBottom(line);
+        if (line.end.x > m_clipMax.x)
+        {
+            ClipEndRight(line);
+        }
+        return true;
+
+    case 0x18:
+        ClipStartLeft(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipEndTop(line);
+        return true;
+
+    case 0x1A:
+        ClipStartLeft(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipEndTop(line);
+        if (line.end.x > m_clipMax.x)
+        {
+            ClipEndRight(line);
+        }
+        return true;
+
+    // right
+    case 0x20:
+        ClipStartRight(line);
+        return true;
+
+    case 0x21:
+        ClipStartRight(line);
+        ClipEndLeft(line);
+        return true;
+
+    case 0x24:
+        ClipStartRight(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipEndBottom(line);
+        return true;
+
+    case 0x25:
+        ClipStartRight(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipEndBottom(line);
+        if (line.end.x < m_clipMin.x)
+        {
+            ClipEndLeft(line);
+        }
+        return true;
+
+    case 0x28:
+        ClipStartRight(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipEndTop(line);
+        return true;
+
+    case 0x29:
+        ClipStartRight(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipEndTop(line);
+        if (line.end.x < m_clipMin.x)
+        {
+            ClipEndLeft(line);
+        }
+        return true;
+
+    // bottom
+    case 0x40:
+        ClipStartBottom(line);
+        return true;
+
+    case 0x41:
+        ClipStartBottom(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            return false;
+        }
+        ClipEndLeft(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        return true;
+
+    case 0x42:
+        ClipStartBottom(line);
+        if (line.start.x > m_clipMax.x)
+        {
+            return false;
+        }
+        ClipEndRight(line);
+        return true;
+
+    case 0x48:
+        ClipStartBottom(line);
+        ClipEndTop(line);
+        return true;
+
+    case 0x49:
+        ClipStartBottom(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            return false;
+        }
+        ClipEndLeft(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            ClipEndTop(line);
+        }
+        return true;
+
+    case 0x4A:
+        ClipStartBottom(line);
+        if (line.start.x > m_clipMax.x)
+        {
+            return false;
+        }
+        ClipEndRight(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            ClipEndTop(line);
+        }
+        return true;
+
+    // bottom-left
+    case 0x50:
+        ClipStartLeft(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            ClipStartBottom(line);
+        }
+        return true;
+
+    case 0x52:
+        ClipEndRight(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipStartBottom(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            ClipStartLeft(line);
+        }
+        return true;
+
+    case 0x58:
+        ClipEndTop(line);
+        if (line.end.x < m_clipMin.x)
+        {
+            return false;
+        }
+        ClipStartBottom(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            ClipStartLeft(line);
+        }
+        return true;
+
+    case 0x5A:
+        ClipStartLeft(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipEndRight(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            return false;
+        }
+        if (line.start.y > m_clipMax.y)
+        {
+            ClipStartBottom(line);
+        }
+        if (line.end.y < m_clipMin.y)
+        {
+            ClipEndTop(line);
+        }
+        return true;
+
+    // bottom-right
+    case 0x60:
+        ClipStartRight(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            ClipStartBottom(line);
+        }
+        return true;
+
+    case 0x61:
+        ClipEndLeft(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipStartBottom(line);
+        if (line.start.x > m_clipMax.x)
+        {
+            ClipStartRight(line);
+        }
+        return true;
+
+    case 0x68:
+        ClipEndTop(line);
+        if (line.end.x > m_clipMax.x)
+        {
+            return false;
+        }
+        ClipStartRight(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            ClipStartBottom(line);
+        }
+        return true;
+
+    case 0x69:
+        ClipEndLeft(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipStartRight(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            return false;
+        }
+        if (line.end.y < m_clipMin.y)
+        {
+            ClipEndTop(line);
+        }
+        if (line.start.y > m_clipMax.y)
+        {
+            ClipStartBottom(line);
+        }
+        return true;
+
+    // top
+    case 0x80:
+        ClipStartTop(line);
+        return true;
+
+    case 0x81:
+        ClipStartTop(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            return false;
+        }
+        ClipEndLeft(line);
+        return true;
+
+    case 0x82:
+        ClipStartTop(line);
+        if (line.start.x > m_clipMax.x)
+        {
+            return false;
+        }
+        ClipEndRight(line);
+        return true;
+
+    case 0x84:
+        ClipStartTop(line);
+        ClipEndBottom(line);
+        return true;
+
+    case 0x85:
+        ClipStartTop(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            return false;
+        }
+        ClipEndLeft(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        return true;
+
+    case 0x86:
+        ClipStartTop(line);
+        if (line.start.x > m_clipMax.x)
+        {
+            return false;
+        }
+        ClipEndRight(line);
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        return true;
+
+    // top-left
+    case 0x90:
+        ClipStartLeft(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            ClipStartTop(line);
+        }
+        return true;
+
+    case 0x92:
+        ClipEndRight(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipStartTop(line);
+        if (line.start.x < m_clipMin.x)
+        {
+            ClipStartLeft(line);
+        }
+        return true;
+
+    case 0x94:
+        ClipEndBottom(line);
+        if (line.end.x < m_clipMin.x)
+        {
+            return false;
+        }
+        ClipStartLeft(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            ClipStartTop(line);
+        }
+        return true;
+
+    case 0x96:
+        ClipStartLeft(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            return false;
+        }
+        ClipEndRight(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            return false;
+        }
+        if (line.start.y < m_clipMin.y)
+        {
+            ClipStartTop(line);
+        }
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        return true;
+
+    // top-right
+    case 0xA0:
+        ClipStartRight(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            ClipStartTop(line);
+        }
+        return true;
+
+    case 0xA1:
+        ClipEndLeft(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipStartTop(line);
+        if (line.start.x > m_clipMax.x)
+        {
+            ClipStartRight(line);
+        }
+        return true;
+
+    case 0xA4:
+        ClipEndBottom(line);
+        if (line.end.x > m_clipMax.x)
+        {
+            return false;
+        }
+        ClipStartRight(line);
+        if (line.start.y < m_clipMin.y)
+        {
+            ClipStartTop(line);
+        }
+        return true;
+
+    case 0xA5:
+        ClipEndLeft(line);
+        if (line.end.y < m_clipMin.y)
+        {
+            return false;
+        }
+        ClipStartRight(line);
+        if (line.start.y > m_clipMax.y)
+        {
+            return false;
+        }
+        if (line.end.y > m_clipMax.y)
+        {
+            ClipEndBottom(line);
+        }
+        if (line.start.y < m_clipMin.y)
+        {
+            ClipStartTop(line);
+        }
+        return true;
+    }
+
+    return false;
+}
+
+} // namespace visualizer
+} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/model/fast-clipping.h ns-3.47/src/visualizer/model/fast-clipping.h
--- ns-3.46/src/visualizer/model/fast-clipping.h	1969-12-31 16:00:00.000000000 -0800
+++ ns-3.47/src/visualizer/model/fast-clipping.h	2026-02-16 16:08:49.842230800 -0800
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2008 INESC Porto
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * Author: Gustavo Carneiro  <gjc@inescporto.pt>
+ */
+
+namespace ns3
+{
+
+namespace visualizer
+{
+/**
+ * @ingroup Visualizer
+ *
+ * This class is used by the visualizer for the process of removing lines or portions of
+ * lines outside of an area of interest.
+ *
+ * This code has been adapted from
+ * http://en.wikipedia.org/w/index.php?title=Line_clipping&oldid=248609574
+ *
+ **/
+class FastClipping
+{
+  public:
+    /**
+     * @ingroup Visualizer
+     *
+     * The Vector 2 struct
+     */
+    struct Vector2
+    {
+        double x; //!< X coordinate.
+        double y; //!< Y coordinate.
+    };
+
+    /**
+     * @ingroup Visualizer
+     *
+     * The line struct
+     */
+    struct Line
+    {
+        Vector2 start; //!< The start point of the line.
+        Vector2 end;   //!< The end point of the line.
+        double dx;     //!< dX
+        double dy;     //!< dY
+    };
+
+    /**
+     * Constructor
+     *
+     * @param clipMin minimum clipping vector
+     * @param clipMax maximum clipping vector
+     */
+    FastClipping(Vector2 clipMin, Vector2 clipMax);
+
+    /**
+     * Clip line function
+     *
+     * @param line the clip line
+     * @returns true if clipped
+     */
+    bool ClipLine(Line& line);
+
+  private:
+    /**
+     * Clip start top function
+     *
+     * @param line the clip line
+     */
+    void ClipStartTop(Line& line) const;
+
+    /**
+     * Clip start bottom function
+     *
+     * @param line the clip line
+     */
+    void ClipStartBottom(Line& line) const;
+
+    /**
+     * Clip start right function
+     *
+     * @param line the clip line
+     */
+    void ClipStartRight(Line& line) const;
+
+    /**
+     * Clip start left function
+     *
+     * @param line the clip line
+     */
+    void ClipStartLeft(Line& line) const;
+
+    /**
+     * Clip end top function
+     *
+     * @param line the clip line
+     */
+    void ClipEndTop(Line& line) const;
+
+    /**
+     * Clip end bottom function
+     *
+     * @param line the clip line
+     */
+    void ClipEndBottom(Line& line) const;
+
+    /**
+     * Clip end right function
+     *
+     * @param line the clip line
+     */
+    void ClipEndRight(Line& line) const;
+
+    /**
+     * Clip end left function
+     *
+     * @param line the clip line
+     */
+    void ClipEndLeft(Line& line) const;
+
+    Vector2 m_clipMin; //!< The minimum point of the bounding area required clipping.
+    Vector2 m_clipMax; //!< The maximum point of the bounding area required clipping.
+};
+
+} // namespace visualizer
+} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/model/pyviz.cc ns-3.47/src/visualizer/model/pyviz.cc
--- ns-3.46/src/visualizer/model/pyviz.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/model/pyviz.cc	2026-02-16 16:08:49.842230800 -0800
@@ -8,12 +8,15 @@
 
 #include "pyviz.h"
 
+#include "fast-clipping.h"
 #include "visual-simulator-impl.h"
 
 #include "ns3/abort.h"
 #include "ns3/config.h"
 #include "ns3/ethernet-header.h"
 #include "ns3/log.h"
+#include "ns3/lr-wpan-mac-header.h"
+#include "ns3/lr-wpan-net-device.h"
 #include "ns3/node-list.h"
 #include "ns3/ppp-header.h"
 #include "ns3/simulator.h"
@@ -23,9 +26,9 @@
 #include <cstdlib>
 #include <sstream>
 
-NS_LOG_COMPONENT_DEFINE("PyViz");
+using namespace ns3::lrwpan;
 
-#define NUM_LAST_PACKETS 10
+NS_LOG_COMPONENT_DEFINE("PyViz");
 
 static std::vector<std::string>
 PathSplit(std::string str)
@@ -50,6 +53,9 @@
 namespace ns3
 {
 
+namespace visualizer
+{
+
 static PyViz* g_visualizer = nullptr; ///< the visualizer
 
 /**
@@ -75,7 +81,8 @@
 TypeId
 PyVizPacketTag::GetTypeId()
 {
-    static TypeId tid = TypeId("ns3::PyVizPacketTag")
+    static TypeId tid = TypeId("ns3::visualizer::PyVizPacketTag")
+                            .AddDeprecatedName("ns3::PyVizPacketTag")
                             .SetParent<Tag>()
                             .SetGroupName("Visualizer")
                             .AddConstructor<PyVizPacketTag>();
@@ -129,6 +136,12 @@
 
     Config::ConnectFailSafe("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/Mac/MacRx",
                             MakeCallback(&PyViz::TraceNetDevRxWifi, this));
+    // Lr-Wpan
+    Config::ConnectFailSafe("/NodeList/*/DeviceList/*/$ns3::lrwpan::LrWpanNetDevice/Mac/MacTx",
+                            MakeCallback(&PyViz::TraceNetDevTxLrWpan, this));
+
+    Config::ConnectFailSafe("/NodeList/*/DeviceList/*/$ns3::lrwpan::LrWpanNetDevice/Mac/MacRx",
+                            MakeCallback(&PyViz::TraceNetDevRxLrWpan, this));
 
     // CSMA
     Config::ConnectFailSafe("/NodeList/*/DeviceList/*/$ns3::CsmaNetDevice/MacTx",
@@ -263,7 +276,7 @@
     NS_LOG_FUNCTION_NOARGS();
     if (m_runUntil <= Simulator::Now())
     {
-        Simulator::Stop(Seconds(0)); // Stop right now
+        Simulator::Stop(); // Stop right now
         m_stop = true;
     }
 }
@@ -309,6 +322,7 @@
     {
         return;
     }
+
     // Schedule a dummy callback function for the target time, to make
     // sure we stop at the right time.  Otherwise, simulations with few
     // events just appear to "jump" big chunks of time.
@@ -332,6 +346,14 @@
     }
 }
 
+Time
+PyViz::GetSimulatorStopTime()
+{
+    Ptr<SimulatorImpl> impl = Simulator::GetImplementation();
+    Ptr<VisualSimulatorImpl> visualImpl = DynamicCast<VisualSimulatorImpl>(impl);
+    return visualImpl->GetStopTime();
+}
+
 bool
 PyViz::TransmissionSampleKey::operator<(const PyViz::TransmissionSampleKey& other) const
 {
@@ -362,7 +384,7 @@
     return retval;
 }
 
-PyViz::NetDeviceStatistics&
+NetDeviceStatistics&
 PyViz::FindNetDeviceStatistics(int node, int interface)
 {
     auto nodeStatsIter = m_nodesStatistics.find(node);
@@ -500,9 +522,10 @@
 // --------- TX device tracing -------------------
 
 void
-PyViz::TraceNetDevTxCommon(const std::string& context,
-                           Ptr<const Packet> packet,
-                           const Mac48Address& destinationAddress)
+PyViz::TraceNetDevTxCommon(
+    const std::string& context,
+    Ptr<const Packet> packet,
+    const std::variant<Mac16Address, Mac48Address, Mac64Address>& destination)
 {
     NS_LOG_FUNCTION(context << packet->GetUid() << *packet);
 
@@ -527,7 +550,8 @@
         lastPacket.time = Simulator::Now();
         lastPacket.packet = packet->Copy();
         lastPacket.device = device;
-        lastPacket.to = destinationAddress;
+        lastPacket.to = destination;
+
         last.lastTransmittedPackets.push_back(lastPacket);
         while (last.lastTransmittedPackets.size() > captureOptions->numLastPackets)
         {
@@ -554,9 +578,29 @@
     }
 
     TxRecordValue record = {Simulator::Now(), node, false};
-    if (destinationAddress == device->GetBroadcast())
+
+    if (std::holds_alternative<Mac16Address>(destination))
+    {
+        if (std::get<Mac16Address>(destination) == Mac16Address("FF:FF"))
+        {
+            record.isBroadcast = true;
+        }
+    }
+    else if (std::holds_alternative<Mac48Address>(destination))
     {
-        record.isBroadcast = true;
+        if (std::get<Mac48Address>(destination) == device->GetBroadcast())
+        {
+            record.isBroadcast = true;
+        }
+    }
+    else if (std::holds_alternative<Mac64Address>(destination))
+    {
+        if (std::get<Mac64Address>(destination) == Mac64Address("FF:FF:FF:FF:FF:FF:FF:FF"))
+        {
+            // Note: A broadcast using th the MAC 64 bit address is not really used in practice.
+            // instead the 16 bit MAC address is used.
+            record.isBroadcast = true;
+        }
     }
 
     m_txRecords[TxRecordKey(device->GetChannel(), packet->GetUid())] = record;
@@ -582,7 +626,8 @@
      */
     WifiMacHeader hdr;
     NS_ABORT_IF(packet->PeekHeader(hdr) == 0);
-    Mac48Address destinationAddress;
+
+    std::variant<Mac16Address, Mac48Address, Mac64Address> destinationAddress;
     if (hdr.IsToDs())
     {
         destinationAddress = hdr.GetAddr3();
@@ -595,17 +640,41 @@
 }
 
 void
+PyViz::TraceNetDevTxLrWpan(std::string context, Ptr<const Packet> packet)
+{
+    LrWpanMacHeader hdr;
+    NS_ABORT_IF(packet->PeekHeader(hdr) == 0);
+    std::variant<Mac16Address, Mac48Address, Mac64Address> destinationAddress;
+
+    switch (hdr.GetDstAddrMode())
+    {
+    case lrwpan::AddressMode::EXT_ADDR:
+        destinationAddress = hdr.GetExtDstAddr();
+        break;
+    default:
+        destinationAddress = hdr.GetShortDstAddr();
+        break;
+    }
+
+    TraceNetDevTxCommon(context, packet, destinationAddress);
+}
+
+void
 PyViz::TraceNetDevTxCsma(std::string context, Ptr<const Packet> packet)
 {
     EthernetHeader ethernetHeader;
     NS_ABORT_IF(packet->PeekHeader(ethernetHeader) == 0);
-    TraceNetDevTxCommon(context, packet, ethernetHeader.GetDestination());
+    std::variant<Mac16Address, Mac48Address, Mac64Address> destinationAddress;
+    destinationAddress = ethernetHeader.GetDestination();
+    TraceNetDevTxCommon(context, packet, destinationAddress);
 }
 
 void
 PyViz::TraceNetDevTxPointToPoint(std::string context, Ptr<const Packet> packet)
 {
-    TraceNetDevTxCommon(context, packet, Mac48Address());
+    std::variant<Mac16Address, Mac48Address, Mac64Address> destinationAddress;
+    destinationAddress = Mac48Address();
+    TraceNetDevTxCommon(context, packet, destinationAddress);
 }
 
 // --------- RX device tracing -------------------
@@ -613,7 +682,7 @@
 void
 PyViz::TraceNetDevRxCommon(const std::string& context,
                            Ptr<const Packet> packet,
-                           const Mac48Address& from)
+                           const std::variant<Mac16Address, Mac48Address, Mac64Address>& source)
 {
     uint32_t uid;
     PyVizPacketTag tag;
@@ -651,7 +720,8 @@
         lastPacket.time = Simulator::Now();
         lastPacket.packet = packet->Copy();
         lastPacket.device = device;
-        lastPacket.from = from;
+        lastPacket.from = source;
+
         last.lastReceivedPackets.push_back(lastPacket);
         while (last.lastReceivedPackets.size() > captureOptions->numLastPackets)
         {
@@ -742,7 +812,7 @@
      */
     WifiMacHeader hdr;
     NS_ABORT_IF(packet->PeekHeader(hdr) == 0);
-    Mac48Address sourceAddress;
+    std::variant<Mac16Address, Mac48Address, Mac64Address> sourceAddress;
     if (!hdr.IsFromDs())
     {
         sourceAddress = hdr.GetAddr2();
@@ -760,17 +830,40 @@
 }
 
 void
+PyViz::TraceNetDevRxLrWpan(std::string context, Ptr<const Packet> packet)
+{
+    LrWpanMacHeader hdr;
+    NS_ABORT_IF(packet->PeekHeader(hdr) == 0);
+    std::variant<Mac16Address, Mac48Address, Mac64Address> sourceAddress;
+    switch (hdr.GetSrcAddrMode())
+    {
+    case lrwpan::AddressMode::EXT_ADDR:
+        sourceAddress = hdr.GetExtSrcAddr();
+        break;
+    default:
+        sourceAddress = hdr.GetShortSrcAddr();
+        break;
+    }
+
+    TraceNetDevRxCommon(context, packet, sourceAddress);
+}
+
+void
 PyViz::TraceNetDevRxCsma(std::string context, Ptr<const Packet> packet)
 {
     EthernetHeader ethernetHeader;
     NS_ABORT_IF(packet->PeekHeader(ethernetHeader) == 0);
-    TraceNetDevRxCommon(context, packet, ethernetHeader.GetSource());
+    std::variant<Mac16Address, Mac48Address, Mac64Address> sourceAddress;
+    sourceAddress = ethernetHeader.GetSource();
+    TraceNetDevRxCommon(context, packet, sourceAddress);
 }
 
 void
 PyViz::TraceNetDevRxPointToPoint(std::string context, Ptr<const Packet> packet)
 {
-    TraceNetDevRxCommon(context, packet, Mac48Address());
+    std::variant<Mac16Address, Mac48Address, Mac64Address> sourceAddress;
+    sourceAddress = Mac48Address();
+    TraceNetDevRxCommon(context, packet, sourceAddress);
 }
 
 void
@@ -785,7 +878,9 @@
     // TraceNetDevRxCsma; we don't want to receive them twice.
     if (packetType == NetDevice::PACKET_OTHERHOST)
     {
-        TraceNetDevRxCommon(context, packet, ethernetHeader.GetDestination());
+        std::variant<Mac16Address, Mac48Address, Mac64Address> destinationAddress;
+        destinationAddress = ethernetHeader.GetDestination();
+        TraceNetDevRxCommon(context, packet, destinationAddress);
     }
 }
 
@@ -795,19 +890,23 @@
                         const Mac48Address& destination)
 {
     NS_LOG_FUNCTION(context);
-    TraceNetDevTxCommon(context, packet, destination);
+    std::variant<Mac16Address, Mac48Address, Mac64Address> destinationAddress;
+    destinationAddress = destination;
+    TraceNetDevTxCommon(context, packet, destinationAddress);
 }
 
 void
 PyViz::TraceNetDevRxLte(std::string context, Ptr<const Packet> packet, const Mac48Address& source)
 {
     NS_LOG_FUNCTION(context);
-    TraceNetDevRxCommon(context, packet, source);
+    std::variant<Mac16Address, Mac48Address, Mac64Address> sourceAddress;
+    sourceAddress = source;
+    TraceNetDevRxCommon(context, packet, sourceAddress);
 }
 
 // ---------------------
 
-PyViz::TransmissionSampleList
+TransmissionSampleList
 PyViz::GetTransmissionSamples() const
 {
     NS_LOG_DEBUG("GetTransmissionSamples BEGIN");
@@ -827,7 +926,7 @@
     return list;
 }
 
-PyViz::PacketDropSampleList
+PacketDropSampleList
 PyViz::GetPacketDropSamples() const
 {
     NS_LOG_DEBUG("GetPacketDropSamples BEGIN");
@@ -851,10 +950,10 @@
     m_nodesOfInterest = nodes;
 }
 
-std::vector<PyViz::NodeStatistics>
+std::vector<NodeStatistics>
 PyViz::GetNodesStatistics() const
 {
-    std::vector<PyViz::NodeStatistics> retval;
+    std::vector<NodeStatistics> retval;
     for (auto iter = m_nodesStatistics.begin(); iter != m_nodesStatistics.end(); iter++)
     {
         NodeStatistics stats = {iter->first, iter->second};
@@ -863,7 +962,7 @@
     return retval;
 }
 
-PyViz::LastPacketsSample
+LastPacketsSample
 PyViz::GetLastPackets(uint32_t nodeId) const
 {
     NS_LOG_DEBUG("GetLastPackets: " << nodeId);
@@ -879,679 +978,6 @@
     }
 }
 
-namespace
-{
-/// Adapted from http://en.wikipedia.org/w/index.php?title=Line_clipping&oldid=248609574
-class FastClipping
-{
-  public:
-    /// Vector2 structure
-    struct Vector2
-    {
-        double x; ///< X
-        double y; ///< Y
-    };
-
-    Vector2 m_clipMin; ///< clip minimum
-    Vector2 m_clipMax; ///< clip maximum
-
-    /// Line structure
-    struct Line
-    {
-        Vector2 start; ///< start
-        Vector2 end;   ///<  end
-        double dx;     ///< dX
-        double dy;     ///< dY
-    };
-
-  private:
-    /**
-     * Clip start top function
-     * @param line the clip line
-     */
-    void ClipStartTop(Line& line) const
-    {
-        line.start.x += line.dx * (m_clipMin.y - line.start.y) / line.dy;
-        line.start.y = m_clipMin.y;
-    }
-
-    /**
-     * Clip start bottom function
-     * @param line the clip line
-     */
-    void ClipStartBottom(Line& line) const
-    {
-        line.start.x += line.dx * (m_clipMax.y - line.start.y) / line.dy;
-        line.start.y = m_clipMax.y;
-    }
-
-    /**
-     * Clip start right function
-     * @param line the clip line
-     */
-    void ClipStartRight(Line& line) const
-    {
-        line.start.y += line.dy * (m_clipMax.x - line.start.x) / line.dx;
-        line.start.x = m_clipMax.x;
-    }
-
-    /**
-     * Clip start left function
-     * @param line the clip line
-     */
-    void ClipStartLeft(Line& line) const
-    {
-        line.start.y += line.dy * (m_clipMin.x - line.start.x) / line.dx;
-        line.start.x = m_clipMin.x;
-    }
-
-    /**
-     * Clip end top function
-     * @param line the clip line
-     */
-    void ClipEndTop(Line& line) const
-    {
-        line.end.x += line.dx * (m_clipMin.y - line.end.y) / line.dy;
-        line.end.y = m_clipMin.y;
-    }
-
-    /**
-     * Clip end bottom function
-     * @param line the clip line
-     */
-    void ClipEndBottom(Line& line) const
-    {
-        line.end.x += line.dx * (m_clipMax.y - line.end.y) / line.dy;
-        line.end.y = m_clipMax.y;
-    }
-
-    /**
-     * Clip end right function
-     * @param line the clip line
-     */
-    void ClipEndRight(Line& line) const
-    {
-        line.end.y += line.dy * (m_clipMax.x - line.end.x) / line.dx;
-        line.end.x = m_clipMax.x;
-    }
-
-    /**
-     * Clip end left function
-     * @param line the clip line
-     */
-    void ClipEndLeft(Line& line) const
-    {
-        line.end.y += line.dy * (m_clipMin.x - line.end.x) / line.dx;
-        line.end.x = m_clipMin.x;
-    }
-
-  public:
-    /**
-     * Constructor
-     *
-     * @param clipMin minimum clipping vector
-     * @param clipMax maximum clipping vector
-     */
-    FastClipping(Vector2 clipMin, Vector2 clipMax)
-        : m_clipMin(clipMin),
-          m_clipMax(clipMax)
-    {
-    }
-
-    /**
-     * Clip line function
-     * @param line the clip line
-     * @returns true if clipped
-     */
-    bool ClipLine(Line& line)
-    {
-        uint8_t lineCode = 0;
-
-        if (line.end.y < m_clipMin.y)
-        {
-            lineCode |= 8;
-        }
-        else if (line.end.y > m_clipMax.y)
-        {
-            lineCode |= 4;
-        }
-
-        if (line.end.x > m_clipMax.x)
-        {
-            lineCode |= 2;
-        }
-        else if (line.end.x < m_clipMin.x)
-        {
-            lineCode |= 1;
-        }
-
-        if (line.start.y < m_clipMin.y)
-        {
-            lineCode |= 128;
-        }
-        else if (line.start.y > m_clipMax.y)
-        {
-            lineCode |= 64;
-        }
-
-        if (line.start.x > m_clipMax.x)
-        {
-            lineCode |= 32;
-        }
-        else if (line.start.x < m_clipMin.x)
-        {
-            lineCode |= 16;
-        }
-
-        // 9 - 8 - A
-        // |   |   |
-        // 1 - 0 - 2
-        // |   |   |
-        // 5 - 4 - 6
-        switch (lineCode)
-        {
-        // center
-        case 0x00:
-            return true;
-
-        case 0x01:
-            ClipEndLeft(line);
-            return true;
-
-        case 0x02:
-            ClipEndRight(line);
-            return true;
-
-        case 0x04:
-            ClipEndBottom(line);
-            return true;
-
-        case 0x05:
-            ClipEndLeft(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            return true;
-
-        case 0x06:
-            ClipEndRight(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            return true;
-
-        case 0x08:
-            ClipEndTop(line);
-            return true;
-
-        case 0x09:
-            ClipEndLeft(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                ClipEndTop(line);
-            }
-            return true;
-
-        case 0x0A:
-            ClipEndRight(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                ClipEndTop(line);
-            }
-            return true;
-
-        // left
-        case 0x10:
-            ClipStartLeft(line);
-            return true;
-
-        case 0x12:
-            ClipStartLeft(line);
-            ClipEndRight(line);
-            return true;
-
-        case 0x14:
-            ClipStartLeft(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipEndBottom(line);
-            return true;
-
-        case 0x16:
-            ClipStartLeft(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipEndBottom(line);
-            if (line.end.x > m_clipMax.x)
-            {
-                ClipEndRight(line);
-            }
-            return true;
-
-        case 0x18:
-            ClipStartLeft(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipEndTop(line);
-            return true;
-
-        case 0x1A:
-            ClipStartLeft(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipEndTop(line);
-            if (line.end.x > m_clipMax.x)
-            {
-                ClipEndRight(line);
-            }
-            return true;
-
-        // right
-        case 0x20:
-            ClipStartRight(line);
-            return true;
-
-        case 0x21:
-            ClipStartRight(line);
-            ClipEndLeft(line);
-            return true;
-
-        case 0x24:
-            ClipStartRight(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipEndBottom(line);
-            return true;
-
-        case 0x25:
-            ClipStartRight(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipEndBottom(line);
-            if (line.end.x < m_clipMin.x)
-            {
-                ClipEndLeft(line);
-            }
-            return true;
-
-        case 0x28:
-            ClipStartRight(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipEndTop(line);
-            return true;
-
-        case 0x29:
-            ClipStartRight(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipEndTop(line);
-            if (line.end.x < m_clipMin.x)
-            {
-                ClipEndLeft(line);
-            }
-            return true;
-
-        // bottom
-        case 0x40:
-            ClipStartBottom(line);
-            return true;
-
-        case 0x41:
-            ClipStartBottom(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                return false;
-            }
-            ClipEndLeft(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            return true;
-
-        case 0x42:
-            ClipStartBottom(line);
-            if (line.start.x > m_clipMax.x)
-            {
-                return false;
-            }
-            ClipEndRight(line);
-            return true;
-
-        case 0x48:
-            ClipStartBottom(line);
-            ClipEndTop(line);
-            return true;
-
-        case 0x49:
-            ClipStartBottom(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                return false;
-            }
-            ClipEndLeft(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                ClipEndTop(line);
-            }
-            return true;
-
-        case 0x4A:
-            ClipStartBottom(line);
-            if (line.start.x > m_clipMax.x)
-            {
-                return false;
-            }
-            ClipEndRight(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                ClipEndTop(line);
-            }
-            return true;
-
-        // bottom-left
-        case 0x50:
-            ClipStartLeft(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                ClipStartBottom(line);
-            }
-            return true;
-
-        case 0x52:
-            ClipEndRight(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipStartBottom(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                ClipStartLeft(line);
-            }
-            return true;
-
-        case 0x58:
-            ClipEndTop(line);
-            if (line.end.x < m_clipMin.x)
-            {
-                return false;
-            }
-            ClipStartBottom(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                ClipStartLeft(line);
-            }
-            return true;
-
-        case 0x5A:
-            ClipStartLeft(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipEndRight(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                return false;
-            }
-            if (line.start.y > m_clipMax.y)
-            {
-                ClipStartBottom(line);
-            }
-            if (line.end.y < m_clipMin.y)
-            {
-                ClipEndTop(line);
-            }
-            return true;
-
-        // bottom-right
-        case 0x60:
-            ClipStartRight(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                ClipStartBottom(line);
-            }
-            return true;
-
-        case 0x61:
-            ClipEndLeft(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipStartBottom(line);
-            if (line.start.x > m_clipMax.x)
-            {
-                ClipStartRight(line);
-            }
-            return true;
-
-        case 0x68:
-            ClipEndTop(line);
-            if (line.end.x > m_clipMax.x)
-            {
-                return false;
-            }
-            ClipStartRight(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                ClipStartBottom(line);
-            }
-            return true;
-
-        case 0x69:
-            ClipEndLeft(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipStartRight(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                return false;
-            }
-            if (line.end.y < m_clipMin.y)
-            {
-                ClipEndTop(line);
-            }
-            if (line.start.y > m_clipMax.y)
-            {
-                ClipStartBottom(line);
-            }
-            return true;
-
-        // top
-        case 0x80:
-            ClipStartTop(line);
-            return true;
-
-        case 0x81:
-            ClipStartTop(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                return false;
-            }
-            ClipEndLeft(line);
-            return true;
-
-        case 0x82:
-            ClipStartTop(line);
-            if (line.start.x > m_clipMax.x)
-            {
-                return false;
-            }
-            ClipEndRight(line);
-            return true;
-
-        case 0x84:
-            ClipStartTop(line);
-            ClipEndBottom(line);
-            return true;
-
-        case 0x85:
-            ClipStartTop(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                return false;
-            }
-            ClipEndLeft(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            return true;
-
-        case 0x86:
-            ClipStartTop(line);
-            if (line.start.x > m_clipMax.x)
-            {
-                return false;
-            }
-            ClipEndRight(line);
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            return true;
-
-        // top-left
-        case 0x90:
-            ClipStartLeft(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                ClipStartTop(line);
-            }
-            return true;
-
-        case 0x92:
-            ClipEndRight(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipStartTop(line);
-            if (line.start.x < m_clipMin.x)
-            {
-                ClipStartLeft(line);
-            }
-            return true;
-
-        case 0x94:
-            ClipEndBottom(line);
-            if (line.end.x < m_clipMin.x)
-            {
-                return false;
-            }
-            ClipStartLeft(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                ClipStartTop(line);
-            }
-            return true;
-
-        case 0x96:
-            ClipStartLeft(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                return false;
-            }
-            ClipEndRight(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                return false;
-            }
-            if (line.start.y < m_clipMin.y)
-            {
-                ClipStartTop(line);
-            }
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            return true;
-
-        // top-right
-        case 0xA0:
-            ClipStartRight(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                ClipStartTop(line);
-            }
-            return true;
-
-        case 0xA1:
-            ClipEndLeft(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipStartTop(line);
-            if (line.start.x > m_clipMax.x)
-            {
-                ClipStartRight(line);
-            }
-            return true;
-
-        case 0xA4:
-            ClipEndBottom(line);
-            if (line.end.x > m_clipMax.x)
-            {
-                return false;
-            }
-            ClipStartRight(line);
-            if (line.start.y < m_clipMin.y)
-            {
-                ClipStartTop(line);
-            }
-            return true;
-
-        case 0xA5:
-            ClipEndLeft(line);
-            if (line.end.y < m_clipMin.y)
-            {
-                return false;
-            }
-            ClipStartRight(line);
-            if (line.start.y > m_clipMax.y)
-            {
-                return false;
-            }
-            if (line.end.y > m_clipMax.y)
-            {
-                ClipEndBottom(line);
-            }
-            if (line.start.y < m_clipMin.y)
-            {
-                ClipStartTop(line);
-            }
-            return true;
-        }
-
-        return false;
-    }
-};
-} // namespace
-
 void
 PyViz::LineClipping(double boundsX1,
                     double boundsY1,
@@ -1577,4 +1003,5 @@
     lineY2 = line.end.y;
 }
 
+} // namespace visualizer
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/model/pyviz.h ns-3.47/src/visualizer/model/pyviz.h
--- ns-3.46/src/visualizer/model/pyviz.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/model/pyviz.h	2026-02-16 16:08:49.842230800 -0800
@@ -15,16 +15,131 @@
 #include "ns3/event-id.h"
 #include "ns3/ipv4-header.h"
 #include "ns3/ipv4-l3-protocol.h"
+#include "ns3/mac16-address.h"
 #include "ns3/mac48-address.h"
+#include "ns3/mac64-address.h"
 #include "ns3/node.h"
 #include "ns3/nstime.h"
 #include "ns3/packet.h"
 
 #include <map>
 #include <set>
+#include <variant>
 
 namespace ns3
 {
+namespace visualizer
+{
+
+/**
+ * TransmissionSample structure
+ */
+struct TransmissionSample
+{
+    Ptr<Node> transmitter; //!< The source node of the transmission
+    Ptr<Node> receiver;    //!< The destination node of the transmission (Null if broadcast)
+    Ptr<Channel> channel;  //!< The channel reference used for the transmission
+    uint32_t bytes;        //!< The number of bytes transmitted
+};
+
+/**
+ * PacketDropSample structure
+ */
+struct PacketDropSample
+{
+    Ptr<Node> transmitter; //!< The transmitter node where the drop was registered
+    uint32_t bytes;        //!< The number of bytes dropped
+};
+
+/**
+ * PacketSample structure.
+ */
+struct PacketSample
+{
+    Time time;             //!< The received or transmitted time of the sample.
+    Ptr<Packet> packet;    //!< The packet reference
+    Ptr<NetDevice> device; //!< The NetDevice reference
+};
+
+/**
+ * TxPacketSample structure
+ */
+struct TxPacketSample : PacketSample
+{
+    std::variant<Mac16Address, Mac48Address, Mac64Address> to; //!< The destination MAC address
+};
+
+/**
+ *  RxPacketSample structure
+ */
+struct RxPacketSample : PacketSample
+{
+    std::variant<Mac16Address, Mac48Address, Mac64Address> from; //!< The source MAC address
+};
+
+/**
+ *  Structure to handle a sample of the last received, transmitted or drop packets
+ *  in a node.
+ */
+struct LastPacketsSample
+{
+    std::vector<RxPacketSample> lastReceivedPackets;    //!< Last received packets
+    std::vector<TxPacketSample> lastTransmittedPackets; //!< Last transmitted packets
+    std::vector<PacketSample> lastDroppedPackets;       //!< Last dropped packets
+};
+
+/**
+ * The NetDeviceStatistics structure
+ */
+struct NetDeviceStatistics
+{
+    NetDeviceStatistics()
+        : transmittedBytes(0),
+          receivedBytes(0),
+          transmittedPackets(0),
+          receivedPackets(0)
+    {
+    }
+
+    uint64_t transmittedBytes;   //!< Transmitted bytes
+    uint64_t receivedBytes;      //!< Received bytes
+    uint32_t transmittedPackets; //!< Transmitted packets
+    uint32_t receivedPackets;    //!< Received packets
+};
+
+/**
+ * The NodeStatistics structure
+ */
+struct NodeStatistics
+{
+    uint32_t nodeId;                             //!<  Node ID
+    std::vector<NetDeviceStatistics> statistics; //!<  Statistics
+};
+
+/**
+ * The PacketCaptureMode enumeration
+ */
+enum PacketCaptureMode
+{
+    PACKET_CAPTURE_DISABLED = 1,       //!< Packet capture is disabled
+    PACKET_CAPTURE_FILTER_HEADERS_OR,  //!< Packet capture if any of the indicated headers is
+                                       //!< present
+    PACKET_CAPTURE_FILTER_HEADERS_AND, //!< Packet capture if all of the indicated headers are
+                                       //!< present
+};
+
+/**
+ * The PacketCaptureOptions structure
+ */
+struct PacketCaptureOptions
+{
+    std::set<TypeId> headers; //!< The headers
+    uint32_t numLastPackets;  //!< Number of last packets
+    PacketCaptureMode mode;   //!< The packet capture node
+};
+
+using TransmissionSampleList = std::vector<TransmissionSample>; //!< The transmission sample list
+using PacketDropSampleList = std::vector<PacketDropSample>;     //!< The packet drop list
 
 /**
  * @ingroup visualizer
@@ -44,172 +159,105 @@
     ~PyViz();
 
     /**
-     * Register drop trace path function
-     * @param tracePath the path to trace
-     */
-    void RegisterDropTracePath(const std::string& tracePath);
-
-    /**
-     * Register CSMA like device function
-     * @param deviceTypeName the device type name
-     */
-    void RegisterCsmaLikeDevice(const std::string& deviceTypeName);
-    /**
-     * Register WIFI like device function
-     * @param deviceTypeName the device type name
-     */
-    void RegisterWifiLikeDevice(const std::string& deviceTypeName);
-    /**
-     * Register point to point like device function
-     * @param deviceTypeName the device type name
+     * Run simulation until a given (simulated, absolute) time is reached.
+     *
+     * @param time the run time
      */
-    void RegisterPointToPointLikeDevice(const std::string& deviceTypeName);
+    void SimulatorRunUntil(Time time);
 
     /**
-     * Run simulation until a given (simulated, absolute) time is reached
-     * @param time the run time
+     * Get the stop time of the underlying visual simulator implementation.
+     *
+     * @return The stop time of the visual simulator implementation.
      */
-    void SimulatorRunUntil(Time time);
+    Time GetSimulatorStopTime();
 
     /**
-     * Pause function
+     * Pause function.
+     *
      * @param message the pause message
      */
     static void Pause(const std::string& message);
+
     /**
-     * Get pause message function
+     * Get pause message function.
+     *
      * @returns the pause message
      */
     std::vector<std::string> GetPauseMessages() const;
 
-    /// TransmissionSample structure
-    struct TransmissionSample
-    {
-        Ptr<Node> transmitter; ///< transmitter
-        Ptr<Node> receiver;    ///< NULL if broadcast
-        Ptr<Channel> channel;  ///< channel
-        uint32_t bytes;        ///< bytes
-    };
-
-    typedef std::vector<TransmissionSample>
-        TransmissionSampleList; ///< TransmissionSampleList typedef
     /**
-     * Get transmission samples
+     * Get transmission samples.
+     *
      * @returns the transmission sample list
      */
     TransmissionSampleList GetTransmissionSamples() const;
 
-    /// PacketDropSample structure
-    struct PacketDropSample
-    {
-        Ptr<Node> transmitter; ///< transmitter
-        uint32_t bytes;        ///< bytes
-    };
-
-    typedef std::vector<PacketDropSample> PacketDropSampleList; ///< PacketDropSampleList typedef
     /**
-     * Get packet drop samples
+     * Get packet drop samples.
+     *
      * @returns the packet drop sample list
      */
     PacketDropSampleList GetPacketDropSamples() const;
 
-    /// PacketSample structure
-    struct PacketSample
-    {
-        Time time;             ///< time
-        Ptr<Packet> packet;    ///< packet
-        Ptr<NetDevice> device; ///< device
-    };
-
-    /// TxPacketSample structure
-    struct TxPacketSample : PacketSample
-    {
-        Mac48Address to; ///< to
-    };
-
-    /// RxPacketSample structure
-    struct RxPacketSample : PacketSample
-    {
-        Mac48Address from; ///< from
-    };
-
-    /// LastPacketsSample structure
-    struct LastPacketsSample
-    {
-        std::vector<RxPacketSample> lastReceivedPackets;    ///< last received packets
-        std::vector<TxPacketSample> lastTransmittedPackets; ///< last transmitted packets
-        std::vector<PacketSample> lastDroppedPackets;       ///< last dropped packets
-    };
-
     /**
-     * Get last packets function
+     * Get last packets function.
+     *
      * @param nodeId the node ID
      * @returns the last packets
      */
     LastPacketsSample GetLastPackets(uint32_t nodeId) const;
 
     /**
-     * Set nodes of interest function
+     * Set nodes of interest function.
+     *
      * @param nodes the collection of nodes
      */
     void SetNodesOfInterest(std::set<uint32_t> nodes);
 
-    /// NetDeviceStatistics structure
-    struct NetDeviceStatistics
-    {
-        /// constructor
-        NetDeviceStatistics()
-            : transmittedBytes(0),
-              receivedBytes(0),
-              transmittedPackets(0),
-              receivedPackets(0)
-        {
-        }
-
-        uint64_t transmittedBytes;   ///< transmitted bytes
-        uint64_t receivedBytes;      ///< received bytes
-        uint32_t transmittedPackets; ///< transmitted packets
-        uint32_t receivedPackets;    ///< received packets
-    };
-
-    /// NodeStatistics structure
-    struct NodeStatistics
-    {
-        uint32_t nodeId;                             ///< node ID
-        std::vector<NetDeviceStatistics> statistics; ///< statistics
-    };
-
     /**
-     * Get node statistics
+     * Get node statistics.
+     *
      * @returns the node statistics
      */
     std::vector<NodeStatistics> GetNodesStatistics() const;
 
-    /// PacketCaptureMode enumeration
-    enum PacketCaptureMode
-    {
-        PACKET_CAPTURE_DISABLED = 1,       // packet capture is disabled
-        PACKET_CAPTURE_FILTER_HEADERS_OR,  // packet capture if any of the indicated headers is
-                                           // present
-        PACKET_CAPTURE_FILTER_HEADERS_AND, // packet capture if all of the indicated headers are
-                                           // present
-    };
-
-    /// PacketCaptureOptions structure
-    struct PacketCaptureOptions
-    {
-        std::set<TypeId> headers; ///< headers
-        uint32_t numLastPackets;  ///< num last packets
-        PacketCaptureMode mode;   ///< mode
-    };
-
     /**
-     * Set packet capture options function
+     * Set packet capture options function.
+     *
      * @param nodeId the node ID
      * @param options the capture options
      */
     void SetPacketCaptureOptions(uint32_t nodeId, PacketCaptureOptions options);
 
+    /**
+     * Register drop trace path function.
+     *
+     * @param tracePath the path to trace
+     */
+    void RegisterDropTracePath(const std::string& tracePath);
+
+    /**
+     * Register CSMA like device function.
+     *
+     * @param deviceTypeName the device type name
+     */
+    void RegisterCsmaLikeDevice(const std::string& deviceTypeName);
+
+    /**
+     * Register WIFI like device function.
+     *
+     * @param deviceTypeName the device type name
+     */
+    void RegisterWifiLikeDevice(const std::string& deviceTypeName);
+
+    /**
+     * Register point to point like device function.
+     *
+     * @param deviceTypeName the device type name
+     */
+    void RegisterPointToPointLikeDevice(const std::string& deviceTypeName);
+
     // Yes, I know, this is just a utility function, not really related to the class in any way.
     /**
      * Utility function - clips a line to a bounding box.
@@ -236,116 +284,117 @@
                              double& lineY1,
                              double& lineX2,
                              double& lineY2);
-    // Don't break the above line or pybindgen will not be able to pick up the above annotation :(
 
   private:
     /**
-     * Get packet capture options function
-     * @param nodeId the node ID
-     * @param outOptions the packet capture options
-     * @returns true if successful
+     * Get packet capture options function.
+     *
+     * @param nodeId The node ID
+     * @param outOptions The packet capture options
+     * @returns True if successful
      */
     bool GetPacketCaptureOptions(uint32_t nodeId, const PacketCaptureOptions** outOptions) const;
+
     /**
-     * Filter packet function
-     * @param packet the packet
-     * @param options the capture options
-     * @returns true if successful
+     * Filter packet function.
+     *
+     * @param packet The packet
+     * @param options The capture options
+     * @returns True if successful
      */
     static bool FilterPacket(Ptr<const Packet> packet, const PacketCaptureOptions& options);
 
-    typedef std::pair<Ptr<Channel>, uint32_t> TxRecordKey; ///< TxRecordKey typedef
-
-    /// TxRecordValue structure
-    struct TxRecordValue
-    {
-        Time time;         ///< time
-        Ptr<Node> srcNode; ///< source node
-        bool isBroadcast;  ///< is broadcast?
-    };
+    /**
+     * Find net device statistics function.
+     *
+     * @param node The node
+     * @param interface The interface number
+     * @returns The device statistics
+     */
+    inline NetDeviceStatistics& FindNetDeviceStatistics(int node, int interface);
 
-    /// TransmissionSampleKey structure
-    struct TransmissionSampleKey
-    {
-        /**
-         * Less than operator
-         *
-         * @param other object to compare
-         * @return true if less than
-         */
-        bool operator<(const TransmissionSampleKey& other) const;
-        /**
-         * Equality operator
-         *
-         * @param other object to compare
-         * @return true if equal
-         */
-        bool operator==(const TransmissionSampleKey& other) const;
-        Ptr<Node> transmitter; ///< transmitter
-        Ptr<Node> receiver;    ///< NULL if broadcast
-        Ptr<Channel> channel;  ///< channel
-    };
+    /**
+     * Do pause function.
+     *
+     * @param message the pause message
+     */
+    void DoPause(const std::string& message);
 
-    /// TransmissionSampleValue structure
-    struct TransmissionSampleValue
-    {
-        uint32_t bytes; ///< bytes
-    };
+    /**
+     * Stop simulation callback function.
+     */
+    void CallbackStopSimulation();
 
-    // Data
-    std::map<uint32_t, PacketCaptureOptions> m_packetCaptureOptions; ///< packet capture options
-    std::vector<std::string> m_pauseMessages;                        ///< pause message
-    std::map<TxRecordKey, TxRecordValue> m_txRecords;                ///< transmit records
-    std::map<TransmissionSampleKey, TransmissionSampleValue>
-        m_transmissionSamples;                   ///< transmission samples
-    std::map<Ptr<Node>, uint32_t> m_packetDrops; ///< packet drops
-    std::set<uint32_t>
-        m_nodesOfInterest; ///< list of node IDs whose transmissions will be monitored
-    std::map<uint32_t, Time> m_packetsOfInterest; ///< list of packet UIDs that will be monitored
-    std::map<uint32_t, LastPacketsSample> m_lastPackets;                    ///< last packets
-    std::map<uint32_t, std::vector<NetDeviceStatistics>> m_nodesStatistics; ///< node statistics
+    /////////////////////
+    // Trace callbacks //
+    /////////////////////
 
-    // Trace callbacks
     /**
-     * Network transmit common trace callback function
+     * Network transmit common trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      * @param destination the destination MAC address
      */
-    void TraceNetDevTxCommon(const std::string& context,
-                             Ptr<const Packet> packet,
-                             const Mac48Address& destination);
+    void TraceNetDevTxCommon(
+        const std::string& context,
+        Ptr<const Packet> packet,
+        const std::variant<Mac16Address, Mac48Address, Mac64Address>& destination);
+
     /**
-     * Network receive common trace callback function
+     * Network receive common trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      * @param source the source MAC address
      */
     void TraceNetDevRxCommon(const std::string& context,
                              Ptr<const Packet> packet,
-                             const Mac48Address& source);
+                             const std::variant<Mac16Address, Mac48Address, Mac64Address>& source);
 
     /**
-     * Wi-Fi transmit trace callback function
+     * Wi-Fi transmit trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevTxWifi(std::string context, Ptr<const Packet> packet);
+
     /**
-     * Wi-Fi receive trace callback function
+     * Wi-Fi receive trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevRxWifi(std::string context, Ptr<const Packet> packet);
 
     /**
-     * Queue drop trace callback function
+     * Lr-Wpan transmit trace callback function.
+     *
+     * @param context the context
+     * @param packet the packet
+     */
+    void TraceNetDevTxLrWpan(std::string context, Ptr<const Packet> packet);
+
+    /**
+     * Lr-Wpan receive trace callback function.
+     *
+     * @param context the context
+     * @param packet the packet
+     */
+    void TraceNetDevRxLrWpan(std::string context, Ptr<const Packet> packet);
+
+    /**
+     * Queue drop trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceDevQueueDrop(std::string context, Ptr<const Packet> packet);
+
     /**
-     * Ipv4 drop trace callback function
+     * Ipv4 drop trace callback function.
+     *
      * @param context the context
      * @param hdr the header
      * @param packet the packet
@@ -361,39 +410,48 @@
                        uint32_t interface);
 
     /**
-     * CSMA transmit trace callback function
+     * CSMA transmit trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevTxCsma(std::string context, Ptr<const Packet> packet);
+
     /**
-     * CSMA receive trace callback function
+     * CSMA receive trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevRxCsma(std::string context, Ptr<const Packet> packet);
+
     /**
-     * CSMA promiscuous receive function
+     * CSMA promiscuous receive function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevPromiscRxCsma(std::string context, Ptr<const Packet> packet);
 
     /**
-     * Point to point transmit trace callback function
+     * Point to point transmit trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevTxPointToPoint(std::string context, Ptr<const Packet> packet);
+
     /**
-     * Point to point receive trace callback function
+     * Point to point receive trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      */
     void TraceNetDevRxPointToPoint(std::string context, Ptr<const Packet> packet);
 
     /**
-     * LTE transmit trace callback function
+     * LTE transmit trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      * @param destination the destination MAC address
@@ -402,7 +460,8 @@
                           Ptr<const Packet> packet,
                           const Mac48Address& destination);
     /**
-     * LTE receive trace callback function
+     * LTE receive trace callback function.
+     *
      * @param context the context
      * @param packet the packet
      * @param source the MAC address of the source
@@ -412,26 +471,66 @@
                           const Mac48Address& source);
 
     /**
-     * Find net device statistics function
-     * @param node the node
-     * @param interface the interface number
-     * @returns the device statistics
+     * The TxRecordValue structure
      */
-    inline NetDeviceStatistics& FindNetDeviceStatistics(int node, int interface);
+    struct TxRecordValue
+    {
+        Time time;         //!< The transmission time
+        Ptr<Node> srcNode; //!< The source node of the transmission
+        bool isBroadcast;  //!< Broadcast flag
+    };
 
     /**
-     * Do pause function
-     * @param message the pause message
+     * The TransmissionSampleKey structure
      */
-    void DoPause(const std::string& message);
+    struct TransmissionSampleKey
+    {
+        /**
+         * Less than operator
+         *
+         * @param other Object to compare
+         * @return True if less than
+         */
+        bool operator<(const TransmissionSampleKey& other) const;
+        /**
+         * Equality operator
+         *
+         * @param other Object to compare
+         * @return True if equal
+         */
+        bool operator==(const TransmissionSampleKey& other) const;
+        Ptr<Node> transmitter; //!< transmitter
+        Ptr<Node> receiver;    //!< NULL if broadcast
+        Ptr<Channel> channel;  //!< channel
+    };
 
-    bool m_stop;     ///< stop?
-    Time m_runUntil; ///< run until time
+    /**
+     * The TransmissionSampleValue structure
+     */
+    struct TransmissionSampleValue
+    {
+        uint32_t bytes; //!< The nuumber of bytes of the transmission sample
+    };
 
-    /// Stop simulation callback function
-    void CallbackStopSimulation();
+    using TxRecordKey = std::pair<Ptr<Channel>, uint32_t>; //!< TxRecordKey type definition
+
+    std::map<uint32_t, PacketCaptureOptions> m_packetCaptureOptions; //!< Packet capture options
+    std::vector<std::string> m_pauseMessages;                        //!< Pause message
+    std::map<TxRecordKey, TxRecordValue> m_txRecords;                //!< Transmit records
+    std::map<TransmissionSampleKey,
+             TransmissionSampleValue>
+        m_transmissionSamples;                    //!< Transmission samples
+    std::map<Ptr<Node>, uint32_t> m_packetDrops;  //!< Packet drops
+    std::set<uint32_t> m_nodesOfInterest;         //!< List of node IDs whose transmissions
+                                                  //!< will be monitored
+    std::map<uint32_t, Time> m_packetsOfInterest; //!< List of packet UIDs that will be monitored
+    std::map<uint32_t, LastPacketsSample> m_lastPackets;                    //!< Last packets
+    std::map<uint32_t, std::vector<NetDeviceStatistics>> m_nodesStatistics; //!< Node statistics
+    bool m_stop;     //!< Stop simulation flag
+    Time m_runUntil; //!< Indicates until when the simulation should run for its next step
 };
 
+} // namespace visualizer
 } // namespace ns3
 
 #endif /* NS3_PYVIZ_H */
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/model/visual-simulator-impl.cc ns-3.47/src/visualizer/model/visual-simulator-impl.cc
--- ns-3.46/src/visualizer/model/visual-simulator-impl.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/model/visual-simulator-impl.cc	2026-02-16 16:08:49.842230800 -0800
@@ -12,6 +12,7 @@
 #include "ns3/default-simulator-impl.h"
 #include "ns3/log.h"
 #include "ns3/packet-metadata.h"
+#include "ns3/system-path.h"
 
 namespace ns3
 {
@@ -106,18 +107,25 @@
 void
 VisualSimulatorImpl::Run()
 {
+    std::stringstream ss;
+    // If not a python script, where we can easily get argv, then inject program path as argv0
+    if (SystemPath::FindSelf().find("python") == std::string::npos)
+    {
+        ss << "import sys\n"
+           << "sys.argv = ['" << SystemPath::FindSelf() << "']\n";
+    }
+    ss << "import visualizer\n"
+       << "visualizer.start()\n";
     if (!Py_IsInitialized())
     {
         Py_Initialize();
-        PyRun_SimpleString("import visualizer\n"
-                           "visualizer.start();\n");
+        PyRun_SimpleString(ss.str().data());
     }
     else
     {
         PyGILState_STATE __py_gil_state = PyGILState_Ensure();
 
-        PyRun_SimpleString("import visualizer\n"
-                           "visualizer.start();\n");
+        PyRun_SimpleString(ss.str().data());
 
         PyGILState_Release(__py_gil_state);
     }
@@ -132,9 +140,16 @@
 EventId
 VisualSimulatorImpl::Stop(const Time& delay)
 {
+    m_stopTime = delay;
     return m_simulator->Stop(delay);
 }
 
+Time
+VisualSimulatorImpl::GetStopTime()
+{
+    return m_stopTime;
+}
+
 //
 // Schedule an event for a _relative_ time in the future.
 //
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/model/visual-simulator-impl.h ns-3.47/src/visualizer/model/visual-simulator-impl.h
--- ns-3.46/src/visualizer/model/visual-simulator-impl.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/model/visual-simulator-impl.h	2026-02-16 16:08:49.842230800 -0800
@@ -62,6 +62,12 @@
     uint32_t GetContext() const override;
     uint64_t GetEventCount() const override;
 
+    /**
+     * Get the simulator implementation stop time.
+     * @returns The stop time of the simulation implementation.
+     */
+    Time GetStopTime();
+
     /// calls Run() in the wrapped simulator
     void RunRealSimulator();
 
@@ -70,8 +76,9 @@
     void NotifyConstructionCompleted() override;
 
   private:
-    Ptr<SimulatorImpl> m_simulator;       ///< the simulator implementation
-    ObjectFactory m_simulatorImplFactory; ///< simulator implementation factory
+    Ptr<SimulatorImpl> m_simulator;       //!< The simulator implementation
+    ObjectFactory m_simulatorImplFactory; //!< Simulator implementation factory
+    Time m_stopTime;                      //!< The stop time of the simulation
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/visualizer/base.py ns-3.47/src/visualizer/visualizer/base.py
--- ns-3.46/src/visualizer/visualizer/base.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/visualizer/base.py	2026-02-16 16:08:49.842230800 -0800
@@ -61,6 +61,8 @@
     ns.MeshPointDevice: NetDeviceTraits(is_virtual=True),
     ns.LteUeNetDevice: NetDeviceTraits(is_wireless=True),
     ns.LteEnbNetDevice: NetDeviceTraits(is_wireless=True),
+    ns.lrwpan.LrWpanNetDevice: NetDeviceTraits(is_wireless=True),
+    ns.SixLowPanNetDevice: NetDeviceTraits(is_virtual=False, is_wireless=True),
 }
 
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/visualizer/core.py ns-3.47/src/visualizer/visualizer/core.py
--- ns-3.46/src/visualizer/visualizer/core.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/visualizer/core.py	2026-02-16 16:08:49.843231000 -0800
@@ -83,9 +83,6 @@
     transform_point_simulation_to_canvas,
 )
 
-PI_OVER_2 = math.pi / 2
-PI_TIMES_2 = math.pi * 2
-
 
 ## Node class
 class Node(PyVizObject):
@@ -262,9 +259,9 @@
 
             self.emit("query-extra-tooltip-info", lines)
 
-            mob = ns3_node.GetObject[ns.MobilityModel]()
-            if mob:
-                mobility_model_name = mob.__deref__().GetInstanceTypeId().GetName()
+            mobility = ns3_node.__deref__().GetObject[ns.MobilityModel]()
+            if mobility:
+                mobility_model_name = mobility.__deref__().GetInstanceTypeId().GetName()
                 lines.append("  <b>Mobility Model</b>: %s" % mobility_model_name)
 
             for devI in range(ns3_node.GetNDevices()):
@@ -277,33 +274,51 @@
                 devname = dev.GetInstanceTypeId().GetName()
                 lines.append("    <b>Type:</b> %s" % devname)
 
-                if ipv4 is not None:
+                # Lr-Wpan devices have both 16 bit (short) and 64 bit (extended) addresses unlike other
+                # NetDevices. These are extracted directed from the MAC layer.
+                if devname == "ns3::lrwpan::LrWpanNetDevice":
+                    lines.append(
+                        "    <b>MAC short address:</b> %s" % (dev.GetMac().GetShortAddress(),)
+                    )
+                    lines.append(
+                        "    <b>MAC extended address:</b> %s" % (dev.GetMac().GetExtendedAddress(),)
+                    )
+                elif devname == "ns3::SixLowPanNetDevice":
+                    mac48Addr = ns.Mac48Address.ConvertFrom(dev.GetAddress())
+                    lines.append("    <b>MAC Address:</b> %s (Generated)" % (mac48Addr,))
+                else:
+                    mac48Addr = ns.Mac48Address.ConvertFrom(dev.GetAddress())
+                    lines.append("    <b>MAC Address:</b> %s" % (mac48Addr,))
+
+                if ipv4:
                     ipv4_idx = ipv4.GetInterfaceForDevice(dev)
                     if ipv4_idx != -1:
                         addresses = [
-                            "%s/%s"
-                            % (
+                            "%s/%s (Subnet Mask: %s)"
+                            % (  # Note, Mask and prefix functions names might change. See !2645
                                 ipv4.GetAddress(ipv4_idx, i).GetLocal(),
+                                ipv4.GetAddress(ipv4_idx, i).GetMask().GetPrefixLength(),
                                 ipv4.GetAddress(ipv4_idx, i).GetMask(),
                             )
                             for i in range(ipv4.GetNAddresses(ipv4_idx))
                         ]
                         lines.append("    <b>IPv4 Addresses:</b> %s" % "; ".join(addresses))
-
-                if ipv6 is not None:
+                if ipv6:
                     ipv6_idx = ipv6.GetInterfaceForDevice(dev)
                     if ipv6_idx != -1:
-                        addresses = [
-                            "%s/%s"
-                            % (
-                                ipv6.GetAddress(ipv6_idx, i).GetAddress(),
-                                ipv6.GetAddress(ipv6_idx, i).GetPrefix(),
-                            )
-                            for i in range(ipv6.GetNAddresses(ipv6_idx))
-                        ]
-                        lines.append("    <b>IPv6 Addresses:</b> %s" % "; ".join(addresses))
+                        n = ipv6.GetNAddresses(ipv6_idx)
 
-                lines.append("    <b>MAC Address:</b> %s" % (dev.GetAddress(),))
+                        if n > 0:
+                            # First address on the same line as title
+                            entry = ipv6.GetAddress(ipv6_idx, 0)
+                            first_addr = "%s%s" % (entry.GetAddress(), entry.GetPrefix())
+                            lines.append("    <b>IPv6 Addresses:</b> %s" % first_addr)
+
+                        # Remaining addresses aligned under the first address
+                        for i in range(1, n):
+                            entry = ipv6.GetAddress(ipv6_idx, i)
+                            addr = "%s%s" % (entry.GetAddress(), entry.GetPrefix())
+                            lines.append("                                     %s" % addr)
 
             tooltip.set_markup("\n".join(lines))
         finally:
@@ -460,7 +475,7 @@
         try:
             bounds = self.visualizer.canvas.get_bounds()
 
-            (min_x, min_y, max_x, max_y) = bounds
+            min_x, min_y, max_x, max_y = bounds
 
             min_x = min(x, min_x)
             min_y = min(y, min_y)
@@ -678,7 +693,7 @@
         self.go.clear()
         self.target_time = 0  # in seconds
         self.quit = False
-        self.sim_helper = ns.PyViz()
+        self.sim_helper = ns.visualizer.PyViz()
         self.pause_messages = []
 
     def set_nodes_of_interest(self, nodes):
@@ -703,30 +718,45 @@
         @return none
         """
         while not self.quit:
-            # print "sim: Wait for go"
-            self.go.wait()  # wait until the main (view) thread gives us the go signal
-            self.go.clear()
-            if self.quit:
-                break
-            # self.go.clear()
-            # print "sim: Acquire lock"
-            self.lock.acquire()
             try:
-                if 0:
-                    if ns3.Simulator.IsFinished():
-                        self.viz.play_button.set_sensitive(False)
+                # Wait until the GUI gives the go signal
+                self.go.wait()
+                self.go.clear()
+
+                if self.quit:
+                    break
+
+                # Check once whether GUI main loop is alive
+                gui_active = Gtk.main_level() > 0
+
+                # Thread-safe region (original code)
+                self.lock.acquire()
+                try:
+                    # Advance simulator to the requested target time
+                    self.sim_helper.SimulatorRunUntil(ns.Seconds(self.target_time))
+
+                    # Collect pause messages
+                    self.pause_messages.extend(self.sim_helper.GetPauseMessages())
+
+                    # Schedule GUI update — but only if GUI still exists
+                    if gui_active:
+                        GLib.idle_add(self.viz.update_model, priority=PRIORITY_UPDATE_MODEL)
+
+                    # Check stop condition
+                    if (
+                        ns.Simulator.Now().GetSeconds()
+                        >= self.sim_helper.GetSimulatorStopTime().GetSeconds()
+                    ):
+                        if gui_active:
+                            GLib.idle_add(self.viz._on_simulation_finished)
                         break
-                # print "sim: Current time is %f; Run until: %f" % (ns3.Simulator.Now ().GetSeconds (), self.target_time)
-                # if ns3.Simulator.Now ().GetSeconds () > self.target_time:
-                #    print "skipping, model is ahead of view!"
-                self.sim_helper.SimulatorRunUntil(ns.Seconds(self.target_time))
-                # print "sim: Run until ended at current time: ", ns3.Simulator.Now ().GetSeconds ()
-                self.pause_messages.extend(self.sim_helper.GetPauseMessages())
-                GLib.idle_add(self.viz.update_model, priority=PRIORITY_UPDATE_MODEL)
-                # print "sim: Run until: ", self.target_time, ": finished."
-            finally:
-                self.lock.release()
-            # print "sim: Release lock, loop."
+
+                finally:
+                    self.lock.release()
+
+            except Exception:
+                # Happens if GUI is already shutting down and idle_add crashes internally
+                break
 
 
 ## ShowTransmissionsMode
@@ -1039,6 +1069,7 @@
 
     def create_gui(self):
         self.window = Gtk.Window()
+        self.window.set_title(sys.argv[0])
         vbox = Gtk.VBox()
         vbox.show()
         self.window.add(vbox)
@@ -1373,7 +1404,7 @@
         bounds_x2, bounds_y2 = self.canvas.convert_from_pixels(
             hadj.get_value() + hadj.get_page_size(), vadj.get_value() + vadj.get_page_size()
         )
-        ns.PyViz.LineClipping(
+        ns.visualizer.PyViz.LineClipping(
             bounds_x1, bounds_y1, bounds_x2, bounds_y2, pos1_x, pos1_y, pos2_x, pos2_y
         )
         return (pos1_x.value + pos2_x.value) / 2, (pos1_y.value + pos2_y.value) / 2
@@ -1435,10 +1466,16 @@
                 visibility_threshold=0.5,
                 font=("Sans Serif %f" % int(1 + BITRATE_FONT_SIZE * k)),
             )
+
             angle = math.atan2((pos2_y - pos1_y), (pos2_x - pos1_x))
-            if -PI_OVER_2 <= angle <= PI_OVER_2:
+            if angle > math.pi / 2 or angle < -math.pi / 2:
+                # Normalize the angle, in essence, adjust the angle to keep
+                # it from rotating beyond +/- 90 degrees. In this way, the
+                # direction of the label remain always readable regardless of
+                # the angle.
+                angle += math.pi
                 label.set_properties(
-                    text=("%.2f kbit/s →" % (kbps,)),
+                    text=("← %.2f kbit/s" % (kbps,)),
                     alignment=Pango.Alignment.CENTER,
                     anchor=GooCanvas.CanvasAnchorType.S,
                     x=0,
@@ -1446,7 +1483,7 @@
                 )
             else:
                 label.set_properties(
-                    text=("← %.2f kbit/s" % (kbps,)),
+                    text=("%.2f kbit/s →" % (kbps,)),
                     alignment=Pango.Alignment.CENTER,
                     anchor=GooCanvas.CanvasAnchorType.N,
                     x=0,
@@ -1544,6 +1581,20 @@
 
         self._drop_arrows = new_arrows + old_arrows
 
+    def _on_simulation_finished(self):
+        print("Simulation finished.")
+        if self._update_timeout_id is not None:
+            idExists = GLib.MainContext.default().find_source_by_id(self._update_timeout_id)
+            if idExists:
+                try:
+                    GLib.Source.remove(self._update_timeout_id)
+                except Exception:
+                    pass
+                finally:
+                    self._update_timeout_id = None
+        self.play_button.set_active(False)
+        self.play_button.set_sensitive(False)
+
     def update_view_timeout(self):
         # print "view: update_view_timeout called at real time ", time.time()
 
@@ -1585,7 +1636,14 @@
 
     def _start_update_timer(self):
         if self._update_timeout_id is not None:
-            GLib.source_remove(self._update_timeout_id)
+            idExists = GLib.MainContext.default().find_source_by_id(self._update_timeout_id)
+            if idExists:
+                try:
+                    GLib.Source.remove(self._update_timeout_id)
+                except Exception:
+                    pass
+                finally:
+                    self._update_timeout_id = None
         # print "start_update_timer"
         self._update_timeout_id = GLib.timeout_add(
             int(SAMPLE_PERIOD / min(self.speed, 1) * 1e3),
@@ -1598,12 +1656,26 @@
             self._start_update_timer()
         else:
             if self._update_timeout_id is not None:
-                GLib.source_remove(self._update_timeout_id)
+                idExists = GLib.MainContext.default().find_source_by_id(self._update_timeout_id)
+                if idExists:
+                    try:
+                        GLib.Source.remove(self._update_timeout_id)
+                    except Exception:
+                        pass
+                    finally:
+                        self._update_timeout_id = None
 
     def _quit(self, *dummy_args):
         if self._update_timeout_id is not None:
-            GLib.source_remove(self._update_timeout_id)
-            self._update_timeout_id = None
+            idExists = GLib.MainContext.default().find_source_by_id(self._update_timeout_id)
+            if idExists:
+                try:
+                    GLib.Source.remove(self._update_timeout_id)
+                except Exception:
+                    pass
+                finally:
+                    self._update_timeout_id = None
+        ns.Simulator.Stop()
         self.simulation.quit = True
         self.simulation.go.set()
         self.simulation.join()
@@ -1718,12 +1790,12 @@
         self.simulation.lock.acquire()
         try:
             ns3_node = ns.NodeList.GetNode(node.node_index)
-            mob = ns3_node.GetObject[ns.MobilityModel]()
-            if not mob:
+            mobility = ns3_node.GetObject[ns.MobilityModel]()
+            if not mobility:
                 return
             if self.node_drag_state is not None:
                 return
-            pos = ns3_node.GetObject[ns.MobilityModel]().__deref__().GetPosition()
+            pos = mobility.__deref__().GetPosition()
         finally:
             self.simulation.lock.release()
         devpos = self.canvas.get_window().get_device_position(event.device)
@@ -1737,8 +1809,8 @@
         self.simulation.lock.acquire()
         try:
             ns3_node = ns.NodeList.GetNode(node.node_index)
-            mob = ns3_node.GetObject[ns.MobilityModel]()
-            if not mob:
+            mobility = ns3_node.GetObject[ns.MobilityModel]()
+            if not mobility:
                 return False
             if self.node_drag_state is None:
                 return False
@@ -1746,11 +1818,11 @@
             canvas_x, canvas_y = self.canvas.convert_from_pixels(devpos.x, devpos.y)
             dx = canvas_x - self.node_drag_state.canvas_x0
             dy = canvas_y - self.node_drag_state.canvas_y0
-            pos = mob.GetPosition()
+            pos = mobility.GetPosition()
             pos.x = self.node_drag_state.sim_x0 + transform_distance_canvas_to_simulation(dx)
             pos.y = self.node_drag_state.sim_y0 + transform_distance_canvas_to_simulation(dy)
             # print "SetPosition(%G, %G)" % (pos.x, pos.y)
-            mob.SetPosition(pos)
+            mobility.SetPosition(pos)
             node.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
         finally:
             self.simulation.lock.release()
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/visualizer/visualizer/ipython_view.py ns-3.47/src/visualizer/visualizer/ipython_view.py
--- ns-3.46/src/visualizer/visualizer/ipython_view.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/visualizer/visualizer/ipython_view.py	2026-02-16 16:08:49.843231000 -0800
@@ -203,7 +203,7 @@
         else:
             if self.no_input_splitter:
                 self.lines.append(line)
-                (status, self.indent_spaces) = self.IP.check_complete("\n".join(self.lines))
+                status, self.indent_spaces = self.IP.check_complete("\n".join(self.lines))
                 self.iter_more = status == "incomplete"
             else:
                 self.IP.input_splitter.push(line)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/CMakeLists.txt ns-3.47/src/wifi/CMakeLists.txt
--- ns-3.46/src/wifi/CMakeLists.txt	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/CMakeLists.txt	2026-02-16 16:08:49.843231000 -0800
@@ -167,7 +167,6 @@
     model/wifi-ru.cc
     model/wifi-spectrum-phy-interface.cc
     model/wifi-spectrum-signal-parameters.cc
-    model/wifi-standard-constants.cc
     model/wifi-standards.cc
     model/wifi-tx-current-model.cc
     model/wifi-tx-parameters.cc
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/doc/source/wifi-design.rst ns-3.47/src/wifi/doc/source/wifi-design.rst
--- ns-3.46/src/wifi/doc/source/wifi-design.rst	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/doc/source/wifi-design.rst	2026-02-16 16:08:49.850231000 -0800
@@ -32,7 +32,6 @@
 * various rate control algorithms including **Aarf, Arf, Cara, Onoe, Rraa,
   ConstantRate, Minstrel and Minstrel-HT**
 * 802.11s (mesh), described in another chapter
-* 802.11p and WAVE (vehicular), described in another chapter
 
 The set of 802.11 models provided in |ns3| attempts to provide an accurate
 MAC-level implementation of the 802.11 specification and to provide a
@@ -194,6 +193,21 @@
   for the first time. This means that this A-MSDU cannot be aggregated to other MSDUs using A-MSDU
   aggregation.
 
+The following open issues are classified as unfixed bugs in the ns-3 tracker, and users should
+be aware of them:
+
+* The default TXOP limits do not conform to the standard (e.g., 2.528 ms for AC_BE is not default); see `Issue 289 <https://gitlab.com/nsnam/ns-3-dev/-/issues/289>_` and `Merge Request 1976 <https://gitlab.com/nsnam/ns-3-dev/-/merge_requests/1976>_`.
+* CCA issues with use of ED and PD thresholds have been reported; see `Issue 1247 <https://gitlab.com/nsnam/ns-3-dev/-/issues/1247>_` and `Issue 1056 <https://gitlab.com/nsnam/ns-3-dev/-/issues/1056>_` and `Issue 900 <https://gitlab.com/nsnam/ns-3-dev/-/issues/900>_`.
+* Use of MLO possibly causes the WifiPhy::MonitorSniffRx trace to miss some packets; see `Issue 1179 <https://gitlab.com/nsnam/ns-3-dev/-/issues/1179>_`.
+* MinstrelHt rate control has reported bugs; see `Issue 886 <https://gitlab.com/nsnam/ns-3-dev/-/issues/886>_` and `Issue 51 <https://gitlab.com/nsnam/ns-3-dev/-/issues/51>_` and `Merge Request 2344 <https://gitlab.com/nsnam/ns-3-dev/-/merge_requests/2344>_`.
+* The 1600ns guard interval configuration performance for 802.11ax seems to be incorrect; see `Issue 1010 <https://gitlab.com/nsnam/ns-3-dev/-/issues/1010>_`.
+* Support for 22 MHz channels for the HE PHY is limited; see `Issue 402 <https://gitlab.com/nsnam/ns-3-dev/-/issues/402>_`.
+* HT simulations cannot use SingleModelSpectrumChannel in 2.4 GHz; see `Issue 1090 <https://gitlab.com/nsnam/ns-3-dev/-/issues/1090>_`.
+* The spatial-reuse-example.cc produces results inconsistent with its commented expected results; see `Issue 566 <https://gitlab.com/nsnam/ns-3-dev/-/issues/566>_`.
+* There may be lingering issues with the Wi-Fi frame capture model; see `Issue 746 <https://gitlab.com/nsnam/ns-3-dev/-/issues/746>_`.
+
+The following hyperlink links to `all open wifi module issues <https://gitlab.com/nsnam/ns-3-dev/-/issues?label_name%5B%5D=module%3A%3Awifi>_`, not all of which are bugs (some are related to feature requests).
+
 Design Details
 **************
 
@@ -776,6 +790,26 @@
 
    Illustration of signals tracking upon channel switching
 
+TX power
+########
+
+The transmission power level used by a PHY is configured via 3 attributes:
+  - ``WifiPhy::TxPowerLevels`` configures the number of power levels supported by the PHY (default 1)
+  - ``WifiPhy::TxPowerStart`` configures the minimum Tx power level supported by the PHY (default 16.0206 dBm, equivalent to 40 mW)
+  - ``WifiPhy::TxPowerEnd`` configures the maximum Tx power level supported by the PHY (default 16.0206 dBm, equivalent to 40 mW)
+
+ Even though the standard allows up to 8 different power levels, the mapping of these integers to power levels is implementation dependent.
+ The ns-3 model allows for up to 255 (1..255) power levels to be supported.
+ If only one power level is configured (the default), the transmission power is equal to ``WifiPhy::TxPowerStart``
+ (or ``WifiPhy::TxPowerEnd``, since they are equal by default).
+ If more than one power level is configured, the transmission power at a given power level is linearly interpolated in decibels
+ between ``WifiPhy::TxPowerStart`` and ``WifiPhy::TxPowerEnd`` according to the following formula:
+
+.. math::
+ txPower = WifiPhy::TxPowerStart + (powerLevel-1) \times \frac{WifiPhy::TxPowerEnd - WifiPhy::TxPowerStart}{WifiPhy::TxPowerLevels-1}
+
+where powerLevel is the requested power level and is an integer between 1 and ``WifiPhy::TxPowerLevels``.
+
 MU-MIMO PHY support
 ###################
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/examples/wifi-bianchi.cc ns-3.47/src/wifi/examples/wifi-bianchi.cc
--- ns-3.46/src/wifi/examples/wifi-bianchi.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/examples/wifi-bianchi.cc	2026-02-16 16:08:49.851230900 -0800
@@ -45,15 +45,24 @@
 
 #include <fstream>
 #include <iomanip>
+#include <numbers>
 #include <vector>
 
-/// Avoid std::numbers::pi because it's C++20
-#define PI 3.1415926535
+/**
+ * @file
+ * Example comparing ns-3 to the Bianchi model.
+ *
+ * See the Wi-Fi Module documentation in the Models chapter.
+ */
 
 NS_LOG_COMPONENT_DEFINE("WifiBianchi");
 
 using namespace ns3;
 
+/** Unnamed namespace, to disambiguate class Experiment. */
+namespace
+{
+
 std::ofstream cwTraceFile;      ///< File that traces CW over time
 std::ofstream backoffTraceFile; ///< File that traces backoff over time
 std::ofstream phyTxTraceFile;   ///< File that traces PHY transmissions  over time
@@ -2409,6 +2418,9 @@
      * @param staTxPower the STA transmit power
      * @param pktInterval the packet interval
      * @return 0 if all went well
+     *
+     * Call graph was not generated because of its size.
+     * @hidecallgraph
      */
     int Run(const WifiHelper& wifi,
             const YansWifiPhyHelper& wifiPhy,
@@ -2536,8 +2548,8 @@
     const auto angle = (static_cast<degree_u>(360) / (nNodes - 1));
     for (uint32_t i = 0; i < (nNodes - 1); ++i)
     {
-        positionAlloc->Add(Vector(1.0 + (distance * cos((i * angle * PI) / 180)),
-                                  1.0 + (distance * sin((i * angle * PI) / 180)),
+        positionAlloc->Add(Vector(1.0 + (distance * cos((i * angle * std::numbers::pi) / 180)),
+                                  1.0 + (distance * sin((i * angle * std::numbers::pi) / 180)),
                                   0.0));
     }
 
@@ -2843,6 +2855,8 @@
     return count;
 }
 
+} // unnamed namespace
+
 int
 main(int argc, char* argv[])
 {
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/examples/wifi-manager-example.cc ns-3.47/src/wifi/examples/wifi-manager-example.cc
--- ns-3.46/src/wifi/examples/wifi-manager-example.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/examples/wifi-manager-example.cc	2026-02-16 16:08:49.851230900 -0800
@@ -94,7 +94,7 @@
 /// Step structure
 struct Step
 {
-    dBm_u stepSize;  ///< step size
+    dB_u stepSize;   ///< step size
     double stepTime; ///< step size in seconds
 };
 
@@ -198,7 +198,7 @@
     uint32_t steps;
     uint32_t rtsThreshold = 999999; // disabled even for large A-MPDU
     uint32_t maxAmpduSize = 65535;
-    dBm_u stepSize{1};
+    dB_u stepSize{1};
     double stepTime = 1;        // seconds
     uint32_t packetSize = 1024; // bytes
     bool broadcast = false;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/examples/wifi-test-interference-helper.cc ns-3.47/src/wifi/examples/wifi-test-interference-helper.cc
--- ns-3.46/src/wifi/examples/wifi-test-interference-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/examples/wifi-test-interference-helper.cc	2026-02-16 16:08:49.851230900 -0800
@@ -23,7 +23,7 @@
 // The program can be configured at run-time by passing command-line arguments.
 // It enables to configure the delay between the transmission from station A
 // and the transmission from station B (--delay option). It is also possible to
-// select the tx power level (--txPowerA and --txPowerB options), the packet size
+// select the tx power (--txPowerA and --txPowerB options), the packet size
 // (--packetSizeA and --packetSizeB options) and the modulation (--txModeA and
 // --txModeB options) used for the respective transmissions.
 //
@@ -77,8 +77,8 @@
         meter_u xB;            ///< x B
         std::string txModeA;   ///< transmit mode A
         std::string txModeB;   ///< transmit mode B
-        dBm_u txPowerLevelA;   ///< transmit power level A
-        dBm_u txPowerLevelB;   ///< transmit power level B
+        dBm_u txPowerA;        ///< transmit power A
+        dBm_u txPowerB;        ///< transmit power B
         uint32_t packetSizeA;  ///< packet size A
         uint32_t packetSizeB;  ///< packet size B
         uint8_t channelA;      ///< channel number A
@@ -129,7 +129,7 @@
     m_uidA = p->GetUid();
     Ptr<WifiPsdu> psdu = Create<WifiPsdu>(p, hdr);
     WifiTxVector txVector;
-    txVector.SetTxPowerLevel(0); // only one TX power level
+    txVector.SetTxPowerLevel(WIFI_MIN_TX_PWR_LEVEL);
     txVector.SetMode(WifiMode(m_input.txModeA));
     txVector.SetChannelWidth(m_input.widthA);
     txVector.SetPreambleType(m_input.preamble);
@@ -146,7 +146,7 @@
     m_uidB = p->GetUid();
     Ptr<WifiPsdu> psdu = Create<WifiPsdu>(p, hdr);
     WifiTxVector txVector;
-    txVector.SetTxPowerLevel(0); // only one TX power level
+    txVector.SetTxPowerLevel(WIFI_MIN_TX_PWR_LEVEL);
     txVector.SetMode(WifiMode(m_input.txModeB));
     txVector.SetChannelWidth(m_input.widthB);
     txVector.SetPreambleType(m_input.preamble);
@@ -185,8 +185,8 @@
       xB(5),
       txModeA("OfdmRate54Mbps"),
       txModeB("OfdmRate54Mbps"),
-      txPowerLevelA(dBm_u{16.0206}),
-      txPowerLevelB(dBm_u{16.0206}),
+      txPowerA(dBm_u{16.0206}),
+      txPowerB(dBm_u{16.0206}),
       packetSizeA(1500),
       packetSizeB(1500),
       channelA(36),
@@ -225,15 +225,15 @@
     Ptr<WifiNetDevice> devA = CreateObject<WifiNetDevice>();
     m_txA = CreateObject<SpectrumWifiPhy>();
     m_txA->SetDevice(devA);
-    m_txA->SetTxPowerStart(input.txPowerLevelA);
-    m_txA->SetTxPowerEnd(input.txPowerLevelA);
+    m_txA->SetTxPowerStart(input.txPowerA);
+    m_txA->SetTxPowerEnd(input.txPowerA);
 
     Ptr<Node> nodeB = CreateObject<Node>();
     Ptr<WifiNetDevice> devB = CreateObject<WifiNetDevice>();
     m_txB = CreateObject<SpectrumWifiPhy>();
     m_txB->SetDevice(devB);
-    m_txB->SetTxPowerStart(input.txPowerLevelB);
-    m_txB->SetTxPowerEnd(input.txPowerLevelB);
+    m_txB->SetTxPowerStart(input.txPowerB);
+    m_txB->SetTxPowerEnd(input.txPowerB);
 
     Ptr<Node> nodeRx = CreateObject<Node>();
     Ptr<WifiNetDevice> devRx = CreateObject<WifiNetDevice>();
@@ -317,8 +317,8 @@
     cmd.AddValue("xB", "The position of transmitter B (> 0)", input.xB);
     cmd.AddValue("packetSizeA", "Packet size in bytes of transmitter A", input.packetSizeA);
     cmd.AddValue("packetSizeB", "Packet size in bytes of transmitter B", input.packetSizeB);
-    cmd.AddValue("txPowerA", "TX power level of transmitter A", input.txPowerLevelA);
-    cmd.AddValue("txPowerB", "TX power level of transmitter B", input.txPowerLevelB);
+    cmd.AddValue("txPowerA", "TX power of transmitter A", input.txPowerA);
+    cmd.AddValue("txPowerB", "TX power of transmitter B", input.txPowerB);
     cmd.AddValue("txModeA", "Wifi mode used for payload transmission of sender A", input.txModeA);
     cmd.AddValue("txModeB", "Wifi mode used for payload transmission of sender B", input.txModeB);
     cmd.AddValue("channelA", "The selected channel number of sender A", input.channelA);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/helper/athstats-helper.cc ns-3.47/src/wifi/helper/athstats-helper.cc
--- ns-3.46/src/wifi/helper/athstats-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/helper/athstats-helper.cc	2026-02-16 16:08:49.851230900 -0800
@@ -234,10 +234,10 @@
                                   Ptr<const Packet> packet,
                                   WifiMode mode,
                                   WifiPreamble preamble,
-                                  uint8_t txPower)
+                                  uint8_t txPowerLevel)
 {
     NS_LOG_FUNCTION(this << context << packet << "PHYTX mode=" << mode << "Preamble=" << preamble
-                         << "Power=" << txPower);
+                         << "Power level=" << txPowerLevel);
     ++m_phyTxCount;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/helper/athstats-helper.h ns-3.47/src/wifi/helper/athstats-helper.h
--- ns-3.46/src/wifi/helper/athstats-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/helper/athstats-helper.h	2026-02-16 16:08:49.852230800 -0800
@@ -188,13 +188,13 @@
      * @param packet the packet
      * @param mode the WifiMode
      * @param preamble the wifi preamble
-     * @param txPower the transmit power level
+     * @param txPowerLevel the transmit power level
      */
     void PhyTxTrace(std::string context,
                     Ptr<const Packet> packet,
                     WifiMode mode,
                     WifiPreamble preamble,
-                    uint8_t txPower);
+                    uint8_t txPowerLevel);
 
     /**
      * Function to be called when the PHY layer of the considered device
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/helper/wifi-helper.cc ns-3.47/src/wifi/helper/wifi-helper.cc
--- ns-3.46/src/wifi/helper/wifi-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/helper/wifi-helper.cc	2026-02-16 16:08:49.852230800 -0800
@@ -354,12 +354,6 @@
 
     header.SetWifiHeader(IsEht(preamble) ? 2 : 1);
 
-    if (signalNoise)
-    {
-        header.SetAntennaSignalPower(signalNoise->signal);
-        header.SetAntennaNoisePower(signalNoise->noise);
-    }
-
     header.SetTsft(Simulator::Now().GetMicroSeconds());
 
     uint8_t frameFlags = RadiotapHeader::FRAME_FLAG_NONE;
@@ -412,6 +406,12 @@
     }
     header.SetChannelFields(channelFields);
 
+    if (signalNoise)
+    {
+        header.SetAntennaSignalPower(signalNoise->signal);
+        header.SetAntennaNoisePower(signalNoise->noise);
+    }
+
     if (modClass == WIFI_MOD_CLASS_HT)
     {
         RadiotapHeader::McsFields mcsFields{.mcs = mcs};
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/helper/wifi-tx-stats-helper.h ns-3.47/src/wifi/helper/wifi-tx-stats-helper.h
--- ns-3.46/src/wifi/helper/wifi-tx-stats-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/helper/wifi-tx-stats-helper.h	2026-02-16 16:08:49.852230800 -0800
@@ -158,7 +158,7 @@
         template <typename Tuple, size_t Index = 0>
         size_t tuple_hash(const Tuple& tuple) const
         {
-            if constexpr (Index < std::tuple_size<Tuple>::value)
+            if constexpr (Index < std::tuple_size_v<Tuple>)
             {
                 size_t seed = tuple_hash<Tuple, Index + 1>(tuple);
                 hash_combine(seed, std::get<Index>(tuple));
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/eht/eht-phy.cc ns-3.47/src/wifi/model/eht/eht-phy.cc
--- ns-3.46/src/wifi/model/eht/eht-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/eht/eht-phy.cc	2026-02-16 16:08:49.857231000 -0800
@@ -428,6 +428,12 @@
     return dataRate;
 }
 
+uint32_t
+EhtPhy::GetMaxPsduSize() const
+{
+    return WIFI_PSDU_MAX_LENGTH_EHT;
+}
+
 dBm_u
 EhtPhy::Per20MHzCcaThreshold(const Ptr<const WifiPpdu> ppdu) const
 {
@@ -527,7 +533,7 @@
          * aCCATime after the signal starts and shall continue to indicate the 20 MHz subchannel is
          * busy while the threshold continues to be exceeded.
          */
-        dBm_u ccaThreshold = -62;
+        dBm_u ccaThreshold = m_wifiPhy->GetCcaEdThreshold();
         auto delayUntilCcaEnd = GetDelayUntilCcaEnd(ccaThreshold, band);
 
         if (ppdu)
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/eht/eht-phy.h ns-3.47/src/wifi/model/eht/eht-phy.h
--- ns-3.46/src/wifi/model/eht/eht-phy.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/eht/eht-phy.h	2026-02-16 16:08:49.857231000 -0800
@@ -252,6 +252,7 @@
                                 WifiPpduField field) override;
     WifiPhyRxfailureReason GetFailureReason(WifiPpduField field) const override;
     uint32_t GetSigBSize(const WifiTxVector& txVector) const override;
+    uint32_t GetMaxPsduSize() const override;
     dBm_u GetCcaThreshold(const Ptr<const WifiPpdu> ppdu,
                           WifiChannelListType channelType) const override;
     const std::map<MHz_u, WifiChannelListType>& GetCcaSecondaryChannels() const override;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/frame-exchange-manager.h ns-3.47/src/wifi/model/frame-exchange-manager.h
--- ns-3.46/src/wifi/model/frame-exchange-manager.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/frame-exchange-manager.h	2026-02-16 16:08:49.858230800 -0800
@@ -315,6 +315,7 @@
 
     /**
      * @return the remote station manager operating on our link
+     * @hidecaller
      */
     Ptr<WifiRemoteStationManager> GetWifiRemoteStationManager() const;
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/he/he-frame-exchange-manager.cc ns-3.47/src/wifi/model/he/he-frame-exchange-manager.cc
--- ns-3.46/src/wifi/model/he/he-frame-exchange-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/he/he-frame-exchange-manager.cc	2026-02-16 16:08:49.859231000 -0800
@@ -1516,7 +1516,7 @@
     {
         NS_LOG_LOGIC("AP requested using the max transmit power (" << m_phy->GetTxPowerEnd()
                                                                    << " dBm)");
-        v.SetTxPowerLevel(m_phy->GetNTxPower());
+        v.SetTxPowerLevel(m_phy->GetNTxPowerLevels());
         return v;
     }
 
@@ -1547,7 +1547,7 @@
     auto reqTxPower = dBm_u{static_cast<double>(userInfoIt->GetUlTargetRxPower() + pathLossDb)};
 
     // Convert the transmit power to a power level
-    uint8_t numPowerLevels = m_phy->GetNTxPower();
+    const auto numPowerLevels = m_phy->GetNTxPowerLevels();
     if (numPowerLevels > 1)
     {
         dBm_u step = (m_phy->GetTxPowerEnd() - m_phy->GetTxPowerStart()) / (numPowerLevels - 1);
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/he/he-phy.cc ns-3.47/src/wifi/model/he/he-phy.cc
--- ns-3.46/src/wifi/model/he/he-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/he/he-phy.cc	2026-02-16 16:08:49.860231000 -0800
@@ -1118,11 +1118,13 @@
 }
 
 void
-HePhy::SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu)
+HePhy::SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu /* = nullptr */)
 {
     NS_LOG_FUNCTION(this);
+
     const auto ccaIndication = GetCcaIndication(ppdu);
     const auto per20MHzDurations = GetPer20MHzDurations(ppdu);
+
     if (ccaIndication.has_value())
     {
         NS_LOG_DEBUG("CCA busy for " << ccaIndication.value().second << " during "
@@ -1130,11 +1132,19 @@
         NotifyCcaBusy(ccaIndication.value().first, ccaIndication.value().second, per20MHzDurations);
         return;
     }
+
     if (ppdu)
     {
-        SwitchMaybeToCcaBusy(nullptr);
+        SwitchMaybeToCcaBusy();
         return;
     }
+
+    // avoid spurious notification if the channel width changed
+    if (m_lastPer20MHzDurations.size() != per20MHzDurations.size())
+    {
+        m_lastPer20MHzDurations.resize(per20MHzDurations.size());
+    }
+
     if (per20MHzDurations != m_lastPer20MHzDurations)
     {
         /*
@@ -1144,6 +1154,12 @@
         NS_LOG_DEBUG("per-20MHz CCA durations changed");
         NotifyCcaBusy(Seconds(0), WIFI_CHANLIST_PRIMARY, per20MHzDurations);
     }
+
+    if (m_wifiPhy->IsStateCcaBusy())
+    {
+        NS_LOG_DEBUG("Update CCA indication to IDLE");
+        m_state->SwitchMaybeToCcaBusy(Seconds(0), WIFI_CHANLIST_PRIMARY, per20MHzDurations);
+    }
 }
 
 void
@@ -1779,7 +1795,7 @@
 uint32_t
 HePhy::GetMaxPsduSize() const
 {
-    return 6500631;
+    return WIFI_PSDU_MAX_LENGTH_HE;
 }
 
 bool
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/he/he-phy.h ns-3.47/src/wifi/model/he/he-phy.h
--- ns-3.46/src/wifi/model/he/he-phy.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/he/he-phy.h	2026-02-16 16:08:49.860231000 -0800
@@ -100,7 +100,7 @@
     Time CalculateTxDuration(const WifiConstPsduMap& psduMap,
                              const WifiTxVector& txVector,
                              WifiPhyBand band) const override;
-    void SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu) override;
+    void SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu = nullptr) override;
     dBm_u GetCcaThreshold(const Ptr<const WifiPpdu> ppdu,
                           WifiChannelListType channelType) const override;
     void NotifyCcaBusy(const Ptr<const WifiPpdu> ppdu,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/he/obss-pd-algorithm.h ns-3.47/src/wifi/model/he/obss-pd-algorithm.h
--- ns-3.46/src/wifi/model/he/obss-pd-algorithm.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/he/obss-pd-algorithm.h	2026-02-16 16:08:49.861230900 -0800
@@ -65,17 +65,17 @@
     /**
      * TracedCallback signature for OBSS_PD reset events.
      *
-     * @param [in] bssColor The BSS color of frame triggering the reset
-     * @param [in] rssiDbm The RSSI (dBm) of frame triggering the reset
+     * @param [in] bssColor The BSS color of the frame triggering the reset
+     * @param [in] rssiDbm The RSSI of the frame triggering the reset
      * @param [in] powerRestricted Whether a TX power restriction is triggered
-     * @param [in] txPowerMaxDbmSiso The SISO TX power restricted level (dBm)
-     * @param [in] txPowerMaxDbmMimo The MIMO TX power restricted level (dBm)
+     * @param [in] txPowerMaxDbmSiso The SISO TX power restricted level
+     * @param [in] txPowerMaxDbmMimo The MIMO TX power restricted level
      */
     typedef void (*ResetTracedCallback)(uint8_t bssColor,
-                                        double rssiDbm,
+                                        dBm_u rssiDbm,
                                         bool powerRestricted,
-                                        double txPowerMaxDbmSiso,
-                                        double txPowerMaxDbmMimo);
+                                        dBm_u txPowerMaxDbmSiso,
+                                        dBm_u txPowerMaxDbmMimo);
 
     /**
      * @param level the current OBSS PD level
@@ -101,7 +101,7 @@
     /**
      * TracedCallback signature for PHY reset events.
      */
-    TracedCallback<uint8_t, double, bool, double, double> m_resetEvent;
+    TracedCallback<uint8_t, dBm_u, bool, dBm_u, dBm_u> m_resetEvent;
 };
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/ht/ht-phy.cc ns-3.47/src/wifi/model/ht/ht-phy.cc
--- ns-3.46/src/wifi/model/ht/ht-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/ht/ht-phy.cc	2026-02-16 16:08:49.862231000 -0800
@@ -816,7 +816,7 @@
 uint32_t
 HtPhy::GetMaxPsduSize() const
 {
-    return 65535;
+    return WIFI_PSDU_MAX_LENGTH_HT;
 }
 
 const std::map<MHz_u, WifiChannelListType>&
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/interference-helper.h ns-3.47/src/wifi/model/interference-helper.h
--- ns-3.46/src/wifi/model/interference-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/interference-helper.h	2026-02-16 16:08:49.862231000 -0800
@@ -25,11 +25,6 @@
 class ErrorRateModel;
 
 /**
- * A map of the received power for each band
- */
-using RxPowerWattPerChannelBand = std::map<WifiSpectrumBandInfo, Watt_u>;
-
-/**
  * @ingroup wifi
  * @brief handles interference calculations
  * @brief signal event for a PPDU.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/non-ht/dsss-error-rate-model.cc ns-3.47/src/wifi/model/non-ht/dsss-error-rate-model.cc
--- ns-3.46/src/wifi/model/non-ht/dsss-error-rate-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/non-ht/dsss-error-rate-model.cc	2026-02-16 16:08:49.863231000 -0800
@@ -25,8 +25,10 @@
 NS_LOG_COMPONENT_DEFINE("DsssErrorRateModel");
 
 #ifndef HAVE_GSL
-const double DsssErrorRateModel::WLAN_SIR_PERFECT = 10.0;
-const double DsssErrorRateModel::WLAN_SIR_IMPOSSIBLE = 0.1;
+/// Perfect Signal to Interference Ratio
+constexpr double WLAN_SIR_PERFECT = 10.0;
+/// Impossible Signal to Interference Ratio
+constexpr double WLAN_SIR_IMPOSSIBLE = 0.1;
 #endif
 
 double
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/non-ht/dsss-error-rate-model.h ns-3.47/src/wifi/model/non-ht/dsss-error-rate-model.h
--- ns-3.46/src/wifi/model/non-ht/dsss-error-rate-model.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/non-ht/dsss-error-rate-model.h	2026-02-16 16:08:49.863231000 -0800
@@ -110,13 +110,6 @@
 #ifdef HAVE_GSL
     static double SymbolErrorProb16Cck(double e2);  /// equation (18) in Pursley's paper
     static double SymbolErrorProb256Cck(double e1); /// equation (17) in Pursley's paper
-#else
-
-  protected:
-    /// WLAN perfect
-    static const double WLAN_SIR_PERFECT;
-    /// WLAN impossible
-    static const double WLAN_SIR_IMPOSSIBLE;
 #endif
 };
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/non-ht/dsss-phy.cc ns-3.47/src/wifi/model/non-ht/dsss-phy.cc
--- ns-3.46/src/wifi/model/non-ht/dsss-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/non-ht/dsss-phy.cc	2026-02-16 16:08:49.863231000 -0800
@@ -387,7 +387,7 @@
 uint32_t
 DsssPhy::GetMaxPsduSize() const
 {
-    return 4095;
+    return WIFI_PSDU_MAX_LENGTH;
 }
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/non-ht/erp-ofdm-phy.cc ns-3.47/src/wifi/model/non-ht/erp-ofdm-phy.cc
--- ns-3.46/src/wifi/model/non-ht/erp-ofdm-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/non-ht/erp-ofdm-phy.cc	2026-02-16 16:08:49.864230900 -0800
@@ -234,7 +234,7 @@
 uint32_t
 ErpOfdmPhy::GetMaxPsduSize() const
 {
-    return 4095;
+    return WIFI_PSDU_MAX_LENGTH;
 }
 
 } // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/non-ht/ofdm-phy.cc ns-3.47/src/wifi/model/non-ht/ofdm-phy.cc
--- ns-3.46/src/wifi/model/non-ht/ofdm-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/non-ht/ofdm-phy.cc	2026-02-16 16:08:49.864230900 -0800
@@ -650,7 +650,7 @@
 uint32_t
 OfdmPhy::GetMaxPsduSize() const
 {
-    return 4095;
+    return WIFI_PSDU_MAX_LENGTH;
 }
 
 MHz_u
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/phy-entity.cc ns-3.47/src/wifi/model/phy-entity.cc
--- ns-3.46/src/wifi/model/phy-entity.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/phy-entity.cc	2026-02-16 16:08:49.864230900 -0800
@@ -1326,12 +1326,7 @@
 void
 PhyEntity::SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu)
 {
-    // We are here because we have received the first bit of a packet and we are
-    // not going to be able to synchronize on it
-    // In this model, CCA becomes busy when the aggregation of all signals as
-    // tracked by the InterferenceHelper class is higher than the CcaBusyThreshold
-    const auto ccaIndication = GetCcaIndication(ppdu);
-    if (ccaIndication.has_value())
+    if (const auto ccaIndication = GetCcaIndication(ppdu))
     {
         NS_LOG_DEBUG("CCA busy for " << ccaIndication.value().second << " during "
                                      << ccaIndication.value().first.As(Time::S));
@@ -1340,9 +1335,17 @@
                                       {});
         return;
     }
+
     if (ppdu)
     {
-        SwitchMaybeToCcaBusy(nullptr);
+        SwitchMaybeToCcaBusy();
+        return;
+    }
+
+    if (m_wifiPhy->IsStateCcaBusy())
+    {
+        NS_LOG_DEBUG("Update CCA indication to IDLE");
+        m_state->SwitchMaybeToCcaBusy(Seconds(0), WIFI_CHANLIST_PRIMARY, {});
     }
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/phy-entity.h ns-3.47/src/wifi/model/phy-entity.h
--- ns-3.46/src/wifi/model/phy-entity.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/phy-entity.h	2026-02-16 16:08:49.864230900 -0800
@@ -13,6 +13,7 @@
 #define PHY_ENTITY_H
 
 #include "wifi-phy-band.h"
+#include "wifi-phy-common.h"
 #include "wifi-phy-state-helper.h"
 #include "wifi-phy.h"
 #include "wifi-ppdu.h"
@@ -35,18 +36,12 @@
  * @ingroup wifi
  * Declaration of:
  * - ns3::PhyEntity class
- * - ns3::SignalNoiseDbm, ns3::MpduInfo, and ns3::RxSignalInfo structs
  * - ns3::RxPowerWattPerChannelBand typedef
  */
 
 namespace ns3
 {
 
-/**
- * A map of the received power for each band
- */
-using RxPowerWattPerChannelBand = std::map<WifiSpectrumBandInfo, Watt_u>;
-
 class WifiMpdu;
 class WifiPhy;
 class InterferenceHelper;
@@ -401,7 +396,8 @@
      *
      * @param ppdu the incoming PPDU or nullptr for any signal
      */
-    virtual void SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu);
+    virtual void SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu = nullptr);
+
     /**
      * Notify PHY state helper to switch to CCA busy state,
      *
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/amrr-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/amrr-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/amrr-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/amrr-wifi-manager.cc	2026-02-16 16:08:49.865231000 -0800
@@ -12,8 +12,6 @@
 #include "ns3/simulator.h"
 #include "ns3/wifi-tx-vector.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/aparf-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/aparf-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/aparf-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/aparf-wifi-manager.cc	2026-02-16 16:08:49.865231000 -0800
@@ -13,8 +13,6 @@
 #include "ns3/uinteger.h"
 #include "ns3/wifi-phy.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
@@ -78,14 +76,14 @@
                           MakeUintegerAccessor(&AparfWifiManager::m_powerMax),
                           MakeUintegerChecker<uint32_t>())
             .AddAttribute("PowerDecrementStep",
-                          "Step size for decrement the power.",
+                          "Step size for decrement the power level.",
                           UintegerValue(1),
-                          MakeUintegerAccessor(&AparfWifiManager::m_powerDec),
+                          MakeUintegerAccessor(&AparfWifiManager::m_powerLevelDec),
                           MakeUintegerChecker<uint8_t>())
             .AddAttribute("PowerIncrementStep",
-                          "Step size for increment the power.",
+                          "Step size for increment the power level.",
                           UintegerValue(1),
-                          MakeUintegerAccessor(&AparfWifiManager::m_powerInc),
+                          MakeUintegerAccessor(&AparfWifiManager::m_powerLevelInc),
                           MakeUintegerChecker<uint8_t>())
             .AddAttribute("RateDecrementStep",
                           "Step size for decrement the rate.",
@@ -122,8 +120,7 @@
 AparfWifiManager::SetupPhy(const Ptr<WifiPhy> phy)
 {
     NS_LOG_FUNCTION(this << phy);
-    m_minPower = 0;
-    m_maxPower = phy->GetNTxPower() - 1;
+    m_maxPowerLevel = WIFI_MIN_TX_PWR_LEVEL + phy->GetNTxPowerLevels() - 1;
     WifiRemoteStationManager::SetupPhy(phy);
 }
 
@@ -173,13 +170,13 @@
         station->m_nSupported = GetNSupported(station);
         station->m_rateIndex = station->m_nSupported - 1;
         station->m_prevRateIndex = station->m_nSupported - 1;
-        station->m_powerLevel = m_maxPower;
-        station->m_prevPowerLevel = m_maxPower;
+        station->m_powerLevel = m_maxPowerLevel;
+        station->m_prevPowerLevel = m_maxPowerLevel;
         station->m_critRateIndex = 0;
         WifiMode mode = GetSupported(station, station->m_rateIndex);
         auto channelWidth = GetChannelWidth(station);
         DataRate rate(mode.GetDataRate(channelWidth));
-        const auto power = GetPhy()->GetPower(m_maxPower);
+        const auto power = GetPhy()->GetPower(m_maxPowerLevel);
         m_powerChange(power, power, station->m_state->m_address);
         m_rateChange(rate, rate, station->m_state->m_address);
         station->m_initialized = true;
@@ -219,7 +216,7 @@
         station->m_nFailed = 0;
         station->m_nSuccess = 0;
         station->m_pCount = 0;
-        if (station->m_powerLevel == m_maxPower)
+        if (station->m_powerLevel == m_maxPowerLevel)
         {
             station->m_critRateIndex = station->m_rateIndex;
             if (station->m_rateIndex != 0)
@@ -231,7 +228,7 @@
         else
         {
             NS_LOG_DEBUG("station=" << station << " inc power");
-            station->m_powerLevel += m_powerInc;
+            station->m_powerLevel += m_powerLevelInc;
         }
     }
 }
@@ -286,10 +283,10 @@
         station->m_nFailed = 0;
         if (station->m_rateIndex == (station->m_state->m_operationalRateSet.size() - 1))
         {
-            if (station->m_powerLevel != m_minPower)
+            if (station->m_powerLevel != m_minPowerLevel)
             {
                 NS_LOG_DEBUG("station=" << station << " dec power");
-                station->m_powerLevel -= m_powerDec;
+                station->m_powerLevel -= m_powerLevelDec;
             }
         }
         else
@@ -306,16 +303,16 @@
             {
                 if (station->m_pCount == m_powerMax)
                 {
-                    station->m_powerLevel = m_maxPower;
+                    station->m_powerLevel = m_maxPowerLevel;
                     station->m_rateIndex = station->m_critRateIndex;
                     station->m_pCount = 0;
                     station->m_critRateIndex = 0;
                 }
                 else
                 {
-                    if (station->m_powerLevel != m_minPower)
+                    if (station->m_powerLevel != m_minPowerLevel)
                     {
-                        station->m_powerLevel -= m_powerDec;
+                        station->m_powerLevel -= m_powerLevelDec;
                         station->m_pCount++;
                     }
                 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/aparf-wifi-manager.h ns-3.47/src/wifi/model/rate-control/aparf-wifi-manager.h
--- ns-3.46/src/wifi/model/rate-control/aparf-wifi-manager.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/aparf-wifi-manager.h	2026-02-16 16:08:49.866231000 -0800
@@ -87,22 +87,22 @@
                             //!< try a new power or rate.
     uint32_t m_failMax;  //!< The minimum number of failed transmissions to try a new power or rate.
     uint32_t m_powerMax; //!< The maximum number of power changes.
-    uint8_t m_powerInc;  //!< Step size for increment the power.
-    uint8_t m_powerDec;  //!< Step size for decrement the power.
-    uint8_t m_rateInc;   //!< Step size for increment the rate.
-    uint8_t m_rateDec;   //!< Step size for decrement the rate.
+    uint8_t m_powerLevelInc; //!< Step size for increment the power level.
+    uint8_t m_powerLevelDec; //!< Step size for decrement the power level.
+    uint8_t m_rateInc;       //!< Step size for increment the rate.
+    uint8_t m_rateDec;       //!< Step size for decrement the rate.
 
     /**
      * Minimal power level.
      * Differently form rate, power levels do not depend on the remote station.
      * The levels depend only on the physical layer of the device.
      */
-    uint8_t m_minPower;
+    uint8_t m_minPowerLevel{WIFI_MIN_TX_PWR_LEVEL};
 
     /**
      * Maximal power level.
      */
-    uint8_t m_maxPower;
+    uint8_t m_maxPowerLevel{WIFI_MIN_TX_PWR_LEVEL};
 
     /**
      * The trace source fired when the transmission power changes.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/arf-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/arf-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/arf-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/arf-wifi-manager.cc	2026-02-16 16:08:49.866231000 -0800
@@ -11,8 +11,6 @@
 #include "ns3/log.h"
 #include "ns3/wifi-tx-vector.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/cara-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/cara-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/cara-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/cara-wifi-manager.cc	2026-02-16 16:08:49.866231000 -0800
@@ -11,8 +11,6 @@
 #include "ns3/log.h"
 #include "ns3/wifi-tx-vector.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/minstrel-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/minstrel-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/minstrel-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/minstrel-wifi-manager.cc	2026-02-16 16:08:49.866231000 -0800
@@ -30,8 +30,6 @@
 #include <iomanip>
 #include <limits>
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/onoe-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/onoe-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/onoe-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/onoe-wifi-manager.cc	2026-02-16 16:08:49.867231000 -0800
@@ -12,8 +12,6 @@
 #include "ns3/simulator.h"
 #include "ns3/wifi-tx-vector.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/parf-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/parf-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/parf-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/parf-wifi-manager.cc	2026-02-16 16:08:49.867231000 -0800
@@ -13,8 +13,6 @@
 #include "ns3/uinteger.h"
 #include "ns3/wifi-phy.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
@@ -88,8 +86,7 @@
 ParfWifiManager::SetupPhy(const Ptr<WifiPhy> phy)
 {
     NS_LOG_FUNCTION(this << phy);
-    m_minPower = 0;
-    m_maxPower = phy->GetNTxPower() - 1;
+    m_maxPowerLevel = WIFI_MIN_TX_PWR_LEVEL + phy->GetNTxPowerLevels() - 1;
     WifiRemoteStationManager::SetupPhy(phy);
 }
 
@@ -140,12 +137,12 @@
         station->m_nSupported = GetNSupported(station);
         station->m_rateIndex = station->m_nSupported - 1;
         station->m_prevRateIndex = station->m_nSupported - 1;
-        station->m_powerLevel = m_maxPower;
-        station->m_prevPowerLevel = m_maxPower;
+        station->m_powerLevel = m_maxPowerLevel;
+        station->m_prevPowerLevel = m_maxPowerLevel;
         WifiMode mode = GetSupported(station, station->m_rateIndex);
         auto channelWidth = GetChannelWidth(station);
         DataRate rate(mode.GetDataRate(channelWidth));
-        const auto power = GetPhy()->GetPower(m_maxPower);
+        const auto power = GetPhy()->GetPower(m_maxPowerLevel);
         m_powerChange(power, power, station->m_state->m_address);
         m_rateChange(rate, rate, station->m_state->m_address);
         station->m_initialized = true;
@@ -202,7 +199,7 @@
         if (station->m_nRetry == 1)
         {
             // need recovery fallback
-            if (station->m_powerLevel < m_maxPower)
+            if (station->m_powerLevel < m_maxPowerLevel)
             {
                 NS_LOG_DEBUG("station=" << station << " inc power");
                 station->m_powerLevel++;
@@ -217,7 +214,7 @@
         if (((station->m_nRetry - 1) % 2) == 1)
         {
             // need normal fallback
-            if (station->m_powerLevel == m_maxPower)
+            if (station->m_powerLevel == m_maxPowerLevel)
             {
                 if (station->m_rateIndex != 0)
                 {
@@ -285,7 +282,7 @@
     else if (station->m_nSuccess == m_successThreshold || station->m_nAttempt == m_attemptThreshold)
     {
         // we are at the maximum rate, we decrease power
-        if (station->m_powerLevel != m_minPower)
+        if (station->m_powerLevel != m_minPowerLevel)
         {
             NS_LOG_DEBUG("station=" << station << " dec power");
             station->m_powerLevel--;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/parf-wifi-manager.h ns-3.47/src/wifi/model/rate-control/parf-wifi-manager.h
--- ns-3.46/src/wifi/model/rate-control/parf-wifi-manager.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/parf-wifi-manager.h	2026-02-16 16:08:49.867231000 -0800
@@ -80,12 +80,12 @@
      * In contrast to rate, power levels do not depend on the remote station.
      * The levels depend only on the physical layer of the device.
      */
-    uint8_t m_minPower;
+    uint8_t m_minPowerLevel{WIFI_MIN_TX_PWR_LEVEL};
 
     /**
      * Maximal power level.
      */
-    uint8_t m_maxPower;
+    uint8_t m_maxPowerLevel{WIFI_MIN_TX_PWR_LEVEL};
 
     /**
      * The trace source fired when the transmission power changes.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/rraa-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/rraa-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/rraa-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/rraa-wifi-manager.cc	2026-02-16 16:08:49.867231000 -0800
@@ -14,8 +14,6 @@
 #include "ns3/wifi-mac.h"
 #include "ns3/wifi-phy.h"
 
-#define Min(a, b) ((a < b) ? a : b)
-
 namespace ns3
 {
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/rate-control/rrpaa-wifi-manager.cc ns-3.47/src/wifi/model/rate-control/rrpaa-wifi-manager.cc
--- ns-3.46/src/wifi/model/rate-control/rrpaa-wifi-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/rate-control/rrpaa-wifi-manager.cc	2026-02-16 16:08:49.867231000 -0800
@@ -140,9 +140,9 @@
     NS_LOG_FUNCTION(this << phy);
     m_sifs = phy->GetSifs();
     m_difs = m_sifs + 2 * phy->GetSlot();
-    m_nPowerLevels = phy->GetNTxPower();
-    m_maxPowerLevel = m_nPowerLevels - 1;
-    m_minPowerLevel = 0;
+    m_nPowerLevels = phy->GetNTxPowerLevels();
+    m_minPowerLevel = WIFI_MIN_TX_PWR_LEVEL;
+    m_maxPowerLevel = WIFI_MIN_TX_PWR_LEVEL + m_nPowerLevels - 1;
     for (const auto& mode : phy->GetModeList())
     {
         WifiTxVector txVector;
@@ -489,25 +489,34 @@
         (static_cast<double>(station->m_counter + station->m_nFailed) / thresholds.m_ewnd);
     NS_LOG_DEBUG("Best loss prob= " << bploss);
     NS_LOG_DEBUG("Worst loss prob= " << wploss);
+    NS_ASSERT(station->m_powerLevel >= WIFI_MIN_TX_PWR_LEVEL);
     if (bploss >= thresholds.m_mtl)
     {
         if (station->m_powerLevel < m_maxPowerLevel)
         {
             NS_LOG_DEBUG("bploss >= MTL and power < maxPower => Increase Power");
-            station->m_pdTable[station->m_rateIndex][station->m_powerLevel] /= m_gamma;
+            station
+                ->m_pdTable[station->m_rateIndex][station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL] /=
+                m_gamma;
             NS_LOG_DEBUG("pdTable["
-                         << +station->m_rateIndex << "][" << station->m_powerLevel << "] = "
-                         << station->m_pdTable[station->m_rateIndex][station->m_powerLevel]);
+                         << +station->m_rateIndex << "]["
+                         << station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL << "] = "
+                         << station->m_pdTable[station->m_rateIndex]
+                                              [station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL]);
             station->m_powerLevel++;
             ResetCountersBasic(station);
         }
         else if (station->m_rateIndex != 0)
         {
             NS_LOG_DEBUG("bploss >= MTL and power = maxPower => Decrease Rate");
-            station->m_pdTable[station->m_rateIndex][station->m_powerLevel] /= m_gamma;
+            station
+                ->m_pdTable[station->m_rateIndex][station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL] /=
+                m_gamma;
             NS_LOG_DEBUG("pdTable["
-                         << +station->m_rateIndex << "][" << station->m_powerLevel << "] = "
-                         << station->m_pdTable[station->m_rateIndex][station->m_powerLevel]);
+                         << +station->m_rateIndex << "]["
+                         << +station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL << "] = "
+                         << station->m_pdTable[station->m_rateIndex]
+                                              [station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL]);
             station->m_rateIndex--;
             ResetCountersBasic(station);
         }
@@ -525,16 +534,19 @@
             // Recalculate probabilities of lower rates.
             for (uint8_t i = 0; i <= station->m_rateIndex; i++)
             {
-                station->m_pdTable[i][station->m_powerLevel] *= m_delta;
-                if (station->m_pdTable[i][station->m_powerLevel] > 1)
+                station->m_pdTable[i][station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL] *= m_delta;
+                if (station->m_pdTable[i][station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL] > 1)
                 {
-                    station->m_pdTable[i][station->m_powerLevel] = 1;
+                    station->m_pdTable[i][station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL] = 1;
                 }
-                NS_LOG_DEBUG("pdTable[" << i << "][" << (int)station->m_powerLevel
-                                        << "] = " << station->m_pdTable[i][station->m_powerLevel]);
+                NS_LOG_DEBUG(
+                    "pdTable["
+                    << i << "][" << +station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL << "] = "
+                    << station->m_pdTable[i][station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL]);
             }
             double rand = m_uniformRandomVariable->GetValue(0, 1);
-            if (rand < station->m_pdTable[station->m_rateIndex + 1][station->m_powerLevel])
+            if (rand < station->m_pdTable[station->m_rateIndex + 1]
+                                         [station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL])
             {
                 NS_LOG_DEBUG("Increase Rate");
                 station->m_rateIndex++;
@@ -547,16 +559,20 @@
             // Recalculate probabilities of higher powers.
             for (uint32_t i = m_maxPowerLevel; i > station->m_powerLevel; i--)
             {
-                station->m_pdTable[station->m_rateIndex][i] *= m_delta;
-                if (station->m_pdTable[station->m_rateIndex][i] > 1)
+                NS_ASSERT(i >= WIFI_MIN_TX_PWR_LEVEL);
+                station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL] *= m_delta;
+                if (station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL] > 1)
                 {
-                    station->m_pdTable[station->m_rateIndex][i] = 1;
+                    station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL] = 1;
                 }
-                NS_LOG_DEBUG("pdTable[" << +station->m_rateIndex << "][" << i
-                                        << "] = " << station->m_pdTable[station->m_rateIndex][i]);
+                NS_LOG_DEBUG(
+                    "pdTable["
+                    << +station->m_rateIndex << "][" << i << "] = "
+                    << station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL]);
             }
             double rand = m_uniformRandomVariable->GetValue(0, 1);
-            if (rand < station->m_pdTable[station->m_rateIndex][station->m_powerLevel - 1])
+            if (rand < station->m_pdTable[station->m_rateIndex]
+                                         [station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL])
             {
                 NS_LOG_DEBUG("Decrease Power");
                 station->m_powerLevel--;
@@ -574,16 +590,20 @@
             // Recalculate probabilities of higher powers.
             for (uint32_t i = m_maxPowerLevel; i >= station->m_powerLevel; i--)
             {
-                station->m_pdTable[station->m_rateIndex][i] *= m_delta;
-                if (station->m_pdTable[station->m_rateIndex][i] > 1)
+                NS_ASSERT(i >= WIFI_MIN_TX_PWR_LEVEL);
+                station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL] *= m_delta;
+                if (station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL] > 1)
                 {
-                    station->m_pdTable[station->m_rateIndex][i] = 1;
+                    station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL] = 1;
                 }
-                NS_LOG_DEBUG("pdTable[" << +station->m_rateIndex << "][" << i
-                                        << "] = " << station->m_pdTable[station->m_rateIndex][i]);
+                NS_LOG_DEBUG(
+                    "pdTable["
+                    << +station->m_rateIndex << "][" << i << "] = "
+                    << station->m_pdTable[station->m_rateIndex][i - WIFI_MIN_TX_PWR_LEVEL]);
             }
             double rand = m_uniformRandomVariable->GetValue(0, 1);
-            if (rand < station->m_pdTable[station->m_rateIndex][station->m_powerLevel - 1])
+            if (rand < station->m_pdTable[station->m_rateIndex]
+                                         [station->m_powerLevel - WIFI_MIN_TX_PWR_LEVEL])
             {
                 NS_LOG_DEBUG("Decrease Power");
                 station->m_powerLevel--;
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/spectrum-wifi-phy.cc ns-3.47/src/wifi/model/spectrum-wifi-phy.cc
--- ns-3.46/src/wifi/model/spectrum-wifi-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/spectrum-wifi-phy.cc	2026-02-16 16:08:49.868231000 -0800
@@ -601,7 +601,7 @@
                                          rxPowers,
                                          interface ? interface->GetFrequencyRange()
                                                    : GetCurrentFrequencyRange());
-        SwitchMaybeToCcaBusy(nullptr);
+        SwitchMaybeToCcaBusy();
         return;
     }
 
@@ -610,7 +610,7 @@
         NS_LOG_INFO("Received Wi-Fi signal but blocked from syncing");
         NS_ASSERT(interface);
         m_interference->AddForeignSignal(rxDuration, rxPowers, interface->GetFrequencyRange());
-        SwitchMaybeToCcaBusy(nullptr);
+        SwitchMaybeToCcaBusy();
         return;
     }
 
@@ -626,7 +626,7 @@
                             ? " (" + std::to_string(WToDbm(totalRxPower)) + " dBm)"
                             : ""));
         m_interference->Add(ppdu, rxDuration, rxPowers, GetCurrentFrequencyRange());
-        SwitchMaybeToCcaBusy(nullptr);
+        SwitchMaybeToCcaBusy();
         return;
     }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/table-based-error-rate-model.cc ns-3.47/src/wifi/model/table-based-error-rate-model.cc
--- ns-3.46/src/wifi/model/table-based-error-rate-model.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/table-based-error-rate-model.cc	2026-02-16 16:08:49.868231000 -0800
@@ -25,8 +25,8 @@
 namespace ns3
 {
 
-static const double SNR_PRECISION = 2;                         //!< precision for SNR
-static const double TABLED_BASED_ERROR_MODEL_PRECISION = 1e-5; //!< precision for PER
+static const double SNR_PRECISION = 2;                        //!< precision for SNR
+static const double TABLE_BASED_ERROR_MODEL_PRECISION = 1e-5; //!< precision for PER
 
 NS_OBJECT_ENSURE_REGISTERED(TableBasedErrorRateModel);
 
@@ -232,7 +232,7 @@
         per = (1.0 - std::pow((1 - per), (static_cast<double>(size) / tableSize)));
     }
 
-    if (per < TABLED_BASED_ERROR_MODEL_PRECISION)
+    if (per < TABLE_BASED_ERROR_MODEL_PRECISION)
     {
         per = 0.0;
     }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/vht/vht-phy.cc ns-3.47/src/wifi/model/vht/vht-phy.cc
--- ns-3.46/src/wifi/model/vht/vht-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/vht/vht-phy.cc	2026-02-16 16:08:49.869231000 -0800
@@ -520,7 +520,7 @@
 uint32_t
 VhtPhy::GetMaxPsduSize() const
 {
-    return 4692480;
+    return WIFI_PSDU_MAX_LENGTH_VHT;
 }
 
 dBm_u
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-mac-header.h ns-3.47/src/wifi/model/wifi-mac-header.h
--- ns-3.46/src/wifi/model/wifi-mac-header.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-mac-header.h	2026-02-16 16:08:49.870231000 -0800
@@ -148,6 +148,7 @@
      * Fill the Address 1 field with the given address.
      *
      * @param address the address to be used in the Address 1 field
+     * @hidecaller
      */
     void SetAddr1(Mac48Address address);
     /**
@@ -175,6 +176,7 @@
      * @param type the WifiMacType for the header
      * @param resetToDsFromDs whether the ToDs and FromDs flags
      *        should be reset.
+     * @hidecaller
      */
     virtual void SetType(WifiMacType type, bool resetToDsFromDs = true);
     /**
@@ -293,6 +295,7 @@
      * Return the address in the Address 1 field.
      *
      * @return the address in the Address 1 field
+     * @hidecaller
      */
     Mac48Address GetAddr1() const;
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-mode.h ns-3.47/src/wifi/model/wifi-mode.h
--- ns-3.46/src/wifi/model/wifi-mode.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-mode.h	2026-02-16 16:08:49.871231000 -0800
@@ -138,6 +138,8 @@
      *
      * @returns the Modulation Class (Section 9.7.8 "Modulation classes"; IEEE 802.11-2012)
      * to which this WifiMode belongs.
+     *
+     * @hidecaller
      */
     WifiModulationClass GetModulationClass() const;
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-phy.cc ns-3.47/src/wifi/model/wifi-phy.cc
--- ns-3.46/src/wifi/model/wifi-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-phy.cc	2026-02-16 16:08:49.873231000 -0800
@@ -172,7 +172,7 @@
                           "Number of transmission power levels available between "
                           "TxPowerStart and TxPowerEnd included.",
                           UintegerValue(1),
-                          MakeUintegerAccessor(&WifiPhy::m_nTxPower),
+                          MakeUintegerAccessor(&WifiPhy::m_nTxPowerLevels),
                           MakeUintegerChecker<uint8_t>())
             .AddAttribute("TxPowerEnd",
                           "Maximum available transmission level (dBm).",
@@ -502,7 +502,7 @@
     if (IsInitialized())
     {
         // provide CCA busy information upon registering a PHY listener
-        SwitchMaybeToCcaBusy(nullptr);
+        SwitchMaybeToCcaBusy();
     }
 }
 
@@ -536,6 +536,10 @@
 {
     NS_LOG_FUNCTION(this << threshold);
     m_ccaEdThreshold = threshold;
+    if (IsInitialized())
+    {
+        SwitchMaybeToCcaBusy();
+    }
 }
 
 dBm_u
@@ -595,16 +599,16 @@
 }
 
 void
-WifiPhy::SetNTxPower(uint8_t n)
+WifiPhy::SetNTxPowerLevels(uint8_t n)
 {
     NS_LOG_FUNCTION(this << +n);
-    m_nTxPower = n;
+    m_nTxPowerLevels = n;
 }
 
 uint8_t
-WifiPhy::GetNTxPower() const
+WifiPhy::GetNTxPowerLevels() const
 {
-    return m_nTxPower;
+    return m_nTxPowerLevels;
 }
 
 void
@@ -728,20 +732,19 @@
 dBm_u
 WifiPhy::GetPower(uint8_t powerLevel) const
 {
+    NS_ASSERT_MSG((powerLevel >= WIFI_MIN_TX_PWR_LEVEL) &&
+                      (powerLevel < (WIFI_MIN_TX_PWR_LEVEL + m_nTxPowerLevels)),
+                  "Invalid TX power level");
     NS_ASSERT(m_txPowerBase <= m_txPowerEnd);
-    NS_ASSERT(m_nTxPower > 0);
-    dBm_u dbm;
-    if (m_nTxPower > 1)
+    NS_ASSERT(m_nTxPowerLevels > 0);
+    NS_ASSERT((m_nTxPowerLevels > 1) || (m_txPowerBase == m_txPowerEnd));
+    auto power{m_txPowerBase};
+    if (m_nTxPowerLevels > 1)
     {
-        dbm = m_txPowerBase + dB_u{powerLevel * (m_txPowerEnd - m_txPowerBase) / (m_nTxPower - 1)};
+        power += dB_u{(powerLevel - WIFI_MIN_TX_PWR_LEVEL) * (m_txPowerEnd - m_txPowerBase) /
+                      (m_nTxPowerLevels - 1)};
     }
-    else
-    {
-        NS_ASSERT_MSG(m_txPowerBase == m_txPowerEnd,
-                      "cannot have TxPowerEnd != TxPowerStart with TxPowerLevels == 1");
-        dbm = m_txPowerBase;
-    }
-    return dbm;
+    return power;
 }
 
 Time
@@ -1307,7 +1310,10 @@
          * state are added to the event list and are employed later to figure
          * out the state of the medium after the switching.
          */
-        SwitchMaybeToCcaBusy(nullptr);
+        Simulator::Schedule(GetChannelSwitchDelay(), [=, this]() {
+            NS_LOG_DEBUG("Channel switching completed: update CCA indication");
+            SwitchMaybeToCcaBusy();
+        });
     }
 }
 
@@ -2218,6 +2224,10 @@
 WifiPhy::SwitchMaybeToCcaBusy(const Ptr<const WifiPpdu> ppdu /* = nullptr */)
 {
     NS_LOG_FUNCTION(this);
+    if (!IsStateIdle() && !IsStateCcaBusy())
+    {
+        return;
+    }
     GetLatestPhyEntity()->SwitchMaybeToCcaBusy(ppdu);
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-phy-common.h ns-3.47/src/wifi/model/wifi-phy-common.h
--- ns-3.46/src/wifi/model/wifi-phy-common.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-phy-common.h	2026-02-16 16:08:49.872231000 -0800
@@ -57,6 +57,9 @@
 /// vector of spectrum bands
 using WifiSpectrumBands = std::vector<WifiSpectrumBandInfo>;
 
+/// A map of the received power for each band
+using RxPowerWattPerChannelBand = std::map<WifiSpectrumBandInfo, Watt_u>;
+
 /**
  * @ingroup wifi
  * Compare two bands.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-phy.h ns-3.47/src/wifi/model/wifi-phy.h
--- ns-3.46/src/wifi/model/wifi-phy.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-phy.h	2026-02-16 16:08:49.873231000 -0800
@@ -69,6 +69,9 @@
     /**
      * @brief Get the type ID.
      * @return the object TypeId
+     *
+     * Call graph was not generated because of its size.
+     * @hidecallgraph
      */
     static TypeId GetTypeId();
 
@@ -252,6 +255,8 @@
      * @param staId the STA-ID of the recipient (only used for MU)
      *
      * @return the total amount of time this PHY will stay busy for the transmission of these bytes.
+     *
+     * @hidecaller
      */
     static Time CalculateTxDuration(uint32_t size,
                                     const WifiTxVector& txVector,
@@ -860,15 +865,15 @@
      * minimum level and the maximum level. Transmission power levels are
      * equally separated (in dBm) with the minimum and the maximum included.
      *
-     * @param n the number of available levels
+     * @param numLevels the number of available levels
      */
-    void SetNTxPower(uint8_t n);
+    void SetNTxPowerLevels(uint8_t numLevels);
     /**
      * Return the number of available transmission power levels.
      *
      * @return the number of available transmission power levels
      */
-    uint8_t GetNTxPower() const;
+    uint8_t GetNTxPowerLevels() const;
     /**
      * Sets the transmission gain.
      *
@@ -1665,7 +1670,7 @@
     dB_u m_rxGain;                     //!< Reception gain
     dBm_u m_txPowerBase;               //!< Minimum transmission power
     dBm_u m_txPowerEnd;                //!< Maximum transmission power
-    uint8_t m_nTxPower;                //!< Number of available transmission power levels
+    uint8_t m_nTxPowerLevels;          //!< Number of available transmission power levels
     dBm_per_MHz_u m_powerDensityLimit; //!< the power density limit
 
     bool m_powerRestricted; //!< Flag whether transmit power is restricted by OBSS PD SR
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-phy-state-helper.cc ns-3.47/src/wifi/model/wifi-phy-state-helper.cc
--- ns-3.46/src/wifi/model/wifi-phy-state-helper.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-phy-state-helper.cc	2026-02-16 16:08:49.872231000 -0800
@@ -526,7 +526,7 @@
     {
         m_startCcaBusy = now;
     }
-    m_endCcaBusy = std::max(m_endCcaBusy, now + duration);
+    m_endCcaBusy = now + duration;
 }
 
 void
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-phy-state-helper.h ns-3.47/src/wifi/model/wifi-phy-state-helper.h
--- ns-3.46/src/wifi/model/wifi-phy-state-helper.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-phy-state-helper.h	2026-02-16 16:08:49.872231000 -0800
@@ -336,14 +336,14 @@
      * TracedCallback signature for transmit event.
      *
      * @param [in] packet The received packet.
-     * @param [in] mode   The transmission mode of the packet.
+     * @param [in] mode The transmission mode of the packet.
      * @param [in] preamble The preamble of the packet.
-     * @param [in] power  The transmit power level.
+     * @param [in] powerLevel The transmit power level.
      */
     typedef void (*TxTracedCallback)(Ptr<const Packet> packet,
                                      WifiMode mode,
                                      WifiPreamble preamble,
-                                     uint8_t power);
+                                     uint8_t powerLevel);
 
     /**
      * Notify all WifiPhyListener objects of the given PHY event.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-remote-station-manager.cc ns-3.47/src/wifi/model/wifi-remote-station-manager.cc
--- ns-3.46/src/wifi/model/wifi-remote-station-manager.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-remote-station-manager.cc	2026-02-16 16:08:49.873231000 -0800
@@ -106,13 +106,15 @@
                           WifiModeValue(),
                           MakeWifiModeAccessor(&WifiRemoteStationManager::m_nonUnicastMode),
                           MakeWifiModeChecker())
-            .AddAttribute("DefaultTxPowerLevel",
-                          "Default power level to be used for transmissions. "
-                          "This is the power level that is used by all those WifiManagers that do "
-                          "not implement TX power control.",
-                          UintegerValue(0),
-                          MakeUintegerAccessor(&WifiRemoteStationManager::m_defaultTxPowerLevel),
-                          MakeUintegerChecker<uint8_t>())
+            .AddAttribute(
+                "DefaultTxPowerLevel",
+                "Default power level to be used for transmissions. "
+                "This is the power level that is used by all those WifiManagers that do "
+                "not implement TX power control.",
+                UintegerValue(WIFI_MIN_TX_PWR_LEVEL), // corresponds to WifiPhy::TxPowerStart
+                MakeUintegerAccessor(&WifiRemoteStationManager::m_defaultTxPowerLevel),
+                MakeUintegerChecker<uint8_t>(WIFI_MIN_TX_PWR_LEVEL,
+                                             std::numeric_limits<uint8_t>::max()))
             .AddAttribute("ErpProtectionMode",
                           "Protection mode used when non-ERP STAs are connected to an ERP AP: "
                           "Rts-Cts or Cts-To-Self",
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-remote-station-manager.h ns-3.47/src/wifi/model/wifi-remote-station-manager.h
--- ns-3.46/src/wifi/model/wifi-remote-station-manager.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-remote-station-manager.h	2026-02-16 16:08:49.874230900 -0800
@@ -1559,6 +1559,7 @@
      *
      * @param address the address of the station
      * @return WifiRemoteStationState corresponding to the address
+     * @hidecaller
      */
     std::shared_ptr<WifiRemoteStationState> LookupState(Mac48Address address) const;
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-standard-constants.cc ns-3.47/src/wifi/model/wifi-standard-constants.cc
--- ns-3.46/src/wifi/model/wifi-standard-constants.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-standard-constants.cc	1969-12-31 16:00:00.000000000 -0800
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2024 DERONNE SOFTWARE ENGINEERING
- *
- * SPDX-License-Identifier: GPL-2.0-only
- *
- * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
- */
-
-#include "wifi-standard-constants.h"
-
-namespace ns3
-{
-
-const Time WIFI_TU = MicroSeconds(1024);
-
-const Time EMLSR_RX_PHY_START_DELAY = MicroSeconds(20);
-
-const Time OFDM_SIFS_TIME_20MHZ = MicroSeconds(16);
-
-const Time OFDM_SLOT_TIME_20MHZ = MicroSeconds(9);
-
-const Time OFDM_SIFS_TIME_10MHZ = MicroSeconds(32);
-
-const Time OFDM_SLOT_TIME_10MHZ = MicroSeconds(13);
-
-const Time OFDM_SIFS_TIME_5MHZ = MicroSeconds(64);
-
-const Time OFDM_SLOT_TIME_5MHZ = MicroSeconds(21);
-
-const Time DSSS_SIFS_TIME = MicroSeconds(10);
-
-const Time DSSS_SLOT_TIME = MicroSeconds(20);
-
-const Time MAX_PROPAGATION_DELAY = MicroSeconds(1);
-
-const Time MEDIUM_SYNC_THRESHOLD = MicroSeconds(72);
-
-} // namespace ns3
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-standard-constants.h ns-3.47/src/wifi/model/wifi-standard-constants.h
--- ns-3.46/src/wifi/model/wifi-standard-constants.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-standard-constants.h	2026-02-16 16:08:49.874230900 -0800
@@ -23,47 +23,50 @@
 
 /// Wi-Fi Time Unit value in microseconds (see IEEE 802.11-2020 sec. 3.1)
 /// Used to initialize WIFI_TU
-WIFI_EXPORT extern const Time WIFI_TU;
+inline const Time WIFI_TU = MicroSeconds(1024);
 
 /// aRxPHYStartDelay value to use when waiting for a new frame in the context of EMLSR operations
 /// (Sec. 35.3.17 of 802.11be D3.1)
-extern const Time EMLSR_RX_PHY_START_DELAY;
+inline const Time EMLSR_RX_PHY_START_DELAY = MicroSeconds(20);
 
 /// aSIFSTime value for OFDM (20 MHz channel spacing)
 /// (Table 17-21 "OFDM PHY characteristics" of 802.11-2020)
-extern const Time OFDM_SIFS_TIME_20MHZ;
+inline const Time OFDM_SIFS_TIME_20MHZ = MicroSeconds(16);
 
 /// aSlotTime duration for OFDM (20 MHz channel spacing)
 /// (Table 17-21 "OFDM PHY characteristics" of 802.11-2020)
-extern const Time OFDM_SLOT_TIME_20MHZ;
+inline const Time OFDM_SLOT_TIME_20MHZ = MicroSeconds(9);
 
 /// aSIFSTime value for OFDM (10 MHz channel spacing)
 /// (Table 17-21 "OFDM PHY characteristics" of 802.11-2020)
-extern const Time OFDM_SIFS_TIME_10MHZ;
+inline const Time OFDM_SIFS_TIME_10MHZ = MicroSeconds(32);
 
 /// aSlotTime duration for OFDM (10 MHz channel spacing)
 /// (Table 17-21 "OFDM PHY characteristics" of 802.11-2020)
-extern const Time OFDM_SLOT_TIME_10MHZ;
+inline const Time OFDM_SLOT_TIME_10MHZ = MicroSeconds(13);
 
 /// aSIFSTime value for OFDM (5 MHz channel spacing)
 /// (Table 17-21 "OFDM PHY characteristics" of 802.11-2020)
-extern const Time OFDM_SIFS_TIME_5MHZ;
+inline const Time OFDM_SIFS_TIME_5MHZ = MicroSeconds(64);
 
 /// aSlotTime duration for OFDM (5 MHz channel spacing)
 /// (Table 17-21 "OFDM PHY characteristics" of 802.11-2020)
-extern const Time OFDM_SLOT_TIME_5MHZ;
+inline const Time OFDM_SLOT_TIME_5MHZ = MicroSeconds(21);
 
 /// aSIFSTime duration for DSSS (Table 16-4 "HR/DSSS PHY characteristics" of 802.11-2020)
-extern const Time DSSS_SIFS_TIME;
+inline const Time DSSS_SIFS_TIME = MicroSeconds(10);
 
 /// aSlotTime duration for DSSS (Table 16-4 "HR/DSSS PHY characteristics" of 802.11-2020)
-extern const Time DSSS_SLOT_TIME;
+inline const Time DSSS_SLOT_TIME = MicroSeconds(20);
 
 /// maximum propagation delay
-WIFI_EXPORT extern const Time MAX_PROPAGATION_DELAY;
+inline const Time MAX_PROPAGATION_DELAY = MicroSeconds(1);
 
 /// The aMediumSyncThreshold defined by Sec. 35.3.16.18.1 of 802.11be D4.0
-WIFI_EXPORT extern const Time MEDIUM_SYNC_THRESHOLD;
+inline const Time MEDIUM_SYNC_THRESHOLD = MicroSeconds(72);
+
+/// minimum TX power level value
+static constexpr uint8_t WIFI_MIN_TX_PWR_LEVEL{1};
 
 /// Subcarrier frequency spacing in Hz (Table 19-6 "Timing-related constants" of 802.11-2020)
 static constexpr uint32_t SUBCARRIER_FREQUENCY_SPACING{312500};
@@ -84,6 +87,22 @@
 /// The length in octets of the IEEE 802.11 MAC FCS field
 static constexpr uint16_t WIFI_MAC_FCS_LENGTH{4};
 
+/// The value for aPSDUMaxLength in Table 15-5 (DSSS PHY characteristics), Table 17-21 (OFDM PHY
+/// characteristics) and Table 18-5 (ERP characteristics) of 802.11-2020
+static constexpr uint32_t WIFI_PSDU_MAX_LENGTH{4095};
+
+/// The value for aPSDUMaxLength in Table 19-25 (HT PHY characteristics) of 802.11-2020
+static constexpr uint32_t WIFI_PSDU_MAX_LENGTH_HT{65'535};
+
+/// The value for aPSDUMaxLength in Table 21-28 (VHT PHY characteristics) of 802.11-2020
+static constexpr uint32_t WIFI_PSDU_MAX_LENGTH_VHT{4'692'480};
+
+/// The value for aPSDUMaxLength in Table 27-54 (HE PHY characteristics) of 802.11ax-2021
+static constexpr uint32_t WIFI_PSDU_MAX_LENGTH_HE{6'500'631};
+
+/// The value for aPSDUMaxLength in Table 36-70 (EHT PHY characteristics) of 802.11be D7.0
+static constexpr uint32_t WIFI_PSDU_MAX_LENGTH_EHT{15'523'200};
+
 /// The minimum value for dot11RTSThreshold (C.3 MIB detail in IEEE Std 802.11-2020)
 static constexpr uint32_t WIFI_MIN_RTS_THRESHOLD{0};
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-tx-vector.cc ns-3.47/src/wifi/model/wifi-tx-vector.cc
--- ns-3.46/src/wifi/model/wifi-tx-vector.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-tx-vector.cc	2026-02-16 16:08:49.874230900 -0800
@@ -24,7 +24,7 @@
 {
 
 WifiTxVector::WifiTxVector()
-    : m_txPowerLevel(1),
+    : m_txPowerLevel(WIFI_MIN_TX_PWR_LEVEL),
       m_preamble(WIFI_PREAMBLE_LONG),
       m_channelWidth(MHz_u{20}),
       m_guardInterval(NanoSeconds(800)),
@@ -293,6 +293,7 @@
 void
 WifiTxVector::SetTxPowerLevel(uint8_t powerlevel)
 {
+    NS_ABORT_MSG_IF(powerlevel < WIFI_MIN_TX_PWR_LEVEL, "TX power level set below allowed range");
     m_txPowerLevel = powerlevel;
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-tx-vector.h ns-3.47/src/wifi/model/wifi-tx-vector.h
--- ns-3.46/src/wifi/model/wifi-tx-vector.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-tx-vector.h	2026-02-16 16:08:49.874230900 -0800
@@ -152,12 +152,14 @@
      *
      * @param staId the station ID for MU
      * @returns the selected payload transmission mode
+     * @hidecaller
      */
     WifiMode GetMode(uint16_t staId = SU_STA_ID) const;
     /**
      * Sets the selected payload transmission mode
      *
      * @param mode the payload WifiMode
+     * @hidecaller
      */
     void SetMode(WifiMode mode);
     /**
@@ -197,6 +199,7 @@
     void SetPreambleType(WifiPreamble preamble);
     /**
      * @returns the channel width
+     * @hidecaller
      */
     MHz_u GetChannelWidth() const;
     /**
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/wifi-types.h ns-3.47/src/wifi/model/wifi-types.h
--- ns-3.46/src/wifi/model/wifi-types.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/wifi-types.h	2026-02-16 16:08:49.875231000 -0800
@@ -97,7 +97,7 @@
     struct SegmentWithoutUnits
     {
         uint8_t number{};                            ///< channel number
-        double width{};                              ///< channel width in MHz
+        MHz_u width{};                               ///< channel width in MHz
         WifiPhyBand band{WIFI_PHY_BAND_UNSPECIFIED}; ///< PHY band
         uint8_t p20Index{};                          ///< primary20 index
 
@@ -109,7 +109,7 @@
          * @param b the PHY band
          * @param i the primary20 index
          */
-        SegmentWithoutUnits(uint8_t n, double w, WifiPhyBand b, uint8_t i)
+        SegmentWithoutUnits(uint8_t n, MHz_u w, WifiPhyBand b, uint8_t i)
             : number(n),
               width(w),
               band(b),
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/model/yans-wifi-phy.cc ns-3.47/src/wifi/model/yans-wifi-phy.cc
--- ns-3.46/src/wifi/model/yans-wifi-phy.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/model/yans-wifi-phy.cc	2026-02-16 16:08:49.875231000 -0800
@@ -94,7 +94,7 @@
 void
 YansWifiPhy::TraceSignalArrival(Ptr<const WifiPpdu> ppdu, double rxPowerDbm, Time duration)
 {
-    NS_LOG_FUNCTION(this << ppdu);
+    NS_LOG_FUNCTION(this << ppdu << rxPowerDbm << duration.As(Time::US));
     m_signalArrivalCb(ppdu, rxPowerDbm, ppdu->GetTxDuration());
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/power-rate-adaptation-test.cc ns-3.47/src/wifi/test/power-rate-adaptation-test.cc
--- ns-3.46/src/wifi/test/power-rate-adaptation-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/power-rate-adaptation-test.cc	2026-02-16 16:08:49.876231000 -0800
@@ -93,7 +93,7 @@
     /*
      * Configure power control parameters.
      */
-    phy->SetNTxPower(18);
+    phy->SetNTxPowerLevels(18);
     phy->SetTxPowerStart(dBm_u{0});
     phy->SetTxPowerEnd(dBm_u{17});
 
@@ -174,7 +174,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Initial data rate wrong");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Initial power level wrong");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Initial power level wrong");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -194,7 +194,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -211,7 +211,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -233,7 +233,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -250,7 +250,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -268,7 +268,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           48000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -288,7 +288,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -305,7 +305,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           48000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -325,7 +325,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -345,7 +345,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -367,7 +367,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "PARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "PARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "PARF: Incorrect value of power level");
 
     Simulator::Destroy();
 }
@@ -419,7 +419,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Initial data rate wrong");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Initial power level wrong");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "APARF: Initial power level wrong");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -440,7 +440,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -457,7 +457,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -477,7 +477,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -498,7 +498,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 15, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 16, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -518,7 +518,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 0, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 1, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -535,7 +535,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 1, "Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 2, "Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -555,7 +555,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -573,7 +573,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           48000000,
                           "Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -593,7 +593,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           48000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -615,7 +615,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           48000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 7, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 8, "APARF: Incorrect value of power level");
 
     for (int i = 0; i < 3; i++)
     {
@@ -629,7 +629,7 @@
     NS_TEST_ASSERT_MSG_EQ(mode.GetDataRate(txVector.GetChannelWidth()),
                           54000000,
                           "APARF: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "APARF: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "APARF: Incorrect value of power level");
 
     Simulator::Destroy();
 }
@@ -714,7 +714,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         6000000,
         "RRPAA: Initial data rate wrong"); // 802.11a minimal rate is 6Mbps
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Initial power level wrong");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Initial power level wrong");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -739,7 +739,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         6000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     /**
      * Test that 7 is enough.
@@ -754,7 +754,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         9000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -775,7 +775,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         9000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     manager->ReportDataFailed(mpdu);
 
@@ -787,7 +787,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         6000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -808,7 +808,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         9000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 10; i++)
     {
@@ -823,7 +823,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         12000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 13; i++)
     {
@@ -838,7 +838,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         18000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 19; i++)
     {
@@ -853,7 +853,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         24000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 23; i++)
     {
@@ -868,7 +868,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 33; i++)
     {
@@ -883,7 +883,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         48000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 43; i++)
     {
@@ -898,7 +898,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         54000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -921,7 +921,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         54000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     manager->ReportDataOk(mpdu, 0, ackMode, 0, txVector);
 
@@ -933,7 +933,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         54000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -955,7 +955,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         54000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 0, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 1, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -976,7 +976,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         54000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 1, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 2, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -997,7 +997,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         54000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -1018,7 +1018,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         48000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     /*
      * Now 11 failures are needed to decrease rate again.
@@ -1037,7 +1037,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -1060,7 +1060,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 18, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 5; i++)
     {
@@ -1075,7 +1075,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 5; i++)
     {
@@ -1090,7 +1090,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 16, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 17, "RRPAA: Incorrect value of power level");
 
     for (int i = 0; i < 25; i++)
     {
@@ -1105,7 +1105,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 15, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 16, "RRPAA: Incorrect value of power level");
 
     //-----------------------------------------------------------------------------------------------------
 
@@ -1134,7 +1134,7 @@
         mode.GetDataRate(txVector.GetChannelWidth(), txVector.GetGuardInterval(), 1),
         36000000,
         "RRPAA: Incorrect vale of data rate");
-    NS_TEST_ASSERT_MSG_EQ(power, 0, "RRPAA: Incorrect value of power level");
+    NS_TEST_ASSERT_MSG_EQ(power, 1, "RRPAA: Incorrect value of power level");
 
     Simulator::Stop(Seconds(10));
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/spectrum-wifi-phy-test.cc ns-3.47/src/wifi/test/spectrum-wifi-phy-test.cc
--- ns-3.46/src/wifi/test/spectrum-wifi-phy-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/spectrum-wifi-phy-test.cc	2026-02-16 16:08:49.876231000 -0800
@@ -181,7 +181,7 @@
 SpectrumWifiPhyBasicTest::MakeSignal(Watt_u txPower, const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector{OfdmPhy::GetOfdmRate6Mbps(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_LONG,
                           NanoSeconds(800),
                           1,
@@ -510,7 +510,7 @@
 SpectrumWifiPhyFilterTest::SendPpdu()
 {
     WifiTxVector txVector{EhtPhy::GetEhtMcs0(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_EHT_MU,
                           NanoSeconds(800),
                           1,
@@ -1310,7 +1310,7 @@
     NS_LOG_FUNCTION(this);
 
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_SU,
                           NanoSeconds(800),
                           1,
@@ -1695,13 +1695,13 @@
      *
      * @param index the index to identify the RX PHY to check
      * @param expectedCcaBusyIndication flag to indicate whether a CCA BUSY notification is expected
-     * @param switchingDelay delay between the TX has started and the time RX switched to the TX
-     * channel
+     * @param timeBeforeChannelSwitchStarted delay between the TX has started and the time RX
+     * switched to the TX channel
      * @param propagationDelay the propagation delay
      */
     void CheckCcaIndication(std::size_t index,
                             bool expectedCcaBusyIndication,
-                            Time switchingDelay,
+                            Time timeBeforeChannelSwitchStarted,
                             Time propagationDelay);
 
     /**
@@ -1728,6 +1728,10 @@
     std::vector<Ptr<SpectrumWifiPhy>> m_rxPhys{};                   //!< RX PHYs
     std::vector<std::shared_ptr<TestPhyListener>> m_listeners{};    //!< listeners
 
+    Time m_switchingDelay; //!< The switching delay to consider to the test, this has to be lessen
+                           //!< than the default one to ensure TX is still ongoing when the RX PHYs
+                           //!< switched to the TX channel
+
     std::vector<uint32_t> m_counts; //!< count number of packets received by PHYs
     std::vector<uint32_t>
         m_countRxSuccess; //!< count number of packets successfully received by PHYs
@@ -1744,7 +1748,10 @@
     ChannelSwitchScenario chanSwitchScenario)
     : TestCase{"SpectrumWifiPhy test operation with multiple RF interfaces"},
       m_trackSignalsInactiveInterfaces{trackSignalsInactiveInterfaces},
-      m_chanSwitchScenario{chanSwitchScenario}
+      m_chanSwitchScenario{chanSwitchScenario},
+      m_switchingDelay{m_chanSwitchScenario == ChannelSwitchScenario::BETWEEN_TX_RX
+                           ? NanoSeconds(1)
+                           : MicroSeconds(50)}
 {
 }
 
@@ -1780,7 +1787,7 @@
                          << phy->GetChannelWidth() << phy->GetChannelNumber());
 
     WifiTxVector txVector{HePhy::GetHeMcs11(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_SU,
                           NanoSeconds(800),
                           1,
@@ -1919,11 +1926,13 @@
 void
 SpectrumWifiPhyMultipleInterfacesTest::CheckCcaIndication(std::size_t index,
                                                           bool expectedCcaBusyIndication,
-                                                          Time switchingDelay,
+                                                          Time timeBeforeChannelSwitchStarted,
                                                           Time propagationDelay)
 {
     const auto expectedCcaBusyStart =
-        expectedCcaBusyIndication ? m_lastTxStart + switchingDelay : Seconds(0);
+        expectedCcaBusyIndication
+            ? m_lastTxStart + timeBeforeChannelSwitchStarted + m_switchingDelay
+            : Seconds(0);
     const auto expectedCcaBusyEnd =
         expectedCcaBusyIndication ? m_lastTxEnd + propagationDelay : Seconds(0);
     NS_LOG_FUNCTION(this << index << expectedCcaBusyIndication << expectedCcaBusyStart
@@ -2065,19 +2074,13 @@
     {
         auto txPhy =
             DynamicCast<SpectrumWifiPhy>(DynamicCast<WifiNetDevice>(apDevice.Get(0))->GetPhy(i));
-        if (m_chanSwitchScenario == ChannelSwitchScenario::BETWEEN_TX_RX)
-        {
-            txPhy->SetAttribute("ChannelSwitchDelay", TimeValue(NanoSeconds(1)));
-        }
+        txPhy->SetAttribute("ChannelSwitchDelay", TimeValue(m_switchingDelay));
         m_txPhys.push_back(txPhy);
 
         const auto index = m_rxPhys.size();
         auto rxPhy =
             DynamicCast<SpectrumWifiPhy>(DynamicCast<WifiNetDevice>(staDevice.Get(0))->GetPhy(i));
-        if (m_chanSwitchScenario == ChannelSwitchScenario::BETWEEN_TX_RX)
-        {
-            rxPhy->SetAttribute("ChannelSwitchDelay", TimeValue(NanoSeconds(1)));
-        }
+        rxPhy->SetAttribute("ChannelSwitchDelay", TimeValue(m_switchingDelay));
         rxPhy->TraceConnectWithoutContext(
             "PhyRxBegin",
             MakeCallback(&SpectrumWifiPhyMultipleInterfacesTest::RxCallback, this).Bind(index));
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-aggregation-test.cc ns-3.47/src/wifi/test/wifi-aggregation-test.cc
--- ns-3.46/src/wifi/test/wifi-aggregation-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-aggregation-test.cc	2026-02-16 16:08:49.877231000 -0800
@@ -44,6 +44,18 @@
 
 using namespace ns3;
 
+namespace
+{
+
+/// Map standard to aPSDUMaxLength
+const std::map<WifiStandard, uint32_t> maxPsduLengths = {
+    {WIFI_STANDARD_80211n, WIFI_PSDU_MAX_LENGTH_HT},
+    {WIFI_STANDARD_80211ac, WIFI_PSDU_MAX_LENGTH_VHT},
+    {WIFI_STANDARD_80211ax, WIFI_PSDU_MAX_LENGTH_HE},
+    {WIFI_STANDARD_80211be, WIFI_PSDU_MAX_LENGTH_EHT},
+};
+} // namespace
+
 /**
  * @ingroup wifi-test
  * @ingroup tests
@@ -93,6 +105,14 @@
     void EnqueuePkts(std::size_t count, uint32_t size, const Mac48Address& dest);
 
     /**
+     * Check the maximum PSDU size
+     *
+     * @param phy the PHY
+     * @param fem the HT frame exchange manager
+     */
+    void CheckMaxPsduSize(Ptr<WifiPhy> phy, Ptr<HtFrameExchangeManager> fem);
+
+    /**
      * @return the Best Effort QosTxop
      */
     Ptr<QosTxop> GetBeQueue() const;
@@ -388,15 +408,42 @@
 }
 
 void
+AmpduAggregationTest::CheckMaxPsduSize(Ptr<WifiPhy> phy, Ptr<HtFrameExchangeManager> fem)
+{
+    const auto expectedMaxPsduLength = maxPsduLengths.at(phy->GetStandard());
+    const auto mc = phy->GetMaxModulationClassSupported();
+
+    NS_TEST_EXPECT_MSG_EQ(phy->GetMaxPsduSize(mc),
+                          expectedMaxPsduLength,
+                          "Unexpected aPSDUMaxLength");
+
+    const auto maxAmpduSize = std::min(m_params.maxAmpduSize, expectedMaxPsduLength);
+    const auto recipient = Mac48Address("00:00:00:00:00:02");
+
+    auto isWithinAmpduSizeLimit = fem->IsWithinAmpduSizeLimit(maxAmpduSize, recipient, 0, mc);
+    NS_TEST_EXPECT_MSG_EQ(isWithinAmpduSizeLimit,
+                          true,
+                          "A-MPDU size limit check failed when the size is smaller than the limit");
+
+    isWithinAmpduSizeLimit = fem->IsWithinAmpduSizeLimit(maxAmpduSize + 1, recipient, 0, mc);
+    NS_TEST_EXPECT_MSG_EQ(isWithinAmpduSizeLimit,
+                          false,
+                          "A-MPDU size limit check failed when the size is larger than the limit");
+}
+
+void
 AmpduAggregationTest::DoRun()
 {
     /*
      * Test behavior when no other packets are in the queue
      */
+    auto phy = m_phys.at(SINGLE_LINK_OP_ID);
     auto fem = m_mac->GetFrameExchangeManager(SINGLE_LINK_OP_ID);
     auto htFem = DynamicCast<HtFrameExchangeManager>(fem);
     auto mpduAggregator = htFem->GetMpduAggregator();
 
+    CheckMaxPsduSize(phy, htFem);
+
     /*
      * Create a dummy packet of 1500 bytes and fill mac header fields.
      */
@@ -404,9 +451,9 @@
 
     auto peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     WifiTxParameters txParams;
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
     auto item = GetBeQueue()->GetNextMpdu(SINGLE_LINK_OP_ID, peeked, txParams, Time::Min(), true);
 
     auto mpduList = mpduAggregator->GetNextAmpdu(item, txParams, Time::Min());
@@ -455,9 +502,9 @@
 
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
     item = GetBeQueue()->GetNextMpdu(SINGLE_LINK_OP_ID, peeked, txParams, Time::Min(), true);
 
     mpduList = mpduAggregator->GetNextAmpdu(item, txParams, Time::Min());
@@ -470,9 +517,9 @@
 
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
     item = GetBeQueue()->GetNextMpdu(SINGLE_LINK_OP_ID, peeked, txParams, Time::Min(), true);
 
     mpduList = mpduAggregator->GetNextAmpdu(item, txParams, Time::Min());
@@ -549,16 +596,19 @@
      * is such that only two MSDUs can be aggregated. Therefore, the first MPDU we get contains
      * an A-MSDU of 2 MSDUs.
      */
+    auto phy = m_phys.at(SINGLE_LINK_OP_ID);
     auto fem = m_mac->GetFrameExchangeManager(SINGLE_LINK_OP_ID);
     auto htFem = DynamicCast<HtFrameExchangeManager>(fem);
     auto msduAggregator = htFem->GetMsduAggregator();
     auto mpduAggregator = htFem->GetMpduAggregator();
 
+    CheckMaxPsduSize(phy, htFem);
+
     auto peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     WifiTxParameters txParams;
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
     htFem->TryAddMpdu(peeked, txParams, Time::Min());
     auto item = msduAggregator->GetNextAmsdu(peeked, txParams, Time::Min());
 
@@ -581,9 +631,9 @@
 
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
     htFem->TryAddMpdu(peeked, txParams, Time::Min());
     item = msduAggregator->GetNextAmsdu(peeked, txParams, Time::Min());
 
@@ -607,9 +657,9 @@
 
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
 
     // Compute the first MPDU to be aggregated in an A-MPDU. It must contain an A-MSDU
     // aggregating two MSDUs
@@ -657,9 +707,9 @@
     // prepare another A-MPDU (e.g., for transmission on another link)
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID, 0, psdu->GetAddr1(), mpduList.at(2));
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
 
     // Compute the first MPDU to be aggregated in an A-MPDU. It must contain an A-MSDU
     // aggregating two MSDUs
@@ -691,9 +741,9 @@
     // A-MSDU aggregation now fails because the first item in the queue contain A-MSDUs
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
 
     htFem->TryAddMpdu(peeked, txParams, Time::Min());
     item = msduAggregator->GetNextAmsdu(peeked, txParams, Time::Min());
@@ -710,9 +760,9 @@
 
     peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     txParams.Clear();
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
 
     NS_TEST_EXPECT_MSG_EQ(peeked->GetHeader().IsQosAmsdu(),
                           false,
@@ -748,15 +798,16 @@
 };
 
 HeAggregationTest::HeAggregationTest(uint16_t bufferSize)
-    : AmpduAggregationTest("Check the correctness of 802.11ax aggregation operations, size=" +
-                               std::to_string(bufferSize),
-                           Params{.standard = WIFI_STANDARD_80211ax,
-                                  .nLinks = 1,
-                                  .dataMode = "HeMcs11",
-                                  .bufferSize = bufferSize,
-                                  .maxAmsduSize = 0,
-                                  .maxAmpduSize = 65535,
-                                  .txopLimit = Seconds(0)})
+    : AmpduAggregationTest(
+          "Check the correctness of 802.11ax aggregation operations, buffer size=" +
+              std::to_string(bufferSize),
+          Params{.standard = WIFI_STANDARD_80211ax,
+                 .nLinks = 1,
+                 .dataMode = "HeMcs11",
+                 .bufferSize = bufferSize,
+                 .maxAmsduSize = 0,
+                 .maxAmpduSize = 6500631,
+                 .txopLimit = Seconds(0)})
 {
 }
 
@@ -766,17 +817,21 @@
     /*
      * Test behavior when 300 packets are ready for transmission
      */
-    EnqueuePkts(300, 100, Mac48Address("00:00:00:00:00:02"));
+    const std::size_t numPackets = 300;
+    EnqueuePkts(numPackets, 100, Mac48Address("00:00:00:00:00:02"));
 
+    auto phy = m_phys.at(SINGLE_LINK_OP_ID);
     auto fem = m_mac->GetFrameExchangeManager(SINGLE_LINK_OP_ID);
     auto htFem = DynamicCast<HtFrameExchangeManager>(fem);
     auto mpduAggregator = htFem->GetMpduAggregator();
 
+    CheckMaxPsduSize(phy, htFem);
+
     auto peeked = GetBeQueue()->PeekNextMpdu(SINGLE_LINK_OP_ID);
     WifiTxParameters txParams;
-    txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-        peeked->GetHeader(),
-        m_phys.at(SINGLE_LINK_OP_ID)->GetChannelWidth());
+    txParams.m_txVector =
+        m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                              phy->GetChannelWidth());
     auto item = GetBeQueue()->GetNextMpdu(SINGLE_LINK_OP_ID, peeked, txParams, Time::Min(), true);
 
     auto mpduList = mpduAggregator->GetNextAmpdu(item, txParams, Time::Min());
@@ -786,7 +841,7 @@
     NS_TEST_EXPECT_MSG_EQ(mpduList.size(),
                           m_params.bufferSize,
                           "A-MPDU contains an unexpected number of MPDUs");
-    uint16_t expectedRemainingPacketsInQueue = 300 - m_params.bufferSize;
+    uint16_t expectedRemainingPacketsInQueue = numPackets - m_params.bufferSize;
     NS_TEST_EXPECT_MSG_EQ(GetBeQueue()->GetWifiMacQueue()->GetNPackets(),
                           expectedRemainingPacketsInQueue,
                           "Queue contains an unexpected number of MPDUs");
@@ -806,23 +861,25 @@
      * Constructor.
      *
      * @param bufferSize the size (in number of MPDUs) of the BlockAck buffer
+     * @param maxAmpduSize the maximum size (in bytes) allowed per A-MPDU
      */
-    EhtAggregationTest(uint16_t bufferSize);
+    EhtAggregationTest(uint16_t bufferSize, uint32_t maxAmpduSize);
 
   private:
     void DoRun() override;
 };
 
-EhtAggregationTest::EhtAggregationTest(uint16_t bufferSize)
-    : AmpduAggregationTest("Check the correctness of 802.11be aggregation operations, size=" +
-                               std::to_string(bufferSize),
-                           Params{.standard = WIFI_STANDARD_80211be,
-                                  .nLinks = 2,
-                                  .dataMode = "EhtMcs13",
-                                  .bufferSize = bufferSize,
-                                  .maxAmsduSize = 0,
-                                  .maxAmpduSize = 102000,
-                                  .txopLimit = Seconds(0)})
+EhtAggregationTest::EhtAggregationTest(uint16_t bufferSize, uint32_t maxAmpduSize)
+    : AmpduAggregationTest(
+          "Check the correctness of 802.11be aggregation operations, buffer size=" +
+              std::to_string(bufferSize) + ", max A-MPDU size=" + std::to_string(maxAmpduSize),
+          Params{.standard = WIFI_STANDARD_80211be,
+                 .nLinks = 2,
+                 .dataMode = "EhtMcs13",
+                 .bufferSize = bufferSize,
+                 .maxAmsduSize = 0,
+                 .maxAmpduSize = maxAmpduSize,
+                 .txopLimit = Seconds(0)})
 {
 }
 
@@ -830,27 +887,34 @@
 EhtAggregationTest::DoRun()
 {
     /*
-     * Test behavior when 1200 packets of 100 bytes each are ready for transmission. The max
-     * A-MPDU size limit (102000 B) is computed to have at most 750 MPDUs aggregated in a single
-     * A-MPDU (each MPDU is 130 B, plus 4 B of A-MPDU subframe header, plus 2 B of padding).
-     */
-    EnqueuePkts(1200, 100, Mac48Address("00:00:00:00:00:02"));
-    const std::size_t maxNMpdus = 750;
+     * Test behavior when 1200 packets of 100 bytes each are ready for transmission. In the first
+     * tests, the max A-MPDU size limit (102000 B) is computed to have at most 750 MPDUs aggregated
+     * in a single A-MPDU (each MPDU is 130 B, plus 4 B of A-MPDU subframe header, plus 2 B of
+     * padding). In the last test, the max A-MPDU size limit is the upper bound allowed by the
+     * standard (6500631 B), which allows to aggregate 1024 MPDUs (the buffer size) in a single
+     * A-MPDU.
+     */
+    const std::size_t numPackets = 1200;
+    EnqueuePkts(numPackets, 100, Mac48Address("00:00:00:00:00:02"));
+    const auto maxNMpdus = std::min<std::size_t>(m_params.maxAmpduSize / 136, m_params.bufferSize);
 
     for (uint8_t linkId = 0; linkId < m_params.nLinks; linkId++)
     {
+        auto phy = m_phys.at(linkId);
         auto fem = m_mac->GetFrameExchangeManager(linkId);
         auto htFem = DynamicCast<HtFrameExchangeManager>(fem);
         auto mpduAggregator = htFem->GetMpduAggregator();
         std::vector<Ptr<WifiMpdu>> mpduList;
 
+        CheckMaxPsduSize(phy, htFem);
+
         auto peeked = GetBeQueue()->PeekNextMpdu(linkId);
         if (peeked)
         {
             WifiTxParameters txParams;
-            txParams.m_txVector = m_mac->GetWifiRemoteStationManager()->GetDataTxVector(
-                peeked->GetHeader(),
-                m_phys.at(linkId)->GetChannelWidth());
+            txParams.m_txVector =
+                m_mac->GetWifiRemoteStationManager()->GetDataTxVector(peeked->GetHeader(),
+                                                                      phy->GetChannelWidth());
             auto item = GetBeQueue()->GetNextMpdu(linkId, peeked, txParams, Time::Min(), true);
 
             mpduList = mpduAggregator->GetNextAmpdu(item, txParams, Time::Min());
@@ -859,7 +923,7 @@
 
         uint16_t expectedRemainingPacketsInQueue;
 
-        if (m_params.bufferSize >= maxNMpdus)
+        if (m_params.bufferSize > maxNMpdus)
         {
             // two A-MPDUs are transmitted concurrently on the two links and together saturate
             // the transmit window
@@ -871,7 +935,7 @@
                 NS_TEST_EXPECT_MSG_EQ(mpduList.size(),
                                       maxNMpdus,
                                       "A-MPDU contains an unexpected number of MPDUs");
-                expectedRemainingPacketsInQueue = 1200 - maxNMpdus;
+                expectedRemainingPacketsInQueue = numPackets - maxNMpdus;
                 NS_TEST_EXPECT_MSG_EQ(GetBeQueue()->GetWifiMacQueue()->GetNPackets(),
                                       expectedRemainingPacketsInQueue,
                                       "Queue contains an unexpected number of MPDUs");
@@ -882,7 +946,7 @@
                 NS_TEST_EXPECT_MSG_EQ(mpduList.size(),
                                       m_params.bufferSize - maxNMpdus,
                                       "A-MPDU contains an unexpected number of MPDUs");
-                expectedRemainingPacketsInQueue = 1200 - m_params.bufferSize;
+                expectedRemainingPacketsInQueue = numPackets - m_params.bufferSize;
                 NS_TEST_EXPECT_MSG_EQ(GetBeQueue()->GetWifiMacQueue()->GetNPackets(),
                                       expectedRemainingPacketsInQueue,
                                       "Queue contains an unexpected number of MPDUs");
@@ -902,7 +966,7 @@
                 NS_TEST_EXPECT_MSG_EQ(mpduList.size(),
                                       m_params.bufferSize,
                                       "A-MPDU contains an unexpected number of MPDUs");
-                expectedRemainingPacketsInQueue = 1200 - m_params.bufferSize;
+                expectedRemainingPacketsInQueue = numPackets - m_params.bufferSize;
                 NS_TEST_EXPECT_MSG_EQ(GetBeQueue()->GetWifiMacQueue()->GetNPackets(),
                                       expectedRemainingPacketsInQueue,
                                       "Queue contains an unexpected number of MPDUs");
@@ -910,7 +974,7 @@
             case 1:
                 // no more MPDUs can be sent, aggregation fails
                 NS_TEST_EXPECT_MSG_EQ(mpduList.empty(), true, "MPDU aggregation did not fail");
-                expectedRemainingPacketsInQueue = 1200 - m_params.bufferSize;
+                expectedRemainingPacketsInQueue = numPackets - m_params.bufferSize;
                 NS_TEST_EXPECT_MSG_EQ(GetBeQueue()->GetWifiMacQueue()->GetNPackets(),
                                       expectedRemainingPacketsInQueue,
                                       "Queue contains an unexpected number of MPDUs");
@@ -1242,8 +1306,9 @@
     AddTestCase(new TwoLevelAggregationTest, TestCase::Duration::QUICK);
     AddTestCase(new HeAggregationTest(64), TestCase::Duration::QUICK);
     AddTestCase(new HeAggregationTest(256), TestCase::Duration::QUICK);
-    AddTestCase(new EhtAggregationTest(512), TestCase::Duration::QUICK);
-    AddTestCase(new EhtAggregationTest(1024), TestCase::Duration::QUICK);
+    AddTestCase(new EhtAggregationTest(512, 102000), TestCase::Duration::QUICK);
+    AddTestCase(new EhtAggregationTest(1024, 102000), TestCase::Duration::QUICK);
+    AddTestCase(new EhtAggregationTest(1024, 15523200), TestCase::Duration::QUICK);
     AddTestCase(new PreservePacketsInAmpdus(true), TestCase::Duration::QUICK);
     AddTestCase(new PreservePacketsInAmpdus(false), TestCase::Duration::QUICK);
 }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-dynamic-bw-op-test.cc ns-3.47/src/wifi/test/wifi-dynamic-bw-op-test.cc
--- ns-3.46/src/wifi/test/wifi-dynamic-bw-op-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-dynamic-bw-op-test.cc	2026-02-16 16:08:49.877231000 -0800
@@ -483,7 +483,7 @@
      *                                            primary20
      */
     AddTestCase(
-        new WifiUseAvailBwTest({"{31, 320, BAND_6GHZ, 10}", "{15, 160, BAND_6GHZ, 7}"}, 160),
+        new WifiUseAvailBwTest({"{31, 320, BAND_6GHZ, 10}", "{15, 160, BAND_6GHZ, 7}"}, MHz_u{160}),
         TestCase::Duration::QUICK);
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-error-rate-models-test.cc ns-3.47/src/wifi/test/wifi-error-rate-models-test.cc
--- ns-3.46/src/wifi/test/wifi-error-rate-models-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-error-rate-models-test.cc	2026-02-16 16:08:49.879231000 -0800
@@ -30,11 +30,11 @@
 NS_LOG_COMPONENT_DEFINE("WifiErrorRateModelsTest");
 
 static double
-FromRss(dBW_u rss)
+FromRss(dBm_u rss)
 {
     // SINR is based on receiver noise figure of 7 dB and thermal noise
     // of -100.5522786 dBm in this 22 MHz bandwidth at 290K
-    dBW_u noisePower = -100.5522786 + 7;
+    dBm_u noisePower = -100.5522786 + 7;
 
     dB_u sinr = rss - noisePower;
     // return SINR expressed as ratio
@@ -426,7 +426,7 @@
     WifiTxVector txVector;
 
     txVector.SetMode(mode);
-    txVector.SetTxPowerLevel(0);
+    txVector.SetTxPowerLevel(WIFI_MIN_TX_PWR_LEVEL);
     txVector.SetChannelWidth(MHz_u{20});
     txVector.SetNss(1);
     txVector.SetNTx(1);
@@ -656,8 +656,8 @@
  * map of PER values that have been manually computed for a given MCS, size (in bytes) and SNR (in
  * dB) in order to verify against the PER calculated by the model
  */
-std::map<std::pair<uint8_t /* mcs */, uint32_t /* size */>,
-         std::map<dB_u /* snr */, double /* per */>>
+const std::map<std::pair<uint8_t /* mcs */, uint32_t /* size */>,
+               std::map<dB_u /* snr */, double /* per */>>
     expectedTableValues = {
         /* MCS 0 - 1458 bytes */
         {std::make_pair(0, 1458),
@@ -1066,22 +1066,30 @@
     // Spot test some values returned from TableBasedErrorRateModel
     for (dB_u snr = -4; snr <= dB_u{30}; snr += dB_u{0.25})
     {
-        double expectedValue = 0;
+        double expectedPer = 0;
         if (m_mode.GetMcsValue() > ERROR_TABLE_BCC_MAX_NUM_MCS)
         {
             Ptr<YansErrorRateModel> yans = CreateObject<YansErrorRateModel>();
-            expectedValue =
+            expectedPer =
                 1 - yans->GetChunkSuccessRate(m_mode, txVector, std::pow(10, snr / 10), m_size * 8);
         }
         else
         {
-            auto it = expectedTableValues.find(std::make_pair(m_mode.GetMcsValue(), m_size));
-            if (it != expectedTableValues.end())
+            const auto cit = expectedTableValues.find(std::make_pair(m_mode.GetMcsValue(), m_size));
+            if (cit != expectedTableValues.cend())
             {
-                auto itValue = it->second.find(snr);
-                if (itValue != it->second.end())
+                // Lambda to find map key within tolerance (avoids floating-point comparison issues)
+                const auto& snrPerMap = cit->second;
+                const auto expectedIt = [&]() {
+                    constexpr dB_u tolerance{1e-6};
+                    const auto iter = snrPerMap.lower_bound(snr - tolerance);
+                    return (iter != snrPerMap.cend() && iter->first <= snr + tolerance)
+                               ? iter
+                               : snrPerMap.cend();
+                }();
+                if (expectedIt != snrPerMap.cend())
                 {
-                    expectedValue = itValue->second;
+                    expectedPer = expectedIt->second;
                 }
                 else
                 {
@@ -1090,15 +1098,15 @@
             }
             else
             {
-                NS_FATAL_ERROR("No expected value found for the combination MCS "
+                NS_FATAL_ERROR("No expected PER found for the combination MCS "
                                << +m_mode.GetMcsValue() << " and size " << m_size << " bytes");
             }
         }
         const auto per =
             1 - table->GetChunkSuccessRate(m_mode, txVector, std::pow(10, snr / 10), m_size * 8);
         NS_LOG_INFO(m_testName << ": snr=" << snr << "dB per=" << per
-                               << " expectedPER=" << expectedValue);
-        NS_TEST_ASSERT_MSG_EQ_TOL(per, expectedValue, 1e-5, "Not equal within tolerance");
+                               << " expectedPER=" << expectedPer);
+        NS_TEST_ASSERT_MSG_EQ_TOL(per, expectedPer, 1e-5, "Not equal within tolerance");
     }
 }
 
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-non-ht-dup-test.cc ns-3.47/src/wifi/test/wifi-non-ht-dup-test.cc
--- ns-3.46/src/wifi/test/wifi-non-ht-dup-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-non-ht-dup-test.cc	2026-02-16 16:08:49.881231000 -0800
@@ -331,7 +331,7 @@
 {
     NS_LOG_FUNCTION(this << channelWidth);
     WifiTxVector txVector = WifiTxVector(OfdmPhy::GetOfdmRate24Mbps(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_LONG,
                                          NanoSeconds(800),
                                          1,
@@ -744,7 +744,7 @@
 
     WifiTxVector txVector =
         WifiTxVector(OfdmPhy::GetOfdmRate54Mbps(), // use less robust modulation for test purpose
-                     0,
+                     WIFI_MIN_TX_PWR_LEVEL,
                      WIFI_PREAMBLE_LONG,
                      NanoSeconds(800),
                      1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-operating-channel-test.cc ns-3.47/src/wifi/test/wifi-operating-channel-test.cc
--- ns-3.46/src/wifi/test/wifi-operating-channel-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-operating-channel-test.cc	2026-02-16 16:08:49.881231000 -0800
@@ -222,12 +222,12 @@
            true);
 
     RunOne("default 320 MHz OFDM channel operating on channel 31",
-           {{31, 0, 320, WIFI_PHY_BAND_6GHZ}},
+           {{31, MHz_u{0}, MHz_u{320}, WIFI_PHY_BAND_6GHZ}},
            WIFI_STANDARD_UNSPECIFIED,
            WIFI_PHY_BAND_UNSPECIFIED,
            false,
            WifiChannelWidthType::CW_320MHZ,
-           {{31, 6105, 320, WIFI_PHY_BAND_6GHZ, FrequencyChannelType::OFDM}});
+           {{31, MHz_u{6105}, MHz_u{320}, WIFI_PHY_BAND_6GHZ, FrequencyChannelType::OFDM}});
 }
 
 /**
@@ -428,7 +428,7 @@
     // TODO: logic should be changed to support passing BAND_UNSPECIFIED
     RunOne("{31, 0, BAND_6GHZ, 0}",
            WifiChannelWidthType::CW_320MHZ,
-           {{31, 6105, 320, WIFI_PHY_BAND_6GHZ}},
+           {{31, MHz_u{6105}, MHz_u{320}, WIFI_PHY_BAND_6GHZ}},
            0);
 
     // Test 80+80 MHz
@@ -480,7 +480,7 @@
     // TODO: logic should be changed to support passing BAND_UNSPECIFIED
     RunOne("{0, 320, BAND_6GHZ, 0}",
            WifiChannelWidthType::CW_320MHZ,
-           {{31, 6105, 320, WIFI_PHY_BAND_6GHZ}},
+           {{31, MHz_u{6105}, MHz_u{320}, WIFI_PHY_BAND_6GHZ}},
            0);
 }
 
@@ -539,7 +539,7 @@
                                               const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-phy-cca-test.cc ns-3.47/src/wifi/test/wifi-phy-cca-test.cc
--- ns-3.46/src/wifi/test/wifi-phy-cca-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-phy-cca-test.cc	2026-02-16 16:08:49.882231000 -0800
@@ -52,22 +52,24 @@
 constexpr MHz_u P160_CENTER_FREQUENCY = P80_CENTER_FREQUENCY + MHz_u{40};
 constexpr MHz_u S160_CENTER_FREQUENCY = P160_CENTER_FREQUENCY + MHz_u{160};
 constexpr MHz_u P320_CENTER_FREQUENCY = P160_CENTER_FREQUENCY + MHz_u{80};
+const Time testStep = Seconds(1);
 const Time smallDelta = NanoSeconds(1);
 const Time aCcaTime = MicroSeconds(4);
-const Time phyHeaderDuration = MicroSeconds(32);
-// add small delta to be right after aCcaTime, since test checks are scheduled before wifi
-// events
+const Time legacyPhyHeaderDuration = MicroSeconds(20);
+const Time hePhyHeaderDuration = MicroSeconds(32);
+const Time ehtPhyHeaderDuration = MicroSeconds(40);
+// add small delta to be right after aCcaTime, since test checks are scheduled before wifi events
 const Time aCcaTimeWithDelta = aCcaTime + smallDelta;
 const std::map<MHz_u, Time> hePpduDurations = {
-    {20, NanoSeconds(1009600)},
-    {40, NanoSeconds(533600)},
-    {80, NanoSeconds(275200)},
+    {MHz_u{20}, NanoSeconds(1009600)},
+    {MHz_u{40}, NanoSeconds(533600)},
+    {MHz_u{80}, NanoSeconds(275200)},
 };
 const std::map<MHz_u, Time> ehtPpduDurations = {
-    {20, NanoSeconds(1017600)},
-    {40, NanoSeconds(541600)},
-    {80, NanoSeconds(287200)},
-    {160, NanoSeconds(178400)},
+    {MHz_u{20}, NanoSeconds(1017600)},
+    {MHz_u{40}, NanoSeconds(541600)},
+    {MHz_u{80}, NanoSeconds(287200)},
+    {MHz_u{160}, NanoSeconds(178400)},
 };
 
 /**
@@ -188,7 +190,7 @@
 WifiPhyCcaThresholdsTest::CreateDummyNonHtPpdu(const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector = WifiTxVector(OfdmPhy::GetOfdmRate6Mbps(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_LONG,
                                          NanoSeconds(800),
                                          1,
@@ -204,7 +206,7 @@
 WifiPhyCcaThresholdsTest::CreateDummyHtPpdu(MHz_u bandwidth, const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector = WifiTxVector(HtPhy::GetHtMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HT_MF,
                                          NanoSeconds(800),
                                          1,
@@ -221,7 +223,7 @@
                                              const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector = WifiTxVector(VhtPhy::GetVhtMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_VHT_SU,
                                          NanoSeconds(800),
                                          1,
@@ -237,7 +239,7 @@
 WifiPhyCcaThresholdsTest::CreateDummyHePpdu(MHz_u bandwidth, const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -254,7 +256,7 @@
                                              const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector{EhtPhy::GetEhtMcs0(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_EHT_MU,
                           NanoSeconds(800),
                           1,
@@ -717,7 +719,7 @@
     // EHT PHY: 160 MHz EHT PPDU in secondary160 channel (160 MHz) if power above the max
     // between the CCA sensitivity threshold for Per 20MHz check and the OBSS-PD level
     VerifyCcaThreshold(m_phy->GetPhyEntity(WIFI_MOD_CLASS_EHT),
-                       CreateDummyEhtPpdu(160, m_phy->GetOperatingChannel()),
+                       CreateDummyEhtPpdu(MHz_u{160}, m_phy->GetOperatingChannel()),
                        WIFI_CHANLIST_SECONDARY160,
                        std::max(m_obssPdLevel, m_per20CcaSensitivity));
 }
@@ -828,10 +830,16 @@
                             const std::vector<Time>& per20MhzDurations) override
     {
         NS_LOG_FUNCTION(this << duration << channelType << per20MhzDurations.size());
-        m_endCcaBusy = Simulator::Now() + duration;
+        NS_ASSERT(duration.IsPositive());
+        const auto now = Simulator::Now();
+        if (m_startCcaBusy.IsZero())
+        {
+            m_startCcaBusy = now;
+        }
+        m_endCcaBusy = now + duration;
         m_lastCcaBusyChannelType = channelType;
         m_lastPer20MhzCcaBusyDurations = per20MhzDurations;
-        m_notifications++;
+        ++m_notifications;
     }
 
     void NotifySwitchingStart(Time duration) override
@@ -860,13 +868,15 @@
     void Reset()
     {
         m_notifications = 0;
-        m_endCcaBusy = Seconds(0);
+        m_startCcaBusy = Time();
+        m_endCcaBusy = Time();
         m_lastCcaBusyChannelType = WIFI_CHANLIST_PRIMARY;
         m_lastPer20MhzCcaBusyDurations.clear();
     }
 
     std::size_t m_notifications{0}; ///< Number of CCA notifications
-    Time m_endCcaBusy{Seconds(0)};  ///< End of the CCA-BUSY duration
+    Time m_startCcaBusy{};          ///< Start of the CCA-BUSY duration
+    Time m_endCcaBusy{};            ///< End of the CCA-BUSY duration
     WifiChannelListType m_lastCcaBusyChannelType{
         WIFI_CHANLIST_PRIMARY}; ///< Channel type indication for the last CCA-BUSY notification
     std::vector<Time>
@@ -877,7 +887,13 @@
  * @ingroup wifi-test
  * @ingroup tests
  *
- * @brief Wifi Phy Threshold Test base class
+ * @brief Test CCA_BUSY indication provided by the PHY
+ *
+ * Many scenarios are being tested, where one or more PPDUs are being received and/or one or more
+ * (non-wifi) signals are being received. The tests verify the PHY states at different time
+ * points, and the CCA_BUSY indication at various point in time (amount of notifications, start and
+ * end of CCA_BUSY periods and per-20MHz bitmaps). The tests are run for all possible channel width
+ * and PPDU/signal widths combinations.
  */
 class WifiPhyCcaIndicationTest : public TestCase
 {
@@ -922,40 +938,59 @@
     void StopSignal(Ptr<WaveformGenerator> signalGenerator);
 
     /**
+     * Update the CCA threshold of the PHY
+     * @param ccaThreshold the new CCA threshold value to be set
+     */
+    void UpdateCcaThreshold(dBm_u ccaThreshold);
+
+    /**
      * Check the PHY state
+     * @param scenario the scenario under test
      * @param expectedState the expected state of the PHY
      */
-    void CheckPhyState(WifiPhyState expectedState);
+    void CheckPhyState(const std::string& scenario, WifiPhyState expectedState);
     /// @copydoc CheckPhyState
-    void DoCheckPhyState(WifiPhyState expectedState);
+    void DoCheckPhyState(const std::string& scenario, WifiPhyState expectedState);
 
     /**
-     * Check the last CCA-BUSY notification
+     * Check the CCA-BUSY notifications
+     * @param scenario the scenario under test
+     * @param expectedCcaNotifications the expected number of CCA_BUSY notifications
+     * @param expectedStartTime the expected CCA-BUSY start time
      * @param expectedEndTime the expected CCA-BUSY end time
      * @param expectedChannelType the expected channel type
      * @param expectedPer20MhzDurations the expected per-20 MHz CCA-BUSY durations
      */
-    void CheckLastCcaBusyNotification(Time expectedEndTime,
-                                      WifiChannelListType expectedChannelType,
-                                      const std::vector<Time>& expectedPer20MhzDurations);
-
+    void CheckCcaBusyNotifications(const std::string& scenario,
+                                   std::size_t expectedCcaNotifications,
+                                   Time expectedStartTime,
+                                   Time expectedEndTime,
+                                   WifiChannelListType expectedChannelType,
+                                   const std::vector<Time>& expectedPer20MhzDurations);
+    /// @copydoc CheckCcaBusyNotifications
+    void DoCheckCcaBusyNotifications(const std::string& scenario,
+                                     std::size_t expectedCcaNotifications,
+                                     Time expectedStartTime,
+                                     Time expectedEndTime,
+                                     WifiChannelListType expectedChannelType,
+                                     const std::vector<Time>& expectedPer20MhzDurations);
     /**
      * Log scenario description
      *
-     * @param log the scenario description to add to log
+     * @param scenario the scenario description to add to log
      */
-    void LogScenario(const std::string& log) const;
+    void LogScenario(const std::string& scenario) const;
 
     /**
      * structure that holds information to generate signals
      */
     struct TxSignalInfo
     {
-        dBm_u power{0.0};           //!< transmit power to use
-        Time startTime{Seconds(0)}; //!< time at which transmission will be started
-        Time duration{Seconds(0)};  //!< the duration of the transmission
-        MHz_u centerFreq{0};        //!< center frequency to use
-        MHz_u bandwidth{0};         //!< bandwidth to use
+        dBm_u power;      //!< transmit power to use
+        Time startTime;   //!< time at which transmission will be started
+        Time duration;    //!< the duration of the transmission
+        MHz_u centerFreq; //!< center frequency to use
+        MHz_u bandwidth;  //!< bandwidth to use
     };
 
     /**
@@ -963,10 +998,10 @@
      */
     struct TxPpduInfo
     {
-        dBm_u power{0.0};           //!< transmit power to use
-        Time startTime{Seconds(0)}; //!< time at which transmission will be started
-        MHz_u centerFreq{0};        //!< center frequency to use
-        MHz_u bandwidth{0};         //!< bandwidth to use
+        dBm_u power;      //!< transmit power to use
+        Time startTime;   //!< time at which transmission will be started
+        MHz_u centerFreq; //!< center frequency to use
+        MHz_u bandwidth;  //!< bandwidth to use
     };
 
     /**
@@ -974,7 +1009,7 @@
      */
     struct StateCheckPoint
     {
-        Time timePoint{Seconds(0)}; //!< time at which the check will performed
+        Time timePoint; //!< time at which the check will performed
         WifiPhyState expectedPhyState{WifiPhyState::IDLE}; //!< expected PHY state
     };
 
@@ -983,26 +1018,41 @@
      */
     struct CcaCheckPoint
     {
-        Time timePoint{Seconds(0)};          //!< time at which the check will performed
-        Time expectedCcaEndTime{Seconds(0)}; //!< expected CCA_BUSY end time
+        Time timePoint;                          //!< time at which the check will performed
+        std::size_t expectedCcaNotifications{0}; //!< expected number of CCA_BUSY notifications
+        Time expectedCcaStartTime;               //!< expected CCA_BUSY start time
+        Time expectedCcaEndTime;                 //!< expected CCA_BUSY end time
         WifiChannelListType expectedChannelListType{
-            WIFI_CHANLIST_PRIMARY};                    //!< expected channel list type
-        std::vector<Time> expectedPer20MhzDurations{}; //!< expected per-20 MHz CCA duration
+            WIFI_CHANLIST_PRIMARY};                  //!< expected channel list type
+        std::vector<Time> expectedPer20MhzDurations; //!< expected per-20 MHz CCA duration
+    };
+
+    /**
+     * structure that holds information to perform CCA threshold update
+     */
+    struct CcaThresholdUpdatePoint
+    {
+        Time timePoint;     //!< time at which the update will be performed
+        dBm_u ccaThreshold; //!< new CCA threshold value to be set
     };
 
     /**
      * Schedule test to perform.
      * @param delay the reference delay to schedule the events
+     * @param scenario description of the scenario to test
      * @param generatedSignals the vector of signals to be generated
      * @param generatedPpdus the vector of PPDUs to be generated
      * @param stateCheckpoints the vector of PHY state checks
      * @param ccaCheckpoints the vector of PHY CCA checks
+     * @param ccaThresholdUpdates the vector of CCA threshold updates
      */
     void ScheduleTest(Time delay,
+                      const std::string& scenario,
                       const std::vector<TxSignalInfo>& generatedSignals,
                       const std::vector<TxPpduInfo>& generatedPpdus,
                       const std::vector<StateCheckPoint>& stateCheckpoints,
-                      const std::vector<CcaCheckPoint>& ccaCheckpoints);
+                      const std::vector<CcaCheckPoint>& ccaCheckpoints,
+                      const std::vector<CcaThresholdUpdatePoint>& ccaThresholdUpdates = {});
 
     /**
      * Reset function
@@ -1022,7 +1072,7 @@
     WifiStandard m_standard; ///< The standard to use for the test
 
     Ptr<SpectrumWifiPhy> m_rxPhy; ///< PHY object of the receiver
-    Ptr<SpectrumWifiPhy> m_txPhy; ///< PHY object of the transmitter
+    Ptr<SpectrumWifiPhy> m_uxPhy; ///< PHY object of the transmitter
 
     std::vector<Ptr<WaveformGenerator>> m_signalGenerators; ///< Generators of non-wifi signals
     std::size_t m_numSignalGenerators; ///< The number of non-wifi signals generators needed for
@@ -1090,37 +1140,50 @@
     auto channelNum =
         WifiPhyOperatingChannel::FindFirst(0, frequency, bandwidth, m_standard, WIFI_PHY_BAND_6GHZ)
             ->number;
-    m_txPhy->SetOperatingChannel(
+    m_uxPhy->SetOperatingChannel(
         WifiPhy::ChannelTuple{channelNum, bandwidth, WIFI_PHY_BAND_6GHZ, 0});
 
     const auto mcs =
         (m_standard == WIFI_STANDARD_80211ax) ? HePhy::GetHeMcs0() : EhtPhy::GetEhtMcs0();
     const auto preamble =
         (m_standard == WIFI_STANDARD_80211ax) ? WIFI_PREAMBLE_HE_SU : WIFI_PREAMBLE_EHT_MU;
-    WifiTxVector txVector{mcs, 0, preamble, NanoSeconds(800), 1, 1, 0, bandwidth, false};
+    WifiTxVector
+        txVector{mcs, WIFI_MIN_TX_PWR_LEVEL, preamble, NanoSeconds(800), 1, 1, 0, bandwidth, false};
     Ptr<Packet> pkt = Create<Packet>(1000);
     WifiMacHeader hdr;
     hdr.SetType(WIFI_MAC_QOSDATA);
     hdr.SetQosTid(0);
     Ptr<WifiPsdu> psdu = Create<WifiPsdu>(pkt, hdr);
 
-    m_txPhy->SetTxPowerStart(txPower);
-    m_txPhy->SetTxPowerEnd(txPower);
+    m_uxPhy->SetTxPowerStart(txPower);
+    m_uxPhy->SetTxPowerEnd(txPower);
 
-    m_txPhy->Send(psdu, txVector);
+    m_uxPhy->Send(psdu, txVector);
+}
+
+void
+WifiPhyCcaIndicationTest::UpdateCcaThreshold(dBm_u ccaThreshold)
+{
+    NS_LOG_FUNCTION(this << ccaThreshold);
+    m_rxPhy->SetCcaEdThreshold(ccaThreshold);
 }
 
 void
-WifiPhyCcaIndicationTest::CheckPhyState(WifiPhyState expectedState)
+WifiPhyCcaIndicationTest::CheckPhyState(const std::string& scenario, WifiPhyState expectedState)
 {
+    NS_LOG_FUNCTION(this << expectedState);
     // This is needed to make sure PHY state will be checked as the last event if a state change
     // occurred at the exact same time as the check
-    Simulator::ScheduleNow(&WifiPhyCcaIndicationTest::DoCheckPhyState, this, expectedState);
+    Simulator::ScheduleNow(&WifiPhyCcaIndicationTest::DoCheckPhyState,
+                           this,
+                           scenario,
+                           expectedState);
 }
 
 void
-WifiPhyCcaIndicationTest::DoCheckPhyState(WifiPhyState expectedState)
+WifiPhyCcaIndicationTest::DoCheckPhyState(const std::string& scenario, WifiPhyState expectedState)
 {
+    NS_LOG_FUNCTION(this << expectedState);
     WifiPhyState currentState;
     PointerValue ptr;
     m_rxPhy->GetAttribute("State", ptr);
@@ -1129,52 +1192,116 @@
     NS_TEST_ASSERT_MSG_EQ(currentState,
                           expectedState,
                           "PHY State " << currentState << " does not match expected state "
-                                       << expectedState << " at " << Simulator::Now());
+                                       << expectedState << " at " << Simulator::Now()
+                                       << " for scenario: " << scenario << " (bw=" << m_channelWidth
+                                       << ")");
+}
+
+void
+WifiPhyCcaIndicationTest::CheckCcaBusyNotifications(
+    const std::string& scenario,
+    std::size_t expectedCcaNotifications,
+    Time expectedStartTime,
+    Time expectedEndTime,
+    WifiChannelListType expectedChannelType,
+    const std::vector<Time>& expectedPer20MhzDurations)
+{
+    Simulator::ScheduleNow(&WifiPhyCcaIndicationTest::DoCheckCcaBusyNotifications,
+                           this,
+                           scenario,
+                           expectedCcaNotifications,
+                           expectedStartTime,
+                           expectedEndTime,
+                           expectedChannelType,
+                           expectedPer20MhzDurations);
 }
 
 void
-WifiPhyCcaIndicationTest::CheckLastCcaBusyNotification(
+WifiPhyCcaIndicationTest::DoCheckCcaBusyNotifications(
+    const std::string& scenario,
+    std::size_t expectedCcaNotifications,
+    Time expectedStartTime,
     Time expectedEndTime,
     WifiChannelListType expectedChannelType,
     const std::vector<Time>& expectedPer20MhzDurations)
 {
+    auto printPer20Bitmap = [](const std::vector<Time>& v) -> std::string {
+        if (v.empty())
+        {
+            return "";
+        }
+        std::stringstream ss;
+        for (const auto& t : v)
+        {
+            ss << t << " ";
+        }
+        return ss.str();
+    };
+    NS_LOG_FUNCTION(this << expectedCcaNotifications << expectedStartTime << expectedEndTime
+                         << expectedChannelType << printPer20Bitmap(expectedPer20MhzDurations));
+    NS_TEST_ASSERT_MSG_EQ(m_rxPhyStateListener->m_notifications,
+                          expectedCcaNotifications,
+                          "Number of CCA busy notifications "
+                              << m_rxPhyStateListener->m_notifications
+                              << " does not match expected number " << expectedCcaNotifications
+                              << " at " << Simulator::Now() << " for scenario: " << scenario
+                              << " (bw=" << m_channelWidth << ")");
+    NS_TEST_ASSERT_MSG_EQ(m_rxPhyStateListener->m_startCcaBusy,
+                          expectedStartTime,
+                          "PHY CCA start time " << m_rxPhyStateListener->m_startCcaBusy
+                                                << " does not match expected time "
+                                                << expectedStartTime << " at " << Simulator::Now()
+                                                << " for scenario: " << scenario
+                                                << " (bw=" << m_channelWidth << ")");
     NS_TEST_ASSERT_MSG_EQ(m_rxPhyStateListener->m_endCcaBusy,
                           expectedEndTime,
                           "PHY CCA end time " << m_rxPhyStateListener->m_endCcaBusy
                                               << " does not match expected time " << expectedEndTime
-                                              << " at " << Simulator::Now());
+                                              << " at " << Simulator::Now() << " for scenario: "
+                                              << scenario << " (bw=" << m_channelWidth << ")");
     NS_TEST_ASSERT_MSG_EQ(m_rxPhyStateListener->m_lastCcaBusyChannelType,
                           expectedChannelType,
                           "PHY CCA-BUSY for " << m_rxPhyStateListener->m_lastCcaBusyChannelType
                                               << " does not match expected channel type "
-                                              << expectedChannelType << " at " << Simulator::Now());
+                                              << expectedChannelType << " at " << Simulator::Now()
+                                              << " for scenario: " << scenario
+                                              << " (bw=" << m_channelWidth << ")");
     NS_TEST_ASSERT_MSG_EQ(m_rxPhyStateListener->m_lastPer20MhzCcaBusyDurations.size(),
                           expectedPer20MhzDurations.size(),
                           "PHY CCA-BUSY per-20 MHz durations does not match expected vector"
-                              << " at " << Simulator::Now());
+                              << " at " << Simulator::Now() << " for scenario: " << scenario
+                              << " (bw=" << m_channelWidth << ")");
     for (std::size_t i = 0; i < expectedPer20MhzDurations.size(); ++i)
     {
         NS_TEST_ASSERT_MSG_EQ(m_rxPhyStateListener->m_lastPer20MhzCcaBusyDurations.at(i),
                               expectedPer20MhzDurations.at(i),
                               "PHY CCA-BUSY per-20 MHz duration at index "
                                   << i << " does not match expected duration at "
-                                  << Simulator::Now());
+                                  << Simulator::Now() << " for scenario: " << scenario
+                                  << " (bw=" << m_channelWidth << ")");
     }
 }
 
 void
-WifiPhyCcaIndicationTest::LogScenario(const std::string& log) const
+WifiPhyCcaIndicationTest::LogScenario(const std::string& scenario) const
 {
-    NS_LOG_INFO(log);
+    NS_LOG_INFO(scenario);
 }
 
 void
-WifiPhyCcaIndicationTest::ScheduleTest(Time delay,
-                                       const std::vector<TxSignalInfo>& generatedSignals,
-                                       const std::vector<TxPpduInfo>& generatedPpdus,
-                                       const std::vector<StateCheckPoint>& stateCheckpoints,
-                                       const std::vector<CcaCheckPoint>& ccaCheckpoints)
+WifiPhyCcaIndicationTest::ScheduleTest(
+    Time delay,
+    const std::string& scenario,
+    const std::vector<TxSignalInfo>& generatedSignals,
+    const std::vector<TxPpduInfo>& generatedPpdus,
+    const std::vector<StateCheckPoint>& stateCheckpoints,
+    const std::vector<CcaCheckPoint>& ccaCheckpoints,
+    const std::vector<CcaThresholdUpdatePoint>& ccaThresholdUpdates)
 {
+    Simulator::Schedule(delay, &WifiPhyCcaIndicationTest::LogScenario, this, scenario);
+
+    Simulator::Schedule(delay, &WifiPhyCcaIndicationTest::Reset, this);
+
     for (const auto& generatedPpdu : generatedPpdus)
     {
         Simulator::Schedule(delay + generatedPpdu.startTime,
@@ -1198,14 +1325,32 @@
                             generatedSignal.duration);
     }
 
-    for (const auto& checkpoint : ccaCheckpoints)
+    if (ccaCheckpoints.empty())
     {
-        Simulator::Schedule(delay + checkpoint.timePoint,
-                            &WifiPhyCcaIndicationTest::CheckLastCcaBusyNotification,
+        Simulator::Schedule(delay + (testStep / 2),
+                            &WifiPhyCcaIndicationTest::CheckCcaBusyNotifications,
                             this,
-                            Simulator::Now() + delay + checkpoint.expectedCcaEndTime,
-                            checkpoint.expectedChannelListType,
-                            checkpoint.expectedPer20MhzDurations);
+                            scenario,
+                            0,
+                            Time(),
+                            Time(),
+                            WIFI_CHANLIST_PRIMARY,
+                            std::vector<Time>{});
+    }
+    else
+    {
+        for (const auto& checkpoint : ccaCheckpoints)
+        {
+            Simulator::Schedule(delay + checkpoint.timePoint,
+                                &WifiPhyCcaIndicationTest::CheckCcaBusyNotifications,
+                                this,
+                                scenario,
+                                checkpoint.expectedCcaNotifications,
+                                Simulator::Now() + delay + checkpoint.expectedCcaStartTime,
+                                Simulator::Now() + delay + checkpoint.expectedCcaEndTime,
+                                checkpoint.expectedChannelListType,
+                                checkpoint.expectedPer20MhzDurations);
+        }
     }
 
     for (const auto& checkpoint : stateCheckpoints)
@@ -1213,16 +1358,26 @@
         Simulator::Schedule(delay + checkpoint.timePoint,
                             &WifiPhyCcaIndicationTest::CheckPhyState,
                             this,
+                            scenario,
                             checkpoint.expectedPhyState);
     }
 
-    Simulator::Schedule(delay + Seconds(0.5), &WifiPhyCcaIndicationTest::Reset, this);
+    for (const auto& ccaThresholdUpdate : ccaThresholdUpdates)
+    {
+        Simulator::Schedule(delay + ccaThresholdUpdate.timePoint,
+                            &WifiPhyCcaIndicationTest::UpdateCcaThreshold,
+                            this,
+                            ccaThresholdUpdate.ccaThreshold);
+    }
 }
 
 void
 WifiPhyCcaIndicationTest::Reset()
 {
+    NS_LOG_FUNCTION(this);
+    m_rxPhy->ConfigureStandard(m_standard); // make sure to clear PHY states
     m_rxPhyStateListener->Reset();
+    UpdateCcaThreshold(dBm_u{-62}); // restore default CCA threshold
 }
 
 void
@@ -1322,7 +1477,7 @@
 WifiPhyCcaIndicationTest::DoSetup()
 {
     // WifiHelper::EnableLogComponents ();
-    // LogComponentEnable ("WifiPhyCcaTest", LOG_LEVEL_ALL);
+    // LogComponentEnable("WifiPhyCcaTest", LOG_LEVEL_ALL);
 
     Ptr<MultiModelSpectrumChannel> spectrumChannel = CreateObject<MultiModelSpectrumChannel>();
 
@@ -1354,16 +1509,16 @@
 
     Ptr<Node> txNode = CreateObject<Node>();
     Ptr<WifiNetDevice> txDev = CreateObject<WifiNetDevice>();
-    m_txPhy = CreateObject<SpectrumWifiPhy>();
-    m_txPhy->SetAttribute("ChannelSwitchDelay", TimeValue(Seconds(0)));
+    m_uxPhy = CreateObject<SpectrumWifiPhy>();
+    m_uxPhy->SetAttribute("ChannelSwitchDelay", TimeValue(Seconds(0)));
     Ptr<InterferenceHelper> txInterferenceHelper = CreateObject<InterferenceHelper>();
-    m_txPhy->SetInterferenceHelper(txInterferenceHelper);
+    m_uxPhy->SetInterferenceHelper(txInterferenceHelper);
     Ptr<ErrorRateModel> txErrorModel = CreateObject<NistErrorRateModel>();
-    m_txPhy->SetErrorRateModel(txErrorModel);
-    m_txPhy->AddChannel(spectrumChannel);
-    m_txPhy->ConfigureStandard(m_standard);
-    m_txPhy->SetDevice(txDev);
-    txDev->SetPhy(m_txPhy);
+    m_uxPhy->SetErrorRateModel(txErrorModel);
+    m_uxPhy->AddChannel(spectrumChannel);
+    m_uxPhy->ConfigureStandard(m_standard);
+    m_uxPhy->SetDevice(txDev);
+    txDev->SetPhy(m_uxPhy);
     txNode->AddDevice(txDev);
 
     for (std::size_t i = 0; i < m_numSignalGenerators; ++i)
@@ -1387,7 +1542,7 @@
     RngSeedManager::SetRun(1);
     int64_t streamNumber = 0;
     m_rxPhy->AssignStreams(streamNumber);
-    m_txPhy->AssignStreams(streamNumber);
+    m_uxPhy->AssignStreams(streamNumber);
 
     auto channelNum = WifiPhyOperatingChannel::FindFirst(0,
                                                          m_frequency,
@@ -1398,110 +1553,113 @@
 
     m_rxPhy->SetOperatingChannel(
         WifiPhy::ChannelTuple{channelNum, m_channelWidth, WIFI_PHY_BAND_6GHZ, 0});
-    m_txPhy->SetOperatingChannel(
+    m_uxPhy->SetOperatingChannel(
         WifiPhy::ChannelTuple{channelNum, m_channelWidth, WIFI_PHY_BAND_6GHZ, 0});
 
+    const auto phyHeaderDuration =
+        (m_standard == WIFI_STANDARD_80211ax) ? hePhyHeaderDuration : ehtPhyHeaderDuration;
+
     const auto& ppduDurations =
         (m_standard == WIFI_STANDARD_80211ax) ? hePpduDurations : ehtPpduDurations;
 
-    Time delay;
-    Simulator::Schedule(delay, &WifiPhyCcaIndicationTest::Reset, this);
-    delay += Seconds(1);
-
+    Time delay{Seconds(1.0)};
     ResetExpectedPer20MhzCcaBusyDurations();
 
+    const auto tx1Start = MicroSeconds(0);
+
+    const auto txEndSignal1 = MicroSeconds(100);
+    const auto txDuration1 = txEndSignal1 - tx1Start;
+
+    const auto tx2Start = MicroSeconds(50);
+
+    const auto txEndSignal2 = MicroSeconds(150);
+    const auto txDuration2 = txEndSignal2 - tx2Start;
+
+    const auto txDiff = tx2Start - tx1Start;
+
+    const auto thresholdUpdateTime = MicroSeconds(75);
+
     //----------------------------------------------------------------------------------------------------------------------------------
-    // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a signal below
-    // the energy detection threshold occupies P20
-    Simulator::Schedule(delay,
-                        &WifiPhyCcaIndicationTest::LogScenario,
-                        this,
-                        "Reception of a signal that occupies P20 below ED threshold");
+    // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a signal below the
+    // energy detection threshold occupies P20
     ScheduleTest(
         delay,
-        {{dBm_u{-65}, MicroSeconds(0), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}}},
+        "Reception of a signal that occupies P20 below ED threshold",
+        {{dBm_u{-65}, tx1Start, txDuration1, P20_CENTER_FREQUENCY, MHz_u{20}}},
         {},
         {
             {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-            {MicroSeconds(100) - smallDelta,
+            {txEndSignal1 - smallDelta,
              WifiPhyState::IDLE}, // IDLE just before the transmission ends
-            {MicroSeconds(100) + smallDelta,
-             WifiPhyState::IDLE} // IDLE just after the transmission ends
+            {txEndSignal1 + smallDelta, WifiPhyState::IDLE} // IDLE just after the transmission ends
         },
         {});
-    delay += Seconds(1);
+    delay += testStep;
 
     //----------------------------------------------------------------------------------------------------------------------------------
-    // Verify PHY state is CCA-BUSY as long as a 20 MHz signal above the energy detection
-    // threshold occupies P20
-    Simulator::Schedule(delay,
-                        &WifiPhyCcaIndicationTest::LogScenario,
-                        this,
-                        "Reception of signal that occupies P20 above ED threshold");
-    if (m_channelWidth > 20)
+    // Verify PHY state is CCA-BUSY as long as a 20 MHz signal above the energy detection threshold
+    // occupies P20
+    if (m_channelWidth > MHz_u{20})
     {
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = MicroSeconds(100);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = txDuration1;
     }
     ScheduleTest(
         delay,
-        {{dBm_u{-60.0}, MicroSeconds(0), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}}},
+        "Reception of signal that occupies P20 above ED threshold",
+        {{dBm_u{-60}, tx1Start, txDuration1, P20_CENTER_FREQUENCY, MHz_u{20}}},
         {},
         {
             {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-            {MicroSeconds(100) - smallDelta,
+            {txEndSignal1 - smallDelta,
              WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-            {MicroSeconds(100) + smallDelta,
-             WifiPhyState::IDLE} // IDLE just after the transmission ends
+            {txEndSignal1 + smallDelta, WifiPhyState::IDLE} // IDLE just after the transmission ends
         },
-        {{MicroSeconds(100) - smallDelta,
-          MicroSeconds(100),
+        {{txEndSignal1 - smallDelta,
+          1,
+          tx1Start,
+          txEndSignal1,
           WIFI_CHANLIST_PRIMARY,
           m_expectedPer20MhzCcaBusyDurations.at(0)}});
-    delay += Seconds(1);
+    delay += testStep;
     ResetExpectedPer20MhzCcaBusyDurations();
 
     //----------------------------------------------------------------------------------------------------------------------------------
-    // Verify PHY state is CCA-BUSY as long as the sum of 20 MHz signals occupying P20 is above
-    // the energy detection threshold
-    Simulator::Schedule(delay,
-                        &WifiPhyCcaIndicationTest::LogScenario,
-                        this,
-                        "Reception of two 20 MHz signals that occupies P20 below ED threshold with "
-                        "sum above ED threshold");
-    if (m_channelWidth > 20)
+    // Verify PHY state is CCA-BUSY as long as the sum of 20 MHz signals occupying P20 is above the
+    // energy detection threshold
+    if (m_channelWidth > MHz_u{20})
     {
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = MicroSeconds(50);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = tx2Start - tx1Start;
     }
-    ScheduleTest(delay,
-                 {{-64.0, MicroSeconds(0), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}},
-                  {-65.0, MicroSeconds(50), MicroSeconds(200), P20_CENTER_FREQUENCY, MHz_u{20}}},
-                 {},
-                 {
-                     {MicroSeconds(50) + aCcaTimeWithDelta,
-                      WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                     {MicroSeconds(100) - smallDelta,
-                      WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                     {MicroSeconds(100) + smallDelta,
-                      WifiPhyState::IDLE} // IDLE just after the transmission ends
-                 },
-                 {{MicroSeconds(100) - smallDelta,
-                   MicroSeconds(100),
-                   WIFI_CHANLIST_PRIMARY,
-                   m_expectedPer20MhzCcaBusyDurations.at(0)}});
-    delay += Seconds(1);
+    ScheduleTest(
+        delay,
+        "Reception of two 20 MHz signals that occupies P20 below ED threshold with "
+        "sum above ED threshold",
+        {{dBm_u{-64}, tx1Start, txDuration1, P20_CENTER_FREQUENCY, MHz_u{20}},
+         {dBm_u{-65}, tx2Start, txDuration2, P20_CENTER_FREQUENCY, MHz_u{20}}},
+        {},
+        {
+            {tx2Start + aCcaTimeWithDelta,
+             WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
+            {txEndSignal1 - smallDelta,
+             WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
+            {txEndSignal1 + smallDelta, WifiPhyState::IDLE} // IDLE just after the transmission ends
+        },
+        {{txEndSignal1 - smallDelta,
+          1,
+          tx2Start,
+          txEndSignal1,
+          WIFI_CHANLIST_PRIMARY,
+          m_expectedPer20MhzCcaBusyDurations.at(0)}});
+    delay += testStep;
     ResetExpectedPer20MhzCcaBusyDurations();
 
     //----------------------------------------------------------------------------------------------------------------------------------
     // Verify PHY state stays IDLE when a 20 MHz PPDU with received power below the
     // corresponding CCA sensitivity threshold occupies P20
-    Simulator::Schedule(
-        delay,
-        &WifiPhyCcaIndicationTest::LogScenario,
-        this,
-        "Reception of a 20 MHz PPDU that occupies P20 below CCA sensitivity threshold");
     ScheduleTest(delay,
+                 "Reception of a 20 MHz PPDU that occupies P20 below CCA sensitivity threshold",
                  {},
-                 {{dBm_u{-85}, MicroSeconds(0), P20_CENTER_FREQUENCY, MHz_u{20}}},
+                 {{dBm_u{-85}, tx1Start, P20_CENTER_FREQUENCY, MHz_u{20}}},
                  {
                      {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                      {ppduDurations.at(MHz_u{20}) - smallDelta,
@@ -1510,21 +1668,17 @@
                       WifiPhyState::IDLE} // IDLE just after the transmission ends
                  },
                  {});
-    delay += Seconds(1);
+    delay += testStep;
 
     //----------------------------------------------------------------------------------------------------------------------------------
     // Verify PHY state transitions to CCA-BUSY when an PPDU with received power above the CCA
-    // sensitivity threshold occupies P20. The per20Bitmap should indicate idle on the primary
-    // 20 MHz subchannel because received power is below -72 dBm (27.3.20.6.5).
-    Simulator::Schedule(
-        delay,
-        &WifiPhyCcaIndicationTest::LogScenario,
-        this,
-        "Reception of a 20 MHz PPDU that occupies P20 above CCA sensitivity threshold");
+    // sensitivity threshold occupies P20. The per20Bitmap should indicate idle on the primary 20
+    // MHz subchannel because received power is below -72 dBm (27.3.20.6.5).
     ScheduleTest(
         delay,
+        "Reception of a 20 MHz PPDU that occupies P20 above CCA sensitivity threshold",
         {},
-        {{dBm_u{-80}, MicroSeconds(0), P20_CENTER_FREQUENCY, MHz_u{20}}},
+        {{dBm_u{-80}, tx1Start, P20_CENTER_FREQUENCY, MHz_u{20}}},
         {
             {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
             {ppduDurations.at(MHz_u{20}) - smallDelta,
@@ -1533,22 +1687,20 @@
              WifiPhyState::IDLE} // IDLE just after the transmission ends
         },
         {{aCcaTimeWithDelta,
-          MicroSeconds(16),
+          1,
+          aCcaTime,
+          legacyPhyHeaderDuration - aCcaTime,
           WIFI_CHANLIST_PRIMARY,
           m_expectedPer20MhzCcaBusyDurations.at(0)}});
-    delay += Seconds(1);
+    delay += testStep;
 
     //----------------------------------------------------------------------------------------------------------------------------------
     // Verify PHY state stays IDLE when a 40 MHz PPDU with received power below the CCA
     // sensitivity threshold occupies P40
-    Simulator::Schedule(
-        delay,
-        &WifiPhyCcaIndicationTest::LogScenario,
-        this,
-        "Reception of a 40 MHz PPDU that occupies P20 below CCA sensitivity threshold");
     ScheduleTest(delay,
+                 "Reception of a 40 MHz PPDU that occupies P20 below CCA sensitivity threshold",
                  {},
-                 {{dBm_u{-80}, MicroSeconds(0), P40_CENTER_FREQUENCY, MHz_u{40}}},
+                 {{dBm_u{-80}, tx1Start, P40_CENTER_FREQUENCY, MHz_u{40}}},
                  {
                      {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                      {ppduDurations.at(MHz_u{40}) - smallDelta,
@@ -1557,21 +1709,17 @@
                       WifiPhyState::IDLE} // IDLE just after the transmission ends
                  },
                  {});
-    delay += Seconds(1);
+    delay += testStep;
 
     //----------------------------------------------------------------------------------------------------------------------------------
     // Verify PHY state transitions to CCA-BUSY when an PPDU with received power above the CCA
-    // sensitivity threshold occupies P40. The per20Bitmap should indicate idle on the primary
-    // 20 MHz subchannel because received power is below -72 dBm (27.3.20.6.5).
-    Simulator::Schedule(
-        delay,
-        &WifiPhyCcaIndicationTest::LogScenario,
-        this,
-        "Reception of a 40 MHz PPDU that occupies P40 above CCA sensitivity threshold");
+    // sensitivity threshold occupies P40. The per20Bitmap should indicate idle on the primary 20
+    // MHz subchannel because received power is below -72 dBm (27.3.20.6.5).
     ScheduleTest(
         delay,
+        "Reception of a 40 MHz PPDU that occupies P40 above CCA sensitivity threshold",
         {},
-        {{dBm_u{-75}, MicroSeconds(0), P40_CENTER_FREQUENCY, MHz_u{40}}},
+        {{dBm_u{-75}, tx1Start, P40_CENTER_FREQUENCY, MHz_u{40}}},
         {
             {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
             {ppduDurations.at(MHz_u{40}) - smallDelta,
@@ -1582,176 +1730,165 @@
              WifiPhyState::IDLE} // IDLE just after the transmission ends
         },
         {{aCcaTimeWithDelta,
-          MicroSeconds(16),
+          1,
+          aCcaTime,
+          legacyPhyHeaderDuration - aCcaTime,
           WIFI_CHANLIST_PRIMARY,
           m_expectedPer20MhzCcaBusyDurations.at(0)}});
-    delay += Seconds(1);
+    delay += testStep;
 
     if (m_channelWidth > MHz_u{20})
     {
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a 20 MHz
-        // signal below the energy detection threshold occupies S20
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies S20 below ED threshold");
-        ScheduleTest(
-            delay,
-            {{dBm_u{-65}, MicroSeconds(0), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}}},
-            {},
-            {
-                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
-                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
-                 WifiPhyState::IDLE} // IDLE just after the transmission ends
-            },
-            {});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a 20 MHz signal
+        // below the energy detection threshold occupies S20
+        ScheduleTest(delay,
+                     "Reception of a 20 MHz signal that occupies S20 below ED threshold",
+                     {{dBm_u{-65}, tx1Start, txDuration1, S20_CENTER_FREQUENCY, MHz_u{20}}},
+                     {},
+                     {
+                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                         {txEndSignal1 - smallDelta,
+                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                         {txEndSignal1 + smallDelta,
+                          WifiPhyState::IDLE} // IDLE just after the transmission ends
+                     },
+                     {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but CCA-BUSY indication is reported when a 20 MHz signal
         // above the energy detection threshold occupies S20
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies S20 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = MicroSeconds(100);
-        ScheduleTest(
-            delay,
-            {{dBm_u{-60}, MicroSeconds(0), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}}},
-            {},
-            {
-                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
-                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
-                 WifiPhyState::IDLE} // IDLE just after the transmission ends
-            },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
-              WIFI_CHANLIST_SECONDARY,
-              m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = txDuration1;
+        ScheduleTest(delay,
+                     "Reception of a 20 MHz signal that occupies S20 above ED threshold",
+                     {{dBm_u{-60}, tx1Start, txDuration1, S20_CENTER_FREQUENCY, MHz_u{20}}},
+                     {},
+                     {
+                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                         {txEndSignal1 - smallDelta,
+                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                         {txEndSignal1 + smallDelta,
+                          WifiPhyState::IDLE} // IDLE just after the transmission ends
+                     },
+                     {{txEndSignal1 - smallDelta,
+                       1,
+                       tx1Start,
+                       txEndSignal1,
+                       WIFI_CHANLIST_SECONDARY,
+                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state is CCA-BUSY as long as a 40 MHz signal above the energy detection
         // threshold occupies P40
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies P40 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 2, MicroSeconds(100));
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 2, txDuration1);
         ScheduleTest(
             delay,
-            {{dBm_u{-55}, MicroSeconds(0), MicroSeconds(100), P40_CENTER_FREQUENCY, MHz_u{40}}},
+            "Reception of a 40 MHz signal that occupies P40 above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, P40_CENTER_FREQUENCY, MHz_u{40}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY notifies CCA-BUSY for the primary channel while the secondary channel was
         // already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S20 followed by the reception of "
-                            "another signal that occupies P20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = MicroSeconds(50);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = txDiff;
         ScheduleTest(
             delay,
-            {{dBm_u{-60}, MicroSeconds(0), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}},
-             {dBm_u{-60}, MicroSeconds(50), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a signal that occupies S20 followed by the reception of another signal "
+            "that occupies P20",
+            {{dBm_u{-60}, tx1Start, txDuration1, S20_CENTER_FREQUENCY, MHz_u{20}},
+             {dBm_u{-60}, tx2Start, txDuration2, P20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::CCA_BUSY}, // state of primary is CCA-BUSY after
-                                          // aCcaTimeWithDelta that followed the second
-                                          // transmission
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {tx2Start + aCcaTimeWithDelta,
+                 WifiPhyState::CCA_BUSY}, // state of primary is CCA-BUSY after aCcaTimeWithDelta
+                                          // that followed the second transmission
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY updates per-20 MHz CCA durations if a signal arrives on the secondary
-        // channel while primary is CCA-BUSY
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies P20 followed by the reception of "
-                            "another signal that occupies S20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = MicroSeconds(50);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = MicroSeconds(100);
+        // Verify PHY updates per-20 MHz CCA durations if a signal arrives on the secondary channel
+        // while primary is CCA-BUSY
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = txDiff;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = txDuration2;
         ScheduleTest(
             delay,
-            {{dBm_u{-60}, MicroSeconds(0), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}},
-             {dBm_u{-60}, MicroSeconds(50), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a signal that occupies P20 followed by the reception of another signal "
+            "that occupies S20",
+            {{dBm_u{-60}, tx1Start, txDuration1, P20_CENTER_FREQUENCY, MHz_u{20}},
+             {dBm_u{-60}, tx2Start, txDuration2, S20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
+                {tx2Start + aCcaTimeWithDelta,
                  WifiPhyState::CCA_BUSY}, // state of primary is still CCA-BUSY after
-                                          // aCcaTimeWithDelta that followed the second
-                                          // transmission
-                {MicroSeconds(100) - smallDelta,
+                                          // aCcaTimeWithDelta that followed the second transmission
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the first transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the first transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE when a 20 MHz PPDU with received power below the CCA
         // sensitivity threshold occupies S40
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 20 MHz PPDU that occupies S20 below CCA sensitivity threshold");
         ScheduleTest(delay,
+                     "Reception of a 20 MHz PPDU that occupies S20 below CCA sensitivity threshold",
                      {},
-                     {{dBm_u{-75}, MicroSeconds(0), S20_CENTER_FREQUENCY, MHz_u{20}}},
+                     {{dBm_u{-75}, tx1Start, S20_CENTER_FREQUENCY, MHz_u{20}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{20}) - smallDelta,
@@ -1760,20 +1897,16 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {});
-        delay += Seconds(1);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but CCA-BUSY indication is reported when a 20 MHz PPDU
         // with received power above the CCA sensitivity threshold occupies S20
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 20 MHz PPDU that occupies S20 above CCA sensitivity threshold");
         m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = ppduDurations.at(MHz_u{20});
         ScheduleTest(delay,
+                     "Reception of a 20 MHz PPDU that occupies S20 above CCA sensitivity threshold",
                      {},
-                     {{dBm_u{-70}, MicroSeconds(0), S20_CENTER_FREQUENCY, MHz_u{20}}},
+                     {{dBm_u{-70}, tx1Start, S20_CENTER_FREQUENCY, MHz_u{20}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{20}) - smallDelta,
@@ -1782,340 +1915,300 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {{aCcaTimeWithDelta,
+                       1,
+                       tx1Start,
                        ppduDurations.at(MHz_u{20}),
                        WIFI_CHANLIST_SECONDARY,
                        m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE but CCA-BUSY indication is still reported as long as a
-        // signal above the energy detection threshold occupies the S20 while a 40 MHz PPDU
-        // below the CCA sensitivity threshold is received on P40.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies S20 above ED threshold "
-                            "followed by a 40 "
-                            "MHz PPDU that occupies P40 below CCA sensitivity threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = MicroSeconds(46);
+        // Verify PHY state stays IDLE but CCA-BUSY indication is still reported as long as a signal
+        // above the energy detection threshold occupies the S20 while a 40 MHz PPDU below the CCA
+        // sensitivity threshold is received on P40.
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(1) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = txDiff - aCcaTime;
         ScheduleTest(
             delay,
+            "Reception of a 20 MHz signal that occupies S20 above ED threshold followed by a 40 "
+            "MHz PPDU that occupies P40 below CCA sensitivity threshold",
             {{dBm_u{-60},
-              MicroSeconds(0),
-              MicroSeconds(100),
+              tx1Start,
+              txDuration1,
               S20_CENTER_FREQUENCY,
               MHz_u{20}}}, // signal on S20 above threshold
             {{dBm_u{-80},
-              MicroSeconds(50),
+              tx2Start,
               P40_CENTER_FREQUENCY,
               MHz_u{40}}}, // PPDU on P40 below threshold
             {
-                {MicroSeconds(50) + aCcaTimeWithDelta, WifiPhyState::IDLE}, // PHY state stays IDLE
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // PHY state stays IDLE
             },
-            {{MicroSeconds(50) - smallDelta,
-              MicroSeconds(100),
+            {{tx2Start - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+             {txEndSignal1 - smallDelta,
+              2,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
     }
 
     if (m_channelWidth > MHz_u{40})
     {
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a signal
-        // below the energy detection threshold occupies S40
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the first subchannel of "
-                            "S40 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S40_CENTER_FREQUENCY - MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a signal below
+        // the energy detection threshold occupies S40
+        ScheduleTest(
+            delay,
+            "Reception of a signal that occupies P20 below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S40_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY notifies CCA-BUSY for the S40 as long as a signal above the energy
-        // detection threshold occupies the first 20 MHz subchannel of the S40: 27.3.20.6.4: Any
-        // signal within the secondary 40 MHz channel at or above a threshold of –59 dBm within
-        // a period of aCcaTimeWithDelta after the signal arrives at the receiver’s antenna(s).
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the first subchannel of "
-                            "S40 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(2) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S40_CENTER_FREQUENCY - MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY40,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        // Verify PHY notifies CCA-BUSY for the S40 as long as a signal above the energy detection
+        // threshold occupies the first 20 MHz subchannel of the S40: 27.3.20.6.4: Any signal within
+        // the secondary 40 MHz channel at or above a threshold of –59 dBm within a period of
+        // aCcaTimeWithDelta after the signal arrives at the receiver’s antenna(s).
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(2) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the first subchannel of S40 above "
+            "ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S40_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY40,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE for the S40 if a signal below the energy detection
-        // threshold occupies the second 20 MHz subchannel of the S40
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S40 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S40_CENTER_FREQUENCY + MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
-
-        //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY notifies CCA-BUSY for the S40 as long as a signal above the energy
-        // detection threshold occupies the second 20 MHz subchannel of the S40: 27.3.20.6.4:
-        // Any signal within the secondary 40 MHz channel at or above a threshold of –59 dBm
-        // within a period of aCcaTimeWithDelta after the signal arrives at the receiver's
-        // antenna(s).
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S40 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(3) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S40_CENTER_FREQUENCY + MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY40,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
-        ResetExpectedPer20MhzCcaBusyDurations();
+        // Verify PHY state stays IDLE for the S40 if a signal below the energy detection threshold
+        // occupies the second 20 MHz subchannel of the S40
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the second subchannel of "
+            "S40 below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S40_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE for the S40 if a signal below the energy detection
-        // threshold occupies S40
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies S40 below ED threshold");
+        // Verify PHY notifies CCA-BUSY for the S40 as long as a signal above the energy detection
+        // threshold occupies the second 20 MHz subchannel of the S40: 27.3.20.6.4: Any signal
+        // within the secondary 40 MHz channel at or above a threshold of –59 dBm within a period of
+        // aCcaTimeWithDelta after the signal arrives at the receiver's antenna(s).
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(3) = txDuration1;
         ScheduleTest(
             delay,
-            {{dBm_u{-60}, MicroSeconds(0), MicroSeconds(100), S40_CENTER_FREQUENCY, MHz_u{40}}},
+            "Reception of a 20 MHz signal that occupies the second subchannel of S40 "
+            "above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S40_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {});
-        delay += Seconds(1);
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY40,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
+        ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY notifies CCA-BUSY for the S40 as long as a signal above the energy
-        // detection threshold occupies S40: 27.3.20.6.4: Any signal within the secondary 40 MHz
-        // channel at or above a threshold of –59 dBm within a period of aCcaTimeWithDelta after
-        // the signal arrives at the receiver's antenna(s).
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S40 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 2, 2, MicroSeconds(100));
+        // Verify PHY state stays IDLE for the S40 if a signal below the energy detection threshold
+        // occupies S40
+        ScheduleTest(delay,
+                     "Reception of a 40 MHz signal that occupies S40 below ED threshold",
+                     {{dBm_u{-60}, tx1Start, txDuration1, S40_CENTER_FREQUENCY, MHz_u{40}}},
+                     {},
+                     {
+                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                         {txEndSignal1 - smallDelta,
+                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                         {txEndSignal1 + smallDelta,
+                          WifiPhyState::IDLE} // IDLE just after the transmission ends
+                     },
+                     {});
+        delay += testStep;
+
+        //----------------------------------------------------------------------------------------------------------------------------------
+        // Verify PHY notifies CCA-BUSY for the S40 as long as a signal above the energy detection
+        // threshold occupies S40: 27.3.20.6.4: Any signal within the secondary 40 MHz channel at or
+        // above a threshold of –59 dBm within a period of aCcaTimeWithDelta after the signal
+        // arrives at the receiver's antenna(s).
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 2, 2, txDuration1);
         ScheduleTest(
             delay,
-            {{dBm_u{-55}, MicroSeconds(0), MicroSeconds(100), S40_CENTER_FREQUENCY, MHz_u{40}}},
+            "Reception of a 20 MHz signal that occupies the second subchannel of S40 above ED "
+            "threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S40_CENTER_FREQUENCY, MHz_u{40}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state is CCA-BUSY as long as a 80 MHz signal above the energy detection
         // threshold occupies P80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies P80 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 4, MicroSeconds(100));
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 4, txDuration1);
         ScheduleTest(
             delay,
-            {{dBm_u{-55}, MicroSeconds(0), MicroSeconds(100), P80_CENTER_FREQUENCY, MHz_u{80}}},
+            "Reception of a 80 MHz signal that occupies P80 above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, P80_CENTER_FREQUENCY, MHz_u{80}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY notifies CCA-BUSY for the P20 channel while the S40 channel was already in
         // CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies S40 followed by the "
-                            "reception of another 20 MHz signal that occupies P20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(2) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(2) = MicroSeconds(50);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(2) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(2) = txDiff;
         ScheduleTest(
             delay,
-            {{dBm_u{-55},
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S40_CENTER_FREQUENCY - MHz_u{10},
-              MHz_u{20}},
-             {dBm_u{-55}, MicroSeconds(50), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a 20 MHz signal that occupies S40 followed by the reception of another "
+            "20 MHz signal that occupies P20",
+            {{dBm_u{-55}, tx1Start, txDuration1, S40_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}},
+             {dBm_u{-55}, tx2Start, txDuration2, P20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::CCA_BUSY}, // state of primary is CCA-BUSY after
-                                          // aCcaTimeWithDelta that followed the second
-                                          // transmission
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {tx2Start + aCcaTimeWithDelta,
+                 WifiPhyState::CCA_BUSY}, // state of primary is CCA-BUSY after aCcaTimeWithDelta
+                                          // that followed the second transmission
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but notifies CCA-BUSY for the S20 channel while the S40
         // channel was already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S40 followed by the reception of "
-                            "another signal that occupies S20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(2) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(2) = MicroSeconds(50);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(2) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(2) = txDiff;
         ScheduleTest(
             delay,
-            {{dBm_u{-55},
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S40_CENTER_FREQUENCY - MHz_u{10},
-              MHz_u{20}},
-             {dBm_u{-55}, MicroSeconds(50), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a signal that occupies S40 followed by the reception of another signal "
+            "that occupies S20",
+            {{dBm_u{-55}, tx1Start, txDuration1, S40_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}},
+             {dBm_u{-55}, tx2Start, txDuration2, S20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays IDLE
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays IDLE
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_SECONDARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE when a 40 MHz PPDU with received power below the CCA
         // sensitivity threshold occupies S40
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 40 MHz PPDU that occupies S40 below CCA sensitivity threshold");
         ScheduleTest(delay,
+                     "Reception of a 40 MHz PPDU that occupies S40 below CCA sensitivity threshold",
                      {},
-                     {{dBm_u{-75}, MicroSeconds(0), S40_CENTER_FREQUENCY, MHz_u{40}}},
+                     {{dBm_u{-75}, tx1Start, S40_CENTER_FREQUENCY, MHz_u{40}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{40}) - smallDelta,
@@ -2124,23 +2217,19 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {});
-        delay += Seconds(1);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but CCA-BUSY indication is reported when a 40 MHz PPDU
         // with received power above the CCA sensitivity threshold occupies S40
-        const dBm_u rxPower{(m_standard == WIFI_STANDARD_80211ax) ? -70.0 : -67.0};
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 40 MHz PPDU that occupies S40 above CCA sensitivity threshold");
+        const dBm_u rxPower{(m_standard == WIFI_STANDARD_80211ax) ? -70 : -67.0};
         std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 2,
                     2,
                     ppduDurations.at(MHz_u{40}));
         ScheduleTest(delay,
+                     "Reception of a 40 MHz PPDU that occupies S40 above CCA sensitivity threshold",
                      {},
-                     {{rxPower, MicroSeconds(0), S40_CENTER_FREQUENCY, MHz_u{40}}},
+                     {{rxPower, tx1Start, S40_CENTER_FREQUENCY, MHz_u{40}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{40}) - smallDelta,
@@ -2149,47 +2238,49 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {{aCcaTimeWithDelta,
+                       1,
+                       tx1Start,
                        ppduDurations.at(MHz_u{40}),
                        WIFI_CHANLIST_SECONDARY40,
                        m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE but CCA-BUSY indication is still reported as long as a
-        // signal above the energy detection threshold occupies the S40 while a 80 MHz PPDU
-        // below the CCA sensitivity threshold is received on P80.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies S40 above ED threshold "
-                            "followed by a 80 "
-                            "MHz PPDU that occupies P80 below CCA sensitivity threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 2, 2, MicroSeconds(100));
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(1).begin() + 2, 2, MicroSeconds(46));
+        // Verify PHY state stays IDLE but CCA-BUSY indication is still reported as long as a signal
+        // above the energy detection threshold occupies the S40 while a 80 MHz PPDU below the CCA
+        // sensitivity threshold is received on P80.
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 2, 2, txDuration1);
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(1).begin() + 2, 2, txDiff - aCcaTime);
         ScheduleTest(
             delay,
+            "Reception of a 40 MHz signal that occupies S40 above ED threshold followed by a 80 "
+            "MHz PPDU that occupies P80 below CCA sensitivity threshold",
             {{dBm_u{-55},
-              MicroSeconds(0),
-              MicroSeconds(100),
+              tx1Start,
+              txDuration1,
               S40_CENTER_FREQUENCY,
               MHz_u{40}}}, // signal on S40 above threshold
             {{dBm_u{-80},
-              MicroSeconds(50),
+              tx2Start,
               P80_CENTER_FREQUENCY,
               MHz_u{80}}}, // PPDU on P80 below threshold
             {
-                {MicroSeconds(50) + aCcaTimeWithDelta, WifiPhyState::IDLE}, // PHY state stays IDLE
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // PHY state stays IDLE
             },
-            {{MicroSeconds(50) - smallDelta,
-              MicroSeconds(100),
+            {{tx2Start - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+             {txEndSignal1 - smallDelta,
+              2,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
     }
     else // 20 or 40 MHz receiver
@@ -2198,15 +2289,12 @@
         // Verify PHY notifies CCA-BUSY when a 80 MHz PPDU with received power above the CCA
         // sensitivity threshold occupies P40 The per20Bitmap should indicate idle for all
         // subchannels because received power is below -62 dBm (27.3.20.6.5).
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz PPDU that occupies the 40 MHz band above CCA "
-                            "sensitivity threshold");
         ScheduleTest(
             delay,
+            "Reception of a 80 MHz PPDU that occupies the 40 MHz band above CCA sensitivity "
+            "threshold",
             {},
-            {{dBm_u{-70}, MicroSeconds(0), P80_CENTER_FREQUENCY, MHz_u{80}}},
+            {{dBm_u{-70}, tx1Start, P80_CENTER_FREQUENCY, MHz_u{80}}},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA_BUSY after aCcaTimeWithDelta
                 {ppduDurations.at(MHz_u{80}) - smallDelta,
@@ -2215,38 +2303,39 @@
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta,
-              MicroSeconds(16),
+              1,
+              aCcaTime,
+              legacyPhyHeaderDuration - aCcaTime,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
              {ppduDurations.at(MHz_u{80}) - smallDelta,
+              4, // 3 extra CCA_BUSY notifications during PHY header reception
+              aCcaTime,
               ppduDurations.at(MHz_u{80}),
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY notifies CCA-BUSY when a 80 MHz PPDU with received power above the CCA
         // sensitivity threshold occupies P40 The per20Bitmap should indicate CCA_BUSY for all
         // subchannels because received power is above -62 dBm (27.3.20.6.5).
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz PPDU that occupies the 40 MHz band above CCA "
-                            "sensitivity threshold");
-        if (m_channelWidth > 20)
+        if (m_channelWidth > MHz_u{20})
         {
             std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(),
                         2,
                         ppduDurations.at(80) - aCcaTime);
             std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(1).begin(),
                         2,
-                        ppduDurations.at(80) - phyHeaderDuration);
+                        ppduDurations.at(MHz_u{80}) - hePhyHeaderDuration);
         }
         ScheduleTest(
             delay,
+            "Reception of a 80 MHz PPDU that occupies the 40 MHz band above CCA sensitivity "
+            "threshold",
             {},
-            {{dBm_u{-55}, MicroSeconds(0), P80_CENTER_FREQUENCY, MHz_u{80}}},
+            {{dBm_u{-55}, tx1Start, P80_CENTER_FREQUENCY, MHz_u{80}}},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA_BUSY after aCcaTimeWithDelta
                 {ppduDurations.at(MHz_u{80}) - smallDelta,
@@ -2255,27 +2344,27 @@
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta,
-              MicroSeconds(16),
+              1,
+              aCcaTime,
+              legacyPhyHeaderDuration - aCcaTime,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
              {ppduDurations.at(MHz_u{80}) - smallDelta,
+              4, // 3 extra CCA_BUSY notifications during PHY header reception
+              aCcaTime,
               ppduDurations.at(MHz_u{80}),
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and no CCA-BUSY indication is reported when a signal not
         // occupying the operational channel is being received
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 40 MHz PPDU that does not occupy the operational channel");
         ScheduleTest(delay,
+                     "Reception of a 40 MHz PPDU that does not occupy the operational channel",
                      {},
-                     {{dBm_u{-50}, MicroSeconds(0), S40_CENTER_FREQUENCY, MHz_u{40}}},
+                     {{dBm_u{-50}, tx1Start, S40_CENTER_FREQUENCY, MHz_u{40}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{20}) - smallDelta,
@@ -2284,590 +2373,518 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {});
-        delay += Seconds(1);
+        delay += testStep;
     }
 
     if (m_channelWidth > MHz_u{80})
     {
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the first 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the first subchannel of "
-                            "S80 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY - MHz_u{30},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
-
-        //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the first 20 MHz subchannel of the
-        // S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the first subchannel of "
-                            "S80 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(4) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY - MHz_u{30},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY80,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
-        ResetExpectedPer20MhzCcaBusyDurations();
-
-        //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the second 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S80 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY - MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
-
-        //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the second 20 MHz subchannel of the
-        // S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S80 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(5) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY - MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY80,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
-        ResetExpectedPer20MhzCcaBusyDurations();
-
-        //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the third 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the third subchannel of "
-                            "S80 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY + MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below the
+        // energy detection threshold occupies the first 20 MHz subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the first subchannel of S80 below "
+            "ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{30}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the third 20 MHz subchannel of the
-        // S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the third subchannel of "
-                            "S80 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(6) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY + MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY80,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        // energy detection threshold occupies the first 20 MHz subchannel of the S80 27.3.20.6.4:
+        // Any signal within the secondary 80 MHz channel at or above –56 dBm.
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(4) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the first subchannel of S80 above "
+            "ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{30}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY80,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the fourth 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the fourth subchannel of "
-                            "S80 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY + MHz_u{30},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a
+        // signal below the energy detection threshold occupies the second 20 MHz
+        // subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the second subchannel of S80 "
+            "below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the fourth 20 MHz subchannel of the
-        // S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the fourth subchannel of "
-                            "S80 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(7) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY + MHz_u{30},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY80,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal
+        // above the energy detection threshold occupies the second 20 MHz subchannel
+        // of the S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or
+        // above –56 dBm.
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(5) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the second subchannel of S80 "
+            "above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY80,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the first and second 20 MHz subchannels of
-        // the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the first and second "
-                            "subchannels of S80 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY - MHz_u{20},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a
+        // signal below the energy detection threshold occupies the third 20 MHz
+        // subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the third subchannel of S80 "
+            "below "
+            "ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the first and second 20 MHz subchannels of the
-        // S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the first and second "
-                            "subchannels of S80 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 4, 2, MicroSeconds(100));
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY - MHz_u{20},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY80,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal
+        // above the energy detection threshold occupies the third 20 MHz subchannel of
+        // the S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or
+        // above –56 dBm.
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(6) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the third subchannel of S80 "
+            "above "
+            "ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY80,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the third and fourth 20 MHz subchannels of
-        // the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the third and fourth "
-                            "subchannels of S80 below ED threshold");
-        ScheduleTest(delay,
-                     {{dBm_u{-65},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY + MHz_u{20},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a
+        // signal below the energy detection threshold occupies the fourth 20 MHz
+        // subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the fourth subchannel of S80 "
+            "below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{30}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the third and fourth 20 MHz subchannels of the
-        // S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the third and fourth "
-                            "subchannels of S80 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 6, 2, MicroSeconds(100));
-        ScheduleTest(delay,
-                     {{dBm_u{-55},
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S80_CENTER_FREQUENCY + MHz_u{20},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY80,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal
+        // above the energy detection threshold occupies the fourth 20 MHz subchannel
+        // of the S80 27.3.20.6.4: Any signal within the secondary 80 MHz channel at or
+        // above –56 dBm.
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(7) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the fourth subchannel of S80 "
+            "above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{30}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY80,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies S80 below ED threshold");
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a
+        // signal below the energy detection threshold occupies the first and second 20
+        // MHz subchannels of the S80
         ScheduleTest(
             delay,
-            {{dBm_u{-65}, MicroSeconds(0), MicroSeconds(100), S80_CENTER_FREQUENCY, MHz_u{80}}},
+            "Reception of a 40 MHz signal that occupies the first and second "
+            "subchannels "
+            "of S80 below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{20}, MHz_u{40}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {});
-        delay += Seconds(1);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the S80 27.3.20.6.4: Any signal within the
-        // secondary 80 MHz channel at or above –56 dBm.
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies S80 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 4, 4, MicroSeconds(100));
+        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal
+        // above the energy detection threshold occupies the first and second 20 MHz
+        // subchannels of the S80 27.3.20.6.4: Any signal within the secondary 80 MHz
+        // channel at or above –56 dBm.
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 4, 2, txDuration1);
         ScheduleTest(
             delay,
-            {{dBm_u{-55}, MicroSeconds(0), MicroSeconds(100), S80_CENTER_FREQUENCY, MHz_u{80}}},
+            "Reception of a 40 MHz signal that occupies the first and second "
+            "subchannels "
+            "of S80 above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{20}, MHz_u{40}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY80,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE as long as a 160 MHz signal below the energy detection
-        // threshold occupies the whole band
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 160 MHz signal that occupies the whole band below ED threshold");
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a
+        // signal below the energy detection threshold occupies the third and fourth 20
+        // MHz subchannels of the S80
         ScheduleTest(
             delay,
-            {{dBm_u{-55}, MicroSeconds(0), MicroSeconds(100), P160_CENTER_FREQUENCY, MHz_u{160}}},
+            "Reception of a 40 MHz signal that occupies the third and fourth "
+            "subchannels "
+            "of S80 below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{20}, MHz_u{40}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {});
-
-        delay += Seconds(1);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state is CCA-BUSY as long as a 160 MHz signal above the energy detection
-        // threshold occupies the whole band
-        Simulator::Schedule(
+        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal
+        // above the energy detection threshold occupies the third and fourth 20 MHz
+        // subchannels of the S80 27.3.20.6.4: Any signal within the secondary 80 MHz
+        // channel at or above –56 dBm.
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 6, 2, txDuration1);
+        ScheduleTest(
             delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 160 MHz signal that occupies the whole band above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 8, MicroSeconds(100));
+            "Reception of a 40 MHz signal that occupies the third and fourth "
+            "subchannels "
+            "of S80 above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{20}, MHz_u{40}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY80,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
+        ResetExpectedPer20MhzCcaBusyDurations();
+
+        //----------------------------------------------------------------------------------------------------------------------------------
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a
+        // signal below the energy detection threshold occupies the S80
+        ScheduleTest(delay,
+                     "Reception of a 80 MHz signal that occupies S80 below ED threshold",
+                     {{dBm_u{-65}, tx1Start, txDuration1, S80_CENTER_FREQUENCY, MHz_u{80}}},
+                     {},
+                     {
+                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                         {txEndSignal1 - smallDelta,
+                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                         {txEndSignal1 + smallDelta,
+                          WifiPhyState::IDLE} // IDLE just after the transmission ends
+                     },
+                     {});
+        delay += testStep;
+
+        //----------------------------------------------------------------------------------------------------------------------------------
+        // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal
+        // above the energy detection threshold occupies the S80 27.3.20.6.4: Any
+        // signal within the secondary 80 MHz channel at or above –56 dBm.
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 4, 4, txDuration1);
+        ScheduleTest(delay,
+                     "Reception of a 80 MHz signal that occupies S80 above ED threshold",
+                     {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY, MHz_u{80}}},
+                     {},
+                     {
+                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                         {txEndSignal1 - smallDelta,
+                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                         {txEndSignal1 + smallDelta,
+                          WifiPhyState::IDLE} // IDLE just after the transmission ends
+                     },
+                     {{txEndSignal1 - smallDelta,
+                       1,
+                       tx1Start,
+                       txEndSignal1,
+                       WIFI_CHANLIST_SECONDARY80,
+                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
+        ResetExpectedPer20MhzCcaBusyDurations();
+
+        //----------------------------------------------------------------------------------------------------------------------------------
+        // Verify PHY state stays IDLE as long as a 160 MHz signal below the energy
+        // detection threshold occupies the whole band
+        ScheduleTest(delay,
+                     "Reception of a 160 MHz signal that occupies the whole band below ED "
+                     "threshold",
+                     {{dBm_u{-55}, tx1Start, txDuration1, P160_CENTER_FREQUENCY, MHz_u{160}}},
+                     {},
+                     {
+                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                         {txEndSignal1 - smallDelta,
+                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                         {txEndSignal1 + smallDelta,
+                          WifiPhyState::IDLE} // IDLE just after the transmission ends
+                     },
+                     {});
+
+        delay += testStep;
+
+        //----------------------------------------------------------------------------------------------------------------------------------
+        // Verify PHY state is CCA-BUSY as long as a 160 MHz signal above the energy
+        // detection threshold occupies the whole band
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 8, txDuration1);
         ScheduleTest(
             delay,
-            {{dBm_u{-50}, MicroSeconds(0), MicroSeconds(100), P160_CENTER_FREQUENCY, MHz_u{160}}},
+            "Reception of a 160 MHz signal that occupies the whole band above ED "
+            "threshold",
+            {{dBm_u{-50}, tx1Start, txDuration1, P160_CENTER_FREQUENCY, MHz_u{160}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY notifies CCA-BUSY for the P20 channel while the S80 channel was already in
-        // CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies S80 followed by the "
-                            "reception of another 20 MHz signal that occupies P20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(6) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(6) = MicroSeconds(50);
+        // Verify PHY notifies CCA-BUSY for the P20 channel while the S80 channel was
+        // already in CCA-BUSY state
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(6) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(6) = txDiff;
         ScheduleTest(
             delay,
-            {{dBm_u{-55},
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S80_CENTER_FREQUENCY + MHz_u{10},
-              MHz_u{20}},
-             {dBm_u{-55}, MicroSeconds(50), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a 20 MHz signal that occupies S80 followed by the reception "
+            "of another "
+            "20 MHz signal that occupies P20",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}},
+             {dBm_u{-55}, tx2Start, txDuration2, P20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
-                {aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays idle
+                                                         // after aCcaTimeWithDelta
+                {tx2Start + aCcaTimeWithDelta,
                  WifiPhyState::CCA_BUSY}, // state of primary is CCA-BUSY after
                                           // aCcaTimeWithDelta that followed the second
                                           // transmission
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY80,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of
+                                            // the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE but notifies CCA-BUSY for the S40 channel while the S80
-        // channel was already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S80 followed by the reception of "
-                            "another signal that occupies S40");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(7) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(2) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(7) = MicroSeconds(50);
+        // Verify PHY state stays IDLE but notifies CCA-BUSY for the S40 channel while
+        // the S80 channel was already in CCA-BUSY state
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(7) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(2) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(7) = txDiff;
         ScheduleTest(
             delay,
-            {{dBm_u{-55},
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S80_CENTER_FREQUENCY + MHz_u{30},
-              MHz_u{20}},
-             {dBm_u{-55},
-              MicroSeconds(50),
-              MicroSeconds(100),
-              S40_CENTER_FREQUENCY - MHz_u{10},
-              MHz_u{20}}},
+            "Reception of a signal that occupies S80 followed by the reception of "
+            "another signal "
+            "that occupies S40",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{30}, MHz_u{20}},
+             {dBm_u{-55}, tx2Start, txDuration2, S40_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
             {},
             {
-                {aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays IDLE
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {aCcaTimeWithDelta, WifiPhyState::IDLE},            // state of primary stays idle
+                                                                    // after aCcaTimeWithDelta
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays IDLE
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY80,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of
+                                            // the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE but notifies CCA-BUSY for the S20 channel while the S80
-        // channel was already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S80 followed by the reception of "
-                            "another signal that occupies S20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(4) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(4) = MicroSeconds(50);
+        // Verify PHY state stays IDLE but notifies CCA-BUSY for the S20 channel while
+        // the S80 channel was already in CCA-BUSY state
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(4) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(4) = txDiff;
         ScheduleTest(
             delay,
-            {{dBm_u{-55},
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S80_CENTER_FREQUENCY - MHz_u{30},
-              MHz_u{20}},
-             {dBm_u{-55}, MicroSeconds(50), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a signal that occupies S80 followed by the reception of "
+            "another signal "
+            "that occupies S20",
+            {{dBm_u{-55}, tx1Start, txDuration1, S80_CENTER_FREQUENCY - MHz_u{30}, MHz_u{20}},
+             {dBm_u{-55}, tx2Start, txDuration2, S20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
-                {aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays IDLE
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {aCcaTimeWithDelta, WifiPhyState::IDLE},            // state of primary stays idle
+                                                                    // after aCcaTimeWithDelta
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays IDLE
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY80,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of
+                                            // the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_SECONDARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE when a 80 MHz PPDU with received power below the CCA
-        // sensitivity threshold occupies S80
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 80 MHz PPDU that occupies S80 below CCA sensitivity threshold");
+        // Verify PHY state stays IDLE when a 80 MHz PPDU with received power below the
+        // CCA sensitivity threshold occupies S80
         ScheduleTest(delay,
+                     "Reception of a 80 MHz PPDU that occupies S80 below CCA sensitivity "
+                     "threshold",
                      {},
-                     {{dBm_u{-70}, MicroSeconds(0), S80_CENTER_FREQUENCY, MHz_u{80}}},
+                     {{dBm_u{-70}, tx1Start, S80_CENTER_FREQUENCY, MHz_u{80}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{80}) - smallDelta,
@@ -2876,20 +2893,20 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {});
-        delay += Seconds(1);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE but CCA-BUSY indication is reported when a 80 MHz PPDU
-        // with received power above the CCA sensitivity threshold occupies S80
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 80 MHz PPDU that occupies S80 above CCA sensitivity threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 4, 4, ppduDurations.at(80));
+        // Verify PHY state stays IDLE but CCA-BUSY indication is reported when a 80
+        // MHz PPDU with received power above the CCA sensitivity threshold occupies
+        // S80
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 4,
+                    4,
+                    ppduDurations.at(MHz_u{80}));
         ScheduleTest(delay,
+                     "Reception of a 80 MHz PPDU that occupies S80 above CCA sensitivity "
+                     "threshold",
                      {},
-                     {{dBm_u{-65}, MicroSeconds(0), S80_CENTER_FREQUENCY, MHz_u{80}}},
+                     {{dBm_u{-65}, tx1Start, S80_CENTER_FREQUENCY, MHz_u{80}}},
                      {
                          {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
                          {ppduDurations.at(MHz_u{80}) - smallDelta,
@@ -2898,88 +2915,77 @@
                           WifiPhyState::IDLE} // IDLE just after the transmission ends
                      },
                      {{aCcaTimeWithDelta,
+                       1,
+                       tx1Start,
                        ppduDurations.at(MHz_u{80}),
                        WIFI_CHANLIST_SECONDARY80,
                        m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         if (m_standard == WIFI_STANDARD_80211ax)
         {
             //----------------------------------------------------------------------------------------------------------------------------------
-            // Verify PHY state stays IDLE and CCA-BUSY indication is reported if only the
-            // per20bitmap parameter changes
-            Simulator::Schedule(
-                delay,
-                &WifiPhyCcaIndicationTest::LogScenario,
-                this,
-                "Reception of a 20 MHz signal that generates a per20bitmap parameter "
-                "change when previous CCA indication reports IDLE");
-            m_expectedPer20MhzCcaBusyDurations.at(0).at(7) = MicroSeconds(100);
+            // Verify PHY state stays IDLE and CCA-BUSY indication is reported if only
+            // the per20bitmap parameter changes
+            m_expectedPer20MhzCcaBusyDurations.at(0).at(7) = txDuration1;
             ScheduleTest(
                 delay,
-                {{dBm_u{-60.0},
-                  MicroSeconds(0),
-                  MicroSeconds(100),
-                  S80_CENTER_FREQUENCY + MHz_u{30},
-                  MHz_u{20}}},
+                "Reception of a 20 MHz signal that generates a per20bitmap parameter "
+                "change when previous CCA indication reports IDLE",
+                {{dBm_u{-60}, tx1Start, txDuration1, S80_CENTER_FREQUENCY + MHz_u{30}, MHz_u{20}}},
                 {},
                 {
                     {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                    {MicroSeconds(100) - smallDelta,
+                    {txEndSignal1 - smallDelta,
                      WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                    {MicroSeconds(100) + smallDelta,
+                    {txEndSignal1 + smallDelta,
                      WifiPhyState::IDLE} // IDLE just after the transmission ends
                 },
                 {{aCcaTimeWithDelta,
-                  Seconds(0),
+                  1,
+                  tx1Start,
+                  tx1Start,
                   WIFI_CHANLIST_PRIMARY,
                   m_expectedPer20MhzCcaBusyDurations.at(0)}});
-            delay += Seconds(1);
+            delay += testStep;
             ResetExpectedPer20MhzCcaBusyDurations();
 
             //----------------------------------------------------------------------------------------------------------------------------------
-            // Verify PHY state stays CCA_BUSY and CCA-BUSY indication is reported if only the
-            // per20bitmap parameter changes
-            Simulator::Schedule(delay,
-                                &WifiPhyCcaIndicationTest::LogScenario,
-                                this,
-                                "Reception of a 20 MHz signal that generates a per20bitmap "
-                                "parameter change when "
-                                "previous CCA indication reports BUSY for the primary channel");
-            std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 4, MicroSeconds(100));
-            std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(1).begin(), 4, MicroSeconds(50));
-            m_expectedPer20MhzCcaBusyDurations.at(1).at(7) = MicroSeconds(200);
+            // Verify PHY state stays CCA_BUSY and CCA-BUSY indication is reported if
+            // only the per20bitmap parameter changes
+            std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 4, txDuration1);
+            std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(1).begin(), 4, txDiff);
+            m_expectedPer20MhzCcaBusyDurations.at(1).at(7) = txDuration2;
             ScheduleTest(
                 delay,
-                {{dBm_u{-50.0},
-                  MicroSeconds(0),
-                  MicroSeconds(100),
-                  P80_CENTER_FREQUENCY,
-                  MHz_u{80}},
-                 {dBm_u{-60.0},
-                  MicroSeconds(50),
-                  MicroSeconds(200),
-                  S80_CENTER_FREQUENCY + MHz_u{30},
-                  MHz_u{20}}},
+                "Reception of a 20 MHz signal that generates a per20bitmap parameter "
+                "change when "
+                "previous CCA indication reports BUSY for the primary channel",
+                {{dBm_u{-50}, tx1Start, txDuration1, P80_CENTER_FREQUENCY, MHz_u{80}},
+                 {dBm_u{-60}, tx2Start, txDuration2, S80_CENTER_FREQUENCY + MHz_u{30}, MHz_u{20}}},
                 {},
                 {
                     {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                    {MicroSeconds(100) - smallDelta,
-                     WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                    {MicroSeconds(100) + smallDelta,
+                    {txEndSignal1 - smallDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the
+                                                                         // transmission ends
+                    {txEndSignal1 + smallDelta,
                      WifiPhyState::IDLE} // IDLE just after the transmission ends
                 },
                 {{aCcaTimeWithDelta,
-                  MicroSeconds(100),
+                  1,
+                  tx1Start,
+                  txEndSignal1,
                   WIFI_CHANLIST_PRIMARY,
                   m_expectedPer20MhzCcaBusyDurations.at(0)},
-                 {MicroSeconds(50) +
-                      aCcaTimeWithDelta, // notification upon reception of the second signal
-                  MicroSeconds(100),
+                 {tx2Start + aCcaTimeWithDelta, // notification upon reception
+                                                // of the second signal
+                  2,
+                  tx1Start,
+                  txEndSignal1,
                   WIFI_CHANLIST_PRIMARY,
                   m_expectedPer20MhzCcaBusyDurations.at(1)}});
-            delay += Seconds(1);
+            delay += testStep;
             ResetExpectedPer20MhzCcaBusyDurations();
         }
     }
@@ -2987,789 +2993,746 @@
     if (m_channelWidth > 160)
     {
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the first 20 MHz subchannel of the S160
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the first subchannel of "
-                            "S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-65.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{70},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below the
+        // energy detection threshold occupies the first 20 MHz subchannel of the S160
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the first subchannel of S160 "
+            "below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{70}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
         // energy detection threshold occupies the first 20 MHz subchannel of the S160.
-        // 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at or above a
-        // threshold of –62 dBm. (...)
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the first subchannel of "
-                            "S160 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(8) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{-61.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{70},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        // 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at or above a threshold
+        // of –62 dBm. (...)
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(8) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the first subchannel of S160 "
+            "above ED threshold",
+            {{dBm_u{-61}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{70}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the second 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-65.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{50},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below the
+        // energy detection threshold occupies the second 20 MHz subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the second subchannel of S160 "
+            "below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{50}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the second 20 MHz subchannel of the
-        // S160 27.3.20.6.4: 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at
-        // or above a threshold of –62 dBm. (...)
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the second subchannel of "
-                            "S160 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(9) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{-61.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{50},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        // energy detection threshold occupies the second 20 MHz subchannel of the S160 27.3.20.6.4:
+        // 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at or above a threshold
+        // of –62 dBm. (...)
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(9) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the second subchannel of S160 "
+            "above ED threshold",
+            {{dBm_u{-61}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{50}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the third 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the third subchannel of "
-                            "S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-65.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{30},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below the
+        // energy detection threshold occupies the third 20 MHz subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the third subchannel of S160 "
+            "below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{30}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the third 20 MHz subchannel of the
-        // S160 27.3.20.6.4: 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at
-        // or above a threshold of –62 dBm. (...)
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the third subchannel of "
-                            "S160 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(10) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{-61.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{30},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        // energy detection threshold occupies the third 20 MHz subchannel of the S160 27.3.20.6.4:
+        // 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at or above a threshold
+        // of –62 dBm. (...)
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(10) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the third subchannel of S160 "
+            "above ED threshold",
+            {{dBm_u{-61}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{30}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below
-        // the energy detection threshold occupies the fourth 20 MHz subchannel of the S80
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the fourth subchannel of "
-                            "S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-65.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal below the
+        // energy detection threshold occupies the fourth 20 MHz subchannel of the S80
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the fourth subchannel of S160 "
+            "below ED threshold",
+            {{dBm_u{-65}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and CCA-BUSY indication is reported if a signal above the
-        // energy detection threshold occupies the fourth 20 MHz subchannel of the
-        // S160 27.3.20.6.4: 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at
-        // or above a threshold of –62 dBm. (...)
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies the fourth subchannel of "
-                            "S160 above ED threshold");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(11) = MicroSeconds(100);
-        ScheduleTest(delay,
-                     {{-61.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{10},
-                       MHz_u{20}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        // energy detection threshold occupies the fourth 20 MHz subchannel of the S160 27.3.20.6.4:
+        // 36.3.21.6.4: (...) A signal is present on the 20 MHz subchannel at or above a threshold
+        // of –62 dBm. (...)
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(11) = txDuration1;
+        ScheduleTest(
+            delay,
+            "Reception of a 20 MHz signal that occupies the fourth subchannel of S160 "
+            "above ED threshold",
+            {{dBm_u{-61}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
-        // occupies the fifth and sixth 20 MHz subchannels of the S160 with received power below
-        // the energy detection threshold for all occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the fifth and sixth "
-                            "subchannels of S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-60.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY + MHz_u{20},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal occupies
+        // the fifth and sixth 20 MHz subchannels of the S160 with received power below the energy
+        // detection threshold for all occupied 20 MHz subchannels
+        ScheduleTest(
+            delay,
+            "Reception of a 40 MHz signal that occupies the fifth and sixth subchannels "
+            "of S160 below ED threshold",
+            {{dBm_u{-60}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{20}, MHz_u{40}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
-        // occupies the fifth and sixth 20 MHz subchannels of the S160 with received power above
-        // the energy detection threshold for any occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the fifth and sixth "
-                            "subchannels of S160 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 12, 2, MicroSeconds(100));
-        ScheduleTest(delay,
-                     {{-58.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY + MHz_u{20},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal occupies
+        // the fifth and sixth 20 MHz subchannels of the S160 with received power above the energy
+        // detection threshold for any occupied 20 MHz subchannels
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 12, 2, txDuration1);
+        ScheduleTest(
+            delay,
+            "Reception of a 40 MHz signal that occupies the fifth and sixth subchannels "
+            "of S160 above ED threshold",
+            {{dBm_u{-58}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{20}, MHz_u{40}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
-        // occupies the seventh and eighth 20 MHz subchannels of the S160 with received power
-        // below the energy detection threshold for all occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the seventh and eighth "
-                            "subchannels of S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-60.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY + MHz_u{60},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal occupies
+        // the seventh and eighth 20 MHz subchannels of the S160 with received power below the
+        // energy detection threshold for all occupied 20 MHz subchannels
+        ScheduleTest(
+            delay,
+            "Reception of a 40 MHz signal that occupies the seventh and eighth "
+            "subchannels of S160 below ED threshold",
+            {{dBm_u{-60}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{60}, MHz_u{40}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
-        // occupies the seventh and eighth 20 MHz subchannels of the S160 with received power
-        // above the energy detection threshold for any occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 40 MHz signal that occupies the seventh and eighth "
-                            "subchannels of S160 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 14, 2, MicroSeconds(100));
-        ScheduleTest(delay,
-                     {{-58.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY + MHz_u{60},
-                       MHz_u{40}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal occupies
+        // the seventh and eighth 20 MHz subchannels of the S160 with received power above the
+        // energy detection threshold for any occupied 20 MHz subchannels
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 14, 2, txDuration1);
+        ScheduleTest(
+            delay,
+            "Reception of a 40 MHz signal that occupies the seventh and eighth "
+            "subchannels of S160 above ED threshold",
+            {{dBm_u{-58}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{60}, MHz_u{40}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
         // occupies the first four 20 MHz subchannels of the S160 with received power below the
         // energy detection threshold for all occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies the first four "
-                            "subchannels of S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-57.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{40},
-                       MHz_u{80}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        ScheduleTest(
+            delay,
+            "Reception of a 80 MHz signal that occupies the first four subchannels of "
+            "S160 below ED threshold",
+            {{dBm_u{-57}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{40}, MHz_u{80}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
         // occupies the first four 20 MHz subchannels of the S160 with received power above the
         // energy detection threshold for any occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies the first four "
-                            "subchannels of S160 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 8, 4, MicroSeconds(100));
-        ScheduleTest(delay,
-                     {{-55.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY - MHz_u{40},
-                       MHz_u{80}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 8, 4, txDuration1);
+        ScheduleTest(
+            delay,
+            "Reception of a 80 MHz signal that occupies the first four subchannels of "
+            "S160 above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S160_CENTER_FREQUENCY - MHz_u{40}, MHz_u{80}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
         // occupies the last four 20 MHz subchannels of the S160 with received power below the
         // energy detection threshold for all occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies the last four "
-                            "subchannels of S160 below ED threshold");
-        ScheduleTest(delay,
-                     {{-57.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY + MHz_u{40},
-                       MHz_u{80}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+        ScheduleTest(
+            delay,
+            "Reception of a 80 MHz signal that occupies the last four subchannels of S160 "
+            "below ED threshold",
+            {{dBm_u{-57}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{40}, MHz_u{80}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
         // occupies the last four 20 MHz subchannels of the S160 with received power above the
         // energy detection threshold for any occupied 20 MHz subchannels
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 80 MHz signal that occupies the last four "
-                            "subchannels of S160 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 12, 4, MicroSeconds(100));
-        ScheduleTest(delay,
-                     {{-55.0,
-                       MicroSeconds(0),
-                       MicroSeconds(100),
-                       S160_CENTER_FREQUENCY + MHz_u{40},
-                       MHz_u{80}}},
-                     {},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {MicroSeconds(100) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {MicroSeconds(100) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{MicroSeconds(100) - smallDelta,
-                       MicroSeconds(100),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 12, 4, txDuration1);
+        ScheduleTest(
+            delay,
+            "Reception of a 80 MHz signal that occupies the last four subchannels of S160 "
+            "above ED threshold",
+            {{dBm_u{-55}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{40}, MHz_u{80}}},
+            {},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {txEndSignal1 - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {txEndSignal1 + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
-        // occupies the whole S160 with received power below the energy detection threshold for
-        // all occupied 20 MHz subchannels
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 160 MHz signal that occupies the whole S160 below ED threshold");
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal occupies
+        // the whole S160 with received power below the energy detection threshold for all occupied
+        // 20 MHz subchannels
         ScheduleTest(
             delay,
-            {{-54.0, MicroSeconds(0), MicroSeconds(100), S160_CENTER_FREQUENCY, MHz_u{160}}},
+            "Reception of a 160 MHz signal that occupies the whole S160 below ED threshold",
+            {{dBm_u{-54}, tx1Start, txDuration1, S160_CENTER_FREQUENCY, MHz_u{160}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {});
-        delay += Seconds(1.0);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
-        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal
-        // occupies the whole S160 with received power above the energy detection threshold for
-        // any occupied 20 MHz subchannels
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 160 MHz signal that occupies the whole S160 above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 8, 8, MicroSeconds(100));
+        // Verify PHY state stays IDLE and no CCA-BUSY indication is reported if a signal occupies
+        // the whole S160 with received power above the energy detection threshold for any occupied
+        // 20 MHz subchannels
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 8, 8, txDuration1);
         ScheduleTest(
             delay,
-            {{-52.0, MicroSeconds(0), MicroSeconds(100), S160_CENTER_FREQUENCY, MHz_u{160}}},
+            "Reception of a 160 MHz signal that occupies the whole S160 above ED threshold",
+            {{dBm_u{-52}, tx1Start, txDuration1, S160_CENTER_FREQUENCY, MHz_u{160}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY160,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE as long as a 320 MHz signal below the energy detection
         // threshold occupies the whole band
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 320 MHz signal that occupies the whole band below ED threshold");
         ScheduleTest(
             delay,
-            {{-51.0, MicroSeconds(0), MicroSeconds(100), P320_CENTER_FREQUENCY, MHz_u{320}}},
+            "Reception of a 320 MHz signal that occupies the whole band below ED threshold",
+            {{dBm_u{-51}, tx1Start, txDuration1, P320_CENTER_FREQUENCY, MHz_u{320}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {});
-
-        delay += Seconds(1.0);
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state is CCA-BUSY as long as a 320 MHz signal above the energy detection
         // threshold occupies the whole band
-        Simulator::Schedule(
-            delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 320 MHz signal that occupies the whole band above ED threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 16, MicroSeconds(100));
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin(), 16, txDuration1);
         ScheduleTest(
             delay,
-            {{-49.0, MicroSeconds(0), MicroSeconds(100), P320_CENTER_FREQUENCY, MHz_u{320}}},
+            "Reception of a 320 MHz signal that occupies the whole band above ED threshold",
+            {{dBm_u{-49}, tx1Start, txDuration1, P320_CENTER_FREQUENCY, MHz_u{320}}},
             {},
             {
                 {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
-                {MicroSeconds(100) - smallDelta,
+                {txEndSignal1 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(100) + smallDelta,
+                {txEndSignal1 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
-            {{MicroSeconds(100) - smallDelta,
-              MicroSeconds(100),
+            {{txEndSignal1 - smallDelta,
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY notifies CCA-BUSY for the P20 channel while the S160 channel was already
         // in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a 20 MHz signal that occupies S160 followed by the "
-                            "reception of another 20 MHz signal that occupies P20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(15) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(15) = MicroSeconds(50);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(15) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(15) = txDiff;
         ScheduleTest(
             delay,
-            {{-52.0,
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S160_CENTER_FREQUENCY + MHz_u{70},
-              MHz_u{20}},
-             {-52.0, MicroSeconds(50), MicroSeconds(100), P20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a 20 MHz signal that occupies S160 followed by the reception of another "
+            "20 MHz signal that occupies P20",
+            {{dBm_u{-52}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{70}, MHz_u{20}},
+             {dBm_u{-52}, tx2Start, txDuration2, P20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
+                {tx2Start + aCcaTimeWithDelta,
                  WifiPhyState::CCA_BUSY}, // state of primary is CCA-BUSY after
                                           // aCcaTimeWithDelta that followed the second
                                           // transmission
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY160,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_PRIMARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1.0);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but notifies CCA-BUSY for the S80 channel while the S160
         // channel was already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S160 followed by the reception of "
-                            "another signal that occupies S80");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(14) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(5) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(14) = MicroSeconds(50);
-        ScheduleTest(
-            delay,
-            {{-52.0,
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S160_CENTER_FREQUENCY + MHz_u{50},
-              MHz_u{20}},
-             {-52.0,
-              MicroSeconds(50),
-              MicroSeconds(100),
-              S80_CENTER_FREQUENCY - MHz_u{10},
-              MHz_u{20}}},
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(14) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(5) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(14) = txDiff;
+        ScheduleTest(
+            delay,
+            "Reception of a signal that occupies S160 followed by the reception of another signal "
+            "that occupies S80",
+            {{dBm_u{-52}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{50}, MHz_u{20}},
+             {dBm_u{-52}, tx2Start, txDuration2, S80_CENTER_FREQUENCY - MHz_u{10}, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays IDLE
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays IDLE
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY160,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_SECONDARY80,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1.0);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but notifies CCA-BUSY for the S40 channel while the S160
         // channel was already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S160 followed by the reception of "
-                            "another signal that occupies S40");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(13) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(3) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(13) = MicroSeconds(50);
-        ScheduleTest(
-            delay,
-            {{-52.0,
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S160_CENTER_FREQUENCY + MHz_u{30},
-              MHz_u{20}},
-             {-52.0,
-              MicroSeconds(50),
-              MicroSeconds(100),
-              S40_CENTER_FREQUENCY + MHz_u{10},
-              MHz_u{20}}},
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(13) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(3) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(13) = txDiff;
+        ScheduleTest(
+            delay,
+            "Reception of a signal that occupies S160 followed by the reception of another signal "
+            "that occupies S40",
+            {{dBm_u{-52}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{30}, MHz_u{20}},
+             {dBm_u{-52}, tx2Start, txDuration2, S40_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays IDLE
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays IDLE
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY160,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_SECONDARY40,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1.0);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but notifies CCA-BUSY for the S20 channel while the S160
         // channel was already in CCA-BUSY state
-        Simulator::Schedule(delay,
-                            &WifiPhyCcaIndicationTest::LogScenario,
-                            this,
-                            "Reception of a signal that occupies S160 followed by the reception of "
-                            "another signal that occupies S20");
-        m_expectedPer20MhzCcaBusyDurations.at(0).at(12) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = MicroSeconds(100);
-        m_expectedPer20MhzCcaBusyDurations.at(1).at(12) = MicroSeconds(50);
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(12) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(1) = txDuration2;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(12) = txDiff;
         ScheduleTest(
             delay,
-            {{-52.0,
-              MicroSeconds(0),
-              MicroSeconds(100),
-              S160_CENTER_FREQUENCY + MHz_u{10},
-              MHz_u{20}},
-             {-52.0, MicroSeconds(50), MicroSeconds(100), S20_CENTER_FREQUENCY, MHz_u{20}}},
+            "Reception of a signal that occupies S160 followed by the reception of another signal "
+            "that occupies S20",
+            {{dBm_u{-52}, tx1Start, txDuration1, S160_CENTER_FREQUENCY + MHz_u{10}, MHz_u{20}},
+             {dBm_u{-52}, tx2Start, txDuration2, S20_CENTER_FREQUENCY, MHz_u{20}}},
             {},
             {
                 {aCcaTimeWithDelta,
                  WifiPhyState::IDLE}, // state of primary stays idle after aCcaTimeWithDelta
-                {MicroSeconds(50) + aCcaTimeWithDelta,
-                 WifiPhyState::IDLE}, // state of primary stays IDLE
-                {MicroSeconds(50) + MicroSeconds(100) - smallDelta,
+                {tx2Start + aCcaTimeWithDelta, WifiPhyState::IDLE}, // state of primary stays IDLE
+                {txEndSignal2 - smallDelta,
                  WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                {MicroSeconds(50) + MicroSeconds(100) + smallDelta,
+                {txEndSignal2 + smallDelta,
                  WifiPhyState::IDLE} // IDLE just after the transmission ends
             },
             {{aCcaTimeWithDelta, // notification upon reception of the first signal
-              MicroSeconds(100),
+              1,
+              tx1Start,
+              txEndSignal1,
               WIFI_CHANLIST_SECONDARY160,
               m_expectedPer20MhzCcaBusyDurations.at(0)},
-             {MicroSeconds(50) +
-                  aCcaTimeWithDelta, // notification upon reception of the second signal
-              MicroSeconds(50) + MicroSeconds(100),
+             {tx2Start + aCcaTimeWithDelta, // notification upon reception of the second signal
+              2,
+              tx1Start,
+              txEndSignal2,
               WIFI_CHANLIST_SECONDARY,
               m_expectedPer20MhzCcaBusyDurations.at(1)}});
-        delay += Seconds(1.0);
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE when a 160 MHz PPDU that occupies S160 with received
         // power below the Per 20 MHz CCA threshold for all occupied 20 MHz subchannels
-        Simulator::Schedule(
+        ScheduleTest(
             delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 160 MHz PPDU that occupies S160 below CCA sensitivity threshold");
-        ScheduleTest(delay,
-                     {},
-                     {{-64.0, MicroSeconds(0), S160_CENTER_FREQUENCY, MHz_u{160}}},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {ppduDurations.at(160) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {ppduDurations.at(160) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {});
-        delay += Seconds(1.0);
+            "Reception of a 160 MHz PPDU that occupies S160 below CCA sensitivity threshold",
+            {},
+            {{dBm_u{-64}, tx1Start, S160_CENTER_FREQUENCY, MHz_u{160}}},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {ppduDurations.at(MHz_u{160}) - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {ppduDurations.at(MHz_u{160}) + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {});
+        delay += testStep;
 
         //----------------------------------------------------------------------------------------------------------------------------------
         // Verify PHY state stays IDLE but CCA-BUSY indication is reported when a 160 MHz PPDU
         // that occupies S160 with received power above the Per 20 MHz CCA threshold for any
         // occupied 20 MHz subchannels
-        Simulator::Schedule(
+        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 8,
+                    8,
+                    ppduDurations.at(MHz_u{160}));
+        ScheduleTest(
             delay,
-            &WifiPhyCcaIndicationTest::LogScenario,
-            this,
-            "Reception of a 160 MHz PPDU that occupies S160 above CCA sensitivity threshold");
-        std::fill_n(m_expectedPer20MhzCcaBusyDurations.at(0).begin() + 8, 8, ppduDurations.at(160));
-        ScheduleTest(delay,
-                     {},
-                     {{-62.0, MicroSeconds(0), S160_CENTER_FREQUENCY, MHz_u{160}}},
-                     {
-                         {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
-                         {ppduDurations.at(160) - smallDelta,
-                          WifiPhyState::IDLE}, // IDLE just before the transmission ends
-                         {ppduDurations.at(160) + smallDelta,
-                          WifiPhyState::IDLE} // IDLE just after the transmission ends
-                     },
-                     {{aCcaTimeWithDelta,
-                       ppduDurations.at(160),
-                       WIFI_CHANLIST_SECONDARY160,
-                       m_expectedPer20MhzCcaBusyDurations.at(0)}});
-        delay += Seconds(1.0);
+            "Reception of a 160 MHz PPDU that occupies S160 above CCA sensitivity threshold",
+            {},
+            {{dBm_u{-62}, tx1Start, S160_CENTER_FREQUENCY, MHz_u{160}}},
+            {
+                {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+                {ppduDurations.at(MHz_u{160}) - smallDelta,
+                 WifiPhyState::IDLE}, // IDLE just before the transmission ends
+                {ppduDurations.at(MHz_u{160}) + smallDelta,
+                 WifiPhyState::IDLE} // IDLE just after the transmission ends
+            },
+            {{aCcaTimeWithDelta,
+              1,
+              tx1Start,
+              ppduDurations.at(MHz_u{160}),
+              WIFI_CHANLIST_SECONDARY160,
+              m_expectedPer20MhzCcaBusyDurations.at(0)}});
+        delay += testStep;
         ResetExpectedPer20MhzCcaBusyDurations();
     }
 
+    if (m_channelWidth > MHz_u{20})
+    {
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = txDuration1;
+        m_expectedPer20MhzCcaBusyDurations.at(1).at(0) = Time();
+    }
+
+    ScheduleTest(
+        delay,
+        "Reception of signal above ED threshold that triggers CCA BUSY indication followed by CCA "
+        "IDLE indication when the threshold is increased",
+        {{dBm_u{-60}, tx1Start, txDuration1, P20_CENTER_FREQUENCY, MHz_u{20}}},
+        {},
+        {
+            {aCcaTimeWithDelta, WifiPhyState::CCA_BUSY}, // CCA-BUSY after aCcaTimeWithDelta
+            {thresholdUpdateTime - smallDelta,
+             WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the threshold changes
+            {thresholdUpdateTime + smallDelta,
+             WifiPhyState::IDLE}, // IDLE just after the threshold changes
+            {txEndSignal1 - smallDelta,
+             WifiPhyState::IDLE}, // IDLE just before the transmission ends
+            {txEndSignal1 + smallDelta, WifiPhyState::IDLE} // IDLE just after the transmission ends
+        },
+        {{thresholdUpdateTime - smallDelta,
+          1, // first notification CCA busy until end of transmission
+          tx1Start,
+          txEndSignal1,
+          WIFI_CHANLIST_PRIMARY,
+          m_expectedPer20MhzCcaBusyDurations.at(0)},
+         {txEndSignal1 - smallDelta,
+          2, // second notification CCA busy ends when threshold changed
+          tx1Start,
+          thresholdUpdateTime,
+          WIFI_CHANLIST_PRIMARY,
+          m_expectedPer20MhzCcaBusyDurations.at(1)}},
+        {
+            {.timePoint = thresholdUpdateTime,
+             .ccaThreshold =
+                 dBm_u{-50}}, // change CCA threshold during the reception to a higher value
+        });
+    delay += testStep;
+    ResetExpectedPer20MhzCcaBusyDurations();
+
+    if (m_channelWidth > MHz_u{20})
+    {
+        m_expectedPer20MhzCcaBusyDurations.at(0).at(0) = txDuration1 - thresholdUpdateTime;
+    }
+
+    ScheduleTest(
+        delay,
+        "Reception of signal below ED threshold that triggers CCA IDLE indication followed by CCA "
+        "BUSY indication when the threshold is decreased",
+        {{dBm_u{-65}, tx1Start, txDuration1, P20_CENTER_FREQUENCY, MHz_u{20}}},
+        {},
+        {
+            {aCcaTimeWithDelta, WifiPhyState::IDLE}, // IDLE after aCcaTimeWithDelta
+            {thresholdUpdateTime - smallDelta,
+             WifiPhyState::IDLE}, // IDLE just before the threshold changes
+            {thresholdUpdateTime + smallDelta,
+             WifiPhyState::CCA_BUSY}, // CCA-BUSY just after the threshold changes
+            {txEndSignal1 - smallDelta,
+             WifiPhyState::CCA_BUSY}, // CCA-BUSY just before the transmission ends
+            {txEndSignal1 + smallDelta, WifiPhyState::IDLE} // IDLE just after the transmission ends
+        },
+        {
+            {txEndSignal1 - smallDelta,
+             1, // first notification CCA busy until end of transmission since threshold changed
+             thresholdUpdateTime,
+             txEndSignal1,
+             WIFI_CHANLIST_PRIMARY,
+             m_expectedPer20MhzCcaBusyDurations.at(0)},
+        },
+        {
+            {.timePoint = thresholdUpdateTime,
+             .ccaThreshold =
+                 dBm_u{-70}}, // change CCA threshold during the reception to a higher value
+        });
+    delay += testStep;
+    ResetExpectedPer20MhzCcaBusyDurations();
+
     Simulator::Run();
 }
 
@@ -3807,8 +3770,8 @@
 {
     m_rxPhy->Dispose();
     m_rxPhy = nullptr;
-    m_txPhy->Dispose();
-    m_txPhy = nullptr;
+    m_uxPhy->Dispose();
+    m_uxPhy = nullptr;
     for (auto& signalGenerator : m_signalGenerators)
     {
         signalGenerator->Dispose();
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-phy-mu-mimo-test.cc ns-3.47/src/wifi/test/wifi-phy-mu-mimo-test.cc
--- ns-3.46/src/wifi/test/wifi-phy-mu-mimo-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-phy-mu-mimo-test.cc	2026-02-16 16:08:49.882231000 -0800
@@ -535,7 +535,7 @@
     NS_ASSERT(staInfos.size() > 1);
 
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs7(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_MU,
                                          NanoSeconds(800),
                                          1,
@@ -1289,7 +1289,7 @@
     WifiConstPsduMap psdus;
 
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs7(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -1324,7 +1324,7 @@
                                                     uint8_t bssColor) const
 {
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs7(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_TB,
                                          NanoSeconds(1600),
                                          1,
@@ -1348,7 +1348,7 @@
 TestUlMuMimoPhyTransmission::SetTrigVector(const std::vector<uint16_t>& staIds, uint8_t bssColor)
 {
     WifiTxVector txVector(HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_TB,
                           NanoSeconds(1600),
                           1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-phy-ofdma-test.cc ns-3.47/src/wifi/test/wifi-phy-ofdma-test.cc
--- ns-3.46/src/wifi/test/wifi-phy-ofdma-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-phy-ofdma-test.cc	2026-02-16 16:08:49.882231000 -0800
@@ -550,7 +550,7 @@
     WifiConstPsduMap psdus;
     WifiTxVector txVector{
         (m_modClass == WIFI_MOD_CLASS_HE) ? HePhy::GetHeMcs7() : EhtPhy::GetEhtMcs7(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_modClass == WIFI_MOD_CLASS_HE) ? WIFI_PREAMBLE_HE_MU : WIFI_PREAMBLE_EHT_MU,
         NanoSeconds(800),
         1,
@@ -1598,7 +1598,7 @@
     NS_LOG_FUNCTION(this << rxStaId1 << rxStaId2);
     WifiConstPsduMap psdus;
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_MU,
                           NanoSeconds(800),
                           1,
@@ -2298,7 +2298,7 @@
 {
     WifiConstPsduMap psdus;
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_MU,
                           NanoSeconds(800),
                           1,
@@ -2350,7 +2350,7 @@
     WifiConstPsduMap psdus2;
 
     WifiTxVector txVector1{HePhy::GetHeMcs7(),
-                           0,
+                           WIFI_MIN_TX_PWR_LEVEL,
                            WIFI_PREAMBLE_HE_TB,
                            NanoSeconds(1600),
                            1,
@@ -2429,7 +2429,7 @@
 {
     WifiConstPsduMap psdus;
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_SU,
                           NanoSeconds(800),
                           1,
@@ -2582,7 +2582,7 @@
     : TestCase("UL-OFDMA multiple RX events test"),
       m_totalBytesDropped(0),
       m_trigVector(HePhy::GetHeMcs7(),
-                   0,
+                   WIFI_MIN_TX_PWR_LEVEL,
                    WIFI_PREAMBLE_HE_TB,
                    NanoSeconds(1600),
                    1,
@@ -2647,7 +2647,7 @@
 {
     WifiConstPsduMap psdus;
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_TB,
                           NanoSeconds(1600),
                           1,
@@ -3482,7 +3482,7 @@
 
     WifiTxVector txVector{
         (m_modClass == WIFI_MOD_CLASS_HE) ? HePhy::GetHeMcs7() : EhtPhy::GetEhtMcs7(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_modClass == WIFI_MOD_CLASS_HE) ? WIFI_PREAMBLE_HE_SU : WIFI_PREAMBLE_EHT_MU,
         NanoSeconds(800),
         1,
@@ -3535,7 +3535,7 @@
 {
     WifiTxVector txVector{
         (m_modClass == WIFI_MOD_CLASS_HE) ? HePhy::GetHeMcs7() : EhtPhy::GetEhtMcs7(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_modClass == WIFI_MOD_CLASS_HE) ? WIFI_PREAMBLE_HE_TB : WIFI_PREAMBLE_EHT_TB,
         NanoSeconds(1600),
         1,
@@ -3619,7 +3619,7 @@
 
     WifiTxVector txVector(
         (m_modClass == WIFI_MOD_CLASS_HE) ? HePhy::GetHeMcs7() : EhtPhy::GetEhtMcs7(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_modClass == WIFI_MOD_CLASS_HE) ? WIFI_PREAMBLE_HE_TB : WIFI_PREAMBLE_EHT_TB,
         NanoSeconds(1600),
         1,
@@ -5269,7 +5269,7 @@
     WifiConstPsduMap psdus;
 
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_TB,
                           NanoSeconds(1600),
                           1,
@@ -5570,7 +5570,7 @@
 TestPhyPaddingExclusion::SetTrigVector(Time ppduDuration)
 {
     WifiTxVector trigVector{HePhy::GetHeMcs7(),
-                            0,
+                            WIFI_MIN_TX_PWR_LEVEL,
                             WIFI_PREAMBLE_HE_TB,
                             NanoSeconds(1600),
                             1,
@@ -5803,7 +5803,7 @@
       m_txPowerAp(dBm_u{0}),
       m_txPowerStart(dBm_u{0}),
       m_txPowerEnd(dBm_u{0}),
-      m_txPowerLevels(0),
+      m_txPowerLevels(1),
       m_requestedRssiSta1(dBm_u{0}),
       m_requestedRssiSta2(dBm_u{0}),
       m_rssiSta1(dBm_u{0}),
@@ -5886,7 +5886,7 @@
 
     WifiConstPsduMap psdus;
     WifiTxVector txVector{HePhy::GetHeMcs7(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_HE_SU,
                           NanoSeconds(800),
                           1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-phy-reception-test.cc ns-3.47/src/wifi/test/wifi-phy-reception-test.cc
--- ns-3.46/src/wifi/test/wifi-phy-reception-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-phy-reception-test.cc	2026-02-16 16:08:49.883231000 -0800
@@ -108,7 +108,7 @@
 WifiPhyReceptionTest::SendPacket(dBm_u rxPower, uint32_t packetSize, uint8_t mcs)
 {
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs(mcs),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -2730,7 +2730,7 @@
 TestAmpduReception::SendAmpduWithThreeMpdus(dBm_u rxPower, uint32_t referencePacketSize)
 {
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -4265,7 +4265,7 @@
 TestUnsupportedBandwidthReception::SendPpdu(MHz_u centerFreq, MHz_u bandwidth)
 {
     auto txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                 0,
+                                 WIFI_MIN_TX_PWR_LEVEL,
                                  WIFI_PREAMBLE_HE_SU,
                                  NanoSeconds(800),
                                  1,
@@ -4526,7 +4526,7 @@
     m_txPhy->SetOperatingChannel(
         WifiPhy::ChannelTuple{channelInfo.number, channelInfo.width, channelInfo.band, 0});
     auto txVector = WifiTxVector(HePhy::GetHeMcs7(),
-                                 0,
+                                 WIFI_MIN_TX_PWR_LEVEL,
                                  WIFI_PREAMBLE_HE_SU,
                                  NanoSeconds(800),
                                  1,
@@ -4736,7 +4736,7 @@
 TestSpectrumChannelWithBandwidthFilter::Send() const
 {
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs7(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -4914,7 +4914,7 @@
 TestPhyDropDueToTx::Send(Ptr<WifiPhy> phy) const
 {
     const auto txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                       0,
+                                       WIFI_MIN_TX_PWR_LEVEL,
                                        WIFI_PREAMBLE_HE_SU,
                                        NanoSeconds(800),
                                        1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-phy-rx-trace-helper-test.cc ns-3.47/src/wifi/test/wifi-phy-rx-trace-helper-test.cc
--- ns-3.46/src/wifi/test/wifi-phy-rx-trace-helper-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-phy-rx-trace-helper-test.cc	2026-02-16 16:08:49.883231000 -0800
@@ -257,7 +257,7 @@
     tx_phy->SetTxPowerEnd(txPower);
 
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -297,7 +297,7 @@
     tx_phy->SetTxPowerEnd(txPower);
 
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -1129,7 +1129,7 @@
     tx_phy->SetTxPowerEnd(txPower);
 
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -1170,7 +1170,7 @@
 
     WifiTxVector txVector =
         WifiTxVector(HePhy::GetHeMcs0(),
-                     0,
+                     WIFI_MIN_TX_PWR_LEVEL,
                      WIFI_PREAMBLE_HE_SU,
                      NanoSeconds(800),
                      1,
@@ -1890,7 +1890,7 @@
     propLoss->SetRss(rxPower);
 
     WifiTxVector txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                         0,
+                                         WIFI_MIN_TX_PWR_LEVEL,
                                          WIFI_PREAMBLE_HE_SU,
                                          NanoSeconds(800),
                                          1,
@@ -1936,7 +1936,7 @@
     propLoss->SetRss(rxPower);
 
     auto txVector = WifiTxVector(HePhy::GetHeMcs0(),
-                                 0,
+                                 WIFI_MIN_TX_PWR_LEVEL,
                                  WIFI_PREAMBLE_HE_SU,
                                  NanoSeconds(800),
                                  1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-phy-thresholds-test.cc ns-3.47/src/wifi/test/wifi-phy-thresholds-test.cc
--- ns-3.46/src/wifi/test/wifi-phy-thresholds-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-phy-thresholds-test.cc	2026-02-16 16:08:49.883231000 -0800
@@ -129,7 +129,7 @@
 WifiPhyThresholdsTest::MakeWifiSignal(Watt_u txPower, const WifiPhyOperatingChannel& channel)
 {
     WifiTxVector txVector{OfdmPhy::GetOfdmRate6Mbps(),
-                          0,
+                          WIFI_MIN_TX_PWR_LEVEL,
                           WIFI_PREAMBLE_LONG,
                           NanoSeconds(800),
                           1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-primary-channels-test.cc ns-3.47/src/wifi/test/wifi-primary-channels-test.cc
--- ns-3.46/src/wifi/test/wifi-primary-channels-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-primary-channels-test.cc	2026-02-16 16:08:49.883231000 -0800
@@ -459,7 +459,7 @@
     pkt->AddHeader(trigger);
 
     m_triggerTxVector = WifiTxVector(OfdmPhy::GetOfdmRate6Mbps(),
-                                     0,
+                                     WIFI_MIN_TX_PWR_LEVEL,
                                      WIFI_PREAMBLE_LONG,
                                      NanoSeconds(800),
                                      1,
@@ -768,7 +768,7 @@
     uint8_t bssColor = apDev->GetHeConfiguration()->m_bssColor;
     auto txVector = WifiTxVector(
         (m_standard == WIFI_STANDARD_80211ax) ? HePhy::GetHeMcs8() : EhtPhy::GetEhtMcs8(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_standard == WIFI_STANDARD_80211ax) ? WIFI_PREAMBLE_HE_SU : WIFI_PREAMBLE_EHT_MU,
         NanoSeconds(800),
         1,
@@ -805,7 +805,7 @@
 
     auto txVector = WifiTxVector(
         (m_standard == WIFI_STANDARD_80211ax) ? HePhy::GetHeMcs8() : EhtPhy::GetEhtMcs8(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_standard == WIFI_STANDARD_80211ax) ? WIFI_PREAMBLE_HE_MU : WIFI_PREAMBLE_EHT_MU,
         NanoSeconds(800),
         1,
@@ -934,7 +934,7 @@
     uint16_t length = 0;
     WifiTxVector trigVector(
         (m_standard == WIFI_STANDARD_80211ax) ? HePhy::GetHeMcs8() : EhtPhy::GetEhtMcs8(),
-        0,
+        WIFI_MIN_TX_PWR_LEVEL,
         (m_standard == WIFI_STANDARD_80211ax) ? WIFI_PREAMBLE_HE_TB : WIFI_PREAMBLE_EHT_TB,
         NanoSeconds(3200),
         1,
@@ -1002,7 +1002,7 @@
         const auto staId = DynamicCast<StaWifiMac>(staDev->GetMac())->GetAssociationId();
         WifiTxVector txVector(
             (m_standard == WIFI_STANDARD_80211ax) ? HePhy::GetHeMcs8() : EhtPhy::GetEhtMcs8(),
-            0,
+            WIFI_MIN_TX_PWR_LEVEL,
             (m_standard == WIFI_STANDARD_80211ax) ? WIFI_PREAMBLE_HE_TB : WIFI_PREAMBLE_EHT_TB,
             NanoSeconds(3200),
             1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-retransmit-test.cc ns-3.47/src/wifi/test/wifi-retransmit-test.cc
--- ns-3.46/src/wifi/test/wifi-retransmit-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-retransmit-test.cc	2026-02-16 16:08:49.883231000 -0800
@@ -393,7 +393,7 @@
 
     // check width of transmitted frames, except CTS because we do not support yet static/dynamic
     // bandwidth operation
-    const auto expectedWidth = (phyId == 0 ? 80 : 40);
+    const auto expectedWidth = (phyId == 0 ? MHz_u{80} : MHz_u{40});
     if (!hdr.IsCts())
     {
         NS_TEST_EXPECT_MSG_EQ(txVector.GetChannelWidth(),
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/wifi/test/wifi-test.cc ns-3.47/src/wifi/test/wifi-test.cc
--- ns-3.46/src/wifi/test/wifi-test.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/wifi/test/wifi-test.cc	2026-02-16 16:08:49.884231000 -0800
@@ -3616,7 +3616,7 @@
 {
     uint8_t staId = 1;
     auto txVector = WifiTxVector(HePhy::GetHeMcs(0),
-                                 0,
+                                 WIFI_MIN_TX_PWR_LEVEL,
                                  WIFI_PREAMBLE_HE_MU,
                                  guardInterval,
                                  1,
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/zigbee/model/zigbee-nwk.cc ns-3.47/src/zigbee/model/zigbee-nwk.cc
--- ns-3.46/src/zigbee/model/zigbee-nwk.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/zigbee/model/zigbee-nwk.cc	2026-02-16 16:08:49.887231000 -0800
@@ -677,7 +677,7 @@
                     }
                 }
 
-                Ptr<PendingTxPkt> pendingTxPkt = Create<PendingTxPkt>();
+                auto pendingTxPkt = std::make_shared<PendingTxPkt>();
                 if (!m_pendingTxQueue.empty() &&
                     DequeuePendingTx(payload.GetRespAddr(), pendingTxPkt))
                 {
@@ -1232,7 +1232,7 @@
 void
 ZigbeeNwk::McpsDataConfirm(McpsDataConfirmParams params)
 {
-    Ptr<TxPkt> bufferedElement;
+    std::shared_ptr<TxPkt> bufferedElement;
     if (RetrieveTxPkt(params.m_msduHandle, bufferedElement))
     {
         ZigbeeNwkHeader nwkHeader;
@@ -3004,7 +3004,7 @@
         ZigbeeNwkHeader peekedNwkHeader;
         p->PeekHeader(peekedNwkHeader);
 
-        Ptr<PendingTxPkt> pendingTxPkt = Create<PendingTxPkt>();
+        auto pendingTxPkt = std::make_shared<PendingTxPkt>();
         pendingTxPkt->dstAddr = peekedNwkHeader.GetDstAddr();
         pendingTxPkt->nsduHandle = nsduHandle;
         pendingTxPkt->txPkt = p;
@@ -3019,7 +3019,7 @@
 }
 
 bool
-ZigbeeNwk::DequeuePendingTx(Mac16Address dst, Ptr<PendingTxPkt> entry)
+ZigbeeNwk::DequeuePendingTx(Mac16Address dst, std::shared_ptr<PendingTxPkt> entry)
 {
     // TODO : PurgeTxQueue();
 
@@ -3055,7 +3055,7 @@
 {
     if (m_txBuffer.size() < m_txBufferMaxSize)
     {
-        Ptr<TxPkt> txPkt = Create<TxPkt>();
+        auto txPkt = std::make_shared<TxPkt>();
         txPkt->macHandle = macHandle;
         txPkt->nwkHandle = nwkHandle;
         txPkt->txPkt = p;
@@ -3069,16 +3069,17 @@
 }
 
 bool
-ZigbeeNwk::RetrieveTxPkt(uint8_t macHandle, Ptr<TxPkt>& txPkt)
+ZigbeeNwk::RetrieveTxPkt(uint8_t macHandle, std::shared_ptr<TxPkt>& txPkt)
 {
-    for (auto bufferedPkt : m_txBuffer)
+    for (const auto& bufferedPkt : m_txBuffer)
     {
         if (bufferedPkt->macHandle == macHandle)
         {
             txPkt = bufferedPkt;
 
-            std::erase_if(m_txBuffer,
-                          [&macHandle](Ptr<TxPkt> pkt) { return pkt->macHandle == macHandle; });
+            std::erase_if(m_txBuffer, [&macHandle](std::shared_ptr<TxPkt> pkt) {
+                return pkt->macHandle == macHandle;
+            });
 
             return true;
         }
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/src/zigbee/model/zigbee-nwk.h ns-3.47/src/zigbee/model/zigbee-nwk.h
--- ns-3.46/src/zigbee/model/zigbee-nwk.h	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/src/zigbee/model/zigbee-nwk.h	2026-02-16 16:08:49.887231000 -0800
@@ -1057,7 +1057,7 @@
     /**
      * Structure representing an element in the pending transaction queue.
      */
-    struct PendingTxPkt : public SimpleRefCount<PendingTxPkt>
+    struct PendingTxPkt
     {
         uint8_t nsduHandle;   //!< The NSDU handle
         Mac16Address dstAddr; //!< The destination network Address
@@ -1069,7 +1069,7 @@
      * The pending transaction queue of data packets awaiting to be transmitted
      * until a route to the destination becomes available.
      */
-    std::deque<Ptr<PendingTxPkt>> m_pendingTxQueue;
+    std::deque<std::shared_ptr<PendingTxPkt>> m_pendingTxQueue;
 
     /**
      * The maximum size of the pending transaction queue.
@@ -1079,7 +1079,7 @@
     /**
      * Structure representing an element in the Tx Buffer.
      */
-    struct TxPkt : public SimpleRefCount<TxPkt>
+    struct TxPkt
     {
         uint8_t macHandle; //!< The mac handle (msdu handle).
         uint8_t nwkHandle; //!< The nwk handle (nsdu handle).
@@ -1090,7 +1090,7 @@
      * The transmission buffer. Copies of DATA packets are stored here
      * for post transmission handling.
      */
-    std::deque<Ptr<TxPkt>> m_txBuffer;
+    std::deque<std::shared_ptr<TxPkt>> m_txBuffer;
 
     /**
      * The maximum size of the transmission buffer
@@ -1293,7 +1293,7 @@
      *
      * @return True if successfully dequeued
      */
-    bool DequeuePendingTx(Mac16Address dst, Ptr<PendingTxPkt> entry);
+    bool DequeuePendingTx(Mac16Address dst, std::shared_ptr<PendingTxPkt> entry);
 
     /**
      * Dispose of all PendingTxPkt accumulated in the pending transmission queue.
@@ -1320,7 +1320,7 @@
      * @param txPkt The packet buffered in the TxPkt buffer.
      * @return True if the txPkt is successfully retrieved.
      */
-    bool RetrieveTxPkt(uint8_t macHandle, Ptr<TxPkt>& txPkt);
+    bool RetrieveTxPkt(uint8_t macHandle, std::shared_ptr<TxPkt>& txPkt);
 
     /**
      * Dispose of all the entries in the TxPkt Buffer.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/test.py ns-3.47/test.py
--- ns-3.46/test.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/test.py	2026-02-16 16:08:49.887231000 -0800
@@ -220,6 +220,9 @@
         # Add all of the C++ examples that were built, i.e. found
         # in the directory, to the list of C++ examples to run.
         if os.path.exists(example_path):
+            # Handle paths with empty spaces
+            example_path = '"%s"' % example_path
+
             # Add any arguments to the path.
             if len(example_name_parts) != 1:
                 example_path = "%s %s" % (example_path, example_arguments)
@@ -322,7 +325,7 @@
 # expected that this output will be fine for developers looking for problems.
 #
 def node_to_text(test, f, test_type="Suite"):
-    (result, name, reason, time_real) = read_test(test)
+    result, name, reason, time_real = read_test(test)
     if reason:
         reason = " (%s)" % reason
 
@@ -390,7 +393,7 @@
             #
             # For each test suite, get its name, result and execution time info
             #
-            (result, name, reason, time) = read_test(suite)
+            result, name, reason, time = read_test(suite)
 
             #
             # Print a level three header with the result, name and time.  If the
@@ -470,7 +473,7 @@
                 # Get the name, result and timing information from xml to use in
                 # printing table below.
                 #
-                (result, name, reason, time) = read_test(case)
+                result, name, reason, time = read_test(case)
 
                 #
                 # If the test case failed, we iterate through possibly multiple
@@ -583,7 +586,7 @@
             #
             # Get the result and name of the example in question
             #
-            (result, name, reason, time) = read_test(example)
+            result, name, reason, time = read_test(example)
 
             #
             # If the example either failed or crashed, print its result status
@@ -867,15 +870,26 @@
     if is_python:
         path_cmd = PYTHON[0] + " " + os.path.join(NS3_BASEDIR, shell_command)
     else:
+
+        # Here we handle paths with empty spaces.
+        # To do that, we need to separate program from arguments, then quote the absolute program path with spaces.
+        shell_args = re.findall(r'(?:".*?"|\S)+', shell_command)
+
         if len(build_path):
-            path_cmd = os.path.join(build_path, shell_command)
+            path_cmd = os.path.join(build_path, shell_args[0])
         else:
-            path_cmd = os.path.join(NS3_BUILDDIR, shell_command)
+            path_cmd = (
+                shell_args[0].strip('"')
+                if NS3_BUILDDIR in shell_args[0]
+                else os.path.join(NS3_BUILDDIR, shell_args[0])
+            )
+
+        path_cmd = f'"{path_cmd}" {" ".join(shell_args[1:])}'
 
     if valgrind:
         if VALGRIND_SUPPRESSIONS_FILE:
             cmd = (
-                "valgrind --suppressions=%s --leak-check=full --show-reachable=yes --error-exitcode=2 --errors-for-leak-kinds=all %s"
+                'valgrind --suppressions="%s" --leak-check=full --show-reachable=yes --error-exitcode=2 --errors-for-leak-kinds=all %s'
                 % (suppressions_path, path_cmd)
             )
         else:
@@ -1193,7 +1207,7 @@
         exit(-1)
 
     for test_type in ["Test", "Example"]:
-        if previous_run_results.find(test_type):
+        if previous_run_results.find(test_type) is not None:
             temp = list(
                 map(
                     lambda x: (x.find("Name").text, x.find("Result").text),
@@ -1372,7 +1386,7 @@
     #
     if args.kinds:
         path_cmd = os.path.join("utils", test_runner_name + " --print-test-type-list")
-        (rc, standard_out, standard_err, et) = run_job_synchronously(
+        rc, standard_out, standard_err, et = run_job_synchronously(
             path_cmd, os.getcwd(), False, False
         )
         print(standard_out)
@@ -1390,7 +1404,7 @@
                 path_cmd = os.path.join(
                     "utils", test_runner_name + " --print-test-name-list --print-test-types"
                 )
-            (rc, standard_out, standard_err, et) = run_job_synchronously(
+            rc, standard_out, standard_err, et = run_job_synchronously(
                 path_cmd, os.getcwd(), False, False
             )
             if rc != 0:
@@ -1508,7 +1522,7 @@
     if len(args.suite):
         # See if this is a valid test suite.
         path_cmd = os.path.join("utils", test_runner_name + " --print-test-name-list")
-        (rc, suites, standard_err, et) = run_job_synchronously(path_cmd, os.getcwd(), False, False)
+        rc, suites, standard_err, et = run_job_synchronously(path_cmd, os.getcwd(), False, False)
 
         if isinstance(suites, bytes):
             suites = suites.decode()
@@ -1533,12 +1547,12 @@
                 "utils",
                 test_runner_name + " --print-test-name-list --test-type=%s" % args.constrain,
             )
-            (rc, suites, standard_err, et) = run_job_synchronously(
+            rc, suites, standard_err, et = run_job_synchronously(
                 path_cmd, os.getcwd(), False, False
             )
         else:
             path_cmd = os.path.join("utils", test_runner_name + " --print-test-name-list")
-            (rc, suites, standard_err, et) = run_job_synchronously(
+            rc, suites, standard_err, et = run_job_synchronously(
                 path_cmd, os.getcwd(), False, False
             )
     else:
@@ -1566,7 +1580,7 @@
         path_cmd = os.path.join(
             "utils", test_runner_name + " --print-test-name-list --test-type=%s" % "performance"
         )
-        (rc, performance_tests, standard_err, et) = run_job_synchronously(
+        rc, performance_tests, standard_err, et = run_job_synchronously(
             path_cmd, os.getcwd(), False, False
         )
         if isinstance(performance_tests, bytes):
@@ -1762,7 +1776,7 @@
 
                 for name, test, do_run, do_valgrind_run, fullness in example_tests:
                     # Remove any arguments and directory names from test.
-                    test_name = test.split(" ", 1)[0]
+                    test_name = re.findall(r'(?:".*?"|\S)+', test)[0].strip('"')
                     test_name = os.path.basename(test_name)
                     test_name = test_name[:-4] if sys.platform == "win32" else test_name
 
@@ -2233,7 +2247,7 @@
     # directories, which will probably surprise the user.
     #
     if not args.retain:
-        shutil.rmtree(testpy_output_dir)
+        shutil.rmtree(TMP_OUTPUT_DIR)
 
     if passed_tests + skipped_tests == total_tests:
         return 0  # success
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/check-style-clang-format.py ns-3.47/utils/check-style-clang-format.py
--- ns-3.46/utils/check-style-clang-format.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/check-style-clang-format.py	2026-02-16 16:08:49.888231000 -0800
@@ -659,7 +659,7 @@
                 continue
 
         # Check if the line is compliant with the style and fix it
-        (is_line_compliant, line_fixed, line_verbose_infos) = check_style_line_function(
+        is_line_compliant, line_fixed, line_verbose_infos = check_style_line_function(
             line, filename, i
         )
 
@@ -850,7 +850,9 @@
     ]
     # fmt: on
 
+    # REUSE-IgnoreStart
     SPDX_LICENSE = "SPDX-License-Identifier: GPL-2.0-only"
+    # REUSE-IgnoreEnd
 
     is_line_compliant = True
     line_fixed = line
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/codespell-ignored-lines ns-3.47/utils/codespell-ignored-lines
--- ns-3.46/utils/codespell-ignored-lines	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/codespell-ignored-lines	2026-02-16 16:08:49.888231000 -0800
@@ -9,21 +9,6 @@
             Ptr<Packet> padd = Create<Packet>(buffer, 46 - p->GetSize());
             p->AddAtEnd(padd);
 
-#epc-gtpu-header.h: 236
-     * This field is used to co-ordinate the data transmission for acknowledged mode
-
-#mobility-model.cc:100, 102
-    Vector oPosition = other->DoGetPosition();
-    return CalculateDistance(position, oPosition);
-
-#statistics.rst:299-300
-    read ANS
-    if [ "$ANS" = "yes" -o "$ANS" = "y" ]
-
-#wifi-example-db.sh:34-35
-  read ANS
-  if [ "$ANS" = "yes" -o "$ANS" = "y" ]
-
 #doxygen.warnings.report.sh:483, 485, 544, 549, 556, 561
 filterin=
     filterin=$(              \
@@ -36,15 +21,9 @@
             tbe = math.log10(dx_over_ndiv)
             div = pow(10, rint(tbe))
 
-#./utils/tests/test-ns3.py:2944
-            'convert {image} -colorspace HSI -channel b -separate +channel -scale 1x1 -format "%[fx:100*u]" info:'
-
 #./doc/tutorial/source/introduction.rst:80
 Network Animator nam.  It is not possible to run a simulation
 
-#./src/internet-apps/model/v4ping.h:37
- * \brief an application which sends one ICMP ECHO request, waits for a REPLYs
-
 #./src/openflow/model/openflow-switch-net-device.cc:544, 548
                 arp_h->ar_tha);                                       // Target hardware address.
             arp_h->ar_hln = sizeof arp_h->ar_tha;                     // Hardware address length.
@@ -84,53 +63,25 @@
  * Copyright (c) 2024 Universita' degli Studi di Napoli Federico II
  * Copyright (c) 2025 Universita' degli Studi di Napoli Federico II
 
-./src/core/examples/hash-example.cc
+#./src/core/examples/hash-example.cc
 6ed21de2  brand-newness       peripherial
 
-./src/core/model/trickle-timer.cc:120
-    return std::countr_zero(m_ticks);
-
-./src/dsr/model/dsr-routing.cc:243, 246, 3360, 3363
-            .AddAttribute("GratReplyHoldoff",
-                          MakeTimeAccessor(&DsrRouting::m_gratReplyHoldoff),
-                                   m_gratReplyHoldoff))) // Find the gratuitous reply entry
-        GraReplyEntry graReplyEntry(source, srcAddress, m_gratReplyHoldoff + Simulator::Now());
-
-./src/dsr/model/dsr-routing.h:882
-    Time m_gratReplyHoldoff; ///< The max gratuitous reply hold off time
-
-./src/dsr/model/dsr-gratuitous-reply-table.cc:75, 76
-            i->m_gratReplyHoldoff =
-                std::max(gratReplyHoldoff + Simulator::Now(), i->m_gratReplyHoldoff);
-
-./src/dsr/model/dsr-gratuitous-reply-table.h:54, 66, 137
-    Time m_gratReplyHoldoff; ///< gratuitous reply holdoff time
-          m_gratReplyHoldoff(h)
-            return (b.m_gratReplyHoldoff < Simulator::Now());
-
-./RELEASE_NOTES.md:1087
+#./RELEASE_NOTES.md:1087
 - Bug 2530 - aodv: Rename aodv::SetBalckListTimeout to aodv::SetBlackListTimeout
 
-./doc/installation/source/linux.rst:122
+#./doc/installation/source/linux.rst:122
   apt install gir1.2-goocanvas-2.0 python3-gi python3-gi-cairo python3-pygraphviz gir1.2-gtk-3.0 ipython3
 
-./doc/tutorial/source/getting-started.rst:347:
-  >> Searching for system dependency gir-bindings - OK
-
-./src/wave/examples/vanet-routing-compare.cc:1565, 1971
-static ns3::GlobalValue g_rate("VRCrate",
-    GlobalValue::GetValueByName("VRCrate", stringValue);
-
-./src/lte/model/lte-mi-error-model.h:23:
+#./src/lte/model/lte-mi-error-model.h:23:
  *      for mobIlE Video AppLications) project, which is a
 
-./src/traffic-control/doc/tbf.rst:84:
+#./src/traffic-control/doc/tbf.rst:84:
 .. [Ref2] J. Vehent; Journey to the Center of the Linux Kernel: Traffic Control, Shaping and QoS; Available online at `<https://web.archive.org/web/20190930031243/http://wiki.linuxwall.info/doku.php/en:ressources:dossiers:networking:traffic_control#tbf_-_token_bucket_filter>`_.
 
-./src/traffic-control/model/codel-queue-disc.h:17:
+#./src/traffic-control/model/codel-queue-disc.h:17:
  * Codel, the COntrolled DELay Queueing discipline
 
-./CHANGES.md:
+#./CHANGES.md:
   * (dsr) Class `DsrOptionRerrUnsupportHeader` from `dsr-option-header.h` was renamed `DsrOptionRerrUnsupportedHeader`.
   * (internet) Enumerated value `IPV6_EXT_AUTHENTIFICATION` from `ipv6-header.h` was renamed `IPV6_EXT_AUTHENTICATION`.
   * (lr-wpan) Constant `aMaxBeaconPayloadLenght` from `lr-wpan-constants.h` was renamed `aMaxBeaconPayloadLength`.
@@ -158,19 +109,13 @@
 * The spelling of the attribute `IntialCellVoltage` from `LiIonEnergySource` was corrected to `InitialCellVoltage`; this will affect existing users who were using the attribute with the misspelling.
 * The attribute `Recievers` in class `YansWifiPhy` was misspelled, so this has been corrected to `Receivers`.
 
-./doc/manual/source/working-with-docker.rst
+#./doc/manual/source/working-with-docker.rst
     067a8b748816715d93ede9099132d943c04b13fcc23b3b8a7e21d23c1096cc2a
 
-./src/lte/model/cqa-ff-mac-scheduler.cc
+#./src/lte/model/cqa-ff-mac-scheduler.cc
     std::map<LteFlowId_t, int> UEtoHOL;
         UEtoHOL.insert(std::pair<LteFlowId_t, int>(itLogicalChannels->first, delay));
                 int hol = UEtoHOL.find(flowId)->second;
 
-./src/mesh/doc/source/mesh-design.rst
-contribution in [Hep15]_.
-when forwarding a frame (e.g., [Hep16]_), but current wifi module maintainers
-value was chosen based on the measurement results reported in [Hep16]_ which
-
-./src/mesh/doc/source/mesh-references.rst
-.. [Hep15] \C. Hepner and \A. Witt and \R. Muenzner, `Validation of the ns-3 802.11s model and proposed changes compliant to IEEE 802.11-2012 <https://www.nsnam.org/wp-content/uploads/2015/05/WNS3_2015_submission_33.pdf>`_, Poster at 2015 Workshop on ns-3, May 2015.
-.. [Hep16] \C. Hepner and \S. Moll and \R. Muenzner, `Influence of Processing Delays on the VoIP Performance for IEEE 802.11s Multihop Wireless Mesh Networks:  Comparison of ns-3 Network Simulations with Hardware Measurements <http://dl.acm.org/citation.cfm?id=3021439>`_, Proceedings of SIMUTOOLS 16, August, 2016.
+#./src/mobility/model/steady-state-random-waypoint-mobility-model.cc
+    // Serie II, Tomo XLVIII (1999), p. 163.
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/print-introspected-doxygen.cc ns-3.47/utils/print-introspected-doxygen.cc
--- ns-3.46/utils/print-introspected-doxygen.cc	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/print-introspected-doxygen.cc	2026-02-16 16:08:49.888231000 -0800
@@ -42,6 +42,10 @@
 
 /**
  * Markup tokens.
+ *
+ * Initializers given here are empty or whitespace values for text output.
+ * Other values for text output, and values for
+ * HTML output are configured in SetMarkup().
  * @{
  */
 std::string anchor;             ///< hyperlink anchor
@@ -51,13 +55,13 @@
 std::string boldWithIdStart;    ///< start of bold with id
 std::string boldWithIdMid;      ///< middle of bold span with id
 std::string boldWithIdStop;     ///< end of bold span with id
-std::string breakBoth;          ///< linebreak
+std::string breakBoth{"\n"};    ///< linebreak
 std::string breakHtmlOnly;      ///< linebreak for html output only
 std::string breakTextOnly;      ///< linebreak for text output only
 std::string brief;              ///< brief tag
 std::string classStart;         ///< start of a class
 std::string classStop;          ///< end of a class
-std::string codeWord;           ///< format next word as source code
+std::string codeWord{" "};      ///< format next word as source code
 std::string commentStart;       ///< start of code comment
 std::string commentStop;        ///< end of code comment
 std::string copyDoc;            ///< copy (or refer) to docs elsewhere
@@ -71,10 +75,11 @@
 std::string headingWithIdStart; ///< start of section heading with id (h3)
 std::string headingWithIdMid;   ///< middle of section heading with id (h3)
 std::string headingWithIdStop;  ///< end of section heading with id (h3)
-// Linking:  [The link text displayed](\ref TheTarget)
+std::string hideCaller;         ///< hide caller graph
+// Linking:  [The link text displayed](@ref TheTarget)
 std::string hrefStart;        ///< start of a link
 std::string hrefMid;          ///< middle part of a link
-std::string hrefStop;         ///< end of a link
+std::string hrefStop{")"};    ///< end of a link
 std::string indentHtmlOnly;   ///< small indent
 std::string listLineStart;    ///< start unordered list item
 std::string listLineStop;     ///< end unordered list item
@@ -82,8 +87,8 @@
 std::string listStop;         ///< end unordered list
 std::string note;             ///< start a note section
 std::string page;             ///< start a separate page
-std::string reference;        ///< reference tag
-std::string referenceNo;      ///< block automatic references
+std::string reference{" "};   ///< reference tag
+std::string referenceNo{" "}; ///< block automatic references
 std::string returns;          ///< the return value
 std::string sectionStart;     ///< start of a section or group
 std::string seeAlso;          ///< Reference to other docs
@@ -192,46 +197,19 @@
     NS_LOG_FUNCTION(outputText);
     if (outputText)
     {
-        anchor = "";
         argument = "  Arg: ";
-        boldStart = "";
-        boldStop = "";
-        boldWithIdStart = "";
-        boldWithIdMid = "";
-        boldWithIdStop = "";
-        breakBoth = "\n";
-        breakHtmlOnly = "";
         breakTextOnly = "\n";
-        brief = "";
-        classStart = "";
-        classStop = "\n\n";
-        codeWord = " ";
         commentStart = "===============================================================\n";
-        commentStop = "";
+        classStop = "\n\n";
         copyDoc = "  See: ";
         file = "File: introspected-doxygen.txt";
-        flagSpanStart = "";
-        flagSpanStop = "";
-        functionStart = "";
         functionStop = "\n\n";
-        headingStart = "";
-        headingStop = "";
-        headingWithIdStart = "";
-        headingWithIdMid = "";
-        headingWithIdStop = "";
         // Linking:  The link text displayed (see TheTarget)
-        hrefStart = "";
         hrefMid = " (see ";
         hrefStop = ")";
-        indentHtmlOnly = "";
         listLineStart = "    * ";
-        listLineStop = "";
-        listStart = "";
-        listStop = "";
         note = "Note: ";
         page = "Page ";
-        reference = " ";
-        referenceNo = " ";
         returns = "  Returns: ";
         sectionStart = "Section:  ";
         seeAlso = "  See: ";
@@ -246,8 +224,8 @@
     }
     else
     {
-        anchor = "\\anchor ";
-        argument = "\\param ";
+        anchor = "@anchor ";
+        argument = "@param ";
         boldStart = "<b>";
         boldStop = "</b>";
         boldWithIdStart = "<b id=\"";
@@ -255,48 +233,46 @@
         boldWithIdStop = "</b>";
         breakBoth = "<br>";
         breakHtmlOnly = "<br>";
-        breakTextOnly = "";
-        brief = "\\brief ";
-        classStart = "\\class ";
-        classStop = "";
-        codeWord = "\\p ";
-        commentStart = "/*!\n";
+        brief = "@brief ";
+        classStart = "@class ";
+        codeWord = "@p ";
+        commentStart = "/**\n";
         commentStop = "*/\n";
-        copyDoc = "\\copydoc ";
-        file = "\\file";
+        copyDoc = "@copydoc ";
+        file = "@file";
         flagSpanStart = "<span class=\"mlabel\">";
         flagSpanStop = "</span>";
-        functionStart = "\\fn ";
-        functionStop = "";
+        functionStart = "@fn ";
         headingStart = "<h3>";
         headingStop = "</h3>";
         headingWithIdStart = "<h3 id=\"";
         headingWithIdMid = "\">";
         headingWithIdStop = "</h3>";
+        hideCaller = "@hidecaller";
         // Linking:  [The link text displayed](\ref TheTarget)
         hrefStart = "[";
-        hrefMid = "](\\ref ";
+        hrefMid = "](@ref ";
         hrefStop = ")";
         indentHtmlOnly = "  ";
         listLineStart = "<li>";
         listLineStop = "</li>";
         listStart = "<ul>";
         listStop = "</ul>";
-        note = "\\note ";
-        page = "\\page ";
-        reference = " \\ref ";
+        note = "@note ";
+        page = "@page ";
+        reference = " @ref ";
         referenceNo = " %";
-        returns = "\\returns ";
-        sectionStart = "\\ingroup ";
-        seeAlso = "\\see ";
+        returns = "@returns ";
+        sectionStart = "@ingroup ";
+        seeAlso = "@see ";
         spanWithIdStart = "<span id=\"";
         spanWithIdMid = "\">";
         spanWithIdStop = "</span>";
-        subSectionStart = "\\addtogroup ";
-        templArgDeduced = "\\deduced ";
-        templArgExplicit = "\\explicit ";
-        templateArgument = "\\tparam ";
-        variable = "\\var ";
+        subSectionStart = "@addtogroup ";
+        templArgDeduced = "@deduced ";
+        templArgExplicit = "@explicit ";
+        templateArgument = "@tparam ";
+        variable = "@var ";
     }
 } // SetMarkup()
 
@@ -476,14 +452,14 @@
  *
  * The container is modified in place; the elements end up sorted.
  *
- * The container must support \c begin(), \c end() and \c erase(),
+ * The container must support @c begin(), @c end() and @c erase(),
  * which, among the STL containers, limits this to
- * \c std::vector, \c std::dequeue and \c std::list.
+ * @c std::vector, @c std::dequeue and @c std::list.
  *
- * The container elements must support \c operator< (for \c std::sort)
- * and \c operator== (for \c std::unique).
+ * The container elements must support @c operator< (for @c std::sort)
+ * and @c operator== (for @c std::unique).
  *
- * @tparam T \deduced The container type.
+ * @tparam T @deduced The container type.
  * @param t The container.
  */
 template <typename T>
@@ -797,15 +773,13 @@
     }
     else
     {
-        os << HeadingWithId("config-paths", "Config Paths") << std::endl;
-        os << std::endl;
-        os << tid.GetName() << " is accessible through the following paths"
-           << " with Config::Set and Config::Connect:" << std::endl;
-        os << listStart << std::endl;
+        os << HeadingWithId("config-paths", "Config Paths") << "\n\n"
+           << tid.GetName() << " is accessible through the following paths"
+           << " with Config::Set and Config::Connect:\n"
+           << listStart << "\n";
         for (const auto& path : paths)
         {
-            os << listLineStart << "\"" << path << "\"" << listLineStop << breakTextOnly
-               << std::endl;
+            os << listLineStart << "\"" << path << "\"" << listLineStop << breakTextOnly << "\n";
         }
         os << listStop << std::endl;
     }
@@ -826,13 +800,13 @@
 
     auto index = SortedAttributeInfo(tid);
 
-    os << listStart << std::endl;
+    os << listStart << "\n";
     for (const auto& [name, info] : index)
     {
-        os << listLineStart << boldStart << name << boldStop << ": " << info.help << std::endl;
-        os << indentHtmlOnly << listStart << std::endl;
-        os << "    " << listLineStart << "Set with class: " << reference
-           << info.checker->GetValueTypeName() << listLineStop << std::endl;
+        os << listLineStart << boldStart << name << boldStop << ": " << info.help << "\n"
+           << indentHtmlOnly << listStart << "\n"
+           << "    " << listLineStart << "Set with class: " << reference
+           << info.checker->GetValueTypeName() << listLineStop << "\n";
 
         std::string underType;
         if (info.checker->HasUnderlyingTypeInformation())
@@ -885,7 +859,7 @@
             {
                 os << codeWord << underType;
             }
-            os << listLineStop << std::endl;
+            os << listLineStop << "\n";
         }
         if (info.flags & TypeId::ATTR_CONSTRUCT && info.accessor->HasSetter())
         {
@@ -894,8 +868,7 @@
             {
                 value = "\"\"";
             }
-            os << "    " << listLineStart << "Initial value: " << value << listLineStop
-               << std::endl;
+            os << "    " << listLineStart << "Initial value: " << value << listLineStop << "\n";
         }
         bool moreFlags{false};
         os << "    " << listLineStart << "Flags: ";
@@ -913,11 +886,11 @@
         flagWrite(TypeId::ATTR_CONSTRUCT, info.accessor->HasSetter(), "construct");
         flagWrite(TypeId::ATTR_SET, info.accessor->HasSetter(), "write");
         flagWrite(TypeId::ATTR_GET, info.accessor->HasGetter(), "read");
-        os << listLineStop << std::endl;
+        os << listLineStop << "\n";
 
         PrintSupportLevel(os, info.supportLevel, info.supportMsg);
 
-        os << indentHtmlOnly << listStop << std::endl;
+        os << indentHtmlOnly << listStop << "\n";
     }
     os << listStop << std::endl;
 } // PrintAttributesTid()
@@ -976,22 +949,21 @@
 
     auto index = SortedTraceSourceInfo(tid);
 
-    os << listStart << std::endl;
+    os << listStart << "\n";
     for (const auto& [name, info] : index)
     {
-        os << listLineStart << boldStart << name << boldStop << ": " << info.help << breakBoth;
-        os << indentHtmlOnly << listStart << std::endl;
-        os << "    " << listLineStart;
+        os << listLineStart << boldStart << name << boldStop << ": " << info.help << breakBoth
+           << indentHtmlOnly << listStart << "\n"
+           << "    " << listLineStart;
         if (!outputText)
         {
             // '%' prevents doxygen from linking to the Callback class...
             os << " %";
         }
-        os << "Callback signature: " << info.callback << std::endl;
-        os << listLineStop << std::endl;
+        os << "Callback signature: " << info.callback << "\n" << listLineStop << "\n";
 
         PrintSupportLevel(os, info.supportLevel, info.supportMsg);
-        os << listStop << std::endl;
+        os << listStop << "\n";
     }
     os << listStop << std::endl;
 } // PrintTraceSourcesTid()
@@ -1067,10 +1039,7 @@
 
     std::string name = tid.GetName();
 
-    os << commentStart << std::endl;
-
-    os << classStart << name << std::endl;
-    os << std::endl;
+    os << commentStart << "\n" << classStart << name << "\n\n";
 
     PrintConfigPaths(os, tid);
     PrintAttributes(os, tid);
@@ -1078,11 +1047,11 @@
 
     if (tid.GetGroupName().empty())
     {
-        os << SpanWithId("group", name + " does not belong to a group") << breakBoth << std::endl;
+        os << SpanWithId("group", name + " does not belong to a group") << breakBoth << "\n";
     }
     else
     {
-        os << BoldWithId("group", "Group:") << tid.GetGroupName() << breakBoth << std::endl;
+        os << BoldWithId("group", "Group:") << tid.GetGroupName() << breakBoth << "\n";
     }
 
     PrintSize(os, tid);
@@ -1131,14 +1100,13 @@
 PrintAllTypeIds(std::ostream& os)
 {
     NS_LOG_FUNCTION_NOARGS();
-    os << commentStart << page << "TypeIdList All ns3::TypeId's\n" << std::endl;
-    os << "This is a list of all" << reference << "ns3::TypeId's.\n"
+    os << commentStart << page << "TypeIdList All ns3::TypeId's\n\n"
+       << "This is a list of all" << reference << "ns3::TypeId's.\n"
        << "For more information see the" << reference << "ns3::TypeId "
        << "section of this API documentation and the" << referenceNo << "TypeId section "
-       << "in the Configuration and " << referenceNo << "Attributes chapter of the Manual.\n"
-       << std::endl;
+       << "in the Configuration and " << referenceNo << "Attributes chapter of the Manual.\n\n";
 
-    os << listStart << std::endl;
+    os << listStart << "\n";
 
     NameMap nameMap = GetNameMap();
     // Iterate over the map, which will print the class names in
@@ -1154,10 +1122,9 @@
         TypeId tid = TypeId::GetRegistered(item.second);
 
         os << indentHtmlOnly << listLineStart << boldStart << tid.GetName() << boldStop
-           << listLineStop << std::endl;
+           << listLineStop << "\n";
     }
-    os << listStop << std::endl;
-    os << commentStop << std::endl;
+    os << listStop << "\n" << commentStop << std::endl;
 
 } // PrintAllTypeIds()
 
@@ -1173,12 +1140,11 @@
 PrintAllAttributes(std::ostream& os)
 {
     NS_LOG_FUNCTION_NOARGS();
-    os << commentStart << page << "AttributeList All Attributes\n" << std::endl;
-    os << "This is a list of all" << reference << "attributes classes.  "
+    os << commentStart << page << "AttributeList All Attributes\n\n"
+       << "This is a list of all" << reference << "attributes classes.  "
        << "For more information see the" << reference << "attributes "
        << "section of this API documentation and the Attributes sections "
-       << "in the Tutorial and Manual.\n"
-       << std::endl;
+       << "in the Tutorial and Manual.\n\n";
 
     NameMap nameMap = GetNameMap();
     // Iterate over the map, which will print the class names in
@@ -1200,14 +1166,13 @@
 
         auto index = SortedAttributeInfo(tid);
 
-        os << boldStart << tid.GetName() << boldStop << breakHtmlOnly << std::endl;
-        os << listStart << std::endl;
+        os << boldStart << tid.GetName() << boldStop << breakHtmlOnly << "\n" << listStart << "\n";
         for (const auto& [name, info] : index)
         {
             os << listLineStart << boldStart << name << boldStop << ": " << info.help
-               << listLineStop << std::endl;
+               << listLineStop << "\n";
         }
-        os << listStop << std::endl;
+        os << listStop << "\n";
     }
     os << commentStop << std::endl;
 
@@ -1222,22 +1187,20 @@
 PrintAllGlobals(std::ostream& os)
 {
     NS_LOG_FUNCTION_NOARGS();
-    os << commentStart << page << "GlobalValueList All GlobalValues\n" << std::endl;
-    os << "This is a list of all" << reference << "ns3::GlobalValue instances.\n"
-       << "See ns3::GlobalValue for how to set these." << std::endl;
+    os << commentStart << page << "GlobalValueList All GlobalValues\n\n"
+       << "This is a list of all" << reference << "ns3::GlobalValue instances.\n"
+       << "See ns3::GlobalValue for how to set these.\n";
 
-    os << listStart << std::endl;
+    os << listStart << "\n";
     for (auto i = GlobalValue::Begin(); i != GlobalValue::End(); ++i)
     {
         StringValue val;
         (*i)->GetValue(val);
         os << indentHtmlOnly << listLineStart << boldStart << hrefStart << (*i)->GetName()
            << hrefMid << "GlobalValue" << (*i)->GetName() << hrefStop << boldStop << ": "
-           << (*i)->GetHelp() << ".  Default value: " << val.Get() << "." << listLineStop
-           << std::endl;
+           << (*i)->GetHelp() << ".  Default value: " << val.Get() << "." << listLineStop << "\n";
     }
-    os << listStop << std::endl;
-    os << commentStop << std::endl;
+    os << listStop << "\n" << commentStop << std::endl;
 
 } // PrintAllGlobals()
 
@@ -1250,25 +1213,24 @@
 PrintAllGroups(std::ostream& os)
 {
     NS_LOG_FUNCTION_NOARGS();
-    os << commentStart << page << "GroupsList All Object Groups\n" << std::endl;
-    os << "This is a list of all Object Groups.\n"
+    os << commentStart << page << "GroupsList All Object Groups\n\n"
+       << "This is a list of all Object Groups.\n"
        << "Objects are added to groups by " << hrefStart << "ns3::TypeId::SetGroupName()" << hrefMid
-       << "ns3::TypeId::SetGroupName" << hrefStop << "\n"
-       << std::endl;
+       << "ns3::TypeId::SetGroupName" << hrefStop << "\n\n";
 
     auto groups = GetGroupsList();
 
     for (const auto& g : groups)
     {
-        os << boldStart << g.first << boldStop << breakHtmlOnly << std::endl;
+        os << boldStart << g.first << boldStop << breakHtmlOnly << "\n";
 
-        os << listStart << std::endl;
+        os << listStart << "\n";
         for (const auto& tid : g.second)
         {
             os << indentHtmlOnly << listLineStart << hrefStart << tid.GetName() << hrefMid
-               << tid.GetName() << hrefStop << listLineStop << std::endl;
+               << tid.GetName() << hrefStop << listLineStop << "\n";
         }
-        os << listStop << std::endl;
+        os << listStop << "\n";
     }
     os << commentStop << std::endl;
 }
@@ -1282,8 +1244,8 @@
 PrintAllLogComponents(std::ostream& os)
 {
     NS_LOG_FUNCTION_NOARGS();
-    os << commentStart << page << "LogComponentList All LogComponents\n" << std::endl;
-    os << "This is a list of all" << reference << "ns3::LogComponent instances.\n" << std::endl;
+    os << commentStart << page << "LogComponentList All LogComponents\n\n"
+       << "This is a list of all" << reference << "ns3::LogComponent instances.\n\n";
 
     /**
      * @todo Switch to a border-less table, so the file links align
@@ -1311,9 +1273,9 @@
 
     // Header line has to be padded to same length as separator line
     os << tLeft << std::setw(widthL) << std::left << "Log Component" << tMid << std::setw(widthR)
-       << std::left << "File" << tRight << std::endl;
-    os << tLeft << ":" << std::string(widthL - 1, '-') << tMid << ":"
-       << std::string(widthR - 1, '-') << tRight << std::endl;
+       << std::left << "File" << tRight << "\n"
+       << tLeft << ":" << std::string(widthL - 1, '-') << tMid << ":"
+       << std::string(widthR - 1, '-') << tRight << "\n";
 
     for (const auto& it : (*logs))
     {
@@ -1326,10 +1288,9 @@
         }
 
         os << tLeft << std::setw(widthL) << std::left << it.first << tMid << std::setw(widthR)
-           << file << tRight << std::endl;
+           << file << tRight << "\n";
     }
-    os << std::right << std::endl;
-    os << commentStop << std::endl;
+    os << std::right << "\n" << commentStop << std::endl;
 } // PrintAllLogComponents()
 
 /**
@@ -1344,12 +1305,11 @@
 PrintAllTraceSources(std::ostream& os)
 {
     NS_LOG_FUNCTION_NOARGS();
-    os << commentStart << page << "TraceSourceList All TraceSources\n" << std::endl;
-    os << "This is a list of all" << reference << "tracing sources.  "
+    os << commentStart << page << "TraceSourceList All TraceSources\n\n"
+       << "This is a list of all" << reference << "tracing sources.  "
        << "For more information see the " << reference << "tracing "
        << "section of this API documentation and the Tracing sections "
-       << "in the Tutorial and Manual.\n"
-       << std::endl;
+       << "in the Tutorial and Manual.\n\n";
 
     NameMap nameMap = GetNameMap();
 
@@ -1372,15 +1332,15 @@
 
         auto index = SortedTraceSourceInfo(tid);
 
-        os << boldStart << tid.GetName() << boldStop << breakHtmlOnly << std::endl;
+        os << boldStart << tid.GetName() << boldStop << breakHtmlOnly << "\n";
 
-        os << listStart << std::endl;
+        os << listStart << "\n";
         for (const auto& [name, info] : index)
         {
             os << listLineStart << boldStart << name << boldStop << ": " << info.help
-               << listLineStop << std::endl;
+               << listLineStop << "\n";
         }
-        os << listStop << std::endl;
+        os << listStop << "\n";
     }
     os << commentStop << std::endl;
 
@@ -1409,8 +1369,8 @@
     NS_LOG_FUNCTION(name);
     std::string section = "attribute_" + name;
 
-    // \ingroup attributes
-    // \defgroup attribute_<name>Value <name> Attribute
+    // @ingroup attributes
+    // @defgroup attribute_<name>Value <name> Attribute
     os << commentStart << sectionStart << "attributes\n"
        << subSectionStart << "attribute_" << name << " " << name << " Attribute\n"
        << "AttributeValue implementation for " << name << "\n";
@@ -1418,14 +1378,14 @@
     {
         os << seeAlso << "ns3::" << name << "\n";
     }
-    os << commentStop;
+    os << hideCaller << "\n" << commentStop;
 
 } // PrintAttributeValueSection()
 
 /**
  * Print the AttributeValue documentation for a class.
  *
- * This will print documentation for the \pname{AttributeValue} class and methods.
+ * This will print documentation for the @pname{AttributeValue} class and methods.
  *
  * @param [in,out] os The output stream.
  * @param [in] name The token to use in defining the accessor name.
@@ -1441,16 +1401,17 @@
     NS_LOG_FUNCTION(name << type << header);
     std::string sectAttr = sectionStart + "attribute_" + name;
 
-    // \ingroup attribute_<name>Value
-    // \class ns3::<name>Value "header"
+    // @ingroup attribute_<name>Value
+    // @class ns3::<name>Value "header"
     std::string valClass = name + "Value";
     std::string qualClass = " ns3::" + valClass;
 
-    os << commentStart << sectAttr << std::endl;
-    os << classStart << qualClass << " \"" << header << "\"" << std::endl;
-    os << "AttributeValue implementation for " << name << "." << std::endl;
-    os << seeAlso << "AttributeValue" << std::endl;
-    os << commentStop;
+    os << commentStart << sectAttr << "\n"
+       << classStart << qualClass << " \"" << header << "\"\n"
+       << "AttributeValue implementation for " << name << ".\n"
+       << seeAlso << "ns3::AttributeValue\n"
+       << hideCaller << "\n"
+       << commentStop;
 
     // Ctor: <name>Value::<name>Value
     os << commentStart << functionStart << qualClass << "::" << valClass;
@@ -1493,7 +1454,7 @@
 /**
  * Print the AttributeValue MakeAccessor documentation for a class.
  *
- * This will print documentation for the \pname{Make<name>Accessor} functions.
+ * This will print documentation for the @pname{Make<name>Accessor} functions.
  *
  * @param [in,out] os The output stream.
  * @param [in] name The token to use in defining the accessor name.
@@ -1505,27 +1466,29 @@
     std::string sectAttr = sectionStart + "attribute_" + name + "\n";
     std::string make = "ns3::Make" + name + "Accessor ";
 
-    // \ingroup attribute_<name>Value
+    // @ingroup attribute_<name>Value
     // Make<name>Accessor(T1 a1)
     os << commentStart << sectAttr << functionStart << "ns3::Ptr<const ns3::AttributeAccessor> "
        << make << "(T1 a1)\n"
        << copyDoc << "ns3::MakeAccessorHelper(T1)\n"
-       << seeAlso << "AttributeAccessor\n"
+       << seeAlso << "ns3::AttributeAccessor\n"
+       << hideCaller << "\n"
        << commentStop;
 
-    // \ingroup attribute_<name>Value
+    // @ingroup attribute_<name>Value
     // Make<name>Accessor(T1 a1)
     os << commentStart << sectAttr << functionStart << "ns3::Ptr<const ns3::AttributeAccessor> "
        << make << "(T1 a1, T2 a2)\n"
        << copyDoc << "ns3::MakeAccessorHelper(T1,T2)\n"
-       << seeAlso << "AttributeAccessor\n"
+       << seeAlso << "ns3::AttributeAccessor\n"
+       << hideCaller << "\n"
        << commentStop;
 } // PrintMakeAccessors()
 
 /**
  * Print the AttributeValue MakeChecker documentation for a class.
  *
- * This will print documentation for the \pname{Make<name>Checker} function.
+ * This will print documentation for the @pname{Make<name>Checker} function.
  *
  * @param [in,out] os The output stream.
  * @param [in] name The token to use in defining the accessor name.
@@ -1538,21 +1501,23 @@
     std::string sectAttr = sectionStart + "attribute_" + name + "\n";
     std::string make = "ns3::Make" + name + "Checker ";
 
-    // \ingroup attribute_<name>Value
+    // @ingroup attribute_<name>Value
     // class <name>Checker
-    os << commentStart << sectAttr << std::endl;
-    os << classStart << " ns3::" << name << "Checker"
-       << " \"" << header << "\"" << std::endl;
-    os << "AttributeChecker implementation for " << name << "Value." << std::endl;
-    os << seeAlso << "AttributeChecker" << std::endl;
-    os << commentStop;
+    os << commentStart << sectAttr << "\n"
+       << classStart << " ns3::" << name << "Checker"
+       << " \"" << header << "\"\n"
+       << "AttributeChecker implementation for " << name << "Value.\n"
+       << seeAlso << "ns3::AttributeChecker\n"
+       << hideCaller << "\n"
+       << commentStop;
 
-    // \ingroup attribute_<name>Value
+    // @ingroup attribute_<name>Value
     // Make<name>Checker()
     os << commentStart << sectAttr << functionStart << "ns3::Ptr<const ns3::AttributeChecker> "
        << make << "()\n"
        << returns << "The AttributeChecker.\n"
-       << seeAlso << "AttributeChecker\n"
+       << seeAlso << "ns3::AttributeChecker\n"
+       << hideCaller << "\n"
        << commentStop;
 } // PrintMakeChecker()
 
@@ -1694,8 +1659,7 @@
         }
         else
         {
-            std::cerr << "Invalid TypeId name: " << typeId << "\n" << std::endl;
-            std::cerr << cmd;
+            std::cerr << "Invalid TypeId name: " << typeId << "\n\n" << cmd << std::endl;
             exit(1);
         }
     }
@@ -1706,10 +1670,10 @@
     NodeContainer c;
     c.Create(1);
 
-    std::cout << std::endl;
-    std::cout << commentStart << file << "\n"
+    std::cout << "\n"
+              << commentStart << file << "\n"
               << sectionStart << "utils\n"
-              << "Doxygen docs generated from the TypeId database.\n"
+              << "Doxygen docs generated from the ns3::TypeId database.\n"
               << note << "This file is automatically generated by " << codeWord
               << "print-introspected-doxygen.cc. Do not edit this file! "
               << "Edit that file instead.\n"
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/python-unit-tests.py ns-3.47/utils/python-unit-tests.py
--- ns-3.46/utils/python-unit-tests.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/python-unit-tests.py	2026-02-16 16:08:49.888231000 -0800
@@ -49,14 +49,12 @@
         ns.Simulator.Destroy()
         self._args_received = None
         self._cb_time = None
-        ns.cppyy.cppdef(
-            """
+        ns.cppyy.cppdef("""
             EventImpl* pythonMakeEvent(void (*f)(std::vector<std::string>), std::vector<std::string> l)
             {
                 return MakeEvent(f, l);
             }
-        """
-        )
+        """)
         event = ns.cppyy.gbl.pythonMakeEvent(callback, sys.argv)
         ns.Simulator.ScheduleNow(event)
         ns.Simulator.Run()
@@ -80,14 +78,12 @@
         ns.Simulator.Destroy()
         self._args_received = None
         self._cb_time = None
-        ns.cppyy.cppdef(
-            """
+        ns.cppyy.cppdef("""
             EventImpl* pythonMakeEvent2(void (*f)(std::vector<std::string>), std::vector<std::string> l)
             {
                 return MakeEvent(f, l);
             }
-        """
-        )
+        """)
         event = ns.cppyy.gbl.pythonMakeEvent2(callback, sys.argv)
         ns.Simulator.Schedule(ns.Seconds(123), event)
         ns.Simulator.Run()
@@ -113,14 +109,12 @@
         self._cb_time = None
         ns.cppyy.cppdef("void null(){ return; }")
         ns.Simulator.Schedule(ns.Seconds(123), ns.cppyy.gbl.null)
-        ns.cppyy.cppdef(
-            """
+        ns.cppyy.cppdef("""
             EventImpl* pythonMakeEvent3(void (*f)(std::vector<std::string>), std::vector<std::string> l)
             {
                 return MakeEvent(f, l);
             }
-        """
-        )
+        """)
         event = ns.cppyy.gbl.pythonMakeEvent3(callback, sys.argv)
         ns.Simulator.ScheduleDestroy(event)
         ns.Simulator.Run()
@@ -148,14 +142,12 @@
         self._args_received = None
         self._cb_time = None
         self._context_received = None
-        ns.cppyy.cppdef(
-            """
+        ns.cppyy.cppdef("""
             EventImpl* pythonMakeEvent4(void (*f)(uint32_t, std::vector<std::string>), uint32_t context, std::vector<std::string> l)
             {
                 return MakeEvent(f, context, l);
             }
-        """
-        )
+        """)
         event = ns.cppyy.gbl.pythonMakeEvent4(callback, 54321, sys.argv)
         ns.Simulator.ScheduleWithContext(54321, ns.Seconds(123), event)
         ns.Simulator.Run()
@@ -207,14 +199,12 @@
         def python_rx_callback(socket) -> None:
             self._received_packet = socket.Recv(maxSize=UINT32_MAX, flags=0)
 
-        ns.cppyy.cppdef(
-            """
+        ns.cppyy.cppdef("""
             Callback<void,ns3::Ptr<ns3::Socket> > make_rx_callback_test_socket(void(*func)(Ptr<Socket>))
             {
                 return MakeCallback(func);
             }
-        """
-        )
+        """)
 
         sink = ns.Socket.CreateSocket(node, ns.TypeId.LookupByName("ns3::UdpSocketFactory"))
         sink.Bind(ns.InetSocketAddress(ns.Ipv4Address.GetAny(), 80).ConvertTo())
@@ -368,8 +358,7 @@
 
         interfaces = address.Assign(devices)
 
-        ns.cppyy.cppdef(
-            """
+        ns.cppyy.cppdef("""
             namespace ns3
             {
                 Callback<void,Ptr<Socket> > make_rx_callback(void(*func)(Ptr<Socket>))
@@ -381,8 +370,7 @@
                     return MakeEvent(f, socket, packet, address);
                 }
             }
-        """
-        )
+        """)
 
         ## EchoServer application class
         class EchoServer(ns.Application):
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/tests/gitlab-ci-build.yml ns-3.47/utils/tests/gitlab-ci-build.yml
--- ns-3.46/utils/tests/gitlab-ci-build.yml	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/tests/gitlab-ci-build.yml	2026-02-16 16:08:49.889231000 -0800
@@ -5,7 +5,7 @@
 # Base platform test job
 .test-platform-job-base:
   stage: test
-  image: ubuntu:rolling
+  image: ubuntu:22.04
   rules:
     - if: $RELEASE == "weekly"
     - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
@@ -14,10 +14,15 @@
   before_script:
     - apt update
     - DEBIAN_FRONTEND=noninteractive apt install -y
-      cmake ccache g++ ninja-build python3 python3-venv pkg-config python3-pip docker.io lldb gdb imagemagick git
+      cmake ccache g++ ninja-build python3 python3-venv pkg-config python3-pip docker.io lldb gdb git
+      libopenmpi-dev openmpi-bin openmpi-common
+      libboost-all-dev
+      doxygen sphinx graphviz dia imagemagick
+      python3-sphinx dia imagemagick texlive dvipng latexmk texlive-extra-utils texlive-latex-extra texlive-font-utils
+      valgrind
     - python3 -m venv ns3env
     - source ./ns3env/bin/activate
-    - python3 -m pip install python_on_whales GitPython cppyy django requests
+    - python3 -m pip install python_on_whales GitPython cppyy django requests cmake-format
   script:
     - mkdir -p $CCACHE_BASEDIR_VALUE
     - export CCACHE_BASEDIR=${PWD}
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/tests/gitlab-ci-code-linting.yml ns-3.47/utils/tests/gitlab-ci-code-linting.yml
--- ns-3.46/utils/tests/gitlab-ci-code-linting.yml	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/tests/gitlab-ci-code-linting.yml	2026-02-16 16:08:49.889231000 -0800
@@ -31,7 +31,7 @@
 #     CLANG_FORMAT_VERSION: 21
 
 # Clang-tidy
-clang-tidy-20:
+clang-tidy-21:
   extends: .code-linting-base
   stage: code-linting
   image: ubuntu:rolling
@@ -43,7 +43,7 @@
     - apt update
     - DEBIAN_FRONTEND=noninteractive apt install -y
       clang cmake
-      clang-tidy clang-tidy-20
+      clang-tidy clang-tidy-21
       libboost-all-dev libeigen3-dev libgtk-3-dev libopenmpi-dev libsqlite3-dev
       gsl-bin libgsl-dev libgsl28
       git ssh
@@ -62,7 +62,10 @@
     # Otherwise, run clang-tidy on changed files only.
     # File paths generated by git diff are relative to the working tree. Therefore, iregex should only contain paths relative to the working tree.
     - |
-      if [[ $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH ]] || (grep -q ".clang-tidy" $FILES_CHANGED) ; then
+      if [[ $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH ]] ||
+         (grep -q ".clang-tidy" $FILES_CHANGED) ||
+         (grep -q "gitlab-ci-code-linting.yml" $FILES_CHANGED) ;
+      then
         FILES_TO_CHECK="";
         echo "Running clang-tidy on all files";
       else
@@ -70,7 +73,7 @@
         echo "Running clang-tidy on the following changed files:";
         echo $FILES_TO_CHECK;
       fi
-    - run-clang-tidy-20 -p cmake-cache/ -quiet $FILES_TO_CHECK
+    - run-clang-tidy-21 -p cmake-cache/ -quiet $FILES_TO_CHECK
       1> $CLANG_TIDY_OUTPUT
       2> /dev/null
 
@@ -107,7 +110,7 @@
 # Check cmake format
 cmake-format:
   extends: .code-linting-base
-  image: python:latest
+  image: python:3.12
   before_script:
     - pip install pyyaml cmake cmake-format ninja
   script:
@@ -123,22 +126,15 @@
       fi
 
 # Check Python format
-.python-format:
+python-format-latest:
   extends: .code-linting-base
+  image: python:latest
   before_script:
     - pip install black isort
   script:
     - black --check .
     - isort --check .
 
-python-format-latest:
-  extends: .python-format
-  image: python:latest
-
-python-format-3.8:
-  extends: .python-format
-  image: python:3.8
-
 # Markdown lint
 markdown-lint:
   extends: .code-linting-base
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/tests/gitlab-ci-doc.yml ns-3.47/utils/tests/gitlab-ci-doc.yml
--- ns-3.46/utils/tests/gitlab-ci-doc.yml	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/tests/gitlab-ci-doc.yml	2026-02-16 16:08:49.889231000 -0800
@@ -1,17 +1,18 @@
 # NS3 CI script for the documentation
 # For the moment we do not install the generated documentation
-#
+
 .doc:
   stage: documentation
   before_script:
     - pacman-key --init
     - pacman -Syu --noconfirm
-      base-devel ninja cmake python ccache gsl gtk3 boost openmpi openssh libxml2-legacy
-      graphviz imagemagick texlive-basic texlive-bin texlive-binextra texlive-latexextra
-      texlive-fontutils texlive-fontsrecommended texlive-mathscience python-sphinx wget
-      doxygen
+      python python-sphinx make libxml2-legacy
+      graphviz imagemagick libx264
+      texlive-basic texlive-bin texlive-binextra texlive-latexextra texlive-fontutils texlive-fontsrecommended texlive-mathscience
     # dia is archived
-    - pacman --noconfirm -U https://archive.archlinux.org/packages/d/dia/dia-0.97.3-9-x86_64.pkg.tar.zst
+    - pacman --noconfirm -U
+      https://archive.archlinux.org/packages/g/gtk2/gtk2-2.24.33-5-x86_64.pkg.tar.zst
+      https://archive.archlinux.org/packages/d/dia/dia-0.97.3-9-x86_64.pkg.tar.zst
     # no policy is a good policy.
     - mv /etc/ImageMagick-7/policy.xml /etc/ImageMagick-7/policy.xml.bak
     # - sed -i "s/EPS,PDF,//g" /etc/ImageMagick-7/policy.xml
@@ -20,22 +21,25 @@
   timeout: 2h
   interruptible: true
   dependencies: []
-  # artifacts:
-  #   paths:
-  #     - public
 
-doxygen:
+doxygen-1.15:
   extends: .doc
   script:
+    # Install Doxygen and dependencies to build ns-3
+    - pacman -Syu --noconfirm
+      base-devel ninja cmake python ccache gsl gtk3 boost openmpi openssh inetutils git
+    - pacman --noconfirm -U https://archive.archlinux.org/packages/d/doxygen/doxygen-1.15.0-2-x86_64.pkg.tar.zst
+
+    # Build ns-3.
+    # Debug build is the fastest, and we do not need to actually execute anything, just the examples for introspection.
     - mkdir -p $CCACHE_BASEDIR_VALUE
     - export CCACHE_BASEDIR=${PWD}
     - export CCACHE_DIR=${PWD}/$CCACHE_BASEDIR_VALUE
-    # debug build is the fastest, and we do not need to actually execute anything, just the examples for introspection.
     - export MPI_CI=1
-    - CXX=$COMPILER ./ns3 configure -d debug -GNinja --enable-examples --enable-tests --enable-asserts --enable-mpi
+    - CXX=g++ ./ns3 configure -d debug -GNinja --enable-examples --enable-tests --enable-asserts --enable-mpi
     - ./ns3 build
     - ./ns3 build assemble-introspected-command-line
-    # - mkdir -p public/doxygen
+
     # The following four lines were needed when ns-3 had warnings, and
     # it was necessary to have a filter to check only the files touched by a MR.
     # - git remote add ns-3-origin https://gitlab.com/nsnam/ns-3-dev.git || true
@@ -43,13 +47,13 @@
     # - export CHANGED_FILES=$(git diff --name-only ns-3-origin/master --diff-filter=d | tr '\n' ' ' | sed 's/ $//g;s/ /\\\|/g')
     # - doc/doxygen.warnings.report.sh -f "$CHANGED_FILES"
     - doc/doxygen.warnings.report.sh
-    # Just need to check doc/introspected-command-line.h and doc/introspected-doxygen.h
-    - python3 utils/check-style-clang-format.py --no-formatting --no-tabs doc/introspected-doxygen.h doc/introspected-command-line.h
+
+    # Clean the build directory
     - ./ns3 clean
-    # print the doxygen version - just to double check what has been used.
-    - doxygen -v
-    # - doxygen doc/doxygen.conf
-    # - mv doc/html/ public/doxygen
+    # Print the Doxygen version - just to double check what has been used.
+    - echo "Doxygen version $(doxygen -v)"
+    # Check the generated documentation files
+    - python3 utils/check-style-clang-format.py --no-formatting --no-tabs doc/introspected-doxygen.h doc/introspected-command-line.h
   cache:
     # Reuse the ccache from the per-commit-gcc-debug job
     key: "ccache-per-commit-gcc-debug"
@@ -58,14 +62,10 @@
     policy: pull
   variables:
     CCACHE_BASEDIR_VALUE: ns-3-ccache-storage
-    COMPILER: g++
   artifacts:
     paths:
       - doc/doxygen.warnings.log
     when: always
-  # tags:
-  #   - nsnam
-  #   - linux
 
 .sphinx-doc:
   extends: .doc
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/tests/gitlab-ci-fedora.yml ns-3.47/utils/tests/gitlab-ci-fedora.yml
--- ns-3.46/utils/tests/gitlab-ci-fedora.yml	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/tests/gitlab-ci-fedora.yml	2026-02-16 16:08:49.889231000 -0800
@@ -44,94 +44,94 @@
   script:
     - echo "Starting Fedora jobs"
 
-# Fedora 40
-weekly-build-fedora-40-debug:
+# Fedora 41
+weekly-build-fedora-41-debug:
   extends: .weekly-build-fedora
   needs: ["weekly-fedora"]
-  image: fedora:40
+  image: fedora:41
   stage: build
   variables:
     MODE: debug
 
-weekly-build-fedora-40-default:
+weekly-build-fedora-41-default:
   extends: .weekly-build-fedora
   needs: ["weekly-fedora"]
-  image: fedora:40
+  image: fedora:41
   stage: build
   variables:
     MODE: default
 
-weekly-build-fedora-40-optimized:
+weekly-build-fedora-41-optimized:
   extends: .weekly-build-fedora
   needs: ["weekly-fedora"]
-  image: fedora:40
+  image: fedora:41
   stage: build
   variables:
     MODE: optimized
 
-# Fedora 40 test
-weekly-test-fedora-40-default:
+# Fedora 41 test
+weekly-test-fedora-41-default:
   extends: .weekly-build-fedora
-  image: fedora:40
+  image: fedora:41
   stage: test
-  needs: ["weekly-build-fedora-40-default"]
+  needs: ["weekly-build-fedora-41-default"]
   dependencies:
-    - weekly-build-fedora-40-default
+    - weekly-build-fedora-41-default
   variables:
     MODE: default
 
-weekly-test-fedora-40-optimized:
+weekly-test-fedora-41-optimized:
   extends: .weekly-build-fedora
-  image: fedora:40
+  image: fedora:41
   stage: test
-  needs: ["weekly-build-fedora-40-optimized"]
+  needs: ["weekly-build-fedora-41-optimized"]
   dependencies:
-    - weekly-build-fedora-40-optimized
+    - weekly-build-fedora-41-optimized
   variables:
     MODE: optimized
 
-# Fedora 41
-weekly-build-fedora-41-debug:
+# Fedora 42
+weekly-build-fedora-42-debug:
   extends: .weekly-build-fedora
   needs: ["weekly-fedora"]
-  image: fedora:41
+  image: fedora:42
   stage: build
   variables:
     MODE: debug
 
-weekly-build-fedora-41-default:
+weekly-build-fedora-42-default:
   extends: .weekly-build-fedora
   needs: ["weekly-fedora"]
-  image: fedora:41
+  image: fedora:42
   stage: build
   variables:
     MODE: default
 
-weekly-build-fedora-41-optimized:
+weekly-build-fedora-42-optimized:
   extends: .weekly-build-fedora
   needs: ["weekly-fedora"]
-  image: fedora:41
+  image: fedora:42
   stage: build
   variables:
     MODE: optimized
 
-# Fedora 41 test
-weekly-test-fedora-41-default:
+# Fedora 42 test
+weekly-test-fedora-42-default:
   extends: .weekly-build-fedora
-  image: fedora:41
+  image: fedora:42
   stage: test
-  needs: ["weekly-build-fedora-41-default"]
+  needs: ["weekly-build-fedora-42-default"]
   dependencies:
-    - weekly-build-fedora-41-default
+    - weekly-build-fedora-42-default
   variables:
     MODE: default
 
-weekly-test-fedora-41-optimized:
+weekly-test-fedora-42-optimized:
   extends: .weekly-build-fedora
-  image: fedora:41
+  image: fedora:42
   stage: test
-  needs: ["weekly-build-fedora-41-optimized"]
+  needs: ["weekly-build-fedora-42-optimized"]
   dependencies:
-    - weekly-build-fedora-41-optimized
+    - weekly-build-fedora-42-optimized
   variables:
     MODE: optimized
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/tests/gitlab-ci-ubuntu.yml ns-3.47/utils/tests/gitlab-ci-ubuntu.yml
--- ns-3.46/utils/tests/gitlab-ci-ubuntu.yml	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/tests/gitlab-ci-ubuntu.yml	2026-02-16 16:08:49.889231000 -0800
@@ -23,7 +23,7 @@
       libboost-all-dev
       libgtk-3-dev
       libfl-dev
-      libxml2 libxml2-dev
+      $LIBXML2 libxml2-dev
       libopenmpi-dev openmpi-bin openmpi-common openmpi-doc
       libsqlite3-dev sqlite3
       libeigen3-dev
@@ -49,6 +49,7 @@
   variables:
     MODE: debug
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-build-ubuntu-22.04-default:
@@ -59,6 +60,7 @@
   variables:
     MODE: default
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-build-ubuntu-22.04-optimized:
@@ -69,6 +71,7 @@
   variables:
     MODE: optimized
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-test-ubuntu-22.04-default:
@@ -81,6 +84,7 @@
   variables:
     MODE: default
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-test-ubuntu-22.04-optimized:
@@ -93,6 +97,7 @@
   variables:
     MODE: optimized
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 # Ubuntu 24.04 (Until April 2029)
@@ -104,6 +109,7 @@
   variables:
     MODE: debug
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-build-ubuntu-24.04-default:
@@ -114,6 +120,7 @@
   variables:
     MODE: default
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-build-ubuntu-24.04-optimized:
@@ -124,6 +131,7 @@
   variables:
     MODE: optimized
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-test-ubuntu-24.04-default:
@@ -136,6 +144,7 @@
   variables:
     MODE: default
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 weekly-test-ubuntu-24.04-optimized:
@@ -148,6 +157,7 @@
   variables:
     MODE: optimized
     LIBGSL: libgsl27
+    LIBXML2: libxml2
     COMPILER: g++
 
 # Ubuntu Rolling (latest released, might be non-LTS)
@@ -159,6 +169,7 @@
   variables:
     MODE: debug
     LIBGSL: libgsl28
+    LIBXML2: libxml2-16
     COMPILER: g++
 
 weekly-build-ubuntu-rolling-default:
@@ -169,6 +180,7 @@
   variables:
     MODE: default
     LIBGSL: libgsl28
+    LIBXML2: libxml2-16
     COMPILER: g++
 
 weekly-build-ubuntu-rolling-optimized:
@@ -179,6 +191,7 @@
   variables:
     MODE: optimized
     LIBGSL: libgsl28
+    LIBXML2: libxml2-16
     COMPILER: g++
 
 weekly-test-ubuntu-rolling-default:
@@ -191,6 +204,7 @@
   variables:
     MODE: default
     LIBGSL: libgsl28
+    LIBXML2: libxml2-16
     COMPILER: g++
 
 weekly-test-ubuntu-rolling-optimized:
@@ -203,4 +217,5 @@
   variables:
     MODE: optimized
     LIBGSL: libgsl28
+    LIBXML2: libxml2-16
     COMPILER: g++
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils/tests/test-ns3.py ns-3.47/utils/tests/test-ns3.py
--- ns-3.46/utils/tests/test-ns3.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils/tests/test-ns3.py	2026-02-16 16:08:49.889231000 -0800
@@ -55,6 +55,7 @@
     @param env: environment variables dictionary
     @param generator: CMake generator
     @return tuple containing (error code, stdout and stderr)
+    @hidecaller
     """
     if "clean" in args:
         possible_leftovers = ["contrib/borked", "contrib/calibre"]
@@ -143,6 +144,8 @@
     @param lib_outdir: path containing libraries
     @return list of built libraries.
     """
+    if not os.path.exists(lib_outdir):
+        lib_outdir += "64"
     libraries = glob.glob(lib_outdir + "/*", recursive=True)
     return list(filter(lambda x: "scratch-nested-subdir-lib" not in x, libraries))
 
@@ -231,8 +234,10 @@
         )
 
         # Redefine the execute command of the container
-        def split_exec(docker_container, cmd):
-            return docker_container._execute(cmd.split(), workdir="/ns-3-dev")
+        def split_exec(docker_container, cmd, workdir="/ns-3-dev"):
+            cmd_split = re.findall(r'(?:".*?"|\S)+', cmd)
+            cmd_split = list(map(lambda x: x.replace('"', ""), cmd_split))
+            return docker_container._execute(cmd_split, workdir=workdir)
 
         self.container._execute = self.container.execute
         self.container.execute = partial(split_exec, self.container)
@@ -1254,15 +1259,13 @@
         # Cause a sigsegv
         sigsegv_example = os.path.join(ns3_path, "scratch", "sigsegv.cc")
         with open(sigsegv_example, "w", encoding="utf-8") as f:
-            f.write(
-                """
+            f.write("""
                     int main (int argc, char *argv[])
                     {
                       char *s = "hello world"; *s = 'H';
                       return 0;
                     }
-                    """
-            )
+                    """)
         return_code, stdout, stderr = run_ns3("run sigsegv")
         if win32:
             self.assertEqual(return_code, 4294967295)  # unsigned -1
@@ -1274,8 +1277,7 @@
         # Cause an abort
         abort_example = os.path.join(ns3_path, "scratch", "abort.cc")
         with open(abort_example, "w", encoding="utf-8") as f:
-            f.write(
-                """
+            f.write("""
                     #include "ns3/core-module.h"
 
                     using namespace ns3;
@@ -1284,8 +1286,7 @@
                       NS_ABORT_IF(true);
                       return 0;
                     }
-                    """
-            )
+                    """)
         return_code, stdout, stderr = run_ns3("run abort")
         if win32:
             self.assertNotEqual(return_code, 0)
@@ -1461,7 +1462,7 @@
         # Get the commands to run sample-simulator in two processes with mpi
         return_code, stdout, stderr = run_ns3(mpi_command)
         self.assertEqual(return_code, 0)
-        self.assertIn("mpiexec -np 2 %s" % sample_simulator_path, stdout)
+        self.assertIn('mpiexec -np 2 "%s"' % sample_simulator_path, stdout)
 
         # Get the commands to run sample-simulator in two processes with mpi, now with the environment variable
         return_code, stdout, stderr = run_ns3(mpi_command)
@@ -1469,25 +1470,26 @@
         if os.getenv("USER", "") == "root":
             if shutil.which("ompi_info"):
                 self.assertIn(
-                    "mpiexec --allow-run-as-root --oversubscribe -np 2 %s" % sample_simulator_path,
+                    'mpiexec --allow-run-as-root --oversubscribe -np 2 "%s"'
+                    % sample_simulator_path,
                     stdout,
                 )
             else:
                 self.assertIn(
-                    "mpiexec --allow-run-as-root -np 2 %s" % sample_simulator_path, stdout
+                    'mpiexec --allow-run-as-root -np 2 "%s"' % sample_simulator_path, stdout
                 )
         else:
-            self.assertIn("mpiexec -np 2 %s" % sample_simulator_path, stdout)
+            self.assertIn('mpiexec -np 2 "%s"' % sample_simulator_path, stdout)
 
         # Now we repeat for the non-mpi command
         return_code, stdout, stderr = run_ns3(non_mpi_command)
         self.assertEqual(return_code, 0)
-        self.assertIn("echo %s" % sample_simulator_path, stdout)
+        self.assertIn('echo "%s"' % sample_simulator_path, stdout)
 
         # Again the non-mpi command, with the MPI_CI environment variable set
         return_code, stdout, stderr = run_ns3(non_mpi_command)
         self.assertEqual(return_code, 0)
-        self.assertIn("echo %s" % sample_simulator_path, stdout)
+        self.assertIn('echo "%s"' % sample_simulator_path, stdout)
 
         return_code, stdout, stderr = run_ns3('configure -G "{generator}" --disable-examples')
         self.assertEqual(return_code, 0)
@@ -1510,16 +1512,13 @@
             f.write("")
         for invalid_or_nonexistent_library in ["", "gsd", "lib", "libfi", "calibre"]:
             with open("contrib/borked/CMakeLists.txt", "w", encoding="utf-8") as f:
-                f.write(
-                    """
+                f.write("""
                         build_lib(
                             LIBNAME borked
                             SOURCE_FILES ${PROJECT_SOURCE_DIR}/build-support/empty.cc
                             LIBRARIES_TO_LINK ${libcore} %s
                         )
-                        """
-                    % invalid_or_nonexistent_library
-                )
+                        """ % invalid_or_nonexistent_library)
 
             return_code, stdout, stderr = run_ns3('configure -G "{generator}" --enable-examples')
             if invalid_or_nonexistent_library in ["", "gsd", "libfi", "calibre"]:
@@ -1558,27 +1557,22 @@
         # - invalid library names (should fail to configure)
         # - valid library names but nonexistent libraries (should not create a target)
         with open("contrib/borked/CMakeLists.txt", "w", encoding="utf-8") as f:
-            f.write(
-                """
+            f.write("""
                     build_lib(
                         LIBNAME borked
                         SOURCE_FILES ${PROJECT_SOURCE_DIR}/build-support/empty.cc
                         LIBRARIES_TO_LINK ${libcore}
                     )
-                    """
-            )
+                    """)
         for invalid_or_nonexistent_library in ["", "gsd", "lib", "libfi", "calibre"]:
             with open("contrib/borked/examples/CMakeLists.txt", "w", encoding="utf-8") as f:
-                f.write(
-                    """
+                f.write("""
                         build_lib_example(
                             NAME borked-example
                             SOURCE_FILES ${PROJECT_SOURCE_DIR}/build-support/empty-main.cc
                             LIBRARIES_TO_LINK ${libborked} %s
                         )
-                        """
-                    % invalid_or_nonexistent_library
-                )
+                        """ % invalid_or_nonexistent_library)
 
             return_code, stdout, stderr = run_ns3('configure -G "{generator}"')
             if invalid_or_nonexistent_library in ["", "gsd", "libfi", "calibre"]:
@@ -1617,15 +1611,13 @@
         with open("contrib/calibre/examples/CMakeLists.txt", "w", encoding="utf-8") as f:
             f.write("")
         with open("contrib/calibre/CMakeLists.txt", "w", encoding="utf-8") as f:
-            f.write(
-                """
+            f.write("""
                 build_lib(
                     LIBNAME calibre
                     SOURCE_FILES ${PROJECT_SOURCE_DIR}/build-support/empty.cc
                     LIBRARIES_TO_LINK ${libcore}
                 )
-                """
-            )
+                """)
 
         return_code, stdout, stderr = run_ns3('configure -G "{generator}"')
 
@@ -2217,7 +2209,7 @@
         relative_path = os.sep.join(["build", "release"])
         for different_out_dir in [absolute_path, relative_path]:
             return_code, stdout, stderr = run_ns3(
-                'configure -G "{generator}" --out=%s' % different_out_dir
+                'configure -G "{generator}" --out="%s"' % different_out_dir
             )
             self.config_ok(return_code, stdout, stderr)
             self.assertIn(
@@ -2285,7 +2277,7 @@
         # Reconfigure setting the installation folder to ns-3-dev/build/install.
         install_prefix = os.sep.join([ns3_path, "build", "install"])
         return_code, stdout, stderr = run_ns3(
-            'configure -G "{generator}" --prefix=%s' % install_prefix
+            'configure -G "{generator}" --prefix="%s"' % install_prefix
         )
         self.config_ok(return_code, stdout, stderr)
 
@@ -2319,8 +2311,7 @@
         # Now create a test CMake project and try to find_package ns-3.
         test_main_file = os.sep.join([install_prefix, "main.cpp"])
         with open(test_main_file, "w", encoding="utf-8") as f:
-            f.write(
-                """
+            f.write("""
             #include <ns3/core-module.h>
             using namespace ns3;
             int main ()
@@ -2330,8 +2321,7 @@
                 Simulator::Destroy ();
                 return 0;
             }
-            """
-            )
+            """)
 
         # We try to use this library without specifying a version,
         # specifying ns3-01 (text version with 'dev' is not supported)
@@ -2344,9 +2334,7 @@
                                   list(APPEND CMAKE_PREFIX_PATH ./{lib}/cmake/ns3)
                                   find_package(ns3 {version} COMPONENTS core)
                                   target_link_libraries(test PRIVATE ns3::core)
-                                  """.format(
-                lib=("lib64" if lib64 else "lib"), version=version
-            )
+                                  """.format(lib=("lib64" if lib64 else "lib"), version=version)
             ns3_import_methods.append(cmake_find_package_import)
 
             # Import ns-3 as pkg-config libraries
@@ -2363,16 +2351,13 @@
 
             # Test the multiple ways of importing ns-3 libraries
             for import_method in ns3_import_methods:
-                test_cmake_project = (
-                    """
+                test_cmake_project = """
                                      cmake_minimum_required(VERSION 3.20..3.20)
                                      project(ns3_consumer CXX)
                                      set(CMAKE_CXX_STANDARD 23)
                                      set(CMAKE_CXX_STANDARD_REQUIRED ON)
                                      add_executable(test main.cpp)
-                                     """
-                    + import_method
-                )
+                                     """ + import_method
 
                 test_cmake_project_file = os.sep.join([install_prefix, "CMakeLists.txt"])
                 with open(test_cmake_project_file, "w", encoding="utf-8") as f:
@@ -2601,7 +2586,9 @@
             self.skipTest("Optional components are not supported on Windows")
 
         # First enable automatic components fetching
-        return_code, stdout, stderr = run_ns3("configure -- -DNS3_FETCH_OPTIONAL_COMPONENTS=ON")
+        return_code, stdout, stderr = run_ns3(
+            "configure --disable-werror -- -DNS3_FETCH_OPTIONAL_COMPONENTS=ON"
+        )
         self.assertEqual(return_code, 0)
 
         # Build the optional components to check if their dependencies were fetched
@@ -3192,6 +3179,47 @@
         if os.path.exists(destination_src):
             shutil.rmtree(destination_src)
 
+    def test_19_EmptySpaceHandlingOnTestAndCommandTemplate(self):
+        """!
+        Test if test.py and command-template handles empty spaces in executable paths correctly
+        @return None
+        """
+        # Clean the ns-3 configuration
+        return_code, stdout, stderr = run_ns3("clean")
+        self.assertEqual(return_code, 0)
+
+        with DockerContainerManager(self, "ubuntu:22.04") as container:
+            # Install toolchain
+            container.execute("apt-get update")
+            container.execute("apt-get install -y python3 cmake g++ ninja-build")
+
+            # Create new copy of ns-3 on a "path with empty spaces"
+            test_path = "/path with empty spaces/ns-3-dev"
+            try:
+                container.execute(f'mkdir -p "{test_path}"')
+                container.execute(f'cp -R ./ "{test_path}"')
+            except DockerException as e:
+                pass
+
+            # Configure enabling examples as tests too, filtering to core and sixlowpan
+            try:
+                container.execute(
+                    './ns3 configure --enable-examples --enable-tests --filter-module-examples-and-tests="core;sixlowpan"',
+                    workdir=test_path,
+                )
+            except DockerException as e:
+                self.fail()
+
+            # Execute tests and examples to see if all work
+            try:
+                container.execute("./test.py", workdir=test_path)
+            except DockerException as e:
+                self.fail()
+
+        # Clean cache to prevent dumb errors
+        return_code, stdout, stderr = run_ns3("clean")
+        self.assertEqual(return_code, 0)
+
 
 class NS3QualityControlTestCase(unittest.TestCase):
     """!
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/utils.py ns-3.47/utils.py
--- ns-3.46/utils.py	2025-10-01 18:46:32.000000000 -0700
+++ ns-3.47/utils.py	2026-02-16 16:08:49.888231000 -0800
@@ -27,7 +27,7 @@
         for line in file_in:
             # Remove any comments.
             if "#" in line:
-                (line, comment) = line.split("#", 1)
+                line, comment = line.split("#", 1)
 
             # Parse the line.
             if list_name in line or parsing_multiline_list:
@@ -63,7 +63,7 @@
         for line in file_in:
             # Remove any comments.
             if "#" in line:
-                (line, comment) = line.split("#", 1)
+                line, comment = line.split("#", 1)
 
             # Parse the line.
             if bool_name in line:
diff -Naur -x '*.dia' -x '*.pcap' -x '*.png' ns-3.46/VERSION ns-3.47/VERSION
--- ns-3.46/VERSION	2025-10-01 18:46:49.000000000 -0700
+++ ns-3.47/VERSION	2026-02-16 16:16:25.803885200 -0800
@@ -1 +1 @@
-3.46
+3.47
