From b43dccbb6663c97d5c8243cc8043c3c92009fc0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:36:04 +0000 Subject: [PATCH 1/3] Initial plan From 40daaf2d1036c899eea544934fb5398ccfbcb5d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:39:46 +0000 Subject: [PATCH 2/3] Add Monte-Carlo FER simulator and CI integration Co-authored-by: sparkainlp-x <281253927+sparkainlp-x@users.noreply.github.com> --- .github/workflows/ci.yml | 14 ++ CMakeLists.txt | 28 +++ README.md | 15 ++ monte_carlo.cpp | 369 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 monte_carlo.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 203c14d..ed129bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,20 @@ jobs: LDPC_MAX_LATENCY_NS: 1000 run: ./build/ldpc_benchmark + - name: Run Monte-Carlo FER simulation + run: >- + ./build/monte_carlo_sim + --trials "${MONTE_CARLO_TRIALS:-10000}" + --csv fer_curve.csv + + - name: Upload Monte-Carlo FER curve + uses: actions/upload-artifact@v4 + with: + name: qldpc-fer-curve + if-no-files-found: error + retention-days: 30 + path: fer_curve.csv + hardware-bitstream-build: # Ne jamais exécuter du code d’une pull request externe sur un runner local. if: github.event_name != 'pull_request' diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f9c52b..bf7275c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ FetchContent_MakeAvailable(ldpc) option(BUILD_UNIT_TESTS "Build Catch2 unit tests" ON) option(BUILD_AXI_DMA_DRIVER "Build the Linux ARM AXI-DMA driver" OFF) option(BUILD_HIL_BENCHMARK "Build the hardware-in-the-loop benchmark" OFF) +option(BUILD_MONTE_CARLO_SIM "Build the Monte-Carlo FER simulator" ON) if(BUILD_AXI_DMA_DRIVER) if(NOT UNIX) message(FATAL_ERROR "BUILD_AXI_DMA_DRIVER requires a Unix-like target") @@ -54,12 +55,25 @@ endif() add_executable(qldpc_decoder main.cpp) add_executable(ldpc_benchmark benchmark.cpp) +if(BUILD_MONTE_CARLO_SIM) + add_executable(monte_carlo_sim monte_carlo.cpp) +endif() if(BUILD_UNIT_TESTS) add_executable(unit_tests tests/test_decoder.cpp) target_link_libraries(unit_tests PRIVATE Catch2::Catch2WithMain ldpc OpenMP::OpenMP_CXX) target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) add_test(NAME unit_tests COMMAND unit_tests) + if(BUILD_MONTE_CARLO_SIM) + add_test( + NAME monte_carlo_smoke + COMMAND monte_carlo_sim + --trials 50 + --ber-list 0.01 + --csv ${CMAKE_BINARY_DIR}/fer_curve_test.csv + --progress-interval 50 + ) + endif() endif() target_link_libraries(qldpc_decoder @@ -74,12 +88,26 @@ target_link_libraries(ldpc_benchmark OpenMP::OpenMP_CXX ) +if(BUILD_MONTE_CARLO_SIM) + target_link_libraries(monte_carlo_sim + PRIVATE + ldpc + OpenMP::OpenMP_CXX + ) +endif() + target_include_directories(qldpc_decoder PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(ldpc_benchmark PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if(BUILD_MONTE_CARLO_SIM) + target_include_directories(monte_carlo_sim PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +endif() if(NOT MSVC) set_property(TARGET qldpc_decoder PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) set_property(TARGET ldpc_benchmark PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) + if(BUILD_MONTE_CARLO_SIM) + set_property(TARGET monte_carlo_sim PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) + endif() if(BUILD_UNIT_TESTS) set_property(TARGET unit_tests PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) endif() diff --git a/README.md b/README.md index f398a7a..623452b 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,21 @@ Le projet construit également `ldpc_benchmark`, qui mesure une multiplication m Dans GitHub Actions, le benchmark échoue si la médiane atteint ou dépasse `1000 ns` (`1 µs`). Le seuil peut être changé avec la variable d’environnement `LDPC_MAX_LATENCY_NS`. Les runners GitHub-hosted étant virtualisés et partagés, le résultat est un contrôle de régression indicatif et ne remplace pas une mesure sur machine dédiée. +## Simulation Monte-Carlo FER + +Le binaire `monte_carlo_sim` exécute une simulation Monte-Carlo de taux d’erreur trame (FER) sur plusieurs points BER et exporte la courbe en CSV (`BER,FER,FramesAtBER,ErrorsAtBER,Timestamp`). + +```bash +./build/monte_carlo_sim --trials 10000 --csv fer_curve.csv +``` + +Options utiles : +- `--ber-list 0.001,0.005,0.01,0.02,0.05` +- `--seed 42` +- `--json fer_curve.json` (export optionnel) + +Dans la CI logicielle, la simulation est lancée avec `MONTE_CARLO_TRIALS` (défaut: `10000`) et publie `fer_curve.csv` comme artefact. + La première configuration télécharge automatiquement le dépôt amont `quantumgizmos/ldpc` dans le répertoire de build. Le fichier `CMakeLists.txt` utilise `-O3 -march=native -ffast-math -flto` sur les compilateurs non-MSVC et `/O2 /arch:AVX2` sous MSVC. > `-march=native` produit un binaire adapté à la machine de compilation. Pour distribuer le binaire sur d’autres processeurs, remplacez ce drapeau par une architecture cible portable. diff --git a/monte_carlo.cpp b/monte_carlo.cpp new file mode 100644 index 0000000..d593f92 --- /dev/null +++ b/monte_carlo.cpp @@ -0,0 +1,369 @@ +#include "bp.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct SimulationConfig { + std::size_t trials_per_ber = 10000; + std::vector ber_points{0.001, 0.005, 0.01, 0.02, 0.05}; + std::uint32_t seed = 42; + std::string csv_path = "fer_curve.csv"; + std::optional json_path; + std::size_t progress_interval = 1000; +}; + +struct FerResult { + double ber = 0.0; + std::size_t frames = 0; + std::size_t errors = 0; + double fer = 0.0; + double standard_error = 0.0; + double ci95_low = 0.0; + double ci95_high = 0.0; +}; + +constexpr int kRows = 2; +constexpr int kColumns = 4; +constexpr int kMaxIterations = 40; +constexpr double kBpScaling = 0.625; + +void print_usage(const char* program_name) +{ + std::cout << "Usage: " << program_name << " [options]\n" + << "Options:\n" + << " --trials Number of trials per BER (default: 10000)\n" + << " --ber-list Comma-separated BER values\n" + << " --seed RNG seed (default: 42)\n" + << " --csv CSV output path (default: fer_curve.csv)\n" + << " --json Optional JSON output path\n" + << " --progress-interval Progress print interval per BER (default: 1000)\n" + << " --help Show this help message\n"; +} + +bool parse_size(const std::string& raw_value, std::size_t& value) +{ + try { + const std::size_t parsed = std::stoull(raw_value); + value = parsed; + return true; + } catch (...) { + return false; + } +} + +bool parse_u32(const std::string& raw_value, std::uint32_t& value) +{ + try { + const unsigned long parsed = std::stoul(raw_value); + if (parsed > std::numeric_limits::max()) { + return false; + } + value = static_cast(parsed); + return true; + } catch (...) { + return false; + } +} + +bool parse_ber_list(const std::string& raw_value, std::vector& values) +{ + std::stringstream ss(raw_value); + std::string token; + std::vector parsed; + while (std::getline(ss, token, ',')) { + if (token.empty()) { + continue; + } + try { + const double ber = std::stod(token); + if (ber < 0.0 || ber > 1.0) { + return false; + } + parsed.push_back(ber); + } catch (...) { + return false; + } + } + if (parsed.empty()) { + return false; + } + values = std::move(parsed); + return true; +} + +bool parse_arguments(int argc, char** argv, SimulationConfig& config) +{ + for (int i = 1; i < argc; ++i) { + const std::string_view arg(argv[i]); + auto require_value = [&](const char* option_name) -> const char* { + if (i + 1 >= argc) { + std::cerr << "Missing value for option " << option_name << ".\n"; + return nullptr; + } + ++i; + return argv[i]; + }; + + if (arg == "--help") { + print_usage(argv[0]); + return false; + } + if (arg == "--trials") { + const char* value = require_value("--trials"); + if (!value || !parse_size(value, config.trials_per_ber) || + config.trials_per_ber == 0) { + std::cerr << "Invalid --trials value.\n"; + return false; + } + continue; + } + if (arg == "--ber-list") { + const char* value = require_value("--ber-list"); + if (!value || !parse_ber_list(value, config.ber_points)) { + std::cerr << "Invalid --ber-list value.\n"; + return false; + } + continue; + } + if (arg == "--seed") { + const char* value = require_value("--seed"); + if (!value || !parse_u32(value, config.seed)) { + std::cerr << "Invalid --seed value.\n"; + return false; + } + continue; + } + if (arg == "--csv") { + const char* value = require_value("--csv"); + if (!value) { + return false; + } + config.csv_path = value; + continue; + } + if (arg == "--json") { + const char* value = require_value("--json"); + if (!value) { + return false; + } + config.json_path = value; + continue; + } + if (arg == "--progress-interval") { + const char* value = require_value("--progress-interval"); + if (!value || !parse_size(value, config.progress_interval)) { + std::cerr << "Invalid --progress-interval value.\n"; + return false; + } + continue; + } + std::cerr << "Unknown option: " << arg << "\n"; + return false; + } + return true; +} + +std::string iso8601_now_utc() +{ + const std::time_t now = std::time(nullptr); + std::tm utc_time {}; +#if defined(_WIN32) + gmtime_s(&utc_time, &now); +#else + gmtime_r(&now, &utc_time); +#endif + std::ostringstream oss; + oss << std::put_time(&utc_time, "%Y-%m-%dT%H:%M:%SZ"); + return oss.str(); +} + +ldpc::bp::BpSparse build_pcm() +{ + ldpc::bp::BpSparse pcm(kRows, kColumns); + std::vector> parity_check{{0, 1, 3}, {1, 2}}; + pcm.csr_insert(parity_check); + return pcm; +} + +std::unique_ptr build_decoder(ldpc::bp::BpSparse& pcm, + double ber) +{ + const double stabilized_ber = std::clamp(ber, 1e-6, 0.49); + std::vector channel_probabilities(kColumns, stabilized_ber); + return std::make_unique( + pcm, channel_probabilities, kMaxIterations, ldpc::bp::MINIMUM_SUM, + ldpc::bp::PARALLEL, kBpScaling, 1, std::vector{}, 0, false, + ldpc::bp::SYNDROME); +} + +bool decode_matches_error(const std::vector& expected_error, + const std::vector& decoded) +{ + if (decoded.size() != expected_error.size()) { + return false; + } + for (std::size_t i = 0; i < expected_error.size(); ++i) { + if ((decoded[i] & 1U) != (expected_error[i] & 1U)) { + return false; + } + } + return true; +} + +FerResult run_single_ber(ldpc::bp::BpSparse& pcm, double ber, + const SimulationConfig& config, std::mt19937& rng) +{ + FerResult result; + result.ber = ber; + result.frames = config.trials_per_ber; + + auto decoder = build_decoder(pcm, ber); + std::bernoulli_distribution bit_error_distribution(ber); + std::vector error_pattern(kColumns, 0); + std::vector syndrome(kRows, 0); + + const std::size_t progress_step = + config.progress_interval == 0 ? config.trials_per_ber : config.progress_interval; + + for (std::size_t trial = 0; trial < config.trials_per_ber; ++trial) { + for (int col = 0; col < kColumns; ++col) { + error_pattern[static_cast(col)] = + static_cast(bit_error_distribution(rng)); + } + + pcm.mulvec(error_pattern, syndrome); + decoder->decode(syndrome); + + if (!decode_matches_error(error_pattern, decoder->decoding)) { + ++result.errors; + } + + const std::size_t done = trial + 1; + if ((done % progress_step) == 0 || done == config.trials_per_ber) { + std::cout << "BER=" << ber << " progress " << done << "/" + << config.trials_per_ber << ", frame_errors=" << result.errors + << "\n"; + } + } + + result.fer = static_cast(result.errors) / static_cast(result.frames); + result.standard_error = + std::sqrt(result.fer * (1.0 - result.fer) / static_cast(result.frames)); + const double ci95 = 1.96 * result.standard_error; + result.ci95_low = std::max(0.0, result.fer - ci95); + result.ci95_high = std::min(1.0, result.fer + ci95); + return result; +} + +bool write_csv(const std::string& output_path, const std::vector& results) +{ + std::ofstream csv(output_path); + if (!csv.is_open()) { + std::cerr << "Unable to open CSV output file: " << output_path << "\n"; + return false; + } + + csv << "BER,FER,FramesAtBER,ErrorsAtBER,Timestamp\n"; + for (const FerResult& result : results) { + csv << std::setprecision(10) << result.ber << "," << result.fer << "," + << result.frames << "," << result.errors << "," << iso8601_now_utc() + << "\n"; + } + return true; +} + +bool write_json(const std::string& output_path, const std::vector& results) +{ + std::ofstream json(output_path); + if (!json.is_open()) { + std::cerr << "Unable to open JSON output file: " << output_path << "\n"; + return false; + } + + json << "{\n \"generated_at\": \"" << iso8601_now_utc() + << "\",\n \"fer_points\": [\n"; + for (std::size_t i = 0; i < results.size(); ++i) { + const FerResult& result = results[i]; + json << " {\n" + << " \"ber\": " << std::setprecision(10) << result.ber << ",\n" + << " \"fer\": " << result.fer << ",\n" + << " \"frames\": " << result.frames << ",\n" + << " \"errors\": " << result.errors << ",\n" + << " \"standard_error\": " << result.standard_error << ",\n" + << " \"ci95_low\": " << result.ci95_low << ",\n" + << " \"ci95_high\": " << result.ci95_high << "\n" + << " }"; + if (i + 1 != results.size()) { + json << ","; + } + json << "\n"; + } + json << " ]\n}\n"; + return true; +} + +} // namespace + +int main(int argc, char** argv) +{ + SimulationConfig config; + if (!parse_arguments(argc, argv, config)) { + if (argc > 1 && std::string_view(argv[1]) == "--help") { + return 0; + } + print_usage(argv[0]); + return 1; + } + + std::cout << "Starting Monte-Carlo FER simulation with " + << config.trials_per_ber << " trials per BER point.\n"; + std::cout << "Seed: " << config.seed << "\n"; + + ldpc::bp::BpSparse pcm = build_pcm(); + std::mt19937 rng(config.seed); + std::vector results; + results.reserve(config.ber_points.size()); + + const auto start = std::chrono::steady_clock::now(); + for (double ber : config.ber_points) { + std::cout << "Running BER point " << ber << "...\n"; + const FerResult result = run_single_ber(pcm, ber, config, rng); + std::cout << "BER=" << ber << " FER=" << result.fer + << " std_error=" << result.standard_error << " ci95=[" + << result.ci95_low << ", " << result.ci95_high << "]\n"; + results.push_back(result); + } + const auto elapsed = std::chrono::steady_clock::now() - start; + const double elapsed_s = std::chrono::duration(elapsed).count(); + + if (!write_csv(config.csv_path, results)) { + return 1; + } + std::cout << "CSV results written to " << config.csv_path << "\n"; + + if (config.json_path.has_value()) { + if (!write_json(*config.json_path, results)) { + return 1; + } + std::cout << "JSON results written to " << *config.json_path << "\n"; + } + + std::cout << "Monte-Carlo FER simulation completed in " << elapsed_s << " s.\n"; + return 0; +} From 20a37b2c2c31e1bbbd0642678ed844302a24058d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:42:04 +0000 Subject: [PATCH 3/3] Refine Monte-Carlo FER behavior and test wiring Co-authored-by: sparkainlp-x <281253927+sparkainlp-x@users.noreply.github.com> --- .github/workflows/ci.yml | 9 +++--- CMakeLists.txt | 25 +++++++++-------- README.md | 2 ++ monte_carlo.cpp | 60 ++++++++++++++++++++++++---------------- 4 files changed, 57 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed129bc..eb5ba41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,11 @@ jobs: run: ./build/ldpc_benchmark - name: Run Monte-Carlo FER simulation - run: >- - ./build/monte_carlo_sim - --trials "${MONTE_CARLO_TRIALS:-10000}" - --csv fer_curve.csv + shell: bash + run: | + set -euo pipefail + trials="${MONTE_CARLO_TRIALS:-10000}" + ./build/monte_carlo_sim --trials "${trials}" --csv fer_curve.csv - name: Upload Monte-Carlo FER curve uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index bf7275c..e63ec7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,9 @@ option(BUILD_UNIT_TESTS "Build Catch2 unit tests" ON) option(BUILD_AXI_DMA_DRIVER "Build the Linux ARM AXI-DMA driver" OFF) option(BUILD_HIL_BENCHMARK "Build the hardware-in-the-loop benchmark" OFF) option(BUILD_MONTE_CARLO_SIM "Build the Monte-Carlo FER simulator" ON) +if(BUILD_UNIT_TESTS OR BUILD_MONTE_CARLO_SIM) + enable_testing() +endif() if(BUILD_AXI_DMA_DRIVER) if(NOT UNIX) message(FATAL_ERROR "BUILD_AXI_DMA_DRIVER requires a Unix-like target") @@ -50,7 +53,6 @@ if(BUILD_UNIT_TESTS) GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(Catch2) - enable_testing() endif() add_executable(qldpc_decoder main.cpp) @@ -64,16 +66,17 @@ if(BUILD_UNIT_TESTS) target_link_libraries(unit_tests PRIVATE Catch2::Catch2WithMain ldpc OpenMP::OpenMP_CXX) target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) add_test(NAME unit_tests COMMAND unit_tests) - if(BUILD_MONTE_CARLO_SIM) - add_test( - NAME monte_carlo_smoke - COMMAND monte_carlo_sim - --trials 50 - --ber-list 0.01 - --csv ${CMAKE_BINARY_DIR}/fer_curve_test.csv - --progress-interval 50 - ) - endif() +endif() + +if(BUILD_MONTE_CARLO_SIM) + add_test( + NAME monte_carlo_smoke + COMMAND monte_carlo_sim + --trials 50 + --ber-list 0.01 + --csv ${CMAKE_BINARY_DIR}/fer_curve_test.csv + --progress-interval 50 + ) endif() target_link_libraries(qldpc_decoder diff --git a/README.md b/README.md index 623452b..7d0daa3 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Options utiles : - `--seed 42` - `--json fer_curve.json` (export optionnel) +Le CSV conserve volontairement le schéma compact requis (`BER,FER,FramesAtBER,ErrorsAtBER,Timestamp`). L’erreur standard et l’intervalle de confiance à 95 % sont affichés en console et inclus dans l’export JSON optionnel. + Dans la CI logicielle, la simulation est lancée avec `MONTE_CARLO_TRIALS` (défaut: `10000`) et publie `fer_curve.csv` comme artefact. La première configuration télécharge automatiquement le dépôt amont `quantumgizmos/ldpc` dans le répertoire de build. Le fichier `CMakeLists.txt` utilise `-O3 -march=native -ffast-math -flto` sur les compilateurs non-MSVC et `/O2 /arch:AVX2` sous MSVC. diff --git a/monte_carlo.cpp b/monte_carlo.cpp index d593f92..e4f8c0c 100644 --- a/monte_carlo.cpp +++ b/monte_carlo.cpp @@ -43,6 +43,8 @@ constexpr int kColumns = 4; constexpr int kMaxIterations = 40; constexpr double kBpScaling = 0.625; +enum class ParseResult { Ok, HelpRequested, Error }; + void print_usage(const char* program_name) { std::cout << "Usage: " << program_name << " [options]\n" @@ -107,7 +109,7 @@ bool parse_ber_list(const std::string& raw_value, std::vector& values) return true; } -bool parse_arguments(int argc, char** argv, SimulationConfig& config) +ParseResult parse_arguments(int argc, char** argv, SimulationConfig& config) { for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); @@ -122,14 +124,14 @@ bool parse_arguments(int argc, char** argv, SimulationConfig& config) if (arg == "--help") { print_usage(argv[0]); - return false; + return ParseResult::HelpRequested; } if (arg == "--trials") { const char* value = require_value("--trials"); if (!value || !parse_size(value, config.trials_per_ber) || config.trials_per_ber == 0) { std::cerr << "Invalid --trials value.\n"; - return false; + return ParseResult::Error; } continue; } @@ -137,7 +139,7 @@ bool parse_arguments(int argc, char** argv, SimulationConfig& config) const char* value = require_value("--ber-list"); if (!value || !parse_ber_list(value, config.ber_points)) { std::cerr << "Invalid --ber-list value.\n"; - return false; + return ParseResult::Error; } continue; } @@ -145,14 +147,14 @@ bool parse_arguments(int argc, char** argv, SimulationConfig& config) const char* value = require_value("--seed"); if (!value || !parse_u32(value, config.seed)) { std::cerr << "Invalid --seed value.\n"; - return false; + return ParseResult::Error; } continue; } if (arg == "--csv") { const char* value = require_value("--csv"); if (!value) { - return false; + return ParseResult::Error; } config.csv_path = value; continue; @@ -160,7 +162,7 @@ bool parse_arguments(int argc, char** argv, SimulationConfig& config) if (arg == "--json") { const char* value = require_value("--json"); if (!value) { - return false; + return ParseResult::Error; } config.json_path = value; continue; @@ -169,14 +171,14 @@ bool parse_arguments(int argc, char** argv, SimulationConfig& config) const char* value = require_value("--progress-interval"); if (!value || !parse_size(value, config.progress_interval)) { std::cerr << "Invalid --progress-interval value.\n"; - return false; + return ParseResult::Error; } continue; } std::cerr << "Unknown option: " << arg << "\n"; - return false; + return ParseResult::Error; } - return true; + return ParseResult::Ok; } std::string iso8601_now_utc() @@ -202,24 +204,31 @@ ldpc::bp::BpSparse build_pcm() } std::unique_ptr build_decoder(ldpc::bp::BpSparse& pcm, - double ber) + double channel_ber) { - const double stabilized_ber = std::clamp(ber, 1e-6, 0.49); - std::vector channel_probabilities(kColumns, stabilized_ber); + std::vector channel_probabilities(kColumns, channel_ber); return std::make_unique( pcm, channel_probabilities, kMaxIterations, ldpc::bp::MINIMUM_SUM, ldpc::bp::PARALLEL, kBpScaling, 1, std::vector{}, 0, false, ldpc::bp::SYNDROME); } -bool decode_matches_error(const std::vector& expected_error, - const std::vector& decoded) +bool decode_is_syndrome_equivalent(ldpc::bp::BpSparse& pcm, + const std::vector& expected_error, + const std::vector& decoded) { if (decoded.size() != expected_error.size()) { return false; } + std::vector residual(expected_error.size(), 0); for (std::size_t i = 0; i < expected_error.size(); ++i) { - if ((decoded[i] & 1U) != (expected_error[i] & 1U)) { + residual[i] = static_cast((decoded[i] ^ expected_error[i]) & 1U); + } + + std::vector residual_syndrome(kRows, 0); + pcm.mulvec(residual, residual_syndrome); + for (std::uint8_t bit : residual_syndrome) { + if (bit != 0U) { return false; } } @@ -233,8 +242,9 @@ FerResult run_single_ber(ldpc::bp::BpSparse& pcm, double ber, result.ber = ber; result.frames = config.trials_per_ber; - auto decoder = build_decoder(pcm, ber); - std::bernoulli_distribution bit_error_distribution(ber); + const double channel_ber = std::clamp(ber, 1e-6, 0.49); + auto decoder = build_decoder(pcm, channel_ber); + std::bernoulli_distribution bit_error_distribution(channel_ber); std::vector error_pattern(kColumns, 0); std::vector syndrome(kRows, 0); @@ -250,7 +260,7 @@ FerResult run_single_ber(ldpc::bp::BpSparse& pcm, double ber, pcm.mulvec(error_pattern, syndrome); decoder->decode(syndrome); - if (!decode_matches_error(error_pattern, decoder->decoding)) { + if (!decode_is_syndrome_equivalent(pcm, error_pattern, decoder->decoding)) { ++result.errors; } @@ -279,10 +289,11 @@ bool write_csv(const std::string& output_path, const std::vector& res return false; } + const std::string generated_at = iso8601_now_utc(); csv << "BER,FER,FramesAtBER,ErrorsAtBER,Timestamp\n"; for (const FerResult& result : results) { csv << std::setprecision(10) << result.ber << "," << result.fer << "," - << result.frames << "," << result.errors << "," << iso8601_now_utc() + << result.frames << "," << result.errors << "," << generated_at << "\n"; } return true; @@ -323,10 +334,11 @@ bool write_json(const std::string& output_path, const std::vector& re int main(int argc, char** argv) { SimulationConfig config; - if (!parse_arguments(argc, argv, config)) { - if (argc > 1 && std::string_view(argv[1]) == "--help") { - return 0; - } + const ParseResult parse_result = parse_arguments(argc, argv, config); + if (parse_result == ParseResult::HelpRequested) { + return 0; + } + if (parse_result == ParseResult::Error) { print_usage(argv[0]); return 1; }