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
//! Cold slot-phase acquisition from off-air FT8 (issue #356).
//!
//! A receiver with no clock has its slot grid at an arbitrary phase.
//! FT8's coarse search covers only ±2.5 s, and a single spectrogram
//! cannot be widened past ~±6.28 s (`bounded_sync_lag_steps`); a
//! widened search also drowns in the far-lag ghosts of issue #280.
//!
//! [`acquire_slot_phase`] instead runs the tested ±2.5 s search over
//! three windows of a longer capture, spaced 5 s apart so their union
//! covers the whole 15 s period, folds each window's offset back into
//! the candidate `dt_sec`, and reduces the lot with
//! [`circular_dt_medoid`].
//!
//! The medoid rather than a mean, and over few candidates rather than
//! many, because a real band supplies a loud outlier: on `qso3_busy`
//! fifteen stations cluster at a median DT of +0.260 s while F5RXL
//! sits at -0.770 s, strong enough to capture a score-weighted mean
//! outright (#358).
//! `tests/ft8_cold_acquisition.rs` is the measurement
//! that chose this over the wide search — tiled recovers every offset
//! across the full period to inside one FT8 symbol; wide is off by up
//! to 720 ms with a misleadingly high confidence.
//!
//! This is the acquisition half only. Once it returns a phase, the
//! caller shifts its grid once and hands over to the existing narrow
//! ±1 s tracking (`bootstrap_dt_median` / the decode-DT median).
use Vec;
use crate;
use crate;
use crateNMAX;
/// One FT8 slot, samples at 12 kHz.
const SLOT: usize = NMAX;
/// Window start offsets, in seconds. Three ±2.5 s searches at 0 / 5 /
/// 10 s cover phases `[−2.5, +12.5]` = the whole 15 s period.
const WINDOW_OFFSETS_S: = ;
/// Samples of contiguous audio [`acquire_slot_phase`] needs: the last
/// window is `audio[10 s .. 25 s]`.
pub const REQUIRED_SAMPLES: usize = SLOT + ;
/// The ±2.5 s window each tiled search uses — WSJT-X's own default,
/// where the far-lag ghosts of #280 are not yet a problem.
const TILE_LAG_S: f32 = 2.5;
/// One tiled search over `audio[off_samples ..][.. SLOT]`, with the
/// window's own phase offset folded into every candidate's `dt_sec`.
/// Candidate slot phases from `audio`, heaviest first — the shortlist
/// [`acquire_slot_phase`] used to collapse into one answer (#358).
///
/// The collapse is the broken part. The tiles propose a usable phase
/// every time; what no statistic over the candidates can do is say
/// *which* of the clusters is the grid rather than a loud outlier
/// station or a correlation artefact. So this hands back the clusters
/// and leaves the choice to the caller, whose one reliable test is to
/// try decoding at each — a phantom counts, since its sync is real and
/// its dt is the grid's.
///
/// Measured on `qso3_busy` across the whole period, fixed-point: the
/// first entry is usable 23 times in 40, and one of the first five
/// every time. Empty if `audio` is too short or nothing was found.
///
/// Each entry is `(dt_sec, weight)`, `dt_sec` in `(−7.5, 7.5]`.
/// Two candidate phases this close are the same cluster. Half a second
/// is comfortably inside the coarse window and comfortably outside the
/// spread of one band's stations, which on `qso3_busy` is 1.07 s
/// end to end.
const CLUSTER_KERNEL_S: f32 = 0.5;
/// Recover the slot grid's phase from `audio`, or `None` if nothing
/// coherent was found.
///
/// **Superseded by [`acquire_slot_phases`] for acquisition** — this
/// reduces to a single answer, and #358 measured that reduction
/// returning a phase which decodes nothing on 16 of 40 offsets, at
/// `r` up to 1.00. Kept for callers that want a point estimate and
/// can tolerate that.
///
/// `audio` must hold at least [`REQUIRED_SAMPLES`] (25 s at 12 kHz);
/// only the first [`REQUIRED_SAMPLES`] are read. `top_k` is the number
/// of highest-score candidates the circular estimate averages — 5 is
/// the value `bootstrap_dt_median` established.
///
/// Returns `(dt_sec, r)`:
///
/// - `dt_sec` in `(−7.5, 7.5]` — how far the grid's phase is from the
/// band's, i.e. shift the grid by `−dt_sec` (or, equivalently, its
/// representative mod 7.5 s for an FT4 grid) to line up.
/// - `r` in `[0, 1]` — the mean resultant length: how much the
/// candidates agree. A real lock sits near 1; a quiet band or a
/// phase the search missed sits low. The caller decides the
/// threshold (≈0.6 was clean on `qso3_busy` across the whole
/// period) and how many slots to require before trusting it.
///
/// `None` if `audio` is too short or the search produced no candidates.