1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! OSD fallback strategy — Step 3 of the per-candidate decode
//! staircase.
//!
//! Runs only when Steps 1–2 (BP staircase) fail and the caller asks
//! for `BpAllOsd` depth at sufficient `sync_quality`. Computes a
//! fresh f32 LLR bundle for the candidate (OSD operates on f32
//! regardless of the embedded fixed-point `LlrT`), walks the four
//! LLR variants (a/b/c/d) through the WSJT-X-faithful OSD entry
//! ([`osd_decode_npre1`] for low-`q` candidates, [`osd_decode_npre1_npre2`]
//! for `q >= Q_NDEEP3_THRESHOLD`), and applies the
//! `nharderrors > 36` cycle gate to weed out high-error CRC-luck
//! codewords.
//!
//! ε.6 of the `docs/CLEANUP_2026_05.md` `decode_block` split. As of
//! issue **#63** this module hosts the WSJT-X-faithful OSD dispatch
//! — the q-conditional split mirrors `osd174_91.f90`'s ndeep=2/3
//! dispatch table, replacing the previous mfsk-core-specific
//! brute-force ndeep=2/3 split (`osd_decode` / `osd_decode_deep`).
use DecodeDepth;
use crateCmplx;
use crateBpResult;
use crate;
/// `sync_quality` threshold for dispatching to the heavier WSJT-X
/// ndeep=3 entry ([`osd_decode_npre1_npre2`]) instead of ndeep=2
/// ([`osd_decode_npre1`]). Mirrors the pre-#63 dispatch's
/// `q >= 18` split between `osd_decode_deep(_, 3, _)` and
/// `osd_decode(_)` (ndeep=2), now with WSJT-X-faithful internals on
/// both sides.
const Q_NDEEP3_THRESHOLD: u32 = 18;
/// OSD-specific `nharderrors` ceiling. mfsk-core-specific deviation
/// from WSJT-X's universal `WSJTX_NHARDERRORS_MAX = 36`
/// (`ft8b.f90:422`): we tighten the gate to 22 *only* for OSD-pass
/// codewords (BP variants still use the looser 36 in
/// `process_one_candidate_inner`).
///
/// **Why this isn't WSJT-X-faithful but worth the deviation.** OSD
/// CRC-luck phantoms surface at the boundary where the encoded
/// candidate disagrees with `hdec` in 25–36 bits and happens to pass
/// CRC-14 (~1 in 16,384 random shot). On `qso3_busy.wav` this surfaces
/// 3 fixed phantoms (`N1API F2VX 73` e=30, `N1API HA6FQ -23` e=25,
/// `CQ EA2BFM IN83` e=31) that all of #86's npre1, #87's npre2, and
/// #88's WSJT-X post-OSD gates failed to filter because their
/// `nsync >= 13` puts them above WSJT-X's `nsync <= 10` bail-out.
///
/// Empirically on the same WAV no legitimate signal surfaces from OSD
/// (`pass >= 14`) with `hard_errors > 22` — real low-SNR signals that
/// BP can't decode but OSD recovers all land at e ≤ 16 here. The
/// gate doesn't touch BP variants 0–3 (which can legitimately produce
/// e=20 cases like `N1PJT HB9CQK -10` on `qso3_busy`), only the
/// OSD-pass codewords this module emits.
///
/// Trade-off: a borderline real signal needing OSD with e=23..36 on
/// some other reference WAV would be dropped here. Reference-suite
/// regression on the WSJT-X-distributed FT8 / FT4 / JT9 / WSPR /
/// FST4 / Q65 samples did not surface any such case in 0.6.3.
const OSD_HARDERRORS_MAX: u32 = 22;
/// Pass-ID range emitted by the OSD fallback (`14..=17` mirrors the
/// llr-variant order `a/b/c/d` — see the post-0.6.1 pass-ID layout
/// documented inline below). Exposed so `process_one_candidate_inner`
/// can size its pass-tracking buffers without re-declaring magic
/// numbers.
pub const PASS_ID_OSD_A: u8 = 14;
/// Try the OSD staircase for a candidate whose BP staircase didn't
/// converge. Returns `Some((bp_result, pass_id))` on the first
/// accepted CRC-pass codeword, `None` otherwise.
///
/// Gates internally on `depth = BpAllOsd` and `q >= 12` so the caller
/// can invoke unconditionally — keeps the per-candidate staircase in
/// `process_one_candidate_inner` straightforward (`accepted = accepted
/// .or_else(|| osd_strategy::try_fallback(...))`).
pub