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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! 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`), and for each of
//! the four LLR variants (a/b/c/d, matching `ft8b.f90`'s `ipass=1..4`
//! `llra/b/c/d`), tries the WSJT-X-faithful OSD entry
//! ([`osd_decode_npre1`] for low-`q` candidates, [`osd_decode_npre1_npre2`]
//! for `q >= Q_NDEEP3_THRESHOLD`) seeded with `bp_llr_zsum(llr, 1)`
//! then `bp_llr_zsum(llr, 2)` — mirroring `decode174_91.f90`'s own
//! `do i=1,nosd` loop over its `zsave(:,i)` snapshots — 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`). As
//! of issue **#182** the OSD *input* is also WSJT-X-faithful: earlier
//! versions fed `osd_decode_npre1`/`_npre2` the raw channel LLR
//! directly, which is not what real WSJT-X does for FT8's blind
//! `ndepth=3` dispatch — that always sets `maxosd=2`
//! (`ft8b.f90:434-441`), and `decode174_91.f90` never calls
//! `osd174_91` on the raw channel LLR when `maxosd>0` (that's a
//! separate `maxosd=0` branch FT8's blind dispatch never takes). It
//! always calls with `zsave(:,i)`, the running BP variable-node soft-
//! estimate sum through the first `i` BP iterations.
//!
//! Host-only (`fft-rustfft`): `DecodeDepth::osd` is a no-op on
//! embedded builds (see its doc comment in `ft8::decode`), so this
//! whole module is compiled out of non-`fft-rustfft` targets entirely
//! rather than merely unreachable at runtime.
//!
//! **FT4/FST4 analog**: this module is FT8's bespoke OSD-fallback
//! dispatch, reached by bypassing [`crate::engine::FecCodec`] entirely
//! (same root cause as issue #198). FT4/FST4 get their OSD escalation
//! through [`crate::engine::pipeline::osd_escalation_gates`] instead —
//! an independent implementation, independently calibrated. Review
//! both when tuning either (issue #192).
use ;
use crateCmplx;
use crateLDPC_N;
use crate;
use crate;
use crateLdpc174_91Params;
/// `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 `nharderrors` ceiling — now [`DecodeStrictness::ft8_nharderrors_max`]
// (issue #221, strictness-wiring follow-up to #220), `Normal` (default) still
// matching WSJT-X's universal `WSJTX_NHARDERRORS_MAX = 36`
// (`ft8b.f90:422`) exactly, same as the BP variants in
// `process_one_candidate_inner`.
//
// History (issue #72 follow-up, 2026-07-18): `Normal`'s 36 was 22
// pre-issue-#72, a deliberate mfsk-core-specific deviation, now
// retracted. The gate had been tightened to 22 specifically to filter 3
// candidates on `qso3_busy.wav` — `N1API F2VX 73` (e=30), `N1API HA6FQ
// -23` (e=25), `CQ EA2BFM IN83` (e=31) — judged phantoms (CRC-14-luck
// false accepts) because JTDX's own 18-entry recall list wasn't
// independently verified for those specific entries (see issue #150). A
// CCIR-fading sensitivity investigation (`docs/notes/FT8_BENCHMARK.md`)
// using `ft8sim`-synthesized WAVs with a *known* golden message found
// the 22 ceiling silently discarding genuine golden decodes under heavy
// fading — traced multiple independent LLR variants converging on the
// exact transmitted text at `hard_errors` in the high 20s/low 30s.
// Loosening back to WSJT-X's 36 recovered those, and as a direct side
// effect also recovered the same 3 `qso3_busy.wav` candidates at their
// *exact* JTDX-claimed text — independent corroboration between two
// separate decoders on a CRC-14-protected message is strong evidence
// against coincidence, resolving them as real rather than phantom. Full
// regression suite (host `ft8_qso3_apoff_recall`,
// `ft8_decode_block_real_qso`, embedded `decode_block` paths) showed
// zero change from this widening — nothing was relying on the tightened
// gate to stay green. That retracted `22` is exactly
// `DecodeStrictness::Strict`'s value now — real prior art, not a fresh
// guess, for callers who explicitly want it back.
/// Pass-ID range for the `zsave(:,1)`-equivalent (BP-refined, 1
/// iteration) OSD attempts, `14..=17` for llr-variants a/b/c/d.
/// Exposed so `process_one_candidate_inner` can size its
/// pass-tracking buffers without re-declaring magic numbers.
///
/// Was the direct-channel-LLR OSD pass-ID range pre-issue-#182; that
/// loop is gone now (see [`try_fallback`]'s doc comment for why), so
/// this range was free to reclaim.
pub const PASS_ID_OSD_ZSAVE1_A: u8 = 14;
/// Pass-ID range for the `zsave(:,2)`-equivalent (BP-refined, 2
/// iterations) OSD attempts — issue #182. `19..=22` for llr-variants
/// a/b/c/d. (18 was the old auto-AP pass ID, removed in the 0.8.0
/// `DecodeDepth` redesign — currently unused, left as a gap rather
/// than renumbered to avoid an unnecessary diff.)
pub const PASS_ID_OSD_ZSAVE2_A: u8 = 19;
/// 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.osd` and `q > 6` 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(...))`).
///
/// **`q > 6`, not `q >= 12`: WSJT-X-faithfulness fix (issue #180
/// follow-up).** The `q >= 12` gate was a deliberate mfsk-core-specific
/// deviation with no counterpart in `ft8b.f90`, which only bails
/// (`if(nsync .le. 6) ... return`) before attempting *any* decode —
/// once a candidate clears that bar, WSJT-X always attempts its full
/// staircase (BP-equivalent and OSD together), regardless of exactly
/// how far above 6 `nsync` is. Root-caused directly: `K1BZM DK8NE -10`
/// (-19 dB, `qso3_busy.wav`) sits at `q=11` on mfsk-core's own SIC
/// residual — verified to be an *exact* per-block match to real jt9's
/// own residual at the same coordinates (`is1=1 is2=7 is3=3`,
/// `nsync=11`, both sides, confirmed via a locally-instrumented jt9
/// rebuild) — yet never reached this function at all under the old
/// `q >= 12` gate. Manually running the OSD dispatch below anyway (llr
/// variant `d`, `osd_decode_npre1_npre2`) decodes it outright at
/// `hard_errors=22`, comfortably under `Normal`'s ceiling (see
/// [`DecodeStrictness::ft8_nharderrors_max`]). The gap was never a
/// sync/LLR/BP/OSD sensitivity problem — mfsk-core's own OSD
/// implementation was already capable of the decode the whole time;
/// this one-line gate was refusing to even try.
pub