nakaolab oct19

This commit is contained in:
2025-10-19 20:03:22 +09:00
parent 8c0641c676
commit ac4dbff76e
28 changed files with 2327 additions and 307 deletions
+1
View File
@@ -5,3 +5,4 @@ common/utils/T/T_IDs.h
common/utils/T/T_messages.txt.h common/utils/T/T_messages.txt.h
common/utils/T/genids common/utils/T/genids
common/utils/T/genids.o common/utils/T/genids.o
artifacts
@@ -153,12 +153,12 @@ gNBs =
}; };
////////// AMF parameters: ////////// AMF parameters:
amf_ip_address = ({ ipv4 = "192.168.70.132"; }); amf_ip_address = ({ ipv4 = "192.168.71.132"; });
NETWORK_INTERFACES : NETWORK_INTERFACES :
{ {
GNB_IPV4_ADDRESS_FOR_NG_AMF = "192.168.70.151"; GNB_IPV4_ADDRESS_FOR_NG_AMF = "192.168.71.140";
GNB_IPV4_ADDRESS_FOR_NGU = "192.168.70.151"; GNB_IPV4_ADDRESS_FOR_NGU = "192.168.71.140";
GNB_PORT_FOR_S1U = 2152; # Spec 2152 GNB_PORT_FOR_S1U = 2152; # Spec 2152
}; };
@@ -172,6 +172,7 @@ MACRLCs = (
tr_n_preference = "local_RRC"; tr_n_preference = "local_RRC";
pusch_TargetSNRx10 = 200; pusch_TargetSNRx10 = 200;
pucch_TargetSNRx10 = 200; pucch_TargetSNRx10 = 200;
min_grant_prb = 1;
} }
); );
@@ -233,7 +234,7 @@ log_config : {
}; };
e2_agent = { e2_agent = {
near_ric_ip_addr = "192.168.70.150"; near_ric_ip_addr = "192.168.71.21";
#sm_dir = "/path/where/the/SMs/are/located/" #sm_dir = "/path/where/the/SMs/are/located/"
sm_dir = "/usr/local/lib/flexric/" sm_dir = "/usr/local/lib/flexric/"
}; };
@@ -1,5 +1,12 @@
Active_gNBs: Active_gNBs:
- gnb-rfsim - gnb-rfsim
e2_agent:
enabled: "yes"
near_ric_ip_addr: 192.168.71.21
near_ric_port: 36421
sm_dir: "/usr/local/lib/flexric/"
# Asn1_verbosity, choice in: none, info, annoying # Asn1_verbosity, choice in: none, info, annoying
Asn1_verbosity: none Asn1_verbosity: none
gNBs: gNBs:
@@ -142,13 +149,13 @@ gNBs:
GNB_IPV4_ADDRESS_FOR_NGU: 192.168.71.140 GNB_IPV4_ADDRESS_FOR_NGU: 192.168.71.140
GNB_PORT_FOR_S1U: 2152 # Spec 2152 GNB_PORT_FOR_S1U: 2152 # Spec 2152
MACRLCs: MACRLCs:
- num_cc: 1 - num_cc: 1
tr_s_preference: local_L1 tr_s_preference: local_L1
tr_n_preference: local_RRC tr_n_preference: local_RRC
pusch_TargetSNRx10: 200 pusch_TargetSNRx10: 200
pucch_TargetSNRx10: 200 pucch_TargetSNRx10: 200
min_grant_prb: 10
L1s: L1s:
- num_cc: 1 - num_cc: 1
@@ -195,6 +202,6 @@ log_config:
mac_log_level: info mac_log_level: info
rlc_log_level: info rlc_log_level: info
pdcp_log_level: info pdcp_log_level: info
rrc_log_level: info rrc_log_level: debug
ngap_log_level: debug ngap_log_level: debug
f1ap_log_level: debug f1ap_log_level: debug
+1 -2
View File
@@ -1,5 +1,4 @@
uicc0 = { uicc0 = { # imsi is omitted
imsi = "208990100001100";
key = "fec86ba6eb707ed08905757b1bb44b8f"; key = "fec86ba6eb707ed08905757b1bb44b8f";
opc= "C42449363BBAD02B66D16BC975D77CC1"; opc= "C42449363BBAD02B66D16BC975D77CC1";
dnn= "oai"; dnn= "oai";
+2 -1
View File
@@ -4,6 +4,7 @@ uicc0:
opc: C42449363BBAD02B66D16BC975D77CC1 opc: C42449363BBAD02B66D16BC975D77CC1
dnn: oai dnn: oai
nssai_sst: 1 nssai_sst: 1
pdu_type: IPV4
thread-pool: "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1" thread-pool: "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"
@@ -24,7 +25,7 @@ channelmod:
- model_name: rfsimu_channel_ue0 - model_name: rfsimu_channel_ue0
type: AWGN type: AWGN
ploss_dB: 20 ploss_dB: 20
nose_power_dB: -2 noise_power_dB: -2
forgetfact: 0 forgetfact: 0
offset: 0 offset: 0
ds_tdl: 0 ds_tdl: 0
@@ -0,0 +1,9 @@
[NEAR-RIC]
NEAR_RIC_IP = 192.168.71.21
NEAR_RIC_PORT = 36422
[XAPP]
DB_DIR = /tmp/
[LOG]
LEVEL = debug
@@ -1,36 +1,3 @@
################################################################################
# Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The OpenAirInterface Software Alliance licenses this file to You under
# the OAI Public License, Version 1.1 (the "License"); you may not use this file
# except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.openairinterface.org/?page_id=698
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#-------------------------------------------------------------------------------
# For more information about the OpenAirInterface (OAI) Software Alliance:
# contact@openairinterface.org
################################################################################
# OAI CN Configuration File
### This file can be used by all OAI NFs
### Some fields are specific to an NF and will be ignored by other NFs
## NOTE ON YAML ANCHORS ##
# We use YAML anchors to ease the configuration and to avoid duplicating parts of the configuration.
# This is especially true for the SNSSAIs, as we have to define them for multiple NFs.
# Please note that the use of anchors is not mandatory, and you can define the SNSSAI in each NF yourself.
# You can read more about anchors here: https://yaml.org/spec/1.2.2/#anchors-and-aliases
############# Common configuration
# Log level for all the NFs # Log level for all the NFs
log_level: log_level:
general: debug general: debug
@@ -103,8 +70,8 @@ amf:
# This really depends on if we want to keep the "mini" version or not # This really depends on if we want to keep the "mini" version or not
support_features_options: support_features_options:
enable_simple_scenario: yes # "no" by default with the normal deployment scenarios with AMF/SMF/UPF/AUSF/UDM/UDR/NRF. enable_simple_scenario: yes # "no" by default with the normal deployment scenarios with AMF/SMF/UPF/AUSF/UDM/UDR/NRF.
# set it to "yes" to use with the minimalist deployment scenario (including only AMF/SMF/UPF) by using the internal AUSF/UDM implemented inside AMF. # set it to "yes" to use with the minimalist deployment scenario (including only AMF/SMF/UPF) by using the internal AUSF/UDM implemented inside AMF.
# There's no NRF in this scenario, SMF info is taken from "nfs" section. # There's no NRF in this scenario, SMF info is taken from "nfs" section.
enable_nssf: no enable_nssf: no
enable_smf_selection: no enable_smf_selection: no
relative_capacity: 30 relative_capacity: 30
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
[mysqld]
wait_timeout = 15
interactive_timeout = 15
@@ -1,12 +1,13 @@
services: services:
mysql: mysql:
container_name: "rfsim5g-mysql" container_name: "mysql"
image: mysql:8.0 image: mysql:8.0
volumes: volumes:
- ./oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql - ./conf/oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql
- ./mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh - ./conf/mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh
- ./conf/sql.cnf:/etc/mysql/conf.d/custom.cnf
environment: environment:
- TZ=Europe/Paris - TZ=Asia/Tokyo
- MYSQL_DATABASE=oai_db - MYSQL_DATABASE=oai_db
- MYSQL_USER=test - MYSQL_USER=test
- MYSQL_PASSWORD=test - MYSQL_PASSWORD=test
@@ -19,37 +20,40 @@ services:
networks: networks:
public_net: public_net:
ipv4_address: 192.168.71.131 ipv4_address: 192.168.71.131
oai-amf: oai-amf:
container_name: "rfsim5g-oai-amf" container_name: "oai-amf"
image: oaisoftwarealliance/oai-amf:v2.1.9 image: oaisoftwarealliance/oai-amf:v2.1.9
environment: environment:
- TZ=Europe/paris - TZ=Asia/Tokyo
volumes: volumes:
- ./mini_nonrf_config.yaml:/openair-amf/etc/config.yaml - ./conf/mini_nonrf_config.yaml:/openair-amf/etc/config.yaml
depends_on: depends_on:
- mysql - mysql
networks: networks:
public_net: public_net:
ipv4_address: 192.168.71.132 ipv4_address: 192.168.71.132
oai-smf: oai-smf:
container_name: "rfsim5g-oai-smf" container_name: "oai-smf"
image: oaisoftwarealliance/oai-smf:v2.1.9 image: oaisoftwarealliance/oai-smf:v2.1.9
environment: environment:
- TZ=Europe/Paris - TZ=Asia/Tokyo
volumes: volumes:
- ./mini_nonrf_config.yaml:/openair-smf/etc/config.yaml - ./conf/mini_nonrf_config.yaml:/openair-smf/etc/config.yaml
depends_on: depends_on:
- oai-amf - oai-amf
networks: networks:
public_net: public_net:
ipv4_address: 192.168.71.133 ipv4_address: 192.168.71.133
oai-upf: oai-upf:
container_name: "rfsim5g-oai-upf" container_name: "oai-upf"
image: oaisoftwarealliance/oai-upf:v2.1.9 image: oaisoftwarealliance/oai-upf:v2.1.9
environment: environment:
- TZ=Europe/Paris - TZ=Asia/Tokyo
volumes: volumes:
- ./mini_nonrf_config.yaml:/openair-upf/etc/config.yaml - ./conf/mini_nonrf_config.yaml:/openair-upf/etc/config.yaml
depends_on: depends_on:
- oai-smf - oai-smf
cap_add: cap_add:
@@ -63,9 +67,10 @@ services:
ipv4_address: 192.168.71.134 ipv4_address: 192.168.71.134
traffic_net: traffic_net:
ipv4_address: 192.168.72.134 ipv4_address: 192.168.72.134
oai-ext-dn: oai-ext-dn:
privileged: true privileged: true
container_name: rfsim5g-oai-ext-dn container_name: oai-ext-dn
image: oaisoftwarealliance/trf-gen-cn5g:focal image: oaisoftwarealliance/trf-gen-cn5g:focal
entrypoint: /bin/bash -c \ entrypoint: /bin/bash -c \
"iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\ "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\
@@ -80,16 +85,15 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-gnb: oai-gnb:
image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop} image: nakaolab:oct1
container_name: rfsim5g-oai-gnb container_name: oai-gnb
cap_drop: cap_drop:
- ALL - ALL
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim --log_config.global_log_options level,nocolor,time
ASAN_OPTIONS: detect_leaks=0
depends_on: depends_on:
- oai-ext-dn - oai-ext-dn
command: ["-c", "/oai-ran/cmake_targets/ran_build/build/nr-softmodem -O /opt/oai-gnb/etc/gnb.yaml -E --rfsim --log_config.global_log_options level,nocolor,time"]
networks: networks:
public_net: public_net:
ipv4_address: 192.168.71.140 ipv4_address: 192.168.71.140
@@ -100,21 +104,55 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-flexric:
container_name: rfsim5g-oai-nr-ue image: nakaolab:oct1
container_name: oai-flexric
command: ["-c", "stdbuf -o0 /oai-ran/openair2/E2AP/flexric/build/examples/ric/nearRT-RIC"]
networks:
public_net:
ipv4_address: 192.168.71.21
volumes:
- ./conf/flexric.conf:/oai-ran/openair2/E2AP/flexric/flexric.conf
- ./conf/flexric.conf:/usr/local/etc/flexric/flexric.conf
healthcheck:
test: /bin/bash -c "pgrep nearRT-RIC"
interval: 10s
timeout: 5s
xapp-chatgpt:
image: nakaolab:oct1
command: ["-c", "XAPP_DURATION=20 stdbuf -oL /oai-ran/openair2/E2AP/flexric/build/examples/xApp/c/monitor/xapp_chatgpt"]
container_name: xapp-chatgpt
environment:
- TZ=Asia/Tokyo
depends_on:
- oai-flexric
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.71.26
healthcheck:
test: /bin/bash -c "pgrep -f xapp_chatgpt"
interval: 10s
timeout: 5s
oai-nr-ue1:
image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001100 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue1
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment: # environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001100 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time # USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001100 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
public_net: public_net:
ipv4_address: 192.168.71.150 ipv4_address: 192.168.71.151
devices: devices:
- /dev/net/tun:/dev/net/tun - /dev/net/tun:/dev/net/tun
volumes: volumes:
@@ -124,40 +162,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue2: oai-nr-ue2:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} image: nakaolab:oct1
container_name: rfsim5g-oai-nr-ue2 command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001101 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue2
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001101 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.71.151
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue3:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
container_name: rfsim5g-oai-nr-ue3
cap_drop:
- ALL
cap_add:
- NET_ADMIN # for interface bringup
- NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001102 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -172,16 +186,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue4:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue3:
container_name: rfsim5g-oai-nr-ue4 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001102 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue3
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001103 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -196,16 +210,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue5:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue4:
container_name: rfsim5g-oai-nr-ue5 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001103 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue4
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001104 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -220,16 +234,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue6:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue5:
container_name: rfsim5g-oai-nr-ue6 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001104 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue5
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001105 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -244,16 +258,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue7:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue6:
container_name: rfsim5g-oai-nr-ue7 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001105 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue6
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001106 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -268,16 +282,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue8:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue7:
container_name: rfsim5g-oai-nr-ue8 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001106 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue7
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001107 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -292,16 +306,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue9:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue8:
container_name: rfsim5g-oai-nr-ue9 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001107 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue8
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001108 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -316,16 +330,16 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue10:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} oai-nr-ue9:
container_name: rfsim5g-oai-nr-ue10 image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001108 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue9
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
- NET_ADMIN # for interface bringup - NET_ADMIN # for interface bringup
- NET_RAW # for ping - NET_RAW # for ping
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001109 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
depends_on: depends_on:
- oai-gnb - oai-gnb
networks: networks:
@@ -341,13 +355,60 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
oai-nr-ue10:
image: nakaolab:oct1
command: ["-c", "stdbuf -oL /oai-ran/cmake_targets/ran_build/build/nr-uesoftmodem -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001109 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"]
container_name: oai-nr-ue10
cap_drop:
- ALL
cap_add:
- NET_ADMIN # for interface bringup
- NET_RAW # for ping
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.71.160
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
# iperf:
# image: phlak/iperf
# container_name: iperf
# ports:
# - "5201:5201/tcp"
# - "5201:5201/udp"
# networks:
# traffic_net:
# ipv4_address: 192.168.72.130
# restart: unless-stopped
iperf:
image: mlabbe/iperf
container_name: iperf
command: ["-s", "-p 5201"]
ports:
- "5201:5201/tcp"
- "5201:5201/udp"
networks:
public_net:
ipv4_address: 192.168.71.130
restart: unless-stopped
networks: networks:
public_net: public_net:
driver: bridge driver: bridge
name: rfsim5g-oai-public-net name: rfsim5g-oai-public-net
ipam: ipam:
config: config:
- subnet: 192.168.71.128/26 - subnet: 192.168.71.0/24
driver_opts: driver_opts:
com.docker.network.bridge.name: "rfsim5g-public" com.docker.network.bridge.name: "rfsim5g-public"
traffic_net: traffic_net:
+654
View File
@@ -0,0 +1,654 @@
#!/bin/bash
# Examples:
# ./exp-35.sh
# UE_MIN=2 UE_MAX=12 UE_PEAK_HOUR=14 UE_NOISE_FRAC=0.15 ./exp-35.sh
# TEST_UE=oai-nr-ue1 TEST_PROFILE=browsing TEST_DURATION_SEC=120 ./exp-35.sh
set -euo pipefail
# ---------------------------[ Random Burst Mode ]----------------------------
# UE scaler mode: 'random' (rapid random targets) or 'diurnal' (legacy cosine)
UE_SCALER_MODE="${UE_SCALER_MODE:-random}"
# Interval (sec) for scaler ticks
UE_SCALER_INTERVAL_SEC="${UE_SCALER_INTERVAL_SEC:-30}"
# Rapid/random system-level bursts: every few minutes trigger a short, global burst
# that nudges all UEs to start/extend a session at once. UE-side script watches
# /tmp/system_burst and reacts (see traffic_runner --profile random).
RANDOM_BURST_MODE="${RANDOM_BURST_MODE:-1}" # 1=enabled, 0=disabled
BURST_MIN_GAP_SEC="${BURST_MIN_GAP_SEC:-180}" # 3 minutes
BURST_MAX_GAP_SEC="${BURST_MAX_GAP_SEC:-600}" # 10 minutes
BURST_MARK_FILE="${BURST_MARK_FILE:-/tmp/system_burst}"
# -------------------------------[ Config ]------------------------------------
ARTIFACT_DIR="/home/nakaolab-go/OAI/research-main/artifacts/"
mkdir -p "$ARTIFACT_DIR"
POWER_LOG_FILE="${POWER_LOG_FILE:-$ARTIFACT_DIR/power_usage.csv}"
CPU_PROXY_LOG_FILE="${CPU_PROXY_LOG_FILE:-$ARTIFACT_DIR/cpu_proxy.csv}"
RRC_SINCE_STATE="${RRC_SINCE_STATE:-$ARTIFACT_DIR/.rrc_since}"
# xApp (mac stats) container + path inside container
MAC_STATS_CONTAINER="${MAC_STATS_CONTAINER:-xapp-chatgpt}"
MAC_STATS_FILE_IN_CONTAINER="${MAC_STATS_FILE_IN_CONTAINER:-/tmp/mac_stats.log}"
# Intervals (seconds)
RRC_LOG_INTERVAL_SEC="${RRC_LOG_INTERVAL_SEC:-10}"
POWER_LOG_INTERVAL_SEC="${POWER_LOG_INTERVAL_SEC:-5}"
CPU_PROXY_INTERVAL_SEC="${CPU_PROXY_INTERVAL_SEC:-5}"
UE_SCALER_INTERVAL_SEC="${UE_SCALER_INTERVAL_SEC:-60}"
# Docker compose helper
DC_BIN=(docker compose)
dc() { command "${DC_BIN[@]}" "$@"; }
dc down -v
# Service names (override if your compose uses different ones)
AMF_SERVICE="${AMF_SERVICE:-oai-amf}"
UPF_SERVICE="${UPF_SERVICE:-oai-upf}"
GNB_SERVICE="${GNB_SERVICE:-oai-gnb}"
XAPP_SERVICE="${XAPP_SERVICE:-xapp-chatgpt}"
# UE service name pattern (for discovery)
UE_NAME_REGEX="${UE_NAME_REGEX:-^oai-nr-ue}"
# Test controls
TEST_UE="${TEST_UE:-}" # e.g. oai-nr-ue1
TEST_PROFILES="${TEST_PROFILES:-}" # profile name
TEST_DURATION_SEC="${TEST_DURATION_SEC:-0}" # 0 = run indefinitely
# Diurnal scaling controls (normal mode only; ignored if TEST_UE is set)
UE_MIN="${UE_MIN:-1}" # Minimum number of UEs to keep connected
# UE_MAX="${UE_MAX:-}" # Max UEs; default = total UEs in compose
UE_MAX=10
UE_PEAK_HOUR="${UE_PEAK_HOUR:-17}" # Local hour with peak load (0..23)
UE_NOISE_FRAC="${UE_NOISE_FRAC:-0.15}" # ±fractional noise applied to target
# -------------------------[ Power Model (mW) ]--------------------------------
declare -A PWR_MW_PROFILE=(
[idle]=60 # screen on, modem camped, no transfers
[iot_chatty]=150 # tiny bursts, long sleeps
[browsing]=380 # short transfers with think-time
[voip]=520 # steady uplink/downlink small packets
[video]=850 # continuous DL; 720–1080p
[gaming]=950 # bursty DL + UL keepalives
[bulk_download]=1100 # sustained UL/DL at MCS ceiling
)
# -------------------------------[ Utils ]-------------------------------------
ts_now() { date +%s; }
ts_iso() { date -Is; }
ts_tag() { date +%Y%m%d_%H%M%S; }
log() { echo "[$(ts_iso)] $*"; }
exec_safe() {
local svc="$1"; shift
dc exec -T "$svc" bash -lc "$*"
}
# -----------------------------[ Discovery ]-----------------------------------
list_all_services() { dc config --services; }
get_all_ue_services() { list_all_services | grep -E "${UE_NAME_REGEX}" || true; }
get_core_services() {
# everything except UEs, gNB, xApp
list_all_services | grep -Ev "(${UE_NAME_REGEX}|^${GNB_SERVICE}$|^${XAPP_SERVICE}$)" || true
}
get_running_ues() {
# Running UEs by service name (include TEST_UE name if custom)
if [[ -n "${TEST_UE:-}" ]]; then
dc ps --services --filter "status=running" \
| grep -E "${UE_NAME_REGEX}|^${TEST_UE}$" || true
else
dc ps --services --filter "status=running" \
| grep -E "${UE_NAME_REGEX}" || true
fi
}
# -------------------------[ Readiness helpers ]-------------------------------
svc_cid() { dc ps -q "$1"; } # container id for a service
wait_healthy() {
local svc="$1" timeout="${2:-60}"
local cid; cid="$(svc_cid "$svc")"
if [[ -z "$cid" ]]; then
log "wait_healthy: service '$svc' not running."
return 1
fi
local health
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)"
if [[ -z "$health" ]]; then
log "wait_healthy: no healthcheck for '$svc'; skipping."
return 0
fi
local start; start="$(ts_now)"
while :; do
health="$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo "")"
if [[ "$health" == "healthy" ]]; then
log "$svc is healthy."
return 0
fi
if (( $(ts_now) - start > timeout )); then
log "wait_healthy timeout for '$svc' (last='$health')."
return 1
fi
sleep 1
done
}
wait_log() {
local svc="$1" pat="$2" timeout="${3:-60}"
local cid; cid="$(svc_cid "$svc")"
if [[ -z "$cid" ]]; then
log "wait_log: service '$svc' not running."
return 1
fi
local start; start="$(ts_now)"
while :; do
if dc logs "$svc" --since=2m 2>&1 | grep -Eiq -- "$pat"; then
log "wait_log: detected '$pat' in '$svc'."
return 0
fi
if (( $(ts_now) - start > timeout )); then
log "wait_log timeout for '$svc' waiting '$pat'."
return 1
fi
sleep 1
done
}
get_container_ip() {
local cid; cid="$(svc_cid "$1")"
[[ -z "$cid" ]] && return 1
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$cid"
}
# --------------------------[ Staged bring-up ]--------------------------------
start_services_list() {
[[ $# -eq 0 ]] && return 0
dc up -d "$@"
}
start_ue_list() {
[[ $# -eq 0 ]] && return 0
for svc in "$@"; do
log "Starting: $svc"
dc up -d "$svc"
sleep 10
done
}
start_in_stages() {
# 1) CORE
mapfile -t core_svcs < <(get_core_services)
if [[ ${#core_svcs[@]} -gt 0 ]]; then
start_services_list "${core_svcs[@]}"
else
log "No CORE services to start."
exit 1
fi
# Ensure AMF is ready
wait_healthy "${AMF_SERVICE}" 15
wait_healthy "${UPF_SERVICE}" 15
exec_safe "${UPF_SERVICE}" 'bash -lc "
GW=192.168.72.129
DEV=\$(ip -4 route get \$GW 2>/dev/null | awk '\''/ dev /{for(i=1;i<=NF;i++) if(\$i==\"dev\"){print \$(i+1); exit}}'\''); DEV=\${DEV:-eth0}
ip -4 route replace \$GW/32 dev \$DEV
ip -4 route replace default via \$GW dev \$DEV onlink
"'
wait_healthy oai-flexric 15
# 2) gNB
start_services_list "${GNB_SERVICE}"
wait_log "${GNB_SERVICE}" "Received NGSetupResponse from AMF" 20
wait_log "${GNB_SERVICE}" "Transaction ID E2 SETUP-REQUEST 0 E2 SETUP-RESPONSE 0" 15
# 3) xApp
start_services_list "${XAPP_SERVICE}"
wait_healthy "${XAPP_SERVICE}" 15
# 4) UE(s)
# if [[ -n "${TEST_UE:-}" ]]; then
# # Allow multiple UEs in TEST_UE (comma and/or space separated)
# IFS=', ' read -r -a _ue_list <<< "${TEST_UE}"
# start_services_list "${_ue_list[@]}"
# for ue in "${_ue_list[@]}"; do
# [[ -z "$ue" ]] && continue
# if wait_oai_tun "$ue" 20; then
# exec_safe "$ue" 'ip route replace default dev oaitun_ue1' || true
# run_traffic_profile "$ue" "random" "0" || true
# else
# exit 1
# fi
# done
# else
# # Initial diurnal target, then enforce UE count
# local target; if [[ "${UE_SCALER_MODE}" == "diurnal" ]]; then target="$(calc_target_ue_count)"; else target="$(random_target_ue_count)"; fi
# log "Initial diurnal target UE count: ${target}"
# ensure_ue_count "$target"
# fi
}
# -------------------------[ Diurnal UE scaling ]------------------------------
calc_target_ue_count() {
local total_ues="${UE_MAX:-}"
if [[ -z "$total_ues" ]]; then
total_ues="$(get_all_ue_services | wc -l | tr -d ' ')"
fi
local min="${UE_MIN:-1}"
if (( total_ues < min )); then total_ues="$min"; fi
local H M
H="$(date +%H)"; M="$(date +%M)"
# Compute target via cosine wave (peak at UE_PEAK_HOUR), then add noise and clamp
awk -v min="$min" -v max="$total_ues" -v peak="${UE_PEAK_HOUR:-14}" \
-v noise="${UE_NOISE_FRAC:-0.15}" -v H="$H" -v M="$M" 'BEGIN{
# time-of-day in days [0,1)
t = (H*60.0 + M)/1440.0
ph = peak/24.0
norm = (cos(2*3.141592653589793*(t - ph)) + 1)/2 # 0..1
base = min + norm*(max-min)
srand(); u = (rand()*2.0 - 1.0) * noise # [-noise, +noise]
val = base * (1.0 + u)
if (val < min) val = min
if (val > max) val = max
# round to nearest integer
printf("%.0f\n", val)
}'
}
wait_oaitun() {
local ue="$1" timeout="${2:-20}"
for ((i=1; i<=timeout; i++)); do
if exec_safe "$ue" 'ip link show oaitun_ue1 >/dev/null 2>&1'; then
return 0
fi
sleep 1
done
return 1
}
ensure_ue_count() {
local target="$1"
# Build arrays
mapfile -t all_ues < <(get_all_ue_services)
mapfile -t running < <(get_running_ues)
# Current and desired counts
local cur="${#running[@]}"
if (( cur == target )); then
log "UE scaler: already at target (${target})."
return 0
fi
# Set helpers for membership tests
declare -A IS_RUNNING=()
for u in "${running[@]}"; do IS_RUNNING["$u"]=1; done
# Determine non-running UEs
declare -a not_running=()
for u in "${all_ues[@]}"; do
[[ -z "${IS_RUNNING[$u]:-}" ]] && not_running+=("$u")
done
# Start more UEs if needed
if (( cur < target )); then
local need=$(( target - cur ))
log "UE scaler: need +${need} UEs (cur=${cur}, target=${target})."
# Choose candidates (prefer random if shuf available)
local to_start=()
if command -v shuf >/dev/null 2>&1; then
mapfile -t to_start < <(printf '%s\n' "${not_running[@]}" | shuf -n "$need" || true)
else
to_start=("${not_running[@]:0:$need}")
fi
if (( ${#to_start[@]} > 0 )); then
start_ue_list "${to_start[@]}"
# kick traffic + (best-effort) readiness
for ue in "${to_start[@]}"; do
if wait_oaitun "$ue" 20; then
exec_safe "$ue" 'ip route replace default dev oaitun_ue1'
run_traffic_profile "$ue" "random" "0"
else
dc restart "$ue"
to_start+="$ue"
fi
done
fi
return 0
fi
# Otherwise stop some UEs to shrink
local drop=$(( cur - target ))
log "UE scaler: need -${drop} UEs (cur=${cur}, target=${target})."
local to_stop=()
if command -v shuf >/dev/null 2>&1; then
mapfile -t to_stop < <(printf '%s\n' "${running[@]}" | shuf -n "$drop" || true)
else
to_stop=("${running[@]:0:$drop}")
fi
for ue in "${to_stop[@]}"; do
log "Stopping UE ${ue}"
dc stop "$ue" || true
dc rm -f "$ue" || true
done
}
# --------------------------[ UE traffic helpers ]-----------------------------
pick_profile() {
local r=$((RANDOM % 100))
if (( r < 30 )); then echo browsing # 45%
elif (( r < 70 )); then echo video # 35%
elif (( r < 90 )); then echo bulk_download # 12%
else echo iot_chatty # 8%
fi
}
run_traffic_profile() {
local ue="$1" profile="$2" duration="${3:-0}"
exec_safe "$ue" "echo 'traffic runner ready for $ue. profile: $profile'"
exec_safe "$ue" "bash -lc '
if [ ! -f /tmp/traffic_profile.lock ]; then
prof=\$1; dur=\$2
/usr/local/bin/traffic_runner --profile \$prof --duration \$dur >/tmp/traffic_runner.log 2>&1 &
printf \"%s %s\n\" \"\$prof\" \"\$dur\" > /tmp/traffic_profile.lock
fi
' -- $profile $duration"
}
# --------------------------[ RRC / MAC scraping ]-----------------------------
rotate_mac_capture() {
local tag="${1:-}"
[[ -z "$tag" ]] && tag="$(ts_tag)"
if dc ps --services --filter "status=running" | grep -q "^${MAC_STATS_CONTAINER}$"; then
log "Rotating mac_stats from ${MAC_STATS_CONTAINER}"
dc cp "${MAC_STATS_CONTAINER}:${MAC_STATS_FILE_IN_CONTAINER}" \
"${ARTIFACT_DIR}/mac_stats_${tag}.log" || true
exec_safe "$MAC_STATS_CONTAINER" "> '${MAC_STATS_FILE_IN_CONTAINER}'"
else
log "mac_stats container ${MAC_STATS_CONTAINER} not running; skipping rotate"
fi
}
_since_arg_from_file() {
local f="$1"
local val
val="$(tr -d '[]\r\n' < "$f" 2>/dev/null || true)"
if [[ -n "$val" ]]; then
printf -- '--since=%s' "$val"
else
printf -- '--since=2m'
fi
}
dump_gnb_rrc_logs() {
local tag="$1"
dc logs "${GNB_SERVICE}" 2>&1 | grep -E "NR_RRC" \
> "${ARTIFACT_DIR}/gnb_rrc_${tag}.log" || true
}
# -----------------------------[ Power / CPU ]---------------------------------
init_power_logs() {
if [[ ! -s "$POWER_LOG_FILE" ]]; then
echo "epoch,ue,profile,power_mW,dt_s,energy_mJ" > "$POWER_LOG_FILE"
fi
if [[ ! -s "$CPU_PROXY_LOG_FILE" ]]; then
echo "epoch,container,cpu_percent,mem_usage" > "$CPU_PROXY_LOG_FILE"
fi
}
profile_total_mw() {
local profile="$1"
echo "${PWR_MW_PROFILE[$profile]:-0}"
}
sample_power_once() {
local epoch="$(ts_now)"
local dt_s="${POWER_LOG_INTERVAL_SEC}"
while IFS= read -r ue; do
[[ -z "$ue" ]] && continue
local lock
lock="$(exec_safe "$ue" 'bash -lc "cat /tmp/traffic_profile.lock 2>/dev/null || true"')"
local picked_profile picked_duration
picked_profile="$(awk "{print \$1}" <<<"$lock")"
picked_duration="$(awk "{print \$2}" <<<"$lock")"
if [[ -z "$picked_profile" ]]; then
picked_profile="${TEST_PROFILE:-idle}"
fi
while [[ "$picked_profile" == -* ]]; do
picked_profile="${picked_profile#-}"
done
picked_profile="${picked_profile,,}"
local p_mw
p_mw="$(profile_total_mw "$picked_profile")"
: "${p_mw:=0}"
local energy_mJ=$(( p_mw * dt_s ))
echo "${epoch},${ue},${picked_profile},${p_mw},${dt_s},${energy_mJ}" >> "$POWER_LOG_FILE"
done < <(get_running_ues)
}
# --- CPU proxy: query docker stats by container ID and map back to service name ---
cpu_proxy_once() {
local epoch="$(ts_now)"
# Build list of UE service names (same as container names)
declare -a SVC_LIST=()
while IFS= read -r svc; do
[[ -z "$svc" ]] && continue
SVC_LIST+=("$svc")
done < <(get_running_ues)
# Query docker stats by NAME and key 'seen' by NAME
local fmt='{{.Name}},{{.CPUPerc}},{{.MemUsage}}'
declare -A SEEN=()
while IFS=',' read -r name cpu mem; do
[[ -z "$name" ]] && continue
name="${name#/}"
SEEN["$name"]=1
cpu="${cpu%%%}" # drop trailing '%'
echo "${epoch},${name},${cpu},${mem}" >> "$CPU_PROXY_LOG_FILE"
done < <(docker stats --no-stream --format "$fmt" "${SVC_LIST[@]}" 2>/dev/null || true)
# Any container absent from docker stats output → record zeros once
local svc
for svc in "${SVC_LIST[@]}"; do
if [[ -z "${SEEN[$svc]:-}" ]]; then
echo "${epoch},${svc},0,0" >> "$CPU_PROXY_LOG_FILE"
fi
done
}
rotate_csv_if_any() {
local src="$1" prefix="$2" tag="${3:-}"
[[ -z "$tag" ]] && tag="$(ts_tag)"
if [[ -s "$src" ]]; then
mv "$src" "${ARTIFACT_DIR}/${prefix}_${tag}.csv" || true
case "$prefix" in
power_usage) echo "epoch,ue,profile,power_mW,dt_s,energy_mJ" > "$src" ;;
cpu_proxy) echo "epoch,container,cpu_percent,mem_usage" > "$src" ;;
esac
fi
}
finalize_and_rotate_artifacts() {
local tag; tag="$(ts_tag)"
rotate_mac_capture "$tag" || true
dump_gnb_rrc_logs "$tag" || true
rotate_csv_if_any "$POWER_LOG_FILE" "power_usage" "$tag"
rotate_csv_if_any "$CPU_PROXY_LOG_FILE" "cpu_proxy" "$tag"
}
# -------------------------------[ Signals ]-----------------------------------
__SHUTTING_DOWN=0
on_interrupt() {
if [[ "${__SHUTTING_DOWN}" -eq 1 ]]; then return; fi
__SHUTTING_DOWN=1
log "Signal received (INT/TERM); taking final samples and rotating artifacts…"
# One last sample tick
sample_power_once || true
cpu_proxy_once || true
# Rotate artifacts and clean up
finalize_and_rotate_artifacts
cleanup
# 130 is the conventional exit code for SIGINT
exit 130
}
# --------------------------------[ Cleanup ]----------------------------------
cleanup() {
# Default: stop only UEs; keep infra up for inspection
log "Cleanup: stopping UE containers only"
while IFS= read -r ue; do
[[ -z "$ue" ]] && continue
dc stop "$ue" || true
dc rm -f "$ue" || true
done < <(dc ps --services | grep -E "${UE_NAME_REGEX}" || true)
}
# ---------------------------------[ Main ]------------------------------------
trigger_system_burst() {
mapfile -t running < <(get_running_ues)
if (( ${#running[@]} == 0 )); then
return 0
fi
log "Triggering system-wide burst for ${#running[@]} UE(s)…"
for ue in "${running[@]}"; do
exec_safe "$ue" "bash -lc 'date +%s > ${BURST_MARK_FILE}'" || true
done
}
random_target_ue_count() {
local total_ues="${UE_MAX:-}"
if [[ -z "$total_ues" ]]; then
total_ues="$(get_all_ue_services | wc -l | tr -d ' ')"
fi
local min="${UE_MIN:-1}"
if (( total_ues < min )); then total_ues="$min"; fi
local val
if command -v shuf >/dev/null 2>&1; then
val="$(shuf -i "${min}-${total_ues}" -n 1)"
else
local span=$(( total_ues - min + 1 ))
val=$(( min + (RANDOM % span) ))
fi
echo "$val"
}
main() {
trap on_interrupt INT TERM
init_power_logs
# Staged bring-up with readiness checks for both normal and TEST modes
start_in_stages
# Initialize next system-burst time
if (( RANDOM_BURST_MODE )); then
local next_burst_ts
next_burst_ts="$(($(ts_now) + BURST_MIN_GAP_SEC + RANDOM % (BURST_MAX_GAP_SEC - BURST_MIN_GAP_SEC + 1)))"
fi
# Kick off traffic workload(s)
if [[ -n "${TEST_UE:-}" ]]; then
log "TEST UE(S) ENABLED"
# Support multiple UEs (comma/space/newline separated)
read -r -a _ue_list <<< "$(tr ',\n' ' ' <<< "${TEST_UE}")"
read -r -a _ue_profiles <<< "$(tr ',\n' ' ' <<< "${TEST_PROFILES}")"
start_ue_list "${_ue_list[@]}"
for i in "${!_ue_list[@]}"; do
ue="${_ue_list[$i]}"
prof="${_ue_profiles[$i]}"
[[ -z "$ue" ]] && continue
if wait_oaitun "$ue" 20; then
exec_safe "$ue" "ip route replace default dev oaitun_ue1"
run_traffic_profile "$ue" "$prof"
else
exit 1
fi
done
fi
# Optional finite duration for TEST mode
if [[ -n "${TEST_UE:-}" && "${TEST_DURATION_SEC:-0}" -gt 0 ]]; then
RUN_STOP_TS=$(( $(ts_now) + TEST_DURATION_SEC ))
fi
# Main sampling & scaling loop
local last_rrc="$(( $(ts_now) - RRC_LOG_INTERVAL_SEC ))"
local last_power="$(( $(ts_now) - POWER_LOG_INTERVAL_SEC ))"
local last_cpu="$(( $(ts_now) - CPU_PROXY_INTERVAL_SEC ))"
local last_scale="$(( $(ts_now) - UE_SCALER_INTERVAL_SEC ))"
log "Entering main loop (sampling + diurnal scaler)…"
while :; do
local now; now="$(ts_now)"
# System-wide random burst scheduler
if (( RANDOM_BURST_MODE )); then
if (( now >= next_burst_ts )); then
trigger_system_burst || true
next_burst_ts="$(( now + BURST_MIN_GAP_SEC + RANDOM % (BURST_MAX_GAP_SEC - BURST_MIN_GAP_SEC + 1) ))"
fi
fi
if (( now - last_power >= POWER_LOG_INTERVAL_SEC )); then
sample_power_once || true
last_power="$now"
fi
if (( now - last_cpu >= CPU_PROXY_INTERVAL_SEC )); then
cpu_proxy_once || true
last_cpu="$now"
fi
# Re-evaluate diurnal target (normal mode only)
if [[ -z "${TEST_UE:-}" ]] && (( now - last_scale >= UE_SCALER_INTERVAL_SEC )); then
local target; if [[ "${UE_SCALER_MODE}" == "diurnal" ]]; then target="$(calc_target_ue_count)"; else target="$(random_target_ue_count)"; fi
log "Diurnal scaler tick: target = ${target}"
ensure_ue_count "$target"
last_scale="$now"
fi
# TEST duration stop gate
if [[ -n "${RUN_STOP_TS:-}" && "$now" -ge "${RUN_STOP_TS}" ]]; then
log "TEST run reached duration (${TEST_DURATION_SEC}s); finalizing artifacts…"
finalize_and_rotate_artifacts
cleanup
exit 0
fi
sleep 1
done
}
main "$@"
@@ -1,6 +0,0 @@
services:
oai-nr-ue:
privileged: true
stdin_open: true
tty: true
command: gdb --args /opt/oai-nr-ue/bin/nr-uesoftmodem -O /opt/oai-nr-ue/etc/nr-ue.conf -E --rfsim -r 106 --numerology 1 --uicc0.imsi 208990100001100 -C 3619200000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time
@@ -1,27 +0,0 @@
include:
- ../local_common_overrides/build_images.yaml
- ../local_common_overrides/rebuild_nr_softmodems.yaml
services:
oai-gnb:
privileged: true
depends_on:
rebuild-nr-softmodems:
condition: service_completed_successfully
volumes:
- ../../../cmake_targets/ran_build/build/nr-softmodem:/opt/oai-gnb/bin/nr-softmodem
- ../../../cmake_targets/ran_build/build/librfsimulator.so:/usr/local/lib/librfsimulator.so
oai-nr-ue:
privileged: true
image: ue-local
depends_on:
build-ran-build:
condition: service_completed_successfully
restart: true
oai-gnb:
condition: service_started
rebuild-nr-softmodems:
condition: service_completed_successfully
volumes:
- ../../../cmake_targets/ran_build/build/nr-uesoftmodem:/opt/oai-nr-ue/bin/nr-uesoftmodem
- ../../../cmake_targets/ran_build/build/librfsimulator.so:/usr/local/lib/librfsimulator.so
- ../../../crashdumps/:/cores/
@@ -0,0 +1,44 @@
docker build -f docker/Dockerfile.gnb_e2 -t nakaolab:sep1 .
O-RU and O-DU are connected via the Fronthaul (FH) interface.
FH traffic is logically diviced into 4 planes:
1. Control Plane which transports real-time slot configuration control messages
2. User Plane which transports IQ samples data (amplitude and phase)
3. Synchronization Plane transports timing information messages
4. Management Plane carries non-real-time management operation messages
C/U Plane messages are exchanged using the eCPRI protocol.
The energy consumption of RU can be modeled as
P = P_0 + P_RF + P_BB + P_PAM
where
P_0 : baseline components in the RU (fabric, ethernet, PCIe, etc.)
P_RF : a-to-d/d-to-a converters (ADC/DAC) and the freq up/down converters
P_BB : core digital processor (eFPGA) and the other low-PHY processing units
(e.g. beamforming) for each antenna chain
P_PAM : power amplifier module (PAM) at each antenna chain
The paper claims that components other than PAM are rather static (?)
-> I guess the PAM is controlled by RF. Thus P_RF is the minimum amount of power
required to run RF.
P_PAM = P_(PAM, 0) * P_(PAM, c)(r) * E_PAM * n_ant
where
P_(PAM, 0) : baseline power consumption that depends on the n of antennas,
ganging elements, and the maximum EIRP per antenna
* EIRP (Effective Isotropic Radiated Power) is the total power radiated
by an antenna in the direction of its maximum gain, referenced to an ideal isotropic radiator.
P_(PAM, c)(r) : a factor that depends on _the amount of subcarriers to be amplified_,
which in turn depends on the n of RBs (r) carried therein
E_PAM : captures the efficiency of the amplifier to convert DC power into signal power gain
n_ant : n of radio chains (with ONE amplifier per radio chain)
> This observation motivates the principle behind the solution we propose in this paper:
consolidate data into as few symbol periods as possible, using more radio resources per period instead.
Since keeping PAM awake consumes more energy, it is better to send data in short bursts, where each burst
utilizes more spectrum resources
MIMO Muting
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
UE Count Plotter
----------------
Parses log lines like:
[2025-09-08 21:33:40.789676] UE RNTI=44512 ...
Groups entries into 1-second slots (by wall-clock time) and plots the number of
unique UEs (by RNTI) per slot for the entire file.
Usage:
python ue_count_plot.py /path/to/log.txt [--save output.png] [--no-show]
Notes:
- Uses matplotlib for a single-line plot.
- Does not set any colors or styles.
- For very large logs, this streams line-by-line with O(N) memory in the number
of distinct seconds encountered (each holds a set of RNTIs).
"""
import argparse
import datetime as dt
import re
import sys
from collections import defaultdict, OrderedDict
from typing import Dict, Set, Iterable, Tuple
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
TIMESTAMP_RE = re.compile(r"\[(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)\]")
RNTI_RE = re.compile(r"\bRNTI=(?P<rnti>\d+)\b")
def parse_lines(lines: Iterable[str]) -> Dict[dt.datetime, Set[int]]:
"""
Parse lines and bucket unique RNTIs per second.
Returns:
dict mapping second (datetime truncated to seconds) -> set of RNTIs
"""
buckets: Dict[dt.datetime, Set[int]] = defaultdict(set)
for line in lines:
ts_m = TIMESTAMP_RE.search(line)
rnti_m = RNTI_RE.search(line)
if not ts_m or not rnti_m:
# Skip lines that don't fit the schema
continue
ts_str = ts_m.group("ts")
# Parse timestamp with or without fractional seconds
# Accept both "%Y-%m-%d %H:%M:%S" and with "%f"
if "." in ts_str:
ts = dt.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S.%f")
else:
ts = dt.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
# Truncate to whole-second wall time
ts_minute = ts.replace(second=0, microsecond=0)
rnti = int(rnti_m.group("rnti"))
buckets[ts_minute].add(rnti)
return buckets
def sort_and_fill(buckets: Dict[dt.datetime, Set[int]]) -> Tuple[list, list]:
"""
Sorts buckets by time and fills missing seconds with zero counts.
Returns:
times (list[datetime]), counts (list[int])
"""
if not buckets:
return [], []
# Sort keys
sorted_times = sorted(buckets.keys())
start = sorted_times[0]
end = sorted_times[-1]
all_times = []
counts = []
cur = start
one_sec = dt.timedelta(minutes=1)
while cur <= end:
all_times.append(cur)
counts.append(len(buckets.get(cur, set())))
cur += one_sec
return all_times, counts
def plot_counts(times: list, counts: list, title: str = "Unique UEs per minute"):
"""
Render a simple matplotlib line plot (no specific colors/styles set).
"""
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(times, counts, linestyle="-")
ax.set_title(title)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Unique UEs")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
fig.autofmt_xdate()
plt.tight_layout()
return fig, ax
def main():
parser = argparse.ArgumentParser(description="Plot unique UEs (by RNTI) per minute from a log file.")
parser.add_argument("--save", help="Optional path to save the plot image (e.g., output.png)")
parser.add_argument("--no-show", action="store_true", help="Do not display the plot window")
args = parser.parse_args()
# Read lines
try:
lines = open("artifacts/mac_stats_20250909_231855.log", "r", encoding="utf-8")
except OSError as e:
print(f"Error opening {args.logfile}: {e}", file=sys.stderr)
sys.exit(1)
buckets = parse_lines(lines)
if lines is not sys.stdin:
lines.close()
times, counts = sort_and_fill(buckets)
if not times:
print("No valid entries found to plot.", file=sys.stderr)
sys.exit(2)
fig, _ = plot_counts(times, counts)
if args.save:
try:
fig.savefig(args.save, dpi=150, bbox_inches="tight")
print(f"Saved plot to: {args.save}")
except Exception as e:
print(f"Failed to save figure: {e}", file=sys.stderr)
if not args.no_show:
plt.show()
if __name__ == "__main__":
main()
@@ -0,0 +1,772 @@
from __future__ import annotations
"""
Memory-safe, drop-in refined version of prediction.py
- Streams very large log files (no giant raw DataFrames)
- Computes observed flags without large Python sets
- Zero-copy feature audit (mask features on the fly)
You can run this directly. Key differences from the original are marked with
"STREAMING" and "ZERO-COPY" comments.
"""
import glob, os, re
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import List, Tuple, Dict, Optional
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
# ----------------------------
# Utils
# ----------------------------
def set_seed(seed=42):
import random
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
set_seed(42)
def ensure_dir(p: Path):
p.mkdir(parents=True, exist_ok=True)
return p
def _run_lengths(mask: np.ndarray):
"""Return number of runs and their lengths (consecutive True segments)."""
if mask.sum() == 0:
return 0, np.array([], dtype=int)
m = mask.astype(bool)
dm = np.diff(np.concatenate(([False], m, [False])).astype(np.int8))
starts = np.where(dm == 1)[0]
ends = np.where(dm == -1)[0]
lens = ends - starts
return len(lens), lens
def report_underpredictions(
timestamps: pd.Series,
actual: np.ndarray,
pred: np.ndarray,
out_csv_path
):
"""
Save a per-minute CSV of under-predictions (pred < actual) and
return a dict of summary metrics.
"""
actual = actual.astype("float64")
pred = pred.astype("float64")
finite = np.isfinite(actual) & np.isfinite(pred)
if finite.sum() == 0:
return {
"n_pred_points": 0, "n_under": 0, "under_rate": np.nan,
"mae_under": np.nan, "sum_under": np.nan, "max_under": np.nan,
"p95_under": np.nan, "n_runs": 0, "avg_run": np.nan, "max_run": 0
}
under = finite & (pred < actual)
gap = (actual - pred) # positive when under
rel_gap = np.divide(gap, np.maximum(actual, 1e-9))
# summary metrics
n_pred = int(finite.sum())
n_under = int(under.sum())
under_rate = n_under / n_pred
if n_under > 0:
mae_under = float(gap[under].mean())
sum_under = float(gap[under].sum())
max_under = float(gap[under].max())
p95_under = float(np.percentile(gap[under], 95))
runs, lens = _run_lengths(under)
avg_run = float(lens.mean()) if runs > 0 else np.nan
max_run = int(lens.max()) if runs > 0 else 0
else:
mae_under = sum_under = max_under = p95_under = np.nan
runs = 0; avg_run = np.nan; max_run = 0
# write detailed CSV (only the under-predicted rows)
df = pd.DataFrame({
"timestamp": np.asarray(timestamps)[finite],
"actual": actual[finite],
"pred": pred[finite],
"under": (pred[finite] < actual[finite]),
"gap": gap[finite],
"rel_gap": rel_gap[finite],
})
df_under = df[df["under"]].drop(columns=["under"])
df_under.to_csv(out_csv_path, index=False)
return {
"n_pred_points": n_pred,
"n_under": n_under,
"under_rate": under_rate,
"mae_under": mae_under,
"sum_under": sum_under,
"max_under": max_under,
"p95_under": p95_under,
"n_runs": runs,
"avg_run": avg_run,
"max_run": max_run,
}
# ----------------------------
# Config
# ----------------------------
# NOTE: fixed missing comma in DATE_STRS
DATE_STRS = ["20250828", "20250829"] # train/eval days handled independently
LOG_DIR = "artifacts"
RESULT_DIR = Path("results")
SEQ_LEN = 45 # history length (minutes)
PRED_LEN = 10 # forecast horizon (minutes)
BATCH = 256
EPOCHS_FIRST = 4 # per-target epochs on the first day (you can raise)
EPOCHS_NEXT = 2 # per-target epochs on subsequent days
LR = 1e-3
ADD_TIME_OF_DAY = True # add sin/cos time-of-day features
# === Feature selection ===
ENABLE_FEATURE_SELECTION = True
AUDIT_DAY = DATE_STRS[0]
FS_EPOCHS = max(2, min(3, EPOCHS_FIRST)) # quick audit training
FS_IMPROVE_THRESH = 0.01 # drop if masking improves val loss by >=1%
FS_NOISE_BAND = 0.002 # ignore micro-changes (<0.2%)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Model targets
TARGETS = ["num_ues", "traffic_load"]
# Columns we aim to have per minute (some may be zero if not present)
FOCUS_COLS = [
"num_ues","traffic_load","dl_prbs","ul_prbs","dl_sched_rbs","ul_sched_rbs",
"wb_cqi","dl_mcs1","ul_mcs1","pusch_snr","pucch_snr",
"dl_bler","ul_bler","bsr","phr",
"rrc_total","rrc_unique_rnti","rrc_setup_cpl","rrc_release",
"rrc_recfg","rrc_recfg_cpl","rrc_resume","rrc_paging"
]
# ----------------------------
# Regex / Parsing
# ----------------------------
LOG_PATTERN_FULL = re.compile(
r"""\[
(?P<ts>.*?)\]
\s+UE\s+RNTI=(?P<rnti>\d+)
\s+DL_TBS=(?P<dl_tbs>\d+)
\s+UL_TBS=(?P<ul_tbs>\d+)
\s+DL_SDUs=(?P<dl_sdus>\d+)
\s+UL_SDUs=(?P<ul_sdus>\d+)
\s+DL_sched_RBs=(?P<dl_sched_rbs>\d+)
\s+UL_sched_RBs=(?P<ul_sched_rbs>\d+)
\s+DL_PRBs=(?P<dl_prbs>\d+)
\s+UL_PRBs=(?P<ul_prbs>\d+)
\s+DL_BLER=(?P<dl_bler>[\d.]+)
\s+UL_BLER=(?P<ul_bler>[\d.]+)
\s+PUSCH_SNR=(?P<pusch_snr>[\d.]+)
\s+PUCCH_SNR=(?P<pucch_snr>[\d.]+)
\s+WB_CQI=(?P<wb_cqi>\d+)
\s+DL_MCS1=(?P<dl_mcs1>\d+)
\s+UL_MCS1=(?P<ul_mcs1>\d+)
\s+DL_HARQ=(?P<dl_harq>\d+)
\s+UL_HARQ=(?P<ul_harq>\d+)
\s+BSR=(?P<bsr>\d+)
\s+PHR=(?P<phr>\d+)
""", re.VERBOSE,
)
RRC_PATTERNS: List[re.Pattern] = [
re.compile(r"\[(?P<ts>[^]]+)\].*RNTI=(?P<rnti>[0-9a-fA-F]+).*RRC[_ ]?MSG=(?P<msg>[A-Za-z_]+)", re.IGNORECASE),
re.compile(r"\[(?P<ts>[^]]+)\].*rrc[=:](?P<msg>[A-Za-z_]+).*rnti[=:](?P<rnti>[0-9a-fA-F]+)", re.IGNORECASE),
re.compile(r"\[(?P<ts>[^]]+)\].*(?P<msg>RRC[A-Za-z]+).*RNTI=(?P<rnti>[0-9a-fA-F]+)", re.IGNORECASE),
re.compile(r"\|\s*(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?).*?RNTI\s*(?P<rnti>[0-9a-fA-F]+)", re.IGNORECASE),
]
# ----------------------------
# STREAMING: Per-minute aggregators without raw-frame concatenation
# ----------------------------
C_RNTI_MIN, C_RNTI_MAX = 0x003D, 0xFFFC
@dataclass
class _MACAgg:
dl_sdus_sum: float = 0.0; ul_sdus_sum: float = 0.0
dl_sched_rbs_sum: float = 0.0; ul_sched_rbs_sum: float = 0.0
dl_prbs_sum: float = 0.0; ul_prbs_sum: float = 0.0
dl_bler_sum: float = 0.0; ul_bler_sum: float = 0.0
pusch_snr_sum: float = 0.0; pucch_snr_sum: float = 0.0
wb_cqi_sum: float = 0.0; dl_mcs1_sum: float = 0.0; ul_mcs1_sum: float = 0.0
dl_harq_sum: float = 0.0; ul_harq_sum: float = 0.0
bsr_sum: float = 0.0; phr_sum: float = 0.0
count: int = 0
dl_bytes: int = 0; ul_bytes: int = 0
ues: Optional[set] = None
def _floor_min(ts: datetime) -> pd.Timestamp:
return pd.Timestamp(ts).floor("min")
def stream_mac_minutes(files: List[str]) -> pd.DataFrame:
if not files: return pd.DataFrame()
acc: Dict[pd.Timestamp, _MACAgg] = {}
for path in files:
with open(path, "r", errors="ignore") as f:
for line in f:
m = LOG_PATTERN_FULL.search(line)
if not m: continue
d = m.groupdict()
try:
ts = datetime.fromisoformat(d["ts"]) # may raise
except Exception:
continue
t = _floor_min(ts)
A = acc.get(t)
if A is None:
A = _MACAgg(ues=set())
acc[t] = A
# unique UE when real RNTI with any traffic
try:
rnti_val = int(d["rnti"]) # MAC logs use decimal here
except Exception:
rnti_val = -1
dl_tbs = int(d["dl_tbs"]); ul_tbs = int(d["ul_tbs"])
if (C_RNTI_MIN <= rnti_val <= C_RNTI_MAX) and (dl_tbs > 0 or ul_tbs > 0):
A.ues.add(rnti_val)
# sums & bytes
A.dl_bytes += dl_tbs; A.ul_bytes += ul_tbs
A.dl_sdus_sum += int(d["dl_sdus"]); A.ul_sdus_sum += int(d["ul_sdus"])
A.dl_sched_rbs_sum += int(d["dl_sched_rbs"]); A.ul_sched_rbs_sum += int(d["ul_sched_rbs"])
A.dl_prbs_sum += int(d["dl_prbs"]); A.ul_prbs_sum += int(d["ul_prbs"])
A.dl_bler_sum += float(d["dl_bler"]); A.ul_bler_sum += float(d["ul_bler"])
A.pusch_snr_sum += float(d["pusch_snr"]); A.pucch_snr_sum += float(d["pucch_snr"])
A.wb_cqi_sum += int(d["wb_cqi"]); A.dl_mcs1_sum += int(d["dl_mcs1"]); A.ul_mcs1_sum += int(d["ul_mcs1"])
A.dl_harq_sum += int(d["dl_harq"]); A.ul_harq_sum += int(d["ul_harq"])
A.bsr_sum += int(d["bsr"]); A.phr_sum += int(d["phr"])
A.count += 1
if not acc: return pd.DataFrame()
rows = []
for t in sorted(acc.keys()):
A = acc[t]
c = max(1, A.count)
row = {
"timestamp": t,
"num_ues": len(A.ues) if A.ues is not None else 0,
"dl_bytes": A.dl_bytes,
"ul_bytes": A.ul_bytes,
"dl_sdus": A.dl_sdus_sum / c,
"ul_sdus": A.ul_sdus_sum / c,
"dl_sched_rbs": A.dl_sched_rbs_sum / c,
"ul_sched_rbs": A.ul_sched_rbs_sum / c,
"dl_prbs": A.dl_prbs_sum / c,
"ul_prbs": A.ul_prbs_sum / c,
"dl_bler": A.dl_bler_sum / c,
"ul_bler": A.ul_bler_sum / c,
"pusch_snr": A.pusch_snr_sum / c,
"pucch_snr": A.pucch_snr_sum / c,
"wb_cqi": A.wb_cqi_sum / c,
"dl_mcs1": A.dl_mcs1_sum / c,
"ul_mcs1": A.ul_mcs1_sum / c,
"dl_harq": A.dl_harq_sum / c,
"ul_harq": A.ul_harq_sum / c,
"bsr": A.bsr_sum / c,
"phr": A.phr_sum / c,
"traffic_load": A.dl_bytes + A.ul_bytes,
"mac_present": True,
}
rows.append(row)
out = pd.DataFrame(rows)
out.sort_values("timestamp", inplace=True)
return out
RRC_FAM_COLS = ["rrc_conn_req","rrc_setup_cpl","rrc_recfg","rrc_recfg_cpl","rrc_reestab",
"rrc_release","rrc_resume","rrc_suspend","rrc_paging","rrc_other"]
def canonical_rrc_family(msg: str | None) -> str:
if not msg: return "rrc_other"
m = msg.lower()
if "connectionrequest" in m or "connrequest" in m or "setupreq" in m: return "rrc_conn_req"
if "connectionsetupcomplete" in m or "setupcomplete" in m: return "rrc_setup_cpl"
if "reconfigurationcomplete" in m or "reconfigcomplete" in m: return "rrc_recfg_cpl"
if "reconfiguration" in m or "reconfig" in m: return "rrc_recfg"
if "reestablish" in m: return "rrc_reestab"
if "release" in m: return "rrc_release"
if "pag" in m: return "rrc_paging"
if "resume" in m: return "rrc_resume"
if "suspend" in m: return "rrc_suspend"
return "rrc_other"
def stream_rrc_minutes(files: List[str]) -> pd.DataFrame:
if not files: return pd.DataFrame()
total = defaultdict(int)
unique = defaultdict(set)
fam = {c: defaultdict(int) for c in RRC_FAM_COLS}
for path in files:
with open(path, "r", errors="ignore") as f:
for line in f:
ts = rnti = msg = None
for pat in RRC_PATTERNS:
m = pat.search(line)
if m:
gd = m.groupdict(); ts = gd.get("ts"); rnti = gd.get("rnti"); msg = gd.get("msg"); break
if not ts: continue
ts = ts.rstrip("Z")
try:
t = _floor_min(datetime.fromisoformat(ts))
except ValueError:
continue
total[t] += 1
fam_key = canonical_rrc_family(msg)
fam[fam_key][t] += 1
# unique per minute if we can parse rnti
rid = None
if rnti is not None:
try:
rid = int(rnti, 16) if any(c.isalpha() for c in rnti) else int(rnti, 10)
except ValueError:
pass
if rid is not None:
unique[t].add(rid)
if not total: return pd.DataFrame()
minutes = sorted(total.keys())
data = {
"timestamp": minutes,
"rrc_total": [total[t] for t in minutes],
"rrc_unique_rnti": [len(unique[t]) if t in unique else 0 for t in minutes],
"rrc_present": [True] * len(minutes),
}
for c in RRC_FAM_COLS:
data[c] = [fam[c][t] if t in fam[c] else 0 for t in minutes]
out = pd.DataFrame(data)
return out
# ----------------------------
# Per-minute features (compat helpers kept for reference)
# ----------------------------
def full_minute_index(start, end):
return pd.date_range(start.floor("min"), end.ceil("min"), freq="1min")
def add_time_of_day_feats(df: pd.DataFrame) -> pd.DataFrame:
if not ADD_TIME_OF_DAY: return df
out = df.copy()
m = out["timestamp"].dt.hour * 60 + out["timestamp"].dt.minute
out["tod_sin"] = np.sin(2*np.pi*m/1440.0).astype("float32")
out["tod_cos"] = np.cos(2*np.pi*m/1440.0).astype("float32")
return out
# ----------------------------
# STREAMING: Memory-safe build_day_features
# ----------------------------
def build_day_features(day: str) -> pd.DataFrame:
mac_files = glob.glob(os.path.join(LOG_DIR, f"mac_stats_{day}.log")) \
+ glob.glob(os.path.join(LOG_DIR, f"mac_stats_{day}_*.log"))
rrc_files = glob.glob(os.path.join(LOG_DIR, f"rrc_messages_{day}.log")) \
+ glob.glob(os.path.join(LOG_DIR, f"rrc_messages_{day}_*.log"))
mac_feat = stream_mac_minutes(mac_files)
rrc_feat = stream_rrc_minutes(rrc_files)
if mac_feat.empty and rrc_feat.empty:
raise SystemExit(f"[{day}] no parsable lines found")
# build the full per-minute index from whichever side exists
frames = [df for df in (mac_feat, rrc_feat) if not df.empty]
t_start = min(df["timestamp"].min() for df in frames)
t_end = max(df["timestamp"].max() for df in frames)
idx = pd.date_range(t_start.floor("min"), t_end.ceil("min"), freq="1min")
# --- MAC side: reindex or fabricate skeleton if empty
if not mac_feat.empty:
mac_feat = (mac_feat.set_index("timestamp")
.reindex(idx).fillna(0.0)
.rename_axis("timestamp").reset_index())
mac_feat["mac_present"] = True
else:
mac_feat = pd.DataFrame({"timestamp": idx})
mac_feat["mac_present"] = False # no MAC rows this minute
# --- RRC side: reindex or fabricate skeleton if empty
if not rrc_feat.empty:
rrc_feat = (rrc_feat.set_index("timestamp")
.reindex(idx).fillna(0.0)
.rename_axis("timestamp").reset_index())
rrc_feat["rrc_present"] = True
else:
rrc_feat = pd.DataFrame({"timestamp": idx})
# minimal set the rest of the code expects:
rrc_feat["rrc_total"] = 0.0
rrc_feat["rrc_unique_rnti"] = 0.0
for c in RRC_FAM_COLS:
rrc_feat[c] = 0.0
rrc_feat["rrc_present"] = False
# merge & flags
df = (mac_feat.merge(rrc_feat, on="timestamp", how="left")
.fillna(0.0).reset_index(drop=True))
present_cols = []
if "mac_present" in df: present_cols.append(df["mac_present"].astype(bool))
if "rrc_present" in df: present_cols.append(df["rrc_present"].astype(bool))
df["observed"] = False if not present_cols else np.logical_or.reduce(present_cols)
# time-of-day & compact dtypes
if ADD_TIME_OF_DAY:
m = df["timestamp"].dt.hour * 60 + df["timestamp"].dt.minute
df["tod_sin"] = np.sin(2*np.pi*m/1440.0).astype("float32")
df["tod_cos"] = np.cos(2*np.pi*m/1440.0).astype("float32")
float64_cols = df.select_dtypes(include=["float64"]).columns
if len(float64_cols): df[float64_cols] = df[float64_cols].astype("float32")
return df
# ----------------------------
# Sequence dataset (streaming)
# ----------------------------
def valid_t_indices(observed: np.ndarray, seq_len: int, pred_len: int) -> np.ndarray:
"""Return t indices where [t-seq_len, t+pred_len-1] are all observed=True."""
obs = observed.astype(np.uint8)
w = seq_len + pred_len
s = pd.Series(obs).rolling(w, min_periods=w).sum().to_numpy()
idx = []
for i in range(w - 1, len(obs)):
if s[i] == w:
t = i - pred_len + 1
if t >= seq_len:
idx.append(t)
return np.array(idx, dtype=np.int64)
class TSWindowDataset(Dataset):
def __init__(self, values_norm: np.ndarray, t_indices: np.ndarray, seq_len: int, pred_len: int, tgt_idx: int):
self.V = values_norm
self.t_idx = t_indices
self.seq = seq_len
self.h = pred_len
self.tgt = tgt_idx
def __len__(self): return len(self.t_idx)
def __getitem__(self, i):
t = self.t_idx[i]
x = self.V[t - self.seq : t, :]
y = self.V[t : t + self.h, self.tgt]
return torch.from_numpy(x), torch.from_numpy(y)
# ----------------------------
# LSTM model
# ----------------------------
class Seq2PointLSTM(nn.Module):
def __init__(self, input_size, hidden=128, layers=1, pred_len=PRED_LEN, dropout=0.0):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden, layers, batch_first=True, dropout=dropout if layers>1 else 0.0)
self.fc = nn.Linear(hidden, pred_len)
def forward(self, x):
enc, _ = self.lstm(x)
return self.fc(enc[:, -1, :])
# ----------------------------
# Train / Forecast per day
# ----------------------------
def prepare_day_matrix(day_df: pd.DataFrame, feat_cols_override: List[str] | None = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, list]:
"""Return values_norm, mu, sigma, observed, feature_names in a fixed order."""
base_cols = FOCUS_COLS + (["tod_sin","tod_cos"] if ADD_TIME_OF_DAY else [])
feat_cols = feat_cols_override if feat_cols_override is not None else base_cols
# ensure presence; if missing, create zeros
df = day_df.copy()
for c in feat_cols:
if c not in df.columns:
df[c] = 0.0
Xdf = df[feat_cols].astype("float32").copy()
observed = df["observed"].to_numpy()
obs_mask = observed.astype(bool)
mu = Xdf[obs_mask].mean(0).to_numpy(dtype=np.float32)
sigma = Xdf[obs_mask].std(0).to_numpy(dtype=np.float32) + 1e-8
values_norm = ((Xdf.to_numpy(dtype=np.float32) - mu) / sigma).astype(np.float32)
return values_norm, mu, sigma, observed.astype(np.bool_), feat_cols
def build_dataloaders(values_norm: np.ndarray, observed: np.ndarray, feat_cols: list, target: str):
tgt_idx = feat_cols.index(target)
t_idx_all = valid_t_indices(observed, SEQ_LEN, PRED_LEN)
if len(t_idx_all) < 100:
raise RuntimeError(f"Not enough valid windows for {target} on this day.")
split = int(0.9 * len(t_idx_all))
t_tr, t_va = t_idx_all[:split], t_idx_all[split:]
ds_tr = TSWindowDataset(values_norm, t_tr, SEQ_LEN, PRED_LEN, tgt_idx)
ds_va = TSWindowDataset(values_norm, t_va, SEQ_LEN, PRED_LEN, tgt_idx)
dl_tr = DataLoader(ds_tr, batch_size=BATCH, shuffle=True, num_workers=0, pin_memory=True)
dl_va = DataLoader(ds_va, batch_size=BATCH, shuffle=False, num_workers=0, pin_memory=True)
return dl_tr, dl_va, tgt_idx
def eval_val_loss(model: nn.Module, dl_va: DataLoader) -> float:
crit = nn.MSELoss()
was_training = model.training # remember mode
model.eval()
with torch.no_grad():
tot = 0.0; n = 0
for xb, yb in dl_va:
xb = xb.to(DEVICE); yb = yb.to(DEVICE)
pred = model(xb)
tot += crit(pred, yb).item() * xb.size(0); n += xb.size(0)
if was_training:
model.train(True) # restore mode
return tot / max(1, n)
def train_one_day(model: nn.Module, dl_tr: DataLoader, dl_va: DataLoader, epochs: int) -> float:
opt = torch.optim.Adam(model.parameters(), lr=LR)
crit = nn.MSELoss()
best_va = float("inf")
for _ in range(epochs):
model.train(True)
for xb, yb in dl_tr:
xb = xb.to(DEVICE); yb = yb.to(DEVICE)
opt.zero_grad()
pred = model(xb)
loss = crit(pred, yb)
loss.backward()
opt.step()
va = eval_val_loss(model, dl_va)
best_va = min(best_va, va)
return best_va
def rolling_forecast_day(model: nn.Module, values_norm: np.ndarray, observed: np.ndarray,
feat_cols: list, target: str, mu: np.ndarray, sigma: np.ndarray) -> np.ndarray:
tgt_idx = feat_cols.index(target)
N = values_norm.shape[0]
merged = np.full(N, np.nan, dtype=np.float32)
counts = np.zeros(N, dtype=np.float32)
t_idx_all = valid_t_indices(observed, SEQ_LEN, PRED_LEN)
model.eval()
with torch.no_grad():
for t in t_idx_all:
x = torch.from_numpy(values_norm[t-SEQ_LEN:t, :]).unsqueeze(0).to(DEVICE)
yhat = model(x).squeeze(0).cpu().numpy() # normalized
yhat_dn = yhat * sigma[tgt_idx] + mu[tgt_idx]
for j, v in enumerate(yhat_dn):
i = t + j
if i < N:
if np.isnan(merged[i]): merged[i] = 0.0
merged[i] += v; counts[i] += 1.0
with np.errstate(invalid="ignore", divide="ignore"):
merged = np.divide(merged, counts, out=merged, where=counts > 0)
return merged
# ----------------------------
# ZERO-COPY: Feature selection (mask-on-the-fly)
# ----------------------------
def eval_val_loss_masked(model: nn.Module, dl_va: DataLoader, col_to_zero: Optional[int]) -> float:
crit = nn.MSELoss()
was_training = model.training
model.eval()
tot = 0.0; n = 0
with torch.no_grad():
for xb, yb in dl_va:
if col_to_zero is not None:
xb = xb.clone()
xb[:, :, col_to_zero] = 0.0
xb = xb.to(DEVICE); yb = yb.to(DEVICE)
pred = model(xb)
tot += crit(pred, yb).item() * xb.size(0); n += xb.size(0)
if was_training:
model.train(True)
return tot / max(1, n)
def feature_audit_and_select(day_df: pd.DataFrame, target: str, base_cols: List[str]) -> Tuple[List[str], Dict[str, float]]:
"""
Memory-safe audit:
- Train once on AUDIT_DAY (fast)
- For each feature, evaluate validation while zeroing that column on-the-fly
(no full matrix copies)
Returns (kept_features, deltas) where deltas[c] = (masked - base)/base.
"""
print(f"[{AUDIT_DAY}] auditing features for target={target}...")
values_norm, mu, sigma, observed, feat_cols = prepare_day_matrix(day_df, feat_cols_override=base_cols)
dl_tr, dl_va, _ = build_dataloaders(values_norm, observed, feat_cols, target)
# Baseline model
model = Seq2PointLSTM(input_size=len(feat_cols), hidden=128, layers=1, pred_len=PRED_LEN).to(DEVICE)
base_va = train_one_day(model, dl_tr, dl_va, epochs=FS_EPOCHS)
print(f"[{AUDIT_DAY}] baseline val MSE (audit) ~ {base_va:.6f}")
deltas: Dict[str, float] = {}
for i, col in enumerate(feat_cols):
if col == target:
deltas[col] = 0.0
continue
va_mask = eval_val_loss_masked(model, dl_va, i)
delta = (va_mask - base_va) / max(1e-12, base_va)
deltas[col] = float(delta)
harmful = [c for c, d in deltas.items() if d <= -FS_IMPROVE_THRESH and c != target]
ignored = [c for c, d in deltas.items() if abs(d) < FS_NOISE_BAND and c != target]
if harmful:
print(f"[{AUDIT_DAY}] dropping harmful features (improve ≥{FS_IMPROVE_THRESH*100:.1f}%): {harmful}")
if ignored:
print(f"[{AUDIT_DAY}] (near-zero impact features): {ignored}")
kept = [c for c in feat_cols if (c == target or c not in harmful)]
print(f"[{AUDIT_DAY}] kept {len(kept)}/{len(feat_cols)} features")
top_help = sorted([(c, d) for c, d in deltas.items() if d < 0], key=lambda x: x[1])[:8]
top_hurt = sorted([(c, d) for c, d in deltas.items() if d > 0], key=lambda x: x[1], reverse=True)[:8]
def fmt(entries): return ", ".join([f"{c}:{d*100:+.1f}%" for c,d in entries]) if entries else "-"
print(f"[{AUDIT_DAY}] best removals (↓val): {fmt(top_help)}")
print(f"[{AUDIT_DAY}] worst removals (↑val): {fmt(top_hurt)}")
return kept, deltas
# ----------------------------
# Orchestration
# ----------------------------
def run_pipeline_for_target(target: str, feat_cols: List[str], run_tag: str, day_tables: Dict[str, pd.DataFrame]) -> Dict[str, float]:
"""
Train across days (inheriting weights). Save plots & underpred CSVs.
Return dict of per-day best validation losses.
"""
print(f"\n=== [{run_tag}] Target: {target} with {len(feat_cols)} features ===")
run_dir = ensure_dir(RESULT_DIR / run_tag)
val_losses: Dict[str, float] = {}
model = None
for k, day in enumerate(DATE_STRS):
print(f"[{day}] preparing data...")
df_day = day_tables[day]
values_norm, mu, sigma, observed, used_cols = prepare_day_matrix(df_day, feat_cols_override=feat_cols)
input_size = values_norm.shape[1]
if model is None:
model = Seq2PointLSTM(input_size=input_size, hidden=128, layers=1, pred_len=PRED_LEN).to(DEVICE)
dl_tr, dl_va, _ = build_dataloaders(values_norm, observed, used_cols, target)
epochs = EPOCHS_FIRST if k == 0 else EPOCHS_NEXT
print(f"[{day}] training ({epochs} epochs)...")
va = train_one_day(model, dl_tr, dl_va, epochs)
val_losses[day] = va
print(f"[{day}] best val MSE ~ {va:.6f}")
print(f"[{day}] forecasting...")
pred = rolling_forecast_day(model, values_norm, observed, used_cols, target, mu, sigma)
actual = df_day[target].astype("float32").to_numpy()
# Under-prediction report
csv_path = run_dir / f"{target}_underpred_{day}.csv"
metrics = report_underpredictions(
timestamps=df_day["timestamp"],
actual=actual,
pred=pred,
out_csv_path=csv_path
)
print(
f"[{day}] {target} under-predictions: "
f"{metrics['n_under']}/{metrics['n_pred_points']} "
f"({metrics['under_rate']*100:.1f}%) | "
f"MAE_under={metrics['mae_under']:.3f}, "
f"sum_under={metrics['sum_under']:.3f}, "
f"max_under={metrics['max_under']:.3f}, "
f"p95_under={metrics['p95_under']:.3f}, "
f"runs={metrics['n_runs']}, avg_run={metrics['avg_run']:.2f} min, "
f"max_run={metrics['max_run']} min"
)
print("saved under-prediction CSV →", csv_path)
# Plot with under-prediction highlighting
plt.figure(figsize=(12,4))
plt.plot(actual, label=f"actual {target}", lw=1)
if np.isfinite(pred).any():
plt.plot(pred, label=f"forecast {target}", lw=2)
finite = np.isfinite(actual) & np.isfinite(pred)
under = finite & (pred < actual)
if under.any():
x = np.arange(len(actual))
pred_f = np.where(np.isfinite(pred), pred, actual)
plt.fill_between(x, pred_f, actual, where=under, alpha=0.15, color="red", label="under-predicted")
plt.title(f"{target} – {PRED_LEN}-min horizon (LSTM, day {day}) [{run_tag}]")
plt.xlabel("minutes (index within day)")
plt.ylabel(target); plt.grid(True); plt.legend()
outp = run_dir / f"{target}_forecast_{day}_lstm.png"
plt.tight_layout(); plt.savefig(outp); plt.close()
print("saved", outp)
return val_losses
def summarize_comparison(target: str, losses_base: Dict[str, float], losses_pruned: Dict[str, float]):
days = sorted(losses_base.keys())
print(f"\n=== Comparison (target={target}) ===")
for d in days:
b, p = losses_base[d], losses_pruned[d]
delta = (p - b) / max(1e-12, b)
print(f"{d}: base={b:.6f} | pruned={p:.6f} | Δ={delta*100:+.2f}%")
b_mean = np.mean([losses_base[d] for d in days])
p_mean = np.mean([losses_pruned[d] for d in days])
d_mean = (p_mean - b_mean) / max(1e-12, b_mean)
print(f"Avg: base={b_mean:.6f} | pruned={p_mean:.6f} | Δ={d_mean*100:+.2f}%")
def main():
ensure_dir(RESULT_DIR)
# Build per-day feature tables once (independently per day)
day_tables: Dict[str, pd.DataFrame] = {d: build_day_features(d) for d in DATE_STRS}
base_cols = FOCUS_COLS + (["tod_sin","tod_cos"] if ADD_TIME_OF_DAY else [])
for target in TARGETS:
# 1) Baseline full-feature run
losses_base = run_pipeline_for_target(target, feat_cols=base_cols, run_tag="baseline", day_tables=day_tables)
# 2) Audit and select (on AUDIT_DAY) then re-run with pruned set
if ENABLE_FEATURE_SELECTION:
kept_cols = base_cols
kept_map = {}
try:
kept_cols, deltas = feature_audit_and_select(day_tables[AUDIT_DAY], target, base_cols)
kept_map = deltas
except Exception as e:
print(f"[WARN] feature audit failed ({e}); proceeding without pruning.")
losses_pruned = run_pipeline_for_target(target, feat_cols=kept_cols, run_tag="pruned", day_tables=day_tables)
# 3) Summarize before/after
summarize_comparison(target, losses_base, losses_pruned)
# Save audit table
audit_path = ensure_dir(RESULT_DIR / "pruned") / f"{target}_feature_audit_{AUDIT_DAY}.csv"
if kept_map:
pd.DataFrame(
[{"feature": c, "delta_val_pct": 100.0 * kept_map[c], "dropped": (c not in kept_cols)} for c in kept_map]
).sort_values("delta_val_pct").to_csv(audit_path, index=False)
print("saved feature audit →", audit_path)
else:
print("[INFO] Feature selection disabled; only baseline run executed.")
if __name__ == "__main__":
main()
@@ -80,27 +80,7 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
oai-gnb: oai-nr-ue1:
image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-gnb
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim --log_config.global_log_options level,nocolor,time
ASAN_OPTIONS: detect_leaks=0
depends_on:
- oai-flexric
networks:
public_net:
ipv4_address: 192.168.70.151
volumes:
- ../../conf_files/gnb.sa.band78.106prb.rfsim.flexric.conf:/opt/oai-gnb/etc/gnb.conf
healthcheck:
test: /bin/bash -c "pgrep nr-softmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true privileged: true
container_name: rfsim5g-oai-nr-ue container_name: rfsim5g-oai-nr-ue
@@ -138,7 +118,154 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks: oai-nr-ue3:
public_net: image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
name: oai-cn5g-public-net privileged: true
external: true container_name: rfsim5g-oai-nr-ue3
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.154
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue4:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue4
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.155
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue5:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue5
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.156
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue6:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue6
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.157
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue7:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue7
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.158
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue8:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue8
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.159
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue9:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue9
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.160
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
oai-nr-ue10:
image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
privileged: true
container_name: rfsim5g-oai-nr-ue10
environment:
USE_ADDITIONAL_OPTIONS: -E --rfsim -r 106 --numerology 1 -C 3619200000 --uicc0.imsi 001010000000004 --rfsimulator.serveraddr 192.168.70.151 --log_config.global_log_options level,nocolor,time
depends_on:
- oai-gnb
networks:
public_net:
ipv4_address: 192.168.70.161
volumes:
- ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
healthcheck:
test: /bin/bash -c "pgrep nr-uesoftmodem"
interval: 10s
timeout: 5s
retries: 5
+51
View File
@@ -0,0 +1,51 @@
FROM ubuntu:noble AS ran-base
ARG NEEDED_GIT_PROXY
ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo
ENV BUILD_UHD_FROM_SOURCE=True
ENV UHD_VERSION=4.4.0.0
RUN apt-get update && \
apt-get upgrade --yes && \
apt-get install --yes \
#gcc needed for build_oai
build-essential \
ccache \
psmisc \
git \
gcc-12 \
g++-12 \
xxd \
libpcre2-dev \
python3-dev \
bison \
flex \
m4 \
# python3-pip for conf template generation
python3-pip \
wget && \
# --break-system-packages allows externally managed environment in pip
pip3 install --ignore-installed pyyaml --break-system-packages
ENV TINI_VERSION=v0.19.0
RUN wget https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TARGETARCH} -O /tini
RUN chmod +x /tini
RUN /bin/bash -c "if [[ -v NEEDED_GIT_PROXY ]]; then git config --global http.proxy $NEEDED_GIT_PROXY; fi"
WORKDIR /oai-ran/cmake_targets/tools
COPY cmake_targets/tools/build_helper \
cmake_targets/tools/uhd-4.x-tdd-patch.diff \
./
WORKDIR /oai-ran/cmake_targets
COPY cmake_targets/build_oai .
WORKDIR /oai-ran
COPY oaienv .
RUN /bin/sh oaienv && \
cd cmake_targets && \
mkdir -p log && \
./build_oai -I -w USRP --install-optional-packages
-84
View File
@@ -1,84 +0,0 @@
#/*
# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
# * contributor license agreements. See the NOTICE file distributed with
# * this work for additional information regarding copyright ownership.
# * The OpenAirInterface Software Alliance licenses this file to You under
# * the OAI Public License, Version 1.1 (the "License"); you may not use this file
# * except in compliance with the License.
# * You may obtain a copy of the License at
# *
# * http://www.openairinterface.org/?page_id=698
# *
# * Unless required by applicable law or agreed to in writing, software
# * distributed under the License is distributed on an "AS IS" BASIS,
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# * See the License for the specific language governing permissions and
# * limitations under the License.
# *-------------------------------------------------------------------------------
# * For more information about the OpenAirInterface (OAI) Software Alliance:
# * contact@openairinterface.org
# */
#---------------------------------------------------------------------
#
# Dockerfile for the Open-Air-Interface BUILD service
# Valid for Ubuntu 22.04
#
#---------------------------------------------------------------------
FROM ubuntu:jammy AS ran-base
ARG NEEDED_GIT_PROXY
ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Europe/Paris
ENV BUILD_UHD_FROM_SOURCE=True
ENV UHD_VERSION=4.4.0.0
#install developers pkg/repo
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade --yes && \
DEBIAN_FRONTEND=noninteractive apt-get install --yes \
#gcc needed for build_oai
build-essential \
psmisc \
git \
#use gcc-12 to avoid problems of default gcc-11 in FlexRIC
gcc-12 \
g++-12 \
xxd \
libpcre2-dev \
python3-dev \
bison \
flex \
m4 \
# python3-pip for conf template generation
python3-pip && \
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 --slave /usr/bin/g++ g++ /usr/bin/g++-12 && \
pip3 install --ignore-installed pyyaml
# Add "Tini - A tiny but valid init for containers", https://github.com/krallin/tini
# it will be copied into target containers, to print exit numbers and handle signals properly
ENV TINI_VERSION=v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TARGETARCH} /tini
RUN chmod +x /tini
# In some network environments, GIT proxy is required
RUN /bin/bash -c "if [[ -v NEEDED_GIT_PROXY ]]; then git config --global http.proxy $NEEDED_GIT_PROXY; fi"
# Copying only the needed files to create ran-base
WORKDIR /oai-ran/cmake_targets/tools
COPY cmake_targets/tools/build_helper \
cmake_targets/tools/uhd-4.x-tdd-patch.diff \
./
WORKDIR /oai-ran/cmake_targets
COPY cmake_targets/build_oai .
WORKDIR /oai-ran
COPY oaienv .
#run build_oai -I to get the builder image
RUN /bin/sh oaienv && \
cd cmake_targets && \
mkdir -p log && \
./build_oai -I -w USRP --install-optional-packages
@@ -21,7 +21,7 @@
#--------------------------------------------------------------------- #---------------------------------------------------------------------
# #
# Dockerfile for the Open-Air-Interface BUILD service # Dockerfile for the Open-Air-Interface BUILD service
# Valid for Ubuntu 22.04 # Valid for Ubuntu 24.04
# #
#--------------------------------------------------------------------- #---------------------------------------------------------------------
@@ -36,7 +36,12 @@ WORKDIR /oai-ran
COPY . . COPY . .
# build all targets so final targets can be created afterwards # build all targets so final targets can be created afterwards
RUN /bin/sh oaienv && \ RUN \
# Mount CCache cache direrctory
--mount=type=cache,target=/root/.cache/ccache/ \
# Mount CPM package cache
--mount=type=cache,target=/root/.cache/cpm/ \
/bin/sh oaienv && \
cd cmake_targets && \ cd cmake_targets && \
mkdir -p log && \ mkdir -p log && \
./build_oai -c --ninja \ ./build_oai -c --ninja \
@@ -54,12 +59,13 @@ RUN /bin/sh oaienv && \
ldd ran_build/build/*.so ldd ran_build/build/*.so
## Build FlexRIC for SM Models ## Build FlexRIC for SM Models
RUN mkdir -p openair2/E2AP/flexric/build && \ RUN \
# Mount CCache cache direrctory
--mount=type=cache,target=/root/.cache/ccache/ \
mkdir -p openair2/E2AP/flexric/build && \
cd openair2/E2AP/flexric/build && \ cd openair2/E2AP/flexric/build && \
cmake -GNinja -DCMAKE_BUILD_TYPE=Release \ cmake -GNinja -DCMAKE_BUILD_TYPE=Release \
-DKPM_VERSION=$KPM_VERSION \ -DKPM_VERSION=$KPM_VERSION \
-DE2AP_VERSION=$E2AP_VERSION \ -DE2AP_VERSION=$E2AP_VERSION .. && \
-DCMAKE_C_COMPILER=gcc-12 \
-DCMAKE_CXX_COMPILER=g++-12 .. && \
ninja && \ ninja && \
ninja install ninja install
+66
View File
@@ -0,0 +1,66 @@
FROM ran-base:latest AS ran-build
ARG BUILD_OPTION
ARG E2AP_VERSION=E2AP_V2
ARG KPM_VERSION=KPM_V2_03
RUN rm -Rf /oai-ran
WORKDIR /oai-ran
COPY . .
# build all targets so final targets can be created afterwards
RUN /bin/sh oaienv && \
cd cmake_targets && \
mkdir -p log && \
./build_oai -c --ninja \
--gNB --RU --nrUE \
--build-lib "telnetsrv enbscope uescope nrscope" \
-w USRP -t Ethernet \
--build-e2 --cmake-opt -DKPM_VERSION=$KPM_VERSION --cmake-opt -DE2AP_VERSION=$E2AP_VERSION \
--noavx512 \
--build-tool-opt -k10 \
--cmake-opt -DCMAKE_C_FLAGS="-Werror" --cmake-opt -DCMAKE_CXX_FLAGS="-Werror" $BUILD_OPTION && \
echo "---- ldd on executables ----" && \
ldd ran_build/build/*softmodem* ran_build/build/nr-cuup ran_build/build/oairu && \
echo "---- ldd on shared libraries ----" && \
ldd ran_build/build/*.so
RUN rm -rf openair2/E2AP/flexric/build && \
mkdir -p openair2/E2AP/flexric/build && \
cd openair2/E2AP/flexric/build && \
cmake -DCMAKE_BUILD_TYPE=Release \
-DKPM_VERSION=KPM_V2_03 \
-DE2AP_VERSION=E2AP_V2 \
-DCMAKE_C_COMPILER=gcc-12 \
-DCMAKE_CXX_COMPILER=g++-12 .. && \
make -j8 && make install
RUN set -eux; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl wget iproute2 iputils-ping dnsutils jq \
iperf iperf3 ffmpeg \
nghttp2-client || true; \
# Ubuntu vs Debian name differences (fallback for nghttp2 client tools)
if ! command -v h2load >/dev/null 2>&1; then \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nghttp2 || true; \
fi; \
# Try to install sipp from apt; if unavailable, build from source
if apt-cache show sipp >/dev/null 2>&1; then \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends sipp; \
else \
echo "sipp not in apt, building from source..."; \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential cmake git libncurses5-dev libpcap-dev libssl-dev libpcre3-dev; \
git clone --depth=1 https://github.com/SIPp/sipp.git /tmp/sipp; \
cd /tmp/sipp; cmake -DENABLE_SSL=1 -DENABLE_PCAP=1 -DCMAKE_BUILD_TYPE=Release .; \
make -j"$(nproc)"; make install; strip /usr/local/bin/sipp || true; \
rm -rf /tmp/sipp; \
fi; \
rm -rf /var/lib/apt/lists/*
# Traffic runner
COPY tools/scripts/traffic_runner.sh /usr/local/bin/traffic_runner
RUN chmod +x /usr/local/bin/traffic_runner
ENTRYPOINT ["/bin/bash"]
@@ -170,7 +170,7 @@ static void capture_sst_sd(test_cond_value_t* test_cond_value, uint8_t *sst, uin
DevAssert(test_cond_value->octet_string_value->len == 4); DevAssert(test_cond_value->octet_string_value->len == 4);
uint8_t *buf = test_cond_value->octet_string_value->buf; uint8_t *buf = test_cond_value->octet_string_value->buf;
*sst = buf[0]; *sst = buf[0];
*sd = malloc(**sd); *sd = malloc(sizeof(uint32_t));
**sd = buf[1] << 16 | buf[2] << 8 | buf[3]; **sd = buf[1] << 16 | buf[2] << 8 | buf[3];
} }
printf("[E2 AGENT][E2SM-KPM] Condition NSSAI (%d, %x).\n", *sst, (*sd) ? **sd : 0xffffff); printf("[E2 AGENT][E2SM-KPM] Condition NSSAI (%d, %x).\n", *sst, (*sd) ? **sd : 0xffffff);
@@ -26,7 +26,6 @@
* \email: guido.casati@iis.fraunhofe.de * \email: guido.casati@iis.fraunhofe.de
* \version 1.0 * \version 1.0
* @ingroup _mac * @ingroup _mac
*/ */
#include "common/utils/nr/nr_common.h" #include "common/utils/nr/nr_common.h"
@@ -708,7 +707,7 @@ static void pf_dl(module_id_t module_id,
qsort(UE_sched, sizeofArray(UE_sched), sizeof(UEsched_t), comparator); qsort(UE_sched, sizeofArray(UE_sched), sizeof(UEsched_t), comparator);
UEsched_t *iterator = UE_sched; UEsched_t *iterator = UE_sched;
const int min_rbSize = 5; const int min_rbSize = 10;
/* Loop UE_sched to find max coeff and allocate transmission */ /* Loop UE_sched to find max coeff and allocate transmission */
while (iterator->UE != NULL) { while (iterator->UE != NULL) {
@@ -772,10 +771,10 @@ static void pf_dl(module_id_t module_id,
while (rbStart < rbStop && (rballoc_mask[rbStart + bwp_start] & slbitmap)) while (rbStart < rbStop && (rballoc_mask[rbStart + bwp_start] & slbitmap))
rbStart++; rbStart++;
uint16_t max_rbSize = 1; uint16_t max_rbSize = 10; // used to be 1
while (rbStart + max_rbSize <= rbStop && !(rballoc_mask[rbStart + max_rbSize + bwp_start] & slbitmap)) /* while (rbStart + max_rbSize <= rbStop && !(rballoc_mask[rbStart + max_rbSize + bwp_start] & slbitmap)) */
max_rbSize++; /* max_rbSize++; */
if (max_rbSize < min_rbSize) { if (max_rbSize < min_rbSize) {
LOG_D(NR_MAC, LOG_D(NR_MAC,
@@ -907,12 +906,13 @@ nr_pp_impl_dl nr_init_dlsch_preprocessor(int CC_id) {
const uint16_t R = nr_get_code_rate_dl(mcs, mcsTableIdx); const uint16_t R = nr_get_code_rate_dl(mcs, mcsTableIdx);
pf_tbs[mcsTableIdx][mcs] = nr_compute_tbs(Qm, pf_tbs[mcsTableIdx][mcs] = nr_compute_tbs(Qm,
R, R,
1, /* rbSize */ 1, /* rbSize */
10, /* hypothetical number of slots */ 10, /* hypothetical number of slots */
0, /* N_PRB_DMRS * N_DMRS_SLOT */ 0, /* N_PRB_DMRS * N_DMRS_SLOT */
0 /* N_PRB_oh, 0 for initialBWP */, 0 /* N_PRB_oh, 0 for initialBWP */,
0 /* tb_scaling */, 0 /* tb_scaling */,
1 /* nrOfLayers */) >> 3; 1 /* nrOfLayers */
) >> 3;
} }
} }
@@ -1374,6 +1374,7 @@ void nr_schedule_ue_spec(module_id_t module_id,
buf=bufEnd; buf=bufEnd;
} }
// mac_stats
UE->mac_stats.dl.total_bytes += TBS; UE->mac_stats.dl.total_bytes += TBS;
UE->mac_stats.dl.current_bytes = TBS; UE->mac_stats.dl.current_bytes = TBS;
UE->mac_stats.dl.total_rbs += sched_pdsch->rbSize; UE->mac_stats.dl.total_rbs += sched_pdsch->rbSize;
@@ -2151,9 +2151,9 @@ static void pf_ul(module_id_t module_id,
while (rbStart < bwpSize && (rballoc_mask[rbStart + bwpStart] & slbitmap)) while (rbStart < bwpSize && (rballoc_mask[rbStart + bwpStart] & slbitmap))
rbStart++; rbStart++;
sched_pusch->rbStart = rbStart; sched_pusch->rbStart = rbStart;
uint16_t max_rbSize = 1; uint16_t max_rbSize = 10; // used to be 1
while (rbStart + max_rbSize < bwpSize && !(rballoc_mask[rbStart + bwpStart + max_rbSize] & slbitmap)) /* while (rbStart + max_rbSize < bwpSize && !(rballoc_mask[rbStart + bwpStart + max_rbSize] & slbitmap)) */
max_rbSize++; /* max_rbSize++; */
if (rbStart + min_rb > bwpSize || max_rbSize < min_rb) { if (rbStart + min_rb > bwpSize || max_rbSize < min_rb) {
reset_beam_status(&nrmac->beam_info, frame, slot, iterator->UE->UE_beam_index, slots_per_frame, dci_beam.new_beam); reset_beam_status(&nrmac->beam_info, frame, slot, iterator->UE->UE_beam_index, slots_per_frame, dci_beam.new_beam);
+233
View File
@@ -0,0 +1,233 @@
#!/bin/bash
# Iperf-exclusive traffic generator for OAI UE containers.
# Profiles: browsing, video, bulk_download, voip, iot_chatty, gaming, idle
# Usage:
# traffic_runner --profile browsing --duration 0 --iperf-server 10.0.0.1:5201
#
set -euo pipefail
# Clear exp.sh lock on exit so sessions can restart later
trap 'rm -f /tmp/traffic_profile.lock' EXIT
DEFAULT_IPERF_PORT="${DEFAULT_IPERF_PORT:-5201}"
TRAFFIC_NET_GATEWAY="${TRAFFIC_NET_GATEWAY:-192.168.71.130}"
if [[ -z "${IPERF_SERVER:-}" ]]; then
# Candidate hostnames (Docker DNS on the same network) then the gateway IP
IPERF_HOST_CANDIDATES=${IPERF_HOST_CANDIDATES:-"iperf iperf3 iperf-server ${TRAFFIC_NET_GATEWAY}"}
for h in $IPERF_HOST_CANDIDATES; do
if getent hosts "$h" >/dev/null 2>&1; then
IPERF_SERVER="${h}:${DEFAULT_IPERF_PORT}"
break
fi
# Accept numeric IPv4 even if DNS not present
if [[ "$h" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
IPERF_SERVER="${h}:${DEFAULT_IPERF_PORT}"
break
fi
done
fi
# Final check
[[ -n "${IPERF_SERVER:-}" ]] || die "No iperf server found. Set --iperf-server or connect a server to rfsim5g-oai-traffic-net."
IPERF_HOST="${IPERF_SERVER%:*}"
IPERF_PORT="${IPERF_SERVER#*:}"
[[ "$IPERF_PORT" =~ ^[0-9]+$ ]] || die "Invalid iperf port in $IPERF_SERVER"
PROFILE=""
DURATION=0
LOG_FILE="/tmp/traffic_runner.log"
log() { printf '[%(%Y-%m-%dT%H:%M:%S%z)T] %s\n' -1 "$*" | tee -a "$LOG_FILE" >&2; }
die() { log "ERROR: $*"; exit 2; }
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) PROFILE="$2"; shift 2;;
--duration) DURATION="$2"; shift 2;;
--iperf-server) IPERF_SERVER="$2"; shift 2;;
--help|-h) cat <<EOF
Usage: $0 --profile <name> [--duration <sec>] [--iperf-server host:port]
Profiles: browsing, video, bulk_download, voip, iot_chatty, gaming, idle
If duration is 0 or omitted, run indefinitely.
EOF
exit 0;;
*) die "Unknown arg: $1";;
esac
done
[[ -n "$PROFILE" ]] || die "--profile is required"
[[ -n "$IPERF_SERVER" ]] || die "--iperf-server or IPERF_SERVER env is required (e.g., 192.168.0.2:5201)"
IPERF_HOST="${IPERF_SERVER%:*}"
IPERF_PORT="${IPERF_SERVER#*:}"
[[ "$IPERF_PORT" =~ ^[0-9]+$ ]] || die "Invalid iperf port in $IPERF_SERVER"
# Helpers
run_for_duration() {
# $1: command string to eval in a loop
# uses global DURATION
local start now elapsed
start=$(date +%s)
while :; do
now=$(date +%s)
elapsed=$(( now - start ))
if (( DURATION > 0 && elapsed >= DURATION )); then break; fi
eval "$1"
done
}
# iperf helpers
iperf_tcp_dl() { iperf3 -c "$IPERF_HOST" -p "$IPERF_PORT" -R -t "$1" -J >/dev/null 2>&1 || true; }
iperf_tcp_ul() { iperf3 -c "$IPERF_HOST" -p "$IPERF_PORT" -t "$1" -J >/dev/null 2>&1 || true; }
iperf_udp_dl() { iperf3 -c "$IPERF_HOST" -p "$IPERF_PORT" -R -u -b "$2" -t "$1" -J >/dev/null 2>&1 || true; }
iperf_udp_ul() { iperf3 -c "$IPERF_HOST" -p "$IPERF_PORT" -u -b "$2" -t "$1" -J >/dev/null 2>&1 || true; }
# Profiles
do_browsing() {
log "Browsing (TCP bursts, short think-time) to $IPERF_SERVER"
run_for_duration '
dur=$(( 1 + RANDOM % 3 )) # 1-3s burst
iperf_tcp_dl "$dur"
sleep "0.$((100 + RANDOM % 600))" # 0.1-0.7s think
'
}
do_video() {
log "Video (FHD video 5 Mbps sustained) to $IPERF_SERVER"
run_for_duration '
iperf3 -c "'"$IPERF_HOST"'" -p "'"$IPERF_PORT"'" -R -u -b 5M -t 30 -J >/dev/null 2>&1
sleep 1
'
}
do_bulk_download() {
# Heavy TCP downlink with multiple streams
log "Bulk download (TCP DL 8 parallel, long sustained) to $IPERF_SERVER"
run_for_duration '
iperf3 -c "'"$IPERF_HOST"'" -p "'"$IPERF_PORT"'" -R -P 8 -t 20 -w 1M -J >/dev/null 2>&1
sleep 1
'
}
do_voip() {
log "VoIP (UDP bi-di ~100–200 kbps) to $IPERF_SERVER"
run_for_duration '
iperf_udp_ul 5 120K &
iperf_udp_dl 5 120K &
wait
sleep 0.5
'
}
do_iot_chatty() {
log "IoT chatty (UDP UL ~50–80 kbps bursts) to $IPERF_SERVER"
run_for_duration '
iperf_udp_ul 2 60K
sleep $(( 3 + RANDOM % 7 ))
'
}
do_gaming() {
log "Gaming (UDP bi-di ~1–3 Mbps bursts) to $IPERF_SERVER"
run_for_duration '
rate_ul=$(( 1 + RANDOM % 3 ))M
rate_dl=$(( 1 + RANDOM % 3 ))M
iperf_udp_ul 5 "$rate_ul" &
iperf_udp_dl 5 "$rate_dl" &
wait
sleep 0.5
'
}
do_idle() {
log "Idle"
run_for_duration "sleep 2"
}
# ------------- Randomized pattern (bursts of 30s..minutes) ------------------
# Defaults can be overridden via env:
# MIN_ACTIVE_SEC / MAX_ACTIVE_SEC — each session duration (default 30..180)
# MIN_IDLE_SEC / MAX_IDLE_SEC — wait between sessions (default 5..60)
# BURST_FILE — path watched for system-wide bursts
# BURST_MAX_AGE_SEC — consider burst file "fresh" within N sec
MIN_ACTIVE_SEC="${MIN_ACTIVE_SEC:-30}"
MAX_ACTIVE_SEC="${MAX_ACTIVE_SEC:-180}"
MIN_IDLE_SEC="${MIN_IDLE_SEC:-5}"
MAX_IDLE_SEC="${MAX_IDLE_SEC:-60}"
BURST_FILE="${BURST_FILE:-/tmp/system_burst}"
BURST_MAX_AGE_SEC="${BURST_MAX_AGE_SEC:-45}"
_pick_random_profile() {
# Weighted pick to feel realistic; adjust as desired
local r=$((RANDOM % 100))
if (( r < 40 )); then echo browsing # 40%
elif (( r < 55 )); then echo voip # 15%
elif (( r < 70 )); then echo video # 15%
elif (( r < 85 )); then echo gaming # 15%
else echo bulk_download # 15%
fi
}
_run_profile_for() {
local name="$1" seconds="$2"
DURATION="$seconds"
case "$name" in
browsing) do_browsing;;
video) do_video;;
bulk_download) do_bulk_download;;
voip) do_voip;;
iot_chatty) do_iot_chatty;;
gaming) do_gaming;;
idle) do_idle;;
*) do_browsing;;
esac
}
_is_burst_now() {
# return 0 if a fresh burst marker exists
[[ -f "$BURST_FILE" ]] || return 1
local now mtime age
now=$(date +%s)
mtime=$(stat -c %Y "$BURST_FILE" 2>/dev/null || echo 0)
age=$(( now - mtime ))
(( age >= 0 && age <= BURST_MAX_AGE_SEC ))
}
do_random() {
log "Random pattern: independent per-UE bursts to $IPERF_SERVER (session: ${MIN_ACTIVE_SEC}-${MAX_ACTIVE_SEC}s; idle: ${MIN_IDLE_SEC}-${MAX_IDLE_SEC}s)"
while :; do
local prof dur idle
prof="$(_pick_random_profile)"
dur=$(( MIN_ACTIVE_SEC + RANDOM % (MAX_ACTIVE_SEC - MIN_ACTIVE_SEC + 1) ))
if _is_burst_now; then
# If a system burst is signaled, start immediately and slightly extend session
idle=0
dur=$(( dur + 20 + RANDOM % 40 )) # +20..60s
else
idle=$(( MIN_IDLE_SEC + RANDOM % (MAX_IDLE_SEC - MIN_IDLE_SEC + 1) ))
fi
log "Random session: profile=${prof} duration=${dur}s (then idle ${idle}s)"
_run_profile_for "$prof" "$dur" || true
sleep "$idle"
done
}
case "$PROFILE" in
browsing) do_browsing;;
video) do_video;;
bulk_download) do_bulk_download;;
voip) do_voip;;
iot_chatty) do_iot_chatty;;
gaming) do_gaming;;
idle) do_idle;;
random) do_random;;
*) die "Unknown profile: $PROFILE";;
esac