diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 203c14d..eb5ba41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,21 @@ jobs: LDPC_MAX_LATENCY_NS: 1000 run: ./build/ldpc_benchmark + - name: Run Monte-Carlo FER simulation + 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 + 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..e63ec7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,10 @@ 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_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") @@ -49,11 +53,13 @@ if(BUILD_UNIT_TESTS) GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(Catch2) - enable_testing() 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) @@ -62,6 +68,17 @@ if(BUILD_UNIT_TESTS) add_test(NAME unit_tests COMMAND unit_tests) 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 PRIVATE ldpc @@ -74,12 +91,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..7d0daa3 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,23 @@ 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) + +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. > `-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..e4f8c0c --- /dev/null +++ b/monte_carlo.cpp @@ -0,0 +1,381 @@ +#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; + +enum class ParseResult { Ok, HelpRequested, Error }; + +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; +} + +ParseResult 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 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 ParseResult::Error; + } + 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 ParseResult::Error; + } + continue; + } + if (arg == "--seed") { + const char* value = require_value("--seed"); + if (!value || !parse_u32(value, config.seed)) { + std::cerr << "Invalid --seed value.\n"; + return ParseResult::Error; + } + continue; + } + if (arg == "--csv") { + const char* value = require_value("--csv"); + if (!value) { + return ParseResult::Error; + } + config.csv_path = value; + continue; + } + if (arg == "--json") { + const char* value = require_value("--json"); + if (!value) { + return ParseResult::Error; + } + 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 ParseResult::Error; + } + continue; + } + std::cerr << "Unknown option: " << arg << "\n"; + return ParseResult::Error; + } + return ParseResult::Ok; +} + +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 channel_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_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) { + 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; + } + } + 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; + + 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); + + 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_is_syndrome_equivalent(pcm, 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; + } + + 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 << "," << generated_at + << "\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; + 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; + } + + 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; +}