[fix](trx-ft8): add OSD-1/OSD-2 to FT2 LDPC decode path

Diagnostic logging showed the FT2 BP/SP decoders consistently reach
1-8 residual parity errors rather than zero — the LLRs are correct
in direction but LDPC belief propagation stalls just short of
convergence.

Add ft2_osd_decode() implementing Ordered Statistics Decoding orders
1 and 2: after the five-pass BP/SP loop fails, sort the 174 codeword
bits by |LLR| ascending and trial-flip single bits (OSD-1, always)
or all pairs of the 50 least-reliable bits (OSD-2, when the remaining
error count is <= 4).  Each trial costs one O(83) parity check;
worst-case overhead is ~1300 checks per candidate, negligible next to
the 5 x 30-iteration BP/SP passes already performed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-15 07:30:17 +01:00
parent 0d2eb7adcd
commit 3b28281684
+95
View File
@@ -913,6 +913,81 @@ static bool ft2_extract_bitmetrics_raw(const float complex* signal, float bitmet
return true;
}
static int ft2_ldpc_check(const uint8_t* codeword)
{
int errors = 0;
for (int m = 0; m < FTX_LDPC_M; ++m)
{
uint8_t x = 0;
for (int i = 0; i < kFTX_LDPC_Num_rows[m]; ++i)
x ^= codeword[kFTX_LDPC_Nm[m][i] - 1];
if (x != 0)
++errors;
}
return errors;
}
// OSD-1 / OSD-2 trial decoder.
// Tries flipping each of the K least-reliable bits (OSD-1), and all pairs when
// nharderror <= 4 (OSD-2). On success cw holds the corrected codeword.
// The caller must verify the message (CRC) separately.
static bool ft2_osd_decode(uint8_t* cw, const float* log174, int nharderror)
{
const int K = 50;
if (nharderror < 1 || nharderror > 8)
return false;
// Find K indices with smallest |LLR| (least reliable), via partial selection.
int sel[50];
bool used[FTX_LDPC_N];
memset(used, 0, sizeof(used));
for (int j = 0; j < K; ++j)
{
int best = -1;
float best_r = INFINITY;
for (int i = 0; i < FTX_LDPC_N; ++i)
{
if (!used[i] && fabsf(log174[i]) < best_r)
{
best_r = fabsf(log174[i]);
best = i;
}
}
if (best < 0)
break;
sel[j] = best;
used[best] = true;
}
// OSD-1: flip each of the K least-reliable bits independently.
for (int j = 0; j < K; ++j)
{
cw[sel[j]] ^= 1;
if (ft2_ldpc_check(cw) == 0)
return true;
cw[sel[j]] ^= 1;
}
// OSD-2: try all pairs from the K least-reliable bits (only for small error counts).
if (nharderror <= 4)
{
for (int j = 0; j < K; ++j)
{
cw[sel[j]] ^= 1;
for (int l = j + 1; l < K; ++l)
{
cw[sel[l]] ^= 1;
if (ft2_ldpc_check(cw) == 0)
return true;
cw[sel[l]] ^= 1;
}
cw[sel[j]] ^= 1;
}
}
return false;
}
static void ft2_pack_bits(const uint8_t bit_array[], int num_bits, uint8_t packed[])
{
int num_bytes = (num_bits + 7) / 8;
@@ -1318,6 +1393,8 @@ static bool ft2_decode_hit(
bool ok = false;
uint8_t cw[FTX_LDPC_N] = { 0 };
int global_best_errors = FTX_LDPC_M;
int global_best_pass = 0;
for (int pass = 0; pass < 5 && !ok; ++pass)
{
float log174[FTX_LDPC_N];
@@ -1364,6 +1441,12 @@ static bool ft2_decode_hit(
}
}
if (nharderror < global_best_errors)
{
global_best_errors = nharderror;
global_best_pass = pass;
}
if (pass_diag)
{
pass_diag->ntype[pass] = ntype;
@@ -1371,6 +1454,18 @@ static bool ft2_decode_hit(
pass_diag->dmin[pass] = dmin;
}
}
// OSD-1 / OSD-2: when BP/SP leave only a few parity errors, try flipping
// the least-reliable bits to bridge the gap.
if (!ok && global_best_errors <= 8)
{
float osd_log174[FTX_LDPC_N];
memcpy(osd_log174, llr_passes[global_best_pass], sizeof(osd_log174));
ft2_normalize_log174(osd_log174);
if (ft2_osd_decode(cw, osd_log174, global_best_errors) && ft2_unpack_message(cw, message))
ok = true;
}
if (!ok && fail_stage)
*fail_stage = FT2_FAIL_LDPC;