This commit is contained in:
2026-09-19 23:28:08 +09:00
parent c50fe1af53
commit 67717ed5c1
43 changed files with 3145 additions and 294 deletions
+189
View File
@@ -13,10 +13,199 @@
#include "nr_common.h"
#include <limits.h>
#include <math.h>
#include <string.h>
#define C_SRS_NUMBER (64)
#define B_SRS_NUMBER (4)
void nr_pdsch_rank_capacity_scores(const float eigenvalues[NR_MAX_NB_LAYERS],
uint8_t max_rank,
float noise_power,
float scores[NR_MAX_NB_LAYERS])
{
AssertFatal(max_rank >= 1 && max_rank <= NR_MAX_NB_LAYERS,
"Invalid maximum PDSCH rank %u\n",
max_rank);
const float finite_noise = isfinite(noise_power) && noise_power > 0.0f ? noise_power : 1.0f;
for (int rank = 1; rank <= NR_MAX_NB_LAYERS; rank++) {
scores[rank - 1] = 0.0f;
if (rank > max_rank)
continue;
for (int layer = 0; layer < rank; layer++) {
const float eigenvalue = isfinite(eigenvalues[layer]) && eigenvalues[layer] > 0.0f
? eigenvalues[layer]
: 0.0f;
scores[rank - 1] += log2f(1.0f + eigenvalue / (rank * finite_noise));
}
}
}
uint8_t nr_pdsch_select_rank(const float scores[NR_MAX_NB_LAYERS],
uint8_t max_rank,
float relative_margin)
{
AssertFatal(max_rank >= 1 && max_rank <= NR_MAX_NB_LAYERS,
"Invalid maximum PDSCH rank %u\n",
max_rank);
const float margin = isfinite(relative_margin) && relative_margin > 0.0f ? relative_margin : 0.0f;
uint8_t best_rank = 1;
float best_score = isfinite(scores[0]) ? scores[0] : 0.0f;
for (uint8_t rank = 2; rank <= max_rank; rank++) {
const float candidate = isfinite(scores[rank - 1]) ? scores[rank - 1] : 0.0f;
if (candidate > best_score * (1.0f + margin)) {
best_rank = rank;
best_score = candidate;
}
}
return best_rank;
}
uint8_t nr_type1_sp_4x1_i11_count(uint8_t layers)
{
switch (layers) {
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
return 16;
case 7:
case 8:
return 8;
default:
return 0;
}
}
uint8_t nr_type1_sp_4x1_i13_count(uint8_t layers)
{
switch (layers) {
case 1:
case 5:
case 6:
case 7:
case 8:
return 1;
case 2:
return 4;
case 3:
case 4:
return 3;
default:
return 0;
}
}
uint8_t nr_type1_sp_4x1_i2_count(uint8_t layers)
{
if (layers == 1)
return 4;
return layers >= 2 && layers <= 8 ? 2 : 0;
}
bool nr_type1_sp_4x1_weights(uint8_t layers,
uint8_t i11,
uint8_t i13,
uint8_t i2,
c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS])
{
memset(weights, 0, NR_MAX_NB_LAYERS * NR_MAX_CSI_PORTS * sizeof(**weights));
const uint8_t i11_count = nr_type1_sp_4x1_i11_count(layers);
const uint8_t i13_count = nr_type1_sp_4x1_i13_count(layers);
const uint8_t i2_count = nr_type1_sp_4x1_i2_count(layers);
if (i11_count == 0 || i11 >= i11_count || i13 >= i13_count || i2 >= i2_count)
return false;
/* TS 38.214 Tables 5.2.2.2.1-5--12, specialized to the one-dimensional
* N1=4 panel. O1=4, so adjacent orthogonal DFT beams are four indices
* apart. Values 0..3 below encode 1, -1, phi, and -phi respectively. */
uint8_t beam_offset[NR_MAX_NB_LAYERS] = {0};
uint8_t polarization[NR_MAX_NB_LAYERS] = {0};
switch (layers) {
case 1:
polarization[0] = 2;
break;
case 2:
beam_offset[1] = 4 * i13;
polarization[0] = 2;
polarization[1] = 3;
break;
case 3:
case 4: {
const uint8_t offset = 4 * (i13 + 1);
beam_offset[1] = offset;
beam_offset[3] = offset;
polarization[0] = 2;
polarization[1] = 2;
polarization[2] = 3;
polarization[3] = 3;
break;
}
case 5: {
const uint8_t offsets[] = {0, 0, 4, 4, 8};
const uint8_t polarizations[] = {2, 3, 0, 1, 0};
memcpy(beam_offset, offsets, sizeof(offsets));
memcpy(polarization, polarizations, sizeof(polarizations));
break;
}
case 6: {
const uint8_t offsets[] = {0, 0, 4, 4, 8, 8};
const uint8_t polarizations[] = {2, 3, 2, 3, 0, 1};
memcpy(beam_offset, offsets, sizeof(offsets));
memcpy(polarization, polarizations, sizeof(polarizations));
break;
}
case 7: {
const uint8_t offsets[] = {0, 0, 4, 8, 8, 12, 12};
const uint8_t polarizations[] = {2, 3, 2, 0, 1, 0, 1};
memcpy(beam_offset, offsets, sizeof(offsets));
memcpy(polarization, polarizations, sizeof(polarizations));
break;
}
case 8: {
const uint8_t offsets[] = {0, 0, 4, 4, 8, 8, 12, 12};
const uint8_t polarizations[] = {2, 3, 2, 3, 0, 1, 0, 1};
memcpy(beam_offset, offsets, sizeof(offsets));
memcpy(polarization, polarizations, sizeof(polarizations));
break;
}
}
const float scale = 1.0f / sqrtf((float)layers);
const float phi_re = cosf((float)M_PI * i2 / 2.0f);
const float phi_im = sinf((float)M_PI * i2 / 2.0f);
for (int layer = 0; layer < layers; layer++) {
const int beam = i11 + beam_offset[layer];
float pol_re = 1.0f;
float pol_im = 0.0f;
switch (polarization[layer]) {
case 1:
pol_re = -1.0f;
break;
case 2:
pol_re = phi_re;
pol_im = phi_im;
break;
case 3:
pol_re = -phi_re;
pol_im = -phi_im;
break;
}
for (int element = 0; element < 4; element++) {
const float angle = 2.0f * (float)M_PI * element * beam / 16.0f;
const float beam_re = cosf(angle);
const float beam_im = sinf(angle);
weights[layer][element].r = lrintf(SHRT_MAX * scale * beam_re);
weights[layer][element].i = lrintf(SHRT_MAX * scale * beam_im);
weights[layer][element + 4].r = lrintf(SHRT_MAX * scale * (pol_re * beam_re - pol_im * beam_im));
weights[layer][element + 4].i = lrintf(SHRT_MAX * scale * (pol_re * beam_im + pol_im * beam_re));
}
}
return true;
}
/* TS 38.211 Table 6.4.1.4.3-1: SRS bandwidth configuration */
static const unsigned short srs_bandwidth_config[C_SRS_NUMBER][B_SRS_NUMBER][2] = {
/* B_SRS = 0 B_SRS = 1 B_SRS = 2 B_SRS = 3 */
+58
View File
@@ -9,6 +9,7 @@
#ifndef __COMMON_UTILS_NR_NR_COMMON__H__
#define __COMMON_UTILS_NR_NR_COMMON__H__
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include "assertions.h"
@@ -93,6 +94,63 @@ static inline uint8_t nr_pdsch_layers_per_codeword(uint8_t layers, uint8_t codew
return layers / 2 + (codeword == 1 ? layers % 2 : 0);
}
/* A two-TB PDSCH has two separate HARQ-ACK decisions even though both TBs
* share one HARQ process. ACKs for padding-only replacement codewords must
* not clear the other TB's pending retransmission. */
static inline uint8_t nr_pdsch_pending_after_ack(uint8_t pending_mask,
uint8_t active_mask,
uint8_t ack_mask)
{
AssertFatal(active_mask == 1 || active_mask == 3, "Invalid active codeword mask %u\n", active_mask);
const uint8_t pending = pending_mask ? pending_mask : active_mask;
return pending & ~(ack_mask & active_mask);
}
/** Compute equal-power spatial-capacity scores for candidate PDSCH ranks.
*
* eigenvalues contains the descending eigenvalues of H^H H. The score for
* rank r is sum_i log2(1 + eigenvalue_i / (r * noise_power)): total TX power
* is held constant and divided equally among the r layers. Scores estimate
* the useful spatial multiplexing opportunity; MCS, coding, and scheduling
* overhead are intentionally left to MAC.
*/
void nr_pdsch_rank_capacity_scores(const float eigenvalues[NR_MAX_NB_LAYERS],
uint8_t max_rank,
float noise_power,
float scores[NR_MAX_NB_LAYERS]);
/** Select the lowest rank that is materially better than all lower ranks.
* relative_margin is expressed as a fraction (0.05 means a five-percent
* required improvement), which avoids switching rank for negligible model
* gains. Returns a one-based rank.
*/
uint8_t nr_pdsch_select_rank(const float scores[NR_MAX_NB_LAYERS],
uint8_t max_rank,
float relative_margin);
/** Number of valid i_1_1 values for the rank-1--8 Type-I single-panel
* codebooks with (N1,N2,O1,O2)=(4,1,4,1). This is the eight-port geometry
* used by the experimental rank-8 branch. Returns zero for invalid ranks.
*/
uint8_t nr_type1_sp_4x1_i11_count(uint8_t layers);
/** Number of valid i_1_3 and i_2 values in the same codebook. */
uint8_t nr_type1_sp_4x1_i13_count(uint8_t layers);
uint8_t nr_type1_sp_4x1_i2_count(uint8_t layers);
/** Build one TS 38.214 Type-I single-panel rank-1--8 precoder for the
* eight-port (N1,N2,O1,O2)=(4,1,4,1), codebookMode=1 geometry.
*
* weights is indexed [layer][antenna port]. Coefficients use OAI's existing
* Q15 codebook convention (1/sqrt(layers) scaling); unused entries are zero.
* Returns false when layers, i11, i13, or i2 is outside this codebook.
*/
bool nr_type1_sp_4x1_weights(uint8_t layers,
uint8_t i11,
uint8_t i13,
uint8_t i2,
c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS]);
// Since the IQ samples are represented by SQ15 R+I (see https://en.wikipedia.org/wiki/Q_(number_format)) we need to compensate when
// calcualting signal energy. Instead of shifting each sample right by 15, we can normalize the result in dB scale once its
// calcualted. Signal energy is calculated using RMS^2, where each sample is squared before taking the average of the sum, therefore
+3
View File
@@ -5,3 +5,6 @@ target_link_libraries(test_nr_common PRIVATE nr_common GTest::gtest minimal_lib)
add_dependencies(tests test_nr_common)
add_test(NAME test_nr_common
COMMAND ./test_nr_common)
add_executable(nr_rank_selector_benchmark nr_rank_selector_benchmark.cpp)
target_link_libraries(nr_rank_selector_benchmark PRIVATE nr_common minimal_lib)
@@ -0,0 +1,63 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include <algorithm>
#include <cmath>
#include <cstdio>
extern "C" {
#include "nr_common.h"
}
struct rank_scenario_t {
const char *name;
float eigenvalues[NR_MAX_NB_LAYERS];
float noise_power;
};
static uint8_t legacy_rank(const float eigenvalues[NR_MAX_NB_LAYERS])
{
uint8_t rank = 1;
for (int layer = 1; layer < NR_MAX_NB_LAYERS; layer++) {
if (eigenvalues[layer] <= 0.0f
|| 10.0f * std::log10(eigenvalues[0] / eigenvalues[layer]) >= 8.0f)
break;
rank++;
}
return rank;
}
int main()
{
const rank_scenario_t scenarios[] = {
{"dominant_one", {100.0f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f}, 1.0f},
{"four_modes", {100.0f, 80.0f, 60.0f, 40.0f, 0.01f, 0.01f, 0.01f, 0.01f}, 1.0f},
{"eight_modes", {100.0f, 95.0f, 90.0f, 85.0f, 80.0f, 75.0f, 70.0f, 65.0f}, 1.0f},
{"balanced_low_snr", {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, 10.0f},
{"legacy_false_high_rank", {1.0f, 0.16f, 0.16f, 0.16f, 0.16f, 0.16f, 0.16f, 0.16f}, 10.0f},
{"weak_eighth", {100.0f, 90.0f, 80.0f, 70.0f, 60.0f, 50.0f, 40.0f, 5.0f}, 1.0f},
};
std::puts("scenario,policy,selected_rank,selected_capacity_bps_hz");
for (const rank_scenario_t &scenario : scenarios) {
float scores[NR_MAX_NB_LAYERS] = {};
nr_pdsch_rank_capacity_scores(scenario.eigenvalues,
NR_MAX_NB_LAYERS,
scenario.noise_power,
scores);
const uint8_t capacity_rank = nr_pdsch_select_rank(scores, NR_MAX_NB_LAYERS, 0.05f);
const uint8_t legacy = legacy_rank(scenario.eigenvalues);
const uint8_t policies[] = {capacity_rank, legacy, 1, 4, 8};
const char *policy_names[] = {"capacity", "legacy", "fixed1", "fixed4", "fixed8"};
for (int policy = 0; policy < 5; policy++) {
const uint8_t rank = std::min<uint8_t>(policies[policy], NR_MAX_NB_LAYERS);
std::printf("%s,%s,%u,%.6f\n",
scenario.name,
policy_names[policy],
rank,
scores[rank - 1]);
}
}
return 0;
}
+108
View File
@@ -2,6 +2,7 @@
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include <climits>
#include <gtest/gtest.h>
extern "C" {
#include "nr_common.h"
@@ -37,6 +38,113 @@ TEST(nr_common, pdsch_codeword_layer_split)
}
}
TEST(nr_common, pdsch_two_codeword_ack_tracks_only_pending_transport_blocks)
{
EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 0), 3); // both NACK or DTX
EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 1), 2); // CW0 ACK, CW1 NACK
EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 2), 1); // CW0 NACK, CW1 ACK
EXPECT_EQ(nr_pdsch_pending_after_ack(0, 3, 3), 0); // both ACK
EXPECT_EQ(nr_pdsch_pending_after_ack(2, 3, 1), 2); // padding CW0 ACK is irrelevant
EXPECT_EQ(nr_pdsch_pending_after_ack(2, 3, 2), 0); // pending CW1 ACK finishes
EXPECT_EQ(nr_pdsch_pending_after_ack(1, 3, 2), 1); // padding CW1 ACK is irrelevant
EXPECT_EQ(nr_pdsch_pending_after_ack(1, 3, 1), 0); // pending CW0 ACK finishes
EXPECT_EQ(nr_pdsch_pending_after_ack(0, 1, 1), 0); // one-codeword legacy HARQ
}
TEST(nr_common, pdsch_rank_capacity_prefers_independent_modes)
{
float eigenvalues[NR_MAX_NB_LAYERS];
for (float &eigenvalue : eigenvalues)
eigenvalue = 100.0f;
float scores[NR_MAX_NB_LAYERS] = {};
nr_pdsch_rank_capacity_scores(eigenvalues, NR_MAX_NB_LAYERS, 1.0f, scores);
for (int rank = 1; rank < NR_MAX_NB_LAYERS; rank++)
EXPECT_GT(scores[rank], scores[rank - 1]);
EXPECT_EQ(nr_pdsch_select_rank(scores, NR_MAX_NB_LAYERS, 0.05f), 8);
}
TEST(nr_common, pdsch_rank_capacity_rejects_weak_modes)
{
float eigenvalues[NR_MAX_NB_LAYERS] = {100.0f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f, 0.01f};
float scores[NR_MAX_NB_LAYERS] = {};
nr_pdsch_rank_capacity_scores(eigenvalues, NR_MAX_NB_LAYERS, 1.0f, scores);
EXPECT_EQ(nr_pdsch_select_rank(scores, NR_MAX_NB_LAYERS, 0.05f), 1);
}
TEST(nr_common, pdsch_rank_capacity_respects_rank_and_gain_margin)
{
const float scores[NR_MAX_NB_LAYERS] = {10.0f, 10.4f, 10.6f, 20.0f, 30.0f, 40.0f, 50.0f, 60.0f};
EXPECT_EQ(nr_pdsch_select_rank(scores, 3, 0.05f), 3);
EXPECT_EQ(nr_pdsch_select_rank(scores, 2, 0.05f), 1);
}
TEST(nr_common, type1_sp_4x1_parameter_ranges)
{
constexpr uint8_t expected_i11[] = {16, 16, 16, 16, 16, 16, 8, 8};
constexpr uint8_t expected_i13[] = {1, 4, 3, 3, 1, 1, 1, 1};
constexpr uint8_t expected_i2[] = {4, 2, 2, 2, 2, 2, 2, 2};
for (uint8_t layers = 1; layers <= NR_MAX_NB_LAYERS; layers++) {
EXPECT_EQ(nr_type1_sp_4x1_i11_count(layers), expected_i11[layers - 1]);
EXPECT_EQ(nr_type1_sp_4x1_i13_count(layers), expected_i13[layers - 1]);
EXPECT_EQ(nr_type1_sp_4x1_i2_count(layers), expected_i2[layers - 1]);
}
c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS];
EXPECT_FALSE(nr_type1_sp_4x1_weights(0, 0, 0, 0, weights));
EXPECT_FALSE(nr_type1_sp_4x1_weights(8, 8, 0, 0, weights));
EXPECT_FALSE(nr_type1_sp_4x1_weights(8, 0, 1, 0, weights));
EXPECT_FALSE(nr_type1_sp_4x1_weights(8, 0, 0, 2, weights));
}
TEST(nr_common, type1_sp_4x1_precoders_are_orthogonal)
{
for (uint8_t layers = 1; layers <= NR_MAX_NB_LAYERS; layers++) {
const uint8_t i11_count = nr_type1_sp_4x1_i11_count(layers);
for (uint8_t i11 = 0; i11 < i11_count; i11++) {
for (uint8_t i13 = 0; i13 < nr_type1_sp_4x1_i13_count(layers); i13++) {
for (uint8_t i2 = 0; i2 < nr_type1_sp_4x1_i2_count(layers); i2++) {
c16_t weights[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS];
ASSERT_TRUE(nr_type1_sp_4x1_weights(layers, i11, i13, i2, weights));
const int64_t expected_norm = 8LL * SHRT_MAX * SHRT_MAX / layers;
const int64_t tolerance = expected_norm / 1000;
for (int a = 0; a < layers; a++) {
int64_t diagonal = 0;
for (int port = 0; port < 8; port++)
diagonal += (int64_t)weights[a][port].r * weights[a][port].r
+ (int64_t)weights[a][port].i * weights[a][port].i;
EXPECT_NEAR((double)diagonal, (double)expected_norm, (double)tolerance);
for (int b = a + 1; b < layers; b++) {
int64_t inner_re = 0;
int64_t inner_im = 0;
for (int port = 0; port < 8; port++) {
inner_re += (int64_t)weights[a][port].r * weights[b][port].r
+ (int64_t)weights[a][port].i * weights[b][port].i;
inner_im += (int64_t)weights[a][port].r * weights[b][port].i
- (int64_t)weights[a][port].i * weights[b][port].r;
}
EXPECT_NEAR((double)inner_re, 0.0, (double)tolerance);
EXPECT_NEAR((double)inner_im, 0.0, (double)tolerance);
}
}
}
}
}
}
}
TEST(nr_common, type1_sp_4x1_i2_rotates_cross_polarization)
{
c16_t i2_zero[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS];
c16_t i2_one[NR_MAX_NB_LAYERS][NR_MAX_CSI_PORTS];
ASSERT_TRUE(nr_type1_sp_4x1_weights(8, 0, 0, 0, i2_zero));
ASSERT_TRUE(nr_type1_sp_4x1_weights(8, 0, 0, 1, i2_one));
EXPECT_EQ(i2_zero[0][4].i, 0);
EXPECT_EQ(i2_one[0][4].r, 0);
EXPECT_EQ(i2_zero[0][4].r, i2_one[0][4].i);
}
int main(int argc, char **argv)
{