From ac4dbff76e4082f44802606b3dc03c69c6f472fc Mon Sep 17 00:00:00 2001 From: maki Date: Sun, 19 Oct 2025 20:03:22 +0900 Subject: [PATCH] nakaolab oct19 --- .dockerignore | 1 + .../gnb.sa.band78.106prb.rfsim.flexric.conf | 9 +- .../gnb.sa.band78.106prb.rfsim.yaml | 11 +- ci-scripts/conf_files/nrue.uicc.conf | 3 +- ci-scripts/conf_files/nrue.uicc.yaml | 3 +- .../5g_rfsimulator/conf/flexric.conf | 9 + .../{ => conf}/mini_nonrf_config.yaml | 37 +- .../{ => conf}/mysql-healthcheck.sh | 0 .../5g_rfsimulator/{ => conf}/oai_db.sql | 12 +- .../yaml_files/5g_rfsimulator/conf/sql.cnf | 3 + .../5g_rfsimulator/docker-compose.yaml | 239 ++++-- ci-scripts/yaml_files/5g_rfsimulator/exp.sh | 654 +++++++++++++++ .../5g_rfsimulator/local-override-ue-gdb.yaml | 6 - .../5g_rfsimulator/local-override.yaml | 27 - ci-scripts/yaml_files/5g_rfsimulator/memo.txt | 44 + .../5g_rfsimulator/oai-end-to-end.jpg | Bin 56238 -> 0 bytes ci-scripts/yaml_files/5g_rfsimulator/plots.py | 144 ++++ .../yaml_files/5g_rfsimulator/prediction.py | 772 ++++++++++++++++++ .../5g_rfsimulator_flexric/docker-compose.yml | 177 +++- docker/Dockerfile.base.ubuntu | 51 ++ docker/Dockerfile.base.ubuntu22 | 84 -- ...build.ubuntu22 => Dockerfile.build.ubuntu} | 18 +- docker/Dockerfile.nakaolab.oct1 | 66 ++ .../E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm.c | 2 +- openair2/E2AP/flexric | 2 +- .../LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c | 21 +- .../LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c | 6 +- tools/scripts/traffic_runner.sh | 233 ++++++ 28 files changed, 2327 insertions(+), 307 deletions(-) create mode 100644 ci-scripts/yaml_files/5g_rfsimulator/conf/flexric.conf rename ci-scripts/yaml_files/5g_rfsimulator/{ => conf}/mini_nonrf_config.yaml (65%) rename ci-scripts/yaml_files/5g_rfsimulator/{ => conf}/mysql-healthcheck.sh (100%) rename ci-scripts/yaml_files/5g_rfsimulator/{ => conf}/oai_db.sql (58%) create mode 100644 ci-scripts/yaml_files/5g_rfsimulator/conf/sql.cnf create mode 100755 ci-scripts/yaml_files/5g_rfsimulator/exp.sh delete mode 100644 ci-scripts/yaml_files/5g_rfsimulator/local-override-ue-gdb.yaml delete mode 100644 ci-scripts/yaml_files/5g_rfsimulator/local-override.yaml create mode 100644 ci-scripts/yaml_files/5g_rfsimulator/memo.txt delete mode 100644 ci-scripts/yaml_files/5g_rfsimulator/oai-end-to-end.jpg create mode 100644 ci-scripts/yaml_files/5g_rfsimulator/plots.py create mode 100644 ci-scripts/yaml_files/5g_rfsimulator/prediction.py create mode 100644 docker/Dockerfile.base.ubuntu delete mode 100644 docker/Dockerfile.base.ubuntu22 rename docker/{Dockerfile.build.ubuntu22 => Dockerfile.build.ubuntu} (85%) create mode 100644 docker/Dockerfile.nakaolab.oct1 create mode 100644 tools/scripts/traffic_runner.sh diff --git a/.dockerignore b/.dockerignore index 59051c1a30..1ca068f828 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ common/utils/T/T_IDs.h common/utils/T/T_messages.txt.h common/utils/T/genids common/utils/T/genids.o +artifacts diff --git a/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.flexric.conf b/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.flexric.conf index 12a86c3e64..8be3135296 100644 --- a/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.flexric.conf +++ b/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.flexric.conf @@ -153,12 +153,12 @@ gNBs = }; ////////// AMF parameters: - amf_ip_address = ({ ipv4 = "192.168.70.132"; }); + amf_ip_address = ({ ipv4 = "192.168.71.132"; }); NETWORK_INTERFACES : { - GNB_IPV4_ADDRESS_FOR_NG_AMF = "192.168.70.151"; - GNB_IPV4_ADDRESS_FOR_NGU = "192.168.70.151"; + GNB_IPV4_ADDRESS_FOR_NG_AMF = "192.168.71.140"; + GNB_IPV4_ADDRESS_FOR_NGU = "192.168.71.140"; GNB_PORT_FOR_S1U = 2152; # Spec 2152 }; @@ -172,6 +172,7 @@ MACRLCs = ( tr_n_preference = "local_RRC"; pusch_TargetSNRx10 = 200; pucch_TargetSNRx10 = 200; + min_grant_prb = 1; } ); @@ -233,7 +234,7 @@ log_config : { }; 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 = "/usr/local/lib/flexric/" }; diff --git a/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.yaml b/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.yaml index 7b75481634..a9257ff2c4 100644 --- a/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.yaml +++ b/ci-scripts/conf_files/gnb.sa.band78.106prb.rfsim.yaml @@ -1,5 +1,12 @@ Active_gNBs: - 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: none gNBs: @@ -142,13 +149,13 @@ gNBs: GNB_IPV4_ADDRESS_FOR_NGU: 192.168.71.140 GNB_PORT_FOR_S1U: 2152 # Spec 2152 - MACRLCs: - num_cc: 1 tr_s_preference: local_L1 tr_n_preference: local_RRC pusch_TargetSNRx10: 200 pucch_TargetSNRx10: 200 + min_grant_prb: 10 L1s: - num_cc: 1 @@ -195,6 +202,6 @@ log_config: mac_log_level: info rlc_log_level: info pdcp_log_level: info - rrc_log_level: info + rrc_log_level: debug ngap_log_level: debug f1ap_log_level: debug diff --git a/ci-scripts/conf_files/nrue.uicc.conf b/ci-scripts/conf_files/nrue.uicc.conf index 5424abe30f..013a105681 100644 --- a/ci-scripts/conf_files/nrue.uicc.conf +++ b/ci-scripts/conf_files/nrue.uicc.conf @@ -1,5 +1,4 @@ -uicc0 = { - imsi = "208990100001100"; +uicc0 = { # imsi is omitted key = "fec86ba6eb707ed08905757b1bb44b8f"; opc= "C42449363BBAD02B66D16BC975D77CC1"; dnn= "oai"; diff --git a/ci-scripts/conf_files/nrue.uicc.yaml b/ci-scripts/conf_files/nrue.uicc.yaml index da3b7e7e19..0d4640833c 100644 --- a/ci-scripts/conf_files/nrue.uicc.yaml +++ b/ci-scripts/conf_files/nrue.uicc.yaml @@ -4,6 +4,7 @@ uicc0: opc: C42449363BBAD02B66D16BC975D77CC1 dnn: oai nssai_sst: 1 + pdu_type: IPV4 thread-pool: "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1" @@ -24,7 +25,7 @@ channelmod: - model_name: rfsimu_channel_ue0 type: AWGN ploss_dB: 20 - nose_power_dB: -2 + noise_power_dB: -2 forgetfact: 0 offset: 0 ds_tdl: 0 diff --git a/ci-scripts/yaml_files/5g_rfsimulator/conf/flexric.conf b/ci-scripts/yaml_files/5g_rfsimulator/conf/flexric.conf new file mode 100644 index 0000000000..fda5bb4a7c --- /dev/null +++ b/ci-scripts/yaml_files/5g_rfsimulator/conf/flexric.conf @@ -0,0 +1,9 @@ +[NEAR-RIC] +NEAR_RIC_IP = 192.168.71.21 +NEAR_RIC_PORT = 36422 + +[XAPP] +DB_DIR = /tmp/ + +[LOG] +LEVEL = debug diff --git a/ci-scripts/yaml_files/5g_rfsimulator/mini_nonrf_config.yaml b/ci-scripts/yaml_files/5g_rfsimulator/conf/mini_nonrf_config.yaml similarity index 65% rename from ci-scripts/yaml_files/5g_rfsimulator/mini_nonrf_config.yaml rename to ci-scripts/yaml_files/5g_rfsimulator/conf/mini_nonrf_config.yaml index 10fb5bcdfd..1dd9cf229a 100644 --- a/ci-scripts/yaml_files/5g_rfsimulator/mini_nonrf_config.yaml +++ b/ci-scripts/yaml_files/5g_rfsimulator/conf/mini_nonrf_config.yaml @@ -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: general: debug @@ -103,8 +70,8 @@ amf: # This really depends on if we want to keep the "mini" version or not support_features_options: 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. - # There's no NRF in this scenario, SMF info is taken from "nfs" section. + # 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. enable_nssf: no enable_smf_selection: no relative_capacity: 30 diff --git a/ci-scripts/yaml_files/5g_rfsimulator/mysql-healthcheck.sh b/ci-scripts/yaml_files/5g_rfsimulator/conf/mysql-healthcheck.sh similarity index 100% rename from ci-scripts/yaml_files/5g_rfsimulator/mysql-healthcheck.sh rename to ci-scripts/yaml_files/5g_rfsimulator/conf/mysql-healthcheck.sh diff --git a/ci-scripts/yaml_files/5g_rfsimulator/oai_db.sql b/ci-scripts/yaml_files/5g_rfsimulator/conf/oai_db.sql similarity index 58% rename from ci-scripts/yaml_files/5g_rfsimulator/oai_db.sql rename to ci-scripts/yaml_files/5g_rfsimulator/conf/oai_db.sql index ec5e497e88..572a3e950f 100644 --- a/ci-scripts/yaml_files/5g_rfsimulator/oai_db.sql +++ b/ci-scripts/yaml_files/5g_rfsimulator/conf/oai_db.sql @@ -190,7 +190,6 @@ CREATE TABLE `users` ( LOCK TABLES `users` WRITE; /*!40000 ALTER TABLE `users` DISABLE KEYS */; -INSERT INTO `users` VALUES ('20834123456789','380561234567','35609204079300',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,'+�E��ų\0�,IH��H',0,0,00000000000000000096,'Px�X \Z1��x��','^��K�����FeU���'),('20810000001234','33611123456','35609204079299',NULL,'PURGED',120,40000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000281454575616225,'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0','�4�s@���z��~�'),('31002890832150','33638060059','35611302209414',NULL,'PURGED',120,40000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012416,'`�F�݆��D��ϛ���','�4�s@���z��~�'),('001010123456789','33600101789','35609204079298',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'\0 \n \r',1,0,00000000000000000351,'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0','L�*\\�����^��]� '),('208930000000001','33638030001','35609204079301',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208950000000002','33638050002','35609204079502',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000020471,'\0 \n \r','�4�s@���z��~�'),('208950000000003','33638050003','35609204079503',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012343,'\0 \n \r','�4�s@���z��~�'),('208950000000004','33638050004','35609204079504',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000005','33638050005','35609204079505',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000001','33638050001','35609204079501',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208950000000006','33638050006','35609204079506',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000007','33638050007','35609204079507',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208930000000002','33638030002','35609204079302',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208930000000003','33638030003','35609204079303',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208930000000004','33638030004','35609204079304',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208930000000005','33638030005','35609204079305',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208930000000006','33638030006','35609204079306',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208930000000007','33638030007','35609204079307',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000007','33638040007','35609204079407',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000006','33638040006','35609204079406',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000005','33638040005','35609204079405',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000004','33638040004','35609204079404',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000003','33638040003','35609204079403',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000002','33638040002','35609204079402',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208940000000001','33638040001','35609204079401',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'��wq��gzW�Ё��Z]','�4�s@���z��~�'),('208920100001100','33638020001','35609204079201',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001101','33638020001','35609204079201',NULL,'NOT_PURGED',120,50000000,100000000,47,0000000000,1,'��k��p~Љu{�K�',1,0,00000281044204937234,'\0 \n \r','�$I6;��+f�k�u�|�'),('208920100001102','33638020002','35609204079202',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001103','33638020003','35609204079203',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001104','33638020004','35609204079204',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001105','33638020005','35609204079205',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001106','33638020006','35609204079206',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��k��p~Љu{�K�',1,0,00000000000000006103,'ebd07771ace8677a','�$I6;��+f�k�u�|�'),('208920100001107','33638020007','35609204079207',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001108','33638020008','35609204079208',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001109','33638020009','35609204079209',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208920100001110','33638020010','35609204079210',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208930100001111','33638030011','35609304079211',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208930100001112','33638030012','35609304079212',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006103,'ebd07771ace8677a','�4�s@���z��~�'),('208930100001113','33638030013','35609304079213',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000006263,'�SNܒ�Iv��e�6','�4�s@���z��~�'),('208950000000008','33638050008','35609204079508',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000009','33638050009','35609204079509',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000010','33638050010','35609204079510',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000011','33638050011','35609204079511',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000012','33638050012','35609204079512',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000013','33638050013','35609204079513',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000014','33638050014','35609204079514',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000012215,'56f0261d9d051063','�4�s@���z��~�'),('208950000000015','33638050015','35609204079515',NULL,'PURGED',120,50000000,100000000,47,0000000000,1,'��G?/�Д���� |hb',1,0,00000000000000000000,'3536663032363164','�4�s@���z��~�'),('208920100001118','33638020010','35609204079210',NULL,'NOT_PURGED',120,50000000,100000000,47,0000000000,1,'��k��p~Љu{�K�',1,0,00000281044204934762,'~?03�u-%�ey�y�','�$I6;��+f�k�u�|�'),('208920100001121','33638020010','35609204079210',NULL,'NOT_PURGED',120,50000000,100000000,47,0000000000,1,'��k��p~Љu{�K�',1,0,00000281044204935293,'&��@xg�]���\n��Vp','�$I6;��+f�k�u�|�'),('208920100001119','33638020010','35609204079210',NULL,'NOT_PURGED',120,50000000,100000000,47,0000000000,1,'��k��p~Љu{�K�',1,0,00000281044204935293,'269482407867805d','�$I6;��+f�k�u�|�'),('208920100001120','33638020010','35609204079210',NULL,'NOT_PURGED',120,50000000,100000000,47,0000000000,1,'��k��p~Љu{�K�',1,0,00000281044204935293,'3236393438323430','�$I6;��+f�k�u�|�'); INSERT INTO `users` VALUES ('208990100001100','1','55000000000000',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0xfec86ba6eb707ed08905757b1bb44b8f,0,0,0x40,'ebd07771ace8677a',0xc42449363bbad02b66d16bc975d77cc1); INSERT INTO `users` VALUES ('208990100001101','1','55000000000000',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0xfec86ba6eb707ed08905757b1bb44b8f,0,0,0x40,'ebd07771ace8677a',0xc42449363bbad02b66d16bc975d77cc1); INSERT INTO `users` VALUES ('208990100001102','1','55000000000000',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0xfec86ba6eb707ed08905757b1bb44b8f,0,0,0x40,'ebd07771ace8677a',0xc42449363bbad02b66d16bc975d77cc1); @@ -207,16 +206,6 @@ INSERT INTO `users` VALUES ('208990100001112','1','55000000000000',NULL,'PURGED' INSERT INTO `users` VALUES ('208990100001113','1','55000000000000',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0xfec86ba6eb707ed08905757b1bb44b8f,0,0,0x40,'ebd07771ace8677a',0xc42449363bbad02b66d16bc975d77cc1); INSERT INTO `users` VALUES ('208990100001114','1','55000000000000',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0xfec86ba6eb707ed08905757b1bb44b8f,0,0,0x40,'ebd07771ace8677a',0xc42449363bbad02b66d16bc975d77cc1); INSERT INTO `users` VALUES ('208990100001115','1','55000000000000',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0xfec86ba6eb707ed08905757b1bb44b8f,0,0,0x40,'ebd07771ace8677a',0xc42449363bbad02b66d16bc975d77cc1); -INSERT INTO `users` VALUES ('208950000000031','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000032','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000033','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000034','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000035','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000036','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000037','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000038','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000039','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); -INSERT INTO `users` VALUES ('208950000000040','380561234567','55000000000001',NULL,'PURGED',50,40000000,100000000,47,0000000000,1,0x0C0A34601D4F07677303652C0462535B,0,0,0x40,'ebd07771ace8677a',0x63bfa50ee6523365ff14c1f45f88737d); /*!40000 ALTER TABLE `users` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -230,3 +219,4 @@ UNLOCK TABLES; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2016-06-28 11:41:40 + diff --git a/ci-scripts/yaml_files/5g_rfsimulator/conf/sql.cnf b/ci-scripts/yaml_files/5g_rfsimulator/conf/sql.cnf new file mode 100644 index 0000000000..ddba2223c0 --- /dev/null +++ b/ci-scripts/yaml_files/5g_rfsimulator/conf/sql.cnf @@ -0,0 +1,3 @@ +[mysqld] +wait_timeout = 15 +interactive_timeout = 15 \ No newline at end of file diff --git a/ci-scripts/yaml_files/5g_rfsimulator/docker-compose.yaml b/ci-scripts/yaml_files/5g_rfsimulator/docker-compose.yaml index 4733737b50..02c3b1ab7b 100644 --- a/ci-scripts/yaml_files/5g_rfsimulator/docker-compose.yaml +++ b/ci-scripts/yaml_files/5g_rfsimulator/docker-compose.yaml @@ -1,12 +1,13 @@ services: mysql: - container_name: "rfsim5g-mysql" + container_name: "mysql" image: mysql:8.0 volumes: - - ./oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql - - ./mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh + - ./conf/oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql + - ./conf/mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh + - ./conf/sql.cnf:/etc/mysql/conf.d/custom.cnf environment: - - TZ=Europe/Paris + - TZ=Asia/Tokyo - MYSQL_DATABASE=oai_db - MYSQL_USER=test - MYSQL_PASSWORD=test @@ -19,37 +20,40 @@ services: networks: public_net: ipv4_address: 192.168.71.131 + oai-amf: - container_name: "rfsim5g-oai-amf" + container_name: "oai-amf" image: oaisoftwarealliance/oai-amf:v2.1.9 environment: - - TZ=Europe/paris + - TZ=Asia/Tokyo volumes: - - ./mini_nonrf_config.yaml:/openair-amf/etc/config.yaml + - ./conf/mini_nonrf_config.yaml:/openair-amf/etc/config.yaml depends_on: - mysql networks: public_net: ipv4_address: 192.168.71.132 + oai-smf: - container_name: "rfsim5g-oai-smf" + container_name: "oai-smf" image: oaisoftwarealliance/oai-smf:v2.1.9 environment: - - TZ=Europe/Paris + - TZ=Asia/Tokyo volumes: - - ./mini_nonrf_config.yaml:/openair-smf/etc/config.yaml + - ./conf/mini_nonrf_config.yaml:/openair-smf/etc/config.yaml depends_on: - oai-amf networks: public_net: ipv4_address: 192.168.71.133 + oai-upf: - container_name: "rfsim5g-oai-upf" + container_name: "oai-upf" image: oaisoftwarealliance/oai-upf:v2.1.9 environment: - - TZ=Europe/Paris + - TZ=Asia/Tokyo volumes: - - ./mini_nonrf_config.yaml:/openair-upf/etc/config.yaml + - ./conf/mini_nonrf_config.yaml:/openair-upf/etc/config.yaml depends_on: - oai-smf cap_add: @@ -63,9 +67,10 @@ services: ipv4_address: 192.168.71.134 traffic_net: ipv4_address: 192.168.72.134 + oai-ext-dn: privileged: true - container_name: rfsim5g-oai-ext-dn + container_name: oai-ext-dn image: oaisoftwarealliance/trf-gen-cn5g:focal entrypoint: /bin/bash -c \ "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\ @@ -80,16 +85,15 @@ services: interval: 10s timeout: 5s retries: 5 + oai-gnb: - image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop} - container_name: rfsim5g-oai-gnb + image: nakaolab:oct1 + container_name: oai-gnb cap_drop: - ALL - environment: - USE_ADDITIONAL_OPTIONS: -E --rfsim --log_config.global_log_options level,nocolor,time - ASAN_OPTIONS: detect_leaks=0 depends_on: - 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: public_net: ipv4_address: 192.168.71.140 @@ -100,21 +104,55 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue + + oai-flexric: + 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: - ALL cap_add: - NET_ADMIN # for interface bringup - NET_RAW # for ping - 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 + # 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 depends_on: - oai-gnb networks: public_net: - ipv4_address: 192.168.71.150 + ipv4_address: 192.168.71.151 devices: - /dev/net/tun:/dev/net/tun volumes: @@ -124,40 +162,16 @@ services: interval: 10s timeout: 5s retries: 5 + oai-nr-ue2: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue2 + image: nakaolab:oct1 + 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: - ALL cap_add: - NET_ADMIN # for interface bringup - 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: - oai-gnb networks: @@ -172,16 +186,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue4: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue4 + + oai-nr-ue3: + 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: - ALL cap_add: - NET_ADMIN # for interface bringup - 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: - oai-gnb networks: @@ -196,16 +210,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue5: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue5 + + 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 208990100001103 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"] + container_name: oai-nr-ue4 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 208990100001104 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time depends_on: - oai-gnb networks: @@ -220,16 +234,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue6: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue6 + + 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 208990100001104 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"] + container_name: oai-nr-ue5 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 208990100001105 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time depends_on: - oai-gnb networks: @@ -244,16 +258,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue7: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue7 + + 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 208990100001105 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"] + container_name: oai-nr-ue6 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 208990100001106 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time depends_on: - oai-gnb networks: @@ -268,16 +282,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue8: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue8 + + 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 208990100001106 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"] + container_name: oai-nr-ue7 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 208990100001107 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time depends_on: - oai-gnb networks: @@ -292,16 +306,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue9: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue9 + + 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 208990100001107 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"] + container_name: oai-nr-ue8 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 208990100001108 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time depends_on: - oai-gnb networks: @@ -316,16 +330,16 @@ services: interval: 10s timeout: 5s retries: 5 - oai-nr-ue10: - image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} - container_name: rfsim5g-oai-nr-ue10 + + 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 208990100001108 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time"] + container_name: oai-nr-ue9 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 208990100001109 -C 3319680000 --rfsimulator.serveraddr 192.168.71.140 --log_config.global_log_options level,nocolor,time depends_on: - oai-gnb networks: @@ -341,13 +355,60 @@ services: timeout: 5s 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: public_net: driver: bridge name: rfsim5g-oai-public-net ipam: config: - - subnet: 192.168.71.128/26 + - subnet: 192.168.71.0/24 driver_opts: com.docker.network.bridge.name: "rfsim5g-public" traffic_net: diff --git a/ci-scripts/yaml_files/5g_rfsimulator/exp.sh b/ci-scripts/yaml_files/5g_rfsimulator/exp.sh new file mode 100755 index 0000000000..baee1a0127 --- /dev/null +++ b/ci-scripts/yaml_files/5g_rfsimulator/exp.sh @@ -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 "$@" diff --git a/ci-scripts/yaml_files/5g_rfsimulator/local-override-ue-gdb.yaml b/ci-scripts/yaml_files/5g_rfsimulator/local-override-ue-gdb.yaml deleted file mode 100644 index 0b2b60210a..0000000000 --- a/ci-scripts/yaml_files/5g_rfsimulator/local-override-ue-gdb.yaml +++ /dev/null @@ -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 diff --git a/ci-scripts/yaml_files/5g_rfsimulator/local-override.yaml b/ci-scripts/yaml_files/5g_rfsimulator/local-override.yaml deleted file mode 100644 index 40d9b7ecc9..0000000000 --- a/ci-scripts/yaml_files/5g_rfsimulator/local-override.yaml +++ /dev/null @@ -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/ diff --git a/ci-scripts/yaml_files/5g_rfsimulator/memo.txt b/ci-scripts/yaml_files/5g_rfsimulator/memo.txt new file mode 100644 index 0000000000..55c22e92bc --- /dev/null +++ b/ci-scripts/yaml_files/5g_rfsimulator/memo.txt @@ -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 + diff --git a/ci-scripts/yaml_files/5g_rfsimulator/oai-end-to-end.jpg b/ci-scripts/yaml_files/5g_rfsimulator/oai-end-to-end.jpg deleted file mode 100644 index f454a016f624196c87baa431f5974d148ec677be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56238 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 yfY{p};9y!mWx\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)\]") +RNTI_RE = re.compile(r"\bRNTI=(?P\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() diff --git a/ci-scripts/yaml_files/5g_rfsimulator/prediction.py b/ci-scripts/yaml_files/5g_rfsimulator/prediction.py new file mode 100644 index 0000000000..3fd9c971b7 --- /dev/null +++ b/ci-scripts/yaml_files/5g_rfsimulator/prediction.py @@ -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.*?)\] + \s+UE\s+RNTI=(?P\d+) + \s+DL_TBS=(?P\d+) + \s+UL_TBS=(?P\d+) + \s+DL_SDUs=(?P\d+) + \s+UL_SDUs=(?P\d+) + \s+DL_sched_RBs=(?P\d+) + \s+UL_sched_RBs=(?P\d+) + \s+DL_PRBs=(?P\d+) + \s+UL_PRBs=(?P\d+) + \s+DL_BLER=(?P[\d.]+) + \s+UL_BLER=(?P[\d.]+) + \s+PUSCH_SNR=(?P[\d.]+) + \s+PUCCH_SNR=(?P[\d.]+) + \s+WB_CQI=(?P\d+) + \s+DL_MCS1=(?P\d+) + \s+UL_MCS1=(?P\d+) + \s+DL_HARQ=(?P\d+) + \s+UL_HARQ=(?P\d+) + \s+BSR=(?P\d+) + \s+PHR=(?P\d+) + """, re.VERBOSE, +) +RRC_PATTERNS: List[re.Pattern] = [ + re.compile(r"\[(?P[^]]+)\].*RNTI=(?P[0-9a-fA-F]+).*RRC[_ ]?MSG=(?P[A-Za-z_]+)", re.IGNORECASE), + re.compile(r"\[(?P[^]]+)\].*rrc[=:](?P[A-Za-z_]+).*rnti[=:](?P[0-9a-fA-F]+)", re.IGNORECASE), + re.compile(r"\[(?P[^]]+)\].*(?PRRC[A-Za-z]+).*RNTI=(?P[0-9a-fA-F]+)", re.IGNORECASE), + re.compile(r"\|\s*(?P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?).*?RNTI\s*(?P[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() diff --git a/ci-scripts/yaml_files/5g_rfsimulator_flexric/docker-compose.yml b/ci-scripts/yaml_files/5g_rfsimulator_flexric/docker-compose.yml index 6b9771e1c1..2e4766e747 100644 --- a/ci-scripts/yaml_files/5g_rfsimulator_flexric/docker-compose.yml +++ b/ci-scripts/yaml_files/5g_rfsimulator_flexric/docker-compose.yml @@ -80,27 +80,7 @@ services: interval: 10s timeout: 5s - oai-gnb: - 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: + oai-nr-ue1: image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} privileged: true container_name: rfsim5g-oai-nr-ue @@ -138,7 +118,154 @@ services: timeout: 5s retries: 5 -networks: - public_net: - name: oai-cn5g-public-net - external: true + oai-nr-ue3: + image: ${REGISTRY:-oaisoftwarealliance}/${NRUE_IMG:-oai-nr-ue}:${TAG:-develop} + privileged: 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 diff --git a/docker/Dockerfile.base.ubuntu b/docker/Dockerfile.base.ubuntu new file mode 100644 index 0000000000..2928040331 --- /dev/null +++ b/docker/Dockerfile.base.ubuntu @@ -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 diff --git a/docker/Dockerfile.base.ubuntu22 b/docker/Dockerfile.base.ubuntu22 deleted file mode 100644 index 60e2aad8ab..0000000000 --- a/docker/Dockerfile.base.ubuntu22 +++ /dev/null @@ -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 diff --git a/docker/Dockerfile.build.ubuntu22 b/docker/Dockerfile.build.ubuntu similarity index 85% rename from docker/Dockerfile.build.ubuntu22 rename to docker/Dockerfile.build.ubuntu index a3c2417b33..b2cc9c6888 100644 --- a/docker/Dockerfile.build.ubuntu22 +++ b/docker/Dockerfile.build.ubuntu @@ -21,7 +21,7 @@ #--------------------------------------------------------------------- # # 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 . . # 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 && \ mkdir -p log && \ ./build_oai -c --ninja \ @@ -54,12 +59,13 @@ RUN /bin/sh oaienv && \ ldd ran_build/build/*.so ## 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 && \ cmake -GNinja -DCMAKE_BUILD_TYPE=Release \ -DKPM_VERSION=$KPM_VERSION \ - -DE2AP_VERSION=$E2AP_VERSION \ - -DCMAKE_C_COMPILER=gcc-12 \ - -DCMAKE_CXX_COMPILER=g++-12 .. && \ + -DE2AP_VERSION=$E2AP_VERSION .. && \ ninja && \ ninja install diff --git a/docker/Dockerfile.nakaolab.oct1 b/docker/Dockerfile.nakaolab.oct1 new file mode 100644 index 0000000000..3a525b008b --- /dev/null +++ b/docker/Dockerfile.nakaolab.oct1 @@ -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"] \ No newline at end of file diff --git a/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm.c b/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm.c index 4468cdf0b9..ea5c683a37 100644 --- a/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm.c +++ b/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_func_kpm.c @@ -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); uint8_t *buf = test_cond_value->octet_string_value->buf; *sst = buf[0]; - *sd = malloc(**sd); + *sd = malloc(sizeof(uint32_t)); **sd = buf[1] << 16 | buf[2] << 8 | buf[3]; } printf("[E2 AGENT][E2SM-KPM] Condition NSSAI (%d, %x).\n", *sst, (*sd) ? **sd : 0xffffff); diff --git a/openair2/E2AP/flexric b/openair2/E2AP/flexric index df754a8553..0a31810d47 160000 --- a/openair2/E2AP/flexric +++ b/openair2/E2AP/flexric @@ -1 +1 @@ -Subproject commit df754a85537f6db255b5330b8d83eda6a2742f25 +Subproject commit 0a31810d47c22f76e539faef6cb43ec6764156a7 diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c index cb3c1bf801..4328fd1ac2 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch.c @@ -26,7 +26,6 @@ * \email: guido.casati@iis.fraunhofe.de * \version 1.0 * @ingroup _mac - */ #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); 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 */ 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)) 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)) - max_rbSize++; + /* while (rbStart + max_rbSize <= rbStop && !(rballoc_mask[rbStart + max_rbSize + bwp_start] & slbitmap)) */ + /* max_rbSize++; */ if (max_rbSize < min_rbSize) { 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); pf_tbs[mcsTableIdx][mcs] = nr_compute_tbs(Qm, R, - 1, /* rbSize */ + 1, /* rbSize */ 10, /* hypothetical number of slots */ - 0, /* N_PRB_DMRS * N_DMRS_SLOT */ - 0 /* N_PRB_oh, 0 for initialBWP */, - 0 /* tb_scaling */, - 1 /* nrOfLayers */) >> 3; + 0, /* N_PRB_DMRS * N_DMRS_SLOT */ + 0 /* N_PRB_oh, 0 for initialBWP */, + 0 /* tb_scaling */, + 1 /* nrOfLayers */ + ) >> 3; } } @@ -1374,6 +1374,7 @@ void nr_schedule_ue_spec(module_id_t module_id, buf=bufEnd; } + // mac_stats UE->mac_stats.dl.total_bytes += TBS; UE->mac_stats.dl.current_bytes = TBS; UE->mac_stats.dl.total_rbs += sched_pdsch->rbSize; diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c index 3fa16f6a51..2561b618f2 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c @@ -2151,9 +2151,9 @@ static void pf_ul(module_id_t module_id, while (rbStart < bwpSize && (rballoc_mask[rbStart + bwpStart] & slbitmap)) rbStart++; sched_pusch->rbStart = rbStart; - uint16_t max_rbSize = 1; - while (rbStart + max_rbSize < bwpSize && !(rballoc_mask[rbStart + bwpStart + max_rbSize] & slbitmap)) - max_rbSize++; + uint16_t max_rbSize = 10; // used to be 1 + /* while (rbStart + max_rbSize < bwpSize && !(rballoc_mask[rbStart + bwpStart + max_rbSize] & slbitmap)) */ + /* max_rbSize++; */ 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); diff --git a/tools/scripts/traffic_runner.sh b/tools/scripts/traffic_runner.sh new file mode 100644 index 0000000000..c18c18f562 --- /dev/null +++ b/tools/scripts/traffic_runner.sh @@ -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 < [--duration ] [--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