diff --git a/CMakeLists.txt b/CMakeLists.txt index e385c25..1802f3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,7 @@ add_library(gf256_core STATIC src/reed_solomon/decoder/high_rate.cc src/reed_solomon/lch_encoder.cc src/reed_solomon/lch_decoder.cc + src/reed_solomon/strong_weak_rs_product_code.cc ) target_compile_features(gf256_core PUBLIC cxx_std_20) target_include_directories(gf256_core @@ -142,7 +143,8 @@ if(GF256_BUILD_TESTS) target_link_libraries(gf_unittests PRIVATE gf256_core gtest gtest_main) add_test(NAME Unittests COMMAND gf_unittests) - add_executable(lch_rs_unittests tests/lch_tests.cc tests/rs_tests.cc) + add_executable(lch_rs_unittests tests/lch_tests.cc tests/rs_tests.cc + tests/product_code_tests.cc) target_include_directories(lch_rs_unittests PRIVATE ${PROJECT_SOURCE_DIR}/src) target_link_libraries(lch_rs_unittests PRIVATE gf256_core gtest gtest_main) if(GF256_ENABLE_NATIVE_ISA AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") @@ -170,8 +172,10 @@ endif() if(GF256_BUILD_BENCHMARKS) FetchContent_MakeAvailable(googlebenchmark) - add_executable(benchmarks benchmarks/matrix_multiplication.cc) + add_executable(benchmarks benchmarks/matrix_multiplication.cc + benchmarks/strong_weak_rs_product_code_benchmarks.cc) target_link_libraries(benchmarks PRIVATE gf256_core benchmark benchmark_main) + target_include_directories(benchmarks PRIVATE ${PROJECT_SOURCE_DIR}/src) add_executable(rs_verbose_benchmarks benchmarks/lch_rs_benchmarks.cc diff --git a/benchmarks/strong_weak_rs_product_code_benchmarks.cc b/benchmarks/strong_weak_rs_product_code_benchmarks.cc new file mode 100644 index 0000000..85f0993 --- /dev/null +++ b/benchmarks/strong_weak_rs_product_code_benchmarks.cc @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include + +#include "benchmark/benchmark.h" +#include "reed_solomon/strong_weak_rs_product_code.h" +#include "reed_solomon/product_code_internal.h" + +namespace { + +void BenchmarkProductCorrectionBSC(benchmark::State& state, int batch_passes = -1) { + using gf2p8::Element; + using gf2p8::rs::ProductCorrectionResult; + using gf2p8::rs::ProductTermination; + constexpr size_t kN = 256; + constexpr size_t kStrongK = 224; + constexpr size_t kWeakK = 254; + constexpr size_t kInformationBytes = kStrongK * kWeakK; + constexpr size_t kBlockBytes = kN * kN; + constexpr size_t kCorpusCount = 64; + constexpr size_t kPassLimit = 16; + constexpr uint32_t kSeed = 0x5b5c0224; + gf2p8::rs::StrongWeakRSProductCode code(kN, kStrongK, kN, kWeakK); + if (!code.Valid() || code.BlockSize() != kBlockBytes) { + state.SkipWithError("invalid product-code dimensions"); + return; + } + + std::mt19937 messages(kSeed); + std::mt19937 channel(kSeed ^ 0x9e3779b9U); + std::vector> original( + kCorpusCount, std::vector(kBlockBytes)); + auto corrupted = original; + auto work = original; + std::vector results(kCorpusCount); + uint64_t channel_bits = 0, channel_symbols = 0; + for (size_t sample = 0; sample < kCorpusCount; ++sample) { + auto& block = original[sample]; + for (size_t row = 0; row < kStrongK; ++row) { + for (size_t col = 0; col < kWeakK; ++col) { + block[row * kN + col] = static_cast(messages()); + } + } + if (code.Encode(block) != gf2p8::lch::Status::ok) { + state.SkipWithError("product corpus encoding failed"); + return; + } + work[sample] = block; + const auto clean = code.Correct(work[sample], kPassLimit); + if (!clean.all_zero_syndromes || clean.changed_symbols != 0 || + work[sample] != block) { + state.SkipWithError("encoded product corpus is not valid"); + return; + } + corrupted[sample] = block; + for (auto& value : corrupted[sample]) { + const auto before = value; + for (unsigned bit = 0; bit < 8; ++bit) { + // Reject the incomplete residue range: exact Bernoulli(1/200), + // reproducible across standard libraries, with no fixed error count. + uint32_t draw; + do { + draw = static_cast(channel()); + } while (draw >= 4294967200U); + if (draw % 200 == 0) { + value ^= static_cast(1U << bit); + ++channel_bits; + } + } + channel_symbols += value != before; + } + } + + uint64_t data_residual_bits = 0, codeword_residual_bits = 0; + uint64_t message_failures = 0, block_failures = 0, validity_failures = 0; + uint64_t valid_wrong_blocks = 0, pass_limits = 0, passes = 0; + uint64_t strong_lines = 0, weak_lines = 0; + size_t max_passes = 0; + // Validate this exact channel corpus against the retained single path outside + // timing, including scheduling outcomes, not just final message recovery. + for (size_t sample = 0; sample < kCorpusCount; ++sample) { + auto reference = corrupted[sample]; + const auto expected = gf2p8::rs::detail::ProductCorrectionAccess::Correct( + code, reference, kPassLimit, 0, false); + work[sample] = corrupted[sample]; + const auto actual = batch_passes < 0 ? code.Correct(work[sample], kPassLimit) + : gf2p8::rs::detail::ProductCorrectionAccess::Correct( + code, work[sample], kPassLimit, batch_passes); + if (work[sample] != reference || actual.termination != expected.termination || + actual.all_zero_syndromes != expected.all_zero_syndromes || + actual.directional_passes != expected.directional_passes || + actual.strong_lines_visited != expected.strong_lines_visited || + actual.weak_lines_visited != expected.weak_lines_visited || + actual.changed_symbols != expected.changed_symbols) { + state.SkipWithError("batch/single corpus differential mismatch"); + return; + } + } + for (auto _ : state) { + state.PauseTiming(); + for (size_t sample = 0; sample < kCorpusCount; ++sample) { + std::copy(corrupted[sample].begin(), corrupted[sample].end(), + work[sample].begin()); + } + state.ResumeTiming(); + for (size_t sample = 0; sample < kCorpusCount; ++sample) { + results[sample] = batch_passes < 0 ? code.Correct(work[sample], kPassLimit) + : gf2p8::rs::detail::ProductCorrectionAccess::Correct( + code, work[sample], kPassLimit, batch_passes); + benchmark::DoNotOptimize(results[sample]); + benchmark::ClobberMemory(); + } + state.PauseTiming(); + for (size_t sample = 0; sample < kCorpusCount; ++sample) { + const auto& result = results[sample]; + if (result.termination == ProductTermination::invalid_argument) { + state.SkipWithError("product correction rejected corpus input"); + break; + } + uint64_t data_bits = 0, block_bits = 0; + for (size_t pos = 0; pos < kBlockBytes; ++pos) { + const auto bits = std::popcount(static_cast( + work[sample][pos] ^ original[sample][pos])); + block_bits += bits; + if (pos / kN < kStrongK && pos % kN < kWeakK) { + data_bits += bits; + } + } + data_residual_bits += data_bits; + codeword_residual_bits += block_bits; + message_failures += data_bits != 0; + block_failures += block_bits != 0; + validity_failures += !result.all_zero_syndromes; + valid_wrong_blocks += result.all_zero_syndromes && block_bits != 0; + pass_limits += result.termination == ProductTermination::pass_limit; + passes += result.directional_passes; + max_passes = std::max(max_passes, result.directional_passes); + strong_lines += result.strong_lines_visited; + weak_lines += result.weak_lines_visited; + } + state.ResumeTiming(); + if (state.skipped()) break; + } + + const double sweeps = static_cast(state.iterations()); + if (sweeps == 0 || state.skipped()) return; + const double blocks = sweeps * kCorpusCount; + state.counters["corpus_blocks"] = kCorpusCount; + state.counters["timed_blocks"] = blocks; + state.counters["seed"] = kSeed; + state.counters["information_bytes_per_block"] = kInformationBytes; + state.counters["target_bit_probability"] = 0.005; + state.counters["channel_codeword_BER"] = + static_cast(channel_bits) / (kCorpusCount * kBlockBytes * 8); + state.counters["channel_flipped_bits"] = static_cast(channel_bits); + state.counters["channel_corrupted_bytes"] = static_cast(channel_symbols); + state.counters["corpus_data_residual_bits"] = data_residual_bits / sweeps; + state.counters["corpus_codeword_residual_bits"] = codeword_residual_bits / sweeps; + state.counters["data_residual_BER"] = + data_residual_bits / (blocks * kInformationBytes * 8); + state.counters["codeword_residual_BER"] = + codeword_residual_bits / (blocks * kBlockBytes * 8); + // Counts per unique corpus, not inflated by timed replays of the same noise. + state.counters["corpus_message_failures"] = message_failures / sweeps; + state.counters["corpus_block_failures"] = block_failures / sweeps; + state.counters["corpus_validity_failures"] = validity_failures / sweeps; + state.counters["corpus_valid_wrong_blocks"] = valid_wrong_blocks / sweeps; + state.counters["message_recovery_fraction"] = 1 - message_failures / blocks; + state.counters["block_recovery_fraction"] = 1 - block_failures / blocks; + state.counters["corpus_pass_limits"] = pass_limits / sweeps; + state.counters["mean_directional_passes"] = passes / blocks; + state.counters["max_directional_passes"] = static_cast(max_passes); + state.counters["mean_strong_lines"] = strong_lines / blocks; + state.counters["mean_weak_lines"] = weak_lines / blocks; + state.counters["pass_limit"] = kPassLimit; + state.SetItemsProcessed(state.iterations() * kCorpusCount); + state.SetBytesProcessed(state.iterations() * kCorpusCount * kInformationBytes); + state.SetLabel("64 blocks/iteration; information bytes=224*254; " + "Correct includes exact final validity; tuned backend"); +} + +const auto* kProductCorrectionBSC = benchmark::RegisterBenchmark( + "LCH/Owned/StrongWeakRSProductCode/Correct/BSC005/" + "Nstrong:256/Kstrong:224/Nweak:256/Kweak:254", + [](benchmark::State& state) { BenchmarkProductCorrectionBSC(state); }); + +const auto* kProductCorrectionSingle = benchmark::RegisterBenchmark( + "LCH/Owned/StrongWeakRSProductCode/Correct/BSC005/Single", + [](benchmark::State& state) { BenchmarkProductCorrectionBSC(state, 0); }); +const auto* kProductCorrectionStrongBatch = benchmark::RegisterBenchmark( + "LCH/Owned/StrongWeakRSProductCode/Correct/BSC005/StrongBatch", + [](benchmark::State& state) { BenchmarkProductCorrectionBSC(state, 1); }); +const auto* kProductCorrectionBothBatch = benchmark::RegisterBenchmark( + "LCH/Owned/StrongWeakRSProductCode/Correct/BSC005/BothBatch", + [](benchmark::State& state) { BenchmarkProductCorrectionBSC(state, 2); }); + +} // namespace diff --git a/include/reed_solomon/error_correction.h b/include/reed_solomon/error_correction.h new file mode 100644 index 0000000..fb6fa55 --- /dev/null +++ b/include/reed_solomon/error_correction.h @@ -0,0 +1,35 @@ +#pragma once + +#include "reed_solomon/lch_decoder.h" + +namespace gf2p8::rs { + +/** @brief Bounded-distance correction outcome. */ +enum class CorrectionStatus { + ok, + invalid_argument, + unsupported_dimensions, + uncorrectable, + reconstruction_failed, +}; + +/** @brief Status and number of repaired symbols (zero on failure). */ +struct CorrectionResult { + CorrectionStatus status = CorrectionStatus::invalid_argument; + size_t error_count = 0; +}; + +/** + * @brief Transactionally repairs data AND parity of one Cantor RS codeword. + * @param decoder Decoder defining K data and R recovery symbols. + * @param codeword Exactly K+R mutable symbols in [data][recovery] order. + * @return Status and corrected symbol count; zero count on an already valid word. + * @details Requires unshortened power-of-two N=K+R <=256 and power-of-two + * R<=K. Corrects up to floor(R/2) unknown symbol errors, with no erasures. + * On any non-ok status every input byte is unchanged. Success means the result + * is a codeword within that radius, NOT necessarily the transmitted codeword. + */ +CorrectionResult CorrectCodeword(const LCHDecoder& decoder, + std::span codeword); + +} // namespace gf2p8::rs diff --git a/include/reed_solomon/strong_weak_rs_product_code.h b/include/reed_solomon/strong_weak_rs_product_code.h new file mode 100644 index 0000000..4a2bc84 --- /dev/null +++ b/include/reed_solomon/strong_weak_rs_product_code.h @@ -0,0 +1,91 @@ +#pragma once + +#include "reed_solomon/error_correction.h" +#include "reed_solomon/lch_encoder.h" + +namespace gf2p8::rs { + +namespace detail { struct ProductCorrectionAccess; } + +/** @brief Reason iterative product correction stopped, independent of validity. */ +enum class ProductTermination { invalid_argument, no_change, pass_limit }; + +/** @brief Product correction outcome; validity does not prove original content. */ +struct ProductCorrectionResult { + ProductTermination termination = ProductTermination::invalid_argument; + bool all_zero_syndromes = false; + size_t directional_passes = 0; + size_t strong_lines_visited = 0; + size_t weak_lines_visited = 0; + /** @brief Accepted symbol writes, counting repeated repairs separately. */ + size_t changed_symbols = 0; +}; + +/** + * @brief Systematic Cantor RS product with strong columns and weak rows. + * @details Row-major block has Nstrong rows and Nweak columns. The top-left + * Kstrong by Kweak rectangle holds data; every column and every row, including + * parity regions, is a component codeword. Errors only: no erasures, + * shortening, or backtracking. Both N must be powers of two <=256; strong R + * must be a power of two with 2<=R<=K, and weak R must equal 2. + */ +class StrongWeakRSProductCode { + public: + /** + * @brief Constructs a product code; unsupported dimensions make Valid false. + * @param strong_n Number of rows. + * @param strong_k Number of systematic rows. + * @param weak_n Number of columns. + * @param weak_k Number of systematic columns. + */ + StrongWeakRSProductCode(size_t strong_n = 256, size_t strong_k = 224, + size_t weak_n = 256, size_t weak_k = 254); + + /** @brief Reports supported dimensions. @return Whether operations are valid. */ + bool Valid() const; + /** @brief Returns required row-major block size. @return Bytes, or zero if invalid. */ + size_t BlockSize() const; + + /** + * @brief Fills all product parity, preserving the systematic rectangle. + * @param block Exactly BlockSize() symbols, with data already in place. + * @param backend Owned encoder backend, scalar available for reference checks. + * @return Encoding status; on failure block is unchanged. + */ + lch::Status Encode(std::span block, + lch::Backend backend = lch::Backend::tuned) const; + + /** + * @brief Applies alternating strong BDD and gated weak single-error passes. + * @param block Exactly BlockSize() mutable symbols. + * @param max_directional_passes Cap counting each direction separately; >=2. + * @return Termination, pass/work counts, and final all-component validity. + * @details Always starts with a full strong pass then a full weak pass, even + * if strong changes nothing. From then on visits only lines intersecting + * accepted byte changes in the preceding pass. Stops on a no-change pass + * (including the initial weak pass), or the cap; no-change takes precedence. + * Strong success, including zero syndromes, protects that column; failure + * leaves its bytes unchanged and unprotects it. Unvisited protection persists. + * Weak candidates commit only for one changed symbol with popcount(old XOR + * new)<=2 in an unprotected column; rejected rows are unchanged. Protection + * changes alone never activate lines. Accepted repairs are retained at exit; + * the entire iterative operation is not transactional. Invalid arguments + * leave the block untouched. Validity is checked separately at exit and does + * not count as a directional pass or guarantee the original message. + */ + ProductCorrectionResult Correct(std::span block, + size_t max_directional_passes = 16) const; + + private: + friend struct detail::ProductCorrectionAccess; + ProductCorrectionResult CorrectImpl(std::span block, + size_t max_directional_passes, + unsigned batch_passes, + bool tracked_validation = true) const; + size_t strong_n_, strong_k_, weak_n_, weak_k_; + bool valid_; + LCHEncoder strong_encoder_, weak_encoder_; + LCHDecoder strong_decoder_, weak_decoder_; +}; + +} // namespace gf2p8::rs diff --git a/src/reed_solomon/error_correction/batched.cc b/src/reed_solomon/error_correction/batched.cc index 29cb8a2..38d68ee 100644 --- a/src/reed_solomon/error_correction/batched.cc +++ b/src/reed_solomon/error_correction/batched.cc @@ -111,7 +111,8 @@ CorrectionStatus CorrectColumnsScalar(const LCHDecoder& decoder, size_t byte_count, size_t first_column, std::span results, - std::span error_masks) { + std::span error_masks, + std::span mutable_recovery) { const size_t data_count = data.size(); const size_t recovery_count = recovery.size(); const size_t codeword_size = data_count + recovery_count; @@ -126,15 +127,29 @@ CorrectionStatus CorrectColumnsScalar(const LCHDecoder& decoder, for (size_t i = 0; i < recovery_count; ++i) { recovery_values[i] = recovery[i][column]; } - const CorrectionResult result = CorrectOne( - decoder, std::span(data_values).first(data_count), - std::span(recovery_values).first(recovery_count), - std::span(mask).first(codeword_size)); + CorrectionResult result; + if (mutable_recovery.empty()) { + result = CorrectOne( + decoder, std::span(data_values).first(data_count), + std::span(recovery_values).first(recovery_count), + std::span(mask).first(codeword_size)); + } else { + std::copy_n(recovery_values.begin(), recovery_count, + data_values.begin() + data_count); + const auto before = data_values; + result = CorrectCodeword(decoder, std::span(data_values).first(codeword_size)); + for (size_t i = 0; i < codeword_size; ++i) { + mask[i] = before[i] != data_values[i]; + } + } results[column] = result; if (result.status == CorrectionStatus::ok) { for (size_t i = 0; i < data_count; ++i) { data[i][column] = data_values[i]; } + for (size_t i = 0; i < mutable_recovery.size(); ++i) { + mutable_recovery[i][column] = data_values[data_count + i]; + } } for (size_t position = 0; position < codeword_size; ++position) { error_masks[position * byte_count + column] = mask[position]; @@ -584,7 +599,8 @@ void CorrectChunk32(std::span data, const CodeParameters& parameters, const lch::detail::ResolvedKernels& kernels, std::span results, - std::span error_masks) { + std::span error_masks, + std::span mutable_recovery) { #if !defined(__GFNI__) static_assert(!UseGFNI); #endif @@ -786,8 +802,10 @@ void CorrectChunk32(std::span data, data_error_lanes |= root_masks[native_position]; } } - const uint32_t location_only_lanes = candidate_lanes & ~data_error_lanes; - candidate_lanes &= data_error_lanes; + // Whole-codeword mode must evaluate and verify parity-only candidates too. + const uint32_t location_only_lanes = mutable_recovery.empty() + ? candidate_lanes & ~data_error_lanes : 0; + if (mutable_recovery.empty()) candidate_lanes &= data_error_lanes; if (candidate_lanes == 0) { PublishChunkResults(results, error_masks, byte_count, column, parameters.family, data_count, recovery_count, @@ -936,18 +954,20 @@ void CorrectChunk32(std::span data, ++native_position) { const size_t public_position = PublicPosition( parameters.family, data_count, recovery_count, native_position); - if (public_position >= data_count) { + if (public_position >= data_count && mutable_recovery.empty()) { continue; } + Element* destination = public_position < data_count + ? data[public_position] : mutable_recovery[public_position - data_count]; const uint32_t active = root_masks[native_position] & candidate_lanes; const __m256i old_data = _mm256_loadu_si256( - reinterpret_cast(data[public_position] + column)); + reinterpret_cast(destination + column)); const __m256i correction = _mm256_and_si256( _mm256_load_si256( reinterpret_cast(Row(work, native_position))), LaneMask(active)); _mm256_storeu_si256( - reinterpret_cast<__m256i*>(data[public_position] + column), + reinterpret_cast<__m256i*>(destination + column), _mm256_xor_si256(old_data, correction)); } PublishChunkResults(results, error_masks, byte_count, column, @@ -960,12 +980,13 @@ void CorrectChunk32(std::span data, } // namespace -CorrectionStatus CorrectBatch(const LCHDecoder& decoder, +static CorrectionStatus CorrectBatchImpl(const LCHDecoder& decoder, std::span data, std::span recovery, size_t byte_count, std::span results, - std::span error_masks) { + std::span error_masks, + std::span mutable_recovery) { if (!decoder.Valid()) { return CorrectionStatus::invalid_argument; } @@ -1026,19 +1047,46 @@ CorrectionStatus CorrectBatch(const LCHDecoder& decoder, #if defined(__GFNI__) for (; column + kBatchLanes <= byte_count; column += kBatchLanes) { CorrectChunk32(data, recovery, byte_count, column, parameters, - *kernels, results, error_masks); + *kernels, results, error_masks, mutable_recovery); } #else for (; column + kBatchLanes <= byte_count; column += kBatchLanes) { CorrectChunk32(data, recovery, byte_count, column, parameters, - *kernels, results, error_masks); + *kernels, results, error_masks, mutable_recovery); } #endif } } #endif return CorrectColumnsScalar(decoder, data, recovery, byte_count, column, - results, error_masks); + results, error_masks, mutable_recovery); +} + +CorrectionStatus CorrectBatch(const LCHDecoder& decoder, + std::span data, + std::span recovery, + size_t byte_count, + std::span results, + std::span error_masks) { + return CorrectBatchImpl(decoder, data, recovery, byte_count, results, + error_masks, {}); +} + +CorrectionStatus CorrectCodewordBatch(const LCHDecoder& decoder, + std::span shards, + size_t byte_count, + std::span results, + std::span error_masks) { + if (!decoder.Valid() || shards.size() > kFieldSize || + shards.size() != decoder.DataCount() + decoder.RecoveryCount()) { + return CorrectionStatus::invalid_argument; + } + const auto recovery = shards.subspan(decoder.DataCount()); + std::array immutable_recovery{}; + std::copy(recovery.begin(), recovery.end(), immutable_recovery.begin()); + return CorrectBatchImpl(decoder, shards.first(decoder.DataCount()), + std::span(immutable_recovery).first(recovery.size()), + byte_count, results, error_masks, recovery); } } // namespace gf2p8::rs::detail::error_correction diff --git a/src/reed_solomon/error_correction/internal.h b/src/reed_solomon/error_correction/internal.h index 216a6d8..459ae66 100644 --- a/src/reed_solomon/error_correction/internal.h +++ b/src/reed_solomon/error_correction/internal.h @@ -4,22 +4,12 @@ #include #include -#include "reed_solomon/lch_decoder.h" +#include "reed_solomon/error_correction.h" namespace gf2p8::rs::detail::error_correction { -enum class CorrectionStatus { - ok, - invalid_argument, - unsupported_dimensions, - uncorrectable, - reconstruction_failed, -}; - -struct CorrectionResult { - CorrectionStatus status = CorrectionStatus::invalid_argument; - size_t error_count = 0; -}; +using ::gf2p8::rs::CorrectionStatus; +using ::gf2p8::rs::CorrectionResult; /** * @brief Corrects one scalar LCH Reed-Solomon codeword. @@ -62,4 +52,19 @@ CorrectionStatus CorrectBatch(const LCHDecoder& decoder, std::span results, std::span error_masks); +/** + * @brief Repairs whole independent codewords, including recovery positions. + * @param decoder Component code dimensions. + * @param shards N disjoint mutable shard ranges in public data/recovery order. + * @param byte_count Independent codewords per shard. + * @param results Per-codeword outcomes; failed codewords remain unchanged. + * @param error_masks Position-major masks, as in CorrectBatch. + * @return Call-level status; all shard and output ranges must be disjoint. + */ +CorrectionStatus CorrectCodewordBatch(const LCHDecoder& decoder, + std::span shards, + size_t byte_count, + std::span results, + std::span error_masks); + } // namespace gf2p8::rs::detail::error_correction diff --git a/src/reed_solomon/error_correction/scalar.cc b/src/reed_solomon/error_correction/scalar.cc index 20389d9..4955662 100644 --- a/src/reed_solomon/error_correction/scalar.cc +++ b/src/reed_solomon/error_correction/scalar.cc @@ -228,10 +228,11 @@ size_t PublicPosition(CodeFamily family, : native_position - recovery_count; } -CorrectionStatus RecoverDataWithEvaluator( +CorrectionStatus RecoverWithEvaluator( CodeFamily family, std::span data, std::span recovery, + std::span mutable_recovery, size_t recovery_count, std::span root_positions, const Values& locator_samples, @@ -369,6 +370,8 @@ CorrectionStatus RecoverDataWithEvaluator( PublicPosition(family, data_count, recovery_count, position); if (data_position < data_count) { data[data_position] ^= corrections[position]; + } else if (!mutable_recovery.empty()) { + mutable_recovery[data_position - data_count] ^= corrections[position]; } } return CorrectionStatus::ok; @@ -376,10 +379,11 @@ CorrectionStatus RecoverDataWithEvaluator( } // namespace -CorrectionResult CorrectOne(const LCHDecoder& decoder, - std::span data, - std::span recovery, - std::span error_mask) { +static CorrectionResult CorrectOneImpl(const LCHDecoder& decoder, + std::span data, + std::span recovery, + std::span error_mask, + std::span mutable_recovery) { if (RangesOverlap(error_mask, data) || RangesOverlap(error_mask, recovery)) { return Result(CorrectionStatus::invalid_argument); } @@ -636,7 +640,9 @@ CorrectionResult CorrectOne(const LCHDecoder& decoder, } CorrectionStatus recovery_status = CorrectionStatus::ok; - if (has_data_error && root_count == 1) { + // Whole-codeword mode verifies every candidate, including parity-only roots. + // Retain the existing data-only fast path for CorrectOne/CorrectBatch callers. + if (has_data_error && root_count == 1 && mutable_recovery.empty()) { // Every aligned R-point native Cantor IFFT has unit leading Lagrange // coefficient. Therefore the highest syndrome coefficient is the error // magnitude when exactly one error is present. @@ -644,9 +650,9 @@ CorrectionResult CorrectOne(const LCHDecoder& decoder, const size_t data_index = PublicPosition(parameters.family, data_count, recovery_count, root_positions[0]); data[data_index] ^= magnitude; - } else if (has_data_error) { - recovery_status = RecoverDataWithEvaluator( - parameters.family, data, recovery, recovery_count, + } else if (has_data_error || !mutable_recovery.empty()) { + recovery_status = RecoverWithEvaluator( + parameters.family, data, recovery, mutable_recovery, recovery_count, std::span(root_positions).first(root_count), locator_samples, locator_coefficients, locator_degree, syndrome_samples, tables); } @@ -661,4 +667,28 @@ CorrectionResult CorrectOne(const LCHDecoder& decoder, return Result(CorrectionStatus::ok, root_count); } +CorrectionResult CorrectOne(const LCHDecoder& decoder, + std::span data, + std::span recovery, + std::span error_mask) { + return CorrectOneImpl(decoder, data, recovery, error_mask, {}); +} + } // namespace gf2p8::rs::detail::error_correction + +namespace gf2p8::rs { + +CorrectionResult CorrectCodeword(const LCHDecoder& decoder, + std::span codeword) { + if (!decoder.Valid() || + codeword.size() != decoder.DataCount() + decoder.RecoveryCount()) { + return {.status = CorrectionStatus::invalid_argument}; + } + std::array mask{}; + auto recovery = codeword.subspan(decoder.DataCount()); + return detail::error_correction::CorrectOneImpl( + decoder, codeword.first(decoder.DataCount()), recovery, + std::span(mask).first(codeword.size()), recovery); +} + +} // namespace gf2p8::rs diff --git a/src/reed_solomon/product_code_internal.h b/src/reed_solomon/product_code_internal.h new file mode 100644 index 0000000..cf94a1c --- /dev/null +++ b/src/reed_solomon/product_code_internal.h @@ -0,0 +1,26 @@ +#pragma once + +#include "reed_solomon/strong_weak_rs_product_code.h" + +namespace gf2p8::rs::detail { + +/** @brief Private differential-test and benchmark access to initial-pass choices. */ +struct ProductCorrectionAccess { + /** + * @brief Runs the same scheduler with zero, one, or two initial batched passes. + * @param code Product dimensions and component decoders. + * @param block Mutable row-major block. + * @param cap Directional pass limit. + * @param batch_passes Initial passes to batch (0: reference single path). + * @param tracked_validation Skip known-clean lines; false retains the full scan. + * @return The normal product outcome and work counts. + */ + static ProductCorrectionResult Correct(const StrongWeakRSProductCode& code, + std::span block, size_t cap, + unsigned batch_passes, + bool tracked_validation = true) { + return code.CorrectImpl(block, cap, batch_passes, tracked_validation); + } +}; + +} // namespace gf2p8::rs::detail diff --git a/src/reed_solomon/strong_weak_rs_product_code.cc b/src/reed_solomon/strong_weak_rs_product_code.cc new file mode 100644 index 0000000..221f680 --- /dev/null +++ b/src/reed_solomon/strong_weak_rs_product_code.cc @@ -0,0 +1,237 @@ +#include "reed_solomon/strong_weak_rs_product_code.h" +#include "reed_solomon/error_correction/internal.h" + +#include +#include +#include +#include + +namespace gf2p8::rs { +namespace { + +bool Aligned(size_t n, size_t k) { + return n <= 256 && std::has_single_bit(n) && k < n && + n - k >= 2 && n - k <= k && std::has_single_bit(n - k); +} + +} // namespace + +StrongWeakRSProductCode::StrongWeakRSProductCode(size_t strong_n, size_t strong_k, + size_t weak_n, size_t weak_k) + : strong_n_(strong_n), strong_k_(strong_k), weak_n_(weak_n), weak_k_(weak_k), + valid_(Aligned(strong_n, strong_k) && Aligned(weak_n, weak_k) && + weak_n - weak_k == 2), + strong_encoder_(valid_ ? strong_k : 0, valid_ ? strong_n - strong_k : 0), + weak_encoder_(valid_ ? weak_k : 0, valid_ ? weak_n - weak_k : 0), + strong_decoder_(valid_ ? strong_k : 0, valid_ ? strong_n - strong_k : 0), + weak_decoder_(valid_ ? weak_k : 0, valid_ ? weak_n - weak_k : 0) {} + +bool StrongWeakRSProductCode::Valid() const { + return valid_ && strong_encoder_.Valid() && weak_encoder_.Valid() && + strong_decoder_.Valid() && weak_decoder_.Valid(); +} + +size_t StrongWeakRSProductCode::BlockSize() const { + return Valid() ? strong_n_ * weak_n_ : 0; +} + +lch::Status StrongWeakRSProductCode::Encode(std::span block, + lch::Backend backend) const { + if (!Valid() || block.size() != BlockSize()) { + return lch::Status::invalid_argument; + } + std::vector candidate(block.begin(), block.end()); + std::array data{}; + std::array recovery{}; + std::vector workspace(std::max(strong_encoder_.WorkspaceSize(weak_n_), + weak_encoder_.WorkspaceSize(1))); + for (size_t row = 0; row < strong_k_; ++row) { + for (size_t col = 0; col < weak_k_; ++col) { + data[col] = &candidate[row * weak_n_ + col]; + } + for (size_t col = weak_k_; col < weak_n_; ++col) { + recovery[col - weak_k_] = &candidate[row * weak_n_ + col]; + } + const auto status = weak_encoder_.Encode( + std::span(data).first(weak_k_), std::span(recovery).first(2), 1, + workspace, backend); + if (status != lch::Status::ok) { + return status; + } + } + for (size_t row = 0; row < strong_k_; ++row) { + data[row] = &candidate[row * weak_n_]; + } + for (size_t row = strong_k_; row < strong_n_; ++row) { + recovery[row - strong_k_] = &candidate[row * weak_n_]; + } + const auto status = strong_encoder_.Encode( + std::span(data).first(strong_k_), + std::span(recovery).first(strong_n_ - strong_k_), weak_n_, workspace, + backend); + if (status == lch::Status::ok) { + std::copy(candidate.begin(), candidate.end(), block.begin()); + } + return status; +} + +ProductCorrectionResult StrongWeakRSProductCode::Correct( + std::span block, size_t max_directional_passes) const { + // Avoid packing overhead when no complete SIMD batch can run. + const unsigned batches = lch::BackendAvailable(lch::Backend::avx2) && + std::max(strong_n_, weak_n_) >= 32 ? 2 : 0; + return CorrectImpl(block, max_directional_passes, batches); +} + +ProductCorrectionResult StrongWeakRSProductCode::CorrectImpl( + std::span block, size_t max_directional_passes, + unsigned batch_passes, bool tracked_validation) const { + ProductCorrectionResult result; + if (!Valid() || block.size() != BlockSize() || max_directional_passes < 2) { + return result; + } + std::array protected_columns{}; + // False means unknown, never proven nonzero: intersecting edits can cancel. + std::array clean_columns{}, clean_rows{}; + std::array active{}; + std::array candidate{}; + std::vector packed(batch_passes != 0 ? block.size() : 0); + std::vector masks(packed.size()); + std::array outcomes{}; + std::array shards{}; + for (size_t pass = 0; pass < max_directional_passes; ++pass) { + const bool strong = pass % 2 == 0; + const size_t lines = strong ? weak_n_ : strong_n_; + const size_t length = strong ? strong_n_ : weak_n_; + std::array next{}; + size_t changes = 0; + const bool batched = pass < std::min(batch_passes, 2u); + if (batched) { + // Columns already have position-major layout; rows need transposition. + // Keep tentative repairs private until the existing weak gates accept. + if (strong) std::copy(block.begin(), block.end(), packed.begin()); + for (size_t pos = 0; pos < length; ++pos) { + shards[pos] = packed.data() + pos * lines; + if (!strong) { + for (size_t line = 0; line < lines; ++line) { + shards[pos][line] = block[line * weak_n_ + pos]; + } + } + } + const auto status = detail::error_correction::CorrectCodewordBatch( + strong ? strong_decoder_ : weak_decoder_, + std::span(shards).first(length), lines, + std::span(outcomes).first(lines), masks); + if (status != CorrectionStatus::ok) return result; + } + if (batched && strong) { + result.strong_lines_visited += lines; + for (size_t line = 0; line < lines; ++line) { + clean_columns[line] = protected_columns[line] = + outcomes[line].status == CorrectionStatus::ok; + } + // Batch failure is transactional and masks describe only verified edits. + // Walk position-major output rather than gathering every column again. + for (size_t pos = 0; pos < length; ++pos) { + for (size_t line = 0; line < lines; ++line) { + const size_t index = pos * lines + line; + if (masks[index]) { + block[index] = packed[index]; + next[pos] = true; + clean_rows[pos] = false; + ++changes; + } + } + } + } else for (size_t line = 0; line < lines; ++line) { + if (pass >= 2 && !active[line]) { + continue; + } + if (strong) { + ++result.strong_lines_visited; + } else { + ++result.weak_lines_visited; + } + const auto index = [&](size_t pos) { + return strong ? pos * weak_n_ + line : line * weak_n_ + pos; + }; + if (!batched) { + for (size_t pos = 0; pos < length; ++pos) { + candidate[pos] = block[index(pos)]; + } + } + const auto correction = batched ? outcomes[line] : CorrectCodeword( + strong ? strong_decoder_ : weak_decoder_, + std::span(candidate).first(length)); + auto& clean = strong ? clean_columns[line] : clean_rows[line]; + clean = correction.status == CorrectionStatus::ok && correction.error_count == 0; + if (strong) { + protected_columns[line] = correction.status == CorrectionStatus::ok; + } + if (correction.status != CorrectionStatus::ok) { + continue; + } + if (correction.error_count == 0) continue; + if (batched) { + for (size_t pos = 0; pos < length; ++pos) candidate[pos] = shards[pos][line]; + } + if (!strong) { + if (correction.error_count != 1) { + continue; + } + bool accept = true; + for (size_t pos = 0; pos < length; ++pos) { + const unsigned delta = block[index(pos)] ^ candidate[pos]; + if (delta != 0 && (protected_columns[pos] || std::popcount(delta) > 2)) { + accept = false; + } + } + if (!accept) { + continue; + } + } + for (size_t pos = 0; pos < length; ++pos) { + if (block[index(pos)] != candidate[pos]) { + block[index(pos)] = candidate[pos]; + next[pos] = true; + (strong ? clean_rows[pos] : clean_columns[pos]) = false; + ++changes; + } + } + // Successful BDD verifies its candidate internally; only committed + // candidates establish validity. Rejected weak candidates do not. + clean = true; + } + ++result.directional_passes; + result.changed_symbols += changes; + active = next; + if (pass >= 1 && changes == 0) { + result.termination = ProductTermination::no_change; + break; + } + result.termination = ProductTermination::pass_limit; + } + // Check the actual final block, not stale protection or tentative candidates. + result.all_zero_syndromes = true; + for (bool strong : {true, false}) { + const size_t lines = strong ? weak_n_ : strong_n_; + const size_t length = strong ? strong_n_ : weak_n_; + for (size_t line = 0; line < lines; ++line) { + if (tracked_validation && (strong ? clean_columns[line] : clean_rows[line])) { + continue; + } + for (size_t pos = 0; pos < length; ++pos) { + candidate[pos] = block[strong ? pos * weak_n_ + line : line * weak_n_ + pos]; + } + const auto check = CorrectCodeword( + strong ? strong_decoder_ : weak_decoder_, + std::span(candidate).first(length)); + if (check.status != CorrectionStatus::ok || check.error_count != 0) { + result.all_zero_syndromes = false; + } + } + } + return result; +} + +} // namespace gf2p8::rs diff --git a/tests/product_code_tests.cc b/tests/product_code_tests.cc new file mode 100644 index 0000000..659b85c --- /dev/null +++ b/tests/product_code_tests.cc @@ -0,0 +1,525 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "reed_solomon/strong_weak_rs_product_code.h" +#include "reed_solomon/error_correction/internal.h" +#include "reed_solomon/product_code_internal.h" + +namespace { + +using gf2p8::Element; +using gf2p8::lch::Backend; +using gf2p8::lch::Status; +using namespace gf2p8::rs; + +std::vector Codeword(size_t n, size_t k, uint32_t seed) { + LCHEncoder encoder(k, n - k); + std::mt19937 random(seed); + std::vector word(n); + for (size_t i = 0; i < k; ++i) word[i] = static_cast(random()); + std::vector data(k); + std::vector recovery(n - k); + for (size_t i = 0; i < k; ++i) data[i] = &word[i]; + for (size_t i = k; i < n; ++i) recovery[i - k] = &word[i]; + std::vector workspace(encoder.WorkspaceSize(1)); + EXPECT_EQ(encoder.Encode(data, recovery, 1, workspace, Backend::scalar), Status::ok); + return word; +} + +TEST(WholeCodeword, RepairsEveryPositionAndFullRadiusIncludingParity) { + for (const auto [n, k] : {std::pair{4u, 2u}, {8u, 4u}, {16u, 12u}, + {256u, 224u}, {256u, 128u}, {256u, 254u}}) { + const auto original = Codeword(n, k, n + k); + LCHDecoder decoder(k, n - k); + for (size_t pos = 0; pos < n; ++pos) { + auto word = original; + word[pos] ^= 0xff; + const auto result = CorrectCodeword(decoder, word); + ASSERT_EQ(result.status, CorrectionStatus::ok) << n << ':' << pos; + EXPECT_EQ(result.error_count, 1u); + ASSERT_EQ(word, original); + } + std::mt19937 random(n); + for (size_t trial = 0; trial < 20; ++trial) { + auto word = original; + std::vector positions(n); + for (size_t i = 0; i < n; ++i) positions[i] = i; + if (trial != 0) std::shuffle(positions.begin(), positions.end(), random); + else std::reverse(positions.begin(), positions.end()); + for (size_t i = 0; i < (n - k) / 2; ++i) { + word[positions[i]] ^= static_cast(1 + random() % 255); + } + const auto result = CorrectCodeword(decoder, word); + ASSERT_EQ(result.status, CorrectionStatus::ok); + EXPECT_EQ(result.error_count, (n - k) / 2); + EXPECT_EQ(word, original); + } + auto clean = original; + EXPECT_EQ(CorrectCodeword(decoder, clean).error_count, 0u); + EXPECT_EQ(clean, original); + } +} + +TEST(WholeCodeword, TransactionalFailureAndInvalidDimensions) { + LCHDecoder decoder(6, 2); + auto word = Codeword(8, 6, 42); + // Equal magnitudes cancel the leading syndrome: no one-error candidate. + word[0] ^= 7; + word[7] ^= 7; + const auto before = word; + const auto result = CorrectCodeword(decoder, word); + EXPECT_EQ(result.status, CorrectionStatus::uncorrectable); + EXPECT_EQ(result.error_count, 0u); + EXPECT_EQ(word, before); + EXPECT_EQ(CorrectCodeword(decoder, std::span(word).first(7)).status, + CorrectionStatus::invalid_argument); + EXPECT_EQ(CorrectCodeword(LCHDecoder(0, 0), word).status, + CorrectionStatus::invalid_argument); + EXPECT_EQ(CorrectCodeword(LCHDecoder(5, 3), word).status, + CorrectionStatus::unsupported_dimensions); + EXPECT_EQ(word, before); + std::vector shortened(7, 1); + const auto saved = shortened; + EXPECT_EQ(CorrectCodeword(LCHDecoder(5, 2), shortened).status, + CorrectionStatus::unsupported_dimensions); + EXPECT_EQ(shortened, saved); +} + +TEST(ProductCode, DimensionsAndInvalidCalls) { + EXPECT_TRUE(StrongWeakRSProductCode().Valid()); + EXPECT_EQ(StrongWeakRSProductCode().BlockSize(), 65536u); + for (const auto [n, k] : {std::pair{0u, 0u}, {8u, 8u}, {8u, 9u}, {7u, 5u}, + {8u, 5u}, {8u, 2u}, {512u, 480u}, {8u, 7u}}) { + StrongWeakRSProductCode code(n, k, 8, 6); + EXPECT_FALSE(code.Valid()); + EXPECT_EQ(code.BlockSize(), 0u); + std::vector block(32, 17); + const auto before = block; + EXPECT_EQ(code.Encode(block), Status::invalid_argument); + EXPECT_EQ(code.Correct(block).termination, ProductTermination::invalid_argument); + EXPECT_EQ(block, before); + } + EXPECT_FALSE(StrongWeakRSProductCode(8, 4, 8, 4).Valid()); + EXPECT_FALSE(StrongWeakRSProductCode(std::numeric_limits::max(), 1).Valid()); + StrongWeakRSProductCode code(4, 2, 8, 6); + std::vector block(32, 17); + const auto before = block; + for (size_t cap : {0u, 1u}) { + const auto result = code.Correct(block, cap); + EXPECT_EQ(result.termination, ProductTermination::invalid_argument); + EXPECT_EQ(result.directional_passes, 0u); + EXPECT_FALSE(result.all_zero_syndromes); + EXPECT_EQ(block, before); + } + EXPECT_EQ(code.Correct(std::span(block).first(31)).termination, + ProductTermination::invalid_argument); + EXPECT_EQ(code.Encode(std::span(block).first(31)), Status::invalid_argument); + EXPECT_EQ(block, before); +} + +TEST(ProductCode, SystematicEncodingScalarAgreementAndAllComponentValidity) { + for (const auto [ns, ks, nw, kw] : + {std::array{4, 2, 8, 6}, {16, 12, 16, 14}, {256, 224, 256, 254}}) { + StrongWeakRSProductCode code(ns, ks, nw, kw); + std::mt19937 random(901); + std::vector block(code.BlockSize()); + for (auto& value : block) value = static_cast(random()); + const auto input = block; + auto scalar = block; + ASSERT_EQ(code.Encode(block), Status::ok); + ASSERT_EQ(code.Encode(scalar, Backend::scalar), Status::ok); + EXPECT_EQ(block, scalar); + for (size_t row = 0; row < ks; ++row) { + for (size_t col = 0; col < kw; ++col) { + ASSERT_EQ(block[row * nw + col], input[row * nw + col]); + } + } + const auto result = code.Correct(block); + EXPECT_TRUE(result.all_zero_syndromes); + EXPECT_EQ(result.directional_passes, 2u); + EXPECT_EQ(result.strong_lines_visited, nw); + EXPECT_EQ(result.weak_lines_visited, ns); + EXPECT_EQ(result.changed_symbols, 0u); + EXPECT_EQ(result.termination, ProductTermination::no_change); + EXPECT_EQ(block, scalar); + // Arbitrary symbol damage in each of the four product regions. + for (auto [row, col] : {std::pair{size_t{0}, size_t{0}}, {ks, size_t{0}}, + {size_t{0}, kw}, {ks, kw}}) { + block[row * nw + col] ^= 0xff; + const auto repaired = code.Correct(block); + EXPECT_TRUE(repaired.all_zero_syndromes); + EXPECT_EQ(repaired.changed_symbols, 1u); + EXPECT_EQ(block, scalar); + } + } +} + +TEST(ProductCode, InitialWeakPassSelectiveActivationAndCap) { + StrongWeakRSProductCode code(4, 2, 8, 6); + for (size_t col : {0u, 6u, 7u}) { + for (Element magnitude : {Element{1}, Element{3}}) { + std::vector damaged(32); + // Strong cannot repair two equal errors; weak can repair both rows, + // including strong-parity rows and the parity/parity corner. + damaged[2 * 8 + col] = magnitude; + damaged[3 * 8 + col] = magnitude; + auto capped = damaged; + const auto cap = code.Correct(capped, 2); + EXPECT_EQ(cap.termination, ProductTermination::pass_limit); + EXPECT_EQ(cap.directional_passes, 2u); + EXPECT_TRUE(cap.all_zero_syndromes); + EXPECT_EQ(capped, std::vector(32)); + const auto result = code.Correct(damaged); + EXPECT_EQ(result.directional_passes, 3u); + EXPECT_EQ(result.strong_lines_visited, 9u); + EXPECT_EQ(result.weak_lines_visited, 4u); + EXPECT_EQ(result.changed_symbols, 2u); + EXPECT_EQ(result.termination, ProductTermination::no_change); + EXPECT_TRUE(result.all_zero_syndromes); + EXPECT_EQ(damaged, capped); + } + } +} + +TEST(ProductCode, DefaultStrongRadiusRepairsAllColumnsIncludingParity) { + StrongWeakRSProductCode code; + std::vector block(code.BlockSize()); + std::mt19937 random(1983); + for (auto& value : block) value = static_cast(random()); + ASSERT_EQ(code.Encode(block, Backend::scalar), Status::ok); + const auto original = block; + for (size_t col = 0; col < 256; ++col) { + for (size_t error = 0; error < 16; ++error) { + const size_t row = (col + error * 17) % 256; + block[row * 256 + col] ^= static_cast(1 + random() % 255); + } + } + const auto result = code.Correct(block); + EXPECT_EQ(result.directional_passes, 2u); + EXPECT_EQ(result.changed_symbols, 4096u); + EXPECT_TRUE(result.all_zero_syndromes); + EXPECT_EQ(block, original); +} + +TEST(ProductCode, WeakBitGateRejectsTransactionallyWithoutActivation) { + StrongWeakRSProductCode code(4, 2, 8, 6); + for (Element magnitude : {Element{7}, Element{255}}) { + std::vector block(32); + block[6] = magnitude; + block[3 * 8 + 6] = magnitude; + const auto before = block; + const auto result = code.Correct(block); + EXPECT_FALSE(result.all_zero_syndromes); + EXPECT_EQ(result.termination, ProductTermination::no_change); + EXPECT_EQ(result.directional_passes, 2u); + EXPECT_EQ(result.changed_symbols, 0u); + EXPECT_EQ(block, before); + } +} + +TEST(ProductCode, ChangesPropagateThroughFourDirectionalPasses) { + StrongWeakRSProductCode code(4, 2, 8, 6); + std::vector block(32); + block[0] = block[8] = block[9] = block[17] = 1; + // Both columns initially fail, and the middle row fails. Weak fixes the + // outer two rows; strong then fixes the middle row in both active columns. + auto capped = block; + const auto limit = code.Correct(capped, 2); + EXPECT_EQ(limit.termination, ProductTermination::pass_limit); + EXPECT_FALSE(limit.all_zero_syndromes); + EXPECT_EQ(limit.changed_symbols, 2u); + EXPECT_EQ(capped[8], 1); + EXPECT_EQ(capped[9], 1); + const auto result = code.Correct(block); + EXPECT_TRUE(result.all_zero_syndromes); + EXPECT_EQ(result.termination, ProductTermination::no_change); + EXPECT_EQ(result.directional_passes, 4u); + EXPECT_EQ(result.strong_lines_visited, 10u); + EXPECT_EQ(result.weak_lines_visited, 5u); + EXPECT_EQ(result.changed_symbols, 4u); + EXPECT_EQ(block, std::vector(32)); +} + +TEST(ProductCode, SuccessfulStrongRepairProtectsItsColumn) { + StrongWeakRSProductCode code(4, 2, 8, 6); + std::vector block(32); + for (size_t row = 0; row < 4; ++row) block[row * 8] = 1; + const auto protected_word = block; + block[0] ^= 2; + const auto result = code.Correct(block); + EXPECT_FALSE(result.all_zero_syndromes); + EXPECT_EQ(result.changed_symbols, 1u); + EXPECT_EQ(result.directional_passes, 2u); + EXPECT_EQ(block, protected_word); +} + +TEST(ProductCode, UnvisitedColumnRetainsProtectionOnLaterWeakPass) { + StrongWeakRSProductCode code(8, 4, 8, 6); + // A valid strong column with systematic symbols [0,1,0,0]. Obtain its + // parity using the owned scalar encoder, independently of product Encode. + LCHEncoder encoder(4, 4); + std::array protected_column{0, 1, 0, 0}; + std::array data{}; + std::array recovery{}; + for (size_t i = 0; i < 4; ++i) { + data[i] = &protected_column[i]; + recovery[i] = &protected_column[4 + i]; + } + std::vector workspace(encoder.WorkspaceSize(1)); + ASSERT_EQ(encoder.Encode(data, recovery, 1, workspace, Backend::scalar), Status::ok); + std::vector block(64); + for (size_t row = 0; row < 8; ++row) block[row * 8 + 1] = protected_column[row]; + const auto expected = block; + for (size_t row = 0; row < 4; ++row) block[row * 8] = 1; + const auto result = code.Correct(block); + EXPECT_EQ(result.directional_passes, 4u); + EXPECT_EQ(result.strong_lines_visited, 9u); + EXPECT_EQ(result.weak_lines_visited, 9u); + EXPECT_EQ(result.changed_symbols, 4u); + EXPECT_FALSE(result.all_zero_syndromes); + EXPECT_EQ(block, expected); +} + +TEST(WholeCodeword, OverRadiusOutcomesAreTransactionalOrVerifiedCodewords) { + std::mt19937 random(8181); + for (const auto [n, k] : {std::pair{8u, 6u}, {16u, 12u}, {32u, 16u}}) { + LCHDecoder decoder(k, n - k); + LCHEncoder encoder(k, n - k); + for (size_t trial = 0; trial < 100; ++trial) { + auto word = Codeword(n, k, random()); + for (size_t pos = 0; pos <= (n - k) / 2; ++pos) { + word[n - 1 - pos] ^= static_cast(1 + random() % 255); + } + const auto before = word; + const auto result = CorrectCodeword(decoder, word); + if (result.status != CorrectionStatus::ok) { + EXPECT_EQ(word, before); + EXPECT_EQ(result.error_count, 0u); + continue; + } + size_t distance = 0; + for (size_t pos = 0; pos < n; ++pos) distance += word[pos] != before[pos]; + EXPECT_EQ(distance, result.error_count); + EXPECT_LE(distance, (n - k) / 2); + std::vector data(k); + std::vector parity(n - k); + std::vector recovery(n - k); + for (size_t i = 0; i < k; ++i) data[i] = &word[i]; + for (size_t i = 0; i < n - k; ++i) recovery[i] = &parity[i]; + std::vector workspace(encoder.WorkspaceSize(1)); + ASSERT_EQ(encoder.Encode(data, recovery, 1, workspace, Backend::scalar), Status::ok); + EXPECT_TRUE(std::equal(parity.begin(), parity.end(), word.begin() + k)); + } + } +} + +TEST(ProductCode, ProtectedColumnRejectsEvenLowWeightWeakCandidate) { + StrongWeakRSProductCode code(4, 2, 8, 6); + // Constant columns are valid RS words. Every row proposes a one-bit repair + // at column 0, but its zero-syndrome strong protection must reject them. + std::vector block(32); + for (size_t row = 0; row < 4; ++row) block[row * 8] = 1; + const auto before = block; + const auto result = code.Correct(block); + EXPECT_FALSE(result.all_zero_syndromes); + EXPECT_EQ(result.changed_symbols, 0u); + EXPECT_EQ(result.directional_passes, 2u); + EXPECT_EQ(block, before); +} + +TEST(WholeCodewordBatch, DifferentialDataParityFailuresAndTails) { + using detail::error_correction::CorrectCodewordBatch; + std::mt19937 random(0xba7c224); + for (const auto [n, k] : {std::pair{4u, 2u}, {8u, 4u}, {16u, 12u}, + {256u, 128u}, {256u, 224u}, {256u, 254u}}) { + LCHDecoder decoder(k, n - k); + for (size_t lanes : {1u, 31u, 32u, 33u, 65u, 256u}) { + std::vector packed(n * lanes); + auto expected = packed; + std::vector results(lanes), reference(lanes); + std::vector masks(packed.size(), 0xff); + std::vector shards(n); + for (size_t pos = 0; pos < n; ++pos) shards[pos] = &packed[pos * lanes]; + for (size_t lane = 0; lane < lanes; ++lane) { + auto word = Codeword(n, k, random()); + const size_t radius = (n - k) / 2; + const size_t errors = lane % 6 == 0 ? 0 : lane % 6 == 1 ? 1 + : lane % 6 == 2 ? radius : lane % 6 == 3 ? radius + 1 + : lane % 6 == 4 ? n : radius; + std::vector positions(n); + for (size_t i = 0; i < n; ++i) positions[i] = i; + std::shuffle(positions.begin(), positions.end(), random); + // Include parity-only full-radius candidates in every vector chunk. + if (lane % 6 == 5) std::sort(positions.rbegin(), positions.rend()); + for (size_t i = 0; i < errors; ++i) { + word[positions[i]] ^= static_cast(1 + random() % 255); + } + for (size_t pos = 0; pos < n; ++pos) packed[pos * lanes + lane] = word[pos]; + const auto before = word; + reference[lane] = CorrectCodeword(decoder, word); + if (reference[lane].status != CorrectionStatus::ok) EXPECT_EQ(word, before); + for (size_t pos = 0; pos < n; ++pos) expected[pos * lanes + lane] = word[pos]; + } + const auto before = packed; + ASSERT_EQ(CorrectCodewordBatch(decoder, shards, lanes, results, masks), + CorrectionStatus::ok); + ASSERT_EQ(packed, expected) << n << ':' << lanes; + for (size_t lane = 0; lane < lanes; ++lane) { + EXPECT_EQ(results[lane].status, reference[lane].status) << lane; + EXPECT_EQ(results[lane].error_count, reference[lane].error_count) << lane; + for (size_t pos = 0; pos < n; ++pos) { + const auto index = pos * lanes + lane; + EXPECT_EQ(masks[index], before[index] != packed[index]); + } + } + } + } +} + +TEST(WholeCodewordBatch, InvalidRangesAreUntouched) { + using detail::error_correction::CorrectCodewordBatch; + LCHDecoder decoder(6, 2); + std::vector packed(8 * 33, 7); + std::array shards{}; + for (size_t i = 0; i < 8; ++i) shards[i] = packed.data() + i * 33; + std::vector results(33); + std::vector masks(packed.size(), 42); + const auto before = packed; + shards[7] = shards[0]; + EXPECT_EQ(CorrectCodewordBatch(decoder, shards, 33, results, masks), + CorrectionStatus::invalid_argument); + shards[7] = nullptr; + EXPECT_EQ(CorrectCodewordBatch(decoder, shards, 33, results, masks), + CorrectionStatus::invalid_argument); + shards[7] = packed.data() + 7 * 33; + EXPECT_EQ(CorrectCodewordBatch(decoder, shards, 33, results, packed), + CorrectionStatus::invalid_argument); + EXPECT_EQ(CorrectCodewordBatch(decoder, shards, 32, results, masks), + CorrectionStatus::invalid_argument); + EXPECT_EQ(CorrectCodewordBatch(LCHDecoder(5, 3), shards, 33, results, masks), + CorrectionStatus::unsupported_dimensions); + EXPECT_EQ(CorrectCodewordBatch(decoder, shards, 0, {}, {}), CorrectionStatus::ok); + EXPECT_EQ(packed, before); + EXPECT_EQ(masks, std::vector(packed.size(), 42)); + for (const auto& result : results) EXPECT_EQ(result.status, CorrectionStatus::invalid_argument); +} + +// Independent parity oracle: no decoder outcomes or scheduler bookkeeping. +bool AllComponentsValid(const std::vector& block, + size_t ns, size_t ks, size_t nw, size_t kw) { + bool valid = true; + for (bool strong : {true, false}) { + const size_t n = strong ? ns : nw, k = strong ? ks : kw; + LCHEncoder encoder(k, n - k); + std::vector parity(n - k); + std::vector data(k); + std::vector recovery(n - k); + std::vector workspace(encoder.WorkspaceSize(1)); + for (size_t i = 0; i < n - k; ++i) recovery[i] = &parity[i]; + for (size_t line = 0; line < (strong ? nw : ns); ++line) { + const auto index = [&](size_t pos) { + return strong ? pos * nw + line : line * nw + pos; + }; + for (size_t i = 0; i < k; ++i) data[i] = &block[index(i)]; + EXPECT_EQ(encoder.Encode(data, recovery, 1, workspace, Backend::scalar), Status::ok); + for (size_t i = k; i < n; ++i) valid &= parity[i - k] == block[index(i)]; + } + } + return valid; +} + +TEST(ProductCode, InitialBatchChoicesMatchSingleOutputsAndAllCounters) { + std::mt19937 random(0x5b5c0224); + for (const auto dims : {std::array{4, 2, 8, 6}, + {32, 16, 64, 62}, {256, 224, 256, 254}}) { + const auto [ns, ks, nw, kw] = dims; + StrongWeakRSProductCode code(ns, ks, nw, kw); + for (size_t trial = 0; trial < 16; ++trial) { + std::vector input(code.BlockSize()); + for (auto& value : input) value = static_cast(random()); + ASSERT_EQ(code.Encode(input), Status::ok); + if (trial < 8) { + for (auto& value : input) { + for (unsigned bit = 0; bit < 8; ++bit) { + if (random() % 200 == 0) value ^= static_cast(1u << bit); + } + } + } else { + // Valid strong columns propose weak repairs into protected columns; + // overloaded columns exercise rejection, bit gates, and activation. + std::fill(input.begin(), input.end(), Element{0}); + for (size_t row = 0; row < ns; ++row) input[row * nw] = 1; + for (size_t row = 0; row <= (ns - ks) / 2; ++row) { + input[row * nw + 1] = trial % 2 ? 7 : 3; + if (row % 2) input[row * nw + 2] = 1; + } + if (trial % 3 == 0) input[0] ^= 2; + } + for (size_t cap : {2u, 3u, 4u, 5u, 6u, 16u}) { + auto reference = input; + const auto expected = detail::ProductCorrectionAccess::Correct(code, reference, cap, 0, false); + EXPECT_EQ(expected.all_zero_syndromes, AllComponentsValid(reference, ns, ks, nw, kw)); + for (unsigned batches : {0u, 1u, 2u}) { + auto actual = input; + const auto result = detail::ProductCorrectionAccess::Correct(code, actual, cap, batches); + ASSERT_EQ(actual, reference) << ns << ':' << trial << ':' << cap << ':' << batches; + EXPECT_EQ(result.termination, expected.termination); + EXPECT_EQ(result.all_zero_syndromes, expected.all_zero_syndromes); + EXPECT_EQ(result.directional_passes, expected.directional_passes); + EXPECT_EQ(result.strong_lines_visited, expected.strong_lines_visited); + EXPECT_EQ(result.weak_lines_visited, expected.weak_lines_visited); + EXPECT_EQ(result.changed_symbols, expected.changed_symbols); + } + } + } + } +} + +TEST(ProductCode, TrackedValidityMatchesIndependentParityAcrossCapsAndCancellations) { + std::mt19937 random(0xc1ea0224); + for (const auto dims : {std::array{4, 2, 8, 6}, + {8, 4, 4, 2}, {32, 28, 32, 30}}) { + const auto [ns, ks, nw, kw] = dims; + StrongWeakRSProductCode code(ns, ks, nw, kw); + for (size_t trial = 0; trial < 128; ++trial) { + std::vector input(code.BlockSize()); + // Include undetected valid words, equal-magnitude cancellations, + // parity damage, dense failures, and both weak rejection gates. + if (trial % 4 == 0) { + for (auto& value : input) value = static_cast(random()); + ASSERT_EQ(code.Encode(input, Backend::scalar), Status::ok); + } + const size_t errors = trial % (ns * 2); + for (size_t i = 0; i < errors; ++i) { + input[random() % input.size()] ^= trial % 3 == 0 ? Element{1} + : trial % 3 == 1 ? Element{7} : static_cast(1 + random() % 255); + } + for (size_t cap : {2u, 3u, 4u, 5u, 6u, 7u, 8u, 16u}) { + auto reference = input; + const auto expected = detail::ProductCorrectionAccess::Correct(code, reference, cap, 0, false); + const bool valid = AllComponentsValid(reference, ns, ks, nw, kw); + EXPECT_EQ(expected.all_zero_syndromes, valid); + for (unsigned batches : {0u, 1u, 2u, 3u}) { + auto actual = input; + const auto result = batches == 3 ? code.Correct(actual, cap) + : detail::ProductCorrectionAccess::Correct(code, actual, cap, batches); + ASSERT_EQ(actual, reference) << ns << ':' << trial << ':' << cap << ':' << batches; + EXPECT_EQ(result.all_zero_syndromes, valid); + EXPECT_EQ(result.termination, expected.termination); + EXPECT_EQ(result.directional_passes, expected.directional_passes); + EXPECT_EQ(result.strong_lines_visited, expected.strong_lines_visited); + EXPECT_EQ(result.weak_lines_visited, expected.weak_lines_visited); + EXPECT_EQ(result.changed_symbols, expected.changed_symbols); + } + } + } + } +} + +} // namespace