mirror of
https://github.com/gnss-sdr/gnss-sdr
synced 2026-10-08 08:41:52 +00:00
271 lines
8.7 KiB
C++
271 lines
8.7 KiB
C++
/*!
|
|
* \file bit_synchronizer_test.cc
|
|
* \brief Deterministic tests for the histogram-based bit synchronizer.
|
|
* \author Carles Fernandez-Prades, 2026. cfernandez(at)cttc.es
|
|
*
|
|
* -----------------------------------------------------------------------------
|
|
*
|
|
* GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
|
* This file is part of GNSS-SDR.
|
|
*
|
|
* Copyright (C) 2010-2026 (see AUTHORS file for a list of contributors)
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* -----------------------------------------------------------------------------
|
|
*/
|
|
|
|
#include "bit_synchronizer.h"
|
|
#include <gtest/gtest.h>
|
|
#include <cmath>
|
|
#include <complex>
|
|
#include <cstdint>
|
|
|
|
namespace
|
|
{
|
|
HistogramBitSynchronizer::Config default_test_config()
|
|
{
|
|
HistogramBitSynchronizer::Config cfg;
|
|
cfg.bit_period_ms = 20;
|
|
cfg.epoch_ms = 1;
|
|
cfg.min_events_for_lock = 4;
|
|
cfg.dominance_ratio = 1.0;
|
|
cfg.runner_up_margin = 0.0;
|
|
cfg.stable_best_required = 1;
|
|
cfg.min_prompt_mag = 0.0F;
|
|
cfg.transition_window_epochs = 3;
|
|
cfg.transition_confidence = 0.6;
|
|
cfg.tentative_events_required = 0;
|
|
cfg.use_phase_dot_detector = true;
|
|
return cfg;
|
|
}
|
|
|
|
|
|
bool feed_alternating_bits(HistogramBitSynchronizer& synchronizer,
|
|
int edge_phase,
|
|
int transition_count,
|
|
float carrier_phase_step_rad = 0.0F,
|
|
int detection_window_epochs = 3)
|
|
{
|
|
const int period = synchronizer.bins();
|
|
if (period <= 0)
|
|
{
|
|
ADD_FAILURE() << "The bit synchronizer must have a positive number of bins";
|
|
return false;
|
|
}
|
|
const int first_edge = (edge_phase == 0) ? period : edge_phase;
|
|
const int epochs = first_edge + (transition_count - 1) * period + detection_window_epochs;
|
|
float polarity = 1.0F;
|
|
bool lock_event = false;
|
|
|
|
for (int k = 0; k < epochs; ++k)
|
|
{
|
|
if (k > 0 && (k % period) == edge_phase)
|
|
{
|
|
polarity = -polarity;
|
|
}
|
|
const float phase = carrier_phase_step_rad * static_cast<float>(k);
|
|
const std::complex<float> prompt = polarity * std::complex<float>(std::cos(phase), std::sin(phase));
|
|
lock_event = synchronizer.update(prompt, true) || lock_event;
|
|
}
|
|
return lock_event;
|
|
}
|
|
|
|
|
|
void feed_until_epoch(HistogramBitSynchronizer& synchronizer,
|
|
std::int64_t target_epoch,
|
|
float& polarity)
|
|
{
|
|
while (synchronizer.get_epoch_count() <= target_epoch)
|
|
{
|
|
synchronizer.update(std::complex<float>(polarity, 0.0F), true);
|
|
}
|
|
}
|
|
|
|
|
|
void add_transition_at(HistogramBitSynchronizer& synchronizer,
|
|
std::int64_t epoch,
|
|
float& polarity)
|
|
{
|
|
feed_until_epoch(synchronizer, epoch - 1, polarity);
|
|
polarity = -polarity;
|
|
synchronizer.update(std::complex<float>(polarity, 0.0F), true);
|
|
}
|
|
} // namespace
|
|
|
|
|
|
TEST(BitSynchronizerTest, LocksAtKnownPhase)
|
|
{
|
|
HistogramBitSynchronizer synchronizer(default_test_config());
|
|
|
|
EXPECT_TRUE(feed_alternating_bits(synchronizer, 7, 4));
|
|
EXPECT_TRUE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), 7);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, HandlesPhaseZeroWraparound)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.min_events_for_lock = 3;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
|
|
EXPECT_TRUE(feed_alternating_bits(synchronizer, 0, 3));
|
|
EXPECT_EQ(synchronizer.edge_phase(), 0);
|
|
EXPECT_TRUE(synchronizer.is_edge_epoch(0));
|
|
EXPECT_TRUE(synchronizer.is_edge_epoch(20));
|
|
EXPECT_FALSE(synchronizer.is_edge_epoch(19));
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, IgnoresPromptsBelowMagnitudeThreshold)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.bit_period_ms = 5;
|
|
cfg.min_events_for_lock = 1;
|
|
cfg.min_prompt_mag = 0.5F;
|
|
cfg.transition_window_epochs = 1;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
|
|
EXPECT_FALSE(synchronizer.update(std::complex<float>(1.0F, 0.0F), true));
|
|
EXPECT_FALSE(synchronizer.update(std::complex<float>(0.1F, 0.0F), true));
|
|
EXPECT_FALSE(synchronizer.update(std::complex<float>(-1.0F, 0.0F), true));
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
|
|
EXPECT_FALSE(synchronizer.update(std::complex<float>(-1.0F, 0.0F), true));
|
|
EXPECT_TRUE(synchronizer.update(std::complex<float>(1.0F, 0.0F), true));
|
|
EXPECT_EQ(synchronizer.edge_phase(), 4);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, AdjacentBinCompetitionRequiresDominantWinner)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.bit_period_ms = 10;
|
|
cfg.min_events_for_lock = 6;
|
|
cfg.dominance_ratio = 0.5;
|
|
cfg.runner_up_margin = 0.4;
|
|
cfg.stable_best_required = 2;
|
|
cfg.transition_window_epochs = 1;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
float polarity = 1.0F;
|
|
|
|
synchronizer.update(std::complex<float>(polarity, 0.0F), true);
|
|
add_transition_at(synchronizer, 4, polarity);
|
|
add_transition_at(synchronizer, 15, polarity);
|
|
add_transition_at(synchronizer, 24, polarity);
|
|
add_transition_at(synchronizer, 35, polarity);
|
|
add_transition_at(synchronizer, 44, polarity);
|
|
add_transition_at(synchronizer, 54, polarity);
|
|
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
|
|
add_transition_at(synchronizer, 64, polarity);
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
add_transition_at(synchronizer, 74, polarity);
|
|
EXPECT_TRUE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), 4);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, RequiresIndependentEvidenceForStability)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.bit_period_ms = 5;
|
|
cfg.min_events_for_lock = 1;
|
|
cfg.stable_best_required = 2;
|
|
cfg.transition_window_epochs = 1;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
|
|
synchronizer.update(std::complex<float>(1.0F, 0.0F), true);
|
|
EXPECT_FALSE(synchronizer.update(std::complex<float>(-1.0F, 0.0F), true));
|
|
for (int k = 0; k < 4; ++k)
|
|
{
|
|
EXPECT_FALSE(synchronizer.update(std::complex<float>(-1.0F, 0.0F), true));
|
|
}
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
|
|
EXPECT_TRUE(synchronizer.update(std::complex<float>(1.0F, 0.0F), true));
|
|
EXPECT_TRUE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), 1);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, ToleratesResidualCarrierRotation)
|
|
{
|
|
HistogramBitSynchronizer synchronizer(default_test_config());
|
|
|
|
EXPECT_TRUE(feed_alternating_bits(synchronizer, 3, 4, 0.2F));
|
|
EXPECT_TRUE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), 3);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, RequiresConfiguredTentativeConfirmations)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.bit_period_ms = 10;
|
|
cfg.min_events_for_lock = 2;
|
|
cfg.stable_best_required = 1;
|
|
cfg.transition_window_epochs = 1;
|
|
cfg.tentative_events_required = 2;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
float polarity = 1.0F;
|
|
|
|
synchronizer.update(std::complex<float>(polarity, 0.0F), true);
|
|
add_transition_at(synchronizer, 4, polarity);
|
|
add_transition_at(synchronizer, 14, polarity);
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
|
|
add_transition_at(synchronizer, 24, polarity);
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
add_transition_at(synchronizer, 34, polarity);
|
|
EXPECT_TRUE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), 4);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, RejectsMismatchedTentativeConfirmation)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.bit_period_ms = 10;
|
|
cfg.min_events_for_lock = 2;
|
|
cfg.dominance_ratio = 0.5;
|
|
cfg.runner_up_margin = 0.1;
|
|
cfg.transition_window_epochs = 1;
|
|
cfg.tentative_events_required = 2;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
float polarity = 1.0F;
|
|
|
|
synchronizer.update(std::complex<float>(polarity, 0.0F), true);
|
|
add_transition_at(synchronizer, 4, polarity);
|
|
add_transition_at(synchronizer, 14, polarity);
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
|
|
add_transition_at(synchronizer, 25, polarity);
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
add_transition_at(synchronizer, 34, polarity);
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
add_transition_at(synchronizer, 44, polarity);
|
|
EXPECT_TRUE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), 4);
|
|
}
|
|
|
|
|
|
TEST(BitSynchronizerTest, ResetClearsStateAndAllowsReacquisition)
|
|
{
|
|
HistogramBitSynchronizer::Config cfg = default_test_config();
|
|
cfg.min_events_for_lock = 2;
|
|
HistogramBitSynchronizer synchronizer(cfg);
|
|
|
|
ASSERT_TRUE(feed_alternating_bits(synchronizer, 7, 2));
|
|
ASSERT_EQ(synchronizer.edge_phase(), 7);
|
|
|
|
synchronizer.reset();
|
|
EXPECT_FALSE(synchronizer.locked());
|
|
EXPECT_EQ(synchronizer.edge_phase(), -1);
|
|
EXPECT_EQ(synchronizer.epochs_until_next_edge(), -1);
|
|
|
|
EXPECT_TRUE(feed_alternating_bits(synchronizer, 11, 2));
|
|
EXPECT_EQ(synchronizer.edge_phase(), 11);
|
|
}
|