From 31b8d98bb543cec5aa4257a0f42406fc1ed0e9b2 Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Mon, 9 Dec 2024 12:45:42 +0100 Subject: [PATCH 01/18] Formating for PHY_ofdm_mod --- openair1/PHY/MODULATION/ofdm_mod.c | 119 ++++++++++++++--------------- 1 file changed, 57 insertions(+), 62 deletions(-) diff --git a/openair1/PHY/MODULATION/ofdm_mod.c b/openair1/PHY/MODULATION/ofdm_mod.c index de19d30819..4d0abf7b61 100644 --- a/openair1/PHY/MODULATION/ofdm_mod.c +++ b/openair1/PHY/MODULATION/ofdm_mod.c @@ -122,36 +122,43 @@ void nr_normal_prefix_mod(c16_t *txdataF, c16_t *txdata, uint8_t nsymb, const NR } -void PHY_ofdm_mod(int *input, /// pointer to complex input - int *output, /// pointer to complex output - int fftsize, /// FFT_SIZE - unsigned char nb_symbols, /// number of OFDM symbols - unsigned short nb_prefix_samples, /// cyclic prefix length - Extension_t etype /// type of extension - ) +void PHY_ofdm_mod(int *input, /// pointer to complex input + int *output, /// pointer to complex output + int fftsize, /// FFT_SIZE + unsigned char nb_symbols, /// number of OFDM symbols + unsigned short nb_prefix_samples, /// cyclic prefix length + Extension_t etype /// type of extension +) { + if (nb_symbols == 0) + return; - if(nb_symbols == 0) return; + int16_t temp[2 * 2 * 6144 * 4] __attribute__((aligned(32))); + int i, j; - int16_t temp[2*2*6144*4] __attribute__((aligned(32))); - int i,j; + volatile int *output_ptr = (int *)0; - volatile int *output_ptr=(int*)0; - - int *temp_ptr=(int*)0; + int *temp_ptr = (int *)0; idft_size_idx_t idft_size = get_idft(fftsize); #ifdef DEBUG_OFDM_MOD printf("[PHY] OFDM mod (size %d,prefix %d) Symbols %d, input %p, output %p\n", - fftsize,nb_prefix_samples,nb_symbols,input,output); + fftsize, + nb_prefix_samples, + nb_symbols, + input, + output); #endif - - - for (i=0; i %p)\n",i,nb_symbols,i*fftsize+(i*nb_prefix_samples),input,&input[i*fftsize],&output[(i*fftsize) + ((i)*nb_prefix_samples)]); + printf("[PHY] symbol %d/%d offset %d (%p,%p -> %p)\n", + i, + nb_symbols, + i * fftsize + (i * nb_prefix_samples), + input, + &input[i * fftsize], + &output[(i * fftsize) + ((i)*nb_prefix_samples)]); #endif // on AVX2 need 256-bit alignment @@ -161,70 +168,58 @@ void PHY_ofdm_mod(int *input, /// pointer to complex input // Note: will have to adjust for synchronization offset! switch (etype) { - case CYCLIC_PREFIX: - output_ptr = &output[(i*fftsize) + ((1+i)*nb_prefix_samples)]; - temp_ptr = (int *)temp; + case CYCLIC_PREFIX: + output_ptr = &output[(i * fftsize) + ((1 + i) * nb_prefix_samples)]; + temp_ptr = (int *)temp; + // msg("Doing cyclic prefix method\n"); - // msg("Doing cyclic prefix method\n"); + { + memcpy((void *)output_ptr, (void *)temp_ptr, fftsize << 2); + } + memcpy((void *)&output_ptr[-nb_prefix_samples], (void *)&output_ptr[fftsize - nb_prefix_samples], nb_prefix_samples << 2); + break; - { - memcpy((void*)output_ptr,(void*)temp_ptr,fftsize<<2); - } - memcpy((void*)&output_ptr[-nb_prefix_samples],(void*)&output_ptr[fftsize-nb_prefix_samples],nb_prefix_samples<<2); - break; + case CYCLIC_SUFFIX: - case CYCLIC_SUFFIX: + output_ptr = &output[(i * fftsize) + (i * nb_prefix_samples)]; + temp_ptr = (int *)temp; - output_ptr = &output[(i*fftsize)+ (i*nb_prefix_samples)]; + // msg("Doing cyclic suffix method\n"); - temp_ptr = (int *)temp; + for (j = 0; j < fftsize; j++) { + output_ptr[j] = temp_ptr[2 * j]; + } - // msg("Doing cyclic suffix method\n"); + for (j = 0; j < nb_prefix_samples; j++) + output_ptr[fftsize + j] = output_ptr[j]; - for (j=0; j Date: Mon, 9 Dec 2024 12:56:56 +0100 Subject: [PATCH 02/18] Optimize PHY_ofdm_mod CYCLIC_PREFIX in case of incidentally aligned pointers It seems that the cyclic prefix is in most cases a multiple of 512 samples. This means that in most cases the idft output pointer is already aligned and there is no need to perform an extra memcpy. This saves the memcpy time in most cyclic prefix insertion cases. --- openair1/PHY/MODULATION/ofdm_mod.c | 57 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/openair1/PHY/MODULATION/ofdm_mod.c b/openair1/PHY/MODULATION/ofdm_mod.c index 4d0abf7b61..61487eb433 100644 --- a/openair1/PHY/MODULATION/ofdm_mod.c +++ b/openair1/PHY/MODULATION/ofdm_mod.c @@ -133,12 +133,6 @@ void PHY_ofdm_mod(int *input, /// pointer to complex input if (nb_symbols == 0) return; - int16_t temp[2 * 2 * 6144 * 4] __attribute__((aligned(32))); - int i, j; - - volatile int *output_ptr = (int *)0; - - int *temp_ptr = (int *)0; idft_size_idx_t idft_size = get_idft(fftsize); #ifdef DEBUG_OFDM_MOD @@ -150,7 +144,7 @@ void PHY_ofdm_mod(int *input, /// pointer to complex input output); #endif - for (i = 0; i < nb_symbols; i++) { + for (int i = 0; i < nb_symbols; i++) { #ifdef DEBUG_OFDM_MOD printf("[PHY] symbol %d/%d offset %d (%p,%p -> %p)\n", i, @@ -162,57 +156,62 @@ void PHY_ofdm_mod(int *input, /// pointer to complex input #endif // on AVX2 need 256-bit alignment - idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); // Copy to frame buffer with Cyclic Extension // Note: will have to adjust for synchronization offset! switch (etype) { - case CYCLIC_PREFIX: - output_ptr = &output[(i * fftsize) + ((1 + i) * nb_prefix_samples)]; - temp_ptr = (int *)temp; - - // msg("Doing cyclic prefix method\n"); - - { - memcpy((void *)output_ptr, (void *)temp_ptr, fftsize << 2); + case CYCLIC_PREFIX: { + int *output_ptr = &output[(i * fftsize) + ((1 + i) * nb_prefix_samples)]; + if ((uintptr_t)output_ptr % 32 == 0) { + // output ptr is aligned, do ifft inplace + idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)output_ptr, 1); + } else { + // output ptr is not aligned, needs an extra memcpy + c16_t temp[fftsize] __attribute__((aligned(32))); + idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); + memcpy((void *)output_ptr, (void *)temp, fftsize << 2); } + // perform cyclic prefix insertion memcpy((void *)&output_ptr[-nb_prefix_samples], (void *)&output_ptr[fftsize - nb_prefix_samples], nb_prefix_samples << 2); break; + } - case CYCLIC_SUFFIX: + case CYCLIC_SUFFIX: { + c16_t temp[fftsize] __attribute__((aligned(32))); + idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); + int *output_ptr = &output[(i * fftsize) + (i * nb_prefix_samples)]; - output_ptr = &output[(i * fftsize) + (i * nb_prefix_samples)]; - - temp_ptr = (int *)temp; + int *temp_ptr = (int *)temp; // msg("Doing cyclic suffix method\n"); - for (j = 0; j < fftsize; j++) { + for (int j = 0; j < fftsize; j++) { output_ptr[j] = temp_ptr[2 * j]; } - for (j = 0; j < nb_prefix_samples; j++) + for (int j = 0; j < nb_prefix_samples; j++) output_ptr[fftsize + j] = output_ptr[j]; - break; + } case ZEROS: break; - case NONE: - + case NONE: { + c16_t temp[fftsize] __attribute__((aligned(32))); + idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); // msg("NO EXTENSION!\n"); - output_ptr = &output[fftsize]; + int *output_ptr = &output[fftsize]; - temp_ptr = (int *)temp; + int *temp_ptr = (int *)temp; - for (j = 0; j < fftsize; j++) { + for (int j = 0; j < fftsize; j++) { output_ptr[j] = temp_ptr[2 * j]; } - break; + } default: break; From b92ee59d419443efca4f423760e97d3af0c8bb5f Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Mon, 16 Dec 2024 15:21:11 +0100 Subject: [PATCH 03/18] Fix instrumentation target for old CMake versions Older make versions do not support source files to be added to INTERFACE type target. The source file is not needed there so it was removed. --- common/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 229c93b043..f58880b04a 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -3,7 +3,7 @@ add_subdirectory(config/yaml) configure_file(oai_version.h.in oai_version.h @ONLY) -add_library(instrumentation INTERFACE instrumentation.h) +add_library(instrumentation INTERFACE) target_include_directories(instrumentation INTERFACE .) if (TRACY_ENABLE) target_link_libraries(instrumentation INTERFACE Tracy::TracyClient) From 5ab9bd95bd29a4a0ed63c29abee7720a71e18eaf Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Mon, 16 Dec 2024 15:22:09 +0100 Subject: [PATCH 04/18] Add `const` to input argument in PHY_ofdm_mod --- openair1/PHY/MODULATION/modulation_common.h | 2 +- openair1/PHY/MODULATION/ofdm_mod.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openair1/PHY/MODULATION/modulation_common.h b/openair1/PHY/MODULATION/modulation_common.h index a131cdae63..df521b10da 100644 --- a/openair1/PHY/MODULATION/modulation_common.h +++ b/openair1/PHY/MODULATION/modulation_common.h @@ -38,7 +38,7 @@ @param nb_prefix_samples The number of prefix/suffix/zero samples @param etype Type of extension (CYCLIC_PREFIX,CYCLIC_SUFFIX,ZEROS) */ -void PHY_ofdm_mod(int *input, +void PHY_ofdm_mod(const int *input, int *output, int fftsize, unsigned char nb_symbols, diff --git a/openair1/PHY/MODULATION/ofdm_mod.c b/openair1/PHY/MODULATION/ofdm_mod.c index 61487eb433..31c056b712 100644 --- a/openair1/PHY/MODULATION/ofdm_mod.c +++ b/openair1/PHY/MODULATION/ofdm_mod.c @@ -122,7 +122,7 @@ void nr_normal_prefix_mod(c16_t *txdataF, c16_t *txdata, uint8_t nsymb, const NR } -void PHY_ofdm_mod(int *input, /// pointer to complex input +void PHY_ofdm_mod(const int *input, /// pointer to complex input int *output, /// pointer to complex output int fftsize, /// FFT_SIZE unsigned char nb_symbols, /// number of OFDM symbols From 9e6c0c8c78848b2a6343ca974c83c87394e38715 Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Mon, 16 Dec 2024 15:37:56 +0100 Subject: [PATCH 05/18] Fix unused code paths in PHY_ofdm_mod For completeness sake, unused CYCLIC_SUFFIX and NONE extension types were fixed to match CYCLIC_PREFIX memcpy usage. --- openair1/PHY/MODULATION/ofdm_mod.c | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/openair1/PHY/MODULATION/ofdm_mod.c b/openair1/PHY/MODULATION/ofdm_mod.c index 31c056b712..595c4ce996 100644 --- a/openair1/PHY/MODULATION/ofdm_mod.c +++ b/openair1/PHY/MODULATION/ofdm_mod.c @@ -170,10 +170,10 @@ void PHY_ofdm_mod(const int *input, /// pointer to complex input // output ptr is not aligned, needs an extra memcpy c16_t temp[fftsize] __attribute__((aligned(32))); idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); - memcpy((void *)output_ptr, (void *)temp, fftsize << 2); + memcpy((void *)output_ptr, (void *)temp, sizeof(temp)); } // perform cyclic prefix insertion - memcpy((void *)&output_ptr[-nb_prefix_samples], (void *)&output_ptr[fftsize - nb_prefix_samples], nb_prefix_samples << 2); + memcpy((void *)&output_ptr[-nb_prefix_samples], (void *)&output_ptr[fftsize - nb_prefix_samples], nb_prefix_samples * sizeof(c16_t)); break; } @@ -181,17 +181,8 @@ void PHY_ofdm_mod(const int *input, /// pointer to complex input c16_t temp[fftsize] __attribute__((aligned(32))); idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); int *output_ptr = &output[(i * fftsize) + (i * nb_prefix_samples)]; - - int *temp_ptr = (int *)temp; - - // msg("Doing cyclic suffix method\n"); - - for (int j = 0; j < fftsize; j++) { - output_ptr[j] = temp_ptr[2 * j]; - } - - for (int j = 0; j < nb_prefix_samples; j++) - output_ptr[fftsize + j] = output_ptr[j]; + memcpy(output_ptr, temp, sizeof(temp)); + memcpy(&output_ptr[fftsize], temp, nb_prefix_samples * sizeof(c16_t)); break; } @@ -202,14 +193,8 @@ void PHY_ofdm_mod(const int *input, /// pointer to complex input case NONE: { c16_t temp[fftsize] __attribute__((aligned(32))); idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); - // msg("NO EXTENSION!\n"); - int *output_ptr = &output[fftsize]; - - int *temp_ptr = (int *)temp; - - for (int j = 0; j < fftsize; j++) { - output_ptr[j] = temp_ptr[2 * j]; - } + int *output_ptr = &output[i * fftsize]; + memcpy(output_ptr, temp, sizeof(temp)); break; } From 28b7e0b7d50c422d07dd24a6f62cdd2c84625b83 Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Tue, 10 Dec 2024 12:53:28 +0100 Subject: [PATCH 06/18] UL BLER vs SNR plot Added a tool for generating UL BLER vs SNR from nr_ulsim runs. --- tools/plots/README.md | 21 +++++ tools/plots/example.png | Bin 0 -> 40957 bytes tools/plots/requirements.txt | 2 + tools/plots/ul_bler_vs_snr_graph.py | 128 ++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 tools/plots/README.md create mode 100644 tools/plots/example.png create mode 100644 tools/plots/requirements.txt create mode 100755 tools/plots/ul_bler_vs_snr_graph.py diff --git a/tools/plots/README.md b/tools/plots/README.md new file mode 100644 index 0000000000..5d5175a882 --- /dev/null +++ b/tools/plots/README.md @@ -0,0 +1,21 @@ +# Plotting tools + +## `ul_bler_vs_snr_graph.py` + +This generates UL BLER vs SNR plots using nr_ulsim. It uses cache so subsequent runs are faster. +Assumes your nr_ulsim is in `../../cmake_targets/ran_build/build`. Run with -h flag to see options + +Example graph: + +![image](./example.png) + +### Cache usage and modifting `nr_ulsim` command + +Modify the script call to `nr_ulsim` if you want a different channel model or other flags in the `nr_ulsim` command. +Remember to clear the cache (remove `cache.pkl` from this folder) so that the results from a different run are not +taken from the cache. + +Same goes for any software modifications of `nr_ulsim` - the cache is not aware of the software version used to generate +the data - remember to clear your cache manually if you are comparing different `nr_ulsim` versions. + +The script also allows to skip usage of the cache with `--rerun` option. diff --git a/tools/plots/example.png b/tools/plots/example.png new file mode 100644 index 0000000000000000000000000000000000000000..56c8af15dfd54316f0376e0a79dd571692ce1c49 GIT binary patch literal 40957 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 literal 0 HcmV?d00001 diff --git a/tools/plots/requirements.txt b/tools/plots/requirements.txt new file mode 100644 index 0000000000..dbfeaf55f1 --- /dev/null +++ b/tools/plots/requirements.txt @@ -0,0 +1,2 @@ +matplotlib==3.8.3 +numpy==1.21.5 diff --git a/tools/plots/ul_bler_vs_snr_graph.py b/tools/plots/ul_bler_vs_snr_graph.py new file mode 100755 index 0000000000..83e7aa5093 --- /dev/null +++ b/tools/plots/ul_bler_vs_snr_graph.py @@ -0,0 +1,128 @@ +#!/bin/python3 +import os +import subprocess +import numpy as np +import re +import matplotlib.pyplot as plt +import matplotlib.cm as cm +import pickle +import argparse +import concurrent.futures +import traceback +import multiprocessing + +default_ulsim_cmd = "../../cmake_targets/ran_build/build/nr_ulsim -i 1,0 -t 100 -R 273" +ulsim_args = "-n {} -s {} -S {} -m {} -r {}" + +def find_throughput(line): + ### Example line + ### SNR 10.000000: Channel BLER (0.000000e+00,-nan,-nan,-nan Channel BER (1.548551e-03,-nan,-nan,-nan) Avg round 1.00, Eff Rate 1672.0000 bits/slot, Eff Throughput 100.00, TBS 1672 bits/slot + ### SNR -0.000000: Channel BLER (0.000000e+00,-nan,-nan,-nan Channel BER (1.888043e-01,-nan,-nan,-nan) Avg round 1.00, Eff Rate 2152.0000 bits/slot, Eff Throughput 100.00, TBS 2152 bits/slot + pattern = r"SNR (-?\d+.\d+).*BLER \((-?\d+\.\d*(?:[eE][+-]\d+)?),.*Eff Throughput (\d+.\d+)" + match = re.search(pattern, line) + if match: + snr, bler, thp = match.groups() + return float(snr), float(bler), float(thp) + return None, None, None + +def generate_mcs_thps(ulsim_cmd_with_args, snr, mcs, num_runs = 10, num_rbs = 50, cache = {}): + thp = 0 + bler = 0 + num_runs_cached = 0 + cache_string = "{},{},{}".format(snr, mcs, num_rbs) + if cache_string in cache: + thp, bler, num_runs_cached = cache[cache_string] + if num_runs_cached >= num_runs: + return bler, thp + + num_runs_left = num_runs - num_runs_cached + + ulsim_cmd_formatted = ulsim_cmd_with_args.format(num_runs_left, snr, snr+0.01, mcs, num_rbs) + print(ulsim_cmd_formatted, "result insufficient in cache, running") + process = subprocess.Popen(ulsim_cmd_formatted.split(" "), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout_data = process.communicate()[0] + output = "" + if stdout_data is not None: + output = stdout_data.decode() + for line in output.splitlines(): + _snr, _bler, _thp = find_throughput(line) + if _snr is not None: + bler = (bler * num_runs_cached + _bler * num_runs_left) / num_runs + thp = (thp * num_runs_cached + _thp * num_runs_left) / num_runs + cache[cache_string] = thp, bler, num_runs + return bler, thp + print("error processing "+ ulsim_cmd_with_args) + print(output) + print(process.communicate()[1].decode()) + return None, None + + + +parser = argparse.ArgumentParser(description="UL SNR/BLER plot generator") +parser.add_argument("--snr-start", help="Starting SNR value") +parser.add_argument("--snr-stop", help="Final SNR value") +parser.add_argument("--snr-step", help="SNR step value") +parser.add_argument("--mcs-start", help="First MCS to test") +parser.add_argument("--mcs-stop", help="Last MCS to test") +parser.add_argument("--num-runs", help="Number of simulations per SNR/MCS value") +parser.add_argument("--rerun", help="Do not use cache to read data, run from scrach", action="store_true") +parser.add_argument("--num-rbs", help="Number of RBs") +parser.add_argument("--ulsim-cmd", help="ULSIM command to run. Do not use `sSrnm` flags, as these are used by the script", default=default_ulsim_cmd) + +cpu_count = multiprocessing.cpu_count() +cpu_to_use = max(1, cpu_count - 2) + +parser.add_argument("--num-threads", help="Number of threads to run", default=cpu_to_use) +args = parser.parse_args() + +_snr_start = float(args.snr_start) +_snr_step = float(args.snr_step) +_snr_end = float(args.snr_stop) + _snr_step/2 +_mcs_start = int(args.mcs_start) +_mcs_stop = int(args.mcs_stop) +_num_runs = int(args.num_runs) +_num_rbs = int(args.num_rbs) +ulsim_cmd = args.ulsim_cmd + " " + ulsim_args +num_threads = int(args.num_threads) + +cmap = cm.viridis +plt.figure() +plt.ylim(-0.1, 1.1) +plt.title("BLER vs SNR for MCS{} to MCS{}, nRB={} (num_runs = {})".format(_mcs_start, _mcs_stop, _num_rbs, _num_runs)) +plt.xlabel("SNR [dB]") +plt.ylabel("BLER") + +cache = {} +if not args.rerun: + try: + with open("cache.pkl", "rb+") as f: + cache = pickle.load(f) + except FileNotFoundError: + pass # no cache found + +snrs_list = np.arange(_snr_start, _snr_end, _snr_step) +mcss_list = np.arange(_mcs_start, _mcs_stop + 1, 1) +plot_index = 0 +for mcs in mcss_list: + results = {} + with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: + result_to_snr_index = {executor.submit(generate_mcs_thps, ulsim_cmd, snrs_list[snr_index], mcs, _num_runs, _num_rbs, cache): snr_index for (snr_index, _) in enumerate(snrs_list)} + for future in concurrent.futures.as_completed(result_to_snr_index): + snr_index = result_to_snr_index[future] + bler, thp = future.result() + results[snr_index] = bler + blers = np.zeros_like(snrs_list) + for snr_index in results: + blers[snr_index] = results[snr_index] + + num_plots = len(mcss_list) + percentage = float(plot_index) / max(1, (num_plots - 1)) + color = cmap(percentage) + plot_index += 1 + plt.plot(snrs_list, blers, label="MCS{}".format(mcs), color = color) + plt.legend() + plt.savefig('MCS={}-{},SNR={}-{},RBs={}graph.png'.format(_mcs_start, _mcs_stop, _snr_start, _snr_end, _num_rbs)) +if not args.rerun: + with open("cache.pkl", "wb+") as f: + pickle.dump(cache, f) + From c66a625dc5660555e9ba80cfc35df71a37dc077d Mon Sep 17 00:00:00 2001 From: rmagueta Date: Mon, 9 Dec 2024 17:13:32 +0000 Subject: [PATCH 07/18] If the ul_ri of srs_feedback changes between a UL 1st transmission and a retransmission, we use the TPMI of 1st transmission --- openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c | 107 +++++++++--------- openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.h | 1 + openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c | 1 + .../NR_MAC_gNB/gNB_scheduler_primitives.c | 2 + .../LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c | 7 ++ openair2/LAYER2/NR_MAC_gNB/mac_proto.h | 1 + openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h | 2 + 7 files changed, 69 insertions(+), 52 deletions(-) diff --git a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c index 08cfd7303c..6c15ec2943 100644 --- a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c +++ b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.c @@ -2822,6 +2822,7 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, dci_field_t srs_resource_indicator, nr_srs_feedback_t *srs_feedback, const uint8_t *nrOfLayers, + int *tpmi, uint32_t *val) { // It is only applicable to codebook based transmission. This field occupies 0 bits for non-codebook based @@ -2841,6 +2842,7 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, long max_rank = *pusch_Config->maxRank; long *ul_FullPowerTransmission = pusch_Config->ext1 ? pusch_Config->ext1->ul_FullPowerTransmission_r16 : NULL; long *codebookSubset = pusch_Config->codebookSubset; + int ul_tpmi = tpmi ? *tpmi : srs_feedback ? srs_feedback->tpmi : -1; if (pusch_antenna_ports == 2) { @@ -2854,21 +2856,21 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, if (ul_FullPowerTransmission && *ul_FullPowerTransmission == NR_PUSCH_Config__ext1__ul_FullPowerTransmission_r16_fullpowerMode1) { nbits = 2; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 2,"TPMI %d is invalid!\n", srs_feedback->tpmi); - *val = srs_feedback->tpmi; + AssertFatal(ul_tpmi <= 2,"TPMI %d is invalid!\n", ul_tpmi); + *val = ul_tpmi; } } else { if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 1; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 1,"TPMI %d is invalid!\n", srs_feedback->tpmi); - *val = srs_feedback->tpmi; + AssertFatal(ul_tpmi <= 1,"TPMI %d is invalid!\n", ul_tpmi); + *val = ul_tpmi; } } else { nbits = 3; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 5,"TPMI %d is invalid!\n", srs_feedback->tpmi); - *val = srs_feedback->tpmi; + AssertFatal(ul_tpmi <= 5,"TPMI %d is invalid!\n", ul_tpmi); + *val = ul_tpmi; } } } @@ -2881,25 +2883,25 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, if (ul_FullPowerTransmission && *ul_FullPowerTransmission == NR_PUSCH_Config__ext1__ul_FullPowerTransmission_r16_fullpowerMode1) { nbits = 2; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 2) || (*nrOfLayers==2 && srs_feedback->tpmi == 0), - "TPMI %d is invalid!\n", srs_feedback->tpmi); - *val = *nrOfLayers==1 ? table_7_3_1_1_2_4A_1layer[srs_feedback->tpmi] : 2; + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 2) || (*nrOfLayers==2 && ul_tpmi == 0), + "TPMI %d is invalid!\n", ul_tpmi); + *val = *nrOfLayers==1 ? table_7_3_1_1_2_4A_1layer[ul_tpmi] : 2; } } else { if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 2; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 1) || (*nrOfLayers==2 && srs_feedback->tpmi == 0), - "TPMI %d is invalid!\n", srs_feedback->tpmi); - *val = *nrOfLayers==1 ? srs_feedback->tpmi : 2; + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 1) || (*nrOfLayers==2 && ul_tpmi == 0), + "TPMI %d is invalid!\n", ul_tpmi); + *val = *nrOfLayers==1 ? ul_tpmi : 2; } } else { nbits = 4; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 5) || (*nrOfLayers==2 && srs_feedback->tpmi <= 2), - "TPMI %d is invalid!\n", srs_feedback->tpmi); - *val = *nrOfLayers==1 ? table_7_3_1_1_2_4_1layer_fullyAndPartialAndNonCoherent[srs_feedback->tpmi] : - table_7_3_1_1_2_4_2layers_fullyAndPartialAndNonCoherent[srs_feedback->tpmi]; + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 5) || (*nrOfLayers==2 && ul_tpmi <= 2), + "TPMI %d is invalid!\n", ul_tpmi); + *val = *nrOfLayers==1 ? table_7_3_1_1_2_4_1layer_fullyAndPartialAndNonCoherent[ul_tpmi] : + table_7_3_1_1_2_4_2layers_fullyAndPartialAndNonCoherent[ul_tpmi]; } } } @@ -2918,36 +2920,36 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 3; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 3 || srs_feedback->tpmi == 13, "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal(ul_tpmi <= 3 || ul_tpmi == 13, "TPMI %d is invalid!\n", ul_tpmi); } } else { nbits = 4; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 15, "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal(ul_tpmi <= 15, "TPMI %d is invalid!\n", ul_tpmi); } } if (val && srs_feedback) { - *val = table_7_3_1_1_2_3A[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_3A[ul_tpmi]; } } else { if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 2; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 3, "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal(ul_tpmi <= 3, "TPMI %d is invalid!\n", ul_tpmi); } } else if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_partialAndNonCoherent) { nbits = 4; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 11, "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal(ul_tpmi <= 11, "TPMI %d is invalid!\n", ul_tpmi); } } else { nbits = 5; if (val && srs_feedback) { - AssertFatal(srs_feedback->tpmi <= 27, "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal(ul_tpmi <= 27, "TPMI %d is invalid!\n", ul_tpmi); } } if (val && srs_feedback) { - *val = srs_feedback->tpmi; + *val = ul_tpmi; } } } else { @@ -2965,48 +2967,48 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 4; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && (srs_feedback->tpmi <= 3 || srs_feedback->tpmi==13)) || (*nrOfLayers==2 && srs_feedback->tpmi <= 6), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && (ul_tpmi <= 3 || ul_tpmi==13)) || (*nrOfLayers==2 && ul_tpmi <= 6), + "TPMI %d is invalid!\n", ul_tpmi); } } else { nbits = 5; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 15) || (*nrOfLayers==2 && srs_feedback->tpmi <= 13), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 15) || (*nrOfLayers==2 && ul_tpmi <= 13), + "TPMI %d is invalid!\n", ul_tpmi); } } if (val && srs_feedback) { - *val = *nrOfLayers==1 ? table_7_3_1_1_2_2A_1layer[srs_feedback->tpmi] : table_7_3_1_1_2_2A_2layers[srs_feedback->tpmi]; + *val = *nrOfLayers==1 ? table_7_3_1_1_2_2A_1layer[ul_tpmi] : table_7_3_1_1_2_2A_2layers[ul_tpmi]; } } else { if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 4; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && (srs_feedback->tpmi <= 3 || srs_feedback->tpmi == 13)) || (*nrOfLayers==2 && srs_feedback->tpmi <= 6) || - (*nrOfLayers==3 && srs_feedback->tpmi <= 1) || (*nrOfLayers==4 && srs_feedback->tpmi == 0), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && (ul_tpmi <= 3 || ul_tpmi == 13)) || (*nrOfLayers==2 && ul_tpmi <= 6) || + (*nrOfLayers==3 && ul_tpmi <= 1) || (*nrOfLayers==4 && ul_tpmi == 0), + "TPMI %d is invalid!\n", ul_tpmi); } } else { nbits = 6; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 15) || (*nrOfLayers==2 && srs_feedback->tpmi <= 13) || - (*nrOfLayers==3 && srs_feedback->tpmi <= 2) || (*nrOfLayers==4 && srs_feedback->tpmi <= 2), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 15) || (*nrOfLayers==2 && ul_tpmi <= 13) || + (*nrOfLayers==3 && ul_tpmi <= 2) || (*nrOfLayers==4 && ul_tpmi <= 2), + "TPMI %d is invalid!\n", ul_tpmi); } } if (val && srs_feedback) { switch (*nrOfLayers) { case 1: - *val = table_7_3_1_1_2_2B_1layer[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2B_1layer[ul_tpmi]; break; case 2: - *val = table_7_3_1_1_2_2B_2layers[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2B_2layers[ul_tpmi]; break; case 3: - *val = table_7_3_1_1_2_2B_3layers[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2B_3layers[ul_tpmi]; break; case 4: - *val = table_7_3_1_1_2_2B_4layers[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2B_4layers[ul_tpmi]; break; default: LOG_E(NR_MAC,"Number of layers %d is invalid!\n", *nrOfLayers); @@ -3017,38 +3019,38 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_nonCoherent) { nbits = 4; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 3) || (*nrOfLayers==2 && srs_feedback->tpmi <= 5) || - (*nrOfLayers==3 && srs_feedback->tpmi == 0) || (*nrOfLayers==4 && srs_feedback->tpmi == 0), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 3) || (*nrOfLayers==2 && ul_tpmi <= 5) || + (*nrOfLayers==3 && ul_tpmi == 0) || (*nrOfLayers==4 && ul_tpmi == 0), + "TPMI %d is invalid!\n", ul_tpmi); } } else if (codebookSubset && *codebookSubset == NR_PUSCH_Config__codebookSubset_partialAndNonCoherent) { nbits = 5; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 11) || (*nrOfLayers==2 && srs_feedback->tpmi <= 13) || - (*nrOfLayers==3 && srs_feedback->tpmi <= 2) || (*nrOfLayers==4 && srs_feedback->tpmi <= 2), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 11) || (*nrOfLayers==2 && ul_tpmi <= 13) || + (*nrOfLayers==3 && ul_tpmi <= 2) || (*nrOfLayers==4 && ul_tpmi <= 2), + "TPMI %d is invalid!\n", ul_tpmi); } } else { nbits = 6; if (val && srs_feedback) { - AssertFatal((*nrOfLayers==1 && srs_feedback->tpmi <= 28) || (*nrOfLayers==2 && srs_feedback->tpmi <= 22) || - (*nrOfLayers==3 && srs_feedback->tpmi <= 7) || (*nrOfLayers==4 && srs_feedback->tpmi <= 5), - "TPMI %d is invalid!\n", srs_feedback->tpmi); + AssertFatal((*nrOfLayers==1 && ul_tpmi <= 28) || (*nrOfLayers==2 && ul_tpmi <= 22) || + (*nrOfLayers==3 && ul_tpmi <= 7) || (*nrOfLayers==4 && ul_tpmi <= 5), + "TPMI %d is invalid!\n", ul_tpmi); } } if (val && srs_feedback) { switch (*nrOfLayers) { case 1: - *val = table_7_3_1_1_2_2_1layer[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2_1layer[ul_tpmi]; break; case 2: - *val = table_7_3_1_1_2_2_2layers[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2_2layers[ul_tpmi]; break; case 3: - *val = table_7_3_1_1_2_2_3layers[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2_3layers[ul_tpmi]; break; case 4: - *val = table_7_3_1_1_2_2_4layers[srs_feedback->tpmi]; + *val = table_7_3_1_1_2_2_4layers[ul_tpmi]; break; default: LOG_E(NR_MAC,"Number of layers %d is invalid!\n", *nrOfLayers); @@ -3299,7 +3301,8 @@ uint16_t nr_dci_size(const NR_UE_DL_BWP_t *DL_BWP, size += dci_pdu->srs_resource_indicator.nbits; LOG_D(NR_MAC, "dci_pdu->srs_resource_indicator.nbits %d\n", dci_pdu->srs_resource_indicator.nbits); // Precoding info and number of layers - dci_pdu->precoding_information.nbits = compute_precoding_information(pusch_Config, srs_config, dci_pdu->srs_resource_indicator, NULL, NULL, NULL); + dci_pdu->precoding_information.nbits = + compute_precoding_information(pusch_Config, srs_config, dci_pdu->srs_resource_indicator, NULL, NULL, NULL, NULL); size += dci_pdu->precoding_information.nbits; LOG_D(NR_MAC, "dci_pdu->precoding_informaiton.nbits=%d\n", dci_pdu->precoding_information.nbits); // Antenna ports diff --git a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.h b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.h index a4930aec47..64f8853e3d 100644 --- a/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.h +++ b/openair2/LAYER2/NR_MAC_COMMON/nr_mac_common.h @@ -79,6 +79,7 @@ uint8_t compute_precoding_information(NR_PUSCH_Config_t *pusch_Config, dci_field_t srs_resource_indicator, nr_srs_feedback_t *srs_feedback, const uint8_t *nrOfLayers, + int *tpmi, uint32_t *val); NR_PDSCH_TimeDomainResourceAllocationList_t *get_dl_tdalist(const NR_UE_DL_BWP_t *DL_BWP, diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c index c7faf78179..edf0f8c25e 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c @@ -894,6 +894,7 @@ static void nr_generate_Msg3_retransmission(module_id_t module_idP, pusch_pdu, &uldci_payload, NULL, + NULL, ra->Msg3_tda_id, ra->msg3_TPC, 1, // Not toggling NDI in msg3 retransmissions diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c index 1f9cb9c0c1..2c51ee5f21 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_primitives.c @@ -782,6 +782,7 @@ void config_uldci(const NR_UE_ServingCell_Info_t *sc_info, const nfapi_nr_pusch_pdu_t *pusch_pdu, dci_pdu_rel15_t *dci_pdu_rel15, nr_srs_feedback_t *srs_feedback, + int *tpmi, int time_domain_assignment, uint8_t tpc, uint8_t ndi, @@ -830,6 +831,7 @@ void config_uldci(const NR_UE_ServingCell_Info_t *sc_info, dci_pdu_rel15->srs_resource_indicator, srs_feedback, &pusch_pdu->nrOfLayers, + tpmi, &dci_pdu_rel15->precoding_information.val); // antenna_ports.val = 0 for transform precoder is disabled, dmrs-Type=1, maxLength=1, Rank=1/2/3/4 diff --git a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c index 0900148073..7f849253da 100644 --- a/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c +++ b/openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_ulsch.c @@ -2256,6 +2256,7 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot, n uint16_t rnti = UE->rnti; sched_ctrl->SR = false; + int *tpmi = NULL; int8_t harq_id = sched_pusch->ul_harq_pid; if (harq_id < 0) { @@ -2297,6 +2298,7 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot, n * retransmissions */ cur_harq->sched_pusch.time_domain_allocation = sched_pusch->time_domain_allocation; cur_harq->sched_pusch.nrOfLayers = sched_pusch->nrOfLayers; + cur_harq->sched_pusch.tpmi = sched_pusch->tpmi; sched_ctrl->sched_ul_bytes += sched_pusch->tb_size; UE->mac_stats.ul.total_rbs += sched_pusch->rbSize; @@ -2397,6 +2399,10 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot, n else pusch_pdu->data_scrambling_id = *scc->physCellId; pusch_pdu->nrOfLayers = sched_pusch->nrOfLayers; + // If nrOfLayers is the same as in srs_feedback, we use the best TPMI, i.e. the one in srs_feedback. + // Otherwise, we use the valid TPMI that we saved in the first transmission. + if (pusch_pdu->nrOfLayers != (sched_ctrl->srs_feedback.ul_ri + 1)) + tpmi = &sched_pusch->tpmi; pusch_pdu->num_dmrs_cdm_grps_no_data = sched_pusch->dmrs_info.num_dmrs_cdm_grps_no_data; /* FAPI: DMRS */ @@ -2572,6 +2578,7 @@ void nr_schedule_ulsch(module_id_t module_id, frame_t frame, sub_frame_t slot, n pusch_pdu, &uldci_payload, &sched_ctrl->srs_feedback, + tpmi, sched_pusch->time_domain_allocation, UE->UE_sched_ctrl.tpc0, cur_harq->ndi, diff --git a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h index c5df1782fd..3e9f02d7ff 100644 --- a/openair2/LAYER2/NR_MAC_gNB/mac_proto.h +++ b/openair2/LAYER2/NR_MAC_gNB/mac_proto.h @@ -163,6 +163,7 @@ void config_uldci(const NR_UE_ServingCell_Info_t *sc_info, const nfapi_nr_pusch_pdu_t *pusch_pdu, dci_pdu_rel15_t *dci_pdu_rel15, nr_srs_feedback_t *srs_feedback, + int *tpmi, int time_domain_assignment, uint8_t tpc, uint8_t ndi, diff --git a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h index 641f6d93d1..54be4358a7 100644 --- a/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h +++ b/openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h @@ -422,6 +422,8 @@ typedef struct NR_sched_pusch { int8_t ul_harq_pid; uint8_t nrOfLayers; + int tpmi; + // time_domain_allocation is the index of a list of tda int time_domain_allocation; NR_tda_info_t tda_info; From f50146962d1551a09f5cd5577028142e4af2539a Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Tue, 17 Dec 2024 17:26:41 +0100 Subject: [PATCH 08/18] Fix instrumentation target for older CMake versions --- common/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 229c93b043..f58880b04a 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -3,7 +3,7 @@ add_subdirectory(config/yaml) configure_file(oai_version.h.in oai_version.h @ONLY) -add_library(instrumentation INTERFACE instrumentation.h) +add_library(instrumentation INTERFACE) target_include_directories(instrumentation INTERFACE .) if (TRACY_ENABLE) target_link_libraries(instrumentation INTERFACE Tracy::TracyClient) From 55b5e04e8ddaa77c6a2b5be2290f4e726ef068b5 Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Tue, 17 Dec 2024 11:02:40 +0100 Subject: [PATCH 09/18] Change alignment of temporary IDFT buffer in PHY_ofdm_mod Alignment of temporary IDFT buffer was changed to 64 bytes. This is in preparation for possible use of different IDFT implementation which could use AVX-512, requiring 64-byte alignment. --- openair1/PHY/MODULATION/ofdm_mod.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openair1/PHY/MODULATION/ofdm_mod.c b/openair1/PHY/MODULATION/ofdm_mod.c index 595c4ce996..e25f7cd7f2 100644 --- a/openair1/PHY/MODULATION/ofdm_mod.c +++ b/openair1/PHY/MODULATION/ofdm_mod.c @@ -40,6 +40,9 @@ This section deals with basic functions for OFDM Modulation. #include "PHY/LTE_TRANSPORT/transport_common_proto.h" //#define DEBUG_OFDM_MOD +// Use 64-byte alignment for IDFT output buffer to ensure no +// runtime error in case IDFT implementation uses AVX-512. +#define IDFT_OUTPUT_BUFFER_ALIGNMENT 64 void normal_prefix_mod(int32_t *txdataF,int32_t *txdata,uint8_t nsymb,LTE_DL_FRAME_PARMS *frame_parms) { @@ -163,12 +166,13 @@ void PHY_ofdm_mod(const int *input, /// pointer to complex input switch (etype) { case CYCLIC_PREFIX: { int *output_ptr = &output[(i * fftsize) + ((1 + i) * nb_prefix_samples)]; + // Current idft implementation uses AVX-256: Check if buffer is already aligned to 256 bits (32 bytes) if ((uintptr_t)output_ptr % 32 == 0) { // output ptr is aligned, do ifft inplace idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)output_ptr, 1); } else { // output ptr is not aligned, needs an extra memcpy - c16_t temp[fftsize] __attribute__((aligned(32))); + c16_t temp[fftsize] __attribute__((aligned(IDFT_OUTPUT_BUFFER_ALIGNMENT))); idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); memcpy((void *)output_ptr, (void *)temp, sizeof(temp)); } @@ -178,7 +182,8 @@ void PHY_ofdm_mod(const int *input, /// pointer to complex input } case CYCLIC_SUFFIX: { - c16_t temp[fftsize] __attribute__((aligned(32))); + // Use alignment of 64 bytes + c16_t temp[fftsize] __attribute__((aligned(IDFT_OUTPUT_BUFFER_ALIGNMENT))); idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); int *output_ptr = &output[(i * fftsize) + (i * nb_prefix_samples)]; memcpy(output_ptr, temp, sizeof(temp)); @@ -191,7 +196,7 @@ void PHY_ofdm_mod(const int *input, /// pointer to complex input break; case NONE: { - c16_t temp[fftsize] __attribute__((aligned(32))); + c16_t temp[fftsize] __attribute__((aligned(IDFT_OUTPUT_BUFFER_ALIGNMENT))); idft(idft_size, (int16_t *)&input[i * fftsize], (int16_t *)temp, 1); int *output_ptr = &output[i * fftsize]; memcpy(output_ptr, temp, sizeof(temp)); From 276a17902eb3471f5cfe9ce67ce16f9044846820 Mon Sep 17 00:00:00 2001 From: Matthias Mehlhose Date: Thu, 12 Dec 2024 12:33:46 +0000 Subject: [PATCH 10/18] fix: UE average --- openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c b/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c index 5dbab85f12..f6ed9b9506 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c @@ -211,7 +211,7 @@ int nr_pbch_channel_level(struct complex16 dl_ch_estimates_ext[][PBCH_MAX_RE_PER int16_t nb_rb=nb_re/12; simde__m128i avg128; simde__m128i *dl_ch128; - int avg1=0,avg2=0; + int avg2=0; for (int aarx=0; aarxnb_antennas_rx; aarx++) { //clear average level @@ -231,6 +231,7 @@ int nr_pbch_channel_level(struct complex16 dl_ch_estimates_ext[][PBCH_MAX_RE_PER }*/ } + int avg1 = 0; for (int i = 0; i < 4; i++) avg1 += ((int *)&avg128)[i] / (nb_rb * 12); From 5ae69ce484babe27033529d97ed615651a2bb65c Mon Sep 17 00:00:00 2001 From: Matthias Mehlhose Date: Thu, 12 Dec 2024 13:00:25 +0000 Subject: [PATCH 11/18] refactor: average 64 bit calculations with shift and internal division (SSE and AVX2) --- .../PHY/NR_TRANSPORT/nr_ulsch_demodulation.c | 16 +-- openair1/PHY/NR_UE_TRANSPORT/dci_nr.c | 26 +--- .../NR_UE_TRANSPORT/nr_dlsch_demodulation.c | 20 +-- openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c | 26 +--- openair1/PHY/sse_intrin.h | 125 +++++++++++++----- 5 files changed, 112 insertions(+), 101 deletions(-) diff --git a/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c b/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c index b7cfa7d1e4..43f6417c10 100644 --- a/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c +++ b/openair1/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c @@ -209,25 +209,17 @@ static void nr_ulsch_channel_level(int size_est, uint32_t len, uint8_t nrOfLayers) { - simde__m128i *ul_ch128, avg128U; - int16_t x = factor2(len); int16_t y = (len)>>x; for (int aatx = 0; aatx < nrOfLayers; aatx++) { for (int aarx = 0; aarx < frame_parms->nb_antennas_rx; aarx++) { - //clear average level - avg128U = simde_mm_setzero_si128(); - ul_ch128 = (simde__m128i *)&ul_ch_estimates_ext[aatx * frame_parms->nb_antennas_rx + aarx][symbol * len]; + simde__m128i *ul_ch128 = (simde__m128i *)&ul_ch_estimates_ext[aatx * frame_parms->nb_antennas_rx + aarx][symbol * len]; - for (int i = 0; i < len >> 2; i++) { - avg128U = simde_mm_add_epi32(avg128U, simde_mm_srai_epi32(simde_mm_madd_epi16(ul_ch128[i], ul_ch128[i]), x)); - } - - int32_t *avg32i = (int32_t *)&avg128U; - int64_t avg64 = (int64_t)avg32i[0] + avg32i[1] + avg32i[2] + avg32i[3]; - avg[aatx * frame_parms->nb_antennas_rx + aarx] = avg64 / y; + //compute average level + avg[aatx * frame_parms->nb_antennas_rx + aarx] = simde_mm_average(ul_ch128, len, x, y); + //LOG_D(PHY, "Channel level: %d\n", avg[aatx * frame_parms->nb_antennas_rx + aarx]); } } diff --git a/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c b/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c index 15bbc4e626..36bd5a1ce7 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c +++ b/openair1/PHY/NR_UE_TRANSPORT/dci_nr.c @@ -226,30 +226,12 @@ void nr_pdcch_channel_level(int32_t rx_size, int nb_rb) { for (int aarx = 0; aarx < frame_parms->nb_antennas_rx; aarx++) { - //clear average level - simde__m128i avg128P = simde_mm_setzero_si128(); + simde__m128i *dl_ch128 = (simde__m128i *)&dl_ch_estimates_ext[aarx][symbol * nb_rb * 12]; - for (int rb = 0; rb < (nb_rb * 3) >> 2; rb++) { - avg128P = simde_mm_add_epi32(avg128P,simde_mm_madd_epi16(dl_ch128[0],dl_ch128[0])); - avg128P = simde_mm_add_epi32(avg128P,simde_mm_madd_epi16(dl_ch128[1],dl_ch128[1])); - avg128P = simde_mm_add_epi32(avg128P,simde_mm_madd_epi16(dl_ch128[2],dl_ch128[2])); - // for (int i=0;i<24;i+=2) printf("pdcch channel re %d (%d,%d)\n",(rb*12)+(i>>1),((int16_t*)dl_ch128)[i],((int16_t*)dl_ch128)[i+1]); - dl_ch128+=3; - /* - if (rb==0) { - print_shorts("dl_ch128",&dl_ch128[0]); - print_shorts("dl_ch128",&dl_ch128[1]); - print_shorts("dl_ch128",&dl_ch128[2]); - } - */ - } - - DevAssert(nb_rb); - avg[aarx] = 0; - for (int i = 0; i < 4; i++) - avg[aarx] += ((int32_t *)&avg128P)[i] / (nb_rb * 9); - LOG_DDD("Channel level : %d\n",avg[aarx]); + //compute average level + avg[aarx] = simde_mm_average(dl_ch128, nb_rb * 12, 0, nb_rb * 12); + //LOG_DDD("Channel level : %d\n", avg[aarx]); } } diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c index c0019fb3ec..217a370054 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_dlsch_demodulation.c @@ -1098,28 +1098,20 @@ void nr_dlsch_channel_level(uint32_t rx_size_symbol, int32_t avg[MAX_ANT][MAX_ANT], uint32_t len) { - simde__m128i *dl_ch128, avg128D; - //nb_rb*nre = y * 2^x int16_t x = factor2(len); int16_t y = (len) >> x; + uint32_t nb_rb_0 = len / NR_NB_SC_PER_RB + ((len % NR_NB_SC_PER_RB) ? 1 : 0); LOG_D(NR_PHY, "nb_rb_0 %d len %d = %d * 2^(%d)\n", nb_rb_0, len, y, x); + for (int aatx = 0; aatx < n_tx; aatx++) { for (int aarx = 0; aarx < n_rx; aarx++) { - //clear average level - avg128D = simde_mm_setzero_si128(); - dl_ch128 = (simde__m128i *)dl_ch_estimates_ext[(aatx * n_rx) + aarx]; + simde__m128i *dl_ch128 = (simde__m128i *)dl_ch_estimates_ext[(aatx * n_rx) + aarx]; - for (int rb = 0; rb < nb_rb_0; rb++) { - avg128D = simde_mm_add_epi32(avg128D,simde_mm_srai_epi32(simde_mm_madd_epi16(dl_ch128[0],dl_ch128[0]),x)); - avg128D = simde_mm_add_epi32(avg128D,simde_mm_srai_epi32(simde_mm_madd_epi16(dl_ch128[1],dl_ch128[1]),x)); - avg128D = simde_mm_add_epi32(avg128D,simde_mm_srai_epi32(simde_mm_madd_epi16(dl_ch128[2],dl_ch128[2]),x)); - dl_ch128+=3; - } - int32_t *tmp = (int32_t *)&avg128D; - avg[aatx][aarx] = ((int64_t)tmp[0] + tmp[1] + tmp[2] + tmp[3]) / y; - LOG_D(PHY, "Channel level: %d\n", avg[aatx][aarx]); + //compute average level + avg[aatx][aarx] = simde_mm_average(dl_ch128, nb_rb_0 * 12, x, y); + //LOG_D(PHY, "Channel level: %d\n", avg[aatx][aarx]); } } } diff --git a/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c b/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c index f6ed9b9506..229191269f 100644 --- a/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c +++ b/openair1/PHY/NR_UE_TRANSPORT/nr_pbch.c @@ -208,32 +208,14 @@ int nr_pbch_channel_level(struct complex16 dl_ch_estimates_ext[][PBCH_MAX_RE_PER const NR_DL_FRAME_PARMS *frame_parms, int nb_re) { - int16_t nb_rb=nb_re/12; - simde__m128i avg128; - simde__m128i *dl_ch128; - int avg2=0; + int32_t avg2 = 0; for (int aarx=0; aarxnb_antennas_rx; aarx++) { - //clear average level - avg128 = simde_mm_setzero_si128(); - dl_ch128=(simde__m128i *)dl_ch_estimates_ext[aarx]; - for (int rb=0; rbavg2) avg2 = avg1; diff --git a/openair1/PHY/sse_intrin.h b/openair1/PHY/sse_intrin.h index 655c38982e..d6c3157ce8 100644 --- a/openair1/PHY/sse_intrin.h +++ b/openair1/PHY/sse_intrin.h @@ -49,13 +49,6 @@ #define SSE_INTRIN_H -#include -#include -#include -#include -#include -#include -#include #include #include #if defined(__x86_64) || defined(__i386__) @@ -81,31 +74,101 @@ * OAI specific */ -static const short minusConjug128[8]__attribute__((aligned(16))) = {-1,1,-1,1,-1,1,-1,1}; -static inline simde__m128i mulByConjugate128(simde__m128i *a, simde__m128i *b, int8_t output_shift) { - - simde__m128i realPart = simde_mm_madd_epi16(*a,*b); - realPart = simde_mm_srai_epi32(realPart,output_shift); - simde__m128i imagPart = simde_mm_shufflelo_epi16(*b, SIMDE_MM_SHUFFLE(2,3,0,1)); - imagPart = simde_mm_shufflehi_epi16(imagPart, SIMDE_MM_SHUFFLE(2,3,0,1)); - imagPart = simde_mm_sign_epi16(imagPart,*(simde__m128i *)minusConjug128); - imagPart = simde_mm_madd_epi16(imagPart,*a); - imagPart = simde_mm_srai_epi32(imagPart,output_shift); - simde__m128i lowPart = simde_mm_unpacklo_epi32(realPart,imagPart); - simde__m128i highPart = simde_mm_unpackhi_epi32(realPart,imagPart); - return ( simde_mm_packs_epi32(lowPart,highPart)); +static const short minusConjug128[8] __attribute__((aligned(16))) = {-1, 1, -1, 1, -1, 1, -1, 1}; +static inline simde__m128i mulByConjugate128(simde__m128i *a, simde__m128i *b, int8_t output_shift) +{ + simde__m128i realPart = simde_mm_madd_epi16(*a, *b); + realPart = simde_mm_srai_epi32(realPart, output_shift); + simde__m128i imagPart = simde_mm_shufflelo_epi16(*b, SIMDE_MM_SHUFFLE(2, 3, 0, 1)); + imagPart = simde_mm_shufflehi_epi16(imagPart, SIMDE_MM_SHUFFLE(2, 3, 0, 1)); + imagPart = simde_mm_sign_epi16(imagPart, *(simde__m128i *)minusConjug128); + imagPart = simde_mm_madd_epi16(imagPart, *a); + imagPart = simde_mm_srai_epi32(imagPart, output_shift); + simde__m128i lowPart = simde_mm_unpacklo_epi32(realPart, imagPart); + simde__m128i highPart = simde_mm_unpackhi_epi32(realPart, imagPart); + return (simde_mm_packs_epi32(lowPart, highPart)); } -#define displaySamples128(vect) {\ - simde__m128i x=vect; \ - printf("vector: %s = (%hd,%hd) (%hd,%hd) (%hd,%hd) (%hd,%hd)\n", #vect, \ - simde_mm_extract_epi16(x,0), \ - simde_mm_extract_epi16(x,1),\ - simde_mm_extract_epi16(x,2),\ - simde_mm_extract_epi16(x,3),\ - simde_mm_extract_epi16(x,4),\ - simde_mm_extract_epi16(x,5),\ - simde_mm_extract_epi16(x,6),\ - simde_mm_extract_epi16(x,7));\ +#define displaySamples128(vect) \ + { \ + simde__m128i x = vect; \ + printf("vector: %s = (%hd,%hd) (%hd,%hd) (%hd,%hd) (%hd,%hd)\n", \ + #vect, \ + simde_mm_extract_epi16(x, 0), \ + simde_mm_extract_epi16(x, 1), \ + simde_mm_extract_epi16(x, 2), \ + simde_mm_extract_epi16(x, 3), \ + simde_mm_extract_epi16(x, 4), \ + simde_mm_extract_epi16(x, 5), \ + simde_mm_extract_epi16(x, 6), \ + simde_mm_extract_epi16(x, 7)); \ } + +__attribute__((always_inline)) static inline int64_t simde_mm_average_sse(simde__m128i *a, int length, int shift) +{ + // compute average level with shift (64-bit verstion) + simde__m128i avg128 = simde_mm_setzero_si128(); + for (int i = 0; i < length >> 2; i++) { + const simde__m128i in1 = a[i]; + avg128 = simde_mm_add_epi32(avg128, simde_mm_srai_epi32(simde_mm_madd_epi16(in1, in1), shift)); + } + + // Horizontally add pairs + // 1st [A + C, B + D] + simde__m128i sum_pairs = simde_mm_add_epi64(simde_mm_unpacklo_epi32(avg128, simde_mm_setzero_si128()), // [A, B] → [A, 0, B, 0] + simde_mm_unpackhi_epi32(avg128, simde_mm_setzero_si128()) // [C, D] → [C, 0, D, 0] + ); + + // 2nd [A + B + C + D, ...] + simde__m128i total_sum = simde_mm_add_epi64(sum_pairs, simde_mm_shuffle_epi32(sum_pairs, SIMDE_MM_SHUFFLE(1, 0, 3, 2))); + + // Extract horizontal sum as a scalar int64_t result + return simde_mm_cvtsi128_si64(total_sum); +} + +__attribute__((always_inline)) static inline int64_t simde_mm_average_avx2(simde__m256i *a, int length, int shift) +{ + simde__m256i avg256 = simde_mm256_setzero_si256(); + for (int i = 0; i < length >> 3; i++) { + const simde__m256i in1 = simde_mm256_loadu_si256(&a[i]); // unaligned load + avg256 = simde_mm256_add_epi32(avg256, simde_mm256_srai_epi32(simde_mm256_madd_epi16(in1, in1), shift)); + } + + // Split the 256-bit vector into two 128-bit halves and convert to 64-bit + // [A + E, B + F, C + G, D + H] + simde__m256i sum_pairs = simde_mm256_add_epi64( + simde_mm256_cvtepi32_epi64(simde_mm256_castsi256_si128(avg256)), // [A, B, C, D] → [A, 0, B, 0, C, 0, D, 0] + simde_mm256_cvtepi32_epi64(simde_mm256_extracti128_si256(avg256, 1)) // [E, F, G, H] → [E, 0, F, 0, G, 0, H, 0] + ); + + // Horizontal sum within the 256-bit vector + // [A + E + B + F, C + G + D + H] + simde__m128i total_sum = simde_mm_add_epi64(simde_mm256_castsi256_si128(sum_pairs), simde_mm256_extracti128_si256(sum_pairs, 1)); + + // [A + E + B + F + C + G + D + H, ...] + total_sum = simde_mm_add_epi64(total_sum, simde_mm_shuffle_epi32(total_sum, SIMDE_MM_SHUFFLE(1, 0, 3, 2))); + + // Extract horizontal sum as a scalar int64_t result + return simde_mm_cvtsi128_si64(total_sum); +} + +__attribute__((always_inline)) static inline int32_t simde_mm_average(simde__m128i *a, int length, int shift, int16_t scale) +{ + int64_t avg = 0; + +#if defined(__x86_64__) || defined(__i386__) + if (__builtin_cpu_supports("avx2")) { + avg += simde_mm_average_avx2((simde__m256i *)a, length, shift); + + // tail processing by SSE + a += ((length & ~7) >> 2); + length -= (length & ~7); + } +#endif + + avg += simde_mm_average_sse(a, length, shift); + + return (uint32_t)(avg / scale); +} + #endif // SSE_INTRIN_H From acae0dc011a3e80e2563e69f1caf33d12a24df24 Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Wed, 18 Dec 2024 09:18:48 +0100 Subject: [PATCH 12/18] Replace AssertFatal with static_assert for cmdline arguments check Replace AssertFatal with static_assert for command line arguments definition length check. The static assert will not allow the code to compile in case the size is incorrect. This could save some time in the future since its easy to forget to extend CMDLINE_PARAMS_CHECK_DESC. --- executables/softmodem-common.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/executables/softmodem-common.c b/executables/softmodem-common.c index 9f39612733..390da3c93c 100644 --- a/executables/softmodem-common.c +++ b/executables/softmodem-common.c @@ -105,8 +105,9 @@ void get_common_options(configmodule_interface_t *cfg, uint32_t execmask) paramdef_t cmdline_params[] = CMDLINE_PARAMS_DESC; checkedparam_t cmdline_CheckParams[] = CMDLINE_PARAMS_CHECK_DESC; + static_assert(sizeofArray(cmdline_params) == sizeofArray(cmdline_CheckParams), + "cmdline_params and cmdline_CheckParams should have the same size"); int numparams = sizeofArray(cmdline_params); - AssertFatal(numparams == sizeofArray(cmdline_CheckParams), "Error in arrays size (%d!=%lu)\n", numparams, sizeofArray(cmdline_CheckParams)); config_set_checkfunctions(cmdline_params, cmdline_CheckParams, numparams); config_get(cfg, cmdline_params, numparams, NULL); nfapi_index = config_paramidx_fromname(cmdline_params, numparams, "nfapi"); From b4395406f0792e8ff4d3e3968a94a831f5dd3fd9 Mon Sep 17 00:00:00 2001 From: Robert Schmidt Date: Thu, 19 Dec 2024 03:12:35 +0100 Subject: [PATCH 13/18] softmodem-common: Replace AssertFatal with static_assert See also parent commit --- executables/softmodem-common.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/executables/softmodem-common.c b/executables/softmodem-common.c index 390da3c93c..2fed359d82 100644 --- a/executables/softmodem-common.c +++ b/executables/softmodem-common.c @@ -116,9 +116,10 @@ void get_common_options(configmodule_interface_t *cfg, uint32_t execmask) paramdef_t cmdline_logparams[] =CMDLINE_LOGPARAMS_DESC ; checkedparam_t cmdline_log_CheckParams[] = CMDLINE_LOGPARAMS_CHECK_DESC; - int numlogparams = sizeofArray(cmdline_logparams); - AssertFatal(numlogparams == sizeofArray(cmdline_log_CheckParams), "Error in arrays size (%d!=%lu)\n", numlogparams, sizeofArray(cmdline_log_CheckParams)); + static_assert(sizeofArray(cmdline_logparams) == sizeofArray(cmdline_log_CheckParams), + "cmdline_logparams and cmdline_log_CheckParams should have the same size"); + int numlogparams = sizeofArray(cmdline_logparams); config_set_checkfunctions(cmdline_logparams, cmdline_log_CheckParams, numlogparams); config_get(cfg, cmdline_logparams, numlogparams, NULL); From 5473ee904fbb976071261b72dbee3115e2eabc30 Mon Sep 17 00:00:00 2001 From: Robert Schmidt Date: Thu, 19 Dec 2024 03:22:27 +0100 Subject: [PATCH 14/18] E1AP: Replace AssertFatal with static_assert See parent commit --- openair2/E1AP/e1ap_setup.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openair2/E1AP/e1ap_setup.c b/openair2/E1AP/e1ap_setup.c index 4ec94bc9b5..fc59a881fa 100644 --- a/openair2/E1AP/e1ap_setup.c +++ b/openair2/E1AP/e1ap_setup.c @@ -94,6 +94,8 @@ MessageDef *RCconfig_NR_CU_E1(const E1_t *entity) paramlist_def_t PLMNParamList = {GNB_CONFIG_STRING_PLMN_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams[] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); From f02bd8b453480d9397e251b6c633edcc801ed1c3 Mon Sep 17 00:00:00 2001 From: Robert Schmidt Date: Thu, 19 Dec 2024 03:23:59 +0100 Subject: [PATCH 15/18] enb_config: Replace AssertFatal with static_assert, align length See parent commit --- openair2/ENB_APP/enb_config.c | 16 ++++++++++++ openair2/ENB_APP/enb_paramdef.h | 37 ---------------------------- openair2/ENB_APP/enb_paramdef_emtc.h | 1 + 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/openair2/ENB_APP/enb_config.c b/openair2/ENB_APP/enb_config.c index 9d0d81cfc8..6daae2907d 100644 --- a/openair2/ENB_APP/enb_config.c +++ b/openair2/ENB_APP/enb_config.c @@ -266,9 +266,13 @@ int RCconfig_RRC(uint32_t i, eNB_RRC_INST *rrc) { paramlist_def_t ENBParamList = {ENB_CONFIG_STRING_ENB_LIST,NULL,0}; checkedparam_t config_check_CCparams[] = CCPARAMS_CHECK; paramdef_t CCsParams[] = CCPARAMS_DESC(ccparams_lte); + static_assert(sizeofArray(config_check_CCparams) == sizeofArray(CCsParams), + "config_check_CCparams and CCsParams should have the same size"); paramlist_def_t CCsParamList = {ENB_CONFIG_STRING_COMPONENT_CARRIERS,NULL,0}; paramdef_t eMTCParams[] = EMTCPARAMS_DESC((&eMTCconfig)); checkedparam_t config_check_eMTCparams[] = EMTCPARAMS_CHECK; + static_assert(sizeofArray(config_check_eMTCparams) == sizeofArray(eMTCParams), + "config_check_eMTCparams and eMTCParamsCCsParams should have the same size"); srb1_params_t srb1_params; memset((void *)&srb1_params,0,sizeof(srb1_params_t)); paramdef_t SRB1Params[] = SRB1PARAMS_DESC(srb1_params); @@ -320,6 +324,8 @@ int RCconfig_RRC(uint32_t i, eNB_RRC_INST *rrc) { paramlist_def_t PLMNParamList = {ENB_CONFIG_STRING_PLMN_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); @@ -1910,6 +1916,8 @@ int RCconfig_M2(MessageDef *msg_p, uint32_t i) { paramlist_def_t PLMNParamList = {ENB_CONFIG_STRING_PLMN_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); @@ -2159,6 +2167,8 @@ int RCconfig_S1( paramdef_t CCsParams[] = CCPARAMS_DESC(ccparams_lte); /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_CCparams[] = CCPARAMS_CHECK; + static_assert(sizeofArray(config_check_CCparams) == sizeofArray(CCsParams), + "config_check_CCparams and CCsParams should have the same size"); for (int I = 0; I < sizeofArray(CCsParams); I++) { CCsParams[I].chkPptr = &(config_check_CCparams[I]); @@ -2166,6 +2176,8 @@ int RCconfig_S1( /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) { PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); @@ -2413,6 +2425,8 @@ int RCconfig_X2(MessageDef *msg_p, uint32_t i) { config_get(config_get_if(), ENBSParams, sizeofArray(ENBSParams), NULL); checkedparam_t config_check_CCparams[] = CCPARAMS_CHECK; paramdef_t CCsParams[] = CCPARAMS_DESC(ccparams_lte); + static_assert(sizeofArray(config_check_CCparams) == sizeofArray(CCsParams), + "config_check_CCparams and CCsParams should have the same size"); paramlist_def_t CCsParamList = {ENB_CONFIG_STRING_COMPONENT_CARRIERS, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ @@ -2450,6 +2464,8 @@ int RCconfig_X2(MessageDef *msg_p, uint32_t i) { paramlist_def_t PLMNParamList = {ENB_CONFIG_STRING_PLMN_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); diff --git a/openair2/ENB_APP/enb_paramdef.h b/openair2/ENB_APP/enb_paramdef.h index 5fd4cdce70..7bc8f677d8 100644 --- a/openair2/ENB_APP/enb_paramdef.h +++ b/openair2/ENB_APP/enb_paramdef.h @@ -749,43 +749,6 @@ typedef struct ccparams_lte_s { { { NULL } } , \ { { NULL } } , \ { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } , \ - { { NULL } } \ } /*-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/ diff --git a/openair2/ENB_APP/enb_paramdef_emtc.h b/openair2/ENB_APP/enb_paramdef_emtc.h index 9d7bfb98e0..52f5b2a60f 100644 --- a/openair2/ENB_APP/enb_paramdef_emtc.h +++ b/openair2/ENB_APP/enb_paramdef_emtc.h @@ -351,6 +351,7 @@ typedef struct ccparams_eMTC_s { { {NULL}} , \ { {NULL}} , \ { {NULL}} , \ + { {NULL}} , \ } // clang-format on From 2f43b343b6bf44f4751a1d8368d5e4e73f70ccf0 Mon Sep 17 00:00:00 2001 From: Robert Schmidt Date: Thu, 19 Dec 2024 03:25:04 +0100 Subject: [PATCH 16/18] gnb_config: Replace AssertFatal with static_assert See parent commit --- openair2/GNB_APP/gnb_config.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openair2/GNB_APP/gnb_config.c b/openair2/GNB_APP/gnb_config.c index 7555d1f5d0..3b8981353d 100644 --- a/openair2/GNB_APP/gnb_config.c +++ b/openair2/GNB_APP/gnb_config.c @@ -1118,6 +1118,8 @@ static int read_du_cell_info(configmodule_interface_t *cfg, paramdef_t PLMNParams[] = GNBPLMNPARAMS_DESC; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams[] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeof(PLMNParams) / sizeof(paramdef_t); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); paramlist_def_t PLMNParamList = {GNB_CONFIG_STRING_PLMN_LIST, NULL, 0}; @@ -1151,6 +1153,8 @@ static int read_du_cell_info(configmodule_interface_t *cfg, paramdef_t SNSSAIParams[] = GNBSNSSAIPARAMS_DESC; paramlist_def_t SNSSAIParamList = {GNB_CONFIG_STRING_SNSSAI_LIST, NULL, 0}; checkedparam_t config_check_SNSSAIParams[] = SNSSAIPARAMS_CHECK; + static_assert(sizeofArray(config_check_SNSSAIParams) == sizeofArray(SNSSAIParams), + "config_check_SNSSAIParams and SNSSAIParams should have the same size"); for (int J = 0; J < sizeofArray(SNSSAIParams); ++J) SNSSAIParams[J].chkPptr = &(config_check_SNSSAIParams[J]); char snssaistr[MAX_OPTNAME_SIZE * 2 + 8]; @@ -1288,6 +1292,8 @@ void RCconfig_nr_macrlc(configmodule_interface_t *cfg) paramdef_t GNBParams[] = GNBPARAMS_DESC; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_GNBParams[] = GNBPARAMS_CHECK; + static_assert(sizeofArray(config_check_GNBParams) == sizeofArray(GNBParams), + "config_check_GNBParams and GNBParams should have the same size"); for (int i = 0; i < sizeofArray(GNBParams); ++i) GNBParams[i].chkPptr = &(config_check_GNBParams[i]); config_getlist(cfg, &GNBParamList, GNBParams, sizeofArray(GNBParams), NULL); @@ -1296,6 +1302,8 @@ void RCconfig_nr_macrlc(configmodule_interface_t *cfg) paramlist_def_t MacRLC_ParamList = {CONFIG_STRING_MACRLC_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_MacRLCParams[] = MACRLCPARAMS_CHECK; + static_assert(sizeofArray(config_check_MacRLCParams) == sizeofArray(MacRLC_Params), + "config_check_MacRLCParams and MacRLC_Params should have the same size"); for (int i = 0; i < sizeofArray(MacRLC_Params); ++i) MacRLC_Params[i].chkPptr = &(config_check_MacRLCParams[i]); config_getlist(config_get_if(), &MacRLC_ParamList, MacRLC_Params, sizeofArray(MacRLC_Params), NULL); @@ -1906,6 +1914,8 @@ gNB_RRC_INST *RCconfig_NRRRC() paramlist_def_t PLMNParamList = {GNB_CONFIG_STRING_PLMN_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); @@ -2013,7 +2023,11 @@ int RCconfig_NR_NG(MessageDef *msg_p, uint32_t i) { paramlist_def_t SNSSAIParamList = {GNB_CONFIG_STRING_SNSSAI_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); checkedparam_t config_check_SNSSAIParams [] = SNSSAIPARAMS_CHECK; + static_assert(sizeofArray(config_check_SNSSAIParams) == sizeofArray(SNSSAIParams), + "config_check_SNSSAIParams and SNSSAIParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); @@ -2277,6 +2291,8 @@ int RCconfig_NR_X2(MessageDef *msg_p, uint32_t i) { paramlist_def_t PLMNParamList = {GNB_CONFIG_STRING_PLMN_LIST, NULL, 0}; /* map parameter checking array instances to parameter definition array instances */ checkedparam_t config_check_PLMNParams [] = PLMNPARAMS_CHECK; + static_assert(sizeofArray(config_check_PLMNParams) == sizeofArray(PLMNParams), + "config_check_PLMNParams and PLMNParams should have the same size"); for (int I = 0; I < sizeofArray(PLMNParams); ++I) PLMNParams[I].chkPptr = &(config_check_PLMNParams[I]); From d820c187fc043847eab47039009ae095f3d3bb0b Mon Sep 17 00:00:00 2001 From: Robert Schmidt Date: Thu, 19 Dec 2024 03:26:27 +0100 Subject: [PATCH 17/18] OPT: Replace AssertFatal with static_assert See parent commit --- openair2/UTIL/OPT/probe.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openair2/UTIL/OPT/probe.c b/openair2/UTIL/OPT/probe.c index 25d1b6b1ec..06c02f1654 100644 --- a/openair2/UTIL/OPT/probe.c +++ b/openair2/UTIL/OPT/probe.c @@ -529,8 +529,9 @@ void trace_pdu_implementation(ws_trace_t *t) int init_opt(void) { paramdef_t opt_params[] = OPT_PARAMS_DESC ; checkedparam_t opt_checkParams[] = OPTPARAMS_CHECK_DESC; + static_assert(sizeofArray(opt_params) == sizeofArray(opt_checkParams), + "opt_params and opt_checkParams should have the same size"); int sz=sizeofArray(opt_params); - AssertFatal(sz == sizeofArray(opt_checkParams), "Error in arrays size (%d!=%lu)\n", sz, sizeofArray(opt_checkParams)); config_set_checkfunctions(opt_params, opt_checkParams, sz); config_get(config_get_if(), opt_params, sz, OPT_CONFIGPREFIX); subframesSinceCaptureStart = 0; From 650b2dc6eb510b28943873a4dec2cc022e168d8b Mon Sep 17 00:00:00 2001 From: Bartosz Podrygajlo Date: Wed, 18 Dec 2024 14:22:38 +0100 Subject: [PATCH 18/18] A script to run CI tests locally. Added ./run_locally.sh script. The script takes one argument being the testcase that one wants to run. The testcase is run locally. This means that some testcases that utilize hardware resources will not run. However most if not all rfsimulator testcases should run as in CI. To this end, the CI python scripting framework was updated, adding --local flag which changes the script behavior as follows: - overrides and XML elements to 'localhost' so all commands are executed locally - Avoid running image pull, image cleanup and workspace creation steps of the scripts: user is responsible for that --- ci-scripts/args_parse.py | 6 +++- ci-scripts/cls_cmd.py | 2 ++ ci-scripts/helpreadme.py | 2 ++ ci-scripts/main.py | 60 ++++++++++++++++++++++++++------------- ci-scripts/run_locally.sh | 35 +++++++++++++++++++++++ 5 files changed, 84 insertions(+), 21 deletions(-) create mode 100755 ci-scripts/run_locally.sh diff --git a/ci-scripts/args_parse.py b/ci-scripts/args_parse.py index c1a13eb9bf..cbf4159da2 100644 --- a/ci-scripts/args_parse.py +++ b/ci-scripts/args_parse.py @@ -47,6 +47,7 @@ def ArgsParse(argvs,CiTestObj,RAN,HTML,EPC,CONTAINERS,HELP,SCA,PHYSIM,CLUSTER): py_param_file_present = False py_params={} + force_local = False while len(argvs) > 1: myArgv = argvs.pop(1) # 0th is this file's name @@ -54,6 +55,9 @@ def ArgsParse(argvs,CiTestObj,RAN,HTML,EPC,CONTAINERS,HELP,SCA,PHYSIM,CLUSTER): if re.match('^\-\-help$', myArgv, re.IGNORECASE): HELP.GenericHelp(CONST.Version) sys.exit(0) + if re.match('^\-\-local$', myArgv, re.IGNORECASE): + force_local = True + #--apply= as parameters file, to replace inline parameters elif re.match('^\-\-Apply=(.+)$', myArgv, re.IGNORECASE): @@ -271,4 +275,4 @@ def ArgsParse(argvs,CiTestObj,RAN,HTML,EPC,CONTAINERS,HELP,SCA,PHYSIM,CLUSTER): HELP.GenericHelp(CONST.Version) sys.exit('Invalid Parameter: ' + myArgv) - return py_param_file_present, py_params, mode + return py_param_file_present, py_params, mode, force_local diff --git a/ci-scripts/cls_cmd.py b/ci-scripts/cls_cmd.py index 8784b70831..b338a6938f 100644 --- a/ci-scripts/cls_cmd.py +++ b/ci-scripts/cls_cmd.py @@ -217,6 +217,8 @@ class RemoteCmd(Cmd): return client def _lookup_ssh_config(hostname): + if is_local(hostname): + raise ValueError("Using localhost as SSH target is not allowed: use LocalCmd instead.") ssh_config = paramiko.SSHConfig() user_config_file = os.path.expanduser("~/.ssh/config") if os.path.exists(user_config_file): diff --git a/ci-scripts/helpreadme.py b/ci-scripts/helpreadme.py index 9ecba905ac..e1640fc899 100644 --- a/ci-scripts/helpreadme.py +++ b/ci-scripts/helpreadme.py @@ -43,6 +43,8 @@ def GenericHelp(vers): print(' InitiateHtml, FinalizeHtml') print(' TerminateeNB, TerminateHSS, TerminateMME, TerminateSPGW') print(' LogCollectBuild, LogCollecteNB, LogCollectHSS, LogCollectMME, LogCollectSPGW, LogCollectPing, LogCollectIperf') + print(' --local Force local execution: rewrites the test xml script before running to always execute on localhost. Assumes') + print(' images are available locally, will not remove any images and will run inside the current repo directory') def GitSrvHelp(repository,branch,commit,mergeallow,targetbranch): print(' --ranRepository=[OAI RAN Repository URL] -- ' + repository) diff --git a/ci-scripts/main.py b/ci-scripts/main.py index bba0f3f57f..7a625eaa7d 100644 --- a/ci-scripts/main.py +++ b/ci-scripts/main.py @@ -212,13 +212,17 @@ def ExecuteActionWithParam(action): elif action == 'Initialize_UE' or action == 'Attach_UE' or action == 'Detach_UE' or action == 'Terminate_UE' or action == 'CheckStatusUE' or action == 'DataEnable_UE' or action == 'DataDisable_UE': CiTestObj.ue_ids = test.findtext('id').split(' ') - if test.findtext('nodes'): - CiTestObj.nodes = test.findtext('nodes').split(' ') - if len(CiTestObj.ue_ids) != len(CiTestObj.nodes): - logging.error('Number of Nodes are not equal to the total number of UEs') - sys.exit("Mismatch in number of Nodes and UIs") + if force_local: + # Change all execution targets to localhost + CiTestObj.nodes = ['localhost'] * len(CiTestObj.ue_ids) else: - CiTestObj.nodes = [None] * len(CiTestObj.ue_ids) + if test.findtext('nodes'): + CiTestObj.nodes = test.findtext('nodes').split(' ') + if len(CiTestObj.ue_ids) != len(CiTestObj.nodes): + logging.error('Number of Nodes are not equal to the total number of UEs') + sys.exit("Mismatch in number of Nodes and UIs") + else: + CiTestObj.nodes = [None] * len(CiTestObj.ue_ids) if action == 'Initialize_UE': success = CiTestObj.InitializeUE(HTML) elif action == 'Attach_UE': @@ -238,13 +242,17 @@ def ExecuteActionWithParam(action): CiTestObj.ping_args = test.findtext('ping_args') CiTestObj.ping_packetloss_threshold = test.findtext('ping_packetloss_threshold') CiTestObj.ue_ids = test.findtext('id').split(' ') - if test.findtext('nodes'): - CiTestObj.nodes = test.findtext('nodes').split(' ') - if len(CiTestObj.ue_ids) != len(CiTestObj.nodes): - logging.error('Number of Nodes are not equal to the total number of UEs') - sys.exit("Mismatch in number of Nodes and UIs") + if force_local: + # Change all execution targets to localhost + CiTestObj.nodes = ['localhost'] * len(CiTestObj.ue_ids) else: - CiTestObj.nodes = [None] * len(CiTestObj.ue_ids) + if test.findtext('nodes'): + CiTestObj.nodes = test.findtext('nodes').split(' ') + if len(CiTestObj.ue_ids) != len(CiTestObj.nodes): + logging.error('Number of Nodes are not equal to the total number of UEs') + sys.exit("Mismatch in number of Nodes and UIs") + else: + CiTestObj.nodes = [None] * len(CiTestObj.ue_ids) ping_rttavg_threshold = test.findtext('ping_rttavg_threshold') or '' success = CiTestObj.Ping(HTML,EPC,CONTAINERS) @@ -252,15 +260,19 @@ def ExecuteActionWithParam(action): CiTestObj.iperf_args = test.findtext('iperf_args') CiTestObj.ue_ids = test.findtext('id').split(' ') CiTestObj.svr_id = test.findtext('svr_id') or None - if test.findtext('nodes'): - CiTestObj.nodes = test.findtext('nodes').split(' ') - if len(CiTestObj.ue_ids) != len(CiTestObj.nodes): - logging.error('Number of Nodes are not equal to the total number of UEs') - sys.exit("Mismatch in number of Nodes and UIs") + if force_local: + # Change all execution targets to localhost + CiTestObj.nodes = ['localhost'] * len(CiTestObj.ue_ids) else: - CiTestObj.nodes = [None] * len(CiTestObj.ue_ids) + if test.findtext('nodes'): + CiTestObj.nodes = test.findtext('nodes').split(' ') + if len(CiTestObj.ue_ids) != len(CiTestObj.nodes): + logging.error('Number of Nodes are not equal to the total number of UEs') + sys.exit("Mismatch in number of Nodes and UIs") + else: + CiTestObj.nodes = [None] * len(CiTestObj.ue_ids) if test.findtext('svr_node'): - CiTestObj.svr_node = test.findtext('svr_node') + CiTestObj.svr_node = test.findtext('svr_node') if not force_local else 'localhost' CiTestObj.iperf_packetloss_threshold = test.findtext('iperf_packetloss_threshold') CiTestObj.iperf_bitrate_threshold = test.findtext('iperf_bitrate_threshold') or '90' CiTestObj.iperf_profile = test.findtext('iperf_profile') or 'balanced' @@ -356,6 +368,9 @@ def ExecuteActionWithParam(action): elif action == 'Undeploy_Object': success = CONTAINERS.UndeployObject(HTML, RAN) elif action == 'Create_Workspace': + if force_local: + # Do not create a working directory when running locally. Current repo directory will be used + return True success = CONTAINERS.Create_Workspace(HTML) elif action == 'Run_Physim': @@ -375,6 +390,9 @@ def ExecuteActionWithParam(action): success = CONTAINERS.Push_Image_to_Local_Registry(HTML, svr_id) elif action == 'Pull_Local_Registry' or action == 'Clean_Test_Server_Images': + if force_local: + # Do not pull or remove images when running locally. User is supposed to handle image creation & cleanup + return True svr_id = test.findtext('svr_id') images = test.findtext('images').split() # hack: for FlexRIC, we need to overwrite the tag to use @@ -463,7 +481,9 @@ CLUSTER = cls_cluster.Cluster() #----------------------------------------------------------- import args_parse -py_param_file_present, py_params, mode = args_parse.ArgsParse(sys.argv,CiTestObj,RAN,HTML,EPC,CONTAINERS,HELP,SCA,PHYSIM,CLUSTER) +# Force local execution, move all execution targets to localhost +force_local = False +py_param_file_present, py_params, mode, force_local = args_parse.ArgsParse(sys.argv,CiTestObj,RAN,HTML,EPC,CONTAINERS,HELP,SCA,PHYSIM,CLUSTER) diff --git a/ci-scripts/run_locally.sh b/ci-scripts/run_locally.sh new file mode 100755 index 0000000000..c8e9a289f0 --- /dev/null +++ b/ci-scripts/run_locally.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e +SHORT_COMMIT_SHA=$(git rev-parse --short=8 HEAD) +COMMIT_SHA=$(git rev-parse HEAD) +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +REPO_PATH=$(dirname $(realpath $0))/../ +TESTCASE=$1 + +if [ $# -eq 0 ] + then + echo "Provide a testcase as an argument" + exit 1 +fi + +# The script assumes you've build the following images: +# +# docker build . -f docker/Dockerfile.gNB.ubuntu22 -t oai-gnb +# docker build . -f docker/Dockerfile.nrUE.ubuntu22 -t oai-nr-ue +# +# The images above depend on the following images: +# +# docker build . -f docker/Dockerfile.build.ubuntu22 -t ran-build +# dokcer build . -f docker/Dockerfile.base.ubuntu22 -t ran-base + +docker tag oai-nr-ue oai-ci/oai-nr-ue:develop-${SHORT_COMMIT_SHA} +docker tag oai-gnb oai-ci/oai-gnb:develop-${SHORT_COMMIT_SHA} + +python3 main.py --mode=TesteNB --ranRepository=NONE --ranBranch=${CURRENT_BRANCH} \ + --ranCommitID=${COMMIT_SHA} --ranAllowMerge=false \ + --ranTargetBranch=NONE --eNBIPAddress=NONE --eNBUserName=NONE --eNBPassword=NONE \ + --eNBSourceCodePath=NONE --EPCIPAddress=NONE --EPCType=OAI --eNBPassword=NONE \ + --eNBSourceCodePath=${REPO_PATH} --EPCIPAddress=NONE \ + --EPCUserName=NONE --EPCPassword=NONE --EPCSourceCodePath=NONE \ + --XMLTestFile=xml_files/${TESTCASE} --local